Compare commits

...

7 Commits

Author SHA1 Message Date
Shizun Ge dc16e45e42
Merge a78294328b into 93bc9ce24d 2024-04-27 07:30:32 -07:00
Ztec 93bc9ce24d add seek and speed controls to media player
When listening to podcast, it is usual to want to speed up the playback.
https://github.com/miniflux/v2/pull/2521 was addressing the need globally, this PR
allow to address it for just the current open enclosure media. (no save) Some Browser
already include this control directly, but firefox does not (directly anyway).

Also, it is often useful to be able to skip chunk of a podcast, to skip commercials
for example, or get back a bit because we couldn't hear the last part. I added rudimentary
seek controls with the usual +/-10 and 30 seconds chuck size. This is pretty handy when podcast
are very long and using the seek bar is way too tricky to just skip 30s.

As always, I'm French and could only provide English and French translation for the few
text I added in the locale/translations files. Any help is welcome.

Tested mostly on Firefox (121.0) and quickly on Vivaldi(6.5.3206.53), chrome based.

Fixes: #1845 #1846
2024-04-26 13:44:26 -07:00
dependabot[bot] 9233568da3 Bump github.com/tdewolff/minify/v2 from 2.20.19 to 2.20.20
Bumps [github.com/tdewolff/minify/v2](https://github.com/tdewolff/minify) from 2.20.19 to 2.20.20.
- [Release notes](https://github.com/tdewolff/minify/releases)
- [Commits](https://github.com/tdewolff/minify/compare/v2.20.19...v2.20.20)

---
updated-dependencies:
- dependency-name: github.com/tdewolff/minify/v2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-04-24 19:09:18 -07:00
Frédéric Guillot fb075b60b5 reader/processor: minifier is breaking HTML entry content 2024-04-23 20:31:52 -07:00
Shizun Ge a78294328b Reschedule next check date for rate limited feeds 2024-02-17 14:23:02 -08:00
Shizun Ge dc3426319d calling store.UpdateFeed and store.UpdateFeedError in a defer function 2024-02-17 14:22:35 -08:00
Shizun Ge 7de6b0fc18 use named return value localizedError in handler.go 2024-02-17 14:22:35 -08:00
31 changed files with 409 additions and 88 deletions

4
go.mod
View File

@ -11,7 +11,7 @@ require (
github.com/gorilla/mux v1.8.1
github.com/lib/pq v1.10.9
github.com/prometheus/client_golang v1.19.0
github.com/tdewolff/minify/v2 v2.20.19
github.com/tdewolff/minify/v2 v2.20.20
github.com/yuin/goldmark v1.7.1
golang.org/x/crypto v0.22.0
golang.org/x/net v0.24.0
@ -38,7 +38,7 @@ require (
github.com/prometheus/client_model v0.5.0 // indirect
github.com/prometheus/common v0.48.0 // indirect
github.com/prometheus/procfs v0.12.0 // indirect
github.com/tdewolff/parse/v2 v2.7.12 // indirect
github.com/tdewolff/parse/v2 v2.7.13 // indirect
github.com/x448/float16 v0.8.4 // indirect
golang.org/x/sys v0.19.0 // indirect
google.golang.org/protobuf v1.33.0 // indirect

8
go.sum
View File

@ -48,10 +48,10 @@ github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k
github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/tdewolff/minify/v2 v2.20.19 h1:tX0SR0LUrIqGoLjXnkIzRSIbKJ7PaNnSENLD4CyH6Xo=
github.com/tdewolff/minify/v2 v2.20.19/go.mod h1:ulkFoeAVWMLEyjuDz1ZIWOA31g5aWOawCFRp9R/MudM=
github.com/tdewolff/parse/v2 v2.7.12 h1:tgavkHc2ZDEQVKy1oWxwIyh5bP4F5fEh/JmBwPP/3LQ=
github.com/tdewolff/parse/v2 v2.7.12/go.mod h1:3FbJWZp3XT9OWVN3Hmfp0p/a08v4h8J9W1aghka0soA=
github.com/tdewolff/minify/v2 v2.20.20 h1:vhULb+VsW2twkplgsawAoUY957efb+EdiZ7zu5fUhhk=
github.com/tdewolff/minify/v2 v2.20.20/go.mod h1:GYaLXFpIIwsX99apQHXfGdISUdlA98wmaoWxjT9C37k=
github.com/tdewolff/parse/v2 v2.7.13 h1:iSiwOUkCYLNfapHoqdLcqZVgvQ0jrsao8YYKP/UJYTI=
github.com/tdewolff/parse/v2 v2.7.13/go.mod h1:3FbJWZp3XT9OWVN3Hmfp0p/a08v4h8J9W1aghka0soA=
github.com/tdewolff/test v1.0.11-0.20231101010635-f1265d231d52/go.mod h1:6DAvZliBAAnD7rhVgwaM7DE5/d9NMOAJ09SqYqeK4QE=
github.com/tdewolff/test v1.0.11-0.20240106005702-7de5f7df4739 h1:IkjBCtQOOjIn03u/dMQK9g+Iw9ewps4mCl1nB8Sscbo=
github.com/tdewolff/test v1.0.11-0.20240106005702-7de5f7df4739/go.mod h1:XPuWBzvdUzhCuxWO1ojpXsyzsA5bFoS3tO/Q3kFuTG8=

View File

@ -529,5 +529,14 @@
"error.unable_to_detect_rssbridge": "Abonnement kann nicht durch RSS-Bridge erkannt werden: %v.",
"error.feed_format_not_detected": "Das Format des Abonnements kann nicht erkannt werden: %v.",
"form.prefs.label.media_playback_rate": "Wiedergabegeschwindigkeit von Audio/Video",
"error.settings_media_playback_rate_range": "Die Wiedergabegeschwindigkeit liegt außerhalb des Bereichs"
"error.settings_media_playback_rate_range": "Die Wiedergabegeschwindigkeit liegt außerhalb des Bereichs",
"enclosure_media_controls.seek" : "Seek:",
"enclosure_media_controls.seek.title" : "Seek %s seconds",
"enclosure_media_controls.speed" : "Speed:",
"enclosure_media_controls.speed.faster" : "Faster",
"enclosure_media_controls.speed.faster.title" : "Faster by %sx",
"enclosure_media_controls.speed.slower" : "Slower",
"enclosure_media_controls.speed.slower.title" : "Slower by %sx",
"enclosure_media_controls.speed.reset" : "Reset",
"enclosure_media_controls.speed.reset.title" : "Reset speed to 1x"
}

View File

@ -529,5 +529,14 @@
"error.unable_to_detect_rssbridge": "Unable to detect feed using RSS-Bridge: %v.",
"error.feed_format_not_detected": "Unable to detect feed format: %v.",
"form.prefs.label.media_playback_rate": "Ταχύτητα αναπαραγωγής του ήχου/βίντεο",
"error.settings_media_playback_rate_range": "Η ταχύτητα αναπαραγωγής είναι εκτός εύρους"
"error.settings_media_playback_rate_range": "Η ταχύτητα αναπαραγωγής είναι εκτός εύρους",
"enclosure_media_controls.seek" : "Seek:",
"enclosure_media_controls.seek.title" : "Seek %s seconds",
"enclosure_media_controls.speed" : "Speed:",
"enclosure_media_controls.speed.faster" : "Faster",
"enclosure_media_controls.speed.faster.title" : "Faster by %sx",
"enclosure_media_controls.speed.slower" : "Slower",
"enclosure_media_controls.speed.slower.title" : "Slower by %sx",
"enclosure_media_controls.speed.reset" : "Reset",
"enclosure_media_controls.speed.reset.title" : "Reset speed to 1x"
}

View File

@ -529,5 +529,14 @@
"error.unable_to_detect_rssbridge": "Unable to detect feed using RSS-Bridge: %v.",
"error.feed_format_not_detected": "Unable to detect feed format: %v.",
"form.prefs.label.media_playback_rate": "Playback speed of the audio/video",
"error.settings_media_playback_rate_range": "Playback speed is out of range"
"error.settings_media_playback_rate_range": "Playback speed is out of range",
"enclosure_media_controls.seek" : "Seek:",
"enclosure_media_controls.seek.title" : "Seek %s seconds",
"enclosure_media_controls.speed" : "Speed:",
"enclosure_media_controls.speed.faster" : "Faster",
"enclosure_media_controls.speed.faster.title" : "Faster by %sx",
"enclosure_media_controls.speed.slower" : "Slower",
"enclosure_media_controls.speed.slower.title" : "Slower by %sx",
"enclosure_media_controls.speed.reset" : "Reset",
"enclosure_media_controls.speed.reset.title" : "Reset speed to 1x"
}

View File

@ -529,5 +529,14 @@
"error.unable_to_detect_rssbridge": "Unable to detect feed using RSS-Bridge: %v.",
"error.feed_format_not_detected": "Unable to detect feed format: %v.",
"form.prefs.label.media_playback_rate": "Velocidad de reproducción del audio/vídeo",
"error.settings_media_playback_rate_range": "La velocidad de reproducción está fuera de rango"
"error.settings_media_playback_rate_range": "La velocidad de reproducción está fuera de rango",
"enclosure_media_controls.seek" : "Seek:",
"enclosure_media_controls.seek.title" : "Seek %s seconds",
"enclosure_media_controls.speed" : "Speed:",
"enclosure_media_controls.speed.faster" : "Faster",
"enclosure_media_controls.speed.faster.title" : "Faster by %sx",
"enclosure_media_controls.speed.slower" : "Slower",
"enclosure_media_controls.speed.slower.title" : "Slower by %sx",
"enclosure_media_controls.speed.reset" : "Reset",
"enclosure_media_controls.speed.reset.title" : "Reset speed to 1x"
}

View File

@ -529,5 +529,14 @@
"error.unable_to_detect_rssbridge": "Unable to detect feed using RSS-Bridge: %v.",
"error.feed_format_not_detected": "Unable to detect feed format: %v.",
"form.prefs.label.media_playback_rate": "Äänen/videon toistonopeus",
"error.settings_media_playback_rate_range": "Toistonopeus on alueen ulkopuolella"
"error.settings_media_playback_rate_range": "Toistonopeus on alueen ulkopuolella",
"enclosure_media_controls.seek" : "Seek:",
"enclosure_media_controls.seek.title" : "Seek %s seconds",
"enclosure_media_controls.speed" : "Speed:",
"enclosure_media_controls.speed.faster" : "Faster",
"enclosure_media_controls.speed.faster.title" : "Faster by %sx",
"enclosure_media_controls.speed.slower" : "Slower",
"enclosure_media_controls.speed.slower.title" : "Slower by %sx",
"enclosure_media_controls.speed.reset" : "Reset",
"enclosure_media_controls.speed.reset.title" : "Reset speed to 1x"
}

