-Implemented YouTube timestamp correction and added the admin Upkeeping section.

What changed:
Added shared YouTube publish-date scraping helpers in 
[includes/db.php](/Users/tyemeclifford/Documents/GH/mediaplayer/includes/db.php).
New YouTube imports now set videos.created_at to the original YouTube 
publish date when available.
Manual Add/Edit saves now also try to correct timestamps for 
YouTube-backed videos.
Added Admin Dashboard → Upkeeping → Correct YouTube timestamps in 
[admin/index.php](/Users/tyemeclifford/Documents/GH/mediaplayer/admin/index.php), 
with batch size control for backfilling existing records.
Renamed the admin table column from Added to Date, since it may now 
reflect original publish date.
Documented the behavior in 
[README.md](/Users/tyemeclifford/Documents/GH/mediaplayer/README.md).
This commit is contained in:
Ty Clifford
2026-06-24 13:40:03 -04:00
parent 99492287e7
commit dc71d26c2a
6 changed files with 205 additions and 8 deletions
+144 -2
View File
@@ -306,13 +306,17 @@ function media_append_query(string $base, array $params): string {
return $base . (strpos($base, '?') === false ? '?' : '&') . $query;
}
function media_http_get(string $url, int $timeout = 12): array {
function media_http_get(string $url, int $timeout = 12, string $accept = ''): array {
$url = normalize_media_url($url);
if ($url === '') return ['body' => '', 'code' => 0, 'content_type' => '', 'error' => 'Invalid URL.'];
if ($accept === '') {
$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';
}
$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',
'Accept: ' . $accept,
'Accept-Language: en-US,en;q=0.9',
];
if (function_exists('curl_init')) {
@@ -411,6 +415,144 @@ function media_youtube_id_from_url(string $url): string {
return media_clean_token($id);
}
function media_normalize_datetime(string $value): string {
$value = trim(html_entity_decode($value, ENT_QUOTES | ENT_HTML5, 'UTF-8'));
if ($value === '') return '';
if (preg_match('/^(\d{4}-\d{2}-\d{2})/', $value, $m)) {
return $m[1] . ' 00:00:00';
}
try {
$date = new DateTimeImmutable($value);
return $date->format('Y-m-d H:i:s');
} catch (Throwable $e) {
return '';
}
}
function media_youtube_publish_datetime_from_html(string $html): string {
$patterns = [
'/<meta[^>]+itemprop=["\']datePublished["\'][^>]+content=["\']([^"\']+)["\']/i',
'/<meta[^>]+content=["\']([^"\']+)["\'][^>]+itemprop=["\']datePublished["\']/i',
'/"datePublished"\s*:\s*"([^"]+)"/',
'/"publishDate"\s*:\s*"([^"]+)"/',
'/"uploadDate"\s*:\s*"([^"]+)"/',
];
foreach ($patterns as $pattern) {
if (!preg_match($pattern, $html, $match)) continue;
$value = json_decode('"' . str_replace('"', '\"', $match[1]) . '"');
$date = media_normalize_datetime(is_string($value) ? $value : $match[1]);
if ($date !== '') return $date;
}
return '';
}
function media_youtube_publish_datetime(string $url): string {
$id = media_youtube_id_from_url($url);
if ($id === '') return '';
$watch_url = 'https://www.youtube.com/watch?v=' . rawurlencode($id);
$fetch = media_http_get($watch_url, 15, 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8');
if ($fetch['body'] === '' || $fetch['error'] !== '') return '';
return media_youtube_publish_datetime_from_html($fetch['body']);
}
function media_youtube_source_url_for_video(int $video_id): string {
$stmt = get_db()->prepare("
SELECT source_url
FROM video_sources
WHERE video_id=? AND source_type='remote' AND source_url!=''
ORDER BY sort_order ASC, id ASC
");
$stmt->execute([$video_id]);
foreach ($stmt->fetchAll() as $source) {
$url = normalize_media_url($source['source_url'] ?? '');
if ($url !== '' && media_youtube_id_from_url($url) !== '') return $url;
}
return '';
}
function media_apply_youtube_timestamp(int $video_id): array {
$url = media_youtube_source_url_for_video($video_id);
if ($url === '') return ['status' => 'no_youtube', 'date' => '', 'url' => ''];
$published_at = media_youtube_publish_datetime($url);
if ($published_at === '') return ['status' => 'not_found', 'date' => '', 'url' => $url];
$stmt = get_db()->prepare("SELECT created_at FROM videos WHERE id=?");
$stmt->execute([$video_id]);
$current = (string)$stmt->fetchColumn();
if (substr($current, 0, 10) === substr($published_at, 0, 10)) {
return ['status' => 'already_correct', 'date' => $published_at, 'url' => $url];
}
get_db()->prepare("UPDATE videos SET created_at=?, updated_at=datetime('now') WHERE id=?")
->execute([$published_at, $video_id]);
return ['status' => 'corrected', 'date' => $published_at, 'url' => $url];
}
function media_youtube_timestamp_candidates(int $limit): array {
$limit = max(1, min(100, $limit));
$stmt = get_db()->prepare("
SELECT DISTINCT v.id
FROM videos v
INNER JOIN video_sources vs ON vs.video_id = v.id
WHERE vs.source_type='remote'
AND (
vs.source_url LIKE '%youtube.com%'
OR vs.source_url LIKE '%youtu.be%'
OR vs.source_url LIKE '%youtube-nocookie.com%'
)
ORDER BY v.id DESC
LIMIT :limit
");
$stmt->bindValue(':limit', $limit, PDO::PARAM_INT);
$stmt->execute();
return array_map('intval', array_column($stmt->fetchAll(), 'id'));
}
function media_youtube_timestamp_candidate_count(): int {
return (int)get_db()->query("
SELECT COUNT(DISTINCT v.id)
FROM videos v
INNER JOIN video_sources vs ON vs.video_id = v.id
WHERE vs.source_type='remote'
AND (
vs.source_url LIKE '%youtube.com%'
OR vs.source_url LIKE '%youtu.be%'
OR vs.source_url LIKE '%youtube-nocookie.com%'
)
")->fetchColumn();
}
function media_correct_youtube_timestamps(int $limit = 25): array {
$result = [
'checked' => 0,
'corrected' => 0,
'already_correct' => 0,
'not_found' => 0,
'no_youtube' => 0,
];
foreach (media_youtube_timestamp_candidates($limit) as $video_id) {
$result['checked']++;
$applied = media_apply_youtube_timestamp($video_id);
$status = $applied['status'] ?? 'not_found';
if (!isset($result[$status])) $status = 'not_found';
$result[$status]++;
}
return $result;
}
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);