- Improved the upload/edit flow to auto-grab thumbnails.
What changed: Added shared thumbnail helpers in [includes/db.php](/Users/tyemeclifford/Documents/GH/mediaplayer/includes/db.php):YouTube direct thumbnails Dailymotion direct thumbnails oEmbed thumbnails for Vimeo, Wistia, TikTok, SoundCloud, Spotify, etc. optional first-frame thumbnail generation for local uploads when ffmpeg is installed Added an “Auto-grab thumbnail” checkbox to [admin/add.php](/Users/tyemeclifford/Documents/GH/mediaplayer/admin/add.php) and [admin/edit.php](/Users/tyemeclifford/Documents/GH/mediaplayer/admin/edit.php). Reused the new shared downloader in [admin/youtube_import.php](/Users/tyemeclifford/Documents/GH/mediaplayer/admin/youtube_import.php). Documented the behavior in [README.md](/Users/tyemeclifford/Documents/GH/mediaplayer/README.md).
This commit is contained in:
@@ -78,6 +78,8 @@ Styled to match the existing `gate.php` / `index.php` dark theme exactly.
|
|||||||
|
|
||||||
External URLs must use `http://` or `https://`. Uploaded files and remote URLs can be mixed on the same video. Recognized provider URLs such as YouTube, Vimeo, Dailymotion, Twitch, Facebook, TikTok, Instagram, X/Twitter, SoundCloud, Spotify, Google Drive, Wistia, Streamable, and Loom render through their embed players.
|
External URLs must use `http://` or `https://`. Uploaded files and remote URLs can be mixed on the same video. Recognized provider URLs such as YouTube, Vimeo, Dailymotion, Twitch, Facebook, TikTok, Instagram, X/Twitter, SoundCloud, Spotify, Google Drive, Wistia, Streamable, and Loom render through their embed players.
|
||||||
|
|
||||||
|
If no thumbnail image is uploaded, the add/edit forms can auto-grab one from the first supported source. YouTube and Dailymotion use direct thumbnail URLs; Vimeo, Wistia, TikTok, SoundCloud, and Spotify use oEmbed when available. Local uploaded videos can generate a first-frame thumbnail when `ffmpeg` is installed on the server.
|
||||||
|
|
||||||
### Importing YouTube Channels
|
### Importing YouTube Channels
|
||||||
|
|
||||||
Use **Admin → YouTube Importer** to paste a YouTube channel URL, `@handle`, or `UC...` channel ID. The importer scrapes the channel's `/videos` page, extracts video data from `ytInitialData`, previews the results, and imports selected videos as remote YouTube embed sources. Duplicate YouTube watch URLs are skipped.
|
Use **Admin → YouTube Importer** to paste a YouTube channel URL, `@handle`, or `UC...` channel ID. The importer scrapes the channel's `/videos` page, extracts video data from `ytInitialData`, previews the results, and imports selected videos as remote YouTube embed sources. Duplicate YouTube watch URLs are skipped.
|
||||||
|
|||||||
+13
-1
@@ -13,6 +13,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|||||||
$duration = (int)($_POST['duration'] ?? 0);
|
$duration = (int)($_POST['duration'] ?? 0);
|
||||||
$sort_order = (int)($_POST['sort_order'] ?? 0);
|
$sort_order = (int)($_POST['sort_order'] ?? 0);
|
||||||
$published = isset($_POST['published']) ? 1 : 0;
|
$published = isset($_POST['published']) ? 1 : 0;
|
||||||
|
$auto_thumb = isset($_POST['auto_thumbnail']);
|
||||||
|
|
||||||
if (!$title) {
|
if (!$title) {
|
||||||
$error = 'Title is required.';
|
$error = 'Title is required.';
|
||||||
@@ -112,6 +113,13 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|||||||
if ($thumbnail && file_exists(MEDIA_DIR . $thumbnail)) unlink(MEDIA_DIR . $thumbnail);
|
if ($thumbnail && file_exists(MEDIA_DIR . $thumbnail)) unlink(MEDIA_DIR . $thumbnail);
|
||||||
$db->prepare("DELETE FROM videos WHERE id=?")->execute([$video_id]);
|
$db->prepare("DELETE FROM videos WHERE id=?")->execute([$video_id]);
|
||||||
} else {
|
} else {
|
||||||
|
if ($thumbnail === '' && $auto_thumb) {
|
||||||
|
$thumbnail = media_auto_thumbnail_for_video($video_id, $slug);
|
||||||
|
if ($thumbnail !== '') {
|
||||||
|
$db->prepare("UPDATE videos SET thumbnail=? WHERE id=?")->execute([$thumbnail, $video_id]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
header('Location: edit.php?id=' . $video_id . '&saved=1');
|
header('Location: edit.php?id=' . $video_id . '&saved=1');
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
@@ -247,7 +255,11 @@ render_head('Add Video — Admin', '
|
|||||||
<label class="form-label" for="thumbnail">Thumbnail Image</label>
|
<label class="form-label" for="thumbnail">Thumbnail Image</label>
|
||||||
<input class="form-input" type="file" id="thumbnail" name="thumbnail"
|
<input class="form-input" type="file" id="thumbnail" name="thumbnail"
|
||||||
accept="image/jpeg,image/png,image/webp,image/gif" style="padding:.45rem .6rem;">
|
accept="image/jpeg,image/png,image/webp,image/gif" style="padding:.45rem .6rem;">
|
||||||
<span class="hint">JPG, PNG, WebP or GIF. Shown as 16:9 preview.</span>
|
<label style="display:flex;align-items:center;gap:.55rem;margin-top:.55rem;cursor:pointer;">
|
||||||
|
<input type="checkbox" name="auto_thumbnail" value="1" <?= isset($_POST['auto_thumbnail']) || $_SERVER['REQUEST_METHOD'] !== 'POST' ? 'checked' : '' ?>>
|
||||||
|
<span style="font-size:.78rem;color:var(--text-2);">Auto-grab thumbnail from source if no image is uploaded</span>
|
||||||
|
</label>
|
||||||
|
<span class="hint">JPG, PNG, WebP or GIF. Auto-grab supports YouTube, Vimeo, Dailymotion and other oEmbed providers; local uploads use ffmpeg when available.</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group" style="justify-content:flex-end;padding-bottom:.2rem;">
|
<div class="form-group" style="justify-content:flex-end;padding-bottom:.2rem;">
|
||||||
<label style="display:flex;align-items:center;gap:.6rem;cursor:pointer;">
|
<label style="display:flex;align-items:center;gap:.6rem;cursor:pointer;">
|
||||||
|
|||||||
+17
-1
@@ -43,6 +43,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|||||||
$duration = (int)($_POST['duration'] ?? 0);
|
$duration = (int)($_POST['duration'] ?? 0);
|
||||||
$sort_order = (int)($_POST['sort_order'] ?? 0);
|
$sort_order = (int)($_POST['sort_order'] ?? 0);
|
||||||
$published = isset($_POST['published']) ? 1 : 0;
|
$published = isset($_POST['published']) ? 1 : 0;
|
||||||
|
$auto_thumb = isset($_POST['auto_thumbnail']);
|
||||||
|
|
||||||
if (!$title) {
|
if (!$title) {
|
||||||
$error = 'Title is required.';
|
$error = 'Title is required.';
|
||||||
@@ -51,6 +52,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|||||||
|
|
||||||
// Handle thumbnail replacement
|
// Handle thumbnail replacement
|
||||||
$thumbnail = $video['thumbnail'];
|
$thumbnail = $video['thumbnail'];
|
||||||
|
$uploaded_thumbnail = false;
|
||||||
if (!empty($_FILES['thumbnail']['tmp_name'])) {
|
if (!empty($_FILES['thumbnail']['tmp_name'])) {
|
||||||
$ext = strtolower(pathinfo($_FILES['thumbnail']['name'], PATHINFO_EXTENSION));
|
$ext = strtolower(pathinfo($_FILES['thumbnail']['name'], PATHINFO_EXTENSION));
|
||||||
$allowed = ['jpg','jpeg','png','webp','gif'];
|
$allowed = ['jpg','jpeg','png','webp','gif'];
|
||||||
@@ -62,6 +64,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|||||||
$fname = $slug . '_thumb_' . time() . '.' . $ext;
|
$fname = $slug . '_thumb_' . time() . '.' . $ext;
|
||||||
if (move_uploaded_file($_FILES['thumbnail']['tmp_name'], $thumb_dir . $fname)) {
|
if (move_uploaded_file($_FILES['thumbnail']['tmp_name'], $thumb_dir . $fname)) {
|
||||||
$thumbnail = 'thumbs/' . $fname;
|
$thumbnail = 'thumbs/' . $fname;
|
||||||
|
$uploaded_thumbnail = true;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
$error = 'Thumbnail must be JPG, PNG, WebP, or GIF.';
|
$error = 'Thumbnail must be JPG, PNG, WebP, or GIF.';
|
||||||
@@ -178,6 +181,15 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|||||||
if (file_exists($moved)) unlink($moved);
|
if (file_exists($moved)) unlink($moved);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
if ($auto_thumb && !$uploaded_thumbnail) {
|
||||||
|
$auto_thumbnail = media_auto_thumbnail_for_video($id, $slug);
|
||||||
|
if ($auto_thumbnail !== '') {
|
||||||
|
if ($thumbnail && file_exists(MEDIA_DIR . $thumbnail)) unlink(MEDIA_DIR . $thumbnail);
|
||||||
|
$thumbnail = $auto_thumbnail;
|
||||||
|
$db->prepare("UPDATE videos SET thumbnail=?, updated_at=datetime('now') WHERE id=?")->execute([$thumbnail, $id]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
header('Location: edit.php?id=' . $id . '&saved=1');
|
header('Location: edit.php?id=' . $id . '&saved=1');
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
@@ -329,7 +341,11 @@ render_head('Edit Video — Admin', '
|
|||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
<input class="form-input" type="file" id="thumbnail" name="thumbnail"
|
<input class="form-input" type="file" id="thumbnail" name="thumbnail"
|
||||||
accept="image/jpeg,image/png,image/webp,image/gif" style="padding:.45rem .6rem;">
|
accept="image/jpeg,image/png,image/webp,image/gif" style="padding:.45rem .6rem;">
|
||||||
<span class="hint">Upload new image to replace current thumbnail.</span>
|
<label style="display:flex;align-items:center;gap:.55rem;margin-top:.55rem;cursor:pointer;">
|
||||||
|
<input type="checkbox" name="auto_thumbnail" value="1" <?= isset($_POST['auto_thumbnail']) || ($_SERVER['REQUEST_METHOD'] !== 'POST' && !$video['thumbnail']) ? 'checked' : '' ?>>
|
||||||
|
<span style="font-size:.78rem;color:var(--text-2);">Auto-grab/refresh thumbnail from source on save</span>
|
||||||
|
</label>
|
||||||
|
<span class="hint">Upload a new image to override auto-grab. Auto-grab supports YouTube, Vimeo, Dailymotion and other oEmbed providers; local uploads use ffmpeg when available.</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -464,20 +464,7 @@ function yt_scrape_channel_videos(string $input, int $limit): array {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function yt_download_thumbnail(string $url, string $slug): string {
|
function yt_download_thumbnail(string $url, string $slug): string {
|
||||||
if ($url === '') return '';
|
return media_download_thumbnail($url, $slug, 'yt');
|
||||||
$fetch = yt_http_get($url, 12);
|
|
||||||
if ($fetch['body'] === '' || $fetch['error']) return '';
|
|
||||||
if (function_exists('getimagesizefromstring') && !@getimagesizefromstring($fetch['body'])) return '';
|
|
||||||
|
|
||||||
$content_type = strtolower((string)$fetch['content_type']);
|
|
||||||
$ext = 'jpg';
|
|
||||||
if (str_contains($content_type, 'png')) $ext = 'png';
|
|
||||||
elseif (str_contains($content_type, 'webp')) $ext = 'webp';
|
|
||||||
|
|
||||||
$thumb_dir = MEDIA_DIR . 'thumbs/';
|
|
||||||
if (!is_dir($thumb_dir)) mkdir($thumb_dir, 0755, true);
|
|
||||||
$fname = $slug . '_yt_' . time() . '.' . $ext;
|
|
||||||
return file_put_contents($thumb_dir . $fname, $fetch['body']) !== false ? 'thumbs/' . $fname : '';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function yt_import_videos(PDO $db, array $videos, array $selected_ids, bool $published): array {
|
function yt_import_videos(PDO $db, array $videos, array $selected_ids, bool $published): array {
|
||||||
|
|||||||
+229
-6
@@ -306,6 +306,70 @@ function media_append_query(string $base, array $params): string {
|
|||||||
return $base . (strpos($base, '?') === false ? '?' : '&') . $query;
|
return $base . (strpos($base, '?') === false ? '?' : '&') . $query;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function media_http_get(string $url, int $timeout = 12): array {
|
||||||
|
$url = normalize_media_url($url);
|
||||||
|
if ($url === '') return ['body' => '', 'code' => 0, 'content_type' => '', 'error' => 'Invalid URL.'];
|
||||||
|
|
||||||
|
$headers = [
|
||||||
|
'User-Agent: Mozilla/5.0 (compatible; TyCliffordMedia/1.0)',
|
||||||
|
'Accept: image/avif,image/webp,image/png,image/jpeg,image/*;q=0.9,application/json;q=0.8,text/html;q=0.7,*/*;q=0.5',
|
||||||
|
];
|
||||||
|
|
||||||
|
if (function_exists('curl_init')) {
|
||||||
|
$ch = curl_init($url);
|
||||||
|
curl_setopt_array($ch, [
|
||||||
|
CURLOPT_RETURNTRANSFER => true,
|
||||||
|
CURLOPT_FOLLOWLOCATION => true,
|
||||||
|
CURLOPT_MAXREDIRS => 5,
|
||||||
|
CURLOPT_CONNECTTIMEOUT => $timeout,
|
||||||
|
CURLOPT_TIMEOUT => $timeout,
|
||||||
|
CURLOPT_HTTPHEADER => $headers,
|
||||||
|
CURLOPT_HEADER => true,
|
||||||
|
]);
|
||||||
|
$raw = curl_exec($ch);
|
||||||
|
$err = curl_error($ch);
|
||||||
|
$code = (int)curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
|
||||||
|
$header_size = (int)curl_getinfo($ch, CURLINFO_HEADER_SIZE);
|
||||||
|
$content_type = (string)curl_getinfo($ch, CURLINFO_CONTENT_TYPE);
|
||||||
|
curl_close($ch);
|
||||||
|
|
||||||
|
if (!is_string($raw) || $raw === '') {
|
||||||
|
return ['body' => '', 'code' => $code, 'content_type' => $content_type, 'error' => $err ?: 'Empty response.'];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'body' => substr($raw, $header_size),
|
||||||
|
'code' => $code,
|
||||||
|
'content_type' => $content_type,
|
||||||
|
'error' => $code >= 400 ? 'HTTP ' . $code : '',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$context = stream_context_create([
|
||||||
|
'http' => [
|
||||||
|
'method' => 'GET',
|
||||||
|
'header' => implode("\r\n", $headers),
|
||||||
|
'timeout' => $timeout,
|
||||||
|
'ignore_errors' => true,
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
$body = @file_get_contents($url, false, $context);
|
||||||
|
$response_headers = $http_response_header ?? [];
|
||||||
|
$code = 0;
|
||||||
|
$content_type = '';
|
||||||
|
foreach ($response_headers as $header) {
|
||||||
|
if (preg_match('#^HTTP/\S+\s+(\d+)#', $header, $m)) $code = (int)$m[1];
|
||||||
|
if (stripos($header, 'Content-Type:') === 0) $content_type = trim(substr($header, 13));
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'body' => is_string($body) ? $body : '',
|
||||||
|
'code' => $code,
|
||||||
|
'content_type' => $content_type,
|
||||||
|
'error' => $code >= 400 ? 'HTTP ' . $code : (!is_string($body) ? 'Unable to fetch URL.' : ''),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
function media_parse_youtube_time(string $value): int {
|
function media_parse_youtube_time(string $value): int {
|
||||||
$value = trim($value);
|
$value = trim($value);
|
||||||
if ($value === '') return 0;
|
if ($value === '') return 0;
|
||||||
@@ -334,6 +398,170 @@ function media_twitch_parent_host(): string {
|
|||||||
return is_string($host) && $host !== '' ? $host : 'localhost';
|
return is_string($host) && $host !== '' ? $host : 'localhost';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function media_youtube_id_from_url(string $url): string {
|
||||||
|
$host = strtolower((string)(parse_url($url, PHP_URL_HOST) ?? ''));
|
||||||
|
$path = (string)(parse_url($url, PHP_URL_PATH) ?? '');
|
||||||
|
$segments = media_path_segments($url);
|
||||||
|
$query = media_query_values($url);
|
||||||
|
|
||||||
|
$id = '';
|
||||||
|
if (media_host_matches($host, 'youtu.be') && isset($segments[0])) $id = $segments[0];
|
||||||
|
if ($id === '' && isset($query['v'])) $id = media_scalar_string($query['v']);
|
||||||
|
if ($id === '' && preg_match('#/(?:embed|shorts|live|v)/([^/?#]+)#', $path, $m)) $id = $m[1];
|
||||||
|
return media_clean_token($id);
|
||||||
|
}
|
||||||
|
|
||||||
|
function media_provider_oembed_endpoint(string $url, string $host): string {
|
||||||
|
if (media_host_matches($host, 'youtube.com') || media_host_matches($host, 'youtu.be') || media_host_matches($host, 'youtube-nocookie.com')) {
|
||||||
|
return 'https://www.youtube.com/oembed?format=json&url=' . rawurlencode($url);
|
||||||
|
}
|
||||||
|
if (media_host_matches($host, 'vimeo.com')) {
|
||||||
|
return 'https://vimeo.com/api/oembed.json?url=' . rawurlencode($url);
|
||||||
|
}
|
||||||
|
if (media_host_matches($host, 'dailymotion.com') || media_host_matches($host, 'dai.ly')) {
|
||||||
|
return 'https://www.dailymotion.com/services/oembed?url=' . rawurlencode($url);
|
||||||
|
}
|
||||||
|
if (media_host_matches($host, 'tiktok.com')) {
|
||||||
|
return 'https://www.tiktok.com/oembed?url=' . rawurlencode($url);
|
||||||
|
}
|
||||||
|
if (media_host_matches($host, 'soundcloud.com')) {
|
||||||
|
return 'https://soundcloud.com/oembed?format=json&url=' . rawurlencode($url);
|
||||||
|
}
|
||||||
|
if (media_host_matches($host, 'open.spotify.com')) {
|
||||||
|
return 'https://open.spotify.com/oembed?url=' . rawurlencode($url);
|
||||||
|
}
|
||||||
|
if (media_host_matches($host, 'wistia.com') || media_host_matches($host, 'wi.st')) {
|
||||||
|
return 'https://fast.wistia.com/oembed?url=' . rawurlencode($url);
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function media_oembed_thumbnail_url(string $url, string $host): string {
|
||||||
|
$endpoint = media_provider_oembed_endpoint($url, $host);
|
||||||
|
if ($endpoint === '') return '';
|
||||||
|
|
||||||
|
$fetch = media_http_get($endpoint, 8);
|
||||||
|
if ($fetch['body'] === '' || $fetch['error'] !== '') return '';
|
||||||
|
|
||||||
|
$data = json_decode($fetch['body'], true);
|
||||||
|
if (!is_array($data)) return '';
|
||||||
|
|
||||||
|
$thumb = normalize_media_url((string)($data['thumbnail_url'] ?? ''));
|
||||||
|
return $thumb;
|
||||||
|
}
|
||||||
|
|
||||||
|
function media_provider_thumbnail_url(string $url): string {
|
||||||
|
$url = normalize_media_url($url);
|
||||||
|
if ($url === '') return '';
|
||||||
|
|
||||||
|
$host = strtolower((string)(parse_url($url, PHP_URL_HOST) ?? ''));
|
||||||
|
$path = (string)(parse_url($url, PHP_URL_PATH) ?? '');
|
||||||
|
$segments = media_path_segments($url);
|
||||||
|
|
||||||
|
if (media_host_matches($host, 'youtu.be') || media_host_matches($host, 'youtube.com') || media_host_matches($host, 'youtube-nocookie.com')) {
|
||||||
|
$id = media_youtube_id_from_url($url);
|
||||||
|
return $id !== '' ? 'https://i.ytimg.com/vi/' . rawurlencode($id) . '/hqdefault.jpg' : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (media_host_matches($host, 'dailymotion.com') || media_host_matches($host, 'dai.ly')) {
|
||||||
|
$id = '';
|
||||||
|
if (media_host_matches($host, 'dai.ly') && isset($segments[0])) $id = $segments[0];
|
||||||
|
if ($id === '' && preg_match('#/(?:embed/)?video/([^/?#_]+)#', $path, $m)) $id = $m[1];
|
||||||
|
$id = media_clean_token($id);
|
||||||
|
if ($id !== '') return 'https://www.dailymotion.com/thumbnail/video/' . rawurlencode($id);
|
||||||
|
}
|
||||||
|
|
||||||
|
return media_oembed_thumbnail_url($url, $host);
|
||||||
|
}
|
||||||
|
|
||||||
|
function media_thumbnail_extension(string $url, string $content_type): string {
|
||||||
|
$content_type = strtolower($content_type);
|
||||||
|
if (str_contains($content_type, 'png')) return 'png';
|
||||||
|
if (str_contains($content_type, 'webp')) return 'webp';
|
||||||
|
if (str_contains($content_type, 'gif')) return 'gif';
|
||||||
|
|
||||||
|
$ext = media_extension_from_url($url);
|
||||||
|
return in_array($ext, ['jpg', 'jpeg', 'png', 'webp', 'gif'], true) ? ($ext === 'jpeg' ? 'jpg' : $ext) : 'jpg';
|
||||||
|
}
|
||||||
|
|
||||||
|
function media_download_thumbnail(string $url, string $slug, string $label = 'auto'): string {
|
||||||
|
$url = normalize_media_url($url);
|
||||||
|
if ($url === '') return '';
|
||||||
|
|
||||||
|
$fetch = media_http_get($url, 12);
|
||||||
|
if ($fetch['body'] === '' || $fetch['error'] !== '') return '';
|
||||||
|
if (function_exists('getimagesizefromstring') && !@getimagesizefromstring($fetch['body'])) return '';
|
||||||
|
|
||||||
|
$thumb_dir = MEDIA_DIR . 'thumbs/';
|
||||||
|
if (!is_dir($thumb_dir)) mkdir($thumb_dir, 0755, true);
|
||||||
|
|
||||||
|
$label = media_clean_token($label);
|
||||||
|
if ($label === '') $label = 'auto';
|
||||||
|
$ext = media_thumbnail_extension($url, (string)$fetch['content_type']);
|
||||||
|
$fname = $slug . '_' . $label . '_' . time() . '_' . substr(sha1($url), 0, 8) . '.' . $ext;
|
||||||
|
|
||||||
|
return file_put_contents($thumb_dir . $fname, $fetch['body']) !== false ? 'thumbs/' . $fname : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function media_disabled_function(string $name): bool {
|
||||||
|
$disabled = array_map('trim', explode(',', (string)ini_get('disable_functions')));
|
||||||
|
return in_array($name, $disabled, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
function media_ffmpeg_path(): string {
|
||||||
|
$configured = trim((string)getenv('FFMPEG_PATH'));
|
||||||
|
if ($configured !== '' && is_executable($configured)) return $configured;
|
||||||
|
|
||||||
|
if (!function_exists('shell_exec') || media_disabled_function('shell_exec')) return '';
|
||||||
|
$path = trim((string)@shell_exec('command -v ffmpeg 2>/dev/null'));
|
||||||
|
return $path !== '' && is_executable($path) ? $path : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function media_thumbnail_from_local_video(string $file_path, string $slug): string {
|
||||||
|
$ffmpeg = media_ffmpeg_path();
|
||||||
|
if ($ffmpeg === '') return '';
|
||||||
|
|
||||||
|
$source = MEDIA_DIR . ltrim($file_path, '/');
|
||||||
|
if ($file_path === '' || !is_file($source) || !is_readable($source)) return '';
|
||||||
|
|
||||||
|
$thumb_dir = MEDIA_DIR . 'thumbs/';
|
||||||
|
if (!is_dir($thumb_dir)) mkdir($thumb_dir, 0755, true);
|
||||||
|
$fname = $slug . '_frame_' . time() . '_' . substr(sha1($file_path), 0, 8) . '.jpg';
|
||||||
|
$target = $thumb_dir . $fname;
|
||||||
|
|
||||||
|
if (!function_exists('exec') || media_disabled_function('exec')) return '';
|
||||||
|
$cmd = escapeshellarg($ffmpeg) . ' -y -ss 00:00:01 -i ' . escapeshellarg($source) . ' -frames:v 1 -vf ' . escapeshellarg('scale=1280:-2') . ' ' . escapeshellarg($target) . ' 2>/dev/null';
|
||||||
|
@exec($cmd, $output, $code);
|
||||||
|
|
||||||
|
if ($code === 0 && is_file($target) && filesize($target) > 0 && (!function_exists('getimagesize') || @getimagesize($target))) {
|
||||||
|
return 'thumbs/' . $fname;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (is_file($target)) unlink($target);
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function media_auto_thumbnail_for_video(int $video_id, string $slug): string {
|
||||||
|
$stmt = get_db()->prepare("SELECT * FROM video_sources WHERE video_id=? ORDER BY sort_order ASC, id ASC");
|
||||||
|
$stmt->execute([$video_id]);
|
||||||
|
$sources = $stmt->fetchAll();
|
||||||
|
|
||||||
|
foreach ($sources as $source) {
|
||||||
|
if (!source_is_remote($source)) continue;
|
||||||
|
$thumb_url = media_provider_thumbnail_url($source['source_url'] ?? '');
|
||||||
|
$thumbnail = media_download_thumbnail($thumb_url, $slug, 'auto');
|
||||||
|
if ($thumbnail !== '') return $thumbnail;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($sources as $source) {
|
||||||
|
if (!source_is_local($source)) continue;
|
||||||
|
$thumbnail = media_thumbnail_from_local_video($source['file_path'] ?? '', $slug);
|
||||||
|
if ($thumbnail !== '') return $thumbnail;
|
||||||
|
}
|
||||||
|
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
function media_provider_embed(string $url): ?array {
|
function media_provider_embed(string $url): ?array {
|
||||||
$parts = @parse_url($url);
|
$parts = @parse_url($url);
|
||||||
if (!is_array($parts) || empty($parts['host'])) return null;
|
if (!is_array($parts) || empty($parts['host'])) return null;
|
||||||
@@ -344,12 +572,7 @@ function media_provider_embed(string $url): ?array {
|
|||||||
$query = media_query_values($url);
|
$query = media_query_values($url);
|
||||||
|
|
||||||
if (media_host_matches($host, 'youtu.be') || media_host_matches($host, 'youtube.com') || media_host_matches($host, 'youtube-nocookie.com')) {
|
if (media_host_matches($host, 'youtu.be') || media_host_matches($host, 'youtube.com') || media_host_matches($host, 'youtube-nocookie.com')) {
|
||||||
$id = '';
|
$id = media_youtube_id_from_url($url);
|
||||||
if (media_host_matches($host, 'youtu.be') && isset($segments[0])) $id = $segments[0];
|
|
||||||
if ($id === '' && isset($query['v'])) $id = media_scalar_string($query['v']);
|
|
||||||
if ($id === '' && preg_match('#/(?:embed|shorts|live|v)/([^/?#]+)#', $path, $m)) $id = $m[1];
|
|
||||||
|
|
||||||
$id = media_clean_token($id);
|
|
||||||
if ($id !== '') {
|
if ($id !== '') {
|
||||||
$params = ['rel' => '0', 'modestbranding' => '1', 'playsinline' => '1'];
|
$params = ['rel' => '0', 'modestbranding' => '1', 'playsinline' => '1'];
|
||||||
$start = isset($query['start'])
|
$start = isset($query['start'])
|
||||||
|
|||||||
Reference in New Issue
Block a user