diff --git a/src/app/Markdown/MediaUrlResolver.php b/src/app/Markdown/MediaUrlResolver.php
index a4dfe5d..a650ee3 100644
--- a/src/app/Markdown/MediaUrlResolver.php
+++ b/src/app/Markdown/MediaUrlResolver.php
@@ -14,7 +14,8 @@ public function resolve(string $url): ?string
return null;
}
return $this->detectVideo($url)
- ?? $this->detectAudio($url);
+ ?? $this->detectAudio($url)
+ ?? $this->detectYouTube($url);
}
private function detectVideo(string $url): ?string
@@ -35,6 +36,40 @@ private function detectAudio(string $url): ?string
return "";
}
+ private function detectYouTube(string $url): ?string
+ {
+ $patterns = [
+ '~^https?://youtu\.be/([A-Za-z0-9_-]{11})(?:[/?#]|$)~',
+ '~^https?://(?:www\.|m\.)?youtube\.com/watch\?(?:[^#]*&)?v=([A-Za-z0-9_-]{11})(?:[]|$)~',
+ '~^https?://(?:www\.|m\.)?youtube\.com/shorts/([A-Za-z0-9_-]{11})(?:[/?#]|$)~',
+ '~^https?://(?:www\.|m\.)?youtube\.com/embed/([A-Za-z0-9_-]{11})(?:[/?#]|$)~',
+ ];
+ $videoId = null;
+ foreach ($patterns as $p) {
+ if (preg_match($p, $url, $m)) {
+ $videoId = $m[1];
+ break;
+ }
+ }
+ if ($videoId === null) {
+ return null;
+ }
+ $src = "https://www.youtube-nocookie.com/embed/{$videoId}";
+ return $this->iframeHtml($src, 'youtube');
+ }
+
+ private function iframeHtml(string $src, string $provider): string
+ {
+ $safe = htmlspecialchars($src, ENT_QUOTES, 'UTF-8');
+ return '';
+ }
+
private function getPathExtension(string $url): string
{
$path = parse_url($url, PHP_URL_PATH);
diff --git a/src/tests/Unit/Markdown/MediaUrlResolverTest.php b/src/tests/Unit/Markdown/MediaUrlResolverTest.php
index 2a64cad..4498802 100644
--- a/src/tests/Unit/Markdown/MediaUrlResolverTest.php
+++ b/src/tests/Unit/Markdown/MediaUrlResolverTest.php
@@ -87,4 +87,43 @@ public static function audioUrls(): array
'm4a' => ['/clip.m4a'],
];
}
+
+ #[DataProvider('youtubeUrls')]
+ public function test_youtube_urls_produce_iframe(string $url): void
+ {
+ $html = $this->resolver->resolve($url);
+ $this->assertNotNull($html);
+ $this->assertStringStartsWith('