diff --git a/config/config_test.go b/config/config_test.go index 8eb0e88d..f492f4f5 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -1616,6 +1616,24 @@ func TestFetchYouTubeWatchTime(t *testing.T) { } } +func TestYouTubeEmbedUrlOverride(t *testing.T) { + os.Clearenv() + os.Setenv("YOUTUBE_EMBED_URL_OVERRIDE", "https://invidious.custom/embed/") + + parser := NewParser() + opts, err := parser.ParseEnvironmentVariables() + if err != nil { + t.Fatalf(`Parsing failure: %v`, err) + } + + expected := "https://invidious.custom/embed/" + result := opts.YouTubeEmbedUrlOverride() + + if result != expected { + t.Fatalf(`Unexpected YOUTUBE_EMBED_URL_OVERRIDE value, got %v instead of %v`, result, expected) + } +} + func TestParseConfigDumpOutput(t *testing.T) { os.Clearenv() diff --git a/config/options.go b/config/options.go index f7448578..52a6ef10 100644 --- a/config/options.go +++ b/config/options.go @@ -50,6 +50,7 @@ const ( defaultProxyMediaTypes = "image" defaultProxyUrl = "" defaultFetchYouTubeWatchTime = false + defaultYouTubeEmbedUrlOverride = "https://www.youtube-nocookie.com/embed/" defaultCreateAdmin = false defaultAdminUsername = "" defaultAdminPassword = "" @@ -126,6 +127,7 @@ type Options struct { proxyMediaTypes []string proxyUrl string fetchYouTubeWatchTime bool + youTubeEmbedUrlOverride string oauth2UserCreationAllowed bool oauth2ClientID string oauth2ClientSecret string @@ -195,6 +197,7 @@ func NewOptions() *Options { proxyMediaTypes: []string{defaultProxyMediaTypes}, proxyUrl: defaultProxyUrl, fetchYouTubeWatchTime: defaultFetchYouTubeWatchTime, + youTubeEmbedUrlOverride: defaultYouTubeEmbedUrlOverride, oauth2UserCreationAllowed: defaultOAuth2UserCreation, oauth2ClientID: defaultOAuth2ClientID, oauth2ClientSecret: defaultOAuth2ClientSecret, @@ -428,6 +431,11 @@ func (o *Options) FetchYouTubeWatchTime() bool { return o.fetchYouTubeWatchTime } +// YouTubeEmbedUrlOverride returns YouTube URL which will be used for embeds +func (o *Options) YouTubeEmbedUrlOverride() string { + return o.youTubeEmbedUrlOverride +} + // ProxyOption returns "none" to never proxy, "http-only" to proxy non-HTTPS, "all" to always proxy. func (o *Options) ProxyOption() string { return o.proxyOption @@ -558,20 +566,20 @@ func (o *Options) SortedOptions(redactSecret bool) []*Option { "BATCH_SIZE": o.batchSize, "CERT_DOMAIN": o.certDomain, "CERT_FILE": o.certFile, + "CLEANUP_ARCHIVE_BATCH_SIZE": o.cleanupArchiveBatchSize, "CLEANUP_ARCHIVE_READ_DAYS": o.cleanupArchiveReadDays, "CLEANUP_ARCHIVE_UNREAD_DAYS": o.cleanupArchiveUnreadDays, - "CLEANUP_ARCHIVE_BATCH_SIZE": o.cleanupArchiveBatchSize, "CLEANUP_FREQUENCY_HOURS": o.cleanupFrequencyHours, "CLEANUP_REMOVE_SESSIONS_DAYS": o.cleanupRemoveSessionsDays, "CREATE_ADMIN": o.createAdmin, + "DATABASE_CONNECTION_LIFETIME": o.databaseConnectionLifetime, "DATABASE_MAX_CONNS": o.databaseMaxConns, "DATABASE_MIN_CONNS": o.databaseMinConns, - "DATABASE_CONNECTION_LIFETIME": o.databaseConnectionLifetime, "DATABASE_URL": redactSecretValue(o.databaseURL, redactSecret), "DEBUG": o.debug, "DISABLE_HSTS": !o.hsts, - "DISABLE_SCHEDULER_SERVICE": !o.schedulerService, "DISABLE_HTTP_SERVICE": !o.httpService, + "DISABLE_SCHEDULER_SERVICE": !o.schedulerService, "FETCH_YOUTUBE_WATCH_TIME": o.fetchYouTubeWatchTime, "HTTPS": o.HTTPS, "HTTP_CLIENT_MAX_BODY_SIZE": o.httpClientMaxBodySize, @@ -580,17 +588,17 @@ func (o *Options) SortedOptions(redactSecret bool) []*Option { "HTTP_CLIENT_USER_AGENT": o.httpClientUserAgent, "HTTP_SERVER_TIMEOUT": o.httpServerTimeout, "HTTP_SERVICE": o.httpService, - "KEY_FILE": o.certKeyFile, "INVIDIOUS_INSTANCE": o.invidiousInstance, + "KEY_FILE": o.certKeyFile, "LISTEN_ADDR": o.listenAddr, "LOG_DATE_TIME": o.logDateTime, "MAINTENANCE_MESSAGE": o.maintenanceMessage, "MAINTENANCE_MODE": o.maintenanceMode, "METRICS_ALLOWED_NETWORKS": strings.Join(o.metricsAllowedNetworks, ","), "METRICS_COLLECTOR": o.metricsCollector, + "METRICS_PASSWORD": redactSecretValue(o.metricsPassword, redactSecret), "METRICS_REFRESH_INTERVAL": o.metricsRefreshInterval, "METRICS_USERNAME": o.metricsUsername, - "METRICS_PASSWORD": redactSecretValue(o.metricsPassword, redactSecret), "OAUTH2_CLIENT_ID": o.oauth2ClientID, "OAUTH2_CLIENT_SECRET": redactSecretValue(o.oauth2ClientSecret, redactSecret), "OAUTH2_OIDC_DISCOVERY_ENDPOINT": o.oauth2OidcDiscoveryEndpoint, @@ -602,9 +610,9 @@ func (o *Options) SortedOptions(redactSecret bool) []*Option { "POLLING_PARSING_ERROR_LIMIT": o.pollingParsingErrorLimit, "POLLING_SCHEDULER": o.pollingScheduler, "PROXY_HTTP_CLIENT_TIMEOUT": o.proxyHTTPClientTimeout, - "PROXY_PRIVATE_KEY": redactSecretValue(string(o.proxyPrivateKey), redactSecret), "PROXY_MEDIA_TYPES": o.proxyMediaTypes, "PROXY_OPTION": o.proxyOption, + "PROXY_PRIVATE_KEY": redactSecretValue(string(o.proxyPrivateKey), redactSecret), "PROXY_URL": o.proxyUrl, "ROOT_URL": o.rootURL, "RUN_MIGRATIONS": o.runMigrations, @@ -612,8 +620,9 @@ func (o *Options) SortedOptions(redactSecret bool) []*Option { "SCHEDULER_ENTRY_FREQUENCY_MIN_INTERVAL": o.schedulerEntryFrequencyMinInterval, "SCHEDULER_SERVICE": o.schedulerService, "SERVER_TIMING_HEADER": o.serverTimingHeader, - "WORKER_POOL_SIZE": o.workerPoolSize, "WATCHDOG": o.watchdog, + "WORKER_POOL_SIZE": o.workerPoolSize, + "YOUTUBE_EMBED_URL_OVERRIDE": o.youTubeEmbedUrlOverride, } keys := make([]string, 0, len(keyValues)) diff --git a/config/parser.go b/config/parser.go index d8c3c7db..a76e02c0 100644 --- a/config/parser.go +++ b/config/parser.go @@ -215,6 +215,8 @@ func (p *Parser) parseLines(lines []string) (err error) { p.opts.metricsPassword = readSecretFile(value, defaultMetricsPassword) case "FETCH_YOUTUBE_WATCH_TIME": p.opts.fetchYouTubeWatchTime = parseBool(value, defaultFetchYouTubeWatchTime) + case "YOUTUBE_EMBED_URL_OVERRIDE": + p.opts.youTubeEmbedUrlOverride = parseString(value, defaultYouTubeEmbedUrlOverride) case "WATCHDOG": p.opts.watchdog = parseBool(value, defaultWatchdog) case "INVIDIOUS_INSTANCE": diff --git a/miniflux.1 b/miniflux.1 index d4d2d8d5..8d834d1c 100644 --- a/miniflux.1 +++ b/miniflux.1 @@ -124,6 +124,11 @@ use it as a reading time\&. .br Disabled by default\&. .TP +.B YOUTUBE_EMBED_URL_OVERRIDE +YouTube URL which will be used for embeds.\&. +.br +Default is https://www.youtube-nocookie.com/embed/\& +.TP .B SERVER_TIMING_HEADER Set the value to 1 to enable server-timing headers\&. .br diff --git a/reader/rewrite/rewrite_functions.go b/reader/rewrite/rewrite_functions.go index 20ba28cf..9443230e 100644 --- a/reader/rewrite/rewrite_functions.go +++ b/reader/rewrite/rewrite_functions.go @@ -208,7 +208,7 @@ func addYoutubeVideo(entryURL, entryContent string) string { matches := youtubeRegex.FindStringSubmatch(entryURL) if len(matches) == 2 { - video := `` + video := `` return video + `
` + entryContent } return entryContent @@ -232,7 +232,8 @@ func addYoutubeVideoFromId(entryContent string) string { sb := strings.Builder{} for _, match := range matches { if len(match) == 2 { - sb.WriteString(`
`) } diff --git a/reader/rewrite/rewriter_test.go b/reader/rewrite/rewriter_test.go index b7e04e90..2781a2a9 100644 --- a/reader/rewrite/rewriter_test.go +++ b/reader/rewrite/rewriter_test.go @@ -4,10 +4,12 @@ package rewrite // import "miniflux.app/reader/rewrite" import ( + "os" "reflect" "strings" "testing" + "miniflux.app/config" "miniflux.app/model" ) @@ -63,6 +65,8 @@ func TestRewriteWithNoMatchingRule(t *testing.T) { } func TestRewriteWithYoutubeLink(t *testing.T) { + config.Opts = config.NewOptions() + controlEntry := &model.Entry{ Title: `A title`, Content: `
Video Description`, @@ -78,6 +82,33 @@ func TestRewriteWithYoutubeLink(t *testing.T) { } } +func TestRewriteWithYoutubeLinkAndCustomEmbedURL(t *testing.T) { + os.Clearenv() + os.Setenv("YOUTUBE_EMBED_URL_OVERRIDE", "https://invidious.custom/embed/") + + var err error + parser := config.NewParser() + config.Opts, err = parser.ParseEnvironmentVariables() + + if err != nil { + t.Fatalf(`Parsing failure: %v`, err) + } + + controlEntry := &model.Entry{ + Title: `A title`, + Content: `
Video Description`, + } + testEntry := &model.Entry{ + Title: `A title`, + Content: `Video Description`, + } + Rewriter("https://www.youtube.com/watch?v=1234", testEntry, ``) + + 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{ Title: `A title`, diff --git a/reader/sanitizer/sanitizer.go b/reader/sanitizer/sanitizer.go index 9d1154b9..e811dd91 100644 --- a/reader/sanitizer/sanitizer.go +++ b/reader/sanitizer/sanitizer.go @@ -441,7 +441,7 @@ func inList(needle string, haystack []string) bool { func rewriteIframeURL(link string) string { matches := youtubeEmbedRegex.FindStringSubmatch(link) if len(matches) == 2 { - return `https://www.youtube-nocookie.com/embed/` + matches[1] + return config.Opts.YouTubeEmbedUrlOverride() + matches[1] } return link diff --git a/reader/sanitizer/sanitizer_test.go b/reader/sanitizer/sanitizer_test.go index 4b2a5cb7..100bf684 100644 --- a/reader/sanitizer/sanitizer_test.go +++ b/reader/sanitizer/sanitizer_test.go @@ -3,7 +3,18 @@ package sanitizer // import "miniflux.app/reader/sanitizer" -import "testing" +import ( + "os" + "testing" + + "miniflux.app/config" +) + +func TestMain(m *testing.M) { + config.Opts = config.NewOptions() + exitCode := m.Run() + os.Exit(exitCode) +} func TestValidInput(t *testing.T) { input := `

This is a text with an image: Test.

` @@ -540,6 +551,27 @@ func TestReplaceProtocolRelativeYoutubeURL(t *testing.T) { } } +func TestReplaceYoutubeURLWithCustomURL(t *testing.T) { + os.Clearenv() + os.Setenv("YOUTUBE_EMBED_URL_OVERRIDE", "https://invidious.custom/embed/") + + var err error + parser := config.NewParser() + config.Opts, err = parser.ParseEnvironmentVariables() + + if err != nil { + t.Fatalf(`Parsing failure: %v`, err) + } + + input := `` + expected := `` + output := Sanitize("http://example.org/", input) + + if expected != output { + t.Errorf(`Wrong output: "%s" != "%s"`, expected, output) + } +} + func TestReplaceIframeURL(t *testing.T) { input := `` expected := ``