['mp4', 'mkv', 'avi', 'mov', 'wmv', 'flv', 'webm', 'm4v'], 'music' => ['mp3', 'wav', 'ogg', 'flac', 'm4a', 'aac', 'wma', 'opus'], ]; } function fm_is_valid_type(string $type): bool { return array_key_exists($type, fm_extensions_by_type()); } function fm_directories_for_base(string $baseDir): array { return [ 'videos' => $baseDir . DIRECTORY_SEPARATOR . 'Videos', 'music' => $baseDir . DIRECTORY_SEPARATOR . 'Music', ]; } function fm_resolve_media_root(?string &$error = null): ?string { $mediaRoot = getenv('MEDIA_ROOT'); if ($mediaRoot === false || trim($mediaRoot) === '') { $error = 'MEDIA_ROOT is not set'; return null; } $baseDir = realpath(trim($mediaRoot)); if ($baseDir === false || !is_dir($baseDir)) { $error = 'MEDIA_ROOT base directory not found'; return null; } return $baseDir; } function fm_resolve_cache_dir(): string { $configured = getenv('MEDIA_CACHE_DIR'); if ($configured === false || trim($configured) === '') { return __DIR__ . DIRECTORY_SEPARATOR . 'var' . DIRECTORY_SEPARATOR . 'cache' . DIRECTORY_SEPARATOR . 'media'; } $configured = trim($configured); if (strpos($configured, DIRECTORY_SEPARATOR) === 0) { return rtrim($configured, DIRECTORY_SEPARATOR); } return rtrim(__DIR__ . DIRECTORY_SEPARATOR . $configured, DIRECTORY_SEPARATOR); } function fm_ensure_cache_dir(?string &$error = null): ?string { $cacheDir = fm_resolve_cache_dir(); if (is_dir($cacheDir)) { return $cacheDir; } if (!@mkdir($cacheDir, 0775, true) && !is_dir($cacheDir)) { $error = 'Failed to create cache directory'; return null; } return $cacheDir; } function fm_cache_paths(string $type, ?string &$error = null): ?array { if (!fm_is_valid_type($type)) { $error = 'Invalid media type'; return null; } $cacheDir = fm_ensure_cache_dir($error); if ($cacheDir === null) { return null; } return [ 'cache' => $cacheDir . DIRECTORY_SEPARATOR . $type . '.cache.json', 'lock' => $cacheDir . DIRECTORY_SEPARATOR . $type . '.cache.lock', ]; } function fm_scan_directory_recursive(string $dir, string $realBase, array $validExtensions): array { $result = []; $items = @scandir($dir); if ($items === false) { return []; } foreach ($items as $item) { if ($item === '.' || $item === '..' || ($item !== '' && $item[0] === '.')) { continue; } $fullPath = $dir . DIRECTORY_SEPARATOR . $item; if (is_dir($fullPath)) { $sub = fm_scan_directory_recursive($fullPath, $realBase, $validExtensions); if (!empty($sub)) { $result[$item] = $sub; } continue; } $ext = strtolower(pathinfo($item, PATHINFO_EXTENSION)); if (!in_array($ext, $validExtensions, true)) { continue; } $realFile = realpath($fullPath); if ($realFile === false || strpos($realFile, $realBase) !== 0) { continue; } $result[$item] = substr($realFile, strlen($realBase)); } uksort($result, function ($a, $b) use ($result) { $aIsDir = is_array($result[$a]); $bIsDir = is_array($result[$b]); if ($aIsDir && !$bIsDir) { return -1; } if (!$aIsDir && $bIsDir) { return 1; } return strcasecmp((string)$a, (string)$b); }); return $result; } function fm_scan_media_tree(string $type, string $baseDir, ?string &$error = null): ?array { if (!fm_is_valid_type($type)) { $error = 'Invalid media type'; return null; } $directories = fm_directories_for_base($baseDir); $targetDir = $directories[$type]; if (!is_dir($targetDir)) { $error = 'Directory not found'; return null; } $realBase = rtrim($baseDir, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR; $validExtensions = fm_extensions_by_type()[$type]; return fm_scan_directory_recursive($targetDir, $realBase, $validExtensions); } function fm_read_cache_payload(string $type, string $baseDir, ?string &$error = null): ?array { $paths = fm_cache_paths($type, $error); if ($paths === null) { return null; } if (!is_file($paths['cache']) || !is_readable($paths['cache'])) { return null; } $json = @file_get_contents($paths['cache']); if ($json === false) { $error = 'Failed to read cache file'; return null; } $payload = json_decode($json, true); if (!is_array($payload) || !isset($payload['tree']) || !is_array($payload['tree'])) { $error = 'Invalid cache payload'; return null; } if (($payload['media_root'] ?? null) !== $baseDir) { return null; } if (($payload['type'] ?? null) !== $type) { return null; } return $payload; } function fm_write_cache_payload(string $type, array $payload, ?string &$error = null): bool { $paths = fm_cache_paths($type, $error); if ($paths === null) { return false; } $json = json_encode($payload, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); if ($json === false) { $error = 'Failed to encode cache payload'; return false; } try { $tmpSuffix = bin2hex(random_bytes(4)); } catch (Throwable $e) { $tmpSuffix = uniqid('', true); } $tmpPath = $paths['cache'] . '.tmp.' . getmypid() . '.' . $tmpSuffix; if (@file_put_contents($tmpPath, $json) === false) { $error = 'Failed to write temporary cache file'; return false; } if (!@rename($tmpPath, $paths['cache'])) { @unlink($tmpPath); $error = 'Failed to finalize cache file'; return false; } return true; } function fm_with_type_lock(string $type, callable $callback, ?string &$error = null) { $paths = fm_cache_paths($type, $error); if ($paths === null) { return null; } $lockHandle = @fopen($paths['lock'], 'c'); if ($lockHandle === false) { $error = 'Failed to open cache lock file'; return null; } if (!@flock($lockHandle, LOCK_EX)) { @fclose($lockHandle); $error = 'Failed to acquire cache lock'; return null; } try { return $callback(); } finally { @flock($lockHandle, LOCK_UN); @fclose($lockHandle); } } function fm_rebuild_cache(string $type, string $baseDir, bool $strictWrite, ?string &$error = null): ?array { return fm_with_type_lock($type, function () use ($type, $baseDir, $strictWrite, &$error) { $scanError = null; $tree = fm_scan_media_tree($type, $baseDir, $scanError); if (!is_array($tree)) { $error = $scanError ?? 'Failed to scan media directory'; return null; } $payload = [ 'generated_at' => gmdate(DATE_ATOM), 'media_root' => $baseDir, 'type' => $type, 'tree' => $tree, ]; $writeError = null; $written = fm_write_cache_payload($type, $payload, $writeError); if (!$written && $strictWrite) { $error = $writeError ?? 'Failed to write cache payload'; return null; } if (!$written) { $error = $writeError; } return $tree; }, $error); }