summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.ameba.yml3
-rw-r--r--.github/CODEOWNERS2
-rw-r--r--assets/js/player.js1
-rw-r--r--config/config.example.yml16
-rw-r--r--locales/ar.json2
-rw-r--r--locales/cs.json2
-rw-r--r--locales/de.json10
-rw-r--r--locales/el.json7
-rw-r--r--locales/en-US.json7
-rw-r--r--locales/es.json2
-rw-r--r--locales/fa.json4
-rw-r--r--locales/fr.json2
-rw-r--r--locales/hr.json26
-rw-r--r--locales/ia.json4
-rw-r--r--locales/is.json2
-rw-r--r--locales/it.json2
-rw-r--r--locales/ja.json6
-rw-r--r--locales/ko.json40
-rw-r--r--locales/nb-NO.json10
-rw-r--r--locales/nl.json10
-rw-r--r--locales/pl.json2
-rw-r--r--locales/pt-BR.json2
-rw-r--r--locales/pt.json4
-rw-r--r--locales/ru.json7
-rw-r--r--locales/sq.json18
-rw-r--r--locales/sr.json2
-rw-r--r--locales/sr_Cyrl.json2
-rw-r--r--locales/sv-SE.json6
-rw-r--r--locales/tr.json6
-rw-r--r--locales/uk.json4
-rw-r--r--locales/zh-CN.json2
-rw-r--r--locales/zh-TW.json6
-rw-r--r--spec/invidious/hashtag_spec.cr16
-rw-r--r--src/invidious.cr2
-rw-r--r--src/invidious/channels/channels.cr4
-rw-r--r--src/invidious/config.cr1
-rw-r--r--src/invidious/helpers/errors.cr2
-rw-r--r--src/invidious/helpers/serialized_yt_data.cr27
-rw-r--r--src/invidious/helpers/utils.cr62
-rw-r--r--src/invidious/jobs/instance_refresh_job.cr97
-rw-r--r--src/invidious/playlists.cr2
-rw-r--r--src/invidious/routes/feeds.cr4
-rw-r--r--src/invidious/routes/misc.cr11
-rw-r--r--src/invidious/routes/preferences.cr5
-rw-r--r--src/invidious/user/preferences.cr1
-rw-r--r--src/invidious/videos.cr80
-rw-r--r--src/invidious/videos/parser.cr70
-rw-r--r--src/invidious/videos/video_preferences.cr6
-rw-r--r--src/invidious/views/components/player.ecr1
-rw-r--r--src/invidious/views/user/preferences.ecr5
-rw-r--r--src/invidious/yt_backend/extractors.cr33
-rw-r--r--src/invidious/yt_backend/youtube_api.cr12
52 files changed, 401 insertions, 259 deletions
diff --git a/.ameba.yml b/.ameba.yml
index df97b539..36d7c48f 100644
--- a/.ameba.yml
+++ b/.ameba.yml
@@ -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/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 219aa03f..e9eebfde 100644
--- a/config/config.example.yml
+++ b/config/config.example.yml
@@ -708,6 +708,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..7827d9c6 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: ",
@@ -422,7 +423,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 +455,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/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/src/invidious.cr b/src/invidious.cr
index 3804197e..63f2a9cc 100644
--- a/src/invidious.cr
+++ b/src/invidious.cr
@@ -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::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 c4ddcdb3..a097b7f1 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", ""]
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/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/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/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/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/parser.cr b/src/invidious/videos/parser.cr
index 95fa3d79..fb8935d9 100644
--- a/src/invidious/videos/parser.cr
+++ b/src/invidious/videos/parser.cr
@@ -53,6 +53,10 @@ end
def extract_video_info(video_id : String)
# Init client config for the API
client_config = YoutubeAPI::ClientConfig.new
+ # Use the WEB_CREATOR when po_token is configured because it fully only works on this client
+ if CONFIG.po_token
+ client_config.client_type = YoutubeAPI::ClientType::WebCreator
+ end
# Fetch data from the player endpoint
player_response = YoutubeAPI.player(video_id: video_id, params: "2AMB", client_config: client_config)
@@ -102,6 +106,13 @@ def extract_video_info(video_id : String)
new_player_response = nil
+ # 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
+ new_player_response = try_fetch_streaming_data(video_id, client_config)
+ end
+
# Don't use Android client if po_token is passed because po_token doesn't
# work for Android client.
if reason.nil? && CONFIG.po_token.nil?
@@ -114,10 +125,9 @@ def extract_video_info(video_id : String)
end
# Last hope
- # Only trigger if reason found and po_token or didn't work wth Android client.
- # TvHtml5ScreenEmbed now requires sig helper for it to work but po_token is not required
- # if the IP address is not blocked.
- if CONFIG.po_token && reason || CONFIG.po_token.nil? && new_player_response.nil?
+ # Only trigger if reason found or didn't work wth Android client.
+ # TvHtml5ScreenEmbed now requires sig helper for it to work but doesn't work with po_token.
+ if reason && CONFIG.po_token.nil?
client_config.client_type = YoutubeAPI::ClientType::TvHtml5ScreenEmbed
new_player_response = try_fetch_streaming_data(video_id, client_config)
end
@@ -132,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)
@@ -185,10 +206,11 @@ def parse_video_info(video_id : String, player_response : Hash(String, JSON::Any
end
video_details = player_response.dig?("videoDetails")
- microformat = player_response.dig?("microformat", "playerMicroformatRenderer")
+ if !(microformat = player_response.dig?("microformat", "playerMicroformatRenderer"))
+ microformat = {} of String => JSON::Any
+ end
raise BrokenTubeException.new("videoDetails") if !video_details
- raise BrokenTubeException.new("microformat") if !microformat
# Basic video infos
@@ -225,7 +247,7 @@ def parse_video_info(video_id : String, player_response : Hash(String, JSON::Any
.try &.as_a.map &.as_s || [] of String
allow_ratings = video_details["allowRatings"]?.try &.as_bool
- family_friendly = microformat["isFamilySafe"].try &.as_bool
+ family_friendly = microformat["isFamilySafe"]?.try &.as_bool
is_listed = video_details["isCrawlable"]?.try &.as_bool
is_upcoming = video_details["isUpcoming"]?.try &.as_bool
@@ -443,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/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/youtube_api.cr b/src/invidious/yt_backend/youtube_api.cr
index d66bf7aa..baa3cd92 100644
--- a/src/invidious/yt_backend/youtube_api.cr
+++ b/src/invidious/yt_backend/youtube_api.cr
@@ -29,6 +29,7 @@ module YoutubeAPI
WebEmbeddedPlayer
WebMobile
WebScreenEmbed
+ WebCreator
Android
AndroidEmbeddedPlayer
@@ -80,6 +81,14 @@ module YoutubeAPI
os_version: WINDOWS_VERSION,
platform: "DESKTOP",
},
+ ClientType::WebCreator => {
+ name: "WEB_CREATOR",
+ name_proto: "62",
+ version: "1.20240918.03.00",
+ os_name: "Windows",
+ os_version: WINDOWS_VERSION,
+ platform: "DESKTOP",
+ },
# Android
@@ -291,8 +300,9 @@ module YoutubeAPI
end
if client_config.screen == "EMBED"
+ # embedUrl https://www.google.com allow loading almost all video that are configured not embeddable
client_context["thirdParty"] = {
- "embedUrl" => "https://www.youtube.com/embed/#{video_id}",
+ "embedUrl" => "https://www.google.com/",
} of String => String | Int64
end