b24f0dc416
Changed: [includes/db.php (line 43)](/Users/tyemeclifford/Documents/GH/mediaplayer/includes/db.php:43) adds source_type and source_url, with an automatic migration for existing SQLite databases. [admin/add.php (line 260)](/Users/tyemeclifford/Documents/GH/mediaplayer/admin/add.php:260) and [admin/edit.php (line 383)](/Users/tyemeclifford/Documents/GH/mediaplayer/admin/edit.php:383) now let each source be an upload or an external URL. [index.php (line 226)](/Users/tyemeclifford/Documents/GH/mediaplayer/index.php:226) and [embed.php (line 193)](/Users/tyemeclifford/Documents/GH/mediaplayer/embed.php:193) resolve playback URLs based on where the source lives. [catalogue.php (line 79)](/Users/tyemeclifford/Documents/GH/mediaplayer/catalogue.php:79) now has public search for published videos only. [README.md (line 63)](/Users/tyemeclifford/Documents/GH/mediaplayer/README.md:63) documents local vs remote sources and public search.
246 lines
8.7 KiB
PHP
246 lines
8.7 KiB
PHP
<?php
|
|
/**
|
|
* db.php — SQLite database bootstrap & helpers
|
|
*/
|
|
|
|
define('DB_PATH', __DIR__ . '/../data/media.db');
|
|
define('MEDIA_DIR', __DIR__ . '/../media/');
|
|
define('MEDIA_URL', 'media/');
|
|
|
|
function get_db(): PDO {
|
|
static $pdo = null;
|
|
if ($pdo) return $pdo;
|
|
|
|
$dir = dirname(DB_PATH);
|
|
if (!is_dir($dir)) mkdir($dir, 0755, true);
|
|
|
|
$pdo = new PDO('sqlite:' . DB_PATH);
|
|
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
|
|
$pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);
|
|
$pdo->exec('PRAGMA journal_mode=WAL');
|
|
$pdo->exec('PRAGMA foreign_keys=ON');
|
|
|
|
// Schema
|
|
$pdo->exec("
|
|
CREATE TABLE IF NOT EXISTS settings (
|
|
key TEXT PRIMARY KEY,
|
|
value TEXT NOT NULL
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS videos (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
title TEXT NOT NULL DEFAULT 'Untitled',
|
|
description TEXT NOT NULL DEFAULT '',
|
|
slug TEXT NOT NULL UNIQUE,
|
|
thumbnail TEXT NOT NULL DEFAULT '',
|
|
duration INTEGER NOT NULL DEFAULT 0,
|
|
sort_order INTEGER NOT NULL DEFAULT 0,
|
|
published INTEGER NOT NULL DEFAULT 1,
|
|
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS video_sources (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
video_id INTEGER NOT NULL REFERENCES videos(id) ON DELETE CASCADE,
|
|
label TEXT NOT NULL DEFAULT '',
|
|
source_type TEXT NOT NULL DEFAULT 'local',
|
|
file_path TEXT NOT NULL DEFAULT '',
|
|
source_url TEXT NOT NULL DEFAULT '',
|
|
mime_type TEXT NOT NULL DEFAULT 'video/mp4',
|
|
quality TEXT NOT NULL DEFAULT '720p',
|
|
sort_order INTEGER NOT NULL DEFAULT 0
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_videos_published ON videos(published);
|
|
CREATE INDEX IF NOT EXISTS idx_videos_sort ON videos(sort_order, id);
|
|
CREATE INDEX IF NOT EXISTS idx_videos_title ON videos(title);
|
|
CREATE INDEX IF NOT EXISTS idx_sources_video ON video_sources(video_id);
|
|
");
|
|
|
|
ensure_video_source_schema($pdo);
|
|
$pdo->exec("CREATE INDEX IF NOT EXISTS idx_sources_type ON video_sources(source_type)");
|
|
|
|
// Default settings
|
|
$defaults = [
|
|
'catalogue_per_page' => '10',
|
|
'admin_password' => password_hash('admin', PASSWORD_DEFAULT),
|
|
'site_title' => 'Ty Clifford',
|
|
'site_sub' => 'tyclifford.com / media',
|
|
];
|
|
$ins = $pdo->prepare("INSERT OR IGNORE INTO settings (key, value) VALUES (?, ?)");
|
|
foreach ($defaults as $k => $v) $ins->execute([$k, $v]);
|
|
|
|
return $pdo;
|
|
}
|
|
|
|
function ensure_video_source_schema(PDO $pdo): void {
|
|
$cols = $pdo->query("PRAGMA table_info(video_sources)")->fetchAll();
|
|
$names = array_column($cols, 'name');
|
|
|
|
if (!in_array('source_type', $names, true)) {
|
|
$pdo->exec("ALTER TABLE video_sources ADD COLUMN source_type TEXT NOT NULL DEFAULT 'local'");
|
|
}
|
|
if (!in_array('source_url', $names, true)) {
|
|
$pdo->exec("ALTER TABLE video_sources ADD COLUMN source_url TEXT NOT NULL DEFAULT ''");
|
|
}
|
|
|
|
$pdo->exec("UPDATE video_sources SET source_type='local' WHERE source_type='' OR source_type IS NULL");
|
|
}
|
|
|
|
function setting(string $key, string $fallback = ''): string {
|
|
$row = get_db()->prepare("SELECT value FROM settings WHERE key=?");
|
|
$row->execute([$key]);
|
|
$r = $row->fetch();
|
|
return $r ? $r['value'] : $fallback;
|
|
}
|
|
|
|
function set_setting(string $key, string $value): void {
|
|
get_db()->prepare("INSERT OR REPLACE INTO settings (key,value) VALUES (?,?)")
|
|
->execute([$key, $value]);
|
|
}
|
|
|
|
function make_slug(string $title, int $id = 0): string {
|
|
$slug = strtolower(trim(preg_replace('/[^a-z0-9]+/i', '-', $title), '-'));
|
|
if ($id) {
|
|
$exists = get_db()->prepare("SELECT id FROM videos WHERE slug=? AND id!=?");
|
|
$exists->execute([$slug, $id]);
|
|
} else {
|
|
$exists = get_db()->prepare("SELECT id FROM videos WHERE slug=?");
|
|
$exists->execute([$slug]);
|
|
}
|
|
if ($exists->fetch()) $slug .= '-' . ($id ?: time());
|
|
return $slug ?: 'video-' . time();
|
|
}
|
|
|
|
function mime_from_ext(string $path): string {
|
|
$url_path = parse_url($path, PHP_URL_PATH);
|
|
if (is_string($url_path) && $url_path !== '') {
|
|
$path = $url_path;
|
|
}
|
|
$ext = strtolower(pathinfo($path, PATHINFO_EXTENSION));
|
|
return match($ext) {
|
|
'mp4' => 'video/mp4',
|
|
'm4v' => 'video/mp4',
|
|
'webm' => 'video/webm',
|
|
'ogv' => 'video/ogg',
|
|
'ogg' => 'video/ogg',
|
|
'mov' => 'video/quicktime',
|
|
'mkv' => 'video/x-matroska',
|
|
'avi' => 'video/x-msvideo',
|
|
'm3u8' => 'application/vnd.apple.mpegurl',
|
|
'mpd' => 'application/dash+xml',
|
|
default => 'video/mp4',
|
|
};
|
|
}
|
|
|
|
function normalize_media_url(string $url): string {
|
|
$url = trim($url);
|
|
if ($url === '') return '';
|
|
if (str_starts_with($url, '//')) $url = 'https:' . $url;
|
|
if (!filter_var($url, FILTER_VALIDATE_URL)) return '';
|
|
$scheme = strtolower((string)parse_url($url, PHP_URL_SCHEME));
|
|
return in_array($scheme, ['http', 'https'], true) ? $url : '';
|
|
}
|
|
|
|
function source_is_remote(array $source): bool {
|
|
return ($source['source_type'] ?? 'local') === 'remote';
|
|
}
|
|
|
|
function source_is_local(array $source): bool {
|
|
return !source_is_remote($source);
|
|
}
|
|
|
|
function source_location(array $source): string {
|
|
return source_is_remote($source) ? ($source['source_url'] ?? '') : ($source['file_path'] ?? '');
|
|
}
|
|
|
|
function source_public_url(array $source, string $media_base = MEDIA_URL): string {
|
|
if (source_is_remote($source)) {
|
|
return $source['source_url'] ?? '';
|
|
}
|
|
|
|
return rtrim($media_base, '/') . '/' . ltrim($source['file_path'] ?? '', '/');
|
|
}
|
|
|
|
function format_duration(int $seconds): string {
|
|
if ($seconds <= 0) return '';
|
|
$h = intdiv($seconds, 3600);
|
|
$m = intdiv($seconds % 3600, 60);
|
|
$s = $seconds % 60;
|
|
if ($h) return sprintf('%d:%02d:%02d', $h, $m, $s);
|
|
return sprintf('%d:%02d', $m, $s);
|
|
}
|
|
|
|
function h(string $s): string {
|
|
return htmlspecialchars($s, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
|
|
}
|
|
|
|
function get_videos_paginated(int $page, int $per_page, bool $published_only = true, string $search = ''): array {
|
|
$db = get_db();
|
|
$where_parts = [];
|
|
$bind = [];
|
|
$search = trim($search);
|
|
if ($published_only) {
|
|
$where_parts[] = 'v.published=1';
|
|
}
|
|
if ($search !== '') {
|
|
$where_parts[] = '(v.title LIKE :q OR v.description LIKE :q OR v.slug LIKE :q)';
|
|
$bind[':q'] = '%' . $search . '%';
|
|
}
|
|
$where = $where_parts ? 'WHERE ' . implode(' AND ', $where_parts) : '';
|
|
$count = $db->prepare("SELECT COUNT(*) FROM videos v $where");
|
|
foreach ($bind as $key => $value) $count->bindValue($key, $value);
|
|
$count->execute();
|
|
$total = $count->fetchColumn();
|
|
$total_pages = (int)ceil($total / $per_page);
|
|
if ($total_pages > 0) {
|
|
$page = min($page, $total_pages);
|
|
}
|
|
$offset = ($page - 1) * $per_page;
|
|
$rows = $db->prepare("
|
|
SELECT v.*, GROUP_CONCAT(CASE WHEN vs.source_type='remote' THEN vs.source_url ELSE vs.file_path END,'||') AS source_paths
|
|
FROM videos v
|
|
LEFT JOIN video_sources vs ON vs.video_id = v.id
|
|
$where
|
|
GROUP BY v.id
|
|
ORDER BY v.sort_order ASC, v.id DESC
|
|
LIMIT :limit OFFSET :offset
|
|
");
|
|
foreach ($bind as $key => $value) $rows->bindValue($key, $value);
|
|
$rows->bindValue(':limit', $per_page, PDO::PARAM_INT);
|
|
$rows->bindValue(':offset', $offset, PDO::PARAM_INT);
|
|
$rows->execute();
|
|
return [
|
|
'videos' => $rows->fetchAll(),
|
|
'total' => (int)$total,
|
|
'page' => $page,
|
|
'per_page' => $per_page,
|
|
'total_pages' => $total_pages,
|
|
];
|
|
}
|
|
|
|
function get_video_by_slug(string $slug): ?array {
|
|
$db = get_db();
|
|
$row = $db->prepare("SELECT * FROM videos WHERE slug=? AND published=1");
|
|
$row->execute([$slug]);
|
|
$v = $row->fetch();
|
|
if (!$v) return null;
|
|
$sources = $db->prepare("SELECT * FROM video_sources WHERE video_id=? ORDER BY sort_order ASC");
|
|
$sources->execute([$v['id']]);
|
|
$v['sources'] = $sources->fetchAll();
|
|
return $v;
|
|
}
|
|
|
|
function get_video_by_id(int $id): ?array {
|
|
$db = get_db();
|
|
$row = $db->prepare("SELECT * FROM videos WHERE id=?");
|
|
$row->execute([$id]);
|
|
$v = $row->fetch();
|
|
if (!$v) return null;
|
|
$sources = $db->prepare("SELECT * FROM video_sources WHERE video_id=? ORDER BY sort_order ASC");
|
|
$sources->execute([$v['id']]);
|
|
$v['sources'] = $sources->fetchAll();
|
|
return $v;
|
|
}
|