From 40727704c2d00a7d6aee09f0b63de95a3850b2a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Guillot?= Date: Thu, 12 Jun 2025 20:43:21 -0700 Subject: [PATCH] feat(rewrite): add support for YouTube Shorts video URL pattern --- internal/reader/rewrite/rewrite_functions.go | 39 ++++++--- internal/reader/rewrite/rewriter.go | 2 +- internal/reader/rewrite/rewriter_test.go | 84 +++++++++++++++++++- 3 files changed, 109 insertions(+), 16 deletions(-) diff --git a/internal/reader/rewrite/rewrite_functions.go b/internal/reader/rewrite/rewrite_functions.go index e128a22a..3290d60b 100644 --- a/internal/reader/rewrite/rewrite_functions.go +++ b/internal/reader/rewrite/rewrite_functions.go @@ -21,10 +21,11 @@ import ( ) var ( - youtubeRegex = regexp.MustCompile(`youtube\.com/watch\?v=(.*)$`) - youtubeIdRegex = regexp.MustCompile(`youtube_id"?\s*[:=]\s*"([a-zA-Z0-9_-]{11})"`) - invidioRegex = regexp.MustCompile(`https?://(.*)/watch\?v=(.*)`) - textLinkRegex = regexp.MustCompile(`(?mi)(\bhttps?:\/\/[-A-Z0-9+&@#\/%?=~_|!:,.;]*[-A-Z0-9+&@#\/%=~_|])`) + youtubeVideoRegex = regexp.MustCompile(`youtube\.com/watch\?v=(.*)$`) + youtubeShortRegex = regexp.MustCompile(`youtube\.com/shorts/([a-zA-Z0-9_-]{11})$`) + youtubeIdRegex = regexp.MustCompile(`youtube_id"?\s*[:=]\s*"([a-zA-Z0-9_-]{11})"`) + invidioRegex = regexp.MustCompile(`https?://(.*)/watch\?v=(.*)`) + textLinkRegex = regexp.MustCompile(`(?mi)(\bhttps?:\/\/[-A-Z0-9+&@#\/%?=~_|!:,.;]*[-A-Z0-9+&@#\/%=~_|])`) ) // titlelize returns a copy of the string s with all Unicode letters that begin words @@ -259,22 +260,34 @@ func useNoScriptImages(entryContent string) string { return output } -func addYoutubeVideo(entryURL, entryContent string) string { - matches := youtubeRegex.FindStringSubmatch(entryURL) +func getYoutubVideoIDFromURL(entryURL string) string { + matches := youtubeVideoRegex.FindStringSubmatch(entryURL) + + if len(matches) != 2 { + matches = youtubeShortRegex.FindStringSubmatch(entryURL) + } if len(matches) == 2 { - video := `` - return video + `
` + entryContent + return matches[1] + } + return "" +} + +func addVideoPlayerIframe(absoluteVideoURL, entryContent string) string { + video := `` + return video + `
` + entryContent +} + +func addYoutubeVideoRewriteRule(entryURL, entryContent string) string { + if videoURL := getYoutubVideoIDFromURL(entryURL); videoURL != "" { + return addVideoPlayerIframe(config.Opts.YouTubeEmbedUrlOverride()+videoURL, entryContent) } return entryContent } func addYoutubeVideoUsingInvidiousPlayer(entryURL, entryContent string) string { - matches := youtubeRegex.FindStringSubmatch(entryURL) - - if len(matches) == 2 { - video := `` - return video + `
` + entryContent + if videoURL := getYoutubVideoIDFromURL(entryURL); videoURL != "" { + return addVideoPlayerIframe(`https://`+config.Opts.InvidiousInstance()+`/embed/`+videoURL, entryContent) } return entryContent } diff --git a/internal/reader/rewrite/rewriter.go b/internal/reader/rewrite/rewriter.go index 50284bc9..d22adf8e 100644 --- a/internal/reader/rewrite/rewriter.go +++ b/internal/reader/rewrite/rewriter.go @@ -29,7 +29,7 @@ func (rule rule) applyRule(entryURL string, entry *model.Entry) { case "add_dynamic_iframe": entry.Content = addDynamicIframe(entry.Content) case "add_youtube_video": - entry.Content = addYoutubeVideo(entryURL, entry.Content) + entry.Content = addYoutubeVideoRewriteRule(entryURL, entry.Content) case "add_invidious_video": entry.Content = addInvidiousVideo(entryURL, entry.Content) case "add_youtube_video_using_invidious_player": diff --git a/internal/reader/rewrite/rewriter_test.go b/internal/reader/rewrite/rewriter_test.go index 4d9ba582..b0b7564d 100644 --- a/internal/reader/rewrite/rewriter_test.go +++ b/internal/reader/rewrite/rewriter_test.go @@ -66,7 +66,7 @@ func TestRewriteWithNoMatchingRule(t *testing.T) { } } -func TestRewriteWithYoutubeLink(t *testing.T) { +func TestRewriteYoutubeVideoLink(t *testing.T) { config.Opts = config.NewOptions() controlEntry := &model.Entry{ @@ -86,7 +86,47 @@ func TestRewriteWithYoutubeLink(t *testing.T) { } } -func TestRewriteWithYoutubeLinkAndCustomEmbedURL(t *testing.T) { +func TestRewriteYoutubeShortLink(t *testing.T) { + config.Opts = config.NewOptions() + + controlEntry := &model.Entry{ + URL: "https://www.youtube.com/shorts/1LUWKWZkPjo", + Title: `A title`, + Content: `
Video Description`, + } + testEntry := &model.Entry{ + URL: "https://www.youtube.com/shorts/1LUWKWZkPjo", + Title: `A title`, + Content: `Video Description`, + } + ApplyContentRewriteRules(testEntry, ``) + + if !reflect.DeepEqual(testEntry, controlEntry) { + t.Errorf(`Not expected output: got "%+v" instead of "%+v"`, testEntry, controlEntry) + } +} + +func TestRewriteIncorrectYoutubeLink(t *testing.T) { + config.Opts = config.NewOptions() + + controlEntry := &model.Entry{ + URL: "https://www.youtube.com/some-page", + Title: `A title`, + Content: `Video Description`, + } + testEntry := &model.Entry{ + URL: "https://www.youtube.com/some-page", + Title: `A title`, + Content: `Video Description`, + } + ApplyContentRewriteRules(testEntry, ``) + + if !reflect.DeepEqual(testEntry, controlEntry) { + t.Errorf(`Not expected output: got "%+v" instead of "%+v"`, testEntry, controlEntry) + } +} + +func TestRewriteYoutubeLinkAndCustomEmbedURL(t *testing.T) { os.Clearenv() os.Setenv("YOUTUBE_EMBED_URL_OVERRIDE", "https://invidious.custom/embed/") @@ -115,6 +155,46 @@ func TestRewriteWithYoutubeLinkAndCustomEmbedURL(t *testing.T) { } } +func TestRewriteYoutubeVideoLinkUsingInvidious(t *testing.T) { + config.Opts = config.NewOptions() + controlEntry := &model.Entry{ + URL: "https://www.youtube.com/watch?v=1234", + Title: `A title`, + Content: `
Video Description`, + } + testEntry := &model.Entry{ + URL: "https://www.youtube.com/watch?v=1234", + Title: `A title`, + Content: `Video Description`, + } + + ApplyContentRewriteRules(testEntry, `add_youtube_video_using_invidious_player`) + + if !reflect.DeepEqual(testEntry, controlEntry) { + t.Errorf(`Not expected output: got "%+v" instead of "%+v"`, testEntry, controlEntry) + } +} + +func TestRewriteYoutubeShortLinkUsingInvidious(t *testing.T) { + config.Opts = config.NewOptions() + controlEntry := &model.Entry{ + URL: "https://www.youtube.com/shorts/1LUWKWZkPjo", + Title: `A title`, + Content: `
Video Description`, + } + testEntry := &model.Entry{ + URL: "https://www.youtube.com/shorts/1LUWKWZkPjo", + Title: `A title`, + Content: `Video Description`, + } + + ApplyContentRewriteRules(testEntry, `add_youtube_video_using_invidious_player`) + + if !reflect.DeepEqual(testEntry, controlEntry) { + t.Errorf(`Not expected output: got "%+v" instead of "%+v"`, testEntry, controlEntry) + } +} + func TestRewriteWithInexistingCustomRule(t *testing.T) { controlEntry := &model.Entry{ URL: "https://www.youtube.com/watch?v=1234",