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 editable_file($f){ $ext = strtolower(pathinfo((string)$f, PATHINFO_EXTENSION)); return $f !== '' && $f[0] !== '.' && $f !== 'data.txt' && in_array($ext, ['jpg','jpeg','png','webp','gif','mp4','mov','m4v','avi','mkv','webm','mp3','wav','m4a','aac','ogg','flac'], true); }
function list_files($dir){ $files = array_values(array_filter(scandir($dir), fn($f)=>editable_file($f) && is_file($dir.'/'.$f))); 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 (!editable_file($file) || !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'])?>
=h($msg)?>
=h($err)?>
Videos without input dir
No orphan videos.
=page_links('videos',$videoPage,$videoPages)?>
Existing input dirs
No input directories yet.
=h($data['title'] ?: basename($d))?>
=h(basename($d))?> ยท =h(count(list_files($d)))?> files
Edit =page_links('edit',$editPage,$editPages)?>