filemtime($b)<=>filemtime($a)); return $dirs; } function safe_input_dir($base, $name){ $name = basename((string)$name); $path = realpath($base . '/' . $name); $root = realpath($base); if (!$path || !$root || !str_starts_with($path, $root . DIRECTORY_SEPARATOR) || !is_dir($path)) throw new RuntimeException('Invalid input directory.'); return $path; } function read_data($dir){ $data = ['title'=>'','date'=>'','place'=>'','description'=>'','captions'=>[]]; $file = $dir . '/data.txt'; if (!is_file($file)) return $data; foreach (file($file, FILE_IGNORE_NEW_LINES) as $line) { $line = trim($line); if ($line === '' || str_starts_with($line, '#') || !str_contains($line, ':')) continue; [$key, $value] = array_map('trim', explode(':', $line, 2)); $low = strtolower($key); if (in_array($low, ['title','name'], true)) $data['title'] = $value; elseif (in_array($low, ['date','dates','when'], true)) $data['date'] = $value; elseif (in_array($low, ['place','location','where'], true)) $data['place'] = $value; elseif (in_array($low, ['description','desc','synopsis'], true)) $data['description'] = $value; elseif ($key !== '') $data['captions'][$key] = $value; } return $data; } function write_data($dir, $post){ $title = trim($post['title'] ?? ''); $place = trim($post['place'] ?? ''); $date = trim($post['date'] ?? ''); $description = trim($post['description'] ?? ''); if (function_exists('mb_substr')) $description = mb_substr($description, 0, 5000, 'UTF-8'); else $description = substr($description, 0, 5000); $lines = []; if ($title !== '') $lines[] = 'Title: ' . $title; if ($place !== '') $lines[] = 'Location: ' . $place; if ($date !== '') $lines[] = 'Date: ' . $date; if ($description !== '') $lines[] = 'Description: ' . str_replace(["\r", "\n"], ' ', $description); $captionFiles = $post['caption_files'] ?? []; $captions = $post['captions'] ?? []; if (is_array($captionFiles) && is_array($captions)) { foreach ($captionFiles as $i => $fileName) { $fileName = basename((string)$fileName); $caption = trim((string)($captions[$i] ?? '')); if ($fileName !== '' && $caption !== '') $lines[] = $fileName . ': ' . $caption; } } file_put_contents($dir . '/data.txt', implode("\n", $lines) . "\n"); } function list_files($dir){ $files = array_values(array_filter(scandir($dir), fn($f)=>$f!=='.' && $f!=='..' && is_file($dir.'/'.$f) && $f !== 'data.txt')); natcasesort($files); return $files; } function orphan_videos($config){ $referenced = []; foreach (input_dirs($config['uploads_dir']) as $dir) { $stateFile = $dir . '/.movmaker-state.json'; $state = is_file($stateFile) ? json_decode((string)file_get_contents($stateFile), true) : []; if (is_array($state) && !empty($state['output'])) $referenced[basename((string)$state['output'])] = true; } $videos = glob($config['videos_dir'].'/*.{mp4,webm,mov,m4v}', GLOB_BRACE) ?: []; return array_values(array_filter($videos, fn($v)=>empty($referenced[basename($v)]))); } function paginate($items, $page, $perPage = 10){ $total = count($items); $pages = max(1, (int)ceil($total / $perPage)); $page = max(1, min((int)$page, $pages)); return [$page, $pages, array_slice($items, ($page - 1) * $perPage, $perPage), $total]; } function page_links($tab, $page, $pages){ if ($pages <= 1) return ''; $html = '
'; for ($i = 1; $i <= $pages; $i++) { $class = $i === $page ? 'active' : ''; $html .= ''.$i.''; } return $html . '
'; } function save_uploads($field, $dest, $allowed){ if (empty($_FILES[$field])) return []; $files = $_FILES[$field]; $saved = []; $count = is_array($files['name']) ? count($files['name']) : 0; for ($i=0; $i<$count; $i++) { if ($files['error'][$i] === UPLOAD_ERR_NO_FILE) continue; if ($files['error'][$i] !== UPLOAD_ERR_OK) throw new RuntimeException("Upload failed: ".$files['name'][$i]); $ext = strtolower(pathinfo($files['name'][$i], PATHINFO_EXTENSION)); if (!in_array($ext, $allowed, true)) throw new RuntimeException("File type not allowed: ".$files['name'][$i]); $base = preg_replace('/[^A-Za-z0-9._-]+/', '_', basename($files['name'][$i])); $target = $dest . '/' . sprintf('%03d_', count(glob($dest.'/*') ?: [])+count($saved)+1) . $base; if (!move_uploaded_file($files['tmp_name'][$i], $target)) throw new RuntimeException("Cannot save upload"); $saved[] = basename($target); } return $saved; } $msg = $_GET['msg'] ?? null; $err = null; $allowedMedia = ['jpg','jpeg','png','webp','gif','mp4','mov','m4v','avi','mkv','webm']; $allowedAudio = ['mp3','wav','m4a','aac','ogg','flac']; try { if ($_SERVER['REQUEST_METHOD'] === 'POST') { $action = $_POST['action'] ?? 'create'; if ($action === 'create') { $title = trim($_POST['title'] ?? ''); if ($title === '') throw new RuntimeException('Title is required.'); $slug = date('Ymd_His') . '_' . slugify($title); $dir = $config['uploads_dir'] . '/' . $slug; if (!mkdir($dir, 0775, true)) throw new RuntimeException('Cannot create input directory.'); $media = save_uploads('media', $dir, $allowedMedia); if (!$media) { rrmdir($dir); throw new RuntimeException('Upload at least one image or video.'); } save_uploads('audio', $dir, $allowedAudio); write_data($dir, $_POST); $msg = "Created movmaker input directory: in-dir/$slug"; $_GET['edit'] = $slug; } elseif ($action === 'update') { $dir = safe_input_dir($config['uploads_dir'], $_POST['dir'] ?? ''); write_data($dir, $_POST); save_uploads('media', $dir, $allowedMedia); save_uploads('audio', $dir, $allowedAudio); $msg = 'Updated input directory: in-dir/' . basename($dir); $_GET['edit'] = basename($dir); } elseif ($action === 'delete_file') { $dir = safe_input_dir($config['uploads_dir'], $_POST['dir'] ?? ''); $file = basename((string)($_POST['file'] ?? '')); if ($file === '' || $file === 'data.txt' || !is_file($dir.'/'.$file)) throw new RuntimeException('Invalid file.'); unlink($dir.'/'.$file); $msg = 'Removed file: ' . $file; $_GET['edit'] = basename($dir); } elseif ($action === 'delete_video') { $file = basename((string)($_POST['video'] ?? '')); $path = $config['videos_dir'] . '/' . $file; if ($file === '' || !is_file($path)) throw new RuntimeException('Invalid video.'); unlink($path); $cache = __DIR__ . '/cache/videos.json'; if (is_file($cache)) unlink($cache); $msg = 'Deleted video: ' . $file; } elseif ($action === 'delete_dir') { $dir = safe_input_dir($config['uploads_dir'], $_POST['dir'] ?? ''); $name = basename($dir); $stateFile = $dir . '/.movmaker-state.json'; $state = is_file($stateFile) ? json_decode((string)file_get_contents($stateFile), true) : []; $output = is_array($state) ? basename((string)($state['output'] ?? '')) : ''; if (!empty($_POST['delete_output']) && $output !== '') { $outPath = $config['videos_dir'] . '/' . $output; if (is_file($outPath)) unlink($outPath); $cache = __DIR__ . '/cache/videos.json'; if (is_file($cache)) unlink($cache); } rrmdir($dir); header('Location: new.php?msg=' . rawurlencode('Deleted input directory: in-dir/' . $name)); exit; } } } catch (Throwable $e) { $err = $e->getMessage(); } $dirs = input_dirs($config['uploads_dir']); $orphanVideos = orphan_videos($config); $tab = $_GET['tab'] ?? (isset($_GET['edit']) ? 'edit' : 'new'); $page = max(1, (int)($_GET['page'] ?? 1)); [$editPage, $editPages, $dirsPage, $dirsTotal] = paginate($dirs, $tab === 'edit' ? $page : 1); [$videoPage, $videoPages, $orphanVideosPage, $orphanVideosTotal] = paginate($orphanVideos, $tab === 'videos' ? $page : 1); $editName = $_GET['edit'] ?? ''; $editDir = null; $editData = ['title'=>'','date'=>'','place'=>'','description'=>'','captions'=>[]]; $editFiles = []; if ($editName !== '') { try { $editDir = safe_input_dir($config['uploads_dir'], $editName); $editData = read_data($editDir); $editFiles = list_files($editDir); } catch (Throwable $e) { $err = $e->getMessage(); } } ?> Admin - <?=h($config['site_name'])?>

MVLog Admin

Videos without input dir

No orphan videos.


MB

Existing input dirs

No input directories yet.


ยท files
Edit

Edit input dir

in-dir/

Files and captions

No caption for audio files

Add new movie input