View File

@ -529,5 +529,14 @@
"error.unable_to_detect_rssbridge": "Impossible de détecter un flux RSS en utilisant RSS-Bridge: %v.",
"error.feed_format_not_detected": "Impossible de détecter le format du flux : %v.",
"form.prefs.label.media_playback_rate": "Vitesse de lecture de l'audio/vidéo",
"error.settings_media_playback_rate_range": "La vitesse de lecture est hors limites"
"error.settings_media_playback_rate_range": "La vitesse de lecture est hors limites",
"enclosure_media_controls.seek" : "Avancer/Reculer :",
"enclosure_media_controls.seek.title" : "Avancer/Reculer de %s seconds",
"enclosure_media_controls.speed" : "Vitesse :",
"enclosure_media_controls.speed.faster" : "Accélérer",
"enclosure_media_controls.speed.faster.title" : "Accélérer de %sx",
"enclosure_media_controls.speed.slower" : "Ralentir",
"enclosure_media_controls.speed.slower.title" : "Ralentir de %sx",
"enclosure_media_controls.speed.reset" : "Réinitialiser",
"enclosure_media_controls.speed.reset.title" : "Réinitialiser la vitesse de lecture à 1x"
}

View File

@ -529,5 +529,14 @@
"error.unable_to_detect_rssbridge": "Unable to detect feed using RSS-Bridge: %v.",
"error.feed_format_not_detected": "Unable to detect feed format: %v.",
"form.prefs.label.media_playback_rate": "ऑडियो/वीडियो की प्लेबैक गति",
"error.settings_media_playback_rate_range": "प्लेबैक गति सीमा से बाहर है"
"error.settings_media_playback_rate_range": "प्लेबैक गति सीमा से बाहर है",
"enclosure_media_controls.seek" : "Seek:",
"enclosure_media_controls.seek.title" : "Seek %s seconds",
"enclosure_media_controls.speed" : "Speed:",
"enclosure_media_controls.speed.faster" : "Faster",
"enclosure_media_controls.speed.faster.title" : "Faster by %sx",
"enclosure_media_controls.speed.slower" : "Slower",
"enclosure_media_controls.speed.slower.title" : "Slower by %sx",
"enclosure_media_controls.speed.reset" : "Reset",
"enclosure_media_controls.speed.reset.title" : "Reset speed to 1x"
}

