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;
|
return null;
|
||||||
}
|
}
|
||||||
$src = "https://www.youtube-nocookie.com/embed/{$videoId}";
|
$src = "https://www.youtube-nocookie.com/embed/{$videoId}";
|
||||||
|
$start = $this->extractYouTubeStart($url);
|
||||||
|
if ($start !== null) {
|
||||||
|
$src .= "?start={$start}";
|
||||||
|
}
|
||||||
return $this->iframeHtml($src, 'youtube');
|
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
|
private function iframeHtml(string $src, string $provider): string
|
||||||
{
|
{
|
||||||
$safe = htmlspecialchars($src, ENT_QUOTES, 'UTF-8');
|
$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>'],
|
'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