From 9486d97c73b315460cd062cd6b1add7aca24c8e0 Mon Sep 17 00:00:00 2001 From: Yutaka Kurosaki <> Date: Sat, 9 May 2026 10:52:49 +0900 Subject: [PATCH] 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) --- src/app/Markdown/MediaUrlResolver.php | 40 +++++++++++++++++++ .../Unit/Markdown/MediaUrlResolverTest.php | 19 +++++++++ 2 files changed, 59 insertions(+) diff --git a/src/app/Markdown/MediaUrlResolver.php b/src/app/Markdown/MediaUrlResolver.php index a650ee3..a277bbb 100644 --- a/src/app/Markdown/MediaUrlResolver.php +++ b/src/app/Markdown/MediaUrlResolver.php @@ -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'); diff --git a/src/tests/Unit/Markdown/MediaUrlResolverTest.php b/src/tests/Unit/Markdown/MediaUrlResolverTest.php index 4498802..6230342 100644 --- a/src/tests/Unit/Markdown/MediaUrlResolverTest.php +++ b/src/tests/Unit/Markdown/MediaUrlResolverTest.php @@ -126,4 +126,23 @@ public static function invalidYoutubeUrls(): array 'XSS attempt in id' => ['https://youtu.be/abc">