diff options
68 files changed, 664 insertions, 405 deletions
@@ -38,6 +38,9 @@ Style/RedundantBegin: Style/RedundantReturn: Enabled: false +Style/RedundantNext: + Enabled: false + Style/ParenthesesAroundCondition: Enabled: false diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 7a2c3760..9ca09368 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -6,7 +6,7 @@ docker/ @unixfox kubernetes/ @unixfox README.md @thefrenchghosty -config/config.example.yml @thefrenchghosty @SamantazFox @unixfox +config/config.example.yml @SamantazFox @unixfox scripts/ @syeopite shards.lock @syeopite diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index de538915..dd472d1a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -38,10 +38,11 @@ jobs: matrix: stable: [true] crystal: - - 1.9.2 - 1.10.1 - 1.11.2 - 1.12.1 + - 1.13.2 + - 1.14.0 include: - crystal: nightly stable: false @@ -51,6 +52,11 @@ jobs: with: submodules: true + - name: Install required APT packages + run: | + sudo apt install -y libsqlite3-dev + shell: bash + - name: Install Crystal uses: crystal-lang/install-crystal@v1.8.0 with: @@ -59,7 +65,9 @@ jobs: - name: Cache Shards uses: actions/cache@v3 with: - path: ./lib + path: | + ./lib + ./bin key: shards-${{ hashFiles('shard.lock') }} - name: Install Shards @@ -71,14 +79,6 @@ jobs: - name: Run tests run: crystal spec - - name: Run lint - run: | - if ! crystal tool format --check; then - crystal tool format - git diff - exit 1 - fi - - name: Build run: crystal build --warnings all --error-on-warnings --error-trace src/invidious.cr @@ -124,8 +124,12 @@ jobs: - name: Test Docker run: while curl -Isf http://localhost:3000; do sleep 1; done - ameba_lint: + lint: + runs-on: ubuntu-latest + + continue-on-error: true + steps: - uses: actions/checkout@v4 with: @@ -145,7 +149,18 @@ jobs: key: shards-${{ hashFiles('shard.lock') }} - name: Install Shards - run: shards install + run: | + if ! shards check; then + shards install + fi + + - name: Check Crystal formatter compliance + run: | + if ! crystal tool format --check; then + crystal tool format + git diff + exit 1 + fi - name: Run Ameba linter run: bin/ameba diff --git a/CHANGELOG.md b/CHANGELOG.md index 2cc5b05c..f9892e17 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,51 @@ # CHANGELOG +## vX.Y.0 (future) + + +### Full list of pull requests merged since the last release (newest first) + +* Add "Filipino (auto-generated)" to the list of caption languages ([#4995], by @SamantazFox) +* Makefile: Add MT option to enable the 'preview_mt' flag ([#4993], by @SamantazFox) +* SigHelper: Reconnect to signature helper ([#4991], thanks @Fijxu) +* Fix player menus hiding onHover ready ([#4750], thanks @giacomocerquone) +* Use connection pools when requesting images from YouTube ([#4326], thanks @syeopite) +* Add support for using Invidious through a HTTP Proxy ([#4270], thanks @syeopite) +* 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 +[#4270]: https://github.com/iv-org/invidious/pull/4270 +[#4326]: https://github.com/iv-org/invidious/pull/4326 +[#4652]: https://github.com/iv-org/invidious/pull/4652 +[#4750]: https://github.com/iv-org/invidious/pull/4750 +[#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 +[#4991]: https://github.com/iv-org/invidious/pull/4991 +[#4993]: https://github.com/iv-org/invidious/pull/4993 +[#4995]: https://github.com/iv-org/invidious/pull/4995 + ## v2.20240825.2 (2024-08-26) @@ -7,6 +7,11 @@ STATIC := 0 NO_DBG_SYMBOLS := 0 +# Enable multi-threading. +# Warning: Experimental feature!! +# invidious is not stable when MT is enabled. +MT := 0 + FLAGS ?= @@ -19,6 +24,10 @@ ifeq ($(STATIC), 1) FLAGS += --static endif +ifeq ($(MT), 1) + FLAGS += -Dpreview_mt +endif + ifeq ($(NO_DBG_SYMBOLS), 1) FLAGS += --no-debug diff --git a/assets/css/player.css b/assets/css/player.css index 50c7a748..9cb400ad 100644 --- a/assets/css/player.css +++ b/assets/css/player.css @@ -68,6 +68,7 @@ .video-js.player-style-youtube .vjs-menu-button-popup .vjs-menu { margin-bottom: 2em; + padding-top: 2em } .video-js.player-style-youtube .vjs-progress-control .vjs-progress-holder, .video-js.player-style-youtube .vjs-progress-control {height: 5px; diff --git a/assets/js/player.js b/assets/js/player.js index d32062c6..353a5296 100644 --- a/assets/js/player.js +++ b/assets/js/player.js @@ -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 options = { - preload: 'auto', liveui: true, playbackRates: [0.25, 0.5, 0.75, 1.0, 1.25, 1.5, 1.75, 2.0], controlBar: { diff --git a/config/config.example.yml b/config/config.example.yml index f746d1f7..a3a2eeb7 100644 --- a/config/config.example.yml +++ b/config/config.example.yml @@ -173,6 +173,17 @@ https_only: false ## #force_resolve: +## +## Configuration for using a HTTP proxy +## +## If unset, then no HTTP proxy will be used. +## +http_proxy: + user: + password: + host: + port: + ## ## Use Innertube's transcripts API instead of timedtext for closed captions @@ -719,6 +730,22 @@ default_user_preferences: # ----------------------------- ## + ## 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. ## ## Accepted values: true, false diff --git a/locales/ar.json b/locales/ar.json index 5d8b230f..b6bab59b 100644 --- a/locales/ar.json +++ b/locales/ar.json @@ -483,7 +483,7 @@ "comments_view_x_replies_3": "عرض رد {{count}}", "comments_view_x_replies_4": "عرض الردود {{count}}", "comments_view_x_replies_5": "عرض رد {{count}}", - "search_message_use_another_instance": " يمكنك أيضًا البحث عن <a href=\"`x`\"> في مثيل آخر </a>.", + "search_message_use_another_instance": "يمكنك أيضًا البحث عن <a href=\"`x`\"> في مثيل آخر </a>.", "comments_points_count_0": "{{count}} نقطة", "comments_points_count_1": "نقطة واحدة", "comments_points_count_2": "نقطتان", diff --git a/locales/cs.json b/locales/cs.json index 1350f146..6e66178d 100644 --- a/locales/cs.json +++ b/locales/cs.json @@ -471,7 +471,7 @@ "search_filters_title": "Filtry", "search_filters_duration_option_medium": "Střední (4 - 20 minut)", "search_filters_duration_option_long": "Dlouhá (> 20 minut)", - "search_message_use_another_instance": " Můžete také <a href=\"`x`\">hledat na jiné instanci</a>.", + "search_message_use_another_instance": "Můžete také <a href=\"`x`\">hledat na jiné instanci</a>.", "search_filters_features_label": "Vlastnosti", "search_filters_features_option_three_sixty": "360°", "search_filters_features_option_vr180": "VR180", diff --git a/locales/de.json b/locales/de.json index d20f7fab..151f2abe 100644 --- a/locales/de.json +++ b/locales/de.json @@ -47,6 +47,7 @@ "Preferences": "Einstellungen", "preferences_category_player": "Wiedergabeeinstellungen", "preferences_video_loop_label": "Immer wiederholen: ", + "preferences_preload_label": "Videodaten vorladen: ", "preferences_autoplay_label": "Automatisch abspielen: ", "preferences_continue_label": "Immer automatisch nächstes Video abspielen: ", "preferences_continue_autoplay_label": "Nächstes Video automatisch abspielen: ", @@ -322,7 +323,7 @@ "channel_tab_community_label": "Gemeinschaft", "search_filters_sort_option_relevance": "Relevanz", "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_type_label": "Inhaltstyp", "search_filters_duration_label": "Dauer", @@ -454,7 +455,7 @@ "Portuguese (auto-generated)": "Portugiesisch (automatisch generiert)", "search_filters_title": "Filtern", "search_message_change_filters_or_query": "Versuchen Sie, Ihre Suchanfrage zu erweitern und/oder die Filter zu ändern.", - "search_message_use_another_instance": " Sie können auch <a href=\"`x`\">auf einer anderen Instanz suchen</a>.", + "search_message_use_another_instance": "Sie können auch <a href=\"`x`\">auf einer anderen Instanz suchen</a>.", "Popular enabled: ": "„Beliebt“-Seite aktiviert: ", "search_message_no_results": "Keine Ergebnisse gefunden.", "search_filters_duration_option_medium": "Mittel (4 - 20 Minuten)", @@ -493,5 +494,8 @@ "Add to playlist": "Einer Wiedergabeliste hinzufügen", "Search for videos": "Nach Videos suchen", "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" } diff --git a/locales/el.json b/locales/el.json index 902c8b97..38550458 100644 --- a/locales/el.json +++ b/locales/el.json @@ -489,5 +489,10 @@ "search_filters_date_label": "Ημερομηνία αναφόρτωσης", "Search for videos": "Αναζήτηση βίντεο", "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": "Αλλαγή θέματος" } diff --git a/locales/en-US.json b/locales/en-US.json index 3987f796..c23f6bc3 100644 --- a/locales/en-US.json +++ b/locales/en-US.json @@ -71,6 +71,7 @@ "Preferences": "Preferences", "preferences_category_player": "Player preferences", "preferences_video_loop_label": "Always loop: ", + "preferences_preload_label": "Preload video data: ", "preferences_autoplay_label": "Autoplay: ", "preferences_continue_label": "Play next by default: ", "preferences_continue_autoplay_label": "Autoplay next video: ", @@ -190,7 +191,7 @@ "Switch Invidious Instance": "Switch Invidious Instance", "search_message_no_results": "No results found.", "search_message_change_filters_or_query": "Try widening your search query and/or changing the filters.", - "search_message_use_another_instance": " You can also <a href=\"`x`\">search on another instance</a>.", + "search_message_use_another_instance": "You can also <a href=\"`x`\">search on another instance</a>.", "Hide annotations": "Hide annotations", "Show annotations": "Show annotations", "Genre: ": "Genre: ", @@ -285,6 +286,7 @@ "Esperanto": "Esperanto", "Estonian": "Estonian", "Filipino": "Filipino", + "Filipino (auto-generated)": "Filipino (auto-generated)", "Finnish": "Finnish", "French": "French", "French (auto-generated)": "French (auto-generated)", @@ -422,7 +424,7 @@ "search_filters_title": "Filters", "search_filters_date_label": "Upload 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_week": "This week", "search_filters_date_option_month": "This month", @@ -454,7 +456,7 @@ "search_filters_sort_label": "Sort By", "search_filters_sort_option_relevance": "Relevance", "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_apply_button": "Apply selected filters", "Current version: ": "Current version: ", diff --git a/locales/es.json b/locales/es.json index 1d082e60..fda29198 100644 --- a/locales/es.json +++ b/locales/es.json @@ -478,7 +478,7 @@ "tokens_count_0": "{{count}} token", "tokens_count_1": "{{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? ", "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", diff --git a/locales/fa.json b/locales/fa.json index 6723aad8..b146385e 100644 --- a/locales/fa.json +++ b/locales/fa.json @@ -360,7 +360,7 @@ "search_filters_duration_label": "مدت", "search_filters_features_label": "ویژگیها", "search_filters_sort_label": "به ترتیب", - "search_filters_date_option_hour": "یک ساعت گذشته", + "search_filters_date_option_hour": "ساعت گذشته", "search_filters_date_option_today": "امروز", "search_filters_date_option_week": "این هفته", "search_filters_date_option_month": "این ماه", @@ -461,7 +461,7 @@ "Song: ": "آهنگ: ", "Channel Sponsor": "اسپانسر کانال", "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": "دریافت غیرفعال است", "crash_page_before_reporting": "پیش از گزارش ایراد، مطمئنید شوید که:", "playlist_button_add_items": "افزودن ویدیو", diff --git a/locales/fr.json b/locales/fr.json index 3bcc9014..6147a159 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -484,7 +484,7 @@ "search_filters_duration_option_medium": "Moyenne (de 4 à 20 minutes)", "search_filters_apply_button": "Appliquer les filtres", "search_message_no_results": "Aucun résultat.", - "search_message_use_another_instance": " Vous pouvez également <a href=\"`x`\">effectuer votre recherche sur une autre instance</a>.", + "search_message_use_another_instance": "Vous pouvez également <a href=\"`x`\">effectuer votre recherche sur une autre instance</a>.", "search_filters_type_option_all": "Tous les types", "search_filters_date_label": "Date d'ajout", "search_filters_features_option_vr180": "VR180", diff --git a/locales/hr.json b/locales/hr.json index 91425248..7b76a41f 100644 --- a/locales/hr.json +++ b/locales/hr.json @@ -449,30 +449,30 @@ "Cantonese (Hong Kong)": "Kantonski (Hong Kong)", "Chinese": "Kineski", "Chinese (Taiwan)": "Kineski (Tajvan)", - "Dutch (auto-generated)": "Nizozemski (automatski generiran)", - "French (auto-generated)": "Francuski (automatski generiran)", - "Indonesian (auto-generated)": "Indonezijski (automatski generiran)", + "Dutch (auto-generated)": "Nizozemski (automatski generirano)", + "French (auto-generated)": "Francuski (automatski generirano)", + "Indonesian (auto-generated)": "Indonezijski (automatski generirano)", "Interlingue": "Interlingua", - "Japanese (auto-generated)": "Japanski (automatski generiran)", - "Russian (auto-generated)": "Ruski (automatski generiran)", - "Turkish (auto-generated)": "Turski (automatski generiran)", - "Vietnamese (auto-generated)": "Vijetnamski (automatski generiran)", + "Japanese (auto-generated)": "Japanski (automatski generirano)", + "Russian (auto-generated)": "Ruski (automatski generirano)", + "Turkish (auto-generated)": "Turski (automatski generirano)", + "Vietnamese (auto-generated)": "Vijetnamski (automatski generirano)", "Spanish (Spain)": "Španjolski (Španjolska)", - "Italian (auto-generated)": "Talijanski (automatski generiran)", + "Italian (auto-generated)": "Talijanski (automatski generirano)", "Portuguese (Brazil)": "Portugalski (Brazil)", "Spanish (Mexico)": "Španjolski (Meksiko)", - "German (auto-generated)": "Njemački (automatski generiran)", + "German (auto-generated)": "Njemački (automatski generirano)", "Chinese (China)": "Kineski (Kina)", "Chinese (Hong Kong)": "Kineski (Hong Kong)", - "Korean (auto-generated)": "Korejski (automatski generiran)", - "Portuguese (auto-generated)": "Portugalski (automatski generiran)", - "Spanish (auto-generated)": "Španjolski (automatski generiran)", + "Korean (auto-generated)": "Korejski (automatski generirano)", + "Portuguese (auto-generated)": "Portugalski (automatski generirano)", + "Spanish (auto-generated)": "Španjolski (automatski generirano)", "preferences_watch_history_label": "Aktiviraj povijest gledanja: ", "search_filters_title": "Filtri", "search_filters_date_option_none": "Bilo koji datum", "search_filters_date_label": "Datum prijenosa", "search_message_no_results": "Nema rezultata.", - "search_message_use_another_instance": " Također možeš <a href=\"`x`\">tražiti na jednoj drugoj instanci</a>.", + "search_message_use_another_instance": "Također možeš <a href=\"`x`\">tražiti na jednoj drugoj instanci</a>.", "search_message_change_filters_or_query": "Pokušaj proširiti upit za pretragu i/ili promijeni filtre.", "search_filters_features_option_vr180": "VR180", "search_filters_duration_option_none": "Bilo koje duljine", diff --git a/locales/ia.json b/locales/ia.json index 2c8cb2b0..236ec4b4 100644 --- a/locales/ia.json +++ b/locales/ia.json @@ -7,7 +7,7 @@ "invidious": "Invidious", "Image CAPTCHA": "Imagine CAPTCHA", "newest": "plus nove", - "generic_button_save": "Salvar", + "generic_button_save": "Salveguardar", "Dark mode: ": "Modo obscur: ", "preferences_dark_mode_label": "Thema: ", "preferences_category_subscription": "Preferentias de subscription", @@ -23,7 +23,7 @@ "light": "clar", "No": "Non", "youtube": "YouTube", - "LIVE": "IN DIRECTE", + "LIVE": "IN DIRECTO", "reddit": "Reddit", "preferences_category_player": "Preferentias de reproductor", "Preferences": "Preferentias", diff --git a/locales/is.json b/locales/is.json index 49f3711e..9d13c5cf 100644 --- a/locales/is.json +++ b/locales/is.json @@ -396,7 +396,7 @@ "toggle_theme": "Víxla þema", "carousel_skip": "Sleppa hringekjunni", "preferences_quality_option_medium": "Miðlungs", - "search_message_use_another_instance": " Þú getur líka <a href=\"`x`\">leitað á öðrum netþjóni</a>.", + "search_message_use_another_instance": "Þú getur líka <a href=\"`x`\">leitað á öðrum netþjóni</a>.", "footer_source_code": "Grunnkóði", "English (United Kingdom)": "Enska (Bretland)", "English (United States)": "Enska (Bandarísk)", diff --git a/locales/it.json b/locales/it.json index 46d7ef13..309adb13 100644 --- a/locales/it.json +++ b/locales/it.json @@ -449,7 +449,7 @@ "Portuguese (Brazil)": "Portoghese (Brasile)", "preferences_watch_history_label": "Attiva cronologia di riproduzione: ", "French (auto-generated)": "Francese (generati automaticamente)", - "search_message_use_another_instance": " Puoi anche <a href=\"`x`\">cercare in un'altra istanza</a>.", + "search_message_use_another_instance": "Puoi anche <a href=\"`x`\">cercare in un'altra istanza</a>.", "search_message_no_results": "Nessun risultato trovato.", "search_message_change_filters_or_query": "Prova ad ampliare la ricerca e/o modificare i filtri.", "English (United States)": "Inglese (Stati Uniti)", diff --git a/locales/ja.json b/locales/ja.json index d430b2a4..7fc9d604 100644 --- a/locales/ja.json +++ b/locales/ja.json @@ -363,7 +363,7 @@ "search_filters_features_option_location": "場所", "search_filters_features_option_hdr": "HDR", "Current version: ": "現在のバージョン: ", - "next_steps_error_message": "以下をお試してください: ", + "next_steps_error_message": "以下をお試しください: ", "next_steps_error_message_refresh": "再読み込み", "next_steps_error_message_go_to_youtube": "YouTubeを開く", "search_filters_duration_option_short": "4分未満", @@ -396,7 +396,7 @@ "download_subtitles": "字幕 - `x` (.vtt)", "search_filters_features_option_purchased": "購入済み", "preferences_quality_option_dash": "DASH (適応的画質)", - "preferences_quality_dash_option_worst": "最悪", + "preferences_quality_dash_option_worst": "最低", "preferences_quality_dash_option_best": "最高", "videoinfo_started_streaming_x_ago": "`x`前に配信を開始", "videoinfo_watch_on_youTube": "YouTubeで視聴", @@ -434,7 +434,7 @@ "crash_page_switch_instance": "<a href=\"`x`\">別のインスタンスを使用</a>を試す", "crash_page_read_the_faq": "<a href=\"`x`\">よくある質問 (FAQ)</a> を読む", "Popular enabled: ": "人気動画を有効化 ", - "search_message_use_another_instance": " <a href=\"`x`\">別のインスタンス上での検索</a>も可能です。", + "search_message_use_another_instance": "<a href=\"`x`\">別のインスタンス上での検索</a>も可能です。", "search_filters_apply_button": "選択したフィルターを適用", "user_saved_playlists": "`x`個の保存済みの再生リスト", "crash_page_you_found_a_bug": "Invidious のバグのようです!", diff --git a/locales/ko.json b/locales/ko.json index 74395f32..4864860a 100644 --- a/locales/ko.json +++ b/locales/ko.json @@ -18,8 +18,8 @@ "preferences_related_videos_label": "관련 동영상 보기: ", "Fallback captions: ": "대체 자막: ", "preferences_captions_label": "기본 자막: ", - "reddit": "Reddit", - "youtube": "YouTube", + "reddit": "레딧", + "youtube": "유튜브", "preferences_comments_label": "기본 댓글: ", "preferences_volume_label": "플레이어 볼륨: ", "preferences_quality_label": "선호하는 비디오 품질: ", @@ -48,7 +48,7 @@ "An alternative front-end to YouTube": "유튜브의 프론트엔드 대안", "History": "시청 기록", "Delete account?": "계정을 삭제 하시겠습니까?", - "Export data as JSON": "JSON으로 데이터 내보내기", + "Export data as JSON": "인비디어스 데이터 내보내기 (.json)", "Export subscriptions as OPML (for NewPipe & FreeTube)": "OPML로 구독 내보내기 (뉴파이프 및 프리튜브)", "Export subscriptions as OPML": "OPML로 구독 내보내기", "Export": "내보내기", @@ -78,10 +78,10 @@ "Subscribe": "구독", "Unsubscribe": "구독 취소", "LIVE": "실시간", - "generic_views_count_0": "조회수 {{count}}회", - "generic_videos_count_0": "동영상 {{count}}개", - "generic_playlists_count_0": "재생목록 {{count}}개", - "generic_subscribers_count_0": "구독자 {{count}}명", + "generic_views_count_0": "{{count}} 조회수", + "generic_videos_count_0": "{{count}} 동영상", + "generic_playlists_count_0": "{{count}} 재생목록", + "generic_subscribers_count_0": "{{count}} 구독자", "generic_subscriptions_count_0": "{{count}} 구독", "search_filters_type_option_playlist": "재생목록", "Korean": "한국어", @@ -109,14 +109,14 @@ "This channel does not exist.": "이 채널은 존재하지 않습니다.", "Deleted or invalid channel": "삭제되었거나 더 이상 존재하지 않는 채널", "channel:`x`": "채널:`x`", - "Show replies": "댓글 보이기", + "Show replies": "댓글 보기", "Hide replies": "댓글 숨기기", "Incorrect password": "잘못된 비밀번호", "License: ": "라이선스: ", "Genre: ": "장르: ", "Editing playlist `x`": "재생목록 `x` 수정하기", "Playlist privacy": "재생목록 공개 범위", - "Watch on YouTube": "YouTube에서 보기", + "Watch on YouTube": "유튜브에서 보기", "Show less": "간략히", "Show more": "더보기", "Title": "제목", @@ -125,7 +125,7 @@ "Delete playlist": "재생목록 삭제", "Delete playlist `x`?": "재생목록 `x` 를 삭제하시겠습니까?", "Updated `x` ago": "`x` 전에 업데이트됨", - "Released under the AGPLv3 on Github.": "GitHub에 AGPLv3 으로 배포됩니다.", + "Released under the AGPLv3 on Github.": "깃허브에 AGPLv3 으로 배포됩니다.", "View all playlists": "모든 재생목록 보기", "Private": "비공개", "Unlisted": "목록에 없음", @@ -135,12 +135,12 @@ "Source available here.": "소스는 여기에서 사용할 수 있습니다.", "Log out": "로그아웃", "search": "검색", - "subscriptions_unseen_notifs_count_0": "읽지 않은 알림 {{count}}개", + "subscriptions_unseen_notifs_count_0": "{{count}} 읽지 않은 알림", "Subscriptions": "구독", "revoke": "철회", "unsubscribe": "구독 취소", "Import/export": "가져오기/내보내기", - "tokens_count_0": "토큰 {{count}}개", + "tokens_count_0": "{{count}} 토큰", "Token": "토큰", "Token manager": "토큰 관리자", "Subscription manager": "구독 관리자", @@ -163,7 +163,7 @@ "Clear watch history": "시청 기록 지우기", "preferences_category_data": "데이터 설정", "`x` is live": "`x` 이(가) 라이브 중입니다", - "`x` uploaded a video": "`x` 이(가) 동영상을 게시했습니다", + "`x` uploaded a video": "`x` 동영상 게시됨", "Enable web notifications": "웹 알림 활성화", "preferences_notifications_only_label": "알림만 표시 (있는 경우): ", "preferences_unseen_only_label": "시청하지 않은 것만 표시: ", @@ -241,7 +241,7 @@ "Could not create mix.": "믹스를 생성할 수 없습니다.", "`x` ago": "`x` 전", "comments_view_x_replies_0": "답글 {{count}}개 보기", - "View Reddit comments": "Reddit 댓글 보기", + "View Reddit comments": "레딧 댓글 보기", "Engagement: ": "약속: ", "Wilson score: ": "Wilson Score: ", "Family friendly? ": "전연령 영상입니까? ", @@ -267,8 +267,8 @@ "Bulgarian": "불가리아어", "Bosnian": "보스니아어", "Belarusian": "벨라루스어", - "View more comments on Reddit": "Reddit에서 댓글 더 보기", - "View YouTube comments": "YouTube 댓글 보기", + "View more comments on Reddit": "레딧에서 댓글 더 보기", + "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.": "자바스크립트가 꺼져 있는 것 같습니다! 댓글을 보려면 여기를 클릭하세요. 댓글을 로드하는 데 시간이 조금 더 걸릴 수 있습니다.", "Shared `x`": "`x` 업로드", "Whitelisted regions: ": "차단되지 않은 지역: ", @@ -289,7 +289,7 @@ "Empty playlist": "재생목록 비어 있음", "Show annotations": "주석 보이기", "Hide annotations": "주석 숨기기", - "Switch Invidious Instance": "Invidious 인스턴스 변경", + "Switch Invidious Instance": "인비디어스 인스턴스 변경", "Spanish": "스페인어", "Southern Sotho": "소토어", "Somali": "소말리어", @@ -329,7 +329,7 @@ "Swedish": "스웨덴어", "Spanish (Latin America)": "스페인어 (라틴 아메리카)", "comments_points_count_0": "{{count}} 포인트", - "Invidious Private Feed for `x`": "`x` 에 대한 Invidious 비공개 피드", + "Invidious Private Feed for `x`": "`x` 에 대한 인비디어스 비공개 피드", "Premieres `x`": "최초 공개 `x`", "Premieres in `x`": "`x` 후 최초 공개", "next_steps_error_message": "다음 방법을 시도해 보세요: ", @@ -408,7 +408,7 @@ "preferences_quality_dash_option_1080p": "1080p", "preferences_quality_dash_option_worst": "최저", "preferences_watch_history_label": "시청 기록 저장: ", - "invidious": "Invidious", + "invidious": "인비디어스", "preferences_quality_option_small": "낮음", "preferences_quality_dash_option_auto": "자동", "preferences_quality_dash_option_480p": "480p", @@ -453,7 +453,7 @@ "channel_tab_streams_label": "실시간 스트리밍", "channel_tab_channels_label": "채널", "channel_tab_playlists_label": "재생목록", - "Standard YouTube license": "표준 YouTube 라이선스", + "Standard YouTube license": "표준 유튜브 라이선스", "Song: ": "제목: ", "Channel Sponsor": "채널 스폰서", "Album: ": "앨범: ", diff --git a/locales/nb-NO.json b/locales/nb-NO.json index fed6d73f..17d64baf 100644 --- a/locales/nb-NO.json +++ b/locales/nb-NO.json @@ -322,13 +322,13 @@ "channel_tab_community_label": "Gemenskap", "search_filters_sort_option_relevance": "relevans", "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_type_label": "innholdstype", "search_filters_duration_label": "varighet", "search_filters_features_label": "funksjoner", "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_week": "uke", "search_filters_date_option_month": "måned", @@ -459,7 +459,7 @@ "search_message_no_results": "Resultatløst.", "search_filters_type_option_all": "Alle typer", "search_filters_duration_option_none": "Enhver varighet", - "search_message_use_another_instance": " Du kan også <a href=\"`x`\">søke på en annen instans</a>.", + "search_message_use_another_instance": "Du kan også <a href=\"`x`\">søke på en annen instans</a>.", "search_filters_date_label": "Opplastningsdato", "search_filters_apply_button": "Bruk valgte filtre", "search_filters_date_option_none": "Siden begynnelsen", @@ -494,5 +494,7 @@ "carousel_slide": "Lysark {{current}} av {{total}}", "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: ", + "The Popular feed has been disabled by the administrator.": "Populært-kilden er koblet ut av administratoren.", + "toggle_theme": "Endre utseende" } diff --git a/locales/nl.json b/locales/nl.json index 26e35e99..f10b3593 100644 --- a/locales/nl.json +++ b/locales/nl.json @@ -317,13 +317,13 @@ "channel_tab_community_label": "Gemeenschap", "search_filters_sort_option_relevance": "relevantie", "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_type_label": "Type inhoud", "search_filters_duration_label": "duur", "search_filters_features_label": "eigenschappen", "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_week": "week", "search_filters_date_option_month": "maand", @@ -357,7 +357,7 @@ "footer_original_source_code": "Originele bron-code", "footer_modfied_source_code": "Gewijzigde bron-code", "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", "search_filters_duration_option_long": "Lang (> 20 minuten)", "preferences_quality_option_dash": "DASH (adaptieve kwaliteit)", @@ -450,7 +450,7 @@ "Chinese (Hong Kong)": "Chinees (Hongkong)", "Korean (auto-generated)": "Koreaans (automatisch gegenereerd)", "search_filters_apply_button": "Geselecteerde filters toepassen", - "search_message_use_another_instance": " Je kan ook <a href=\"`x`\">zoeken op een andere instantie</a>.", + "search_message_use_another_instance": "Je kan ook <a href=\"`x`\">zoeken op een andere instantie</a>.", "Cantonese (Hong Kong)": "Kantonees (Hongkong)", "Chinese (China)": "Chinees (China)", "crash_page_read_the_faq": "de <a href=\"`x`\">veelgestelde vragen (FAQ)</a> gelezen hebt", @@ -477,7 +477,7 @@ "Song: ": "Lied: ", "generic_channels_count": "{{count}} kanaal", "generic_channels_count_plural": "{{count}} kanalen", - "Popular enabled: ": "Populair geactiveerd: ", + "Popular enabled: ": "Populair ingeschakeld: ", "channel_tab_playlists_label": "Afspeellijsten", "generic_button_edit": "Bewerken", "Music in this video": "Muziek in deze video", diff --git a/locales/pl.json b/locales/pl.json index f24e9766..73d65647 100644 --- a/locales/pl.json +++ b/locales/pl.json @@ -478,7 +478,7 @@ "search_filters_date_label": "Data przesłania", "search_filters_features_option_vr180": "VR180", "search_filters_date_option_none": "Dowolna data", - "search_message_use_another_instance": " Możesz także <a href=\"`x`\">wyszukać w innej instancji</a>.", + "search_message_use_another_instance": "Możesz także <a href=\"`x`\">wyszukać w innej instancji</a>.", "search_filters_type_option_all": "Dowolny typ", "search_filters_duration_option_none": "Dowolna długość", "search_filters_duration_option_medium": "Średnia (4-20 minut)", diff --git a/locales/pt-BR.json b/locales/pt-BR.json index 0887e697..1d29d2fe 100644 --- a/locales/pt-BR.json +++ b/locales/pt-BR.json @@ -474,7 +474,7 @@ "Spanish (auto-generated)": "Espanhol (gerado automaticamente)", "Spanish (Mexico)": "Espanhol (México)", "search_filters_duration_option_none": "Qualquer duração", - "search_message_use_another_instance": " Você também pode <a href=\"`x`\">pesquisar em outra instância</a>.", + "search_message_use_another_instance": "Você também pode <a href=\"`x`\">pesquisar em outra instância</a>.", "Spanish (Spain)": "Espanhol (Espanha)", "Turkish (auto-generated)": "Turco (gerado automaticamente)", "search_filters_duration_option_medium": "Médio (4 - 20 minutos)", diff --git a/locales/pt.json b/locales/pt.json index 304e9cda..0bb1be66 100644 --- a/locales/pt.json +++ b/locales/pt.json @@ -448,7 +448,7 @@ "Chinese (Taiwan)": "Chinês (Taiwan)", "search_message_no_results": "Nenhum resultado encontrado.", "search_message_change_filters_or_query": "Tente alargar os termos genéricos da pesquisa e/ou alterar os filtros.", - "search_message_use_another_instance": " Também pode <a href=\"`x`\">pesquisar noutra instância</a>.", + "search_message_use_another_instance": "Também pode <a href=\"`x`\">pesquisar noutra instância</a>.", "English (United Kingdom)": "Inglês (Reino Unido)", "English (United States)": "Inglês (Estados Unidos)", "Cantonese (Hong Kong)": "Cantonês (Hong Kong)", @@ -508,7 +508,7 @@ "toggle_theme": "Trocar tema", "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", "carousel_slide": "Diapositivo {{current}} de{{total}}", "carousel_skip": "Ignorar carrossel", diff --git a/locales/ru.json b/locales/ru.json index efdaa640..80c98de8 100644 --- a/locales/ru.json +++ b/locales/ru.json @@ -509,6 +509,9 @@ "Add to playlist: ": "Добавить в плейлист: ", "Answer": "Ответить", "Search for videos": "Поиск видео", - "The Popular feed has been disabled by the administrator.": "Популярная лента была отключена администратором.", - "toggle_theme": "Переключатель тем" + "The Popular feed has been disabled by the administrator.": "Лента популярного была отключена администратором.", + "toggle_theme": "Переключатель тем", + "carousel_slide": "Пролистано {{current}} из {{total}}", + "carousel_skip": "Пропустить всё", + "carousel_go_to": "Перейти к странице `x`" } diff --git a/locales/sq.json b/locales/sq.json index 363a70b0..ea20ce56 100644 --- a/locales/sq.json +++ b/locales/sq.json @@ -257,13 +257,13 @@ "Video mode": "Mënyrë video", "channel_tab_videos_label": "Video", "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_type_label": "Lloj", "search_filters_duration_label": "Kohëzgjatje", "search_filters_features_label": "Veçori", "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_duration_option_long": "E gjatë (> 20 minuta)", "search_filters_features_option_hd": "HD", @@ -435,14 +435,14 @@ "tokens_count_plural": "{{count}} tokenë", "preferences_save_player_pos_label": "Mba mend pozicionin e luajtjes: ", "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", "preferences_vr_mode_label": "Video me ndërveprim 360 gradë (lyp WebGL): ", "Shared `x`": "Ndarë me të tjerë më `x`", "search_filters_title": "Filtra", "Popular enabled: ": "Me populloret të aktivizuara: ", "error_video_not_in_playlist": "Videoja e kërkuar s’ekziston në këtë luajlistë. <a href=\"`x`\">Klikoni këtu për faqen hyrëse të luajlistës.</a>", - "search_message_use_another_instance": " Mundeni edhe të <a href=\"`x`\">kërkoni në një instancë tjetër</a>.", + "search_message_use_another_instance": "Mundeni edhe të <a href=\"`x`\">kërkoni në një instancë tjetër</a>.", "search_filters_date_label": "Datë ngarkimi", "preferences_watch_history_label": "Aktivizo historik parjesh: ", "Top enabled: ": "Me kryesueset të aktivizuara: ", @@ -484,5 +484,13 @@ "Import YouTube watch history (.json)": "Importo historik parjesh YouTube (.json)", "preferences_local_label": "Video përmes ndërmjetësi: ", "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`" } diff --git a/locales/sr.json b/locales/sr.json index df3177c8..d28b2459 100644 --- a/locales/sr.json +++ b/locales/sr.json @@ -404,7 +404,7 @@ "generic_count_months_0": "{{count}} mesec", "generic_count_months_1": "{{count}} meseca", "generic_count_months_2": "{{count}} meseci", - "search_message_use_another_instance": " Takođe, možete <a href=\"`x`\">pretraživati na drugoj instanci</a>.", + "search_message_use_another_instance": "Takođe, možete <a href=\"`x`\">pretraživati na drugoj instanci</a>.", "generic_subscribers_count_0": "{{count}} pratilac", "generic_subscribers_count_1": "{{count}} pratioca", "generic_subscribers_count_2": "{{count}} pratilaca", diff --git a/locales/sr_Cyrl.json b/locales/sr_Cyrl.json index b59fba09..483e7fc4 100644 --- a/locales/sr_Cyrl.json +++ b/locales/sr_Cyrl.json @@ -404,7 +404,7 @@ "generic_count_months_0": "{{count}} месец", "generic_count_months_1": "{{count}} месеца", "generic_count_months_2": "{{count}} месеци", - "search_message_use_another_instance": " Такође, можете <a href=\"`x`\">претраживати на другој инстанци</a>.", + "search_message_use_another_instance": "Такође, можете <a href=\"`x`\">претраживати на другој инстанци</a>.", "generic_subscribers_count_0": "{{count}} пратилац", "generic_subscribers_count_1": "{{count}} пратиоца", "generic_subscribers_count_2": "{{count}} пратилаца", diff --git a/locales/sv-SE.json b/locales/sv-SE.json index b2f0fd17..f1313a4d 100644 --- a/locales/sv-SE.json +++ b/locales/sv-SE.json @@ -320,13 +320,13 @@ "channel_tab_community_label": "Gemenskap", "search_filters_sort_option_relevance": "Relevans", "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_type_label": "Typ", "search_filters_duration_label": "Varaktighet", "search_filters_features_label": "Funktioner", "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_week": "Denna vecka", "search_filters_date_option_month": "Denna månad", @@ -393,7 +393,7 @@ "Artist: ": "Artist: ", "generic_count_months": "{{count}}månad", "generic_count_months_plural": "{{count}}månader", - "search_message_use_another_instance": " Du kan också <a href=\"`x`\">söka på en annan instans</a>.", + "search_message_use_another_instance": "Du kan också <a href=\"`x`\">söka på en annan instans</a>.", "generic_subscribers_count": "{{count}} prenumerant", "generic_subscribers_count_plural": "{{count}} prenumeranter", "download_subtitles": "Undertexter - `x` (.vtt)", diff --git a/locales/tr.json b/locales/tr.json index 3b7bf3a4..282cbf88 100644 --- a/locales/tr.json +++ b/locales/tr.json @@ -322,13 +322,13 @@ "channel_tab_community_label": "Topluluk", "search_filters_sort_option_relevance": "İlgi", "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_type_label": "Tür", "search_filters_duration_label": "Süre", "search_filters_features_label": "Özellikler", "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_week": "Bu Hafta", "search_filters_date_option_month": "Bu Ay", @@ -452,7 +452,7 @@ "Spanish (Spain)": "İspanyolca (İspanya)", "Vietnamese (auto-generated)": "Vietnamca (Otomatik Oluşturuldu)", "preferences_watch_history_label": "İzleme Geçmişini Etkinleştir: ", - "search_message_use_another_instance": " Ayrıca <a href=\"`x`\">başka bir örnekte arayabilirsiniz</a>.", + "search_message_use_another_instance": "Ayrıca <a href=\"`x`\">başka bir örnekte arayabilirsiniz</a>.", "search_filters_type_option_all": "Herhangi Bir Tür", "search_filters_duration_option_none": "Herhangi Bir Süre", "search_message_no_results": "Sonuç bulunamadı.", diff --git a/locales/uk.json b/locales/uk.json index 5d008fa3..64329032 100644 --- a/locales/uk.json +++ b/locales/uk.json @@ -455,7 +455,7 @@ "search_filters_date_option_week": "Цей тиждень", "search_filters_type_label": "Тип", "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_date_option_hour": "Остання година", "search_filters_date_option_month": "Цей місяць", @@ -472,7 +472,7 @@ "search_filters_features_option_three_sixty": "360°", "search_filters_features_option_hdr": "HDR", "search_filters_sort_label": "Спершу", - "search_filters_sort_option_date": "Нещодавні", + "search_filters_sort_option_date": "Дата вивантаження", "search_filters_apply_button": "Застосувати фільтри", "search_filters_features_option_vr180": "VR180", "search_filters_features_option_purchased": "Придбано", diff --git a/locales/zh-CN.json b/locales/zh-CN.json index 756645f4..776c5ddb 100644 --- a/locales/zh-CN.json +++ b/locales/zh-CN.json @@ -436,7 +436,7 @@ "Turkish (auto-generated)": "土耳其语 (自动生成)", "Spanish (Spain)": "西班牙语 (西班牙)", "preferences_watch_history_label": "启用观看历史: ", - "search_message_use_another_instance": " 你也可以 <a href=\"`x`\">在另一实例上搜索</a>。", + "search_message_use_another_instance": "你也可以 <a href=\"`x`\">在另一实例上搜索</a>。", "search_filters_title": "过滤器", "search_filters_date_label": "上传日期", "search_filters_apply_button": "应用所选过滤器", diff --git a/locales/zh-TW.json b/locales/zh-TW.json index 2584db9c..1e17deb6 100644 --- a/locales/zh-TW.json +++ b/locales/zh-TW.json @@ -338,13 +338,13 @@ "channel_tab_community_label": "社群", "search_filters_sort_option_relevance": "關聯", "search_filters_sort_option_rating": "評分", - "search_filters_sort_option_date": "日期", + "search_filters_sort_option_date": "上傳日期", "search_filters_sort_option_views": "檢視", "search_filters_type_label": "內容類型", "search_filters_duration_label": "時長", "search_filters_features_label": "特色", "search_filters_sort_label": "排序", - "search_filters_date_option_hour": "小時", + "search_filters_date_option_hour": "最後一小時", "search_filters_date_option_today": "今天", "search_filters_date_option_week": "週", "search_filters_date_option_month": "月", @@ -442,7 +442,7 @@ "search_filters_duration_option_none": "任何時長", "search_filters_duration_option_medium": "中等(4到20分鐘)", "search_filters_features_option_vr180": "VR180", - "search_message_use_another_instance": " 您也可以<a href=\"`x`\">在其他站台上搜尋</a>。", + "search_message_use_another_instance": "您也可以<a href=\"`x`\">在其他站台上搜尋</a>。", "search_filters_title": "過濾條件", "search_filters_date_label": "上傳日期", "search_filters_type_option_all": "任何類型", diff --git a/mocks b/mocks -Subproject 11ec372f72747c09d48ffef04843f72be67d5b5 +Subproject b55d58dea94f7144ff0205857dfa70ec14eaa87 @@ -10,7 +10,7 @@ shards: backtracer: git: https://github.com/sija/backtracer.cr.git - version: 1.2.1 + version: 1.2.2 db: git: https://github.com/crystal-lang/crystal-db.git @@ -20,6 +20,10 @@ shards: git: https://github.com/crystal-loot/exception_page.git version: 0.2.2 + http_proxy: + git: https://github.com/mamantoha/http_proxy.git + version: 0.10.3 + kemal: git: https://github.com/kemalcr/kemal.git version: 1.1.2 @@ -42,7 +46,7 @@ shards: spectator: git: https://github.com/icy-arctic-fox/spectator.git - version: 0.10.4 + version: 0.10.6 sqlite3: git: https://github.com/crystal-lang/crystal-sqlite3.git @@ -28,6 +28,9 @@ dependencies: athena-negotiation: github: athena-framework/negotiation version: ~> 0.1.1 + http_proxy: + github: mamantoha/http_proxy + version: ~> 0.10.3 development_dependencies: spectator: diff --git a/spec/invidious/hashtag_spec.cr b/spec/invidious/hashtag_spec.cr index 266ec57b..abc81225 100644 --- a/spec/invidious/hashtag_spec.cr +++ b/spec/invidious/hashtag_spec.cr @@ -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.views).to eq(40_504_893) - expect(video_11.live_now).to be_false - expect(video_11.premium).to be_false + expect(video_11.badges.live_now?).to be_false + expect(video_11.badges.premium?).to be_false 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.views).to eq(30_790_049) - expect(video_35.live_now).to be_false - expect(video_35.premium).to be_false + expect(video_35.badges.live_now?).to be_false + expect(video_35.badges.premium?).to be_false expect(video_35.premiere_timestamp).to be_nil 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.views).to eq(63_240) - expect(video_41.live_now).to be_false - expect(video_41.premium).to be_false + expect(video_41.badges.live_now?).to be_false + expect(video_41.badges.premium?).to be_false 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.views).to eq(68_704) - expect(video_48.live_now).to be_false - expect(video_48.premium).to be_false + expect(video_48.badges.live_now?).to be_false + expect(video_48.badges.premium?).to be_false expect(video_48.premiere_timestamp).to be_nil end end diff --git a/spec/invidious/videos/regular_videos_extract_spec.cr b/spec/invidious/videos/regular_videos_extract_spec.cr index c647c1d1..f96703f6 100644 --- a/spec/invidious/videos/regular_videos_extract_spec.cr +++ b/spec/invidious/videos/regular_videos_extract_spec.cr @@ -17,8 +17,8 @@ Spectator.describe "parse_video_info" do # Basic video infos 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["likes"].as_i).to eq(5_157_654) + expect(info["views"].as_i).to eq(220_226_287) + expect(info["likes"].as_i).to eq(6_870_691) # For some reason the video length from VideoDetails and the # 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"][0]["id"]).to eq("Hwybp38GnZw") - expect(info["relatedVideos"][0]["title"]).to eq("I Built Willy Wonka's Chocolate Factory!") + expect(info["relatedVideos"][0]["id"]).to eq("krsBRQbOPQ4") + 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]["ucid"]).to eq("UCX6OQ3DkcsbYNE6H8uQQuVA") - expect(info["relatedVideos"][0]["view_count"]).to eq("179877630") - expect(info["relatedVideos"][0]["short_view_count"]).to eq("179M") + expect(info["relatedVideos"][0]["view_count"]).to eq("230617484") + expect(info["relatedVideos"][0]["short_view_count"]).to eq("230M") expect(info["relatedVideos"][0]["author_verified"]).to eq("true") # Description @@ -76,11 +76,11 @@ Spectator.describe "parse_video_info" do expect(info["ucid"].as_s).to eq("UCX6OQ3DkcsbYNE6H8uQQuVA") 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["subCountText"].as_s).to eq("143M") + expect(info["subCountText"].as_s).to eq("320M") end it "parses a regular video with no descrition/comments" do @@ -99,8 +99,8 @@ Spectator.describe "parse_video_info" do # Basic video infos expect(info["title"].as_s).to eq("Chris Rea - Auberge") - expect(info["views"].as_i).to eq(10_943_126) - expect(info["likes"].as_i).to eq(0) + expect(info["views"].as_i).to eq(14_324_584) + expect(info["likes"].as_i).to eq(35_870) expect(info["lengthSeconds"].as_i).to eq(283_i64) expect(info["published"].as_s).to eq("2012-05-21T00:00:00Z") @@ -132,14 +132,14 @@ Spectator.describe "parse_video_info" do # 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]["title"]).to eq("Chris Rea") - expect(info["relatedVideos"][0]["author"]).to eq("PanMusic") - expect(info["relatedVideos"][0]["ucid"]).to eq("UCsKAPSuh1iNbLWUga_igPyA") - expect(info["relatedVideos"][0]["view_count"]).to eq("31581") - expect(info["relatedVideos"][0]["short_view_count"]).to eq("31K") + expect(info["relatedVideos"][0]["id"]).to eq("gUUdQfnshJ4") + expect(info["relatedVideos"][0]["title"]).to eq("Chris Rea - The Road To Hell 1989 Full Version") + expect(info["relatedVideos"][0]["author"]).to eq("NEA ZIXNH") + expect(info["relatedVideos"][0]["ucid"]).to eq("UCYMEOGcvav3gCgImK2J07CQ") + expect(info["relatedVideos"][0]["view_count"]).to eq("53298661") + expect(info["relatedVideos"][0]["short_view_count"]).to eq("53M") expect(info["relatedVideos"][0]["author_verified"]).to eq("false") # Description @@ -156,11 +156,13 @@ Spectator.describe "parse_video_info" do # 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["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["subCountText"].as_s).to eq("-") + expect(info["subCountText"].as_s).to eq("3.11K") end end diff --git a/src/invidious.cr b/src/invidious.cr index d9a479d1..b422dcbb 100644 --- a/src/invidious.cr +++ b/src/invidious.cr @@ -23,6 +23,7 @@ require "kilt" require "./ext/kemal_content_for.cr" require "./ext/kemal_static_file_handler.cr" +require "http_proxy" require "athena-negotiation" require "openssl/hmac" require "option_parser" @@ -92,6 +93,10 @@ SOFTWARE = { YT_POOL = YoutubeConnectionPool.new(YT_URL, capacity: CONFIG.pool_size) +# Image request pool + +GGPHT_POOL = YoutubeConnectionPool.new(URI.parse("https://yt3.ggpht.com"), capacity: CONFIG.pool_size) + # CLI Kemal.config.extra_options do |parser| parser.banner = "Usage: invidious [arguments]" @@ -192,6 +197,8 @@ Invidious::Jobs.register Invidious::Jobs::NotificationJob.new(CONNECTION_CHANNEL Invidious::Jobs.register Invidious::Jobs::ClearExpiredItemsJob.new +Invidious::Jobs.register Invidious::Jobs::InstanceListRefreshJob.new + Invidious::Jobs.start_all def popular_videos diff --git a/src/invidious/channels/channels.cr b/src/invidious/channels/channels.cr index 29546e38..1478c8fc 100644 --- a/src/invidious/channels/channels.cr +++ b/src/invidious/channels/channels.cr @@ -223,7 +223,7 @@ def fetch_channel(ucid, pull_all_videos : Bool) length_seconds = channel_video.try &.length_seconds length_seconds ||= 0 - live_now = channel_video.try &.live_now + live_now = channel_video.try &.badges.live_now? live_now ||= false premiere_timestamp = channel_video.try &.premiere_timestamp @@ -275,7 +275,7 @@ def fetch_channel(ucid, pull_all_videos : Bool) ucid: video.ucid, author: video.author, length_seconds: video.length_seconds, - live_now: video.live_now, + live_now: video.badges.live_now?, premiere_timestamp: video.premiere_timestamp, views: video.views, }) diff --git a/src/invidious/config.cr b/src/invidious/config.cr index d8543d35..c4ca622f 100644 --- a/src/invidious/config.cr +++ b/src/invidious/config.cr @@ -13,6 +13,7 @@ struct ConfigPreferences property annotations : Bool = false property annotations_subscribed : Bool = false + property preload : Bool = true property autoplay : Bool = false property captions : Array(String) = ["", "", ""] property comments : Array(String) = ["youtube", ""] @@ -54,6 +55,15 @@ struct ConfigPreferences end end +struct HTTPProxyConfig + include YAML::Serializable + + property user : String + property password : String + property host : String + property port : Int32 +end + class Config include YAML::Serializable @@ -130,6 +140,8 @@ class Config property host_binding : String = "0.0.0.0" # Pool size for HTTP requests to youtube.com and ytimg.com (each domain has a separate pool of `pool_size`) property pool_size : Int32 = 100 + # HTTP Proxy configuration + property http_proxy : HTTPProxyConfig? = nil # Use Innertube's transcripts API instead of timedtext for closed captions property use_innertube_for_captions : Bool = false diff --git a/src/invidious/helpers/crystal_class_overrides.cr b/src/invidious/helpers/crystal_class_overrides.cr index fec3f62c..3040d7a0 100644 --- a/src/invidious/helpers/crystal_class_overrides.cr +++ b/src/invidious/helpers/crystal_class_overrides.cr @@ -18,6 +18,40 @@ end class HTTP::Client property family : Socket::Family = Socket::Family::UNSPEC + # Override stdlib to automatically initialize proxy if configured + # + # Accurate as of crystal 1.12.1 + + def initialize(@host : String, port = nil, tls : TLSContext = nil) + check_host_only(@host) + + {% if flag?(:without_openssl) %} + if tls + raise "HTTP::Client TLS is disabled because `-D without_openssl` was passed at compile time" + end + @tls = nil + {% else %} + @tls = case tls + when true + OpenSSL::SSL::Context::Client.new + when OpenSSL::SSL::Context::Client + tls + when false, nil + nil + end + {% end %} + + @port = (port || (@tls ? 443 : 80)).to_i + + self.proxy = make_configured_http_proxy_client() if CONFIG.http_proxy + end + + def initialize(@io : IO, @host = "", @port = 80) + @reconnect = false + + self.proxy = make_configured_http_proxy_client() if CONFIG.http_proxy + end + private def io io = @io return io if io diff --git a/src/invidious/helpers/errors.cr b/src/invidious/helpers/errors.cr index b2df682d..b7643194 100644 --- a/src/invidious/helpers/errors.cr +++ b/src/invidious/helpers/errors.cr @@ -43,6 +43,8 @@ def error_template_helper(env : HTTP::Server::Context, status_code : Int32, exce # URLs for the error message below 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 += "?q=is:issue+is:open+" + url_search_issues += URI.encode_www_form("[Bug] #{issue_title}") url_switch = "https://redirect.invidious.io" + env.request.resource diff --git a/src/invidious/helpers/serialized_yt_data.cr b/src/invidious/helpers/serialized_yt_data.cr index 463d5557..1fef5f93 100644 --- a/src/invidious/helpers/serialized_yt_data.cr +++ b/src/invidious/helpers/serialized_yt_data.cr @@ -1,3 +1,16 @@ +@[Flags] +enum VideoBadges + LiveNow + Premium + ThreeD + FourK + New + EightK + VR180 + VR360 + ClosedCaptions +end + struct SearchVideo include DB::Serializable @@ -9,10 +22,9 @@ struct SearchVideo property views : Int64 property description_html : String property length_seconds : Int32 - property live_now : Bool - property premium : Bool property premiere_timestamp : Time? property author_verified : Bool + property badges : VideoBadges def to_xml(auto_generated, query_params, xml : XML::Builder) query_params["v"] = self.id @@ -88,13 +100,20 @@ struct SearchVideo json.field "published", self.published.to_unix json.field "publishedText", translate(locale, "`x` ago", recode_date(self.published, locale)) json.field "lengthSeconds", self.length_seconds - json.field "liveNow", self.live_now - json.field "premium", self.premium + json.field "liveNow", self.badges.live_now? + json.field "premium", self.badges.premium? json.field "isUpcoming", self.upcoming? if self.premiere_timestamp json.field "premiereTimestamp", self.premiere_timestamp.try &.to_unix 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 diff --git a/src/invidious/helpers/sig_helper.cr b/src/invidious/helpers/sig_helper.cr index 9e72c1c7..6d198a42 100644 --- a/src/invidious/helpers/sig_helper.cr +++ b/src/invidious/helpers/sig_helper.cr @@ -175,8 +175,9 @@ module Invidious::SigHelper @queue = {} of TransactionID => Transaction @conn : Connection + @uri_or_path : String - def initialize(uri_or_path) + def initialize(@uri_or_path) @conn = Connection.new(uri_or_path) listen end @@ -186,10 +187,26 @@ module Invidious::SigHelper LOGGER.debug("SigHelper: Multiplexor listening") - # TODO: reopen socket if unexpectedly closed spawn do loop do - receive_data + begin + receive_data + rescue ex + LOGGER.info("SigHelper: Connection to helper died with '#{ex.message}' trying to reconnect...") + # We close the socket because for some reason is not closed. + @conn.close + loop do + begin + @conn = Connection.new(@uri_or_path) + LOGGER.info("SigHelper: Reconnected to SigHelper!") + rescue ex + LOGGER.debug("SigHelper: Reconnection to helper unsuccessful with error '#{ex.message}'. Retrying") + sleep 500.milliseconds + next + end + break if !@conn.closed? + end + end Fiber.yield end end diff --git a/src/invidious/helpers/utils.cr b/src/invidious/helpers/utils.cr index 8e9e9a6a..4d9bb28d 100644 --- a/src/invidious/helpers/utils.cr +++ b/src/invidious/helpers/utils.cr @@ -323,68 +323,6 @@ def parse_range(range) return 0_i64, nil 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 str = uri.to_s.sub(/^https?:\/\//, "") if str.size > max_length diff --git a/src/invidious/jobs/instance_refresh_job.cr b/src/invidious/jobs/instance_refresh_job.cr new file mode 100644 index 00000000..cb4280b9 --- /dev/null +++ b/src/invidious/jobs/instance_refresh_job.cr @@ -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 diff --git a/src/invidious/playlists.cr b/src/invidious/playlists.cr index 3e6eef95..a51e88b4 100644 --- a/src/invidious/playlists.cr +++ b/src/invidious/playlists.cr @@ -270,7 +270,7 @@ end def subscribe_playlist(user, playlist) playlist = InvidiousPlaylist.new({ - title: playlist.title.byte_slice(0, 150), + title: playlist.title[..150], id: playlist.id, author: user.email, description: "", # Max 5000 characters diff --git a/src/invidious/routes/api/v1/search.cr b/src/invidious/routes/api/v1/search.cr index 2922b060..59a30745 100644 --- a/src/invidious/routes/api/v1/search.cr +++ b/src/invidious/routes/api/v1/search.cr @@ -31,9 +31,7 @@ module Invidious::Routes::API::V1::Search query = env.params.query["q"]? || "" begin - client = HTTP::Client.new("suggestqueries-clients6.youtube.com") - client.before_request { |r| add_yt_headers(r) } - + client = make_client(URI.parse("https://suggestqueries-clients6.youtube.com"), force_youtube_headers: true) url = "/complete/search?client=youtube&hl=en&gl=#{region}&q=#{URI.encode_www_form(query)}&gs_ri=youtube&ds=yt" response = client.get(url).body diff --git a/src/invidious/routes/feeds.cr b/src/invidious/routes/feeds.cr index e20a7139..ea7fb396 100644 --- a/src/invidious/routes/feeds.cr +++ b/src/invidious/routes/feeds.cr @@ -192,11 +192,9 @@ module Invidious::Routes::Feeds views: views, description_html: description_html, length_seconds: 0, - live_now: false, - paid: false, - premium: false, premiere_timestamp: nil, author_verified: false, + badges: VideoBadges::None, }) end diff --git a/src/invidious/routes/images.cr b/src/invidious/routes/images.cr index b6a2e110..639697db 100644 --- a/src/invidious/routes/images.cr +++ b/src/invidious/routes/images.cr @@ -11,29 +11,9 @@ module Invidious::Routes::Images end end - # We're encapsulating this into a proc in order to easily reuse this - # portion of the code for each request block below. - request_proc = ->(response : HTTP::Client::Response) { - env.response.status_code = response.status_code - response.headers.each do |key, value| - if !RESPONSE_HEADERS_BLACKLIST.includes?(key.downcase) - env.response.headers[key] = value - end - end - - env.response.headers["Access-Control-Allow-Origin"] = "*" - - if response.status_code >= 300 - env.response.headers.delete("Transfer-Encoding") - return - end - - proxy_file(response, env) - } - begin - HTTP::Client.get("https://yt3.ggpht.com#{url}") do |resp| - return request_proc.call(resp) + GGPHT_POOL.client &.get(url, headers) do |resp| + return self.proxy_image(env, resp) end rescue ex end @@ -61,27 +41,10 @@ module Invidious::Routes::Images end end - request_proc = ->(response : HTTP::Client::Response) { - env.response.status_code = response.status_code - response.headers.each do |key, value| - if !RESPONSE_HEADERS_BLACKLIST.includes?(key.downcase) - env.response.headers[key] = value - end - end - - env.response.headers["Connection"] = "close" - env.response.headers["Access-Control-Allow-Origin"] = "*" - - if response.status_code >= 300 - return env.response.headers.delete("Transfer-Encoding") - end - - proxy_file(response, env) - } - begin - HTTP::Client.get("https://#{authority}.ytimg.com#{url}") do |resp| - return request_proc.call(resp) + get_ytimg_pool(authority).client &.get(url, headers) do |resp| + env.response.headers["Connection"] = "close" + return self.proxy_image(env, resp) end rescue ex end @@ -101,26 +64,9 @@ module Invidious::Routes::Images end end - request_proc = ->(response : HTTP::Client::Response) { - env.response.status_code = response.status_code - response.headers.each do |key, value| - if !RESPONSE_HEADERS_BLACKLIST.includes?(key.downcase) - env.response.headers[key] = value - end - end - - env.response.headers["Access-Control-Allow-Origin"] = "*" - - if response.status_code >= 300 && response.status_code != 404 - return env.response.headers.delete("Transfer-Encoding") - end - - proxy_file(response, env) - } - begin - HTTP::Client.get("https://i9.ytimg.com#{url}") do |resp| - return request_proc.call(resp) + get_ytimg_pool("i9").client &.get(url, headers) do |resp| + return self.proxy_image(env, resp) end rescue ex end @@ -165,8 +111,7 @@ module Invidious::Routes::Images if name == "maxres.jpg" build_thumbnails(id).each do |thumb| thumbnail_resource_path = "/vi/#{id}/#{thumb[:url]}.jpg" - # This can likely be optimized into a (small) pool sometime in the future. - if HTTP::Client.head("https://i.ytimg.com#{thumbnail_resource_path}").status_code == 200 + if get_ytimg_pool("i9").client &.head(thumbnail_resource_path, headers).status_code == 200 name = thumb[:url] + ".jpg" break end @@ -181,29 +126,28 @@ module Invidious::Routes::Images end end - request_proc = ->(response : HTTP::Client::Response) { - env.response.status_code = response.status_code - response.headers.each do |key, value| - if !RESPONSE_HEADERS_BLACKLIST.includes?(key.downcase) - env.response.headers[key] = value - end + begin + get_ytimg_pool("i").client &.get(url, headers) do |resp| + return self.proxy_image(env, resp) end + rescue ex + end + end - env.response.headers["Access-Control-Allow-Origin"] = "*" - - if response.status_code >= 300 && response.status_code != 404 - return env.response.headers.delete("Transfer-Encoding") + private def self.proxy_image(env, response) + env.response.status_code = response.status_code + response.headers.each do |key, value| + if !RESPONSE_HEADERS_BLACKLIST.includes?(key.downcase) + env.response.headers[key] = value end + end - proxy_file(response, env) - } + env.response.headers["Access-Control-Allow-Origin"] = "*" - begin - # This can likely be optimized into a (small) pool sometime in the future. - HTTP::Client.get("https://i.ytimg.com#{url}") do |resp| - return request_proc.call(resp) - end - rescue ex + if response.status_code >= 300 + return env.response.headers.delete("Transfer-Encoding") end + + return proxy_file(response, env) end end diff --git a/src/invidious/routes/misc.cr b/src/invidious/routes/misc.cr index d6bd9571..8b620d63 100644 --- a/src/invidious/routes/misc.cr +++ b/src/invidious/routes/misc.cr @@ -40,7 +40,16 @@ module Invidious::Routes::Misc def self.cross_instance_redirect(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}" end end diff --git a/src/invidious/routes/preferences.cr b/src/invidious/routes/preferences.cr index 05bc2714..39ca77c0 100644 --- a/src/invidious/routes/preferences.cr +++ b/src/invidious/routes/preferences.cr @@ -27,6 +27,10 @@ module Invidious::Routes::PreferencesRoute annotations_subscribed ||= "off" 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 ||= "off" autoplay = autoplay == "on" @@ -144,6 +148,7 @@ module Invidious::Routes::PreferencesRoute preferences = Preferences.from_json({ annotations: annotations, annotations_subscribed: annotations_subscribed, + preload: preload, autoplay: autoplay, captions: captions, comments: comments, diff --git a/src/invidious/routes/video_playback.cr b/src/invidious/routes/video_playback.cr index 24693662..26852d06 100644 --- a/src/invidious/routes/video_playback.cr +++ b/src/invidious/routes/video_playback.cr @@ -42,7 +42,7 @@ module Invidious::Routes::VideoPlayback headers["Range"] = "bytes=#{range_for_head}" end - client = make_client(URI.parse(host), region, force_resolve = true) + client = make_client(URI.parse(host), region, force_resolve: true) response = HTTP::Client::Response.new(500) error = "" 5.times do @@ -57,7 +57,7 @@ module Invidious::Routes::VideoPlayback if new_host != host host = new_host client.close - client = make_client(URI.parse(new_host), region, force_resolve = true) + client = make_client(URI.parse(new_host), region, force_resolve: true) end url = "#{location.request_target}&host=#{location.host}#{region ? "®ion=#{region}" : ""}" @@ -71,7 +71,7 @@ module Invidious::Routes::VideoPlayback fvip = "3" host = "https://r#{fvip}---#{mn}.googlevideo.com" - client = make_client(URI.parse(host), region, force_resolve = true) + client = make_client(URI.parse(host), region, force_resolve: true) rescue ex error = ex.message end @@ -196,7 +196,7 @@ module Invidious::Routes::VideoPlayback break else client.close - client = make_client(URI.parse(host), region, force_resolve = true) + client = make_client(URI.parse(host), region, force_resolve: true) end end diff --git a/src/invidious/user/preferences.cr b/src/invidious/user/preferences.cr index b3059403..0a8525f3 100644 --- a/src/invidious/user/preferences.cr +++ b/src/invidious/user/preferences.cr @@ -4,6 +4,7 @@ struct Preferences property annotations : Bool = CONFIG.default_user_preferences.annotations 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 automatic_instance_redirect : Bool = CONFIG.default_user_preferences.automatic_instance_redirect diff --git a/src/invidious/videos.cr b/src/invidious/videos.cr index 921132f0..ae09e736 100644 --- a/src/invidious/videos.cr +++ b/src/invidious/videos.cr @@ -27,12 +27,6 @@ struct Video @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)] property description : String? module JSONConverter @@ -98,72 +92,24 @@ struct Video # Methods for parsing streaming data - 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("Videos: Decoding '#{cfr}'") - - unsig = DECRYPT_FUNCTION.try &.decrypt_signature(cfr["s"]) - params[sp] = unsig if unsig + def fmt_stream : Array(Hash(String, JSON::Any)) + if formats = info.dig?("streamingData", "formats") + return formats + .as_a.map(&.as_h) + .sort_by! { |f| f["width"]?.try &.as_i || 0 } 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 - - 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)) + return [] of Hash(String, JSON::Any) 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)) + def adaptive_fmts : Array(Hash(String, JSON::Any)) + if formats = info.dig?("streamingData", "adaptiveFormats") + return formats + .as_a.map(&.as_h) + .sort_by! { |f| f["width"]?.try &.as_i || 0 } + else + return [] of Hash(String, JSON::Any) 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 def video_streams diff --git a/src/invidious/videos/caption.cr b/src/invidious/videos/caption.cr index 484e61d2..c811cfe1 100644 --- a/src/invidious/videos/caption.cr +++ b/src/invidious/videos/caption.cr @@ -123,6 +123,7 @@ module Invidious::Videos "Esperanto", "Estonian", "Filipino", + "Filipino (auto-generated)", "Finnish", "French", "French (auto-generated)", diff --git a/src/invidious/videos/parser.cr b/src/invidious/videos/parser.cr index ca6500ed..fb8935d9 100644 --- a/src/invidious/videos/parser.cr +++ b/src/invidious/videos/parser.cr @@ -106,7 +106,7 @@ def extract_video_info(video_id : String) new_player_response = nil - # Second try in case WEB_EMBEDDED_PLAYER doesn't work with po_token. + # Second try in case WEB_CREATOR doesn't work with po_token. # Only trigger if reason found and po_token configured. if reason && CONFIG.po_token client_config.client_type = YoutubeAPI::ClientType::WebEmbeddedPlayer @@ -142,10 +142,21 @@ def extract_video_info(video_id : String) params.delete("reason") 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]? 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 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 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 diff --git a/src/invidious/videos/video_preferences.cr b/src/invidious/videos/video_preferences.cr index 34cf7ff0..48177bd8 100644 --- a/src/invidious/videos/video_preferences.cr +++ b/src/invidious/videos/video_preferences.cr @@ -2,6 +2,7 @@ struct VideoPreferences include JSON::Serializable property annotations : Bool + property preload : Bool property autoplay : Bool property comments : Array(String) property continue : Bool @@ -28,6 +29,7 @@ end def process_video_params(query, preferences) 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 } comments = query["comments"]?.try &.split(",").map(&.downcase) continue = query["continue"]?.try { |q| (q == "true" || q == "1").to_unsafe } @@ -50,6 +52,7 @@ def process_video_params(query, preferences) if preferences # region ||= preferences.region annotations ||= preferences.annotations.to_unsafe + preload ||= preferences.preload.to_unsafe autoplay ||= preferences.autoplay.to_unsafe comments ||= preferences.comments continue ||= preferences.continue.to_unsafe @@ -70,6 +73,7 @@ def process_video_params(query, preferences) end annotations ||= CONFIG.default_user_preferences.annotations.to_unsafe + preload ||= CONFIG.default_user_preferences.preload.to_unsafe autoplay ||= CONFIG.default_user_preferences.autoplay.to_unsafe comments ||= CONFIG.default_user_preferences.comments 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 annotations = annotations == 1 + preload = preload == 1 autoplay = autoplay == 1 continue = continue == 1 continue_autoplay = continue_autoplay == 1 @@ -128,6 +133,7 @@ def process_video_params(query, preferences) params = VideoPreferences.new({ annotations: annotations, + preload: preload, autoplay: autoplay, comments: comments, continue: continue, diff --git a/src/invidious/views/components/player.ecr b/src/invidious/views/components/player.ecr index c3c02df0..5c28358b 100644 --- a/src/invidious/views/components/player.ecr +++ b/src/invidious/views/components/player.ecr @@ -1,5 +1,6 @@ <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 %>" + preload="<% if params.preload %>auto<% else %>none<% end %>" <% if params.autoplay %>autoplay<% end %> <% if params.video_loop %>loop<% end %> <% if params.controls %>controls<% end %>> diff --git a/src/invidious/views/user/preferences.ecr b/src/invidious/views/user/preferences.ecr index b89c73ca..cf8b5593 100644 --- a/src/invidious/views/user/preferences.ecr +++ b/src/invidious/views/user/preferences.ecr @@ -13,6 +13,11 @@ </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"> <label for="autoplay"><%= translate(locale, "preferences_autoplay_label") %></label> <input name="autoplay" id="autoplay" type="checkbox" <% if preferences.autoplay %>checked<% end %>> </div> diff --git a/src/invidious/yt_backend/connection_pool.cr b/src/invidious/yt_backend/connection_pool.cr index ca612083..7bbccfd5 100644 --- a/src/invidious/yt_backend/connection_pool.cr +++ b/src/invidious/yt_backend/connection_pool.cr @@ -1,17 +1,6 @@ -def add_yt_headers(request) - request.headers.delete("User-Agent") if request.headers["User-Agent"] == "Crystal" - request.headers["User-Agent"] ||= "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36" - - request.headers["Accept-Charset"] ||= "ISO-8859-1,utf-8;q=0.7,*;q=0.7" - request.headers["Accept"] ||= "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8" - request.headers["Accept-Language"] ||= "en-us,en;q=0.5" - - # Preserve original cookies and add new YT consent cookie for EU servers - request.headers["Cookie"] = "#{request.headers["cookie"]?}; CONSENT=PENDING+#{Random.rand(100..999)}" - if !CONFIG.cookies.empty? - request.headers["Cookie"] = "#{(CONFIG.cookies.map { |c| "#{c.name}=#{c.value}" }).join("; ")}; #{request.headers["cookie"]?}" - end -end +# Mapping of subdomain => YoutubeConnectionPool +# This is needed as we may need to access arbitrary subdomains of ytimg +private YTIMG_POOLS = {} of String => YoutubeConnectionPool struct YoutubeConnectionPool property! url : URI @@ -26,15 +15,16 @@ struct YoutubeConnectionPool def client(&) conn = pool.checkout + # Proxy needs to be reinstated every time we get a client from the pool + conn.proxy = make_configured_http_proxy_client() if CONFIG.http_proxy + begin response = yield conn rescue ex conn.close - conn = HTTP::Client.new(url) - conn.family = CONFIG.force_resolve - conn.family = Socket::Family::INET if conn.family == Socket::Family::UNSPEC - conn.before_request { |r| add_yt_headers(r) } if url.host == "www.youtube.com" + conn = make_client(url, force_resolve: true) + response = yield conn ensure pool.release(conn) @@ -45,24 +35,37 @@ struct YoutubeConnectionPool private def build_pool DB::Pool(HTTP::Client).new(initial_pool_size: 0, max_pool_size: capacity, max_idle_pool_size: capacity, checkout_timeout: timeout) do - conn = HTTP::Client.new(url) - conn.family = CONFIG.force_resolve - conn.family = Socket::Family::INET if conn.family == Socket::Family::UNSPEC - conn.before_request { |r| add_yt_headers(r) } if url.host == "www.youtube.com" - conn + next make_client(url, force_resolve: true) end end end -def make_client(url : URI, region = nil, force_resolve : Bool = false) +def add_yt_headers(request) + request.headers.delete("User-Agent") if request.headers["User-Agent"] == "Crystal" + request.headers["User-Agent"] ||= "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36" + + request.headers["Accept-Charset"] ||= "ISO-8859-1,utf-8;q=0.7,*;q=0.7" + request.headers["Accept"] ||= "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8" + request.headers["Accept-Language"] ||= "en-us,en;q=0.5" + + # Preserve original cookies and add new YT consent cookie for EU servers + request.headers["Cookie"] = "#{request.headers["cookie"]?}; CONSENT=PENDING+#{Random.rand(100..999)}" + if !CONFIG.cookies.empty? + request.headers["Cookie"] = "#{(CONFIG.cookies.map { |c| "#{c.name}=#{c.value}" }).join("; ")}; #{request.headers["cookie"]?}" + end +end + +def make_client(url : URI, region = nil, force_resolve : Bool = false, force_youtube_headers : Bool = false) client = HTTP::Client.new(url) + client.proxy = make_configured_http_proxy_client() if CONFIG.http_proxy # Force the usage of a specific configured IP Family if force_resolve client.family = CONFIG.force_resolve + client.family = Socket::Family::INET if client.family == Socket::Family::UNSPEC end - client.before_request { |r| add_yt_headers(r) } if url.host == "www.youtube.com" + client.before_request { |r| add_yt_headers(r) } if url.host.try &.ends_with?("youtube.com") || force_youtube_headers client.read_timeout = 10.seconds client.connect_timeout = 10.seconds @@ -70,10 +73,38 @@ def make_client(url : URI, region = nil, force_resolve : Bool = false) end def make_client(url : URI, region = nil, force_resolve : Bool = false, &) - client = make_client(url, region, force_resolve) + client = make_client(url, region, force_resolve: force_resolve) begin yield client ensure client.close end end + +def make_configured_http_proxy_client + # This method is only called when configuration for an HTTP proxy are set + config_proxy = CONFIG.http_proxy.not_nil! + + return HTTP::Proxy::Client.new( + config_proxy.host, + config_proxy.port, + + username: config_proxy.user, + password: config_proxy.password, + ) +end + +# Fetches a HTTP pool for the specified subdomain of ytimg.com +# +# Creates a new one when the specified pool for the subdomain does not exist +def get_ytimg_pool(subdomain) + if pool = YTIMG_POOLS[subdomain]? + return pool + else + LOGGER.info("ytimg_pool: Creating a new HTTP pool for \"https://#{subdomain}.ytimg.com\"") + pool = YoutubeConnectionPool.new(URI.parse("https://#{subdomain}.ytimg.com"), capacity: CONFIG.pool_size) + YTIMG_POOLS[subdomain] = pool + + return pool + end +end diff --git a/src/invidious/yt_backend/extractors.cr b/src/invidious/yt_backend/extractors.cr index 38dc2c04..4074de86 100644 --- a/src/invidious/yt_backend/extractors.cr +++ b/src/invidious/yt_backend/extractors.cr @@ -108,21 +108,30 @@ private module Parsers length_seconds = 0 end - live_now = false - premium = false - 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| b = badge["metadataBadgeRenderer"] case b["label"].as_s - when "LIVE NOW" - live_now = true - when "New", "4K", "CC" - # TODO + when "LIVE" + badges |= VideoBadges::LiveNow + when "New" + 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" # TODO: Potentially available as item_contents["topStandaloneBadge"]["metadataBadgeRenderer"] - premium = true + badges |= VideoBadges::Premium else nil # Ignore end end @@ -136,10 +145,9 @@ private module Parsers views: view_count, description_html: description_html, length_seconds: length_seconds, - live_now: live_now, - premium: premium, premiere_timestamp: premiere_timestamp, author_verified: author_verified, + badges: badges, }) end @@ -563,10 +571,9 @@ private module Parsers views: view_count, description_html: "", length_seconds: duration, - live_now: false, - premium: false, premiere_timestamp: Time.unix(0), author_verified: false, + badges: VideoBadges::None, }) end diff --git a/src/invidious/yt_backend/url_sanitizer.cr b/src/invidious/yt_backend/url_sanitizer.cr index 725382ee..d539dadb 100644 --- a/src/invidious/yt_backend/url_sanitizer.cr +++ b/src/invidious/yt_backend/url_sanitizer.cr @@ -111,7 +111,7 @@ module UrlSanitizer new_uri.path = "/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 end diff --git a/src/invidious/yt_backend/youtube_api.cr b/src/invidious/yt_backend/youtube_api.cr index 99ec6e63..e0a3181f 100644 --- a/src/invidious/yt_backend/youtube_api.cr +++ b/src/invidious/yt_backend/youtube_api.cr @@ -84,7 +84,7 @@ module YoutubeAPI ClientType::WebCreator => { name: "WEB_CREATOR", name_proto: "62", - version: "1.20220918", + version: "1.20240918.03.00", os_name: "Windows", os_version: WINDOWS_VERSION, platform: "DESKTOP", @@ -638,6 +638,11 @@ module YoutubeAPI # Send the POST request body = YT_POOL.client() do |client| client.post(url, headers: headers, body: data.to_json) do |response| + if response.status_code != 200 + raise InfoException.new("Error: non 200 status code. Youtube API returned \ + status code #{response.status_code}. See <a href=\"https://docs.invidious.io/youtube-errors-explained/\"> \ + https://docs.invidious.io/youtube-errors-explained/</a> for troubleshooting.") + end self._decompress(response.body_io, response.headers["Content-Encoding"]?) end end |
