serve files cleanup
This commit is contained in:
parent
c9f51869cf
commit
8f545c3f67
2 changed files with 233 additions and 208 deletions
115
get_files.php
115
get_files.php
|
|
@ -3,100 +3,111 @@
|
||||||
* get_files.php - Recursively scans media directories and returns file structure as JSON
|
* 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
|
header('Content-Type: application/json; charset=utf-8');
|
||||||
$type = isset($_GET['type']) ? $_GET['type'] : 'videos';
|
|
||||||
|
|
||||||
// Define base directories
|
// Media type from query parameter (default: videos)
|
||||||
$homeDir = getenv('HOME') ?: (getenv('USERPROFILE') ?: '/home/' . get_current_user());
|
$type = $_GET['type'] ?? 'videos';
|
||||||
$baseDir = $homeDir . '/GDrive';
|
|
||||||
|
// 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 = [
|
$directories = [
|
||||||
'videos' => $baseDir . '/Videos',
|
'videos' => $baseDir . DIRECTORY_SEPARATOR . 'Videos',
|
||||||
'music' => $baseDir . '/Music'
|
'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])) {
|
if (!isset($directories[$type])) {
|
||||||
echo json_encode(['error' => 'Invalid media type']);
|
echo json_encode(['error' => 'Invalid media type']);
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
$targetDir = $directories[$type];
|
$targetDir = $directories[$type];
|
||||||
|
$validExtensions = $extensionsByType[$type];
|
||||||
|
|
||||||
// Check if directory exists
|
// Fail fast if dir missing
|
||||||
if (!is_dir($targetDir)) {
|
if (!is_dir($targetDir)) {
|
||||||
echo json_encode(['error' => 'Directory not found', 'path' => $targetDir]);
|
echo json_encode(['error' => 'Directory not found']);
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Recursively scan directory and build file tree
|
* Recursively scan a directory and build a folder->(folders/files) structure.
|
||||||
* @param string $dir Directory to scan
|
* Files are returned as paths relative to $baseDir.
|
||||||
* @param string $baseDir Base directory for relative paths
|
|
||||||
* @return array File tree structure
|
|
||||||
*/
|
*/
|
||||||
function scanDirectory($dir, $baseDir) {
|
function scanDirectory(string $dir, string $baseDir, array $validExtensions): array
|
||||||
|
{
|
||||||
$result = [];
|
$result = [];
|
||||||
|
|
||||||
// Valid media extensions
|
// scandir() can return false on permissions
|
||||||
$videoExtensions = ['mp4', 'mkv', 'avi', 'mov', 'wmv', 'flv', 'webm', 'm4v'];
|
$items = @scandir($dir);
|
||||||
$audioExtensions = ['mp3', 'wav', 'ogg', 'flac', 'm4a', 'aac', 'wma', 'opus'];
|
if ($items === false) return [];
|
||||||
$validExtensions = array_merge($videoExtensions, $audioExtensions);
|
|
||||||
|
|
||||||
try {
|
|
||||||
$items = scandir($dir);
|
|
||||||
|
|
||||||
foreach ($items as $item) {
|
foreach ($items as $item) {
|
||||||
// Skip hidden files and parent directory references
|
// Skip dot/hidden entries
|
||||||
if ($item === '.' || $item === '..' || $item[0] === '.') {
|
if ($item === '.' || $item === '..' || ($item !== '' && $item[0] === '.')) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
$fullPath = $dir . DIRECTORY_SEPARATOR . $item;
|
$fullPath = $dir . DIRECTORY_SEPARATOR . $item;
|
||||||
|
|
||||||
if (is_dir($fullPath)) {
|
if (is_dir($fullPath)) {
|
||||||
// Recursively scan subdirectory
|
$sub = scanDirectory($fullPath, $baseDir, $validExtensions);
|
||||||
$subItems = scanDirectory($fullPath, $baseDir);
|
if (!empty($sub)) {
|
||||||
if (!empty($subItems)) {
|
$result[$item] = $sub;
|
||||||
$result[$item] = $subItems;
|
|
||||||
}
|
}
|
||||||
} else {
|
continue;
|
||||||
// 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
|
$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: folders first, then files, alphabetically
|
||||||
uksort($result, function ($a, $b) use ($result) {
|
uksort($result, function ($a, $b) use ($result) {
|
||||||
$aIsDir = is_array($result[$a]);
|
$aIsDir = is_array($result[$a]);
|
||||||
$bIsDir = is_array($result[$b]);
|
$bIsDir = is_array($result[$b]);
|
||||||
|
|
||||||
if ($aIsDir && !$bIsDir) return -1;
|
if ($aIsDir && !$bIsDir) return -1;
|
||||||
if (!$aIsDir && $bIsDir) return 1;
|
if (!$aIsDir && $bIsDir) return 1;
|
||||||
return strcasecmp($a, $b);
|
|
||||||
|
// IMPORTANT: keys can be ints if the filename is numeric (e.g. "01", "2026")
|
||||||
|
return strcasecmp((string)$a, (string)$b);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
return $result;
|
return $result;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Scan the directory and return JSON
|
$tree = scanDirectory($targetDir, $baseDir, $validExtensions);
|
||||||
$fileTree = scanDirectory($targetDir, $baseDir);
|
|
||||||
|
|
||||||
if (empty($fileTree)) {
|
echo json_encode(
|
||||||
echo json_encode(['message' => 'No media files found in this directory']);
|
empty($tree) ? ['message' => 'No media files found'] : $tree,
|
||||||
} else {
|
JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE
|
||||||
echo json_encode($fileTree);
|
);
|
||||||
}
|
|
||||||
?>
|
|
||||||
|
|
||||||
|
|
|
||||||
238
serve_media.php
238
serve_media.php
|
|
@ -1,65 +1,56 @@
|
||||||
<?php
|
<?php
|
||||||
/**
|
/**
|
||||||
* serve_media.php - Serves media files with proper headers for streaming
|
* serve_media.php - Serves media files with proper headers for streaming + Range
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// Get the requested file path
|
declare(strict_types=1);
|
||||||
$requestedFile = isset($_GET['file']) ? $_GET['file'] : '';
|
|
||||||
|
|
||||||
if (empty($requestedFile)) {
|
// ---- Config (prefer env vars in production) ----
|
||||||
header('HTTP/1.1 400 Bad Request');
|
$homeDir = getenv('HOME') ?: (getenv('USERPROFILE') ?: ('/home/' . get_current_user()));
|
||||||
|
$baseDir = rtrim($homeDir, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . 'GDrive';
|
||||||
|
|
||||||
|
// ---- Input ----
|
||||||
|
$requested = $_GET['file'] ?? '';
|
||||||
|
if ($requested === '') {
|
||||||
|
http_response_code(400);
|
||||||
echo 'No file specified';
|
echo 'No file specified';
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
// IMPORTANT: Set base directory to where media actually lives
|
// Normalize separators (avoid backslash weirdness)
|
||||||
// Using home directory + GDrive instead of __DIR__
|
$requested = str_replace(['\\'], '/', $requested);
|
||||||
$homeDir = getenv('HOME') ?: (getenv('USERPROFILE') ?: '/home/' . get_current_user());
|
|
||||||
$baseDir = $homeDir . '/GDrive';
|
|
||||||
|
|
||||||
// Construct full path
|
// Compute real paths and enforce base directory containment
|
||||||
$filePath = $baseDir . DIRECTORY_SEPARATOR . $requestedFile;
|
|
||||||
|
|
||||||
// Security: Prevent directory traversal attacks
|
|
||||||
$realBase = realpath($baseDir);
|
$realBase = realpath($baseDir);
|
||||||
$realFile = realpath($filePath);
|
$realFile = realpath($baseDir . DIRECTORY_SEPARATOR . $requested);
|
||||||
|
|
||||||
// Additional debug info (remove in production)
|
if ($realBase === false || $realFile === false) {
|
||||||
if ($realFile === false) {
|
http_response_code(404);
|
||||||
header('HTTP/1.1 404 Not Found');
|
|
||||||
echo 'File not found. Debug info:' . "\n";
|
|
||||||
echo 'Base dir: ' . $baseDir . "\n";
|
|
||||||
echo 'Requested: ' . $requestedFile . "\n";
|
|
||||||
echo 'Full path: ' . $filePath . "\n";
|
|
||||||
exit;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (strpos($realFile, $realBase) !== 0) {
|
|
||||||
header('HTTP/1.1 403 Forbidden');
|
|
||||||
echo 'Access denied - path outside base directory';
|
|
||||||
exit;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if file exists
|
|
||||||
if (!file_exists($filePath) || !is_file($filePath)) {
|
|
||||||
header('HTTP/1.1 404 Not Found');
|
|
||||||
echo 'File not found';
|
echo 'File not found';
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get file information
|
$realBase = rtrim($realBase, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR;
|
||||||
clearstatcache(true, $filePath); // Clear file stat cache
|
if (strpos($realFile, $realBase) !== 0) {
|
||||||
$fileSize = filesize($filePath);
|
http_response_code(403);
|
||||||
$fileExtension = strtolower(pathinfo($filePath, PATHINFO_EXTENSION));
|
echo 'Access denied';
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
// Disable output buffering to prevent Content-Length mismatches
|
if (!is_file($realFile) || !is_readable($realFile)) {
|
||||||
if (ob_get_level()) {
|
http_response_code(404);
|
||||||
|
echo 'File not found';
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Disable output buffering to prevent Content-Length mismatch
|
||||||
|
while (ob_get_level() > 0) {
|
||||||
ob_end_clean();
|
ob_end_clean();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set content type based on extension
|
// Mime types (extend as needed)
|
||||||
$mimeTypes = [
|
$ext = strtolower(pathinfo($realFile, PATHINFO_EXTENSION));
|
||||||
// Video
|
$mime = [
|
||||||
'mp4' => 'video/mp4',
|
'mp4' => 'video/mp4',
|
||||||
'mkv' => 'video/x-matroska',
|
'mkv' => 'video/x-matroska',
|
||||||
'avi' => 'video/x-msvideo',
|
'avi' => 'video/x-msvideo',
|
||||||
|
|
@ -68,7 +59,6 @@ $mimeTypes = [
|
||||||
'flv' => 'video/x-flv',
|
'flv' => 'video/x-flv',
|
||||||
'webm' => 'video/webm',
|
'webm' => 'video/webm',
|
||||||
'm4v' => 'video/x-m4v',
|
'm4v' => 'video/x-m4v',
|
||||||
// Audio
|
|
||||||
'mp3' => 'audio/mpeg',
|
'mp3' => 'audio/mpeg',
|
||||||
'wav' => 'audio/wav',
|
'wav' => 'audio/wav',
|
||||||
'ogg' => 'audio/ogg',
|
'ogg' => 'audio/ogg',
|
||||||
|
|
@ -76,93 +66,117 @@ $mimeTypes = [
|
||||||
'm4a' => 'audio/mp4',
|
'm4a' => 'audio/mp4',
|
||||||
'aac' => 'audio/aac',
|
'aac' => 'audio/aac',
|
||||||
'wma' => 'audio/x-ms-wma',
|
'wma' => 'audio/x-ms-wma',
|
||||||
'opus' => 'audio/opus'
|
'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
|
// Always advertise range support
|
||||||
$range = isset($_SERVER['HTTP_RANGE']) ? $_SERVER['HTTP_RANGE'] : '';
|
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)) {
|
$rangeHeader = $_SERVER['HTTP_RANGE'] ?? '';
|
||||||
// Parse range header
|
if ($rangeHeader === '') {
|
||||||
list($unit, $range) = explode('=', $range, 2);
|
// Full response
|
||||||
|
header('Content-Length: ' . $size);
|
||||||
|
http_response_code(200);
|
||||||
|
|
||||||
if ($unit === 'bytes') {
|
$fp = fopen($realFile, 'rb');
|
||||||
// Parse range values
|
if ($fp === false) {
|
||||||
list($start, $end) = explode('-', $range, 2);
|
http_response_code(500);
|
||||||
$start = intval($start);
|
echo 'Failed to open file';
|
||||||
$end = !empty($end) ? intval($end) : $fileSize - 1;
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
// Validate range
|
$buf = 1024 * 256; // 256KB chunks (fewer syscalls than 8KB)
|
||||||
if ($start > $end || $start < 0 || $end >= $fileSize) {
|
while (!feof($fp) && !connection_aborted()) {
|
||||||
header('HTTP/1.1 416 Requested Range Not Satisfiable');
|
$data = fread($fp, $buf);
|
||||||
header("Content-Range: bytes */$fileSize");
|
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;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
$length = $end - $start + 1;
|
$length = $end - $start + 1;
|
||||||
|
|
||||||
// Set headers for partial content
|
http_response_code(206);
|
||||||
header('HTTP/1.1 206 Partial Content');
|
header("Content-Range: bytes {$start}-{$end}/{$size}");
|
||||||
header("Content-Range: bytes $start-$end/$fileSize");
|
header("Content-Length: {$length}");
|
||||||
header("Content-Length: $length");
|
|
||||||
header("Content-Type: $contentType");
|
|
||||||
header('Accept-Ranges: bytes');
|
|
||||||
|
|
||||||
// Open file and seek to start position
|
$fp = fopen($realFile, 'rb');
|
||||||
$file = fopen($filePath, 'rb');
|
if ($fp === false) {
|
||||||
if ($file === false) {
|
http_response_code(500);
|
||||||
header('HTTP/1.1 500 Internal Server Error');
|
|
||||||
echo 'Failed to open file';
|
echo 'Failed to open file';
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
fseek($file, $start);
|
if (fseek($fp, $start) !== 0) {
|
||||||
|
fclose($fp);
|
||||||
// Output the requested range
|
http_response_code(500);
|
||||||
$buffer = 8192; // 8KB chunks
|
echo 'Failed to seek';
|
||||||
$bytesRemaining = $length;
|
exit;
|
||||||
|
|
||||||
while ($bytesRemaining > 0 && !feof($file)) {
|
|
||||||
$bytesToRead = min($buffer, $bytesRemaining);
|
|
||||||
$data = fread($file, $bytesToRead);
|
|
||||||
if ($data === false) {
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$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;
|
echo $data;
|
||||||
$bytesRemaining -= strlen($data);
|
$remaining -= strlen($data);
|
||||||
if (connection_aborted()) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fclose($file);
|
fclose($fp);
|
||||||
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;
|
exit;
|
||||||
}
|
|
||||||
|
|
||||||
$buffer = 8192; // 8KB chunks
|
|
||||||
while (!feof($file) && !connection_aborted()) {
|
|
||||||
echo fread($file, $buffer);
|
|
||||||
}
|
|
||||||
|
|
||||||
fclose($file);
|
|
||||||
exit; // Important: exit after streaming
|
|
||||||
}
|
|
||||||
?>
|
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue