feat: support for custom youtube embed URL

This commit is contained in:
Igor Rzegocki 2023-07-05 17:11:56 +02:00 committed by Frédéric Guillot
parent f286c3c1c9
commit 9b42d0e25e
8 changed files with 109 additions and 11 deletions

View file

@ -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) { func TestParseConfigDumpOutput(t *testing.T) {
os.Clearenv() os.Clearenv()

View file

@ -50,6 +50,7 @@ const (
defaultProxyMediaTypes = "image" defaultProxyMediaTypes = "image"
defaultProxyUrl = "" defaultProxyUrl = ""
defaultFetchYouTubeWatchTime = false defaultFetchYouTubeWatchTime = false
defaultYouTubeEmbedUrlOverride = "https://www.youtube-nocookie.com/embed/"
defaultCreateAdmin = false defaultCreateAdmin = false
defaultAdminUsername = "" defaultAdminUsername = ""
defaultAdminPassword = "" defaultAdminPassword = ""
@ -126,6 +127,7 @@ type Options struct {
proxyMediaTypes []string proxyMediaTypes []string
proxyUrl string proxyUrl string
fetchYouTubeWatchTime bool fetchYouTubeWatchTime bool
youTubeEmbedUrlOverride string
oauth2UserCreationAllowed bool oauth2UserCreationAllowed bool
oauth2ClientID string oauth2ClientID string
oauth2ClientSecret string oauth2ClientSecret string
@ -195,6 +197,7 @@ func NewOptions() *Options {
proxyMediaTypes: []string{defaultProxyMediaTypes}, proxyMediaTypes: []string{defaultProxyMediaTypes},
proxyUrl: defaultProxyUrl, proxyUrl: defaultProxyUrl,
fetchYouTubeWatchTime: defaultFetchYouTubeWatchTime, fetchYouTubeWatchTime: defaultFetchYouTubeWatchTime,
youTubeEmbedUrlOverride: defaultYouTubeEmbedUrlOverride,
oauth2UserCreationAllowed: defaultOAuth2UserCreation, oauth2UserCreationAllowed: defaultOAuth2UserCreation,
oauth2ClientID: defaultOAuth2ClientID, oauth2ClientID: defaultOAuth2ClientID,
oauth2ClientSecret: defaultOAuth2ClientSecret, oauth2ClientSecret: defaultOAuth2ClientSecret,
@ -428,6 +431,11 @@ func (o *Options) FetchYouTubeWatchTime() bool {
return o.fetchYouTubeWatchTime 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. // ProxyOption returns "none" to never proxy, "http-only" to proxy non-HTTPS, "all" to always proxy.
func (o *Options) ProxyOption() string { func (o *Options) ProxyOption() string {
return o.proxyOption return o.proxyOption
@ -558,20 +566,20 @@ func (o *Options) SortedOptions(redactSecret bool) []*Option {
"BATCH_SIZE": o.batchSize, "BATCH_SIZE": o.batchSize,
"CERT_DOMAIN": o.certDomain, "CERT_DOMAIN": o.certDomain,
"CERT_FILE": o.certFile, "CERT_FILE": o.certFile,
"CLEANUP_ARCHIVE_BATCH_SIZE": o.cleanupArchiveBatchSize,
"CLEANUP_ARCHIVE_READ_DAYS": o.cleanupArchiveReadDays, "CLEANUP_ARCHIVE_READ_DAYS": o.cleanupArchiveReadDays,
"CLEANUP_ARCHIVE_UNREAD_DAYS": o.cleanupArchiveUnreadDays, "CLEANUP_ARCHIVE_UNREAD_DAYS": o.cleanupArchiveUnreadDays,
"CLEANUP_ARCHIVE_BATCH_SIZE": o.cleanupArchiveBatchSize,
"CLEANUP_FREQUENCY_HOURS": o.cleanupFrequencyHours, "CLEANUP_FREQUENCY_HOURS": o.cleanupFrequencyHours,
"CLEANUP_REMOVE_SESSIONS_DAYS": o.cleanupRemoveSessionsDays, "CLEANUP_REMOVE_SESSIONS_DAYS": o.cleanupRemoveSessionsDays,
"CREATE_ADMIN": o.createAdmin, "CREATE_ADMIN": o.createAdmin,
"DATABASE_CONNECTION_LIFETIME": o.databaseConnectionLifetime,
"DATABASE_MAX_CONNS": o.databaseMaxConns, "DATABASE_MAX_CONNS": o.databaseMaxConns,
"DATABASE_MIN_CONNS": o.databaseMinConns, "DATABASE_MIN_CONNS": o.databaseMinConns,
"DATABASE_CONNECTION_LIFETIME": o.databaseConnectionLifetime,
"DATABASE_URL": redactSecretValue(o.databaseURL, redactSecret), "DATABASE_URL": redactSecretValue(o.databaseURL, redactSecret),
"DEBUG": o.debug, "DEBUG": o.debug,
"DISABLE_HSTS": !o.hsts, "DISABLE_HSTS": !o.hsts,
"DISABLE_SCHEDULER_SERVICE": !o.schedulerService,
"DISABLE_HTTP_SERVICE": !o.httpService, "DISABLE_HTTP_SERVICE": !o.httpService,
"DISABLE_SCHEDULER_SERVICE": !o.schedulerService,
"FETCH_YOUTUBE_WATCH_TIME": o.fetchYouTubeWatchTime, "FETCH_YOUTUBE_WATCH_TIME": o.fetchYouTubeWatchTime,
"HTTPS": o.HTTPS, "HTTPS": o.HTTPS,
"HTTP_CLIENT_MAX_BODY_SIZE": o.httpClientMaxBodySize, "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_CLIENT_USER_AGENT": o.httpClientUserAgent,
"HTTP_SERVER_TIMEOUT": o.httpServerTimeout, "HTTP_SERVER_TIMEOUT": o.httpServerTimeout,
"HTTP_SERVICE": o.httpService, "HTTP_SERVICE": o.httpService,
"KEY_FILE": o.certKeyFile,
"INVIDIOUS_INSTANCE": o.invidiousInstance, "INVIDIOUS_INSTANCE": o.invidiousInstance,
"KEY_FILE": o.certKeyFile,
"LISTEN_ADDR": o.listenAddr, "LISTEN_ADDR": o.listenAddr,
"LOG_DATE_TIME": o.logDateTime, "LOG_DATE_TIME": o.logDateTime,
"MAINTENANCE_MESSAGE": o.maintenanceMessage, "MAINTENANCE_MESSAGE": o.maintenanceMessage,
"MAINTENANCE_MODE": o.maintenanceMode, "MAINTENANCE_MODE": o.maintenanceMode,
"METRICS_ALLOWED_NETWORKS": strings.Join(o.metricsAllowedNetworks, ","), "METRICS_ALLOWED_NETWORKS": strings.Join(o.metricsAllowedNetworks, ","),
"METRICS_COLLECTOR": o.metricsCollector, "METRICS_COLLECTOR": o.metricsCollector,
"METRICS_PASSWORD": redactSecretValue(o.metricsPassword, redactSecret),
"METRICS_REFRESH_INTERVAL": o.metricsRefreshInterval, "METRICS_REFRESH_INTERVAL": o.metricsRefreshInterval,
"METRICS_USERNAME": o.metricsUsername, "METRICS_USERNAME": o.metricsUsername,
"METRICS_PASSWORD": redactSecretValue(o.metricsPassword, redactSecret),
"OAUTH2_CLIENT_ID": o.oauth2ClientID, "OAUTH2_CLIENT_ID": o.oauth2ClientID,
"OAUTH2_CLIENT_SECRET": redactSecretValue(o.oauth2ClientSecret, redactSecret), "OAUTH2_CLIENT_SECRET": redactSecretValue(o.oauth2ClientSecret, redactSecret),
"OAUTH2_OIDC_DISCOVERY_ENDPOINT": o.oauth2OidcDiscoveryEndpoint, "OAUTH2_OIDC_DISCOVERY_ENDPOINT": o.oauth2OidcDiscoveryEndpoint,
@ -602,9 +610,9 @@ func (o *Options) SortedOptions(redactSecret bool) []*Option {
"POLLING_PARSING_ERROR_LIMIT": o.pollingParsingErrorLimit, "POLLING_PARSING_ERROR_LIMIT": o.pollingParsingErrorLimit,
"POLLING_SCHEDULER": o.pollingScheduler, "POLLING_SCHEDULER": o.pollingScheduler,
"PROXY_HTTP_CLIENT_TIMEOUT": o.proxyHTTPClientTimeout, "PROXY_HTTP_CLIENT_TIMEOUT": o.proxyHTTPClientTimeout,
"PROXY_PRIVATE_KEY": redactSecretValue(string(o.proxyPrivateKey), redactSecret),
"PROXY_MEDIA_TYPES": o.proxyMediaTypes, "PROXY_MEDIA_TYPES": o.proxyMediaTypes,
"PROXY_OPTION": o.proxyOption, "PROXY_OPTION": o.proxyOption,
"PROXY_PRIVATE_KEY": redactSecretValue(string(o.proxyPrivateKey), redactSecret),
"PROXY_URL": o.proxyUrl, "PROXY_URL": o.proxyUrl,
"ROOT_URL": o.rootURL, "ROOT_URL": o.rootURL,
"RUN_MIGRATIONS": o.runMigrations, "RUN_MIGRATIONS": o.runMigrations,
@ -612,8 +620,9 @@ func (o *Options) SortedOptions(redactSecret bool) []*Option {
"SCHEDULER_ENTRY_FREQUENCY_MIN_INTERVAL": o.schedulerEntryFrequencyMinInterval, "SCHEDULER_ENTRY_FREQUENCY_MIN_INTERVAL": o.schedulerEntryFrequencyMinInterval,
"SCHEDULER_SERVICE": o.schedulerService, "SCHEDULER_SERVICE": o.schedulerService,
"SERVER_TIMING_HEADER": o.serverTimingHeader, "SERVER_TIMING_HEADER": o.serverTimingHeader,
"WORKER_POOL_SIZE": o.workerPoolSize,
"WATCHDOG": o.watchdog, "WATCHDOG": o.watchdog,
"WORKER_POOL_SIZE": o.workerPoolSize,
"YOUTUBE_EMBED_URL_OVERRIDE": o.youTubeEmbedUrlOverride,
} }
keys := make([]string, 0, len(keyValues)) keys := make([]string, 0, len(keyValues))

View file

@ -215,6 +215,8 @@ func (p *Parser) parseLines(lines []string) (err error) {
p.opts.metricsPassword = readSecretFile(value, defaultMetricsPassword) p.opts.metricsPassword = readSecretFile(value, defaultMetricsPassword)
case "FETCH_YOUTUBE_WATCH_TIME": case "FETCH_YOUTUBE_WATCH_TIME":
p.opts.fetchYouTubeWatchTime = parseBool(value, defaultFetchYouTubeWatchTime) p.opts.fetchYouTubeWatchTime = parseBool(value, defaultFetchYouTubeWatchTime)
case "YOUTUBE_EMBED_URL_OVERRIDE":
p.opts.youTubeEmbedUrlOverride = parseString(value, defaultYouTubeEmbedUrlOverride)
case "WATCHDOG": case "WATCHDOG":
p.opts.watchdog = parseBool(value, defaultWatchdog) p.opts.watchdog = parseBool(value, defaultWatchdog)
case "INVIDIOUS_INSTANCE": case "INVIDIOUS_INSTANCE":

View file

@ -124,6 +124,11 @@ use it as a reading time\&.
.br .br
Disabled by default\&. Disabled by default\&.
.TP .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 .B SERVER_TIMING_HEADER
Set the value to 1 to enable server-timing headers\&. Set the value to 1 to enable server-timing headers\&.
.br .br

View file

@ -208,7 +208,7 @@ func addYoutubeVideo(entryURL, entryContent string) string {
matches := youtubeRegex.FindStringSubmatch(entryURL) matches := youtubeRegex.FindStringSubmatch(entryURL)
if len(matches) == 2 { 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 video + `<br>` + entryContent
} }
return entryContent return entryContent
@ -232,7 +232,8 @@ func addYoutubeVideoFromId(entryContent string) string {
sb := strings.Builder{} sb := strings.Builder{}
for _, match := range matches { for _, match := range matches {
if len(match) == 2 { 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(match[1])
sb.WriteString(`" allowfullscreen></iframe><br>`) sb.WriteString(`" allowfullscreen></iframe><br>`)
} }

View file

@ -4,10 +4,12 @@
package rewrite // import "miniflux.app/reader/rewrite" package rewrite // import "miniflux.app/reader/rewrite"
import ( import (
"os"
"reflect" "reflect"
"strings" "strings"
"testing" "testing"
"miniflux.app/config"
"miniflux.app/model" "miniflux.app/model"
) )
@ -63,6 +65,8 @@ func TestRewriteWithNoMatchingRule(t *testing.T) {
} }
func TestRewriteWithYoutubeLink(t *testing.T) { func TestRewriteWithYoutubeLink(t *testing.T) {
config.Opts = config.NewOptions()
controlEntry := &model.Entry{ controlEntry := &model.Entry{
Title: `A title`, Title: `A title`,
Content: `<iframe width="650" height="350" frameborder="0" src="https://www.youtube-nocookie.com/embed/1234" allowfullscreen></iframe><br>Video Description`, 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) { func TestRewriteWithInexistingCustomRule(t *testing.T) {
controlEntry := &model.Entry{ controlEntry := &model.Entry{
Title: `A title`, Title: `A title`,

View file

@ -441,7 +441,7 @@ func inList(needle string, haystack []string) bool {
func rewriteIframeURL(link string) string { func rewriteIframeURL(link string) string {
matches := youtubeEmbedRegex.FindStringSubmatch(link) matches := youtubeEmbedRegex.FindStringSubmatch(link)
if len(matches) == 2 { if len(matches) == 2 {
return `https://www.youtube-nocookie.com/embed/` + matches[1] return config.Opts.YouTubeEmbedUrlOverride() + matches[1]
} }
return link return link

View file

@ -3,7 +3,18 @@
package sanitizer // import "miniflux.app/reader/sanitizer" 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) { 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>` 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) { func TestReplaceIframeURL(t *testing.T) {
input := `<iframe src="https://player.vimeo.com/video/123456?title=0&amp;byline=0"></iframe>` 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>` 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>`