sync.json (einmalig) $legacyFile = __DIR__ . DIRECTORY_SEPARATOR . 'sync.txt'; if (!file_exists(STATE_FILE) && file_exists($legacyFile)) { @rename($legacyFile, STATE_FILE); } if (!is_dir(SONGS_DIR)) { @mkdir(SONGS_DIR, 0775, true); } if (!is_dir(DIASHOW_DIR)) { @mkdir(DIASHOW_DIR, 0775, true); } // ----- Pfad-Helper (Windows/Linux sicher) ----- function norm_path($p) { $r = realpath($p); if ($r === false) return false; $r = str_replace('\\', '/', $r); if (DIRECTORY_SEPARATOR === '\\') $r = strtolower($r); return rtrim($r, '/'); } function is_path_inside($base, $path) { $b = norm_path($base); $p = norm_path($path); if ($b === false || $p === false) return false; return strpos($p, $b) === 0; } // ----- Meta aus String extrahieren ----- function parse_meta_from_content($raw) { $lines = preg_split("/(\r\n|\r|\n)/", $raw); $meta = []; foreach ($lines as $ln) { $r = rtrim($ln, "\r\n"); if (preg_match('/^#(\w+)=(.*)$/', $r, $m)) { $meta[$m[1]] = $m[2]; } elseif (trim($r) !== '') { // Stoppe bei Nicht-Meta-Zeilen break; } } return $meta; } // ----- Alle .sng-Dateien rekursiv auflisten ----- function list_sng_files($root, $q = '') { $q = trim(mb_strtolower($q)); $result = []; $rootReal = norm_path($root); $it = new RecursiveIteratorIterator( new RecursiveDirectoryIterator($root, FilesystemIterator::SKIP_DOTS) ); foreach ($it as $file) { /** @var SplFileInfo $file */ if (!$file->isFile()) continue; if (strtolower($file->getExtension()) !== 'sng') continue; $absReal = norm_path($file->getRealPath()); if ($absReal === false || !is_path_inside($rootReal, $absReal)) continue; // Relativen Pfad bestimmen (mit / als Trennzeichen) $rel = ltrim(substr($absReal, strlen($rootReal)), '/'); $raw = read_file_utf8($absReal); $lower_raw = mb_strtolower($raw); $meta = parse_meta_from_content($raw); $name = $file->getBasename('.sng'); $name_lower = mb_strtolower($name); $rel_lower = strtolower($rel); $csid = mb_strtolower($meta['ChurchSongID'] ?? ''); if ($q) { $has_name = strpos($name_lower, $q) !== false; $has_rel = strpos($rel_lower, $q) !== false; $has_csid = strpos($csid, $q) !== false; $has_content = strpos($lower_raw, $q) !== false; if (!$has_name && !$has_rel && !$has_csid && !$has_content) continue; } $result[] = [ 'name' => $name, 'rel' => $rel, 'mtime' => $file->getMTime(), 'size' => $file->getSize(), 'churchsongid' => $meta['ChurchSongID'] ?? '' // 'content' wird nicht mehr gesendet, da nicht benötigt ]; } // Alphabetisch nach Name usort($result, fn($a, $b) => strcmp(mb_strtolower($a['name']), mb_strtolower($b['name']))); return $result; } // ----- Datei lesen & nach UTF-8 konvertieren ----- function read_file_utf8($path) { $bin = @file_get_contents($path); if ($bin === false) return ''; // UTF-8 BOM entfernen $bin = preg_replace("/^\xEF\xBB\xBF/", '', $bin); // Encoding erkennen/konvertieren $enc = mb_detect_encoding($bin, ['UTF-8', 'UTF-16LE', 'UTF-16BE', 'Windows-1252', 'ISO-8859-1'], true); if ($enc && $enc !== 'UTF-8') { $bin = mb_convert_encoding($bin, 'UTF-8', $enc); } return $bin; } // ----- .sng Parser ----- // - Ignoriert Zeilen, die mit # beginnen (Meta, z. B. #Title, #LangCount, ...) // - Abschnitte wie [Verse], [Chorus] etc. werden erkannt // - Slides = Textblöcke getrennt durch Leerzeilen oder Zeilen nur mit '---' function parse_sng($path) { $raw = read_file_utf8($path); $lines = preg_split("/(\r\n|\r|\n)/", $raw); $meta = []; $sections = []; $current = null; $nonMetaAll = []; // Für Fallback, falls keine [Sections] foreach ($lines as $ln) { $r = rtrim($ln, "\r\n"); // Meta wie #Title=..., #LangCount=... if (preg_match('/^#(\w+)=(.*)$/', $r, $m)) { $meta[$m[1]] = $m[2]; continue; } // Abschnittsüberschrift [Verse], [Chorus], ... if (preg_match('/^\s*\[(.+?)\]\s*$/u', $r, $m)) { if ($current) $sections[] = $current; $current = ['name' => $m[1], 'text' => '']; continue; } $nonMetaAll[] = $r; if ($current) { $current['text'] .= ($current['text'] === '' ? '' : "\n") . $r; } } if ($current) $sections[] = $current; // Fallback: keine Section-Tags vorhanden -> ganzen Nicht-Meta-Text als eine Section if (!$sections) { $body = trim(implode("\n", $nonMetaAll)); $sections = [['name' => $meta['Title'] ?? basename($path, '.sng'), 'text' => $body]]; } // Slides erzeugen foreach ($sections as &$s) { $normalized = str_replace("\r", '', $s['text']); $slides = preg_split('/\n\s*\n|^\s*---+\s*$/m', trim($normalized)); $s['slides'] = array_values(array_filter(array_map('trim', $slides), fn($x) => $x !== '')); } return ['meta' => $meta, 'sections' => $sections]; } // ----- JSON-Ausgabe ----- function json_out($data) { header('Content-Type: application/json; charset=UTF-8'); echo json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); exit; } // ==================== API Router ==================== $action = $_GET['action'] ?? null; // Helper: Lokaler Datei-Leser für Bibel (Bypass für 403 Server Sperre) if ($action === 'get_bible') { $file = __DIR__ . '/GerSch.json'; if (file_exists($file)) { // Caching Headern für Speed $mtime = filemtime($file); $etag = md5($mtime . $file); header("Last-Modified: " . gmdate("D, d M Y H:i:s", $mtime) . " GMT"); header("Etag: $etag"); header("Cache-Control: public, max-age=31536000"); // 1 Jahr Cache // Prüfen ob Browser Cache hat if ( (isset($_SERVER['HTTP_IF_MODIFIED_SINCE']) && strtotime($_SERVER['HTTP_IF_MODIFIED_SINCE']) >= $mtime) || (isset($_SERVER['HTTP_IF_NONE_MATCH']) && trim($_SERVER['HTTP_IF_NONE_MATCH']) === $etag) ) { header("HTTP/1.1 304 Not Modified"); exit; } header('Content-Type: application/json; charset=UTF-8'); readfile($file); exit; } else { http_response_code(404); echo json_encode(['ok' => false, 'error' => 'Datei GerSch.json nicht gefunden']); exit; } } if ($action === 'list') { $q = $_GET['q'] ?? ''; json_out(['ok' => true, 'files' => list_sng_files(SONGS_DIR, $q)]); } if ($action === 'song') { $rel = $_GET['file'] ?? ''; // Pfad bereinigen: .. entfernen, Backslashes zu / normalisieren $rel = ltrim(str_replace(['..', '\\'], ['', '/'], $rel), '/'); // In OS-Pfad umwandeln $abs = SONGS_DIR . DIRECTORY_SEPARATOR . str_replace('/', DIRECTORY_SEPARATOR, $rel); $absReal = realpath($abs); if (!$absReal || !is_path_inside(SONGS_DIR, $absReal) || !is_file($absReal) || strtolower(pathinfo($absReal, PATHINFO_EXTENSION)) !== 'sng') { http_response_code(404); json_out(['ok' => false, 'error' => 'Song not found']); } $parsed = parse_sng($absReal); $info = [ 'title' => $parsed['meta']['Title'] ?? pathinfo($absReal, PATHINFO_FILENAME), 'langcount' => (int) ($parsed['meta']['LangCount'] ?? 1), 'author' => $parsed['meta']['Author'] ?? ($parsed['meta']['Writer'] ?? ''), 'copyright' => $parsed['meta']['Copyright'] ?? '', 'churchsongid' => $parsed['meta']['ChurchSongID'] ?? '' ]; json_out(['ok' => true, 'info' => $info, 'sections' => $parsed['sections']]); } if ($action === 'list_diashows') { $result = []; if (is_dir(DIASHOW_DIR)) { $it = new DirectoryIterator(DIASHOW_DIR); foreach ($it as $fileinfo) { if (!$fileinfo->isDot() && $fileinfo->isDir()) { $result[] = [ 'name' => $fileinfo->getBasename(), 'rel' => $fileinfo->getBasename(), 'mtime' => $fileinfo->getMTime() ]; } } // Alphabetisch sortieren usort($result, fn($a, $b) => strcmp(mb_strtolower($a['name']), mb_strtolower($b['name']))); } json_out(['ok' => true, 'diashows' => $result]); } if ($action === 'get_diashow') { $rel = $_GET['dir'] ?? ''; $rel = ltrim(str_replace(['..', '\\'], ['', '/'], $rel), '/'); $abs = DIASHOW_DIR . DIRECTORY_SEPARATOR . str_replace('/', DIRECTORY_SEPARATOR, $rel); $absReal = realpath($abs); if (!$absReal || !is_path_inside(DIASHOW_DIR, $absReal) || !is_dir($absReal)) { http_response_code(404); json_out(['ok' => false, 'error' => 'Diashow not found']); } $mediaFiles = []; $it = new DirectoryIterator($absReal); foreach ($it as $f) { if (!$f->isDot() && $f->isFile()) { $ext = strtolower($f->getExtension()); if (in_array($ext, ['jpg', 'jpeg', 'png', 'gif', 'mp4', 'webm', 'mov'])) { $mediaFiles[] = [ 'name' => $f->getBasename(), 'ext' => $ext, 'url' => 'files/media/diashow/' . $rel . '/' . rawurlencode($f->getBasename()) ]; } } } usort($mediaFiles, fn($a, $b) => strcmp(mb_strtolower($a['name']), mb_strtolower($b['name']))); json_out(['ok' => true, 'media' => $mediaFiles, 'name' => basename($rel)]); } if ($action === 'set_state') { $raw = file_get_contents('php://input'); $data = json_decode($raw, true); if ($data) { file_put_contents(STATE_FILE, json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)); json_out(['ok' => true]); } else { http_response_code(400); json_out(['ok' => false, 'error' => 'Invalid data']); } } if ($action === 'get_state') { if (file_exists(STATE_FILE)) { $data = json_decode(file_get_contents(STATE_FILE), true); json_out(['ok' => true, 'state' => $data]); } else { json_out(['ok' => true, 'state' => null]); } } ?>