Code simplification, move to wizard style UI, JSON file cache (scans every 15 mins)

This commit is contained in:
markmental 2026-03-02 13:45:28 -05:00
commit 92362efd47
11 changed files with 1027 additions and 438 deletions

View file

@ -5,124 +5,96 @@
declare(strict_types=1);
require_once __DIR__ . '/auth.php';
require_once __DIR__ . '/media_cache_lib.php';
require_auth(true);
header('Content-Type: application/json; charset=utf-8');
/**
* Emit JSON response with cache/timing observability headers.
*/
function fm_emit_response(array $payload, string $type, string $cacheStatus, float $startedAt, bool $forceRefresh, ?string $cacheGeneratedAt = null): void
{
$elapsedMs = (int)round((microtime(true) - $startedAt) * 1000);
header('X-Media-Type: ' . $type);
header('X-Media-Cache: ' . $cacheStatus);
header('X-Media-Time-Ms: ' . (string)$elapsedMs);
header('X-Media-Force-Refresh: ' . ($forceRefresh ? '1' : '0'));
if (is_string($cacheGeneratedAt) && $cacheGeneratedAt !== '') {
header('X-Media-Cache-Generated-At: ' . $cacheGeneratedAt);
}
echo json_encode($payload, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
exit;
}
$startedAt = microtime(true);
// Media type from query parameter (default: videos)
$type = $_GET['type'] ?? 'videos';
// Resolve base directory via MEDIA_ROOT
$mediaRoot = getenv('MEDIA_ROOT');
if ($mediaRoot === false || trim($mediaRoot) === '') {
echo json_encode(['error' => 'MEDIA_ROOT is not set']);
exit;
if (!is_string($type)) {
$type = 'unknown';
}
$baseDir = realpath(trim($mediaRoot));
if ($baseDir === false || !is_dir($baseDir)) {
echo json_encode(['error' => 'MEDIA_ROOT base directory not found']);
exit;
$forceRefresh = isset($_GET['refresh']) && (string)$_GET['refresh'] === '1';
if (!is_string($type) || !fm_is_valid_type($type)) {
fm_emit_response(['error' => 'Invalid media type'], $type, 'invalid_type', $startedAt, $forceRefresh);
}
$directories = [
'videos' => $baseDir . DIRECTORY_SEPARATOR . 'Videos',
'music' => $baseDir . DIRECTORY_SEPARATOR . 'Music',
];
// Extensions per tab (dont mix)
$extensionsByType = [
'videos' => ['mp4', 'mkv', 'avi', 'mov', 'wmv', 'flv', 'webm', 'm4v'],
'music' => ['mp3', 'wav', 'ogg', 'flac', 'm4a', 'aac', 'wma', 'opus'],
];
if (!isset($directories[$type])) {
echo json_encode(['error' => 'Invalid media type']);
exit;
$resolveError = null;
$baseDir = fm_resolve_media_root($resolveError);
if ($baseDir === null) {
fm_emit_response(['error' => $resolveError ?? 'MEDIA_ROOT base directory not found'], $type, 'error', $startedAt, $forceRefresh);
}
$targetDir = $directories[$type];
$validExtensions = $extensionsByType[$type];
// Fail fast if dir missing
if (!is_dir($targetDir)) {
echo json_encode(['error' => 'Directory not found']);
exit;
}
/**
* Recursively scan a directory and build a folder->(folders/files) structure.
* Files are returned as paths relative to $baseDir.
*/
function scanDirectory(string $dir, string $baseDir, array $validExtensions): array
{
$result = [];
// Resolve base once (prevents per-file realpath overhead and handles normalization)
$realBase = realpath($baseDir);
if ($realBase === false) {
return [];
if (!$forceRefresh) {
$cacheError = null;
$cached = fm_read_cache_payload($type, $baseDir, $cacheError);
if (is_array($cached)) {
$tree = $cached['tree'];
fm_emit_response(
empty($tree) ? ['message' => 'No media files found'] : $tree,
$type,
'hit',
$startedAt,
$forceRefresh,
isset($cached['generated_at']) && is_string($cached['generated_at']) ? $cached['generated_at'] : null
);
}
$realBase = rtrim($realBase, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR;
}
// scandir() can return false on permissions
$items = @scandir($dir);
if ($items === false) return [];
$rebuildError = null;
$tree = fm_rebuild_cache($type, $baseDir, false, $rebuildError);
foreach ($items as $item) {
// Skip dot/hidden entries
if ($item === '.' || $item === '..' || ($item !== '' && $item[0] === '.')) {
continue;
}
$fullPath = $dir . DIRECTORY_SEPARATOR . $item;
if (is_dir($fullPath)) {
$sub = scanDirectory($fullPath, $baseDir, $validExtensions);
if (!empty($sub)) {
$result[$item] = $sub;
}
continue;
}
$ext = strtolower(pathinfo($item, PATHINFO_EXTENSION));
if (!in_array($ext, $validExtensions, true)) {
continue;
}
// Normalize and enforce: file must live under baseDir
$realFile = realpath($fullPath);
if ($realFile === false) {
continue;
}
if (strpos($realFile, $realBase) !== 0) {
continue;
}
// Return relative path from baseDir
$relativePath = substr($realFile, strlen($realBase));
$result[$item] = $relativePath;
if (!is_array($tree)) {
$scanError = null;
$tree = fm_scan_media_tree($type, $baseDir, $scanError);
if (!is_array($tree)) {
fm_emit_response(
['error' => $scanError ?? $rebuildError ?? 'Failed to scan media directory'],
$type,
'error',
$startedAt,
$forceRefresh
);
}
// Sort: folders first, then files, alphabetically
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;
// IMPORTANT: keys can be ints if the filename is numeric (e.g. "01", "2026")
return strcasecmp((string)$a, (string)$b);
});
return $result;
fm_emit_response(
empty($tree) ? ['message' => 'No media files found'] : $tree,
$type,
'fallback_scan',
$startedAt,
$forceRefresh
);
}
$tree = scanDirectory($targetDir, $baseDir, $validExtensions);
echo json_encode(
fm_emit_response(
empty($tree) ? ['message' => 'No media files found'] : $tree,
JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE
$type,
$forceRefresh ? 'rebuild_forced' : 'rebuild',
$startedAt,
$forceRefresh
);