From c22e525fb787a9ddeaa7d23c27a1226be2eb08c8 Mon Sep 17 00:00:00 2001 From: Ty Clifford Date: Fri, 5 Jun 2026 10:27:17 -0400 Subject: [PATCH] - Revisit later --- player/import.php | 486 ---------------------------------------------- 1 file changed, 486 deletions(-) delete mode 100644 player/import.php diff --git a/player/import.php b/player/import.php deleted file mode 100644 index 7f21a49..0000000 --- a/player/import.php +++ /dev/null @@ -1,486 +0,0 @@ -#!/usr/bin/env php - [options] - * - * Options: - * --published=0|1 Set published state (default: 1) - * --move Move files instead of copying (default: copy) - * --dry-run Show what would happen without making changes - * --overwrite Re-import files whose slug already exists - * --help Show this help text - * - * Examples: - * php import.php /home/user/videos - * php import.php /tmp/uploads --move --published=0 - * php import.php /srv/media --dry-run - * - * Title is derived from the filename: - * "My Awesome Video (2024).mp4" → "My Awesome Video (2024)" - * - * Multiple files with the same base name but different extensions are grouped - * into a single video entry as alternate sources (e.g. video.mp4 + video.webm). - * - * ffprobe is used automatically when available to detect duration, resolution, - * codec, and MIME type. Falls back to extension-based detection gracefully. - */ - -// ── Bootstrap ──────────────────────────────────────────────────────────────── - -define('SCRIPT_DIR', __DIR__); - -// Must be run from CLI -if (PHP_SAPI !== 'cli') { - fwrite(STDERR, "This script must be run from the command line.\n"); - exit(1); -} - -require_once SCRIPT_DIR . '/includes/db.php'; - -// ── ANSI colour helpers ─────────────────────────────────────────────────────── - -function ansi(string $code, string $text): string { - return "\033[{$code}m{$text}\033[0m"; -} -function c_green(string $s): string { return ansi('32', $s); } -function c_yellow(string $s): string { return ansi('33', $s); } -function c_red(string $s): string { return ansi('31', $s); } -function c_cyan(string $s): string { return ansi('36', $s); } -function c_bold(string $s): string { return ansi('1', $s); } -function c_dim(string $s): string { return ansi('2', $s); } - -function out(string $line): void { echo $line . "\n"; } -function err(string $line): void { fwrite(STDERR, $line . "\n"); } -function info(string $s): void { out(' ' . c_cyan('→') . ' ' . $s); } -function ok(string $s): void { out(' ' . c_green('✔') . ' ' . $s); } -function warn(string $s): void { out(' ' . c_yellow('⚠') . ' ' . $s); } -function fail(string $s): void { out(' ' . c_red('✘') . ' ' . $s); } -function skip(string $s): void { out(' ' . c_dim('–') . ' ' . $s); } -function section(string $s): void { out("\n" . c_bold($s)); } - -// ── Help ───────────────────────────────────────────────────────────────────── - -function show_help(): void { - $script = basename(__FILE__); - echo << [options] - - \033[36mOptions:\033[0m - --published=0|1 Published state for new videos (default: 1) - --move Move files to media/videos/ instead of copying - --dry-run Preview actions without writing anything - --overwrite Re-import slugs that already exist in the database - --help Show this help text - - \033[36mExamples:\033[0m - php {$script} /home/ty/videos - php {$script} /tmp/uploads --move --published=0 - php {$script} /srv/incoming --dry-run - php {$script} /srv/incoming --overwrite - - \033[36mNotes:\033[0m - • Supported formats: mp4, webm, ogv, ogg, mov, mkv, avi - • Files with the same base name (different extensions) are grouped - as multiple sources for one video entry. - • Title is derived from the filename (extension and trailing/leading - whitespace stripped; underscores and hyphens replaced with spaces). - • ffprobe is used when available for duration/quality auto-detection. - -HELP; -} - -// ── Argument parsing ────────────────────────────────────────────────────────── - -$args = array_slice($argv, 1); -$source_dir = null; -$opt_published = 1; -$opt_move = false; -$opt_dry_run = false; -$opt_overwrite = false; - -foreach ($args as $arg) { - if ($arg === '--help' || $arg === '-h') { show_help(); exit(0); } - if ($arg === '--move') { $opt_move = true; continue; } - if ($arg === '--dry-run') { $opt_dry_run = true; continue; } - if ($arg === '--overwrite') { $opt_overwrite = true; continue; } - if (preg_match('/^--published=([01])$/', $arg, $m)) { $opt_published = (int)$m[1]; continue; } - if ($arg[0] !== '-') { $source_dir = rtrim($arg, '/\\'); continue; } - err(c_red("Unknown option: $arg")); - show_help(); - exit(1); -} - -if (!$source_dir) { - err(c_red("Error: source directory is required.")); - show_help(); - exit(1); -} - -if (!is_dir($source_dir)) { - err(c_red("Error: '$source_dir' is not a directory or does not exist.")); - exit(1); -} - -// ── Constants & paths ───────────────────────────────────────────────────────── - -const ALLOWED_EXTS = ['mp4', 'webm', 'ogv', 'ogg', 'mov', 'mkv', 'avi']; - -$dest_dir = MEDIA_DIR . 'videos/'; -if (!$opt_dry_run && !is_dir($dest_dir)) { - mkdir($dest_dir, 0755, true); -} - -// ── ffprobe detection ───────────────────────────────────────────────────────── - -function find_ffprobe(): ?string { - $candidates = ['/usr/bin/ffprobe', '/usr/local/bin/ffprobe']; - foreach ($candidates as $c) { - if (is_executable($c)) return $c; - } - $which = trim(shell_exec('which ffprobe 2>/dev/null') ?? ''); - return ($which && is_executable($which)) ? $which : null; -} - -$FFPROBE = find_ffprobe(); - -function probe_file(string $path): array { - global $FFPROBE; - - $result = [ - 'duration' => 0, - 'width' => 0, - 'height' => 0, - 'quality' => '', - 'fps' => 0.0, - 'video_codec' => '', - 'audio_codec' => '', - 'bitrate_kbps' => 0, - 'format_name' => '', - 'mime_type' => '', - ]; - - if (!$FFPROBE || !is_readable($path)) return $result; - - $cmd = $FFPROBE . ' -v quiet -print_format json -show_streams -show_format ' . escapeshellarg($path) . ' 2>/dev/null'; - $raw = shell_exec($cmd); - $data = $raw ? json_decode($raw, true) : null; - if (!is_array($data)) return $result; - - // Format / container - $fmt = $data['format'] ?? []; - if (!empty($fmt['duration'])) $result['duration'] = (int)round((float)$fmt['duration']); - if (!empty($fmt['bit_rate'])) $result['bitrate_kbps'] = (int)round((int)$fmt['bit_rate'] / 1000); - if (!empty($fmt['format_name'])) { - $fmts = explode(',', $fmt['format_name']); - $result['format_name'] = trim($fmts[0]); - } - - // Streams - foreach (($data['streams'] ?? []) as $stream) { - $type = $stream['codec_type'] ?? ''; - if ($type === 'video' && empty($result['video_codec'])) { - $result['video_codec'] = $stream['codec_name'] ?? ''; - $w = (int)($stream['width'] ?? $stream['coded_width'] ?? 0); - $h = (int)($stream['height'] ?? $stream['coded_height'] ?? 0); - $result['width'] = $w; - $result['height'] = $h; - $result['quality'] = height_to_quality($h); - foreach (['r_frame_rate', 'avg_frame_rate'] as $k) { - if (!empty($stream[$k])) { - $parts = explode('/', $stream[$k]); - if (count($parts) === 2 && (float)$parts[1] > 0) { - $result['fps'] = round((float)$parts[0] / (float)$parts[1], 2); - break; - } - } - } - if (empty($result['duration']) && !empty($stream['duration'])) { - $result['duration'] = (int)round((float)$stream['duration']); - } - } - if ($type === 'audio' && empty($result['audio_codec'])) { - $result['audio_codec'] = $stream['codec_name'] ?? ''; - } - } - - // MIME type (codec-aware) - $result['mime_type'] = smart_mime( - $result['format_name'], - $result['video_codec'], - $path - ); - - return $result; -} - -function height_to_quality(int $h): string { - if ($h <= 0) return ''; - if ($h >= 2160) return '4K'; - if ($h >= 1440) return '1440p'; - if ($h >= 1080) return '1080p'; - if ($h >= 720) return '720p'; - if ($h >= 480) return '480p'; - if ($h >= 360) return '360p'; - if ($h >= 240) return '240p'; - return $h . 'p'; -} - -function smart_mime(string $fmt, string $codec, string $path): string { - $ext = strtolower(pathinfo($path, PATHINFO_EXTENSION)); - if ($fmt === 'webm' || in_array($codec, ['vp8','vp9','av1']) || $ext === 'webm') return 'video/webm'; - if (in_array($fmt, ['ogg','ogv']) || $codec === 'theora' || in_array($ext, ['ogv','ogg'])) return 'video/ogg'; - if ($fmt === 'mov' || $ext === 'mov') return 'video/quicktime'; - if (in_array($fmt, ['matroska','matroska,webm']) || $ext === 'mkv') { - return in_array($codec, ['vp8','vp9','av1']) ? 'video/webm' : 'video/x-matroska'; - } - if ($fmt === 'avi' || $ext === 'avi') return 'video/x-msvideo'; - return 'video/mp4'; -} - -// ── Title derivation ────────────────────────────────────────────────────────── - -/** - * Turn a filename into a human-readable title. - * "my_awesome_video--2024.mp4" → "My Awesome Video 2024" - * "Episode.01.Pilot.webm" → "Episode 01 Pilot" - */ -function filename_to_title(string $filename): string { - // Strip extension - $name = pathinfo($filename, PATHINFO_FILENAME); - // Replace common separators with spaces - $name = preg_replace('/[_\-\.]+/', ' ', $name); - // Collapse multiple spaces - $name = preg_replace('/\s{2,}/', ' ', $name); - // Trim and title-case - $name = trim($name); - // ucwords is good enough for titles; respect existing caps for acronyms - return mb_convert_case($name, MB_CASE_TITLE, 'UTF-8'); -} - -// ── Scan directory ──────────────────────────────────────────────────────────── - -/** - * Scan $source_dir for video files, group files with the same base name - * (different extensions) together as a single video with multiple sources. - * - * Returns: [ 'base_name' => [ 'title'=>..., 'files'=>[...] ], ... ] - */ -function scan_videos(string $dir): array { - $groups = []; - $items = scandir($dir); - if (!$items) return $groups; - - sort($items); // deterministic order - - foreach ($items as $item) { - if ($item === '.' || $item === '..') continue; - $full = $dir . DIRECTORY_SEPARATOR . $item; - if (!is_file($full)) continue; - - $ext = strtolower(pathinfo($item, PATHINFO_EXTENSION)); - if (!in_array($ext, ALLOWED_EXTS)) continue; - - $base = pathinfo($item, PATHINFO_FILENAME); - $title = filename_to_title($item); - - if (!isset($groups[$base])) { - $groups[$base] = ['title' => $title, 'files' => []]; - } - $groups[$base]['files'][] = $full; - } - - return $groups; -} - -// ── Main ────────────────────────────────────────────────────────────────────── - -$db = get_db(); - -// Banner -out(''); -out(c_bold(' ╔══════════════════════════════════════╗')); -out(c_bold(' ║ Media Player — Bulk Video Importer ║')); -out(c_bold(' ╚══════════════════════════════════════╝')); -out(''); -info('Source dir : ' . c_cyan($source_dir)); -info('Dest dir : ' . c_cyan($dest_dir)); -info('Published : ' . ($opt_published ? c_green('yes') : c_yellow('no (draft)'))); -info('Mode : ' . ($opt_move ? c_yellow('move') : 'copy')); -if ($opt_dry_run) info(c_yellow('DRY RUN — no changes will be made')); -if ($opt_overwrite) info(c_yellow('OVERWRITE — existing slugs will be re-imported')); -info('ffprobe : ' . ($FFPROBE ? c_green($FFPROBE) : c_yellow('not found — using extension-based detection'))); - -// Scan -section('Scanning directory…'); -$groups = scan_videos($source_dir); - -if (empty($groups)) { - warn('No supported video files found in ' . $source_dir); - out(' Supported extensions: ' . implode(', ', ALLOWED_EXTS)); - exit(0); -} - -out(' Found ' . c_bold((string)count($groups)) . ' video group(s) across ' . - array_sum(array_map(fn($g) => count($g['files']), $groups)) . ' file(s).'); - -// Process -section('Importing videos…'); - -$count_added = 0; -$count_skipped = 0; -$count_failed = 0; - -foreach ($groups as $base => $group) { - $title = $group['title']; - $files = $group['files']; - - out(''); - out(' ' . c_bold($title) . c_dim(' (' . implode(', ', array_map('basename', $files)) . ')')); - - // Check for existing slug - $slug_candidate = strtolower(trim(preg_replace('/[^a-z0-9]+/i', '-', $title), '-')) ?: 'video-' . time(); - $existing = $db->prepare("SELECT id FROM videos WHERE slug=?"); - $existing->execute([$slug_candidate]); - $existing_row = $existing->fetch(); - - if ($existing_row && !$opt_overwrite) { - skip('Already in database (slug: ' . $slug_candidate . ') — skipping. Use --overwrite to re-import.'); - $count_skipped++; - continue; - } - - // Probe all files and pick best duration/quality - $probed = []; - $best_dur = 0; - - foreach ($files as $fpath) { - $info = probe_file($fpath); - if (!$info['mime_type']) { - $info['mime_type'] = mime_from_ext($fpath); - } - if (!$info['quality']) { - $info['quality'] = '720p'; // fallback label - } - $probed[$fpath] = $info; - - if ($info['duration'] > $best_dur) $best_dur = $info['duration']; - - $summary_parts = array_filter([ - $info['quality'], - $info['width'] && $info['height'] ? $info['width'] . '×' . $info['height'] : '', - $info['video_codec'] ? strtoupper($info['video_codec']) : '', - $info['fps'] > 0 ? $info['fps'] . ' fps' : '', - $info['duration'] ? format_duration($info['duration']) : '', - ]); - info(basename($fpath) . ' ' . c_dim(implode(' · ', $summary_parts))); - } - - if ($opt_dry_run) { - ok(c_yellow('[dry-run] Would add: ') . $title . c_dim(' (' . format_duration($best_dur) . ')')); - $count_added++; - continue; - } - - // ── Insert video record ──────────────────────────────────────────────── - try { - // If overwriting, delete old record first (CASCADE removes sources) - if ($existing_row && $opt_overwrite) { - $db->prepare("DELETE FROM videos WHERE slug=?")->execute([$slug_candidate]); - warn('Deleted existing entry for re-import.'); - } - - $slug = make_slug($title); - - $db->prepare(" - INSERT INTO videos (title, description, slug, thumbnail, duration, sort_order, published) - VALUES (?, '', ?, '', ?, 0, ?) - ")->execute([$title, $slug, $best_dur, $opt_published]); - - $video_id = (int)$db->lastInsertId(); - } catch (PDOException $e) { - fail('Database error inserting video: ' . $e->getMessage()); - $count_failed++; - continue; - } - - // ── Move / copy each source file ─────────────────────────────────────── - $source_ok = 0; - foreach ($files as $fpath) { - $info = $probed[$fpath]; - $ext = strtolower(pathinfo($fpath, PATHINFO_EXTENSION)); - $fname = $slug . '_' . ($source_ok + 1) . '_' . time() . '.' . $ext; - $dest = $dest_dir . $fname; - - // Move or copy - if ($opt_move) { - $transferred = rename($fpath, $dest); - $verb = 'moved'; - } else { - $transferred = copy($fpath, $dest); - $verb = 'copied'; - } - - if (!$transferred) { - fail('Could not ' . ($opt_move ? 'move' : 'copy') . ' ' . basename($fpath) . ' → ' . $dest); - // Roll back this source — video record stays but we note the failure - $count_failed++; - continue; - } - - // Set permissions consistent with web uploads - chmod($dest, 0644); - - // Insert source record - try { - $db->prepare(" - INSERT INTO video_sources (video_id, label, file_path, mime_type, quality, sort_order) - VALUES (?, ?, ?, ?, ?, ?) - ")->execute([ - $video_id, - $info['quality'], // label = quality string - 'videos/' . $fname, - $info['mime_type'], - $info['quality'], - $source_ok, - ]); - } catch (PDOException $e) { - fail('Database error inserting source: ' . $e->getMessage()); - // Clean up the file we just moved/copied - if (file_exists($dest)) unlink($dest); - continue; - } - - ok(c_green($verb) . ' → media/videos/' . $fname); - $source_ok++; - } - - if ($source_ok > 0) { - ok(c_bold('Added: ') . $title . c_dim(' [id=' . $video_id . ', slug=' . $slug . ', sources=' . $source_ok . ']')); - $count_added++; - } else { - // No sources successfully transferred — remove the orphan video record - $db->prepare("DELETE FROM videos WHERE id=?")->execute([$video_id]); - fail('No sources could be transferred — video entry rolled back.'); - $count_failed++; - } -} - -// ── Summary ─────────────────────────────────────────────────────────────────── - -section('Done.'); -out(''); -out(' ' . c_green('✔ Added : ') . c_bold((string)$count_added)); -out(' ' . c_dim('– Skipped: ') . $count_skipped); -if ($count_failed > 0) { - out(' ' . c_red('✘ Failed : ') . c_bold((string)$count_failed)); -} -out(''); - -exit($count_failed > 0 ? 1 : 0);