Browse Source

feat: support for custom youtube embed URL

Igor Rzegocki 2 years ago
parent
commit
9b42d0e25e

+ 18 - 0
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()
 

+ 16 - 7
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))

+ 2 - 0
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":

+ 5 - 0
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

+ 3 - 2
reader/rewrite/rewrite_functions.go

@@ -208,7 +208,7 @@ func addYoutubeVideo(entryURL, entryContent string) string {
 	matches := youtubeRegex.FindStringSubmatch(entryURL)
 
 	if len(matches) == 2 {
-		video := `<iframe width="650" height="350" frameborder="0" src="https://www.youtube-nocookie.com/embed/` + matches[1] + `" allowfullscreen></iframe>`
+		video := `<iframe width="650" height="350" frameborder="0" src="` + config.Opts.YouTubeEmbedUrlOverride() + matches[1] + `" allowfullscreen></iframe>`
 		return video + `<br>` + 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(`<iframe width="650" height="350" frameborder="0" src="https://www.youtube-nocookie.com/embed/`)
+			sb.WriteString(`<iframe width="650" height="350" frameborder="0" src="`)
+			sb.WriteString(config.Opts.YouTubeEmbedUrlOverride())
 			sb.WriteString(match[1])
 			sb.WriteString(`" allowfullscreen></iframe><br>`)
 		}

+ 31 - 0
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: `<iframe width="650" height="350" frameborder="0" src="https://www.youtube-nocookie.com/embed/1234" allowfullscreen></iframe><br>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: `<iframe width="650" height="350" frameborder="0" src="https://invidious.custom/embed/1234" allowfullscreen></iframe><br>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`,

+ 1 - 1
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

+ 33 - 1
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 := `<p>This is a <strong>text</strong> with an image: <img src="http://example.org/" alt="Test" loading="lazy">.</p>`
@@ -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 := `<iframe src="https://www.youtube.com/embed/test123?version=3&#038;rel=1&#038;fs=1&#038;autohide=2&#038;showsearch=0&#038;showinfo=1&#038;iv_load_policy=1&#038;wmode=transparent"></iframe>`
+	expected := `<iframe src="https://invidious.custom/embed/test123?version=3&amp;rel=1&amp;fs=1&amp;autohide=2&amp;showsearch=0&amp;showinfo=1&amp;iv_load_policy=1&amp;wmode=transparent" sandbox="allow-scripts allow-same-origin allow-popups" loading="lazy"></iframe>`
+	output := Sanitize("http://example.org/", input)
+
+	if expected != output {
+		t.Errorf(`Wrong output: "%s" != "%s"`, expected, output)
+	}
+}
+
 func TestReplaceIframeURL(t *testing.T) {
 	input := `<iframe src="https://player.vimeo.com/video/123456?title=0&amp;byline=0"></iframe>`
 	expected := `<iframe src="https://player.vimeo.com/video/123456?title=0&amp;byline=0" sandbox="allow-scripts allow-same-origin allow-popups" loading="lazy"></iframe>`