290 lines
7.5 KiB
PHP
290 lines
7.5 KiB
PHP
<?php
|
|
declare(strict_types=1);
|
|
|
|
function fm_extensions_by_type(): array
|
|
{
|
|
return [
|
|
'videos' => ['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);
|
|
}
|