View File

@ -512,5 +512,14 @@
"error.unable_to_detect_rssbridge": "Unable to detect feed using RSS-Bridge: %v.",
"error.feed_format_not_detected": "Unable to detect feed format: %v.",
"form.prefs.label.media_playback_rate": "Kecepatan pemutaran audio/video",
"error.settings_media_playback_rate_range": "Kecepatan pemutaran di luar jangkauan"
"error.settings_media_playback_rate_range": "Kecepatan pemutaran di luar jangkauan",
"enclosure_media_controls.seek" : "Seek:",
"enclosure_media_controls.seek.title" : "Seek %s seconds",
"enclosure_media_controls.speed" : "Speed:",
"enclosure_media_controls.speed.faster" : "Faster",
"enclosure_media_controls.speed.faster.title" : "Faster by %sx",
"enclosure_media_controls.speed.slower" : "Slower",
"enclosure_media_controls.speed.slower.title" : "Slower by %sx",
"enclosure_media_controls.speed.reset" : "Reset",
"enclosure_media_controls.speed.reset.title" : "Reset speed to 1x"
}

View File

@ -529,5 +529,14 @@
"error.unable_to_detect_rssbridge": "Unable to detect feed using RSS-Bridge: %v.",
"error.feed_format_not_detected": "Unable to detect feed format: %v.",
"form.prefs.label.media_playback_rate": "Velocità di riproduzione dell'audio/video",
"error.settings_media_playback_rate_range": "La velocità di riproduzione non rientra nell'intervallo"
"error.settings_media_playback_rate_range": "La velocità di riproduzione non rientra nell'intervallo",
"enclosure_media_controls.seek" : "Seek:",
"enclosure_media_controls.seek.title" : "Seek %s seconds",
"enclosure_media_controls.speed" : "Speed:",
"enclosure_media_controls.speed.faster" : "Faster",
"enclosure_media_controls.speed.faster.title" : "Faster by %sx",
"enclosure_media_controls.speed.slower" : "Slower",
"enclosure_media_controls.speed.slower.title" : "Slower by %sx",
"enclosure_media_controls.speed.reset" : "Reset",
"enclosure_media_controls.speed.reset.title" : "Reset speed to 1x"
}

View File

@ -512,5 +512,14 @@
"error.unable_to_detect_rssbridge": "Unable to detect feed using RSS-Bridge: %v.",
"error.feed_format_not_detected": "Unable to detect feed format: %v.",
"form.prefs.label.media_playback_rate": "オーディオ/ビデオの再生速度",
"error.settings_media_playback_rate_range": "再生速度が範囲外"
"error.settings_media_playback_rate_range": "再生速度が範囲外",
"enclosure_media_controls.seek" : "Seek:",
"enclosure_media_controls.seek.title" : "Seek %s seconds",
"enclosure_media_controls.speed" : "Speed:",
"enclosure_media_controls.speed.faster" : "Faster",
"enclosure_media_controls.speed.faster.title" : "Faster by %sx",
"enclosure_media_controls.speed.slower" : "Slower",
"enclosure_media_controls.speed.slower.title" : "Slower by %sx",
"enclosure_media_controls.speed.reset" : "Reset",
"enclosure_media_controls.speed.reset.title" : "Reset speed to 1x"
}

View File

@ -529,5 +529,14 @@
"error.unable_to_detect_rssbridge": "Unable to detect feed using RSS-Bridge: %v.",
"error.feed_format_not_detected": "Unable to detect feed format: %v.",
"form.prefs.label.media_playback_rate": "Afspeelsnelheid van de audio/video",
"error.settings_media_playback_rate_range": "Afspeelsnelheid is buiten bereik"
"error.settings_media_playback_rate_range": "Afspeelsnelheid is buiten bereik",
"enclosure_media_controls.seek" : "Seek:",
"enclosure_media_controls.seek.title" : "Seek %s seconds",
"enclosure_media_controls.speed" : "Speed:",
"enclosure_media_controls.speed.faster" : "Faster",
"enclosure_media_controls.speed.faster.title" : "Faster by %sx",
"enclosure_media_controls.speed.slower" : "Slower",
"enclosure_media_controls.speed.slower.title" : "Slower by %sx",
"enclosure_media_controls.speed.reset" : "Reset",
"enclosure_media_controls.speed.reset.title" : "Reset speed to 1x"
}

View File

@ -546,5 +546,14 @@
"error.unable_to_detect_rssbridge": "Unable to detect feed using RSS-Bridge: %v.",
"error.feed_format_not_detected": "Unable to detect feed format: %v.",
"form.prefs.label.media_playback_rate": "Prędkość odtwarzania audio/wideo",
"error.settings_media_playback_rate_range": "Prędkość odtwarzania jest poza zakresem"
"error.settings_media_playback_rate_range": "Prędkość odtwarzania jest poza zakresem",
"enclosure_media_controls.seek" : "Seek:",
"enclosure_media_controls.seek.title" : "Seek %s seconds",
"enclosure_media_controls.speed" : "Speed:",
"enclosure_media_controls.speed.faster" : "Faster",
"enclosure_media_controls.speed.faster.title" : "Faster by %sx",
"enclosure_media_controls.speed.slower" : "Slower",
"enclosure_media_controls.speed.slower.title" : "Slower by %sx",
"enclosure_media_controls.speed.reset" : "Reset",
"enclosure_media_controls.speed.reset.title" : "Reset speed to 1x"
}

View File

@ -529,5 +529,14 @@
"error.unable_to_detect_rssbridge": "Unable to detect feed using RSS-Bridge: %v.",
"error.feed_format_not_detected": "Unable to detect feed format: %v.",
"form.prefs.label.media_playback_rate": "Velocidade de reprodução do áudio/vídeo",
"error.settings_media_playback_rate_range": "A velocidade de reprodução está fora do intervalo"
"error.settings_media_playback_rate_range": "A velocidade de reprodução está fora do intervalo",
"enclosure_media_controls.seek" : "Seek:",
"enclosure_media_controls.seek.title" : "Seek %s seconds",
"enclosure_media_controls.speed" : "Speed:",
"enclosure_media_controls.speed.faster" : "Faster",
"enclosure_media_controls.speed.faster.title" : "Faster by %sx",
"enclosure_media_controls.speed.slower" : "Slower",
"enclosure_media_controls.speed.slower.title" : "Slower by %sx",
"enclosure_media_controls.speed.reset" : "Reset",
"enclosure_media_controls.speed.reset.title" : "Reset speed to 1x"
}

