Normalize YouTube timestamp parameters to ?start=N
Accepts ?t=30s, ?t=30, ?t=1m20s, ?t=1h2m3s, and ?start=N. Converts to seconds and emits as ?start=N on the embed URL. ?t= takes priority over ?start= when both are present. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -55,9 +55,49 @@ private function detectYouTube(string $url): ?string
|
||||
return null;
|
||||
}
|
||||
$src = "https://www.youtube-nocookie.com/embed/{$videoId}";
|
||||
$start = $this->extractYouTubeStart($url);
|
||||
if ($start !== null) {
|
||||
$src .= "?start={$start}";
|
||||
}
|
||||
return $this->iframeHtml($src, 'youtube');
|
||||
}
|
||||
|
||||
private function extractYouTubeStart(string $url): ?int
|
||||
{
|
||||
if (preg_match('/[?&]t=([^&#]+)/', $url, $m)) {
|
||||
$seconds = $this->parseTimestamp($m[1]);
|
||||
if ($seconds !== null) {
|
||||
return $seconds;
|
||||
}
|
||||
}
|
||||
if (preg_match('/[?&]start=(\d+)/', $url, $m)) {
|
||||
return (int) $m[1];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private function parseTimestamp(string $t): ?int
|
||||
{
|
||||
if (ctype_digit($t)) {
|
||||
return (int) $t;
|
||||
}
|
||||
$total = 0;
|
||||
$matched = false;
|
||||
if (preg_match('/(\d+)h/', $t, $m)) {
|
||||
$total += (int) $m[1] * 3600;
|
||||
$matched = true;
|
||||
}
|
||||
if (preg_match('/(\d+)m/', $t, $m)) {
|
||||
$total += (int) $m[1] * 60;
|
||||
$matched = true;
|
||||
}
|
||||
if (preg_match('/(\d+)s/', $t, $m)) {
|
||||
$total += (int) $m[1];
|
||||
$matched = true;
|
||||
}
|
||||
return $matched ? $total : null;
|
||||
}
|
||||
|
||||
private function iframeHtml(string $src, string $provider): string
|
||||
{
|
||||
$safe = htmlspecialchars($src, ENT_QUOTES, 'UTF-8');
|
||||
|
||||
@@ -126,4 +126,23 @@ public static function invalidYoutubeUrls(): array
|
||||
'XSS attempt in id' => ['https://youtu.be/abc"><script>'],
|
||||
];
|
||||
}
|
||||
|
||||
#[DataProvider('youtubeTimestampUrls')]
|
||||
public function test_youtube_timestamp_normalizes_to_start(string $url, int $expectedStart): void
|
||||
{
|
||||
$html = $this->resolver->resolve($url);
|
||||
$this->assertNotNull($html);
|
||||
$this->assertStringContainsString("?start={$expectedStart}", $html);
|
||||
}
|
||||
|
||||
public static function youtubeTimestampUrls(): array
|
||||
{
|
||||
return [
|
||||
't=30s' => ['https://youtu.be/dQw4w9WgXcQ?t=30s', 30],
|
||||
't=30 (no suffix)' => ['https://www.youtube.com/watch?v=dQw4w9WgXcQ&t=30', 30],
|
||||
't=1m20s' => ['https://youtu.be/dQw4w9WgXcQ?t=1m20s', 80],
|
||||
't=1h2m3s' => ['https://youtu.be/dQw4w9WgXcQ?t=1h2m3s', 3723],
|
||||
'start=45' => ['https://www.youtube.com/watch?v=dQw4w9WgXcQ&start=45', 45],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user