From 8f545c3f6739acffe4e68322a0cfbf091a8f2476 Mon Sep 17 00:00:00 2001 From: markmental Date: Mon, 2 Feb 2026 12:26:55 -0500 Subject: [PATCH] serve files cleanup --- get_files.php | 149 ++++++++++++------------ serve_media.php | 292 +++++++++++++++++++++++++----------------------- 2 files changed, 233 insertions(+), 208 deletions(-) diff --git a/get_files.php b/get_files.php index cac2372..b32acbe 100644 --- a/get_files.php +++ b/get_files.php @@ -3,100 +3,111 @@ * get_files.php - Recursively scans media directories and returns file structure as JSON */ -header('Content-Type: application/json'); +declare(strict_types=1); -// Get the media type from query parameter -$type = isset($_GET['type']) ? $_GET['type'] : 'videos'; +header('Content-Type: application/json; charset=utf-8'); -// Define base directories -$homeDir = getenv('HOME') ?: (getenv('USERPROFILE') ?: '/home/' . get_current_user()); -$baseDir = $homeDir . '/GDrive'; +// Media type from query parameter (default: videos) +$type = $_GET['type'] ?? 'videos'; + +// Resolve base directory (make this configurable via env in production) +$homeDir = getenv('HOME') ?: (getenv('USERPROFILE') ?: ('/home/' . get_current_user())); +$baseDir = rtrim($homeDir, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . 'GDrive'; $directories = [ - 'videos' => $baseDir . '/Videos', - 'music' => $baseDir . '/Music' + 'videos' => $baseDir . DIRECTORY_SEPARATOR . 'Videos', + 'music' => $baseDir . DIRECTORY_SEPARATOR . 'Music', +]; + +// Extensions per tab (don’t mix) +$extensionsByType = [ + 'videos' => ['mp4', 'mkv', 'avi', 'mov', 'wmv', 'flv', 'webm', 'm4v'], + 'music' => ['mp3', 'wav', 'ogg', 'flac', 'm4a', 'aac', 'wma', 'opus'], ]; -// Validate type if (!isset($directories[$type])) { echo json_encode(['error' => 'Invalid media type']); exit; } $targetDir = $directories[$type]; +$validExtensions = $extensionsByType[$type]; -// Check if directory exists +// Fail fast if dir missing if (!is_dir($targetDir)) { - echo json_encode(['error' => 'Directory not found', 'path' => $targetDir]); + echo json_encode(['error' => 'Directory not found']); exit; } /** - * Recursively scan directory and build file tree - * @param string $dir Directory to scan - * @param string $baseDir Base directory for relative paths - * @return array File tree structure + * Recursively scan a directory and build a folder->(folders/files) structure. + * Files are returned as paths relative to $baseDir. */ -function scanDirectory($dir, $baseDir) { +function scanDirectory(string $dir, string $baseDir, array $validExtensions): array +{ $result = []; - - // Valid media extensions - $videoExtensions = ['mp4', 'mkv', 'avi', 'mov', 'wmv', 'flv', 'webm', 'm4v']; - $audioExtensions = ['mp3', 'wav', 'ogg', 'flac', 'm4a', 'aac', 'wma', 'opus']; - $validExtensions = array_merge($videoExtensions, $audioExtensions); - - try { - $items = scandir($dir); - - foreach ($items as $item) { - // Skip hidden files and parent directory references - if ($item === '.' || $item === '..' || $item[0] === '.') { - continue; - } - - $fullPath = $dir . DIRECTORY_SEPARATOR . $item; - - if (is_dir($fullPath)) { - // Recursively scan subdirectory - $subItems = scanDirectory($fullPath, $baseDir); - if (!empty($subItems)) { - $result[$item] = $subItems; - } - } else { - // Check if file has valid media extension - $extension = strtolower(pathinfo($item, PATHINFO_EXTENSION)); - if (in_array($extension, $validExtensions)) { - // Store relative path from base directory - $relativePath = str_replace($baseDir . DIRECTORY_SEPARATOR, '', $fullPath); - $result[$item] = $relativePath; - } - } + + // scandir() can return false on permissions + $items = @scandir($dir); + if ($items === false) return []; + + foreach ($items as $item) { + // Skip dot/hidden entries + if ($item === '.' || $item === '..' || ($item !== '' && $item[0] === '.')) { + continue; } - } catch (Exception $e) { - // Handle permission errors gracefully - return []; + + $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; + } + + // Build a relative path from baseDir. Use realpath() to normalize. + $realBase = realpath($baseDir); + $realFile = realpath($fullPath); + if ($realBase === false || $realFile === false) { + continue; + } + + // Enforce: file must live under baseDir + if (strpos($realFile, $realBase . DIRECTORY_SEPARATOR) !== 0) { + continue; + } + + $relativePath = substr($realFile, strlen($realBase) + 1); // +1 skips the slash + $result[$item] = $relativePath; } - - // Sort results: directories first, then files - 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($a, $b); + + // 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; } -// Scan the directory and return JSON -$fileTree = scanDirectory($targetDir, $baseDir); +$tree = scanDirectory($targetDir, $baseDir, $validExtensions); -if (empty($fileTree)) { - echo json_encode(['message' => 'No media files found in this directory']); -} else { - echo json_encode($fileTree); -} -?> +echo json_encode( + empty($tree) ? ['message' => 'No media files found'] : $tree, + JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE +); diff --git a/serve_media.php b/serve_media.php index e338214..b8bac65 100644 --- a/serve_media.php +++ b/serve_media.php @@ -1,168 +1,182 @@ 0) { ob_end_clean(); } -// Set content type based on extension -$mimeTypes = [ - // Video - 'mp4' => 'video/mp4', - 'mkv' => 'video/x-matroska', - 'avi' => 'video/x-msvideo', - 'mov' => 'video/quicktime', - 'wmv' => 'video/x-ms-wmv', - 'flv' => 'video/x-flv', +// Mime types (extend as needed) +$ext = strtolower(pathinfo($realFile, PATHINFO_EXTENSION)); +$mime = [ + 'mp4' => 'video/mp4', + 'mkv' => 'video/x-matroska', + 'avi' => 'video/x-msvideo', + 'mov' => 'video/quicktime', + 'wmv' => 'video/x-ms-wmv', + 'flv' => 'video/x-flv', 'webm' => 'video/webm', - 'm4v' => 'video/x-m4v', - // Audio - 'mp3' => 'audio/mpeg', - 'wav' => 'audio/wav', - 'ogg' => 'audio/ogg', + 'm4v' => 'video/x-m4v', + 'mp3' => 'audio/mpeg', + 'wav' => 'audio/wav', + 'ogg' => 'audio/ogg', 'flac' => 'audio/flac', - 'm4a' => 'audio/mp4', - 'aac' => 'audio/aac', - 'wma' => 'audio/x-ms-wma', - 'opus' => 'audio/opus' -]; + 'm4a' => 'audio/mp4', + 'aac' => 'audio/aac', + 'wma' => 'audio/x-ms-wma', + 'opus' => 'audio/opus', +][$ext] ?? 'application/octet-stream'; -$contentType = isset($mimeTypes[$fileExtension]) ? $mimeTypes[$fileExtension] : 'application/octet-stream'; +$size = filesize($realFile); +if ($size === false) { + http_response_code(500); + echo 'Failed to stat file'; + exit; +} -// Handle range requests for seeking -$range = isset($_SERVER['HTTP_RANGE']) ? $_SERVER['HTTP_RANGE'] : ''; +// Always advertise range support +header('Accept-Ranges: bytes'); +header('Content-Type: ' . $mime); +// Optional caching (fine for local LAN usage) +header('Cache-Control: public, max-age=3600'); -if (!empty($range)) { - // Parse range header - list($unit, $range) = explode('=', $range, 2); - - if ($unit === 'bytes') { - // Parse range values - list($start, $end) = explode('-', $range, 2); - $start = intval($start); - $end = !empty($end) ? intval($end) : $fileSize - 1; - - // Validate range - if ($start > $end || $start < 0 || $end >= $fileSize) { - header('HTTP/1.1 416 Requested Range Not Satisfiable'); - header("Content-Range: bytes */$fileSize"); - exit; - } - - $length = $end - $start + 1; - - // Set headers for partial content - header('HTTP/1.1 206 Partial Content'); - header("Content-Range: bytes $start-$end/$fileSize"); - header("Content-Length: $length"); - header("Content-Type: $contentType"); - header('Accept-Ranges: bytes'); - - // Open file and seek to start position - $file = fopen($filePath, 'rb'); - if ($file === false) { - header('HTTP/1.1 500 Internal Server Error'); - echo 'Failed to open file'; - exit; - } - - fseek($file, $start); - - // Output the requested range - $buffer = 8192; // 8KB chunks - $bytesRemaining = $length; - - while ($bytesRemaining > 0 && !feof($file)) { - $bytesToRead = min($buffer, $bytesRemaining); - $data = fread($file, $bytesToRead); - if ($data === false) { - break; - } - echo $data; - $bytesRemaining -= strlen($data); - if (connection_aborted()) { - break; - } - } - - fclose($file); - exit; // Important: exit after streaming - } -} else { - // Normal full file response - header('HTTP/1.1 200 OK'); - header("Content-Type: $contentType"); - header("Content-Length: $fileSize"); - header('Accept-Ranges: bytes'); - header('Cache-Control: public, max-age=3600'); - - // Stream file in chunks to avoid memory issues - $file = fopen($filePath, 'rb'); - if ($file === false) { - header('HTTP/1.1 500 Internal Server Error'); +$rangeHeader = $_SERVER['HTTP_RANGE'] ?? ''; +if ($rangeHeader === '') { + // Full response + header('Content-Length: ' . $size); + http_response_code(200); + + $fp = fopen($realFile, 'rb'); + if ($fp === false) { + http_response_code(500); echo 'Failed to open file'; exit; } - - $buffer = 8192; // 8KB chunks - while (!feof($file) && !connection_aborted()) { - echo fread($file, $buffer); - } - - fclose($file); - exit; // Important: exit after streaming -} -?> + + $buf = 1024 * 256; // 256KB chunks (fewer syscalls than 8KB) + while (!feof($fp) && !connection_aborted()) { + $data = fread($fp, $buf); + if ($data === false) break; + echo $data; + } + fclose($fp); + exit; +} + +// ---- Range response (single-range only) ---- +// Examples: +// - bytes=0-999 +// - bytes=1000- +// - bytes=-500 (suffix range) +if (!preg_match('/^bytes=(\d*)-(\d*)$/', trim($rangeHeader), $m)) { + http_response_code(416); + header("Content-Range: bytes */{$size}"); + exit; +} + +$startRaw = $m[1]; +$endRaw = $m[2]; + +if ($startRaw === '' && $endRaw === '') { + http_response_code(416); + header("Content-Range: bytes */{$size}"); + exit; +} + +if ($startRaw === '') { + // Suffix range: last N bytes + $suffixLen = (int)$endRaw; + if ($suffixLen <= 0) { + http_response_code(416); + header("Content-Range: bytes */{$size}"); + exit; + } + $start = max(0, $size - $suffixLen); + $end = $size - 1; +} else { + $start = (int)$startRaw; + $end = ($endRaw === '') ? ($size - 1) : (int)$endRaw; +} + +if ($start < 0 || $end < $start || $end >= $size) { + http_response_code(416); + header("Content-Range: bytes */{$size}"); + exit; +} + +$length = $end - $start + 1; + +http_response_code(206); +header("Content-Range: bytes {$start}-{$end}/{$size}"); +header("Content-Length: {$length}"); + +$fp = fopen($realFile, 'rb'); +if ($fp === false) { + http_response_code(500); + echo 'Failed to open file'; + exit; +} + +if (fseek($fp, $start) !== 0) { + fclose($fp); + http_response_code(500); + echo 'Failed to seek'; + exit; +} + +$buf = 1024 * 256; +$remaining = $length; + +while ($remaining > 0 && !feof($fp) && !connection_aborted()) { + $read = min($buf, $remaining); + $data = fread($fp, $read); + if ($data === false || $data === '') break; + echo $data; + $remaining -= strlen($data); +} + +fclose($fp); +exit;