View File

@ -546,5 +546,14 @@
"error.unable_to_detect_rssbridge": "Unable to detect feed using RSS-Bridge: %v.",
"error.feed_format_not_detected": "Unable to detect feed format: %v.",
"form.prefs.label.media_playback_rate": "Скорость воспроизведения аудио/видео",
"error.settings_media_playback_rate_range": "Скорость воспроизведения выходит за пределы диапазона"
"error.settings_media_playback_rate_range": "Скорость воспроизведения выходит за пределы диапазона",
"enclosure_media_controls.seek" : "Seek:",
"enclosure_media_controls.seek.title" : "Seek %s seconds",
"enclosure_media_controls.speed" : "Speed:",
"enclosure_media_controls.speed.faster" : "Faster",
"enclosure_media_controls.speed.faster.title" : "Faster by %sx",
"enclosure_media_controls.speed.slower" : "Slower",
"enclosure_media_controls.speed.slower.title" : "Slower by %sx",
"enclosure_media_controls.speed.reset" : "Reset",
"enclosure_media_controls.speed.reset.title" : "Reset speed to 1x"
}

View File

@ -496,5 +496,14 @@
"time_elapsed.years": ["%d yıl önce", "%d yıl önce"],
"time_elapsed.yesterday": "dün",
"tooltip.keyboard_shortcuts": "Klavye Kısayolu: %s",
"tooltip.logged_user": "%s olarak giriş yapıldı"
"tooltip.logged_user": "%s olarak giriş yapıldı",
"enclosure_media_controls.seek" : "Seek:",
"enclosure_media_controls.seek.title" : "Seek %s seconds",
"enclosure_media_controls.speed" : "Speed:",
"enclosure_media_controls.speed.faster" : "Faster",
"enclosure_media_controls.speed.faster.title" : "Faster by %sx",
"enclosure_media_controls.speed.slower" : "Slower",
"enclosure_media_controls.speed.slower.title" : "Slower by %sx",
"enclosure_media_controls.speed.reset" : "Reset",
"enclosure_media_controls.speed.reset.title" : "Reset speed to 1x"
}

View File

@ -546,5 +546,14 @@
"error.unable_to_detect_rssbridge": "Unable to detect feed using RSS-Bridge: %v.",
"error.feed_format_not_detected": "Unable to detect feed format: %v.",
"form.prefs.label.media_playback_rate": "Швидкість відтворення аудіо/відео",
"error.settings_media_playback_rate_range": "Швидкість відтворення виходить за межі діапазону"
"error.settings_media_playback_rate_range": "Швидкість відтворення виходить за межі діапазону",
"enclosure_media_controls.seek" : "Seek:",
"enclosure_media_controls.seek.title" : "Seek %s seconds",
"enclosure_media_controls.speed" : "Speed:",
"enclosure_media_controls.speed.faster" : "Faster",
"enclosure_media_controls.speed.faster.title" : "Faster by %sx",
"enclosure_media_controls.speed.slower" : "Slower",
"enclosure_media_controls.speed.slower.title" : "Slower by %sx",
"enclosure_media_controls.speed.reset" : "Reset",
"enclosure_media_controls.speed.reset.title" : "Reset speed to 1x"
}

View File

@ -512,5 +512,14 @@
"error.unable_to_detect_rssbridge": "Unable to detect feed using RSS-Bridge: %v.",
"error.feed_format_not_detected": "Unable to detect feed format: %v.",
"form.prefs.label.media_playback_rate": "音频/视频的播放速度",
"error.settings_media_playback_rate_range": "播放速度超出范围"
"error.settings_media_playback_rate_range": "播放速度超出范围",
"enclosure_media_controls.seek" : "Seek:",
"enclosure_media_controls.seek.title" : "Seek %s seconds",
"enclosure_media_controls.speed" : "Speed:",
"enclosure_media_controls.speed.faster" : "Faster",
"enclosure_media_controls.speed.faster.title" : "Faster by %sx",
"enclosure_media_controls.speed.slower" : "Slower",
"enclosure_media_controls.speed.slower.title" : "Slower by %sx",
"enclosure_media_controls.speed.reset" : "Reset",
"enclosure_media_controls.speed.reset.title" : "Reset speed to 1x"
}

View File

@ -512,5 +512,14 @@
"error.unable_to_detect_rssbridge": "Unable to detect feed using RSS-Bridge: %v.",
"error.feed_format_not_detected": "Unable to detect feed format: %v.",
"form.prefs.label.media_playback_rate": "音訊/視訊的播放速度",
"error.settings_media_playback_rate_range": "播放速度超出範圍"
"error.settings_media_playback_rate_range": "播放速度超出範圍",
"enclosure_media_controls.seek" : "Seek:",
"enclosure_media_controls.seek.title" : "Seek %s seconds",
"enclosure_media_controls.speed" : "Speed:",
"enclosure_media_controls.speed.faster" : "Faster",
"enclosure_media_controls.speed.faster.title" : "Faster by %sx",
"enclosure_media_controls.speed.slower" : "Slower",
"enclosure_media_controls.speed.slower.title" : "Slower by %sx",
"enclosure_media_controls.speed.reset" : "Reset",
"enclosure_media_controls.speed.reset.title" : "Reset speed to 1x"
}

View File

