Merge remote-tracking branch 'github/master'
Some checks failed
Build and release container directly from master / release (push) Waiting to run
Invidious CI / build-docker (push) Waiting to run
Invidious CI / build-docker-arm64 (push) Waiting to run
Invidious CI / ameba_lint (push) Waiting to run
Invidious CI / build - crystal: 1.10.1, stable: true (push) Failing after 10s
Invidious CI / build - crystal: 1.11.2, stable: true (push) Failing after 4s
Invidious CI / build - crystal: 1.12.1, stable: true (push) Failing after 4s
Invidious CI / build - crystal: 1.9.2, stable: true (push) Failing after 4s
Invidious CI / build - crystal: nightly, stable: false (push) Failing after 4s
Some checks failed
Build and release container directly from master / release (push) Waiting to run
Invidious CI / build-docker (push) Waiting to run
Invidious CI / build-docker-arm64 (push) Waiting to run
Invidious CI / ameba_lint (push) Waiting to run
Invidious CI / build - crystal: 1.10.1, stable: true (push) Failing after 10s
Invidious CI / build - crystal: 1.11.2, stable: true (push) Failing after 4s
Invidious CI / build - crystal: 1.12.1, stable: true (push) Failing after 4s
Invidious CI / build - crystal: 1.9.2, stable: true (push) Failing after 4s
Invidious CI / build - crystal: nightly, stable: false (push) Failing after 4s
This commit is contained in:
commit
97ab3459b4
56 changed files with 435 additions and 273 deletions
|
@ -38,6 +38,9 @@ Style/RedundantBegin:
|
||||||
Style/RedundantReturn:
|
Style/RedundantReturn:
|
||||||
Enabled: false
|
Enabled: false
|
||||||
|
|
||||||
|
Style/RedundantNext:
|
||||||
|
Enabled: false
|
||||||
|
|
||||||
Style/ParenthesesAroundCondition:
|
Style/ParenthesesAroundCondition:
|
||||||
Enabled: false
|
Enabled: false
|
||||||
|
|
||||||
|
|
2
.github/CODEOWNERS
vendored
2
.github/CODEOWNERS
vendored
|
@ -6,7 +6,7 @@ docker/ @unixfox
|
||||||
kubernetes/ @unixfox
|
kubernetes/ @unixfox
|
||||||
|
|
||||||
README.md @thefrenchghosty
|
README.md @thefrenchghosty
|
||||||
config/config.example.yml @thefrenchghosty @SamantazFox @unixfox
|
config/config.example.yml @SamantazFox @unixfox
|
||||||
|
|
||||||
scripts/ @syeopite
|
scripts/ @syeopite
|
||||||
shards.lock @syeopite
|
shards.lock @syeopite
|
||||||
|
|
5
.github/workflows/ci.yml
vendored
5
.github/workflows/ci.yml
vendored
|
@ -51,6 +51,11 @@ jobs:
|
||||||
with:
|
with:
|
||||||
submodules: true
|
submodules: true
|
||||||
|
|
||||||
|
- name: Install required APT packages
|
||||||
|
run: |
|
||||||
|
sudo apt install -y libsqlite3-dev
|
||||||
|
shell: bash
|
||||||
|
|
||||||
- name: Install Crystal
|
- name: Install Crystal
|
||||||
uses: crystal-lang/install-crystal@v1.8.0
|
uses: crystal-lang/install-crystal@v1.8.0
|
||||||
with:
|
with:
|
||||||
|
|
34
CHANGELOG.md
34
CHANGELOG.md
|
@ -1,5 +1,39 @@
|
||||||
# CHANGELOG
|
# CHANGELOG
|
||||||
|
|
||||||
|
## vX.Y.0 (future)
|
||||||
|
|
||||||
|
|
||||||
|
### Full list of pull requests merged since the last release (newest first)
|
||||||
|
|
||||||
|
* Search: Fix 'youtu.be' URLs in sanitizer ([#4894], by @SamantazFox)
|
||||||
|
* Ameba: Disable Style/RedundantNext rule ([#4888], thanks @syeopite)
|
||||||
|
* Playlists: Fix 'invalid byte sequence' error when subscribing ([#4887], thanks @DmitrySandalov)
|
||||||
|
* Parse more metadata badges for SearchVideos ([#4863], thanks @ChunkyProgrammer)
|
||||||
|
* Translations update from Hosted Weblate ([#4862], thanks to our many translators)
|
||||||
|
* Videos: Convert URL before putting result into cache ([#4850], by @SamantazFox)
|
||||||
|
* HTML: Add error message to "search issues on GitHub" link ([#4652], thanks @tracedgod)
|
||||||
|
* Preferences: Add option to control preloading of video data ([#4122], thanks @Nerdmind)
|
||||||
|
* Performance: Improve speed of automatic instance redirection ([#4193], thanks @syeopite)
|
||||||
|
* Remove myself from CODEOWNERS on the config file ([#4942], by @TheFrenchGhosty)
|
||||||
|
* Update latest version WEB_CREATOR + fix comment web embed ([#4930], thanks @unixfox)
|
||||||
|
* use WEB_CREATOR when po_token with WEB_EMBED as a fallback ([#4928], thanks @unixfox)
|
||||||
|
* Revert "use web screen embed for fixing potoken functionality"
|
||||||
|
* use web screen embed for fixing potoken functionality ([#4923], thanks @unixfox)
|
||||||
|
|
||||||
|
[#4122]: https://github.com/iv-org/invidious/pull/4122
|
||||||
|
[#4193]: https://github.com/iv-org/invidious/pull/4193
|
||||||
|
[#4652]: https://github.com/iv-org/invidious/pull/4652
|
||||||
|
[#4850]: https://github.com/iv-org/invidious/pull/4850
|
||||||
|
[#4862]: https://github.com/iv-org/invidious/pull/4862
|
||||||
|
[#4863]: https://github.com/iv-org/invidious/pull/4863
|
||||||
|
[#4887]: https://github.com/iv-org/invidious/pull/4887
|
||||||
|
[#4888]: https://github.com/iv-org/invidious/pull/4888
|
||||||
|
[#4894]: https://github.com/iv-org/invidious/pull/4894
|
||||||
|
[#4923]: https://github.com/iv-org/invidious/pull/4923
|
||||||
|
[#4928]: https://github.com/iv-org/invidious/pull/4928
|
||||||
|
[#4930]: https://github.com/iv-org/invidious/pull/4930
|
||||||
|
[#4942]: https://github.com/iv-org/invidious/pull/4942
|
||||||
|
|
||||||
|
|
||||||
## v2.20240825.2 (2024-08-26)
|
## v2.20240825.2 (2024-08-26)
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,6 @@ var player_data = JSON.parse(document.getElementById('player_data').textContent)
|
||||||
var video_data = JSON.parse(document.getElementById('video_data').textContent);
|
var video_data = JSON.parse(document.getElementById('video_data').textContent);
|
||||||
|
|
||||||
var options = {
|
var options = {
|
||||||
preload: 'auto',
|
|
||||||
liveui: true,
|
liveui: true,
|
||||||
playbackRates: [0.25, 0.5, 0.75, 1.0, 1.25, 1.5, 1.75, 2.0],
|
playbackRates: [0.25, 0.5, 0.75, 1.0, 1.25, 1.5, 1.75, 2.0],
|
||||||
controlBar: {
|
controlBar: {
|
||||||
|
|
|
@ -714,6 +714,22 @@ default_user_preferences:
|
||||||
# Video player behavior
|
# Video player behavior
|
||||||
# -----------------------------
|
# -----------------------------
|
||||||
|
|
||||||
|
##
|
||||||
|
## This option controls the value of the HTML5 <video> element's
|
||||||
|
## "preload" attribute.
|
||||||
|
##
|
||||||
|
## If set to 'false', no video data will be loaded until the user
|
||||||
|
## explicitly starts the video by clicking the "Play" button.
|
||||||
|
## If set to 'true', the web browser will buffer some video data
|
||||||
|
## while the page is loading.
|
||||||
|
##
|
||||||
|
## See: https://www.w3schools.com/tags/att_video_preload.asp
|
||||||
|
##
|
||||||
|
## Accepted values: true, false
|
||||||
|
## Default: true
|
||||||
|
##
|
||||||
|
#preload: true
|
||||||
|
|
||||||
##
|
##
|
||||||
## Automatically play videos on page load.
|
## Automatically play videos on page load.
|
||||||
##
|
##
|
||||||
|
|
|
@ -47,6 +47,7 @@
|
||||||
"Preferences": "Einstellungen",
|
"Preferences": "Einstellungen",
|
||||||
"preferences_category_player": "Wiedergabeeinstellungen",
|
"preferences_category_player": "Wiedergabeeinstellungen",
|
||||||
"preferences_video_loop_label": "Immer wiederholen: ",
|
"preferences_video_loop_label": "Immer wiederholen: ",
|
||||||
|
"preferences_preload_label": "Videodaten vorladen: ",
|
||||||
"preferences_autoplay_label": "Automatisch abspielen: ",
|
"preferences_autoplay_label": "Automatisch abspielen: ",
|
||||||
"preferences_continue_label": "Immer automatisch nächstes Video abspielen: ",
|
"preferences_continue_label": "Immer automatisch nächstes Video abspielen: ",
|
||||||
"preferences_continue_autoplay_label": "Nächstes Video automatisch abspielen: ",
|
"preferences_continue_autoplay_label": "Nächstes Video automatisch abspielen: ",
|
||||||
|
@ -322,7 +323,7 @@
|
||||||
"channel_tab_community_label": "Gemeinschaft",
|
"channel_tab_community_label": "Gemeinschaft",
|
||||||
"search_filters_sort_option_relevance": "Relevanz",
|
"search_filters_sort_option_relevance": "Relevanz",
|
||||||
"search_filters_sort_option_rating": "Bewertung",
|
"search_filters_sort_option_rating": "Bewertung",
|
||||||
"search_filters_sort_option_date": "Datum",
|
"search_filters_sort_option_date": "Hochladedatum",
|
||||||
"search_filters_sort_option_views": "Aufrufe",
|
"search_filters_sort_option_views": "Aufrufe",
|
||||||
"search_filters_type_label": "Inhaltstyp",
|
"search_filters_type_label": "Inhaltstyp",
|
||||||
"search_filters_duration_label": "Dauer",
|
"search_filters_duration_label": "Dauer",
|
||||||
|
@ -494,5 +495,8 @@
|
||||||
"Add to playlist": "Einer Wiedergabeliste hinzufügen",
|
"Add to playlist": "Einer Wiedergabeliste hinzufügen",
|
||||||
"Search for videos": "Nach Videos suchen",
|
"Search for videos": "Nach Videos suchen",
|
||||||
"toggle_theme": "Thema wechseln",
|
"toggle_theme": "Thema wechseln",
|
||||||
"Add to playlist: ": "Einer Wiedergabeliste hinzufügen: "
|
"Add to playlist: ": "Einer Wiedergabeliste hinzufügen: ",
|
||||||
|
"carousel_go_to": "Zu Folie `x` gehen",
|
||||||
|
"carousel_slide": "Folie {{current}} von {{total}}",
|
||||||
|
"carousel_skip": "Karussell überspringen"
|
||||||
}
|
}
|
||||||
|
|
|
@ -489,5 +489,10 @@
|
||||||
"search_filters_date_label": "Ημερομηνία αναφόρτωσης",
|
"search_filters_date_label": "Ημερομηνία αναφόρτωσης",
|
||||||
"Search for videos": "Αναζήτηση βίντεο",
|
"Search for videos": "Αναζήτηση βίντεο",
|
||||||
"The Popular feed has been disabled by the administrator.": "Η δημοφιλής ροή έχει απενεργοποιηθεί από τον διαχειριστή.",
|
"The Popular feed has been disabled by the administrator.": "Η δημοφιλής ροή έχει απενεργοποιηθεί από τον διαχειριστή.",
|
||||||
"Answer": "Απάντηση"
|
"Answer": "Απάντηση",
|
||||||
|
"Add to playlist": "Λίιστα αναπαραγωγής",
|
||||||
|
"Add to playlist: ": "Λίστα αναπαραγωγής: ",
|
||||||
|
"carousel_slide": "Εικόνα {{current}}απο {{total}}",
|
||||||
|
"carousel_go_to": "Πήγαινε στην εικόνα`x`",
|
||||||
|
"toggle_theme": "Αλλαγή θέματος"
|
||||||
}
|
}
|
||||||
|
|
|
@ -71,6 +71,7 @@
|
||||||
"Preferences": "Preferences",
|
"Preferences": "Preferences",
|
||||||
"preferences_category_player": "Player preferences",
|
"preferences_category_player": "Player preferences",
|
||||||
"preferences_video_loop_label": "Always loop: ",
|
"preferences_video_loop_label": "Always loop: ",
|
||||||
|
"preferences_preload_label": "Preload video data: ",
|
||||||
"preferences_autoplay_label": "Autoplay: ",
|
"preferences_autoplay_label": "Autoplay: ",
|
||||||
"preferences_continue_label": "Play next by default: ",
|
"preferences_continue_label": "Play next by default: ",
|
||||||
"preferences_continue_autoplay_label": "Autoplay next video: ",
|
"preferences_continue_autoplay_label": "Autoplay next video: ",
|
||||||
|
@ -422,7 +423,7 @@
|
||||||
"search_filters_title": "Filters",
|
"search_filters_title": "Filters",
|
||||||
"search_filters_date_label": "Upload date",
|
"search_filters_date_label": "Upload date",
|
||||||
"search_filters_date_option_none": "Any date",
|
"search_filters_date_option_none": "Any date",
|
||||||
"search_filters_date_option_hour": "Last Hour",
|
"search_filters_date_option_hour": "Last hour",
|
||||||
"search_filters_date_option_today": "Today",
|
"search_filters_date_option_today": "Today",
|
||||||
"search_filters_date_option_week": "This week",
|
"search_filters_date_option_week": "This week",
|
||||||
"search_filters_date_option_month": "This month",
|
"search_filters_date_option_month": "This month",
|
||||||
|
@ -454,7 +455,7 @@
|
||||||
"search_filters_sort_label": "Sort By",
|
"search_filters_sort_label": "Sort By",
|
||||||
"search_filters_sort_option_relevance": "Relevance",
|
"search_filters_sort_option_relevance": "Relevance",
|
||||||
"search_filters_sort_option_rating": "Rating",
|
"search_filters_sort_option_rating": "Rating",
|
||||||
"search_filters_sort_option_date": "Upload Date",
|
"search_filters_sort_option_date": "Upload date",
|
||||||
"search_filters_sort_option_views": "View count",
|
"search_filters_sort_option_views": "View count",
|
||||||
"search_filters_apply_button": "Apply selected filters",
|
"search_filters_apply_button": "Apply selected filters",
|
||||||
"Current version: ": "Current version: ",
|
"Current version: ": "Current version: ",
|
||||||
|
|
|
@ -478,7 +478,7 @@
|
||||||
"tokens_count_0": "{{count}} token",
|
"tokens_count_0": "{{count}} token",
|
||||||
"tokens_count_1": "{{count}} tokens",
|
"tokens_count_1": "{{count}} tokens",
|
||||||
"tokens_count_2": "{{count}} tokens",
|
"tokens_count_2": "{{count}} tokens",
|
||||||
"search_message_use_another_instance": " También puede <a href=\"`x`\">buscar en otra instancia</a>.",
|
"search_message_use_another_instance": "También puedes <a href=\"`x`\">buscar en otra instancia</a>.",
|
||||||
"Popular enabled: ": "¿Habilitar la sección popular? ",
|
"Popular enabled: ": "¿Habilitar la sección popular? ",
|
||||||
"error_video_not_in_playlist": "El video que solicitaste no existe en esta lista de reproducción. <a href=\"`x`\">Haz clic aquí para acceder a la página de inicio de la lista de reproducción.</a>",
|
"error_video_not_in_playlist": "El video que solicitaste no existe en esta lista de reproducción. <a href=\"`x`\">Haz clic aquí para acceder a la página de inicio de la lista de reproducción.</a>",
|
||||||
"channel_tab_streams_label": "Directos",
|
"channel_tab_streams_label": "Directos",
|
||||||
|
|
|
@ -360,7 +360,7 @@
|
||||||
"search_filters_duration_label": "مدت",
|
"search_filters_duration_label": "مدت",
|
||||||
"search_filters_features_label": "ویژگیها",
|
"search_filters_features_label": "ویژگیها",
|
||||||
"search_filters_sort_label": "به ترتیب",
|
"search_filters_sort_label": "به ترتیب",
|
||||||
"search_filters_date_option_hour": "یک ساعت گذشته",
|
"search_filters_date_option_hour": "ساعت گذشته",
|
||||||
"search_filters_date_option_today": "امروز",
|
"search_filters_date_option_today": "امروز",
|
||||||
"search_filters_date_option_week": "این هفته",
|
"search_filters_date_option_week": "این هفته",
|
||||||
"search_filters_date_option_month": "این ماه",
|
"search_filters_date_option_month": "این ماه",
|
||||||
|
@ -461,7 +461,7 @@
|
||||||
"Song: ": "آهنگ: ",
|
"Song: ": "آهنگ: ",
|
||||||
"Channel Sponsor": "اسپانسر کانال",
|
"Channel Sponsor": "اسپانسر کانال",
|
||||||
"Standard YouTube license": "پروانه استاندارد YouTube",
|
"Standard YouTube license": "پروانه استاندارد YouTube",
|
||||||
"search_message_use_another_instance": " شما همچنین میتوانید <a href=\"`x`\">در نمونه دیگر هم جستجو کنید</a>.",
|
"search_message_use_another_instance": "همچنین میتوانید <a href=\"`x`\">در نمونهای دیگر هم جستوجو کنید</a>.",
|
||||||
"Download is disabled": "دریافت غیرفعال است",
|
"Download is disabled": "دریافت غیرفعال است",
|
||||||
"crash_page_before_reporting": "پیش از گزارش ایراد، مطمئنید شوید که:",
|
"crash_page_before_reporting": "پیش از گزارش ایراد، مطمئنید شوید که:",
|
||||||
"playlist_button_add_items": "افزودن ویدیو",
|
"playlist_button_add_items": "افزودن ویدیو",
|
||||||
|
|
|
@ -449,24 +449,24 @@
|
||||||
"Cantonese (Hong Kong)": "Kantonski (Hong Kong)",
|
"Cantonese (Hong Kong)": "Kantonski (Hong Kong)",
|
||||||
"Chinese": "Kineski",
|
"Chinese": "Kineski",
|
||||||
"Chinese (Taiwan)": "Kineski (Tajvan)",
|
"Chinese (Taiwan)": "Kineski (Tajvan)",
|
||||||
"Dutch (auto-generated)": "Nizozemski (automatski generiran)",
|
"Dutch (auto-generated)": "Nizozemski (automatski generirano)",
|
||||||
"French (auto-generated)": "Francuski (automatski generiran)",
|
"French (auto-generated)": "Francuski (automatski generirano)",
|
||||||
"Indonesian (auto-generated)": "Indonezijski (automatski generiran)",
|
"Indonesian (auto-generated)": "Indonezijski (automatski generirano)",
|
||||||
"Interlingue": "Interlingua",
|
"Interlingue": "Interlingua",
|
||||||
"Japanese (auto-generated)": "Japanski (automatski generiran)",
|
"Japanese (auto-generated)": "Japanski (automatski generirano)",
|
||||||
"Russian (auto-generated)": "Ruski (automatski generiran)",
|
"Russian (auto-generated)": "Ruski (automatski generirano)",
|
||||||
"Turkish (auto-generated)": "Turski (automatski generiran)",
|
"Turkish (auto-generated)": "Turski (automatski generirano)",
|
||||||
"Vietnamese (auto-generated)": "Vijetnamski (automatski generiran)",
|
"Vietnamese (auto-generated)": "Vijetnamski (automatski generirano)",
|
||||||
"Spanish (Spain)": "Španjolski (Španjolska)",
|
"Spanish (Spain)": "Španjolski (Španjolska)",
|
||||||
"Italian (auto-generated)": "Talijanski (automatski generiran)",
|
"Italian (auto-generated)": "Talijanski (automatski generirano)",
|
||||||
"Portuguese (Brazil)": "Portugalski (Brazil)",
|
"Portuguese (Brazil)": "Portugalski (Brazil)",
|
||||||
"Spanish (Mexico)": "Španjolski (Meksiko)",
|
"Spanish (Mexico)": "Španjolski (Meksiko)",
|
||||||
"German (auto-generated)": "Njemački (automatski generiran)",
|
"German (auto-generated)": "Njemački (automatski generirano)",
|
||||||
"Chinese (China)": "Kineski (Kina)",
|
"Chinese (China)": "Kineski (Kina)",
|
||||||
"Chinese (Hong Kong)": "Kineski (Hong Kong)",
|
"Chinese (Hong Kong)": "Kineski (Hong Kong)",
|
||||||
"Korean (auto-generated)": "Korejski (automatski generiran)",
|
"Korean (auto-generated)": "Korejski (automatski generirano)",
|
||||||
"Portuguese (auto-generated)": "Portugalski (automatski generiran)",
|
"Portuguese (auto-generated)": "Portugalski (automatski generirano)",
|
||||||
"Spanish (auto-generated)": "Španjolski (automatski generiran)",
|
"Spanish (auto-generated)": "Španjolski (automatski generirano)",
|
||||||
"preferences_watch_history_label": "Aktiviraj povijest gledanja: ",
|
"preferences_watch_history_label": "Aktiviraj povijest gledanja: ",
|
||||||
"search_filters_title": "Filtri",
|
"search_filters_title": "Filtri",
|
||||||
"search_filters_date_option_none": "Bilo koji datum",
|
"search_filters_date_option_none": "Bilo koji datum",
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
"invidious": "Invidious",
|
"invidious": "Invidious",
|
||||||
"Image CAPTCHA": "Imagine CAPTCHA",
|
"Image CAPTCHA": "Imagine CAPTCHA",
|
||||||
"newest": "plus nove",
|
"newest": "plus nove",
|
||||||
"generic_button_save": "Salvar",
|
"generic_button_save": "Salveguardar",
|
||||||
"Dark mode: ": "Modo obscur: ",
|
"Dark mode: ": "Modo obscur: ",
|
||||||
"preferences_dark_mode_label": "Thema: ",
|
"preferences_dark_mode_label": "Thema: ",
|
||||||
"preferences_category_subscription": "Preferentias de subscription",
|
"preferences_category_subscription": "Preferentias de subscription",
|
||||||
|
@ -23,7 +23,7 @@
|
||||||
"light": "clar",
|
"light": "clar",
|
||||||
"No": "Non",
|
"No": "Non",
|
||||||
"youtube": "YouTube",
|
"youtube": "YouTube",
|
||||||
"LIVE": "IN DIRECTE",
|
"LIVE": "IN DIRECTO",
|
||||||
"reddit": "Reddit",
|
"reddit": "Reddit",
|
||||||
"preferences_category_player": "Preferentias de reproductor",
|
"preferences_category_player": "Preferentias de reproductor",
|
||||||
"Preferences": "Preferentias",
|
"Preferences": "Preferentias",
|
||||||
|
|
|
@ -363,7 +363,7 @@
|
||||||
"search_filters_features_option_location": "場所",
|
"search_filters_features_option_location": "場所",
|
||||||
"search_filters_features_option_hdr": "HDR",
|
"search_filters_features_option_hdr": "HDR",
|
||||||
"Current version: ": "現在のバージョン: ",
|
"Current version: ": "現在のバージョン: ",
|
||||||
"next_steps_error_message": "以下をお試してください: ",
|
"next_steps_error_message": "以下をお試しください: ",
|
||||||
"next_steps_error_message_refresh": "再読み込み",
|
"next_steps_error_message_refresh": "再読み込み",
|
||||||
"next_steps_error_message_go_to_youtube": "YouTubeを開く",
|
"next_steps_error_message_go_to_youtube": "YouTubeを開く",
|
||||||
"search_filters_duration_option_short": "4分未満",
|
"search_filters_duration_option_short": "4分未満",
|
||||||
|
@ -396,7 +396,7 @@
|
||||||
"download_subtitles": "字幕 - `x` (.vtt)",
|
"download_subtitles": "字幕 - `x` (.vtt)",
|
||||||
"search_filters_features_option_purchased": "購入済み",
|
"search_filters_features_option_purchased": "購入済み",
|
||||||
"preferences_quality_option_dash": "DASH (適応的画質)",
|
"preferences_quality_option_dash": "DASH (適応的画質)",
|
||||||
"preferences_quality_dash_option_worst": "最悪",
|
"preferences_quality_dash_option_worst": "最低",
|
||||||
"preferences_quality_dash_option_best": "最高",
|
"preferences_quality_dash_option_best": "最高",
|
||||||
"videoinfo_started_streaming_x_ago": "`x`前に配信を開始",
|
"videoinfo_started_streaming_x_ago": "`x`前に配信を開始",
|
||||||
"videoinfo_watch_on_youTube": "YouTubeで視聴",
|
"videoinfo_watch_on_youTube": "YouTubeで視聴",
|
||||||
|
|
|
@ -18,8 +18,8 @@
|
||||||
"preferences_related_videos_label": "관련 동영상 보기: ",
|
"preferences_related_videos_label": "관련 동영상 보기: ",
|
||||||
"Fallback captions: ": "대체 자막: ",
|
"Fallback captions: ": "대체 자막: ",
|
||||||
"preferences_captions_label": "기본 자막: ",
|
"preferences_captions_label": "기본 자막: ",
|
||||||
"reddit": "Reddit",
|
"reddit": "레딧",
|
||||||
"youtube": "YouTube",
|
"youtube": "유튜브",
|
||||||
"preferences_comments_label": "기본 댓글: ",
|
"preferences_comments_label": "기본 댓글: ",
|
||||||
"preferences_volume_label": "플레이어 볼륨: ",
|
"preferences_volume_label": "플레이어 볼륨: ",
|
||||||
"preferences_quality_label": "선호하는 비디오 품질: ",
|
"preferences_quality_label": "선호하는 비디오 품질: ",
|
||||||
|
@ -48,7 +48,7 @@
|
||||||
"An alternative front-end to YouTube": "유튜브의 프론트엔드 대안",
|
"An alternative front-end to YouTube": "유튜브의 프론트엔드 대안",
|
||||||
"History": "시청 기록",
|
"History": "시청 기록",
|
||||||
"Delete account?": "계정을 삭제 하시겠습니까?",
|
"Delete account?": "계정을 삭제 하시겠습니까?",
|
||||||
"Export data as JSON": "JSON으로 데이터 내보내기",
|
"Export data as JSON": "인비디어스 데이터 내보내기 (.json)",
|
||||||
"Export subscriptions as OPML (for NewPipe & FreeTube)": "OPML로 구독 내보내기 (뉴파이프 및 프리튜브)",
|
"Export subscriptions as OPML (for NewPipe & FreeTube)": "OPML로 구독 내보내기 (뉴파이프 및 프리튜브)",
|
||||||
"Export subscriptions as OPML": "OPML로 구독 내보내기",
|
"Export subscriptions as OPML": "OPML로 구독 내보내기",
|
||||||
"Export": "내보내기",
|
"Export": "내보내기",
|
||||||
|
@ -78,10 +78,10 @@
|
||||||
"Subscribe": "구독",
|
"Subscribe": "구독",
|
||||||
"Unsubscribe": "구독 취소",
|
"Unsubscribe": "구독 취소",
|
||||||
"LIVE": "실시간",
|
"LIVE": "실시간",
|
||||||
"generic_views_count_0": "조회수 {{count}}회",
|
"generic_views_count_0": "{{count}} 조회수",
|
||||||
"generic_videos_count_0": "동영상 {{count}}개",
|
"generic_videos_count_0": "{{count}} 동영상",
|
||||||
"generic_playlists_count_0": "재생목록 {{count}}개",
|
"generic_playlists_count_0": "{{count}} 재생목록",
|
||||||
"generic_subscribers_count_0": "구독자 {{count}}명",
|
"generic_subscribers_count_0": "{{count}} 구독자",
|
||||||
"generic_subscriptions_count_0": "{{count}} 구독",
|
"generic_subscriptions_count_0": "{{count}} 구독",
|
||||||
"search_filters_type_option_playlist": "재생목록",
|
"search_filters_type_option_playlist": "재생목록",
|
||||||
"Korean": "한국어",
|
"Korean": "한국어",
|
||||||
|
@ -109,14 +109,14 @@
|
||||||
"This channel does not exist.": "이 채널은 존재하지 않습니다.",
|
"This channel does not exist.": "이 채널은 존재하지 않습니다.",
|
||||||
"Deleted or invalid channel": "삭제되었거나 더 이상 존재하지 않는 채널",
|
"Deleted or invalid channel": "삭제되었거나 더 이상 존재하지 않는 채널",
|
||||||
"channel:`x`": "채널:`x`",
|
"channel:`x`": "채널:`x`",
|
||||||
"Show replies": "댓글 보이기",
|
"Show replies": "댓글 보기",
|
||||||
"Hide replies": "댓글 숨기기",
|
"Hide replies": "댓글 숨기기",
|
||||||
"Incorrect password": "잘못된 비밀번호",
|
"Incorrect password": "잘못된 비밀번호",
|
||||||
"License: ": "라이선스: ",
|
"License: ": "라이선스: ",
|
||||||
"Genre: ": "장르: ",
|
"Genre: ": "장르: ",
|
||||||
"Editing playlist `x`": "재생목록 `x` 수정하기",
|
"Editing playlist `x`": "재생목록 `x` 수정하기",
|
||||||
"Playlist privacy": "재생목록 공개 범위",
|
"Playlist privacy": "재생목록 공개 범위",
|
||||||
"Watch on YouTube": "YouTube에서 보기",
|
"Watch on YouTube": "유튜브에서 보기",
|
||||||
"Show less": "간략히",
|
"Show less": "간략히",
|
||||||
"Show more": "더보기",
|
"Show more": "더보기",
|
||||||
"Title": "제목",
|
"Title": "제목",
|
||||||
|
@ -125,7 +125,7 @@
|
||||||
"Delete playlist": "재생목록 삭제",
|
"Delete playlist": "재생목록 삭제",
|
||||||
"Delete playlist `x`?": "재생목록 `x` 를 삭제하시겠습니까?",
|
"Delete playlist `x`?": "재생목록 `x` 를 삭제하시겠습니까?",
|
||||||
"Updated `x` ago": "`x` 전에 업데이트됨",
|
"Updated `x` ago": "`x` 전에 업데이트됨",
|
||||||
"Released under the AGPLv3 on Github.": "GitHub에 AGPLv3 으로 배포됩니다.",
|
"Released under the AGPLv3 on Github.": "깃허브에 AGPLv3 으로 배포됩니다.",
|
||||||
"View all playlists": "모든 재생목록 보기",
|
"View all playlists": "모든 재생목록 보기",
|
||||||
"Private": "비공개",
|
"Private": "비공개",
|
||||||
"Unlisted": "목록에 없음",
|
"Unlisted": "목록에 없음",
|
||||||
|
@ -135,12 +135,12 @@
|
||||||
"Source available here.": "소스는 여기에서 사용할 수 있습니다.",
|
"Source available here.": "소스는 여기에서 사용할 수 있습니다.",
|
||||||
"Log out": "로그아웃",
|
"Log out": "로그아웃",
|
||||||
"search": "검색",
|
"search": "검색",
|
||||||
"subscriptions_unseen_notifs_count_0": "읽지 않은 알림 {{count}}개",
|
"subscriptions_unseen_notifs_count_0": "{{count}} 읽지 않은 알림",
|
||||||
"Subscriptions": "구독",
|
"Subscriptions": "구독",
|
||||||
"revoke": "철회",
|
"revoke": "철회",
|
||||||
"unsubscribe": "구독 취소",
|
"unsubscribe": "구독 취소",
|
||||||
"Import/export": "가져오기/내보내기",
|
"Import/export": "가져오기/내보내기",
|
||||||
"tokens_count_0": "토큰 {{count}}개",
|
"tokens_count_0": "{{count}} 토큰",
|
||||||
"Token": "토큰",
|
"Token": "토큰",
|
||||||
"Token manager": "토큰 관리자",
|
"Token manager": "토큰 관리자",
|
||||||
"Subscription manager": "구독 관리자",
|
"Subscription manager": "구독 관리자",
|
||||||
|
@ -163,7 +163,7 @@
|
||||||
"Clear watch history": "시청 기록 지우기",
|
"Clear watch history": "시청 기록 지우기",
|
||||||
"preferences_category_data": "데이터 설정",
|
"preferences_category_data": "데이터 설정",
|
||||||
"`x` is live": "`x` 이(가) 라이브 중입니다",
|
"`x` is live": "`x` 이(가) 라이브 중입니다",
|
||||||
"`x` uploaded a video": "`x` 이(가) 동영상을 게시했습니다",
|
"`x` uploaded a video": "`x` 동영상 게시됨",
|
||||||
"Enable web notifications": "웹 알림 활성화",
|
"Enable web notifications": "웹 알림 활성화",
|
||||||
"preferences_notifications_only_label": "알림만 표시 (있는 경우): ",
|
"preferences_notifications_only_label": "알림만 표시 (있는 경우): ",
|
||||||
"preferences_unseen_only_label": "시청하지 않은 것만 표시: ",
|
"preferences_unseen_only_label": "시청하지 않은 것만 표시: ",
|
||||||
|
@ -241,7 +241,7 @@
|
||||||
"Could not create mix.": "믹스를 생성할 수 없습니다.",
|
"Could not create mix.": "믹스를 생성할 수 없습니다.",
|
||||||
"`x` ago": "`x` 전",
|
"`x` ago": "`x` 전",
|
||||||
"comments_view_x_replies_0": "답글 {{count}}개 보기",
|
"comments_view_x_replies_0": "답글 {{count}}개 보기",
|
||||||
"View Reddit comments": "Reddit 댓글 보기",
|
"View Reddit comments": "레딧 댓글 보기",
|
||||||
"Engagement: ": "약속: ",
|
"Engagement: ": "약속: ",
|
||||||
"Wilson score: ": "Wilson Score: ",
|
"Wilson score: ": "Wilson Score: ",
|
||||||
"Family friendly? ": "전연령 영상입니까? ",
|
"Family friendly? ": "전연령 영상입니까? ",
|
||||||
|
@ -267,8 +267,8 @@
|
||||||
"Bulgarian": "불가리아어",
|
"Bulgarian": "불가리아어",
|
||||||
"Bosnian": "보스니아어",
|
"Bosnian": "보스니아어",
|
||||||
"Belarusian": "벨라루스어",
|
"Belarusian": "벨라루스어",
|
||||||
"View more comments on Reddit": "Reddit에서 댓글 더 보기",
|
"View more comments on Reddit": "레딧에서 댓글 더 보기",
|
||||||
"View YouTube comments": "YouTube 댓글 보기",
|
"View YouTube comments": "유튜브 댓글 보기",
|
||||||
"Hi! Looks like you have JavaScript turned off. Click here to view comments, keep in mind they may take a bit longer to load.": "자바스크립트가 꺼져 있는 것 같습니다! 댓글을 보려면 여기를 클릭하세요. 댓글을 로드하는 데 시간이 조금 더 걸릴 수 있습니다.",
|
"Hi! Looks like you have JavaScript turned off. Click here to view comments, keep in mind they may take a bit longer to load.": "자바스크립트가 꺼져 있는 것 같습니다! 댓글을 보려면 여기를 클릭하세요. 댓글을 로드하는 데 시간이 조금 더 걸릴 수 있습니다.",
|
||||||
"Shared `x`": "`x` 업로드",
|
"Shared `x`": "`x` 업로드",
|
||||||
"Whitelisted regions: ": "차단되지 않은 지역: ",
|
"Whitelisted regions: ": "차단되지 않은 지역: ",
|
||||||
|
@ -289,7 +289,7 @@
|
||||||
"Empty playlist": "재생목록 비어 있음",
|
"Empty playlist": "재생목록 비어 있음",
|
||||||
"Show annotations": "주석 보이기",
|
"Show annotations": "주석 보이기",
|
||||||
"Hide annotations": "주석 숨기기",
|
"Hide annotations": "주석 숨기기",
|
||||||
"Switch Invidious Instance": "Invidious 인스턴스 변경",
|
"Switch Invidious Instance": "인비디어스 인스턴스 변경",
|
||||||
"Spanish": "스페인어",
|
"Spanish": "스페인어",
|
||||||
"Southern Sotho": "소토어",
|
"Southern Sotho": "소토어",
|
||||||
"Somali": "소말리어",
|
"Somali": "소말리어",
|
||||||
|
@ -329,7 +329,7 @@
|
||||||
"Swedish": "스웨덴어",
|
"Swedish": "스웨덴어",
|
||||||
"Spanish (Latin America)": "스페인어 (라틴 아메리카)",
|
"Spanish (Latin America)": "스페인어 (라틴 아메리카)",
|
||||||
"comments_points_count_0": "{{count}} 포인트",
|
"comments_points_count_0": "{{count}} 포인트",
|
||||||
"Invidious Private Feed for `x`": "`x` 에 대한 Invidious 비공개 피드",
|
"Invidious Private Feed for `x`": "`x` 에 대한 인비디어스 비공개 피드",
|
||||||
"Premieres `x`": "최초 공개 `x`",
|
"Premieres `x`": "최초 공개 `x`",
|
||||||
"Premieres in `x`": "`x` 후 최초 공개",
|
"Premieres in `x`": "`x` 후 최초 공개",
|
||||||
"next_steps_error_message": "다음 방법을 시도해 보세요: ",
|
"next_steps_error_message": "다음 방법을 시도해 보세요: ",
|
||||||
|
@ -408,7 +408,7 @@
|
||||||
"preferences_quality_dash_option_1080p": "1080p",
|
"preferences_quality_dash_option_1080p": "1080p",
|
||||||
"preferences_quality_dash_option_worst": "최저",
|
"preferences_quality_dash_option_worst": "최저",
|
||||||
"preferences_watch_history_label": "시청 기록 저장: ",
|
"preferences_watch_history_label": "시청 기록 저장: ",
|
||||||
"invidious": "Invidious",
|
"invidious": "인비디어스",
|
||||||
"preferences_quality_option_small": "낮음",
|
"preferences_quality_option_small": "낮음",
|
||||||
"preferences_quality_dash_option_auto": "자동",
|
"preferences_quality_dash_option_auto": "자동",
|
||||||
"preferences_quality_dash_option_480p": "480p",
|
"preferences_quality_dash_option_480p": "480p",
|
||||||
|
@ -453,7 +453,7 @@
|
||||||
"channel_tab_streams_label": "실시간 스트리밍",
|
"channel_tab_streams_label": "실시간 스트리밍",
|
||||||
"channel_tab_channels_label": "채널",
|
"channel_tab_channels_label": "채널",
|
||||||
"channel_tab_playlists_label": "재생목록",
|
"channel_tab_playlists_label": "재생목록",
|
||||||
"Standard YouTube license": "표준 YouTube 라이선스",
|
"Standard YouTube license": "표준 유튜브 라이선스",
|
||||||
"Song: ": "제목: ",
|
"Song: ": "제목: ",
|
||||||
"Channel Sponsor": "채널 스폰서",
|
"Channel Sponsor": "채널 스폰서",
|
||||||
"Album: ": "앨범: ",
|
"Album: ": "앨범: ",
|
||||||
|
|
|
@ -322,13 +322,13 @@
|
||||||
"channel_tab_community_label": "Gemenskap",
|
"channel_tab_community_label": "Gemenskap",
|
||||||
"search_filters_sort_option_relevance": "relevans",
|
"search_filters_sort_option_relevance": "relevans",
|
||||||
"search_filters_sort_option_rating": "vurdering",
|
"search_filters_sort_option_rating": "vurdering",
|
||||||
"search_filters_sort_option_date": "dato",
|
"search_filters_sort_option_date": "Opplastingsdato",
|
||||||
"search_filters_sort_option_views": "visninger",
|
"search_filters_sort_option_views": "visninger",
|
||||||
"search_filters_type_label": "innholdstype",
|
"search_filters_type_label": "innholdstype",
|
||||||
"search_filters_duration_label": "varighet",
|
"search_filters_duration_label": "varighet",
|
||||||
"search_filters_features_label": "funksjoner",
|
"search_filters_features_label": "funksjoner",
|
||||||
"search_filters_sort_label": "sorter",
|
"search_filters_sort_label": "sorter",
|
||||||
"search_filters_date_option_hour": "time",
|
"search_filters_date_option_hour": "Siste time",
|
||||||
"search_filters_date_option_today": "i dag",
|
"search_filters_date_option_today": "i dag",
|
||||||
"search_filters_date_option_week": "uke",
|
"search_filters_date_option_week": "uke",
|
||||||
"search_filters_date_option_month": "måned",
|
"search_filters_date_option_month": "måned",
|
||||||
|
@ -494,5 +494,7 @@
|
||||||
"carousel_slide": "Lysark {{current}} av {{total}}",
|
"carousel_slide": "Lysark {{current}} av {{total}}",
|
||||||
"carousel_skip": "Hopp over karusellen",
|
"carousel_skip": "Hopp over karusellen",
|
||||||
"Add to playlist": "Legg til i spilleliste",
|
"Add to playlist": "Legg til i spilleliste",
|
||||||
"Add to playlist: ": "Legg til i spilleliste: "
|
"Add to playlist: ": "Legg til i spilleliste: ",
|
||||||
|
"The Popular feed has been disabled by the administrator.": "Populært-kilden er koblet ut av administratoren.",
|
||||||
|
"toggle_theme": "Endre utseende"
|
||||||
}
|
}
|
||||||
|
|
|
@ -317,13 +317,13 @@
|
||||||
"channel_tab_community_label": "Gemeenschap",
|
"channel_tab_community_label": "Gemeenschap",
|
||||||
"search_filters_sort_option_relevance": "relevantie",
|
"search_filters_sort_option_relevance": "relevantie",
|
||||||
"search_filters_sort_option_rating": "beoordeling",
|
"search_filters_sort_option_rating": "beoordeling",
|
||||||
"search_filters_sort_option_date": "datum",
|
"search_filters_sort_option_date": "Upload datum",
|
||||||
"search_filters_sort_option_views": "keren bekeken",
|
"search_filters_sort_option_views": "keren bekeken",
|
||||||
"search_filters_type_label": "Type inhoud",
|
"search_filters_type_label": "Type inhoud",
|
||||||
"search_filters_duration_label": "duur",
|
"search_filters_duration_label": "duur",
|
||||||
"search_filters_features_label": "eigenschappen",
|
"search_filters_features_label": "eigenschappen",
|
||||||
"search_filters_sort_label": "sorteren",
|
"search_filters_sort_label": "sorteren",
|
||||||
"search_filters_date_option_hour": "uur",
|
"search_filters_date_option_hour": "Laatste uur",
|
||||||
"search_filters_date_option_today": "vandaag",
|
"search_filters_date_option_today": "vandaag",
|
||||||
"search_filters_date_option_week": "week",
|
"search_filters_date_option_week": "week",
|
||||||
"search_filters_date_option_month": "maand",
|
"search_filters_date_option_month": "maand",
|
||||||
|
@ -357,7 +357,7 @@
|
||||||
"footer_original_source_code": "Originele bron-code",
|
"footer_original_source_code": "Originele bron-code",
|
||||||
"footer_modfied_source_code": "Gewijzigde bron-code",
|
"footer_modfied_source_code": "Gewijzigde bron-code",
|
||||||
"adminprefs_modified_source_code_url_label": "URL naar gewijzigde bron-code-opslagplaats",
|
"adminprefs_modified_source_code_url_label": "URL naar gewijzigde bron-code-opslagplaats",
|
||||||
"next_steps_error_message": "Daarna moet u proberen om: ",
|
"next_steps_error_message": "Waarna u zou kunnen proberen om: ",
|
||||||
"footer_source_code": "Bron-code",
|
"footer_source_code": "Bron-code",
|
||||||
"search_filters_duration_option_long": "Lang (> 20 minuten)",
|
"search_filters_duration_option_long": "Lang (> 20 minuten)",
|
||||||
"preferences_quality_option_dash": "DASH (adaptieve kwaliteit)",
|
"preferences_quality_option_dash": "DASH (adaptieve kwaliteit)",
|
||||||
|
@ -477,7 +477,7 @@
|
||||||
"Song: ": "Lied: ",
|
"Song: ": "Lied: ",
|
||||||
"generic_channels_count": "{{count}} kanaal",
|
"generic_channels_count": "{{count}} kanaal",
|
||||||
"generic_channels_count_plural": "{{count}} kanalen",
|
"generic_channels_count_plural": "{{count}} kanalen",
|
||||||
"Popular enabled: ": "Populair geactiveerd: ",
|
"Popular enabled: ": "Populair ingeschakeld: ",
|
||||||
"channel_tab_playlists_label": "Afspeellijsten",
|
"channel_tab_playlists_label": "Afspeellijsten",
|
||||||
"generic_button_edit": "Bewerken",
|
"generic_button_edit": "Bewerken",
|
||||||
"Music in this video": "Muziek in deze video",
|
"Music in this video": "Muziek in deze video",
|
||||||
|
|
|
@ -508,7 +508,7 @@
|
||||||
"toggle_theme": "Trocar tema",
|
"toggle_theme": "Trocar tema",
|
||||||
"Add to playlist": "Adicionar à lista de reprodução",
|
"Add to playlist": "Adicionar à lista de reprodução",
|
||||||
"Add to playlist: ": "Adicionar à lista de reprodução: ",
|
"Add to playlist: ": "Adicionar à lista de reprodução: ",
|
||||||
"Answer": "Resposta",
|
"Answer": "Responder",
|
||||||
"Search for videos": "Procurar vídeos",
|
"Search for videos": "Procurar vídeos",
|
||||||
"carousel_slide": "Diapositivo {{current}} de{{total}}",
|
"carousel_slide": "Diapositivo {{current}} de{{total}}",
|
||||||
"carousel_skip": "Ignorar carrossel",
|
"carousel_skip": "Ignorar carrossel",
|
||||||
|
|
|
@ -509,6 +509,9 @@
|
||||||
"Add to playlist: ": "Добавить в плейлист: ",
|
"Add to playlist: ": "Добавить в плейлист: ",
|
||||||
"Answer": "Ответить",
|
"Answer": "Ответить",
|
||||||
"Search for videos": "Поиск видео",
|
"Search for videos": "Поиск видео",
|
||||||
"The Popular feed has been disabled by the administrator.": "Популярная лента была отключена администратором.",
|
"The Popular feed has been disabled by the administrator.": "Лента популярного была отключена администратором.",
|
||||||
"toggle_theme": "Переключатель тем"
|
"toggle_theme": "Переключатель тем",
|
||||||
|
"carousel_slide": "Пролистано {{current}} из {{total}}",
|
||||||
|
"carousel_skip": "Пропустить всё",
|
||||||
|
"carousel_go_to": "Перейти к странице `x`"
|
||||||
}
|
}
|
||||||
|
|
|
@ -257,13 +257,13 @@
|
||||||
"Video mode": "Mënyrë video",
|
"Video mode": "Mënyrë video",
|
||||||
"channel_tab_videos_label": "Video",
|
"channel_tab_videos_label": "Video",
|
||||||
"search_filters_sort_option_rating": "Vlerësim",
|
"search_filters_sort_option_rating": "Vlerësim",
|
||||||
"search_filters_sort_option_date": "Datë Ngarkimi",
|
"search_filters_sort_option_date": "Datë ngarkimi",
|
||||||
"search_filters_sort_option_views": "Numër parjesh",
|
"search_filters_sort_option_views": "Numër parjesh",
|
||||||
"search_filters_type_label": "Lloj",
|
"search_filters_type_label": "Lloj",
|
||||||
"search_filters_duration_label": "Kohëzgjatje",
|
"search_filters_duration_label": "Kohëzgjatje",
|
||||||
"search_filters_features_label": "Veçori",
|
"search_filters_features_label": "Veçori",
|
||||||
"search_filters_sort_label": "Renditi Sipas",
|
"search_filters_sort_label": "Renditi Sipas",
|
||||||
"search_filters_date_option_hour": "Orën e Fundit",
|
"search_filters_date_option_hour": "Orën e fundit",
|
||||||
"search_filters_date_option_today": "Sot",
|
"search_filters_date_option_today": "Sot",
|
||||||
"search_filters_duration_option_long": "E gjatë (> 20 minuta)",
|
"search_filters_duration_option_long": "E gjatë (> 20 minuta)",
|
||||||
"search_filters_features_option_hd": "HD",
|
"search_filters_features_option_hd": "HD",
|
||||||
|
@ -435,7 +435,7 @@
|
||||||
"tokens_count_plural": "{{count}} tokenë",
|
"tokens_count_plural": "{{count}} tokenë",
|
||||||
"preferences_save_player_pos_label": "Mba mend pozicionin e luajtjes: ",
|
"preferences_save_player_pos_label": "Mba mend pozicionin e luajtjes: ",
|
||||||
"Import Invidious data": "Importoni të dhëna JSON Invidious",
|
"Import Invidious data": "Importoni të dhëna JSON Invidious",
|
||||||
"Import YouTube subscriptions": "Importoni pajtime YouTube/OPML",
|
"Import YouTube subscriptions": "Importoni pajtime YouTube CSV ose OPML",
|
||||||
"Export data as JSON": "Eksportoji të dhënat Invidious si JSON",
|
"Export data as JSON": "Eksportoji të dhënat Invidious si JSON",
|
||||||
"preferences_vr_mode_label": "Video me ndërveprim 360 gradë (lyp WebGL): ",
|
"preferences_vr_mode_label": "Video me ndërveprim 360 gradë (lyp WebGL): ",
|
||||||
"Shared `x`": "Ndarë me të tjerë më `x`",
|
"Shared `x`": "Ndarë me të tjerë më `x`",
|
||||||
|
@ -484,5 +484,13 @@
|
||||||
"Import YouTube watch history (.json)": "Importo historik parjesh YouTube (.json)",
|
"Import YouTube watch history (.json)": "Importo historik parjesh YouTube (.json)",
|
||||||
"preferences_local_label": "Video përmes ndërmjetësi: ",
|
"preferences_local_label": "Video përmes ndërmjetësi: ",
|
||||||
"Fallback captions: ": "Titra nga halli: ",
|
"Fallback captions: ": "Titra nga halli: ",
|
||||||
"Erroneous challenge": "Zgjidhje e gabuar"
|
"Erroneous challenge": "Zgjidhje e gabuar",
|
||||||
|
"Add to playlist: ": "Shtoje te luajlistë: ",
|
||||||
|
"Add to playlist": "Shtoje te luajlistë",
|
||||||
|
"Answer": "Përgjigje",
|
||||||
|
"Search for videos": "Kërko për video",
|
||||||
|
"The Popular feed has been disabled by the administrator.": "Prurja Popullore është çaktivizuar nga përgjegjësi.",
|
||||||
|
"carousel_skip": "Anashkaloje Rrotullamen",
|
||||||
|
"carousel_slide": "Diapozitiv {{current}} nga {{total}}",
|
||||||
|
"carousel_go_to": "Kalo te diapozitivi `x`"
|
||||||
}
|
}
|
||||||
|
|
|
@ -320,13 +320,13 @@
|
||||||
"channel_tab_community_label": "Gemenskap",
|
"channel_tab_community_label": "Gemenskap",
|
||||||
"search_filters_sort_option_relevance": "Relevans",
|
"search_filters_sort_option_relevance": "Relevans",
|
||||||
"search_filters_sort_option_rating": "Rankning",
|
"search_filters_sort_option_rating": "Rankning",
|
||||||
"search_filters_sort_option_date": "Uppladdnings Datum",
|
"search_filters_sort_option_date": "Uppladdnings datum",
|
||||||
"search_filters_sort_option_views": "Visningar",
|
"search_filters_sort_option_views": "Visningar",
|
||||||
"search_filters_type_label": "Typ",
|
"search_filters_type_label": "Typ",
|
||||||
"search_filters_duration_label": "Varaktighet",
|
"search_filters_duration_label": "Varaktighet",
|
||||||
"search_filters_features_label": "Funktioner",
|
"search_filters_features_label": "Funktioner",
|
||||||
"search_filters_sort_label": "Sortera efter",
|
"search_filters_sort_label": "Sortera efter",
|
||||||
"search_filters_date_option_hour": "Senaste Timmen",
|
"search_filters_date_option_hour": "Senaste timmen",
|
||||||
"search_filters_date_option_today": "Idag",
|
"search_filters_date_option_today": "Idag",
|
||||||
"search_filters_date_option_week": "Denna vecka",
|
"search_filters_date_option_week": "Denna vecka",
|
||||||
"search_filters_date_option_month": "Denna månad",
|
"search_filters_date_option_month": "Denna månad",
|
||||||
|
|
|
@ -322,13 +322,13 @@
|
||||||
"channel_tab_community_label": "Topluluk",
|
"channel_tab_community_label": "Topluluk",
|
||||||
"search_filters_sort_option_relevance": "İlgi",
|
"search_filters_sort_option_relevance": "İlgi",
|
||||||
"search_filters_sort_option_rating": "Değerlendirme",
|
"search_filters_sort_option_rating": "Değerlendirme",
|
||||||
"search_filters_sort_option_date": "Yükleme Tarihi",
|
"search_filters_sort_option_date": "Yükleme tarihi",
|
||||||
"search_filters_sort_option_views": "Görüntüleme Sayısı",
|
"search_filters_sort_option_views": "Görüntüleme Sayısı",
|
||||||
"search_filters_type_label": "Tür",
|
"search_filters_type_label": "Tür",
|
||||||
"search_filters_duration_label": "Süre",
|
"search_filters_duration_label": "Süre",
|
||||||
"search_filters_features_label": "Özellikler",
|
"search_filters_features_label": "Özellikler",
|
||||||
"search_filters_sort_label": "Sıralama Ölçütü",
|
"search_filters_sort_label": "Sıralama Ölçütü",
|
||||||
"search_filters_date_option_hour": "Son Saat",
|
"search_filters_date_option_hour": "Son saat",
|
||||||
"search_filters_date_option_today": "Bugün",
|
"search_filters_date_option_today": "Bugün",
|
||||||
"search_filters_date_option_week": "Bu Hafta",
|
"search_filters_date_option_week": "Bu Hafta",
|
||||||
"search_filters_date_option_month": "Bu Ay",
|
"search_filters_date_option_month": "Bu Ay",
|
||||||
|
|
|
@ -455,7 +455,7 @@
|
||||||
"search_filters_date_option_week": "Цей тиждень",
|
"search_filters_date_option_week": "Цей тиждень",
|
||||||
"search_filters_type_label": "Тип",
|
"search_filters_type_label": "Тип",
|
||||||
"search_filters_type_option_channel": "Канал",
|
"search_filters_type_option_channel": "Канал",
|
||||||
"search_message_use_another_instance": " Можете також <a href=\"`x`\">пошукати іншим сервером</a>.",
|
"search_message_use_another_instance": "Можете також <a href=\"`x`\">пошукати на іншому сервері</a>.",
|
||||||
"search_filters_title": "Фільтри",
|
"search_filters_title": "Фільтри",
|
||||||
"search_filters_date_option_hour": "Остання година",
|
"search_filters_date_option_hour": "Остання година",
|
||||||
"search_filters_date_option_month": "Цей місяць",
|
"search_filters_date_option_month": "Цей місяць",
|
||||||
|
@ -472,7 +472,7 @@
|
||||||
"search_filters_features_option_three_sixty": "360°",
|
"search_filters_features_option_three_sixty": "360°",
|
||||||
"search_filters_features_option_hdr": "HDR",
|
"search_filters_features_option_hdr": "HDR",
|
||||||
"search_filters_sort_label": "Спершу",
|
"search_filters_sort_label": "Спершу",
|
||||||
"search_filters_sort_option_date": "Нещодавні",
|
"search_filters_sort_option_date": "Дата вивантаження",
|
||||||
"search_filters_apply_button": "Застосувати фільтри",
|
"search_filters_apply_button": "Застосувати фільтри",
|
||||||
"search_filters_features_option_vr180": "VR180",
|
"search_filters_features_option_vr180": "VR180",
|
||||||
"search_filters_features_option_purchased": "Придбано",
|
"search_filters_features_option_purchased": "Придбано",
|
||||||
|
|
|
@ -338,13 +338,13 @@
|
||||||
"channel_tab_community_label": "社群",
|
"channel_tab_community_label": "社群",
|
||||||
"search_filters_sort_option_relevance": "關聯",
|
"search_filters_sort_option_relevance": "關聯",
|
||||||
"search_filters_sort_option_rating": "評分",
|
"search_filters_sort_option_rating": "評分",
|
||||||
"search_filters_sort_option_date": "日期",
|
"search_filters_sort_option_date": "上傳日期",
|
||||||
"search_filters_sort_option_views": "檢視",
|
"search_filters_sort_option_views": "檢視",
|
||||||
"search_filters_type_label": "內容類型",
|
"search_filters_type_label": "內容類型",
|
||||||
"search_filters_duration_label": "時長",
|
"search_filters_duration_label": "時長",
|
||||||
"search_filters_features_label": "特色",
|
"search_filters_features_label": "特色",
|
||||||
"search_filters_sort_label": "排序",
|
"search_filters_sort_label": "排序",
|
||||||
"search_filters_date_option_hour": "小時",
|
"search_filters_date_option_hour": "最後一小時",
|
||||||
"search_filters_date_option_today": "今天",
|
"search_filters_date_option_today": "今天",
|
||||||
"search_filters_date_option_week": "週",
|
"search_filters_date_option_week": "週",
|
||||||
"search_filters_date_option_month": "月",
|
"search_filters_date_option_month": "月",
|
||||||
|
|
2
mocks
2
mocks
|
@ -1 +1 @@
|
||||||
Subproject commit 11ec372f72747c09d48ffef04843f72be67d5b54
|
Subproject commit b55d58dea94f7144ff0205857dfa70ec14eaa872
|
|
@ -27,8 +27,8 @@ Spectator.describe Invidious::Hashtag do
|
||||||
expect(video_11.length_seconds).to eq((56.minutes + 41.seconds).total_seconds.to_i32)
|
expect(video_11.length_seconds).to eq((56.minutes + 41.seconds).total_seconds.to_i32)
|
||||||
expect(video_11.views).to eq(40_504_893)
|
expect(video_11.views).to eq(40_504_893)
|
||||||
|
|
||||||
expect(video_11.live_now).to be_false
|
expect(video_11.badges.live_now?).to be_false
|
||||||
expect(video_11.premium).to be_false
|
expect(video_11.badges.premium?).to be_false
|
||||||
expect(video_11.premiere_timestamp).to be_nil
|
expect(video_11.premiere_timestamp).to be_nil
|
||||||
|
|
||||||
#
|
#
|
||||||
|
@ -49,8 +49,8 @@ Spectator.describe Invidious::Hashtag do
|
||||||
expect(video_35.length_seconds).to eq((3.minutes + 14.seconds).total_seconds.to_i32)
|
expect(video_35.length_seconds).to eq((3.minutes + 14.seconds).total_seconds.to_i32)
|
||||||
expect(video_35.views).to eq(30_790_049)
|
expect(video_35.views).to eq(30_790_049)
|
||||||
|
|
||||||
expect(video_35.live_now).to be_false
|
expect(video_35.badges.live_now?).to be_false
|
||||||
expect(video_35.premium).to be_false
|
expect(video_35.badges.premium?).to be_false
|
||||||
expect(video_35.premiere_timestamp).to be_nil
|
expect(video_35.premiere_timestamp).to be_nil
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -80,8 +80,8 @@ Spectator.describe Invidious::Hashtag do
|
||||||
expect(video_41.length_seconds).to eq((1.hour).total_seconds.to_i32)
|
expect(video_41.length_seconds).to eq((1.hour).total_seconds.to_i32)
|
||||||
expect(video_41.views).to eq(63_240)
|
expect(video_41.views).to eq(63_240)
|
||||||
|
|
||||||
expect(video_41.live_now).to be_false
|
expect(video_41.badges.live_now?).to be_false
|
||||||
expect(video_41.premium).to be_false
|
expect(video_41.badges.premium?).to be_false
|
||||||
expect(video_41.premiere_timestamp).to be_nil
|
expect(video_41.premiere_timestamp).to be_nil
|
||||||
|
|
||||||
#
|
#
|
||||||
|
@ -102,8 +102,8 @@ Spectator.describe Invidious::Hashtag do
|
||||||
expect(video_48.length_seconds).to eq((35.minutes + 46.seconds).total_seconds.to_i32)
|
expect(video_48.length_seconds).to eq((35.minutes + 46.seconds).total_seconds.to_i32)
|
||||||
expect(video_48.views).to eq(68_704)
|
expect(video_48.views).to eq(68_704)
|
||||||
|
|
||||||
expect(video_48.live_now).to be_false
|
expect(video_48.badges.live_now?).to be_false
|
||||||
expect(video_48.premium).to be_false
|
expect(video_48.badges.premium?).to be_false
|
||||||
expect(video_48.premiere_timestamp).to be_nil
|
expect(video_48.premiere_timestamp).to be_nil
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -17,8 +17,8 @@ Spectator.describe "parse_video_info" do
|
||||||
# Basic video infos
|
# Basic video infos
|
||||||
|
|
||||||
expect(info["title"].as_s).to eq("I Gave My 100,000,000th Subscriber An Island")
|
expect(info["title"].as_s).to eq("I Gave My 100,000,000th Subscriber An Island")
|
||||||
expect(info["views"].as_i).to eq(126_573_823)
|
expect(info["views"].as_i).to eq(220_226_287)
|
||||||
expect(info["likes"].as_i).to eq(5_157_654)
|
expect(info["likes"].as_i).to eq(6_870_691)
|
||||||
|
|
||||||
# For some reason the video length from VideoDetails and the
|
# For some reason the video length from VideoDetails and the
|
||||||
# one from microformat differs by 1s...
|
# one from microformat differs by 1s...
|
||||||
|
@ -48,12 +48,12 @@ Spectator.describe "parse_video_info" do
|
||||||
|
|
||||||
expect(info["relatedVideos"].as_a.size).to eq(20)
|
expect(info["relatedVideos"].as_a.size).to eq(20)
|
||||||
|
|
||||||
expect(info["relatedVideos"][0]["id"]).to eq("Hwybp38GnZw")
|
expect(info["relatedVideos"][0]["id"]).to eq("krsBRQbOPQ4")
|
||||||
expect(info["relatedVideos"][0]["title"]).to eq("I Built Willy Wonka's Chocolate Factory!")
|
expect(info["relatedVideos"][0]["title"]).to eq("$1 vs $250,000,000 Private Island!")
|
||||||
expect(info["relatedVideos"][0]["author"]).to eq("MrBeast")
|
expect(info["relatedVideos"][0]["author"]).to eq("MrBeast")
|
||||||
expect(info["relatedVideos"][0]["ucid"]).to eq("UCX6OQ3DkcsbYNE6H8uQQuVA")
|
expect(info["relatedVideos"][0]["ucid"]).to eq("UCX6OQ3DkcsbYNE6H8uQQuVA")
|
||||||
expect(info["relatedVideos"][0]["view_count"]).to eq("179877630")
|
expect(info["relatedVideos"][0]["view_count"]).to eq("230617484")
|
||||||
expect(info["relatedVideos"][0]["short_view_count"]).to eq("179M")
|
expect(info["relatedVideos"][0]["short_view_count"]).to eq("230M")
|
||||||
expect(info["relatedVideos"][0]["author_verified"]).to eq("true")
|
expect(info["relatedVideos"][0]["author_verified"]).to eq("true")
|
||||||
|
|
||||||
# Description
|
# Description
|
||||||
|
@ -76,11 +76,11 @@ Spectator.describe "parse_video_info" do
|
||||||
expect(info["ucid"].as_s).to eq("UCX6OQ3DkcsbYNE6H8uQQuVA")
|
expect(info["ucid"].as_s).to eq("UCX6OQ3DkcsbYNE6H8uQQuVA")
|
||||||
|
|
||||||
expect(info["authorThumbnail"].as_s).to eq(
|
expect(info["authorThumbnail"].as_s).to eq(
|
||||||
"https://yt3.ggpht.com/ytc/AL5GRJVuqw82ERvHzsmBxL7avr1dpBtsVIXcEzBPZaloFg=s48-c-k-c0x00ffffff-no-rj"
|
"https://yt3.ggpht.com/fxGKYucJAVme-Yz4fsdCroCFCrANWqw0ql4GYuvx8Uq4l_euNJHgE-w9MTkLQA805vWCi-kE0g=s48-c-k-c0x00ffffff-no-rj"
|
||||||
)
|
)
|
||||||
|
|
||||||
expect(info["authorVerified"].as_bool).to be_true
|
expect(info["authorVerified"].as_bool).to be_true
|
||||||
expect(info["subCountText"].as_s).to eq("143M")
|
expect(info["subCountText"].as_s).to eq("320M")
|
||||||
end
|
end
|
||||||
|
|
||||||
it "parses a regular video with no descrition/comments" do
|
it "parses a regular video with no descrition/comments" do
|
||||||
|
@ -99,8 +99,8 @@ Spectator.describe "parse_video_info" do
|
||||||
# Basic video infos
|
# Basic video infos
|
||||||
|
|
||||||
expect(info["title"].as_s).to eq("Chris Rea - Auberge")
|
expect(info["title"].as_s).to eq("Chris Rea - Auberge")
|
||||||
expect(info["views"].as_i).to eq(10_943_126)
|
expect(info["views"].as_i).to eq(14_324_584)
|
||||||
expect(info["likes"].as_i).to eq(0)
|
expect(info["likes"].as_i).to eq(35_870)
|
||||||
expect(info["lengthSeconds"].as_i).to eq(283_i64)
|
expect(info["lengthSeconds"].as_i).to eq(283_i64)
|
||||||
expect(info["published"].as_s).to eq("2012-05-21T00:00:00Z")
|
expect(info["published"].as_s).to eq("2012-05-21T00:00:00Z")
|
||||||
|
|
||||||
|
@ -132,14 +132,14 @@ Spectator.describe "parse_video_info" do
|
||||||
|
|
||||||
# Related videos
|
# Related videos
|
||||||
|
|
||||||
expect(info["relatedVideos"].as_a.size).to eq(19)
|
expect(info["relatedVideos"].as_a.size).to eq(20)
|
||||||
|
|
||||||
expect(info["relatedVideos"][0]["id"]).to eq("Ww3KeZ2_Yv4")
|
expect(info["relatedVideos"][0]["id"]).to eq("gUUdQfnshJ4")
|
||||||
expect(info["relatedVideos"][0]["title"]).to eq("Chris Rea")
|
expect(info["relatedVideos"][0]["title"]).to eq("Chris Rea - The Road To Hell 1989 Full Version")
|
||||||
expect(info["relatedVideos"][0]["author"]).to eq("PanMusic")
|
expect(info["relatedVideos"][0]["author"]).to eq("NEA ZIXNH")
|
||||||
expect(info["relatedVideos"][0]["ucid"]).to eq("UCsKAPSuh1iNbLWUga_igPyA")
|
expect(info["relatedVideos"][0]["ucid"]).to eq("UCYMEOGcvav3gCgImK2J07CQ")
|
||||||
expect(info["relatedVideos"][0]["view_count"]).to eq("31581")
|
expect(info["relatedVideos"][0]["view_count"]).to eq("53298661")
|
||||||
expect(info["relatedVideos"][0]["short_view_count"]).to eq("31K")
|
expect(info["relatedVideos"][0]["short_view_count"]).to eq("53M")
|
||||||
expect(info["relatedVideos"][0]["author_verified"]).to eq("false")
|
expect(info["relatedVideos"][0]["author_verified"]).to eq("false")
|
||||||
|
|
||||||
# Description
|
# Description
|
||||||
|
@ -156,11 +156,13 @@ Spectator.describe "parse_video_info" do
|
||||||
|
|
||||||
# Author infos
|
# Author infos
|
||||||
|
|
||||||
expect(info["author"].as_s).to eq("ChrisReaOfficial")
|
expect(info["author"].as_s).to eq("ChrisReaVideos")
|
||||||
expect(info["ucid"].as_s).to eq("UC_5q6nWPbD30-y6oiWF_oNA")
|
expect(info["ucid"].as_s).to eq("UC_5q6nWPbD30-y6oiWF_oNA")
|
||||||
|
|
||||||
expect(info["authorThumbnail"].as_s).to be_empty
|
expect(info["authorThumbnail"].as_s).to eq(
|
||||||
|
"https://yt3.ggpht.com/ytc/AIdro_n71nsegpKfjeRKwn1JJmK5IVMh_7j5m_h3_1KnUUg=s48-c-k-c0x00ffffff-no-rj"
|
||||||
|
)
|
||||||
expect(info["authorVerified"].as_bool).to be_false
|
expect(info["authorVerified"].as_bool).to be_false
|
||||||
expect(info["subCountText"].as_s).to eq("-")
|
expect(info["subCountText"].as_s).to eq("3.11K")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -189,6 +189,8 @@ Invidious::Jobs.register Invidious::Jobs::NotificationJob.new(CONNECTION_CHANNEL
|
||||||
|
|
||||||
Invidious::Jobs.register Invidious::Jobs::ClearExpiredItemsJob.new
|
Invidious::Jobs.register Invidious::Jobs::ClearExpiredItemsJob.new
|
||||||
|
|
||||||
|
Invidious::Jobs.register Invidious::Jobs::InstanceListRefreshJob.new
|
||||||
|
|
||||||
Invidious::Jobs.start_all
|
Invidious::Jobs.start_all
|
||||||
|
|
||||||
def popular_videos
|
def popular_videos
|
||||||
|
|
|
@ -223,7 +223,7 @@ def fetch_channel(ucid, pull_all_videos : Bool)
|
||||||
length_seconds = channel_video.try &.length_seconds
|
length_seconds = channel_video.try &.length_seconds
|
||||||
length_seconds ||= 0
|
length_seconds ||= 0
|
||||||
|
|
||||||
live_now = channel_video.try &.live_now
|
live_now = channel_video.try &.badges.live_now?
|
||||||
live_now ||= false
|
live_now ||= false
|
||||||
|
|
||||||
premiere_timestamp = channel_video.try &.premiere_timestamp
|
premiere_timestamp = channel_video.try &.premiere_timestamp
|
||||||
|
@ -275,7 +275,7 @@ def fetch_channel(ucid, pull_all_videos : Bool)
|
||||||
ucid: video.ucid,
|
ucid: video.ucid,
|
||||||
author: video.author,
|
author: video.author,
|
||||||
length_seconds: video.length_seconds,
|
length_seconds: video.length_seconds,
|
||||||
live_now: video.live_now,
|
live_now: video.badges.live_now?,
|
||||||
premiere_timestamp: video.premiere_timestamp,
|
premiere_timestamp: video.premiere_timestamp,
|
||||||
views: video.views,
|
views: video.views,
|
||||||
})
|
})
|
||||||
|
|
|
@ -13,6 +13,7 @@ struct ConfigPreferences
|
||||||
|
|
||||||
property annotations : Bool = false
|
property annotations : Bool = false
|
||||||
property annotations_subscribed : Bool = false
|
property annotations_subscribed : Bool = false
|
||||||
|
property preload : Bool = true
|
||||||
property autoplay : Bool = false
|
property autoplay : Bool = false
|
||||||
property captions : Array(String) = ["", "", ""]
|
property captions : Array(String) = ["", "", ""]
|
||||||
property comments : Array(String) = ["youtube", ""]
|
property comments : Array(String) = ["youtube", ""]
|
||||||
|
|
|
@ -43,6 +43,8 @@ def error_template_helper(env : HTTP::Server::Context, status_code : Int32, exce
|
||||||
# URLs for the error message below
|
# URLs for the error message below
|
||||||
url_faq = "https://github.com/iv-org/documentation/blob/master/docs/faq.md"
|
url_faq = "https://github.com/iv-org/documentation/blob/master/docs/faq.md"
|
||||||
url_search_issues = "https://github.com/iv-org/invidious/issues"
|
url_search_issues = "https://github.com/iv-org/invidious/issues"
|
||||||
|
url_search_issues += "?q=is:issue+is:open+"
|
||||||
|
url_search_issues += URI.encode_www_form("[Bug] #{issue_title}")
|
||||||
|
|
||||||
url_switch = "https://redirect.invidious.io" + env.request.resource
|
url_switch = "https://redirect.invidious.io" + env.request.resource
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,16 @@
|
||||||
|
@[Flags]
|
||||||
|
enum VideoBadges
|
||||||
|
LiveNow
|
||||||
|
Premium
|
||||||
|
ThreeD
|
||||||
|
FourK
|
||||||
|
New
|
||||||
|
EightK
|
||||||
|
VR180
|
||||||
|
VR360
|
||||||
|
ClosedCaptions
|
||||||
|
end
|
||||||
|
|
||||||
struct SearchVideo
|
struct SearchVideo
|
||||||
include DB::Serializable
|
include DB::Serializable
|
||||||
|
|
||||||
|
@ -9,10 +22,9 @@ struct SearchVideo
|
||||||
property views : Int64
|
property views : Int64
|
||||||
property description_html : String
|
property description_html : String
|
||||||
property length_seconds : Int32
|
property length_seconds : Int32
|
||||||
property live_now : Bool
|
|
||||||
property premium : Bool
|
|
||||||
property premiere_timestamp : Time?
|
property premiere_timestamp : Time?
|
||||||
property author_verified : Bool
|
property author_verified : Bool
|
||||||
|
property badges : VideoBadges
|
||||||
|
|
||||||
def to_xml(auto_generated, query_params, xml : XML::Builder)
|
def to_xml(auto_generated, query_params, xml : XML::Builder)
|
||||||
query_params["v"] = self.id
|
query_params["v"] = self.id
|
||||||
|
@ -88,13 +100,20 @@ struct SearchVideo
|
||||||
json.field "published", self.published.to_unix
|
json.field "published", self.published.to_unix
|
||||||
json.field "publishedText", translate(locale, "`x` ago", recode_date(self.published, locale))
|
json.field "publishedText", translate(locale, "`x` ago", recode_date(self.published, locale))
|
||||||
json.field "lengthSeconds", self.length_seconds
|
json.field "lengthSeconds", self.length_seconds
|
||||||
json.field "liveNow", self.live_now
|
json.field "liveNow", self.badges.live_now?
|
||||||
json.field "premium", self.premium
|
json.field "premium", self.badges.premium?
|
||||||
json.field "isUpcoming", self.upcoming?
|
json.field "isUpcoming", self.upcoming?
|
||||||
|
|
||||||
if self.premiere_timestamp
|
if self.premiere_timestamp
|
||||||
json.field "premiereTimestamp", self.premiere_timestamp.try &.to_unix
|
json.field "premiereTimestamp", self.premiere_timestamp.try &.to_unix
|
||||||
end
|
end
|
||||||
|
json.field "isNew", self.badges.new?
|
||||||
|
json.field "is4k", self.badges.four_k?
|
||||||
|
json.field "is8k", self.badges.eight_k?
|
||||||
|
json.field "isVr180", self.badges.vr180?
|
||||||
|
json.field "isVr360", self.badges.vr360?
|
||||||
|
json.field "is3d", self.badges.three_d?
|
||||||
|
json.field "hasCaptions", self.badges.closed_captions?
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -323,68 +323,6 @@ def parse_range(range)
|
||||||
return 0_i64, nil
|
return 0_i64, nil
|
||||||
end
|
end
|
||||||
|
|
||||||
def fetch_random_instance
|
|
||||||
begin
|
|
||||||
instance_api_client = make_client(URI.parse("https://api.invidious.io"))
|
|
||||||
|
|
||||||
# Timeouts
|
|
||||||
instance_api_client.connect_timeout = 10.seconds
|
|
||||||
instance_api_client.dns_timeout = 10.seconds
|
|
||||||
|
|
||||||
instance_list = JSON.parse(instance_api_client.get("/instances.json").body).as_a
|
|
||||||
instance_api_client.close
|
|
||||||
rescue Socket::ConnectError | IO::TimeoutError | JSON::ParseException
|
|
||||||
instance_list = [] of JSON::Any
|
|
||||||
end
|
|
||||||
|
|
||||||
filtered_instance_list = [] of String
|
|
||||||
|
|
||||||
instance_list.each do |data|
|
|
||||||
# TODO Check if current URL is onion instance and use .onion types if so.
|
|
||||||
if data[1]["type"] == "https"
|
|
||||||
# Instances can have statistics disabled, which is an requirement of version validation.
|
|
||||||
# as_nil? doesn't exist. Thus we'll have to handle the error raised if as_nil fails.
|
|
||||||
begin
|
|
||||||
data[1]["stats"].as_nil
|
|
||||||
next
|
|
||||||
rescue TypeCastError
|
|
||||||
end
|
|
||||||
|
|
||||||
# stats endpoint could also lack the software dict.
|
|
||||||
next if data[1]["stats"]["software"]?.nil?
|
|
||||||
|
|
||||||
# Makes sure the instance isn't too outdated.
|
|
||||||
if remote_version = data[1]["stats"]?.try &.["software"]?.try &.["version"]
|
|
||||||
remote_commit_date = remote_version.as_s.match(/\d{4}\.\d{2}\.\d{2}/)
|
|
||||||
next if !remote_commit_date
|
|
||||||
|
|
||||||
remote_commit_date = Time.parse(remote_commit_date[0], "%Y.%m.%d", Time::Location::UTC)
|
|
||||||
local_commit_date = Time.parse(CURRENT_VERSION, "%Y.%m.%d", Time::Location::UTC)
|
|
||||||
|
|
||||||
next if (remote_commit_date - local_commit_date).abs.days > 30
|
|
||||||
|
|
||||||
begin
|
|
||||||
data[1]["monitor"].as_nil
|
|
||||||
health = data[1]["monitor"].as_h["dailyRatios"][0].as_h["ratio"]
|
|
||||||
filtered_instance_list << data[0].as_s if health.to_s.to_f > 90
|
|
||||||
rescue TypeCastError
|
|
||||||
# We can't check the health if the monitoring is broken. Thus we'll just add it to the list
|
|
||||||
# and move on. Ideally we'll ignore any instance that has broken health monitoring but due to the fact that
|
|
||||||
# it's an error that often occurs with all the instances at the same time, we have to just skip the check.
|
|
||||||
filtered_instance_list << data[0].as_s
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# If for some reason no instances managed to get fetched successfully then we'll just redirect to redirect.invidious.io
|
|
||||||
if filtered_instance_list.size == 0
|
|
||||||
return "redirect.invidious.io"
|
|
||||||
end
|
|
||||||
|
|
||||||
return filtered_instance_list.sample(1)[0]
|
|
||||||
end
|
|
||||||
|
|
||||||
def reduce_uri(uri : URI | String, max_length : Int32 = 50, suffix : String = "…") : String
|
def reduce_uri(uri : URI | String, max_length : Int32 = 50, suffix : String = "…") : String
|
||||||
str = uri.to_s.sub(/^https?:\/\//, "")
|
str = uri.to_s.sub(/^https?:\/\//, "")
|
||||||
if str.size > max_length
|
if str.size > max_length
|
||||||
|
|
97
src/invidious/jobs/instance_refresh_job.cr
Normal file
97
src/invidious/jobs/instance_refresh_job.cr
Normal file
|
@ -0,0 +1,97 @@
|
||||||
|
class Invidious::Jobs::InstanceListRefreshJob < Invidious::Jobs::BaseJob
|
||||||
|
# We update the internals of a constant as so it can be accessed from anywhere
|
||||||
|
# within the codebase
|
||||||
|
#
|
||||||
|
# "INSTANCES" => Array(Tuple(String, String)) # region, instance
|
||||||
|
|
||||||
|
INSTANCES = {"INSTANCES" => [] of Tuple(String, String)}
|
||||||
|
|
||||||
|
def initialize
|
||||||
|
end
|
||||||
|
|
||||||
|
def begin
|
||||||
|
loop do
|
||||||
|
refresh_instances
|
||||||
|
LOGGER.info("InstanceListRefreshJob: Done, sleeping for 30 minutes")
|
||||||
|
sleep 30.minute
|
||||||
|
Fiber.yield
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Refreshes the list of instances used for redirects.
|
||||||
|
#
|
||||||
|
# Does the following three checks for each instance
|
||||||
|
# - Is it a clear-net instance?
|
||||||
|
# - Is it an instance with a good uptime?
|
||||||
|
# - Is it an updated instance?
|
||||||
|
private def refresh_instances
|
||||||
|
raw_instance_list = self.fetch_instances
|
||||||
|
filtered_instance_list = [] of Tuple(String, String)
|
||||||
|
|
||||||
|
raw_instance_list.each do |instance_data|
|
||||||
|
# TODO allow Tor hidden service instances when the current instance
|
||||||
|
# is also a hidden service. Same for i2p and any other non-clearnet instances.
|
||||||
|
begin
|
||||||
|
domain = instance_data[0]
|
||||||
|
info = instance_data[1]
|
||||||
|
stats = info["stats"]
|
||||||
|
|
||||||
|
next unless info["type"] == "https"
|
||||||
|
next if bad_uptime?(info["monitor"])
|
||||||
|
next if outdated?(stats["software"]["version"])
|
||||||
|
|
||||||
|
filtered_instance_list << {info["region"].as_s, domain.as_s}
|
||||||
|
rescue ex
|
||||||
|
if domain
|
||||||
|
LOGGER.info("InstanceListRefreshJob: failed to parse information from '#{domain}' because \"#{ex}\"\n\"#{ex.backtrace.join('\n')}\" ")
|
||||||
|
else
|
||||||
|
LOGGER.info("InstanceListRefreshJob: failed to parse information from an instance because \"#{ex}\"\n\"#{ex.backtrace.join('\n')}\" ")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if !filtered_instance_list.empty?
|
||||||
|
INSTANCES["INSTANCES"] = filtered_instance_list
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Fetches information regarding instances from api.invidious.io or an otherwise configured URL
|
||||||
|
private def fetch_instances : Array(JSON::Any)
|
||||||
|
begin
|
||||||
|
# We directly call the stdlib HTTP::Client here as it allows us to negate the effects
|
||||||
|
# of the force_resolve config option. This is needed as api.invidious.io does not support ipv6
|
||||||
|
# and as such the following request raises if we were to use force_resolve with the ipv6 value.
|
||||||
|
instance_api_client = HTTP::Client.new(URI.parse("https://api.invidious.io"))
|
||||||
|
|
||||||
|
# Timeouts
|
||||||
|
instance_api_client.connect_timeout = 10.seconds
|
||||||
|
instance_api_client.dns_timeout = 10.seconds
|
||||||
|
|
||||||
|
raw_instance_list = JSON.parse(instance_api_client.get("/instances.json").body).as_a
|
||||||
|
instance_api_client.close
|
||||||
|
rescue ex : Socket::ConnectError | IO::TimeoutError | JSON::ParseException
|
||||||
|
raw_instance_list = [] of JSON::Any
|
||||||
|
end
|
||||||
|
|
||||||
|
return raw_instance_list
|
||||||
|
end
|
||||||
|
|
||||||
|
# Checks if the given target instance is outdated
|
||||||
|
private def outdated?(target_instance_version) : Bool
|
||||||
|
remote_commit_date = target_instance_version.as_s.match(/\d{4}\.\d{2}\.\d{2}/)
|
||||||
|
return false if !remote_commit_date
|
||||||
|
|
||||||
|
remote_commit_date = Time.parse(remote_commit_date[0], "%Y.%m.%d", Time::Location::UTC)
|
||||||
|
local_commit_date = Time.parse(CURRENT_VERSION, "%Y.%m.%d", Time::Location::UTC)
|
||||||
|
|
||||||
|
return (remote_commit_date - local_commit_date).abs.days > 30
|
||||||
|
end
|
||||||
|
|
||||||
|
# Checks if the uptime of the target instance is greater than 90% over a 30 day period
|
||||||
|
private def bad_uptime?(target_instance_health_monitor) : Bool
|
||||||
|
return true if !target_instance_health_monitor["down"].as_bool == false
|
||||||
|
return true if target_instance_health_monitor["uptime"].as_f < 90
|
||||||
|
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
end
|
|
@ -270,7 +270,7 @@ end
|
||||||
|
|
||||||
def subscribe_playlist(user, playlist)
|
def subscribe_playlist(user, playlist)
|
||||||
playlist = InvidiousPlaylist.new({
|
playlist = InvidiousPlaylist.new({
|
||||||
title: playlist.title.byte_slice(0, 150),
|
title: playlist.title[..150],
|
||||||
id: playlist.id,
|
id: playlist.id,
|
||||||
author: user.email,
|
author: user.email,
|
||||||
description: "", # Max 5000 characters
|
description: "", # Max 5000 characters
|
||||||
|
|
|
@ -192,11 +192,9 @@ module Invidious::Routes::Feeds
|
||||||
views: views,
|
views: views,
|
||||||
description_html: description_html,
|
description_html: description_html,
|
||||||
length_seconds: 0,
|
length_seconds: 0,
|
||||||
live_now: false,
|
|
||||||
paid: false,
|
|
||||||
premium: false,
|
|
||||||
premiere_timestamp: nil,
|
premiere_timestamp: nil,
|
||||||
author_verified: false,
|
author_verified: false,
|
||||||
|
badges: VideoBadges::None,
|
||||||
})
|
})
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -40,7 +40,16 @@ module Invidious::Routes::Misc
|
||||||
|
|
||||||
def self.cross_instance_redirect(env)
|
def self.cross_instance_redirect(env)
|
||||||
referer = get_referer(env)
|
referer = get_referer(env)
|
||||||
instance_url = fetch_random_instance
|
|
||||||
|
instance_list = Invidious::Jobs::InstanceListRefreshJob::INSTANCES["INSTANCES"]
|
||||||
|
if instance_list.empty?
|
||||||
|
instance_url = "redirect.invidious.io"
|
||||||
|
else
|
||||||
|
# Sample returns an array
|
||||||
|
# Instances are packaged as {region, domain} in the instance list
|
||||||
|
instance_url = instance_list.sample(1)[0][1]
|
||||||
|
end
|
||||||
|
|
||||||
env.redirect "https://#{instance_url}#{referer}"
|
env.redirect "https://#{instance_url}#{referer}"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -27,6 +27,10 @@ module Invidious::Routes::PreferencesRoute
|
||||||
annotations_subscribed ||= "off"
|
annotations_subscribed ||= "off"
|
||||||
annotations_subscribed = annotations_subscribed == "on"
|
annotations_subscribed = annotations_subscribed == "on"
|
||||||
|
|
||||||
|
preload = env.params.body["preload"]?.try &.as(String)
|
||||||
|
preload ||= "off"
|
||||||
|
preload = preload == "on"
|
||||||
|
|
||||||
autoplay = env.params.body["autoplay"]?.try &.as(String)
|
autoplay = env.params.body["autoplay"]?.try &.as(String)
|
||||||
autoplay ||= "off"
|
autoplay ||= "off"
|
||||||
autoplay = autoplay == "on"
|
autoplay = autoplay == "on"
|
||||||
|
@ -144,6 +148,7 @@ module Invidious::Routes::PreferencesRoute
|
||||||
preferences = Preferences.from_json({
|
preferences = Preferences.from_json({
|
||||||
annotations: annotations,
|
annotations: annotations,
|
||||||
annotations_subscribed: annotations_subscribed,
|
annotations_subscribed: annotations_subscribed,
|
||||||
|
preload: preload,
|
||||||
autoplay: autoplay,
|
autoplay: autoplay,
|
||||||
captions: captions,
|
captions: captions,
|
||||||
comments: comments,
|
comments: comments,
|
||||||
|
|
|
@ -4,6 +4,7 @@ struct Preferences
|
||||||
|
|
||||||
property annotations : Bool = CONFIG.default_user_preferences.annotations
|
property annotations : Bool = CONFIG.default_user_preferences.annotations
|
||||||
property annotations_subscribed : Bool = CONFIG.default_user_preferences.annotations_subscribed
|
property annotations_subscribed : Bool = CONFIG.default_user_preferences.annotations_subscribed
|
||||||
|
property preload : Bool = CONFIG.default_user_preferences.preload
|
||||||
property autoplay : Bool = CONFIG.default_user_preferences.autoplay
|
property autoplay : Bool = CONFIG.default_user_preferences.autoplay
|
||||||
property automatic_instance_redirect : Bool = CONFIG.default_user_preferences.automatic_instance_redirect
|
property automatic_instance_redirect : Bool = CONFIG.default_user_preferences.automatic_instance_redirect
|
||||||
|
|
||||||
|
|
|
@ -26,12 +26,6 @@ struct Video
|
||||||
@[DB::Field(ignore: true)]
|
@[DB::Field(ignore: true)]
|
||||||
@captions = [] of Invidious::Videos::Captions::Metadata
|
@captions = [] of Invidious::Videos::Captions::Metadata
|
||||||
|
|
||||||
@[DB::Field(ignore: true)]
|
|
||||||
property adaptive_fmts : Array(Hash(String, JSON::Any))?
|
|
||||||
|
|
||||||
@[DB::Field(ignore: true)]
|
|
||||||
property fmt_stream : Array(Hash(String, JSON::Any))?
|
|
||||||
|
|
||||||
@[DB::Field(ignore: true)]
|
@[DB::Field(ignore: true)]
|
||||||
property description : String?
|
property description : String?
|
||||||
|
|
||||||
|
@ -98,72 +92,24 @@ struct Video
|
||||||
|
|
||||||
# Methods for parsing streaming data
|
# Methods for parsing streaming data
|
||||||
|
|
||||||
def convert_url(fmt)
|
def fmt_stream : Array(Hash(String, JSON::Any))
|
||||||
if cfr = fmt["signatureCipher"]?.try { |json| HTTP::Params.parse(json.as_s) }
|
if formats = info.dig?("streamingData", "formats")
|
||||||
sp = cfr["sp"]
|
return formats
|
||||||
url = URI.parse(cfr["url"])
|
.as_a.map(&.as_h)
|
||||||
params = url.query_params
|
.sort_by! { |f| f["width"]?.try &.as_i || 0 }
|
||||||
|
|
||||||
LOGGER.debug("Videos: Decoding '#{cfr}'")
|
|
||||||
|
|
||||||
unsig = DECRYPT_FUNCTION.try &.decrypt_signature(cfr["s"])
|
|
||||||
params[sp] = unsig if unsig
|
|
||||||
else
|
else
|
||||||
url = URI.parse(fmt["url"].as_s)
|
return [] of Hash(String, JSON::Any)
|
||||||
params = url.query_params
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
n = DECRYPT_FUNCTION.try &.decrypt_nsig(params["n"])
|
def adaptive_fmts : Array(Hash(String, JSON::Any))
|
||||||
params["n"] = n if n
|
if formats = info.dig?("streamingData", "adaptiveFormats")
|
||||||
|
return formats
|
||||||
if token = CONFIG.po_token
|
.as_a.map(&.as_h)
|
||||||
params["pot"] = token
|
.sort_by! { |f| f["width"]?.try &.as_i || 0 }
|
||||||
|
else
|
||||||
|
return [] of Hash(String, JSON::Any)
|
||||||
end
|
end
|
||||||
|
|
||||||
params["host"] = url.host.not_nil!
|
|
||||||
if region = self.info["region"]?.try &.as_s
|
|
||||||
params["region"] = region
|
|
||||||
end
|
|
||||||
|
|
||||||
url.query_params = params
|
|
||||||
LOGGER.trace("Videos: new url is '#{url}'")
|
|
||||||
|
|
||||||
return url.to_s
|
|
||||||
rescue ex
|
|
||||||
LOGGER.debug("Videos: Error when parsing video URL")
|
|
||||||
LOGGER.trace(ex.inspect_with_backtrace)
|
|
||||||
return ""
|
|
||||||
end
|
|
||||||
|
|
||||||
def fmt_stream
|
|
||||||
return @fmt_stream.as(Array(Hash(String, JSON::Any))) if @fmt_stream
|
|
||||||
|
|
||||||
fmt_stream = info.dig?("streamingData", "formats")
|
|
||||||
.try &.as_a.map &.as_h || [] of Hash(String, JSON::Any)
|
|
||||||
|
|
||||||
fmt_stream.each do |fmt|
|
|
||||||
fmt["url"] = JSON::Any.new(self.convert_url(fmt))
|
|
||||||
end
|
|
||||||
|
|
||||||
fmt_stream.sort_by! { |f| f["width"]?.try &.as_i || 0 }
|
|
||||||
@fmt_stream = fmt_stream
|
|
||||||
return @fmt_stream.as(Array(Hash(String, JSON::Any)))
|
|
||||||
end
|
|
||||||
|
|
||||||
def adaptive_fmts
|
|
||||||
return @adaptive_fmts.as(Array(Hash(String, JSON::Any))) if @adaptive_fmts
|
|
||||||
|
|
||||||
fmt_stream = info.dig("streamingData", "adaptiveFormats")
|
|
||||||
.try &.as_a.map &.as_h || [] of Hash(String, JSON::Any)
|
|
||||||
|
|
||||||
fmt_stream.each do |fmt|
|
|
||||||
fmt["url"] = JSON::Any.new(self.convert_url(fmt))
|
|
||||||
end
|
|
||||||
|
|
||||||
fmt_stream.sort_by! { |f| f["width"]?.try &.as_i || 0 }
|
|
||||||
@adaptive_fmts = fmt_stream
|
|
||||||
|
|
||||||
return @adaptive_fmts.as(Array(Hash(String, JSON::Any)))
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def video_streams
|
def video_streams
|
||||||
|
|
|
@ -142,10 +142,21 @@ def extract_video_info(video_id : String)
|
||||||
params.delete("reason")
|
params.delete("reason")
|
||||||
end
|
end
|
||||||
|
|
||||||
{"captions", "playabilityStatus", "playerConfig", "storyboards", "streamingData"}.each do |f|
|
{"captions", "playabilityStatus", "playerConfig", "storyboards"}.each do |f|
|
||||||
params[f] = player_response[f] if player_response[f]?
|
params[f] = player_response[f] if player_response[f]?
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Convert URLs, if those are present
|
||||||
|
if streaming_data = player_response["streamingData"]?
|
||||||
|
%w[formats adaptiveFormats].each do |key|
|
||||||
|
streaming_data.as_h[key]?.try &.as_a.each do |format|
|
||||||
|
format.as_h["url"] = JSON::Any.new(convert_url(format))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
params["streamingData"] = streaming_data
|
||||||
|
end
|
||||||
|
|
||||||
# Data structure version, for cache control
|
# Data structure version, for cache control
|
||||||
params["version"] = JSON::Any.new(Video::SCHEMA_VERSION.to_i64)
|
params["version"] = JSON::Any.new(Video::SCHEMA_VERSION.to_i64)
|
||||||
|
|
||||||
|
@ -454,3 +465,35 @@ def parse_video_info(video_id : String, player_response : Hash(String, JSON::Any
|
||||||
|
|
||||||
return params
|
return params
|
||||||
end
|
end
|
||||||
|
|
||||||
|
private def convert_url(fmt)
|
||||||
|
if cfr = fmt["signatureCipher"]?.try { |json| HTTP::Params.parse(json.as_s) }
|
||||||
|
sp = cfr["sp"]
|
||||||
|
url = URI.parse(cfr["url"])
|
||||||
|
params = url.query_params
|
||||||
|
|
||||||
|
LOGGER.debug("convert_url: Decoding '#{cfr}'")
|
||||||
|
|
||||||
|
unsig = DECRYPT_FUNCTION.try &.decrypt_signature(cfr["s"])
|
||||||
|
params[sp] = unsig if unsig
|
||||||
|
else
|
||||||
|
url = URI.parse(fmt["url"].as_s)
|
||||||
|
params = url.query_params
|
||||||
|
end
|
||||||
|
|
||||||
|
n = DECRYPT_FUNCTION.try &.decrypt_nsig(params["n"])
|
||||||
|
params["n"] = n if n
|
||||||
|
|
||||||
|
if token = CONFIG.po_token
|
||||||
|
params["pot"] = token
|
||||||
|
end
|
||||||
|
|
||||||
|
url.query_params = params
|
||||||
|
LOGGER.trace("convert_url: new url is '#{url}'")
|
||||||
|
|
||||||
|
return url.to_s
|
||||||
|
rescue ex
|
||||||
|
LOGGER.debug("convert_url: Error when parsing video URL")
|
||||||
|
LOGGER.trace(ex.inspect_with_backtrace)
|
||||||
|
return ""
|
||||||
|
end
|
||||||
|
|
|
@ -2,6 +2,7 @@ struct VideoPreferences
|
||||||
include JSON::Serializable
|
include JSON::Serializable
|
||||||
|
|
||||||
property annotations : Bool
|
property annotations : Bool
|
||||||
|
property preload : Bool
|
||||||
property autoplay : Bool
|
property autoplay : Bool
|
||||||
property comments : Array(String)
|
property comments : Array(String)
|
||||||
property continue : Bool
|
property continue : Bool
|
||||||
|
@ -28,6 +29,7 @@ end
|
||||||
|
|
||||||
def process_video_params(query, preferences)
|
def process_video_params(query, preferences)
|
||||||
annotations = query["iv_load_policy"]?.try &.to_i?
|
annotations = query["iv_load_policy"]?.try &.to_i?
|
||||||
|
preload = query["preload"]?.try { |q| (q == "true" || q == "1").to_unsafe }
|
||||||
autoplay = query["autoplay"]?.try { |q| (q == "true" || q == "1").to_unsafe }
|
autoplay = query["autoplay"]?.try { |q| (q == "true" || q == "1").to_unsafe }
|
||||||
comments = query["comments"]?.try &.split(",").map(&.downcase)
|
comments = query["comments"]?.try &.split(",").map(&.downcase)
|
||||||
continue = query["continue"]?.try { |q| (q == "true" || q == "1").to_unsafe }
|
continue = query["continue"]?.try { |q| (q == "true" || q == "1").to_unsafe }
|
||||||
|
@ -50,6 +52,7 @@ def process_video_params(query, preferences)
|
||||||
if preferences
|
if preferences
|
||||||
# region ||= preferences.region
|
# region ||= preferences.region
|
||||||
annotations ||= preferences.annotations.to_unsafe
|
annotations ||= preferences.annotations.to_unsafe
|
||||||
|
preload ||= preferences.preload.to_unsafe
|
||||||
autoplay ||= preferences.autoplay.to_unsafe
|
autoplay ||= preferences.autoplay.to_unsafe
|
||||||
comments ||= preferences.comments
|
comments ||= preferences.comments
|
||||||
continue ||= preferences.continue.to_unsafe
|
continue ||= preferences.continue.to_unsafe
|
||||||
|
@ -70,6 +73,7 @@ def process_video_params(query, preferences)
|
||||||
end
|
end
|
||||||
|
|
||||||
annotations ||= CONFIG.default_user_preferences.annotations.to_unsafe
|
annotations ||= CONFIG.default_user_preferences.annotations.to_unsafe
|
||||||
|
preload ||= CONFIG.default_user_preferences.preload.to_unsafe
|
||||||
autoplay ||= CONFIG.default_user_preferences.autoplay.to_unsafe
|
autoplay ||= CONFIG.default_user_preferences.autoplay.to_unsafe
|
||||||
comments ||= CONFIG.default_user_preferences.comments
|
comments ||= CONFIG.default_user_preferences.comments
|
||||||
continue ||= CONFIG.default_user_preferences.continue.to_unsafe
|
continue ||= CONFIG.default_user_preferences.continue.to_unsafe
|
||||||
|
@ -89,6 +93,7 @@ def process_video_params(query, preferences)
|
||||||
save_player_pos ||= CONFIG.default_user_preferences.save_player_pos.to_unsafe
|
save_player_pos ||= CONFIG.default_user_preferences.save_player_pos.to_unsafe
|
||||||
|
|
||||||
annotations = annotations == 1
|
annotations = annotations == 1
|
||||||
|
preload = preload == 1
|
||||||
autoplay = autoplay == 1
|
autoplay = autoplay == 1
|
||||||
continue = continue == 1
|
continue = continue == 1
|
||||||
continue_autoplay = continue_autoplay == 1
|
continue_autoplay = continue_autoplay == 1
|
||||||
|
@ -128,6 +133,7 @@ def process_video_params(query, preferences)
|
||||||
|
|
||||||
params = VideoPreferences.new({
|
params = VideoPreferences.new({
|
||||||
annotations: annotations,
|
annotations: annotations,
|
||||||
|
preload: preload,
|
||||||
autoplay: autoplay,
|
autoplay: autoplay,
|
||||||
comments: comments,
|
comments: comments,
|
||||||
continue: continue,
|
continue: continue,
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
<video style="outline:none;width:100%;background-color:#000" playsinline poster="<%= thumbnail %>"
|
<video style="outline:none;width:100%;background-color:#000" playsinline poster="<%= thumbnail %>"
|
||||||
id="player" class="on-video_player video-js player-style-<%= params.player_style %>"
|
id="player" class="on-video_player video-js player-style-<%= params.player_style %>"
|
||||||
|
preload="<% if params.preload %>auto<% else %>none<% end %>"
|
||||||
<% if params.autoplay %>autoplay<% end %>
|
<% if params.autoplay %>autoplay<% end %>
|
||||||
<% if params.video_loop %>loop<% end %>
|
<% if params.video_loop %>loop<% end %>
|
||||||
<% if params.controls %>controls<% end %>>
|
<% if params.controls %>controls<% end %>>
|
||||||
|
|
|
@ -12,6 +12,11 @@
|
||||||
<input name="video_loop" id="video_loop" type="checkbox" <% if preferences.video_loop %>checked<% end %>>
|
<input name="video_loop" id="video_loop" type="checkbox" <% if preferences.video_loop %>checked<% end %>>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="pure-control-group">
|
||||||
|
<label for="preload"><%= translate(locale, "preferences_preload_label") %></label>
|
||||||
|
<input name="preload" id="preload" type="checkbox" <% if preferences.preload %>checked<% end %>>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="pure-control-group">
|
<div class="pure-control-group">
|
||||||
<label for="autoplay"><%= translate(locale, "preferences_autoplay_label") %></label>
|
<label for="autoplay"><%= translate(locale, "preferences_autoplay_label") %></label>
|
||||||
<input name="autoplay" id="autoplay" type="checkbox" <% if preferences.autoplay %>checked<% end %>>
|
<input name="autoplay" id="autoplay" type="checkbox" <% if preferences.autoplay %>checked<% end %>>
|
||||||
|
|
|
@ -108,21 +108,30 @@ private module Parsers
|
||||||
length_seconds = 0
|
length_seconds = 0
|
||||||
end
|
end
|
||||||
|
|
||||||
live_now = false
|
|
||||||
premium = false
|
|
||||||
|
|
||||||
premiere_timestamp = item_contents.dig?("upcomingEventData", "startTime").try { |t| Time.unix(t.as_s.to_i64) }
|
premiere_timestamp = item_contents.dig?("upcomingEventData", "startTime").try { |t| Time.unix(t.as_s.to_i64) }
|
||||||
|
badges = VideoBadges::None
|
||||||
item_contents["badges"]?.try &.as_a.each do |badge|
|
item_contents["badges"]?.try &.as_a.each do |badge|
|
||||||
b = badge["metadataBadgeRenderer"]
|
b = badge["metadataBadgeRenderer"]
|
||||||
case b["label"].as_s
|
case b["label"].as_s
|
||||||
when "LIVE NOW"
|
when "LIVE"
|
||||||
live_now = true
|
badges |= VideoBadges::LiveNow
|
||||||
when "New", "4K", "CC"
|
when "New"
|
||||||
# TODO
|
badges |= VideoBadges::New
|
||||||
|
when "4K"
|
||||||
|
badges |= VideoBadges::FourK
|
||||||
|
when "8K"
|
||||||
|
badges |= VideoBadges::EightK
|
||||||
|
when "VR180"
|
||||||
|
badges |= VideoBadges::VR180
|
||||||
|
when "360°"
|
||||||
|
badges |= VideoBadges::VR360
|
||||||
|
when "3D"
|
||||||
|
badges |= VideoBadges::ThreeD
|
||||||
|
when "CC"
|
||||||
|
badges |= VideoBadges::ClosedCaptions
|
||||||
when "Premium"
|
when "Premium"
|
||||||
# TODO: Potentially available as item_contents["topStandaloneBadge"]["metadataBadgeRenderer"]
|
# TODO: Potentially available as item_contents["topStandaloneBadge"]["metadataBadgeRenderer"]
|
||||||
premium = true
|
badges |= VideoBadges::Premium
|
||||||
else nil # Ignore
|
else nil # Ignore
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -136,10 +145,9 @@ private module Parsers
|
||||||
views: view_count,
|
views: view_count,
|
||||||
description_html: description_html,
|
description_html: description_html,
|
||||||
length_seconds: length_seconds,
|
length_seconds: length_seconds,
|
||||||
live_now: live_now,
|
|
||||||
premium: premium,
|
|
||||||
premiere_timestamp: premiere_timestamp,
|
premiere_timestamp: premiere_timestamp,
|
||||||
author_verified: author_verified,
|
author_verified: author_verified,
|
||||||
|
badges: badges,
|
||||||
})
|
})
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -563,10 +571,9 @@ private module Parsers
|
||||||
views: view_count,
|
views: view_count,
|
||||||
description_html: "",
|
description_html: "",
|
||||||
length_seconds: duration,
|
length_seconds: duration,
|
||||||
live_now: false,
|
|
||||||
premium: false,
|
|
||||||
premiere_timestamp: Time.unix(0),
|
premiere_timestamp: Time.unix(0),
|
||||||
author_verified: false,
|
author_verified: false,
|
||||||
|
badges: VideoBadges::None,
|
||||||
})
|
})
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -111,7 +111,7 @@ module UrlSanitizer
|
||||||
new_uri.path = "/watch"
|
new_uri.path = "/watch"
|
||||||
|
|
||||||
new_params = copy_params(unsafe_uri.query_params, :watch)
|
new_params = copy_params(unsafe_uri.query_params, :watch)
|
||||||
new_params["id"] = breadcrumbs[0]
|
new_params["v"] = breadcrumbs[0]
|
||||||
|
|
||||||
new_uri.query_params = new_params
|
new_uri.query_params = new_params
|
||||||
end
|
end
|
||||||
|
|
Loading…
Reference in a new issue