From c9f51869cfec80a01a415e130ab2c68ce7612eec Mon Sep 17 00:00:00 2001 From: markmental Date: Mon, 2 Feb 2026 12:14:18 -0500 Subject: [PATCH] Initial commit, plex-like web media server --- get_files.php | 102 +++++++++ index.php | 582 ++++++++++++++++++++++++++++++++++++++++++++++++ serve_media.php | 168 ++++++++++++++ 3 files changed, 852 insertions(+) create mode 100644 get_files.php create mode 100644 index.php create mode 100644 serve_media.php diff --git a/get_files.php b/get_files.php new file mode 100644 index 0000000..cac2372 --- /dev/null +++ b/get_files.php @@ -0,0 +1,102 @@ + $baseDir . '/Videos', + 'music' => $baseDir . '/Music' +]; + +// Validate type +if (!isset($directories[$type])) { + echo json_encode(['error' => 'Invalid media type']); + exit; +} + +$targetDir = $directories[$type]; + +// Check if directory exists +if (!is_dir($targetDir)) { + echo json_encode(['error' => 'Directory not found', 'path' => $targetDir]); + 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 + */ +function scanDirectory($dir, $baseDir) { + $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; + } + } + } + } catch (Exception $e) { + // Handle permission errors gracefully + return []; + } + + // 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); + }); + + return $result; +} + +// Scan the directory and return JSON +$fileTree = scanDirectory($targetDir, $baseDir); + +if (empty($fileTree)) { + echo json_encode(['message' => 'No media files found in this directory']); +} else { + echo json_encode($fileTree); +} +?> + diff --git a/index.php b/index.php new file mode 100644 index 0000000..378d156 --- /dev/null +++ b/index.php @@ -0,0 +1,582 @@ + + + + + + Media Player + + + + +
+
+

Media Player

+
Your Digital Entertainment Hub
+
+ +
+ + +
+
+
+
🎵
+

Select a file to start playing

+
+
+ +
+
+
+ + + + + diff --git a/serve_media.php b/serve_media.php new file mode 100644 index 0000000..e338214 --- /dev/null +++ b/serve_media.php @@ -0,0 +1,168 @@ + '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', + 'flac' => 'audio/flac', + 'm4a' => 'audio/mp4', + 'aac' => 'audio/aac', + 'wma' => 'audio/x-ms-wma', + 'opus' => 'audio/opus' +]; + +$contentType = isset($mimeTypes[$fileExtension]) ? $mimeTypes[$fileExtension] : 'application/octet-stream'; + +// Handle range requests for seeking +$range = isset($_SERVER['HTTP_RANGE']) ? $_SERVER['HTTP_RANGE'] : ''; + +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'); + 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 +} +?> +