@ -108,7 +108,7 @@ func (f *Feed) CheckedNow() {
}
// ScheduleNextCheck set "next_check_at" of a feed based on the scheduler selected from the configuration.
func (f *Feed) ScheduleNextCheck(weeklyCount int, newTTL int) {
func (f *Feed) ScheduleNextCheck(weeklyCount int, newTTL int, rateLimited bool) {
f.TTL = newTTL
// Default to the global config Polling Frequency.
var intervalMinutes int
@ -124,6 +124,9 @@ func (f *Feed) ScheduleNextCheck(weeklyCount int, newTTL int) {
default:
intervalMinutes = config.Opts.SchedulerRoundRobinMinInterval()
}
if rateLimited {
intervalMinutes += (12 * 60)
}
// If the feed has a TTL defined, we use it to make sure we don't check it too often.
if newTTL > intervalMinutes && newTTL > 0 {
intervalMinutes = newTTL

View File

@ -15,6 +15,7 @@ import (
const (
largeWeeklyCount = 10080
noNewTTL = 0
noRateLimited = false
)
func TestFeedCategorySetter(t *testing.T) {
@ -89,7 +90,7 @@ func TestFeedScheduleNextCheckDefault(t *testing.T) {
timeBefore := time.Now()
feed := &Feed{}
weeklyCount := 10
feed.ScheduleNextCheck(weeklyCount, noNewTTL)
feed.ScheduleNextCheck(weeklyCount, noNewTTL, noRateLimited)
if feed.NextCheckAt.IsZero() {
t.Error(`The next_check_at must be set`)
@ -115,7 +116,7 @@ func TestFeedScheduleNextCheckRoundRobinMinInterval(t *testing.T) {
timeBefore := time.Now()
feed := &Feed{}
weeklyCount := 100
feed.ScheduleNextCheck(weeklyCount, noNewTTL)
feed.ScheduleNextCheck(weeklyCount, noNewTTL, noRateLimited)
if feed.NextCheckAt.IsZero() {
t.Error(`The next_check_at must be set`)
@ -144,7 +145,7 @@ func TestFeedScheduleNextCheckEntryFrequencyMaxInterval(t *testing.T) {
feed := &Feed{}
// Use a very small weekly count to trigger the max interval
weeklyCount := 1
feed.ScheduleNextCheck(weeklyCount, noNewTTL)
feed.ScheduleNextCheck(weeklyCount, noNewTTL, noRateLimited)
if feed.NextCheckAt.IsZero() {
t.Error(`The next_check_at must be set`)
@ -173,7 +174,7 @@ func TestFeedScheduleNextCheckEntryFrequencyMaxIntervalZeroWeeklyCount(t *testin
feed := &Feed{}
// Use a very small weekly count to trigger the max interval
weeklyCount := 0
feed.ScheduleNextCheck(weeklyCount, noNewTTL)
feed.ScheduleNextCheck(weeklyCount, noNewTTL, noRateLimited)
if feed.NextCheckAt.IsZero() {
t.Error(`The next_check_at must be set`)
@ -202,7 +203,7 @@ func TestFeedScheduleNextCheckEntryFrequencyMinInterval(t *testing.T) {
feed := &Feed{}
// Use a very large weekly count to trigger the min interval
weeklyCount := largeWeeklyCount
feed.ScheduleNextCheck(weeklyCount, noNewTTL)
feed.ScheduleNextCheck(weeklyCount, noNewTTL, noRateLimited)
if feed.NextCheckAt.IsZero() {
t.Error(`The next_check_at must be set`)
@ -228,7 +229,7 @@ func TestFeedScheduleNextCheckEntryFrequencyFactor(t *testing.T) {
timeBefore := time.Now()
feed := &Feed{}
weeklyCount := 7
feed.ScheduleNextCheck(weeklyCount, noNewTTL)
feed.ScheduleNextCheck(weeklyCount, noNewTTL, noRateLimited)
if feed.NextCheckAt.IsZero() {
t.Error(`The next_check_at must be set`)
@ -260,7 +261,7 @@ func TestFeedScheduleNextCheckEntryFrequencySmallNewTTL(t *testing.T) {
weeklyCount := largeWeeklyCount
// TTL is smaller than minInterval.
newTTL := minInterval / 2
feed.ScheduleNextCheck(weeklyCount, newTTL)
feed.ScheduleNextCheck(weeklyCount, newTTL, noRateLimited)
if feed.NextCheckAt.IsZero() {
t.Error(`The next_check_at must be set`)
@ -296,7 +297,7 @@ func TestFeedScheduleNextCheckEntryFrequencyLargeNewTTL(t *testing.T) {
weeklyCount := largeWeeklyCount
// TTL is larger than minInterval.
newTTL := minInterval * 2
feed.ScheduleNextCheck(weeklyCount, newTTL)
feed.ScheduleNextCheck(weeklyCount, newTTL, noRateLimited)
if feed.NextCheckAt.IsZero() {
t.Error(`The next_check_at must be set`)
@ -309,3 +310,25 @@ func TestFeedScheduleNextCheckEntryFrequencyLargeNewTTL(t *testing.T) {
t.Error(`The next_check_at should be after timeBefore + entry frequency min interval`)
}
}
func TestFeedScheduleNextCheckRateLimited(t *testing.T) {
var err error
parser := config.NewParser()
config.Opts, err = parser.ParseEnvironmentVariables()
if err != nil {
t.Fatalf(`Parsing failure: %v`, err)
}
feed := &Feed{}
weeklyCount := 10
rateLimited := true
feed.ScheduleNextCheck(weeklyCount, noNewTTL, rateLimited)
if feed.NextCheckAt.IsZero() {
t.Error(`The next_check_at must be set`)
}
if feed.NextCheckAt.Before(time.Now().Add(time.Minute * time.Duration(60*12))) {
t.Error(`The next_check_at should not be before the now + 12 hours`)
}
}

View File

@ -67,6 +67,10 @@ func (r *ResponseHandler) IsModified(lastEtagValue, lastModifiedValue string) bo
return true
}
func (r *ResponseHandler) IsRateLimited() bool {
return r.httpResponse.StatusCode == http.StatusTooManyRequests
}
func (r *ResponseHandler) Close() {
if r.httpResponse != nil && r.httpResponse.Body != nil && r.clientErr == nil {
r.httpResponse.Body.Close()

View File

@ -198,12 +198,13 @@ func CreateFeed(store *storage.Storage, userID int64, feedCreationRequest *model
}
// RefreshFeed refreshes a feed.
func RefreshFeed(store *storage.Storage, userID, feedID int64, forceRefresh bool) *locale.LocalizedErrorWrapper {
func RefreshFeed(store *storage.Storage, userID, feedID int64, forceRefresh bool) (localizedError *locale.LocalizedErrorWrapper) {
slog.Debug("Begin feed refresh process",
slog.Int64("user_id", userID),
slog.Int64("feed_id", feedID),
slog.Bool("force_refresh", forceRefresh),
)
localizedError = nil
user, storeErr := store.UserByID(userID)
if storeErr != nil {
@ -221,6 +222,7 @@ func RefreshFeed(store *storage.Storage, userID, feedID int64, forceRefresh bool
weeklyEntryCount := 0
newTTL := 0
rateLimited := false
if config.Opts.PollingScheduler() == model.SchedulerEntryFrequency {
var weeklyCountErr error
weeklyEntryCount, weeklyCountErr = store.WeeklyFeedEntryCount(userID, feedID)
@ -230,7 +232,31 @@ func RefreshFeed(store *storage.Storage, userID, feedID int64, forceRefresh bool
}
originalFeed.CheckedNow()
originalFeed.ScheduleNextCheck(weeklyEntryCount, newTTL)
// Commit the result to the database at the end of this function.
// If we met an error before entering the defer function, localizedError would not be nil.
defer func() {
originalFeed.ScheduleNextCheck(weeklyEntryCount, newTTL, rateLimited)
slog.Debug("Updated next check date",
slog.Int64("user_id", userID),
slog.Int64("feed_id", feedID),
slog.Int("weeklyEntryCount", weeklyEntryCount),
slog.Int("ttl", newTTL),
slog.Bool("rateLimited", rateLimited),
slog.Time("new_next_check_at", originalFeed.NextCheckAt),
)
if localizedError == nil {
// We have not encountered any error before entering this delay function.
originalFeed.ResetErrorCounter()
if storeErr := store.UpdateFeed(originalFeed); storeErr != nil {
// Update the return value when there is an error.
localizedError = locale.NewLocalizedErrorWrapper(storeErr, "error.database_error", storeErr)
}
}
if localizedError != nil {
originalFeed.WithTranslatedErrorMessage(localizedError.Translate(user.Language))
store.UpdateFeedError(originalFeed)
}
}()
requestBuilder := fetcher.NewRequestBuilder()
requestBuilder.WithUsernameAndPassword(originalFeed.Username, originalFeed.Password)
@ -251,17 +277,18 @@ func RefreshFeed(store *storage.Storage, userID, feedID int64, forceRefresh bool
responseHandler := fetcher.NewResponseHandler(requestBuilder.ExecuteRequest(originalFeed.FeedURL))
defer responseHandler.Close()
if localizedError := responseHandler.LocalizedError(); localizedError != nil {
rateLimited = responseHandler.IsRateLimited()
if rateLimited {
slog.Warn("Feed is rate limited (429 status code)", slog.String("feed_url", originalFeed.FeedURL))
}
if localizedError = responseHandler.LocalizedError(); localizedError != nil {
slog.Warn("Unable to fetch feed", slog.String("feed_url", originalFeed.FeedURL), slog.Any("error", localizedError.Error()))
originalFeed.WithTranslatedErrorMessage(localizedError.Translate(user.Language))
store.UpdateFeedError(originalFeed)
return localizedError
}
if store.AnotherFeedURLExists(userID, originalFeed.ID, responseHandler.EffectiveURL()) {
localizedError := locale.NewLocalizedErrorWrapper(ErrDuplicatedFeed, "error.duplicated_feed")
originalFeed.WithTranslatedErrorMessage(localizedError.Translate(user.Language))
store.UpdateFeedError(originalFeed)
localizedError = locale.NewLocalizedErrorWrapper(ErrDuplicatedFeed, "error.duplicated_feed")
return localizedError
}
@ -279,27 +306,16 @@ func RefreshFeed(store *storage.Storage, userID, feedID int64, forceRefresh bool
updatedFeed, parseErr := parser.ParseFeed(responseHandler.EffectiveURL(), bytes.NewReader(responseBody))
if parseErr != nil {
localizedError := locale.NewLocalizedErrorWrapper(parseErr, "error.unable_to_parse_feed", parseErr)
if errors.Is(parseErr, parser.ErrFeedFormatNotDetected) {
localizedError = locale.NewLocalizedErrorWrapper(parseErr, "error.feed_format_not_detected", parseErr)
} else {
localizedError = locale.NewLocalizedErrorWrapper(parseErr, "error.unable_to_parse_feed", parseErr)
}
originalFeed.WithTranslatedErrorMessage(localizedError.Translate(user.Language))
store.UpdateFeedError(originalFeed)
return localizedError
}
// If the feed has a TTL defined, we use it to make sure we don't check it too often.
newTTL = updatedFeed.TTL
// Set the next check at with updated arguments.
originalFeed.ScheduleNextCheck(weeklyEntryCount, newTTL)
slog.Debug("Updated next check date",
slog.Int64("user_id", userID),
slog.Int64("feed_id", feedID),
slog.Int("ttl", newTTL),
slog.Time("new_next_check_at", originalFeed.NextCheckAt),
)
originalFeed.Entries = updatedFeed.Entries
processor.ProcessFeedEntries(store, originalFeed, user, forceRefresh)
@ -308,9 +324,7 @@ func RefreshFeed(store *storage.Storage, userID, feedID int64, forceRefresh bool
updateExistingEntries := forceRefresh || !originalFeed.Crawler
newEntries, storeErr := store.RefreshFeedEntries(originalFeed.UserID, originalFeed.ID, originalFeed.Entries, updateExistingEntries)
if storeErr != nil {
localizedError := locale.NewLocalizedErrorWrapper(storeErr, "error.database_error", storeErr)
originalFeed.WithTranslatedErrorMessage(localizedError.Translate(user.Language))
store.UpdateFeedError(originalFeed)
localizedError = locale.NewLocalizedErrorWrapper(storeErr, "error.database_error", storeErr)
return localizedError
}
@ -344,16 +358,7 @@ func RefreshFeed(store *storage.Storage, userID, feedID int64, forceRefresh bool
)
}
originalFeed.ResetErrorCounter()
if storeErr := store.UpdateFeed(originalFeed); storeErr != nil {
localizedError := locale.NewLocalizedErrorWrapper(storeErr, "error.database_error", storeErr)
originalFeed.WithTranslatedErrorMessage(localizedError.Translate(user.Language))
store.UpdateFeedError(originalFeed)
return localizedError
}
return nil
return localizedError
}
func checkFeedIcon(store *storage.Storage, requestBuilder *fetcher.RequestBuilder, feedID int64, websiteURL, feedIconURL string) {

View File

@ -38,9 +38,6 @@ var (
func ProcessFeedEntries(store *storage.Storage, feed *model.Feed, user *model.User, forceRefresh bool) {
var filteredEntries model.Entries
minifier := minify.New()
minifier.AddFunc("text/html", html.Minify)
// Process older entries first
for i := len(feed.Entries) - 1; i >= 0; i-- {
entry := feed.Entries[i]
@ -107,11 +104,7 @@ func ProcessFeedEntries(store *storage.Storage, feed *model.Feed, user *model.Us
)
} else if content != "" {
// We replace the entry content only if the scraper doesn't return any error.
if minifiedHTML, err := minifier.String("text/html", content); err == nil {
entry.Content = minifiedHTML
} else {
entry.Content = content
}
entry.Content = minifyEntryContent(content)
}
}
@ -189,9 +182,6 @@ func isAllowedEntry(feed *model.Feed, entry *model.Entry) bool {
// ProcessEntryWebPage downloads the entry web page and apply rewrite rules.
func ProcessEntryWebPage(feed *model.Feed, entry *model.Entry, user *model.User) error {
minifier := minify.New()
minifier.AddFunc("text/html", html.Minify)
startTime := time.Now()
websiteURL := getUrlFromEntry(feed, entry)
@ -223,11 +213,7 @@ func ProcessEntryWebPage(feed *model.Feed, entry *model.Entry, user *model.User)
}
if content != "" {
if minifiedHTML, err := minifier.String("text/html", content); err == nil {
entry.Content = minifiedHTML
} else {
entry.Content = content
}
entry.Content = minifyEntryContent(content)
if user.ShowReadingTime {
entry.ReadingTime = readingtime.EstimateReadingTime(entry.Content, user.DefaultReadingSpeed, user.CJKReadingSpeed)
}
@ -439,3 +425,19 @@ func isRecentEntry(entry *model.Entry) bool {
}
return false
}
func minifyEntryContent(entryContent string) string {
m := minify.New()
// Options required to avoid breaking the HTML content.
m.Add("text/html", &html.Minifier{
KeepEndTags: true,
KeepQuotes: true,
})
if minifiedHTML, err := m.String("text/html", entryContent); err == nil {
entryContent = minifiedHTML
}
return entryContent
}

View File

@ -117,3 +117,12 @@ func TestIsRecentEntry(t *testing.T) {
}
}
}
func TestMinifyEntryContent(t *testing.T) {
input := `<p> Some text with a <a href="http://example.org/"> link </a> </p>`
expected := `<p>Some text with a <a href="http://example.org/">link</a></p>`
result := minifyEntryContent(input)
if expected != result {
t.Errorf(`Unexpected result, got %q`, result)
}
}

View File

@ -0,0 +1,18 @@
{{ define "enclosure_media_controls" }}
<div class="media-controls">
<div class="media-seek-control">
<div class="media-control-label">{{ t "enclosure_media_controls.seek" }} </div>
<button class="page-button" data-enclosure-id="{{.ID}}" data-enclosure-action="seek" data-action-value="-30" title="{{ t "enclosure_media_controls.seek.title" "-30" }}" ><span class="icon-label" >-30s</span></button>
<button class="page-button" data-enclosure-id="{{.ID}}" data-enclosure-action="seek" data-action-value="-10" title="{{ t "enclosure_media_controls.seek.title" "-10" }}" ><span class="icon-label" >-10s</span></button>
<button class="page-button" data-enclosure-id="{{.ID}}" data-enclosure-action="seek" data-action-value="+10" title="{{ t "enclosure_media_controls.seek.title" "+10" }}" ><span class="icon-label" >+10s</span></button>
<button class="page-button" data-enclosure-id="{{.ID}}" data-enclosure-action="seek" data-action-value="+30" title="{{ t "enclosure_media_controls.seek.title" "+30" }}" ><span class="icon-label" >+30s</span></button>
</div>
<div class="media-speed-control">
<div class="media-control-label">{{ t "enclosure_media_controls.speed" }} (<span class="speed-indicator" data-enclosure-id="{{.ID}}">x.xxx</span>)</div> <!-- Need JS to display the current speed unfortunately -->
<button class="page-button" data-enclosure-id="{{.ID}}" data-enclosure-action="speed" data-action-value="-0.25" title="{{ t "enclosure_media_controls.speed.slower.title" "0.25" }}"><span class="icon-label" >{{ t "enclosure_media_controls.speed.slower" }}</span></button>
<button class="page-button" data-enclosure-id="{{.ID}}" data-enclosure-action="speed-reset" data-action-value="1" title="{{ t "enclosure_media_controls.speed.reset.title"}}"><span class="icon-label" >{{ t "enclosure_media_controls.speed.reset" }}</span></button>
<button class="page-button" data-enclosure-id="{{.ID}}" data-enclosure-action="speed" data-action-value="+0.25" title="{{ t "enclosure_media_controls.speed.faster.title" "0.25" }}"><span class="icon-label" >{{ t "enclosure_media_controls.speed.faster" }}</span></button>
</div>
</div>
{{ end }}

View File

@ -174,6 +174,7 @@
data-last-position="{{ .MediaProgression }}"
{{ if $.user.MediaPlaybackRate }}data-playback-rate="{{ $.user.MediaPlaybackRate }}"{{ end }}
data-save-url="{{ route "saveEnclosureProgression" "enclosureID" .ID }}"
data-enclosure-id="{{.ID}}"
>
{{ if (and $.user (mustBeProxyfied "audio")) }}
<source src="{{ proxyURL .URL }}" type="{{ .Html5MimeType }}">
@ -181,6 +182,7 @@
<source src="{{ .URL | safeURL }}" type="{{ .Html5MimeType }}">
{{ end }}
</audio>
{{ template "enclosure_media_controls" . }}
</div>
{{ else if hasPrefix .MimeType "video/" }}
<div class="enclosure-video">
@ -188,6 +190,7 @@
data-last-position="{{ .MediaProgression }}"
{{ if $.user.MediaPlaybackRate }}data-playback-rate="{{ $.user.MediaPlaybackRate }}"{{ end }}
data-save-url="{{ route "saveEnclosureProgression" "enclosureID" .ID }}"
data-enclosure-id="{{.ID}}"
>
{{ if (and $.user (mustBeProxyfied "video")) }}
<source src="{{ proxyURL .URL }}" type="{{ .Html5MimeType }}">
@ -195,6 +198,7 @@
<source src="{{ .URL | safeURL }}" type="{{ .Html5MimeType }}">
{{ end }}
</video>
{{ template "enclosure_media_controls" . }}
</div>
{{ end }}
{{ end }}
@ -218,6 +222,7 @@
data-last-position="{{ .MediaProgression }}"
{{ if $.user.MediaPlaybackRate }}data-playback-rate="{{ $.user.MediaPlaybackRate }}"{{ end }}
data-save-url="{{ route "saveEnclosureProgression" "enclosureID" .ID }}"
data-enclosure-id="{{.ID}}"
>
{{ if (and $.user (mustBeProxyfied "audio")) }}
<source src="{{ proxyURL .URL }}" type="{{ .Html5MimeType }}">
@ -225,6 +230,7 @@
<source src="{{ .URL | safeURL }}" type="{{ .Html5MimeType }}">
{{ end }}
</audio>
{{ template "enclosure_media_controls" . }}
</div>
{{ else if hasPrefix .MimeType "video/" }}
<div class="enclosure-video">
@ -232,6 +238,7 @@
data-last-position="{{ .MediaProgression }}"
{{ if $.user.MediaPlaybackRate }}data-playback-rate="{{ $.user.MediaPlaybackRate }}"{{ end }}
data-save-url="{{ route "saveEnclosureProgression" "enclosureID" .ID }}"
data-enclosure-id="{{.ID}}"
>
{{ if (and $.user (mustBeProxyfied "video")) }}
<source src="{{ proxyURL .URL }}" type="{{ .Html5MimeType }}">
@ -239,6 +246,7 @@
<source src="{{ .URL | safeURL }}" type="{{ .Html5MimeType }}">
{{ end }}
</video>
{{ template "enclosure_media_controls" . }}
</div>
{{ else if hasPrefix .MimeType "image/" }}
<div class="enclosure-image">

View File

@ -1215,6 +1215,39 @@ audio, video {
width: 100%;
}
.media-controls{
font-size: .9em;
display: flex;
flex-wrap: wrap;
}
.media-controls .media-control-label{
line-height: 1em;
}
.media-controls>div{
display: flex;
flex-wrap: nowrap;
justify-content:center;
min-width: 50%;
align-items: center;
}
.media-controls>div>*{
padding-left:12px;
}
.media-controls>div>*:first-child{
padding-left:0;
}
.media-controls span.speed-indicator{
/*monospace to ensure constant width even when value change. JS ensure the value is always on 4 characters (in most cases)
This reduce ui flickering due to element moving around a bit
*/
font-family: monospace;
}
.integration-form summary {
font-weight: 700;
}

View File

@ -446,8 +446,8 @@ function goToPage(page, fallbackSelf) {
}
/**
*
* @param {(number|event)} offset - many items to jump for focus.
*
* @param {(number|event)} offset - many items to jump for focus.
*/
function goToPrevious(offset) {
if (offset instanceof KeyboardEvent) {
@ -461,8 +461,8 @@ function goToPrevious(offset) {
}
/**
*
* @param {(number|event)} offset - How many items to jump for focus.
*
* @param {(number|event)} offset - How many items to jump for focus.
*/
function goToNext(offset) {
if (offset instanceof KeyboardEvent) {
@ -521,7 +521,7 @@ function goToListItem(offset) {
items[i].classList.remove("current-item");
// By default adjust selection by offset
let itemOffset = (i + offset + items.length) % items.length;
let itemOffset = (i + offset + items.length) % items.length;
// Allow jumping to top or bottom
if (offset == TOP) {
itemOffset = 0;
@ -742,3 +742,43 @@ function getCsrfToken() {
return "";
}
/**
* Handle all clicks on media player controls button on enclosures.
* Will change the current speed and position of the player accordingly.
* Will not save anything, all is done client-side, however, changing the position
* will trigger the handlePlayerProgressionSave and save the new position backends side.
* @param {Element} button
*/
function handleMediaControl(button) {
const action = button.dataset.enclosureAction;
const value = parseFloat(button.dataset.actionValue);
const targetEnclosureId = button.dataset.enclosureId;
const enclosures = document.querySelectorAll(`audio[data-enclosure-id="${targetEnclosureId}"],video[data-enclosure-id="${targetEnclosureId}"]`);
const speedIndicator = document.querySelectorAll(`span.speed-indicator[data-enclosure-id="${targetEnclosureId}"]`);
enclosures.forEach((enclosure) => {
switch (action) {
case "seek":
enclosure.currentTime = enclosure.currentTime + value > 0 ? enclosure.currentTime + value : 0;
break;
case "speed":
// I set a floor speed of 0.25 to avoid too slow speed where it gives the impression it stopped.
// 0.25 was chosen because it will allow to get back to 1x in two "faster" click, and lower value with same property would be 0.
enclosure.playbackRate = Math.max(0.25, enclosure.playbackRate + value);
speedIndicator.forEach((speedI) => {
// Two digit precision to ensure we always have the same number of characters (4) to avoid controls moving when clicking buttons because of more or less characters.
// The trick only work on rate less than 10, but it feels an acceptable tread of considering the feature
speedI.innerText = `${enclosure.playbackRate.toFixed(2)}x`;
});
break;
case "speed-reset":
enclosure.playbackRate = value ;
speedIndicator.forEach((speedI) => {
// Two digit precision to ensure we always have the same number of characters (4) to avoid controls moving when clicking buttons because of more or less characters.
// The trick only work on rate less than 10, but it feels an acceptable tread of considering the feature
speedI.innerText = `${enclosure.playbackRate.toFixed(2)}x`;
});
break;
}
});
}

View File

@ -167,6 +167,20 @@ document.addEventListener("DOMContentLoaded", () => {
playbackRateElements.forEach((element) => {
if (element.dataset.playbackRate) {
element.playbackRate = element.dataset.playbackRate;
if (element.dataset.enclosureId){
// In order to display properly the speed we need to do it on bootstrap.
// Could not do it backend side because I didn't know how to do it because of the template inclusion and
// the way the initial playback speed is handled. See enclosure_media_controls.html if you want to try to fix this
document.querySelectorAll(`span.speed-indicator[data-enclosure-id="${element.dataset.enclosureId}"]`).forEach((speedI)=>{
speedI.innerText = `${element.dataset.playbackRate}x`;
});
}
}
});
// Set enclosure media controls handlers
const mediaControlsElements = document.querySelectorAll("button[data-enclosure-action]");
mediaControlsElements.forEach((element) => {
element.addEventListener("click", () => handleMediaControl(element));
});
});