diff options
55 files changed, 1153 insertions, 145 deletions
diff --git a/assets/js/pagination.js b/assets/js/pagination.js new file mode 100644 index 00000000..2e560a34 --- /dev/null +++ b/assets/js/pagination.js @@ -0,0 +1,93 @@ +'use strict'; + +const CURRENT_CONTINUATION = (new URL(document.location)).searchParams.get("continuation"); +const CONT_CACHE_KEY = `continuation_cache_${encodeURIComponent(window.location.pathname)}`; + +function get_data(){ + return JSON.parse(sessionStorage.getItem(CONT_CACHE_KEY)) || []; +} + +function save_data(){ + const prev_data = get_data(); + prev_data.push(CURRENT_CONTINUATION); + + sessionStorage.setItem(CONT_CACHE_KEY, JSON.stringify(prev_data)); +} + +function button_press(){ + let prev_data = get_data(); + if (!prev_data.length) return null; + + // Sanity check. Nowhere should the current continuation token exist in the cache + // but it can happen when using the browser's back feature. As such we'd need to travel + // back to the point where the current continuation token first appears in order to + // account for the rewind. + const conflict_at = prev_data.indexOf(CURRENT_CONTINUATION); + if (conflict_at != -1) { + prev_data.length = conflict_at; + } + + const prev_ctoken = prev_data.pop(); + + // On the first page, the stored continuation token is null. + if (prev_ctoken === null) { + sessionStorage.removeItem(CONT_CACHE_KEY); + let url = set_continuation(); + window.location.href = url; + + return; + } + + sessionStorage.setItem(CONT_CACHE_KEY, JSON.stringify(prev_data)); + let url = set_continuation(prev_ctoken); + + window.location.href = url; +}; + +// Method to set the current page's continuation token +// Removes the continuation parameter when a continuation token is not given +function set_continuation(prev_ctoken = null){ + let url = window.location.href.split('?')[0]; + let params = window.location.href.split('?')[1]; + let url_params = new URLSearchParams(params); + + if (prev_ctoken) { + url_params.set("continuation", prev_ctoken); + } else { + url_params.delete('continuation'); + }; + + if(Array.from(url_params).length > 0){ + return `${url}?${url_params.toString()}`; + } else { + return url; + } +} + +addEventListener('DOMContentLoaded', function(){ + const pagination_data = JSON.parse(document.getElementById('pagination-data').textContent); + const next_page_containers = document.getElementsByClassName("page-next-container"); + + for (let container of next_page_containers){ + const next_page_button = container.getElementsByClassName("pure-button") + + // exists? + if (next_page_button.length > 0){ + next_page_button[0].addEventListener("click", save_data); + } + } + + // Only add previous page buttons when not on the first page + if (CURRENT_CONTINUATION) { + const prev_page_containers = document.getElementsByClassName("page-prev-container") + + for (let container of prev_page_containers) { + if (pagination_data.is_rtl) { + container.innerHTML = `<button class="pure-button pure-button-secondary">${pagination_data.prev_page} <i class="icon ion-ios-arrow-forward"></i></button>` + } else { + container.innerHTML = `<button class="pure-button pure-button-secondary"><i class="icon ion-ios-arrow-back"></i> ${pagination_data.prev_page}</button>` + } + container.getElementsByClassName("pure-button")[0].addEventListener("click", button_press); + } + } +}); diff --git a/assets/js/player.js b/assets/js/player.js index 353a5296..f32c9b56 100644 --- a/assets/js/player.js +++ b/assets/js/player.js @@ -134,26 +134,32 @@ player.on('timeupdate', function () { // YouTube links let elem_yt_watch = document.getElementById('link-yt-watch'); + if (elem_yt_watch) { + let base_url_yt_watch = elem_yt_watch.getAttribute('data-base-url'); + elem_yt_watch.href = addCurrentTimeToURL(base_url_yt_watch); + } + let elem_yt_embed = document.getElementById('link-yt-embed'); - - let base_url_yt_watch = elem_yt_watch.getAttribute('data-base-url'); - let base_url_yt_embed = elem_yt_embed.getAttribute('data-base-url'); - - elem_yt_watch.href = addCurrentTimeToURL(base_url_yt_watch); - elem_yt_embed.href = addCurrentTimeToURL(base_url_yt_embed); + if (elem_yt_embed) { + let base_url_yt_embed = elem_yt_embed.getAttribute('data-base-url'); + elem_yt_embed.href = addCurrentTimeToURL(base_url_yt_embed); + } // Invidious links let domain = window.location.origin; let elem_iv_embed = document.getElementById('link-iv-embed'); + if (elem_iv_embed) { + let base_url_iv_embed = elem_iv_embed.getAttribute('data-base-url'); + elem_iv_embed.href = addCurrentTimeToURL(base_url_iv_embed, domain); + } + let elem_iv_other = document.getElementById('link-iv-other'); - - let base_url_iv_embed = elem_iv_embed.getAttribute('data-base-url'); - let base_url_iv_other = elem_iv_other.getAttribute('data-base-url'); - - elem_iv_embed.href = addCurrentTimeToURL(base_url_iv_embed, domain); - elem_iv_other.href = addCurrentTimeToURL(base_url_iv_other, domain); + if (elem_iv_other) { + let base_url_iv_other = elem_iv_other.getAttribute('data-base-url'); + elem_iv_other.href = addCurrentTimeToURL(base_url_iv_other, domain); + } }); diff --git a/config/config.example.yml b/config/config.example.yml index bc2deda5..b04e0a30 100644 --- a/config/config.example.yml +++ b/config/config.example.yml @@ -130,6 +130,20 @@ https_only: false ## #hsts: true +## +## Path and permissions of a UNIX socket to listen on for incoming connections. +## +## Note: Enabling socket will make invidious stop listening on the address +## specified by 'host_binding' and 'port'. +## +## Accepted values: Any path to a new file (that doesn't exist yet) and its +## permissions following the UNIX octal convention. +## Default: <none> +## +#socket_binding: +# path: /tmp/invidious.sock +# permissions: 777 + # ----------------------------- # Network (outbound) diff --git a/docker/Dockerfile b/docker/Dockerfile index 900c9e74..a07bef28 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -21,7 +21,7 @@ COPY ./videojs-dependencies.yml ./videojs-dependencies.yml RUN crystal spec --warnings all \ --link-flags "-lxml2 -llzma" -RUN if [[ "${release}" == 1 ]] ; then \ +RUN --mount=type=cache,target=/root/.cache/crystal if [[ "${release}" == 1 ]] ; then \ crystal build ./src/invidious.cr \ --release \ --static --warnings all \ diff --git a/docker/Dockerfile.arm64 b/docker/Dockerfile.arm64 index ce9bab08..7fcb176e 100644 --- a/docker/Dockerfile.arm64 +++ b/docker/Dockerfile.arm64 @@ -22,7 +22,7 @@ COPY ./videojs-dependencies.yml ./videojs-dependencies.yml RUN crystal spec --warnings all \ --link-flags "-lxml2 -llzma" -RUN if [[ "${release}" == 1 ]] ; then \ +RUN --mount=type=cache,target=/root/.cache/crystal if [[ "${release}" == 1 ]] ; then \ crystal build ./src/invidious.cr \ --release \ --static --warnings all \ diff --git a/locales/ar.json b/locales/ar.json index b6bab59b..a8f5e62d 100644 --- a/locales/ar.json +++ b/locales/ar.json @@ -559,10 +559,12 @@ "toggle_theme": "تبديل الموضوع", "Add to playlist": "أضف إلى قائمة التشغيل", "Add to playlist: ": "أضف إلى قائمة التشغيل: ", - "Answer": "الرد", + "Answer": "اجابة", "Search for videos": "ابحث عن مقاطع الفيديو", "The Popular feed has been disabled by the administrator.": "تم تعطيل الخلاصة الشائعة من قبل المسؤول.", "carousel_slide": "الشريحة {{current}} من {{total}}", "carousel_skip": "تخطي الكاروسيل", - "carousel_go_to": "انتقل إلى الشريحة `x`" + "carousel_go_to": "انتقل إلى الشريحة `x`", + "preferences_preload_label": "التحميل المسبق لبيانات الفيديو: ", + "Filipino (auto-generated)": "الفلبينية (المولدة تلقائيًا)" } diff --git a/locales/cs.json b/locales/cs.json index 6e66178d..d28f2098 100644 --- a/locales/cs.json +++ b/locales/cs.json @@ -137,7 +137,7 @@ "Family friendly? ": "Vhodné pro rodiny? ", "Engagement: ": "Zapojení: ", "English": "Angličtina", - "English (auto-generated)": "Angličtina (automaticky generováno)", + "English (auto-generated)": "Angličtina (vytvořeno automaticky)", "Afrikaans": "Afrikánština", "Albanian": "Albánština", "Amharic": "Amharština", @@ -294,8 +294,8 @@ "Chinese (China)": "Čínština (Čína)", "Chinese (Hong Kong)": "Čínština (Hong Kong)", "Chinese (Taiwan)": "Čínština (Taiwan)", - "Portuguese (auto-generated)": "Portugalština (automaticky generováno)", - "Spanish (auto-generated)": "Španělština (automaticky generováno)", + "Portuguese (auto-generated)": "Portugalština (vytvořeno automaticky)", + "Spanish (auto-generated)": "Španělština (vytvořeno automaticky)", "Spanish (Mexico)": "Španělština (Mexiko)", "Spanish (Spain)": "Španělština (Španělsko)", "generic_count_years_0": "{{count}} rokem", @@ -352,13 +352,13 @@ "comments_points_count_0": "{{count}} bod", "comments_points_count_1": "{{count}} body", "comments_points_count_2": "{{count}} bodů", - "German (auto-generated)": "Němčina (automaticky generováno)", - "Indonesian (auto-generated)": "Indonéština (automaticky generováno)", + "German (auto-generated)": "Němčina (vytvořeno automaticky)", + "Indonesian (auto-generated)": "Indonéština (vytvořeno automaticky)", "Interlingue": "Interlingue", - "Italian (auto-generated)": "Italština (automaticky generováno)", - "Japanese (auto-generated)": "Japonština (automaticky generováno)", - "Korean (auto-generated)": "Korejština (automaticky generováno)", - "Russian (auto-generated)": "Ruština (automaticky generováno)", + "Italian (auto-generated)": "Italština (vytvořeno automaticky)", + "Japanese (auto-generated)": "Japonština (vytvořeno automaticky)", + "Korean (auto-generated)": "Korejština (vytvořeno automaticky)", + "Russian (auto-generated)": "Ruština (vytvořeno automaticky)", "generic_count_months_0": "{{count}} měsícem", "generic_count_months_1": "{{count}} měsíci", "generic_count_months_2": "{{count}} měsíci", @@ -371,7 +371,7 @@ "footer_documentation": "Dokumentace", "next_steps_error_message_refresh": "Obnovit stránku", "Chinese": "Čínština", - "Dutch (auto-generated)": "Nizozemština (automaticky generováno)", + "Dutch (auto-generated)": "Nizozemština (vytvořeno automaticky)", "Erroneous token": "Chybný token", "tokens_count_0": "{{count}} token", "tokens_count_1": "{{count}} tokeny", @@ -380,9 +380,9 @@ "Token is expired, please try again": "Token vypršel, zkuste to prosím znovu", "English (United States)": "Angličtina (Spojené státy)", "Cantonese (Hong Kong)": "Kantonština (Hong Kong)", - "French (auto-generated)": "Francouzština (automaticky generováno)", - "Turkish (auto-generated)": "Turečtina (automaticky generováno)", - "Vietnamese (auto-generated)": "Vietnamština (automaticky generováno)", + "French (auto-generated)": "Francouzština (vytvořeno automaticky)", + "Turkish (auto-generated)": "Turečtina (vytvořeno automaticky)", + "Vietnamese (auto-generated)": "Vietnamština (vytvořeno automaticky)", "Current version: ": "Aktuální verze: ", "next_steps_error_message": "Měli byste zkusit: ", "footer_donate_page": "Přispět", @@ -513,5 +513,7 @@ "The Popular feed has been disabled by the administrator.": "Kategorie Populární byla zakázána administrátorem.", "carousel_slide": "Snímek {{current}} z {{total}}", "carousel_skip": "Přeskočit galerii", - "carousel_go_to": "Přejít na snímek `x`" + "carousel_go_to": "Přejít na snímek `x`", + "preferences_preload_label": "Předem načíst data videa: ", + "Filipino (auto-generated)": "Filipínština (vytvořeno automaticky)" } diff --git a/locales/de.json b/locales/de.json index 151f2abe..ce6fde8b 100644 --- a/locales/de.json +++ b/locales/de.json @@ -11,6 +11,7 @@ "last": "neueste", "Next page": "Nächste Seite", "Previous page": "Vorherige Seite", + "First page": "Erste Seite", "Clear watch history?": "Verlauf löschen?", "New password": "Neues Passwort", "New passwords must match": "Neue Passwörter müssen übereinstimmen", @@ -490,12 +491,13 @@ "generic_channels_count_plural": "{{count}} Kanäle", "Import YouTube watch history (.json)": "YouTube Wiedergabeverlauf importieren (.json)", "Answer": "Antwort", - "The Popular feed has been disabled by the administrator.": "Der Angesagt-Feed wurde vom Administrator deaktiviert.", + "The Popular feed has been disabled by the administrator.": "Der Feed für beliebte Inhalte wurde vom Administrator deaktiviert.", "Add to playlist": "Einer Wiedergabeliste hinzufügen", "Search for videos": "Nach Videos suchen", "toggle_theme": "Thema wechseln", "Add to playlist: ": "Einer Wiedergabeliste hinzufügen: ", - "carousel_go_to": "Zu Folie `x` gehen", - "carousel_slide": "Folie {{current}} von {{total}}", - "carousel_skip": "Karussell überspringen" + "carousel_go_to": "Zu Element `x` springen", + "carousel_slide": "Seite {{current}} von {{total}}", + "carousel_skip": "Galerie überspringen", + "Filipino (auto-generated)": "Philippinisch (automatisch generiert)" } diff --git a/locales/el.json b/locales/el.json index 38550458..32efaaf8 100644 --- a/locales/el.json +++ b/locales/el.json @@ -21,7 +21,7 @@ "Import and Export Data": "Εισαγωγή και Εξαγωγή Δεδομένων", "Import": "Εισαγωγή", "Import Invidious data": "Εsαγωγή δεδομένων Invidious JSON", - "Import YouTube subscriptions": "Εισαγωγή συνδρομών YouTube/OPML", + "Import YouTube subscriptions": "Εισαγωγή συνδρομών YouTube απο CVS/OPML", "Import FreeTube subscriptions (.db)": "Εισαγωγή συνδρομών FreeTube (.db)", "Import NewPipe subscriptions (.json)": "Εισαγωγή συνδρομών NewPipe (.json)", "Import NewPipe data (.zip)": "Εισαγωγή δεδομένων NewPipe (.zip)", @@ -455,7 +455,7 @@ "channel_tab_streams_label": "Ζωντανή μετάδοση", "playlist_button_add_items": "Προσθήκη βίντεο", "Artist: ": "Καλλιτέχνης: ", - "search_message_use_another_instance": " Μπορείτε επίσης <a href=\"`x`\">να αναζητήσετε σε άλλο instance</a>.", + "search_message_use_another_instance": "Μπορείτε επίσης <a href=\"`x`\">να αναζητήσετε σε άλλο instance</a>.", "generic_button_save": "Αποθήκευση", "generic_button_cancel": "Ακύρωση", "subscriptions_unseen_notifs_count": "{{count}} μη αναγνωσμένη ειδοποίηση", @@ -490,9 +490,13 @@ "Search for videos": "Αναζήτηση βίντεο", "The Popular feed has been disabled by the administrator.": "Η δημοφιλής ροή έχει απενεργοποιηθεί από τον διαχειριστή.", "Answer": "Απάντηση", - "Add to playlist": "Λίιστα αναπαραγωγής", - "Add to playlist: ": "Λίστα αναπαραγωγής: ", + "Add to playlist": "Προσθήκη στην λίιστα αναπαραγωγής", + "Add to playlist: ": "Προσθήκη στην λίστα αναπαραγωγής : ", "carousel_slide": "Εικόνα {{current}}απο {{total}}", "carousel_go_to": "Πήγαινε στην εικόνα`x`", - "toggle_theme": "Αλλαγή θέματος" + "toggle_theme": "Αλλαγή θέματος", + "Import YouTube watch history (.json)": "Εισαγωγή ιστορικού προβολής YouTube (.json)", + "Filipino (auto-generated)": "Φιλιππινέζικα (αυτόματη παραγωγή)", + "preferences_preload_label": "Προφόρτιση δεδομένων βίντεο: ", + "carousel_skip": "Αποφυγή εμφάνισης εικόνων" } diff --git a/locales/en-US.json b/locales/en-US.json index 5f99c1ef..4f2c2770 100644 --- a/locales/en-US.json +++ b/locales/en-US.json @@ -33,6 +33,7 @@ "last": "last", "Next page": "Next page", "Previous page": "Previous page", + "First page": "First page", "Clear watch history?": "Clear watch history?", "New password": "New password", "New passwords must match": "New passwords must match", @@ -492,6 +493,7 @@ "channel_tab_streams_label": "Livestreams", "channel_tab_podcasts_label": "Podcasts", "channel_tab_releases_label": "Releases", + "channel_tab_courses_label": "Courses", "channel_tab_playlists_label": "Playlists", "channel_tab_community_label": "Community", "channel_tab_posts_label": "Posts", diff --git a/locales/es.json b/locales/es.json index fda29198..ad65e07d 100644 --- a/locales/es.json +++ b/locales/es.json @@ -513,5 +513,7 @@ "The Popular feed has been disabled by the administrator.": "El feed Popular ha sido desactivado por el administrador.", "carousel_slide": "Diapositiva {{current}} de {{total}}", "carousel_skip": "Saltar el carrusel", - "carousel_go_to": "Ir a la diapositiva `x`" + "carousel_go_to": "Ir a la diapositiva `x`", + "preferences_preload_label": "Precargar datos del vídeo: ", + "Filipino (auto-generated)": "Filipino (generado automáticamente)" } diff --git a/locales/fa.json b/locales/fa.json index b146385e..2326370d 100644 --- a/locales/fa.json +++ b/locales/fa.json @@ -496,5 +496,6 @@ "crash_page_search_issue": "دنبال <a href=\"`x`\"> گشتیم بین مشکلات در گیت هاب </a>", "crash_page_report_issue": "اگر هیچ یک از روش های بالا کمکی نکردند لطفا <a href=\"`x`\"> (ترجیحا به انگلیسی) یک سوال جدید در گیت هاب بپرسید و </a> طوری که سوالتون شامل متن زیر باشه:", "channel_tab_releases_label": "آثار", - "toggle_theme": "تغییر وضعیت تم" + "toggle_theme": "تغییر وضعیت تم", + "preferences_preload_label": "پیش بار کردن دادههای ویدیو: " } diff --git a/locales/fi.json b/locales/fi.json index b0df1e46..13fef6de 100644 --- a/locales/fi.json +++ b/locales/fi.json @@ -460,7 +460,7 @@ "search_filters_apply_button": "Ota valitut suodattimet käyttöön", "search_filters_date_label": "Latausaika", "search_filters_duration_option_medium": "Keskipituinen (4 - 20 minuuttia)", - "search_message_use_another_instance": " Voit myös <a href=\"`x`\">hakea toisella instanssilla</a>.", + "search_message_use_another_instance": "Voit myös <a href=\"`x`\">hakea toisella instanssilla</a>.", "search_filters_date_option_none": "Milloin tahansa", "search_filters_type_option_all": "Mikä tahansa tyyppi", "Popular enabled: ": "Suosittu käytössä: ", @@ -496,5 +496,6 @@ "generic_channels_count_plural": "{{count}} kanavaa", "The Popular feed has been disabled by the administrator.": "Järjestelmänvalvoja on poistanut Suositut-syötteen.", "Import YouTube watch history (.json)": "Tuo Youtube-katseluhistoria (.json)", - "toggle_theme": "Vaihda teemaa" + "toggle_theme": "Vaihda teemaa", + "preferences_preload_label": "Esilataa video data. " } diff --git a/locales/fr.json b/locales/fr.json index 6147a159..800c7aaf 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -505,7 +505,7 @@ "channel_tab_releases_label": "Parutions", "channel_tab_podcasts_label": "Émissions audio", "Import YouTube watch history (.json)": "Importer l'historique de visionnement YouTube (.json)", - "Add to playlist: ": "Ajouter à la playlist : ", + "Add to playlist: ": "Ajouter à la playlist : ", "Add to playlist": "Ajouter à la playlist", "Answer": "Répondre", "Search for videos": "Rechercher des vidéos", @@ -513,5 +513,7 @@ "carousel_skip": "Passez le carrousel", "carousel_slide": "Diapositive {{current}} sur {{total}}", "carousel_go_to": "Aller à la diapositive `x`", - "toggle_theme": "Changer le Thème" + "toggle_theme": "Changer le Thème", + "Filipino (auto-generated)": "Philippines (automatiquement générer)", + "preferences_preload_label": "Précharger les données de la vidéo : " } diff --git a/locales/hr.json b/locales/hr.json index 7b76a41f..6adbcdc3 100644 --- a/locales/hr.json +++ b/locales/hr.json @@ -513,5 +513,7 @@ "toggle_theme": "Uklj./Isklj. temu", "carousel_slide": "Kadar {{current}} od {{total}}", "carousel_go_to": "Idi na kadar `x`", - "carousel_skip": "Preskoči vrtuljak" + "carousel_skip": "Preskoči vrtuljak", + "Filipino (auto-generated)": "Filipinski (automatski generirano)", + "preferences_preload_label": "Unaprijed učitaj podatke videa: " } diff --git a/locales/is.json b/locales/is.json index 9d13c5cf..d94357f1 100644 --- a/locales/is.json +++ b/locales/is.json @@ -496,5 +496,7 @@ "footer_documentation": "Leiðbeiningar", "channel_tab_channels_label": "Rásir", "Import YouTube playlist (.csv)": "Flytja inn YouTube spilunarlista (.csv)", - "preferences_quality_option_dash": "DASH (aðlaganleg gæði)" + "preferences_quality_option_dash": "DASH (aðlaganleg gæði)", + "preferences_preload_label": "Forhlaða gögnum myndskeiðs: ", + "Filipino (auto-generated)": "Filippínska (sjálfvirkt útbúin)" } diff --git a/locales/it.json b/locales/it.json index 309adb13..3f008ccd 100644 --- a/locales/it.json +++ b/locales/it.json @@ -469,8 +469,8 @@ "Spanish (auto-generated)": "Spagnolo (generati automaticamente)", "Spanish (Mexico)": "Spagnolo (Messico)", "Spanish (Spain)": "Spagnolo (Spagna)", - "Turkish (auto-generated)": "Turco (auto-generato)", - "Vietnamese (auto-generated)": "Vietnamita (auto-generato)", + "Turkish (auto-generated)": "Turco (generati automaticamente)", + "Vietnamese (auto-generated)": "Vietnamita (generati automaticamente)", "search_filters_date_label": "Data caricamento", "search_filters_date_option_none": "Qualunque data", "search_filters_type_option_all": "Qualunque tipo", @@ -513,5 +513,7 @@ "The Popular feed has been disabled by the administrator.": "La sezione dei contenuti popolari è stata disabilitata dall'amministratore.", "carousel_slide": "Fotogramma {{current}} di {{total}}", "carousel_skip": "Salta la galleria", - "carousel_go_to": "Vai al fotogramma `x`" + "carousel_go_to": "Vai al fotogramma `x`", + "preferences_preload_label": "Precarica dati video: ", + "Filipino (auto-generated)": "Filippino (generati automaticamente)" } diff --git a/locales/ja.json b/locales/ja.json index 7fc9d604..5e90148d 100644 --- a/locales/ja.json +++ b/locales/ja.json @@ -479,5 +479,7 @@ "carousel_go_to": "スライド`x`を表示", "carousel_slide": "スライド{{current}} / 全{{total}}個中", "carousel_skip": "画像のスライド表示をスキップ", - "toggle_theme": "テーマの切り替え" + "toggle_theme": "テーマの切り替え", + "preferences_preload_label": "動画データを事前に読み込む: ", + "Filipino (auto-generated)": "フィリピノ語 (自動生成)" } diff --git a/locales/ko.json b/locales/ko.json index 4864860a..c2d3f6e2 100644 --- a/locales/ko.json +++ b/locales/ko.json @@ -70,7 +70,7 @@ "Next page": "다음 페이지", "last": "마지막", "Shared `x` ago": "`x` 전", - "popular": "인기", + "popular": "인기순", "oldest": "과거순", "newest": "최신순", "View playlist on YouTube": "유튜브에서 재생목록 보기", @@ -479,5 +479,6 @@ "carousel_go_to": "`x` 슬라이드로 이동", "Search for videos": "비디오 검색", "toggle_theme": "테마 전환", - "carousel_slide": "{{total}}의 슬라이드 {{current}}" + "carousel_slide": "{{total}}의 슬라이드 {{current}}", + "preferences_preload_label": "비디오 데이터 사전 로드: " } diff --git a/locales/nb-NO.json b/locales/nb-NO.json index 17d64baf..38402fed 100644 --- a/locales/nb-NO.json +++ b/locales/nb-NO.json @@ -496,5 +496,6 @@ "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" + "toggle_theme": "Endre utseende", + "preferences_preload_label": "Last videodata på forhånd: " } diff --git a/locales/nl.json b/locales/nl.json index f10b3593..a908e26a 100644 --- a/locales/nl.json +++ b/locales/nl.json @@ -496,5 +496,7 @@ "Answer": "Antwoorden", "Search for videos": "Naar video's zoeken", "carousel_skip": "Carousel overslaan", - "toggle_theme": "Thema omschakelen" + "toggle_theme": "Thema omschakelen", + "preferences_preload_label": "Videogegevens vooraf laden: ", + "Filipino (auto-generated)": "Filipijns (automatisch gegenereerd)" } diff --git a/locales/pl.json b/locales/pl.json index 73d65647..b119ab22 100644 --- a/locales/pl.json +++ b/locales/pl.json @@ -513,5 +513,7 @@ "Add to playlist: ": "Dodaj do playlisty: ", "carousel_slide": "Slajd {{current}} z {{total}}", "carousel_skip": "Pomiń karuzelę", - "carousel_go_to": "Przejdź do slajdu `x`" + "carousel_go_to": "Przejdź do slajdu `x`", + "preferences_preload_label": "Wstępne ładowanie danych wideo: ", + "Filipino (auto-generated)": "filipiński (wygenerowany automatycznie)" } diff --git a/locales/pt-BR.json b/locales/pt-BR.json index 1d29d2fe..3d653caf 100644 --- a/locales/pt-BR.json +++ b/locales/pt-BR.json @@ -513,5 +513,7 @@ "Answer": "Resposta", "carousel_slide": "Slide {{current}} de {{total}}", "carousel_skip": "Ignorar carrossel", - "carousel_go_to": "Ir ao slide `x`" + "carousel_go_to": "Ir ao slide `x`", + "preferences_preload_label": "Pré-carregar dados do vídeo: ", + "Filipino (auto-generated)": "Filipino (gerado automaticamente)" } diff --git a/locales/pt.json b/locales/pt.json index 0bb1be66..9c8562f2 100644 --- a/locales/pt.json +++ b/locales/pt.json @@ -513,5 +513,7 @@ "carousel_slide": "Diapositivo {{current}} de{{total}}", "carousel_skip": "Ignorar carrossel", "carousel_go_to": "Ir para o diapositivo`x`", - "The Popular feed has been disabled by the administrator.": "O feed Popular foi desativado por um administrador." + "The Popular feed has been disabled by the administrator.": "O feed Popular foi desativado por um administrador.", + "preferences_preload_label": "Pré-carregamento dos dados: ", + "Filipino (auto-generated)": "Filipino (gerado automaticamente)" } diff --git a/locales/ru.json b/locales/ru.json index 80c98de8..b7dc91cf 100644 --- a/locales/ru.json +++ b/locales/ru.json @@ -11,6 +11,7 @@ "last": "последние", "Next page": "Следующая страница", "Previous page": "Предыдущая страница", + "First page": "Первая страница", "Clear watch history?": "Очистить историю просмотров?", "New password": "Новый пароль", "New passwords must match": "Новые пароли не совпадают", @@ -48,8 +49,8 @@ "preferences_category_player": "Настройки проигрывателя", "preferences_video_loop_label": "Всегда повторять: ", "preferences_autoplay_label": "Автовоспроизведение: ", - "preferences_continue_label": "Переходить к следующему видео? ", - "preferences_continue_autoplay_label": "Автопроигрывание следующего видео: ", + "preferences_continue_label": "Воспроизводить следующее видео: ", + "preferences_continue_autoplay_label": "Автовоспроизведение следующего видео: ", "preferences_listen_label": "Режим «только аудио» по умолчанию: ", "preferences_local_label": "Проигрывать видео через прокси? ", "preferences_speed_label": "Скорость видео по умолчанию: ", @@ -513,5 +514,6 @@ "toggle_theme": "Переключатель тем", "carousel_slide": "Пролистано {{current}} из {{total}}", "carousel_skip": "Пропустить всё", - "carousel_go_to": "Перейти к странице `x`" + "carousel_go_to": "Перейти к странице `x`", + "preferences_preload_label": "Предзагрузка видеоданных: " } diff --git a/locales/sl.json b/locales/sl.json index 3803d09c..c36ad522 100644 --- a/locales/sl.json +++ b/locales/sl.json @@ -13,7 +13,7 @@ "Import and Export Data": "Uvoz in izvoz podatkov", "Import": "Uvozi", "Import Invidious data": "Uvozi Invidious JSON podatke", - "Import YouTube subscriptions": "Uvozi YouTube/OPML naročnine", + "Import YouTube subscriptions": "Uvozi YouTube CSV ali OPML naročnine", "Import FreeTube subscriptions (.db)": "Uvozi FreeTube (.db) naročnine", "Import NewPipe data (.zip)": "Uvozi NewPipe (.zip) podatke", "Export": "Izvozi", @@ -105,7 +105,7 @@ "Show more": "Pokaži več", "Switch Invidious Instance": "Preklopi Invidious instanco", "search_message_change_filters_or_query": "Poskusi razširiti iskalno poizvedbo in/ali spremeniti filtre.", - "search_message_use_another_instance": " Lahko tudi <a href=\"`x`\">iščeš v drugi istanci</a>.", + "search_message_use_another_instance": "Lahko tudi <a href=\"`x`\">iščeš v drugi istanci</a>.", "Wilson score: ": "Wilsonov rezultat: ", "Engagement: ": "Sodelovanje: ", "Blacklisted regions: ": "Regije na seznamu nedovoljenih: ", @@ -462,7 +462,7 @@ "search_filters_features_option_four_k": "4K", "search_filters_features_option_hdr": "HDR", "next_steps_error_message_refresh": "Osveži", - "search_filters_date_option_hour": "Zadnja ura", + "search_filters_date_option_hour": "V zadnji uri", "search_filters_features_option_purchased": "Kupljeno", "search_filters_sort_label": "Razvrsti po", "search_filters_sort_option_views": "številu ogledov", @@ -521,5 +521,16 @@ "generic_channels_count_1": "{{count}} kanala", "generic_channels_count_2": "{{count}} kanali", "generic_channels_count_3": "{{count}} kanalov", - "Import YouTube watch history (.json)": "Uvozi zgodovino gledanja YouTube (.json)" + "Import YouTube watch history (.json)": "Uvozi zgodovino gledanja YouTube (.json)", + "Add to playlist": "Dodaj na seznam predvajanja", + "Add to playlist: ": "Dodaj na seznam predvajanja: ", + "Search for videos": "Iskanje videoposnetkov", + "The Popular feed has been disabled by the administrator.": "Administrator je onemogočil priljubljeni vir.", + "Answer": "Odgovor", + "Filipino (auto-generated)": "filipinščina (samodejno ustvarjeno)", + "toggle_theme": "Preklopi temo", + "carousel_slide": "Diapozitiv {{current}} od {{total}}", + "carousel_skip": "Preskoči galerijo", + "carousel_go_to": "Pojdi na diapozitiv `x`", + "preferences_preload_label": "Predhodno naloži video podatke: " } diff --git a/locales/sq.json b/locales/sq.json index ea20ce56..2a404828 100644 --- a/locales/sq.json +++ b/locales/sq.json @@ -492,5 +492,7 @@ "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`" + "carousel_go_to": "Kalo te diapozitivi `x`", + "Filipino (auto-generated)": "Filipineze (të prodhuara automatikisht)", + "preferences_preload_label": "Parangarko të dhëna videoje: " } diff --git a/locales/sr.json b/locales/sr.json index d28b2459..1d54972c 100644 --- a/locales/sr.json +++ b/locales/sr.json @@ -513,5 +513,7 @@ "Answer": "Odgovor", "Search for videos": "Pretražite video snimke", "carousel_skip": "Preskoči karusel", - "toggle_theme": "Подеси тему" + "toggle_theme": "Подеси тему", + "preferences_preload_label": "Unapred učitaj podatke o video snimku: ", + "Filipino (auto-generated)": "Filipinski (automatski generisano)" } diff --git a/locales/sr_Cyrl.json b/locales/sr_Cyrl.json index 483e7fc4..e5279c8a 100644 --- a/locales/sr_Cyrl.json +++ b/locales/sr_Cyrl.json @@ -513,5 +513,7 @@ "Add to playlist: ": "Додајте на плејлисту: ", "carousel_skip": "Прескочи карусел", "The Popular feed has been disabled by the administrator.": "Администратор је онемогућио фид „Популарно“.", - "carousel_slide": "Слајд {{current}} од {{total}}" + "carousel_slide": "Слајд {{current}} од {{total}}", + "preferences_preload_label": "Унапред учитај податке о видео снимку: ", + "Filipino (auto-generated)": "Филипински (аутоматски генерисано)" } diff --git a/locales/sv-SE.json b/locales/sv-SE.json index f1313a4d..614132e0 100644 --- a/locales/sv-SE.json +++ b/locales/sv-SE.json @@ -496,5 +496,7 @@ "The Popular feed has been disabled by the administrator.": "Det populära flödet har inaktiverats av administratören.", "carousel_slide": "Bildspel {{current}} av {{total}}", "carousel_skip": "Hoppa över karusellen", - "carousel_go_to": "Gå till bildspel `x`" + "carousel_go_to": "Gå till bildspel `x`", + "preferences_preload_label": "Förladda video data: ", + "Filipino (auto-generated)": "Filippinska (auto-genererad)" } diff --git a/locales/ta.json b/locales/ta.json new file mode 100644 index 00000000..89e58668 --- /dev/null +++ b/locales/ta.json @@ -0,0 +1,502 @@ +{ + "Add to playlist": "பிளேலிச்ட்டில் சேர்க்கவும்", + "generic_channels_count": "{{count}} சேனல்", + "generic_channels_count_plural": "{{count}} சேனல்கள்", + "generic_views_count": "{{count}} பார்வை", + "generic_views_count_plural": "{{count}} காட்சிகள்", + "generic_videos_count": "{{count}} வீடியோ", + "generic_videos_count_plural": "{{count}} வீடியோக்கள்", + "generic_playlists_count": "{{count}} பிளேலிச்ட்", + "generic_playlists_count_plural": "{{count}} பிளேலிச்ட்கள்", + "generic_subscribers_count": "{{count}} சந்தாதாரர்", + "generic_subscribers_count_plural": "{{count}} சந்தாதாரர்கள்", + "generic_button_delete": "நீக்கு", + "generic_button_rss": "ஆர்.எச்.எச்", + "LIVE": "வாழ", + "Shared `x` ago": "`X` முன்பு பகிரப்பட்டது", + "Unsubscribe": "குழுவிலகவும்", + "View playlist on YouTube": "யூடியூப்பில் பிளேலிச்ட்டைக் காண்க", + "newest": "புதியது", + "oldest": "பழமையானது", + "popular": "மக்கள்", + "last": "கடைசி", + "Next page": "அடுத்த பக்கம்", + "Previous page": "முந்தைய பக்கம்", + "Clear watch history?": "தெளிவான கண்காணிப்பு வரலாறு?", + "New password": "புதிய கடவுச்சொல்", + "New passwords must match": "புதிய கடவுச்சொற்கள் பொருந்த வேண்டும்", + "Authorize token?": "கிள்ளாக்கை அங்கீகரிக்கவா?", + "Yes": "ஆம்", + "Import YouTube playlist (.csv)": "யூடியூப் பிளேலிச்ட்டை இறக்குமதி செய்க (.csv)", + "Import YouTube watch history (.json)": "YouTube வாட்ச் வரலாற்றை இறக்குமதி செய்க (.json)", + "Import Invidious data": "வன்கவர்வு சாதொபொகு தரவை இறக்குமதி செய்க", + "Import YouTube subscriptions": "YouTube காபிம அல்லது OPML சந்தாக்களை இறக்குமதி செய்க", + "Import FreeTube subscriptions (.db)": "ஃப்ரீட்யூப் சந்தாக்களை இறக்குமதி செய்க (.db)", + "Import NewPipe data (.zip)": "நியூபைப் தரவை இறக்குமதி செய்க (.zip)", + "Export subscriptions as OPML (for NewPipe & FreeTube)": "OPML ஆக சந்தாக்களை ஏற்றுமதி செய்யுங்கள் (நியூபைப் & ஃப்ரீட்யூப்பிற்கு)", + "Export subscriptions as OPML": "OPML ஆக சந்தாக்களை ஏற்றுமதி செய்யுங்கள்", + "Export data as JSON": "சாதொபொகு ஆக வன்கவர்வு தரவை ஏற்றுமதி செய்யுங்கள்", + "Delete account?": "கணக்கை நீக்கவா?", + "History": "வரலாறு", + "JavaScript license information": "சாவாச்கிரிப்ட் உரிம செய்தி", + "source": "மூலம்", + "An alternative front-end to YouTube": "YouTube க்கு ஒரு மாற்று முன் இறுதியில்", + "Log in": "புகுபதிகை", + "Log in/register": "உள்நுழைக/பதிவு செய்யுங்கள்", + "User ID": "பயனர் ஐடி", + "Password": "கடவுச்சொல்", + "Time (h:mm:ss):": "நேரம் (h: மிமீ: எச்எச்):", + "Sign In": "விடுபதிகை", + "Register": "பதிவு செய்யுங்கள்", + "E-mail": "மின்னஞ்சல்", + "Preferences": "விருப்பத்தேர்வுகள்", + "preferences_preload_label": "வீடியோ தரவை முன்பே ஏற்றவும்: ", + "preferences_autoplay_label": "தன்னியக்க: ", + "preferences_continue_label": "இயல்பாக அடுத்து விளையாடுங்கள்: ", + "preferences_local_label": "பதிலாள் வீடியோக்கள்: ", + "preferences_watch_history_label": "கண்காணிப்பு வரலாற்றை இயக்கு: ", + "preferences_speed_label": "இயல்புநிலை வேகம்: ", + "preferences_quality_label": "விருப்பமான வீடியோ தரம்: ", + "preferences_quality_dash_label": "விருப்பமான கோடு வீடியோ தரம்: ", + "preferences_quality_dash_option_auto": "தானி", + "preferences_quality_dash_option_best": "சிறந்த", + "preferences_quality_dash_option_worst": "மோசமான", + "preferences_quality_dash_option_4320p": "4320 ப", + "preferences_quality_dash_option_1080p": "1080 ப", + "preferences_quality_dash_option_720p": "720 ஆ", + "preferences_quality_dash_option_480p": "480 ப", + "preferences_quality_dash_option_360p": "360 ப", + "preferences_quality_dash_option_144p": "144 ப", + "preferences_volume_label": "பிளேயர் தொகுதி: ", + "preferences_comments_label": "இயல்புநிலை கருத்துகள்: ", + "Fallback captions: ": "குறைவடையும் தலைப்புகள்: ", + "preferences_captions_label": "இயல்புநிலை தலைப்புகள்: ", + "preferences_related_videos_label": "தொடர்புடைய வீடியோக்களைக் காட்டு: ", + "preferences_annotations_label": "முன்னிருப்பாக சிறுகுறிப்புகளைக் காட்டு: ", + "preferences_vr_mode_label": "ஊடாடும் 360 டிகிரி வீடியோக்கள் (வெப்சிஎல் தேவை): ", + "preferences_category_visual": "காட்சி விருப்பத்தேர்வுகள்", + "light": "ஒளி", + "preferences_thin_mode_label": "மெல்லிய பயன்முறை: ", + "preferences_category_misc": "இதர விருப்பத்தேர்வுகள்", + "preferences_category_subscription": "சந்தா விருப்பத்தேர்வுகள்", + "preferences_annotations_subscribed_label": "சந்தா சேனல்களுக்கு முன்னிருப்பாக சிறுகுறிப்புகளைக் காட்டவா? ", + "Redirect homepage to feed: ": "உணவளிக்க முகப்புப்பக்கத்தை திருப்பி விடுங்கள்: ", + "preferences_sort_label": "வீடியோக்களை வரிசைப்படுத்துங்கள்: ", + "published": "வெளியிடப்பட்டது", + "published - reverse": "வெளியிடப்பட்டது - தலைகீழ்", + "alphabetically": "அகரவரிசை", + "preferences_unseen_only_label": "கவனக்குறைவாக மட்டுமே காட்டுங்கள்: ", + "preferences_notifications_only_label": "அறிவிப்புகளைக் காட்டுங்கள் (ஏதேனும் இருந்தால்): ", + "Enable web notifications": "வலை அறிவிப்புகளை இயக்கவும்", + "`x` is live": "`x` நேரலையில்", + "preferences_category_data": "தரவு விருப்பத்தேர்வுகள்", + "Manage subscriptions": "சந்தாக்களை நிர்வகிக்கவும்", + "Watch history": "வரலாற்றைப் பாருங்கள்", + "Delete account": "கணக்கை நீக்கு", + "preferences_category_admin": "நிர்வாகி விருப்பத்தேர்வுகள்", + "preferences_default_home_label": "இயல்புநிலை முகப்புப்பக்கம்: ", + "preferences_feed_menu_label": "ஊட்ட மெனு: ", + "preferences_show_nick_label": "மேலே புனைப்பெயரைக் காட்டு: ", + "Top enabled: ": "மேலே இயக்கப்பட்டது: ", + "CAPTCHA enabled: ": "கேப்ட்சா இயக்கப்பட்டது: ", + "Login enabled: ": "உள்நுழைவு இயக்கப்பட்டது: ", + "Registration enabled: ": "பதிவு இயக்கப்பட்டது: ", + "Report statistics: ": "அறிக்கை புள்ளிவிவரங்கள்: ", + "Save preferences": "விருப்பங்களை சேமிக்கவும்", + "Subscription manager": "சந்தா மேலாளர்", + "Token manager": "கிள்ளாக்கு மேலாளர்", + "Token": "கிள்ளாக்கு", + "search": "தேடல்", + "Released under the AGPLv3 on Github.": "கிட்அப்பில் AgPlv3 இன் கீழ் வெளியிடப்பட்டது.", + "View JavaScript license information.": "சாவாச்கிரிப்ட் உரிமத் தகவலைக் காண்க.", + "View privacy policy.": "தனியுரிமைக் கொள்கையைக் காண்க.", + "Trending": "டிரெண்டிங்", + "Public": "பொது", + "Unlisted": "பட்டியலிடப்படாதது", + "Private": "தனிப்பட்ட", + "View all playlists": "அனைத்து பிளேலிச்ட்களையும் காண்க", + "Updated `x` ago": "`X` முன்பு புதுப்பிக்கப்பட்டது", + "Delete playlist `x`?": "பிளேலிச்ட்டை நீக்கவா?", + "Playlist privacy": "பிளேலிச்ட் தனியுரிமை", + "Watch on YouTube": "YouTube இல் பாருங்கள்", + "Hide annotations": "சிறுகுறிப்புகளை மறைக்கவும்", + "Show replies": "பதில்களைக் காட்டு", + "Incorrect password": "தவறான கடவுச்சொல்", + "Wrong answer": "தவறான பதில்", + "Erroneous CAPTCHA": "தவறான கேப்ட்சா", + "CAPTCHA is a required field": "கேப்ட்சா ஒரு தேவையான புலம்", + "User ID is a required field": "பயனர் ஐடி தேவையான புலம்", + "Password is a required field": "கடவுச்சொல் தேவையான புலம்", + "Password cannot be empty": "கடவுச்சொல் காலியாக இருக்க முடியாது", + "Please log in": "தயவுசெய்து உள்நுழைக", + "This channel does not exist.": "இந்த சேனல் இல்லை.", + "Could not get channel info.": "சேனல் தகவலைப் பெற முடியவில்லை.", + "Could not fetch comments": "கருத்துகளைப் பெற முடியவில்லை", + "comments_points_count": "{{count}} புள்ளி", + "comments_points_count_plural": "{{count}} புள்ளிகள்", + "Could not create mix.": "கலவையை உருவாக்க முடியவில்லை.", + "Empty playlist": "வெற்று பிளேலிச்ட்", + "Not a playlist.": "ஒரு பிளேலிச்ட் அல்ல.", + "Playlist does not exist.": "பிளேலிச்ட் இல்லை.", + "Could not pull trending pages.": "பிரபலமான பக்கங்களை இழுக்க முடியவில்லை.", + "Erroneous challenge": "தவறான அறைகூவல்", + "Erroneous token": "தவறான கிள்ளாக்கு", + "No such user": "அத்தகைய பயனர் இல்லை", + "Token is expired, please try again": "கிள்ளாக்கு காலாவதியானது, தயவுசெய்து மீண்டும் முயற்சிக்கவும்", + "English": "ஆங்கிலம்", + "English (United States)": "ஆங்கிலம் (ஐக்கிய அமெரிக்க)", + "English (United Kingdom)": "ஆங்கிலம் (ஐக்கிய முடியரசு)", + "English (auto-generated)": "ஆங்கிலம் (தானாக உருவாக்கப்பட்ட)", + "Afrikaans": "ஆப்பிரிக்கா", + "Albanian": "அல்பேனிய", + "Amharic": "அம்ஆரிக்", + "Arabic": "அரபு", + "Armenian": "ஆர்மீனியன்", + "Azerbaijani": "அசர்பைசானி", + "Bangla": "பாங்லா", + "Basque": "பாச்க்", + "Belarusian": "பெலாருசியன்", + "Bosnian": "போச்னிய", + "Bulgarian": "பல்கேரியன்", + "Burmese": "பர்மீச்", + "Cantonese (Hong Kong)": "கான்டோனீச் (ஆங்காங்)", + "Catalan": "கற்றலான்", + "Cebuano": "செபுவானோ", + "Chinese": "சீன", + "Chinese (China)": "சீன (சீனா)", + "Chinese (Hong Kong)": "சீன (ஆங்காங்)", + "Chinese (Simplified)": "சீன (எளிமைப்படுத்தப்பட்ட)", + "Chinese (Taiwan)": "சீன (தைவான்)", + "Chinese (Traditional)": "சீன (பாரம்பரிய)", + "Dutch": "டச்சு", + "Finnish": "பின்னிச்", + "French": "பிரஞ்சு", + "German (auto-generated)": "செர்மன் (தானாக உருவாக்கப்பட்ட)", + "Greek": "கிரேக்கம்", + "Gujarati": "குசராத்தி", + "Haitian Creole": "ஐட்டிய கிரியோல்", + "Hungarian": "அங்கேரியன்", + "Icelandic": "ஐச்லாந்திய", + "Igbo": "இக்போ", + "Korean (auto-generated)": "கொரிய (தானாக உருவாக்கப்பட்ட)", + "Macedonian": "மாசிடோனியன்", + "Malagasy": "மலகாசி", + "Maltese": "மால்டிச்", + "Maori": "மௌரி", + "Malayalam": "மலையாளம்", + "Marathi": "மராத்தி", + "Mongolian": "மங்கோலியன்", + "Nepali": "நேபாளி", + "Norwegian Bokmål": "நார்வேசியன் பொக்மால்", + "Nyanja": "நயன்சா", + "Russian": "ரச்ய", + "Russian (auto-generated)": "ரச்ய (தானாக உருவாக்கப்பட்ட)", + "Samoan": "சமோவான்", + "Scottish Gaelic": "ச்கோட்டிச் கயாலிக்", + "Serbian": "செர்பிய", + "Shona": "சோனா", + "Sindhi": "சிந்தி", + "Somali": "சோமாலி", + "Southern Sotho": "தெற்கத்திய சோதோ", + "Spanish": "ச்பானிச்", + "Spanish (auto-generated)": "ச்பானிச் (தானாக உருவாக்கப்பட்ட)", + "Sundanese": "சுந்தானியர்கள்", + "Swahili": "ச்வாஇலி", + "Swedish": "ச்வீடிச்", + "Tajik": "தசிக்", + "Tamil": "தமிழ்", + "Thai": "தாய்", + "Turkish": "துருக்கிய", + "Vietnamese": "வியட்நாமிய", + "Welsh": "வேல்ச்", + "Xhosa": "ஓசா", + "Yiddish": "யெட்டிச்", + "Yoruba": "யோருபா", + "Top": "மேலே", + "About": "பற்றி", + "View as playlist": "பிளேலிச்ட்டாக காண்க", + "Gaming": "கேமிங்", + "News": "செய்தி", + "Movies": "திரைப்படங்கள்", + "Download as: ": "என பதிவிறக்கவும்: ", + "Download is disabled": "பதிவிறக்கம் முடக்கப்பட்டுள்ளது", + "(edited)": "(திருத்தப்பட்டது)", + "YouTube comment permalink": "YouTube கருத்து பெர்மாலின்க்", + "`x` marked it with a ❤": "`x` அதை a உடன் குறித்தது", + "Video mode": "வீடியோ பயன்முறை", + "Playlists": "பிளேலிச்ட்கள்", + "search_filters_date_option_today": "இன்று", + "search_filters_date_option_week": "இந்த வாரம்", + "search_filters_date_option_month": "இந்த மாதம்", + "search_filters_type_option_channel": "வாய்க்கால்", + "search_filters_type_option_playlist": "பிளேலிச்ட்", + "search_filters_duration_label": "காலம்", + "search_filters_duration_option_none": "எந்த காலமும்", + "search_filters_duration_option_medium": "நடுத்தர (4 - 20 நிமிடங்கள்)", + "search_filters_duration_option_long": "நீண்ட (> 20 நிமிடங்கள்)", + "search_filters_features_label": "நற்பொருத்தங்கள்", + "search_filters_features_option_four_k": "எச்.சி.", + "search_filters_features_option_live": "நேரடி", + "search_filters_features_option_hd": "எச்டி", + "search_filters_features_option_subtitles": "வசன வரிகள்/சிசி", + "search_filters_features_option_c_commons": "கிரியேட்டிவ் காமன்ச்", + "search_filters_features_option_three_sixty": "360 °", + "search_filters_features_option_three_d": "ZD", + "search_filters_features_option_hdr": "எச்.டி.ஆர்", + "search_filters_features_option_location": "இடம்", + "search_filters_sort_option_relevance": "பொருத்தமானது", + "search_filters_sort_option_rating": "செயல்வரம்பு", + "Current version: ": "தற்போதைய பதிப்பு: ", + "next_steps_error_message": "அதன் பிறகு நீங்கள் முயற்சி செய்ய வேண்டும்: ", + "next_steps_error_message_refresh": "புதுப்பிப்பு", + "next_steps_error_message_go_to_youtube": "YouTube க்குச் செல்லுங்கள்", + "footer_donate_page": "நன்கொடை", + "footer_modfied_source_code": "மாற்றியமைக்கப்பட்ட மூலக் குறியீடு", + "adminprefs_modified_source_code_url_label": "மாற்றியமைக்கப்பட்ட மூலக் குறியீடு களஞ்சியத்திற்கு முகவரி", + "videoinfo_started_streaming_x_ago": "`X` முன்பு ச்ட்ரீமிங் செய்யத் தொடங்கியது", + "videoinfo_watch_on_youTube": "YouTube இல் பாருங்கள்", + "download_subtitles": "வசன வரிகள் - `x` (.vtt)", + "user_created_playlists": "`x` உருவாக்கியது பிளேலிச்ட்கள்", + "user_saved_playlists": "`x` சேமித்த பிளேலிச்ட்கள்", + "crash_page_before_reporting": "ஒரு பிழையைப் புகாரளிப்பதற்கு முன், உங்களிடம் இருப்பதை உறுதிப்படுத்திக் கொள்ளுங்கள்:", + "crash_page_switch_instance": "<a href = \"` x` \"> மற்றொரு நிகழ்வைப் பயன்படுத்த முயற்சித்தேன் </a>", + "crash_page_search_issue": "அறிவிலிமையத்தில் உள்ள <a href=\"`x`\"> தற்போதைய சிக்கல்களைத் தேடியது</a>", + "channel_tab_shorts_label": "குறுக்குகள்", + "channel_tab_streams_label": "லைவ்ச்ட்ரீம்கள்", + "carousel_go_to": "`X` ச்லைடு செல்லவும்", + "Popular": "புகழ்பெற்ற", + "Subscribe": "குழுசேர்", + "View channel on YouTube": "YouTube இல் சேனலைக் காண்க", + "Authorize token for `x`?": "`X` க்கு கிள்ளாக்கை அங்கீகரிக்கவா?", + "No": "இல்லை", + "Add to playlist: ": "பிளேலிச்ட்டில் சேர்க்கவும்: ", + "Answer": "பதில்", + "Search for videos": "வீடியோக்களைத் தேடுங்கள்", + "The Popular feed has been disabled by the administrator.": "பிரபலமான ஊட்டத்தை நிர்வாகியால் முடக்கப்பட்டுள்ளது.", + "generic_subscriptions_count": "{{count}} சந்தா", + "generic_subscriptions_count_plural": "{{count}} சந்தாக்கள்", + "generic_button_edit": "தொகு", + "generic_button_save": "சேமி", + "generic_button_cancel": "ரத்துசெய்", + "Import and Export Data": "தரவை இறக்குமதி செய்து ஏற்றுமதி செய்யுங்கள்", + "Import": "இறக்குமதி", + "Import NewPipe subscriptions (.json)": "நியூபிப்பிப் சந்தாக்களை இறக்குமதி செய்யுங்கள் (.json)", + "Export": "ஏற்றுமதி", + "Text CAPTCHA": "உரை கேப்ட்சா", + "Image CAPTCHA": "பட கேப்ட்சா", + "preferences_category_player": "பிளேயர் விருப்பத்தேர்வுகள்", + "preferences_video_loop_label": "எப்போதும் லூப்: ", + "preferences_continue_autoplay_label": "தன்னியக்க அடுத்த வீடியோ: ", + "preferences_listen_label": "இயல்பாக கேளுங்கள்: ", + "preferences_quality_option_dash": "கோடு (தகவமைப்பு தரம்)", + "preferences_quality_option_hd720": "HD720", + "preferences_quality_option_medium": "சராசரி", + "preferences_quality_option_small": "சிறிய", + "preferences_quality_dash_option_2160p": "2160 ப", + "preferences_quality_dash_option_1440p": "1440 ப", + "preferences_quality_dash_option_240p": "240 ப", + "youtube": "YouTube", + "reddit": "ரெடிட்", + "invidious": "வெகுவாக", + "preferences_extend_desc_label": "வீடியோ விளக்கத்தை தானாக நீட்டிக்கவும்: ", + "preferences_region_label": "உள்ளடக்க நாடு: ", + "preferences_player_style_label": "பிளேயர் ச்டைல்: ", + "Dark mode: ": "இருண்ட முறை: ", + "preferences_dark_mode_label": "தீம்: ", + "dark": "இருண்ட", + "preferences_automatic_instance_redirect_label": "தானியங்கி நிகழ்வு திசைதிருப்பல் (redirect.invidious.io க்கு குறைவடையும்): ", + "preferences_max_results_label": "ஊட்டத்தில் காட்டப்பட்டுள்ள வீடியோக்களின் எண்ணிக்கை: ", + "alphabetically - reverse": "அகரவரிசை - தலைகீழ்", + "channel name": "சேனல் பெயர்", + "channel name - reverse": "சேனல் பெயர் - தலைகீழ்", + "Only show latest video from channel: ": "சேனலில் இருந்து அண்மைக் கால வீடியோவைக் காட்டுங்கள்: ", + "Only show latest unwatched video from channel: ": "சேனலில் இருந்து அண்மைக் கால கவனிக்கப்படாத வீடியோவைக் காட்டுங்கள்: ", + "`x` uploaded a video": "`x` ஒரு வீடியோவைப் பதிவேற்றியது", + "Clear watch history": "தெளிவான கண்காணிப்பு வரலாறு", + "Log out": "விடுபதிகை", + "Source available here.": "சான்று இங்கே கிடைக்கிறது.", + "Delete playlist": "பிளேலிச்ட்டை நீக்கு", + "Create playlist": "பிளேலிச்ட்டை உருவாக்கவும்", + "Title": "தலைப்பு", + "Import/export data": "தரவு இறக்குமதி/ஏற்றுமதி", + "Change password": "கடவுச்சொல்லை மாற்றவும்", + "Manage tokens": "டோக்கன்களை நிர்வகிக்கவும்", + "Popular enabled: ": "பிரபலமான இயக்கப்பட்டது: ", + "tokens_count": "{{count}} கிள்ளாக்கு", + "tokens_count_plural": "{{count}} டோக்கன்கள்", + "Import/export": "இறக்குமதி/ஏற்றுமதி", + "unsubscribe": "குழுவிலகவும்", + "revoke": "ரத்து செய்யுங்கள்", + "Subscriptions": "சந்தாக்கள்", + "subscriptions_unseen_notifs_count": "{{count}} காணப்படாத அறிவிப்பு", + "subscriptions_unseen_notifs_count_plural": "{{count}} காணப்படாத அறிவிப்புகள்", + "Editing playlist `x`": "பிளேலிச்ட்டைத் திருத்துதல் `x`", + "playlist_button_add_items": "வீடியோக்களைச் சேர்க்கவும்", + "Show more": "மேலும் காட்டு", + "Show less": "குறைவாகக் காட்டு", + "Switch Invidious Instance": "அக்யோர்ட் உதாரணத்தை மாற்றவும்", + "search_message_no_results": "முடிவுகள் எதுவும் கிடைக்கவில்லை.", + "search_message_change_filters_or_query": "உங்கள் தேடல் வினவலை அகலப்படுத்த முயற்சிக்கவும்/அல்லது வடிப்பான்களை மாற்றவும்.", + "search_message_use_another_instance": "நீங்கள் <a href = \"` x` \"> மற்றொரு நிகழ்வில் தேடலாம் </a>.", + "Show annotations": "சிறுகுறிப்புகளைக் காட்டு", + "Genre: ": "வகை: ", + "License: ": "உரிமம்: ", + "Standard YouTube license": "நிலையான YouTube உரிமம்", + "Family friendly? ": "குடும்ப நட்பு? ", + "Wilson score: ": "வில்சன் மதிப்பெண்: ", + "Engagement: ": "நிச்சயதார்த்தம்: ", + "Whitelisted regions: ": "அனுமதிப்பட்டிய பகுதிகள்: ", + "Blacklisted regions: ": "தடுப்புப்பட்டியாக்கப்பட்ட பகுதிகள்: ", + "Music in this video": "இந்த வீடியோவில் இசை", + "Artist: ": "கலைஞர்: ", + "Song: ": "பாடல்: ", + "Album: ": "ஆல்பம்: ", + "Shared `x`": "பகிரப்பட்டது `x`", + "Premieres in `x`": "`X` இல் பிரீமியர்ச்", + "Premieres `x`": "பிரீமியர்ச் `x`", + "Hi! Looks like you have JavaScript turned off. Click here to view comments, keep in mind they may take a bit longer to load.": "ஆய்! நீங்கள் சாவாச்கிரிப்ட் முடக்கப்பட்டிருப்பது போல் தெரிகிறது. கருத்துகளைக் காண இங்கே சொடுக்கு செய்க, அவர்கள் ஏற்றுவதற்கு சிறிது நேரம் ஆகலாம் என்பதை நினைவில் கொள்ளுங்கள்.", + "View YouTube comments": "YouTube கருத்துகளைக் காண்க", + "View more comments on Reddit": "ரெடிட் குறித்த கூடுதல் கருத்துகளைக் காண்க", + "View `x` comments": { + "([^.,0-9]|^)1([^.,0-9]|$)": "`X` கருத்தைக் காண்க", + "": "`X` கருத்துகளைக் காண்க" + }, + "View Reddit comments": "ரெடிட் கருத்துகளைக் காண்க", + "Hide replies": "பதில்களை மறைக்கவும்", + "Wrong username or password": "தவறான பயனர்பெயர் அல்லது கடவுச்சொல்", + "Password cannot be longer than 55 characters": "கடவுச்சொல் 55 எழுத்துகளை விட நீளமாக இருக்க முடியாது", + "Invidious Private Feed for `x`": "`X` க்கான மோசமான தனியார் ஊட்டம்", + "channel:`x`": "சேனல்: `x`", + "Deleted or invalid channel": "நீக்கப்பட்ட அல்லது தவறான சேனல்", + "comments_view_x_replies": "{{count}} பதிலைக் காண்க", + "comments_view_x_replies_plural": "{{count}} பதில்களைக் காண்க", + "`x` ago": "`x` முன்பு", + "Load more": "மேலும் ஏற்றவும்", + "Hidden field \"challenge\" is a required field": "மறைக்கப்பட்ட புலம் \"அறைகூவல்\" என்பது தேவையான புலம்", + "Hidden field \"token\" is a required field": "மறைக்கப்பட்ட புலம் \"கிள்ளாக்கு\" என்பது தேவையான புலம்", + "Corsican": "கார்சிகன்", + "Croatian": "குரோசியன்", + "Czech": "செக்", + "Danish": "டேனிச்", + "Dutch (auto-generated)": "டச்சு (தானாக உருவாக்கப்பட்ட)", + "Esperanto": "எச்பெராண்டோ", + "Estonian": "எச்டோனிய", + "Filipino": "ஃபிலிபினோ", + "Filipino (auto-generated)": "பிலிப்பைன்ச் (தானாக உருவாக்கிய)", + "French (auto-generated)": "பிரஞ்சு (தானாக உருவாக்கப்பட்ட)", + "Galician": "காலிசியன்", + "Georgian": "சார்சியன்", + "German": "செர்மன்", + "Hausa": "ஔசா", + "Lao": "லாவோ", + "Latin": "லத்தீன்", + "Latvian": "லாட்வியன்", + "Hawaiian": "அவாயியன்", + "Hebrew": "எபிரேய", + "Lithuanian": "லிதுவேனியன்", + "Hindi": "இந்தி", + "Hmong": "அமோங்", + "Indonesian": "இந்தோனேசிய", + "Indonesian (auto-generated)": "இந்தோனேசிய (தானாக உருவாக்கப்பட்ட)", + "Interlingue": "இன்டர்லின்குய்", + "Irish": "ஐரிச்", + "Italian": "இத்தாலிய", + "Italian (auto-generated)": "இத்தாலியன் (தானாக உருவாக்கப்பட்ட)", + "Japanese": "சப்பானியர்கள்", + "Japanese (auto-generated)": "சப்பானிய (தானாக உருவாக்கப்பட்ட)", + "Javanese": "சாவானீச்", + "Kannada": "கன்னடா", + "Kazakh": "கசாக்", + "Khmer": "கெமர்", + "Korean": "கொரிய", + "Kurdish": "குர்திச்", + "Kyrgyz": "கிர்கிச்", + "Luxembourgish": "லக்சம்போர்கிச்", + "Malay": "மலாய்", + "Pashto": "பச்தோ", + "Persian": "பெர்சியன்", + "Polish": "போலீச்", + "Portuguese": "போர்த்துகீசியம்", + "Portuguese (auto-generated)": "போர்த்துகீசியம் (தானாக உருவாக்கிய)", + "generic_count_minutes": "{{count}} மணித்துளி", + "generic_count_minutes_plural": "{{count}} நிமிடங்கள்", + "generic_count_seconds": "{{count}} இரண்டாவது", + "generic_count_seconds_plural": "{{count}} வினாடிகள்", + "Fallback comments: ": "குறைவடையும் கருத்துரைகள்: ", + "Portuguese (Brazil)": "போர்த்துகீசியம் (பிரேசில்)", + "Punjabi": "பஞ்சாபி", + "Romanian": "ருமேனிய", + "Sinhala": "சிங்களம்", + "Slovak": "ச்லோவாக்", + "Slovenian": "ச்லோவேனியன்", + "Spanish (Latin America)": "ச்பானிச் (லத்தீன் அமெரிக்கா)", + "Spanish (Mexico)": "ச்பானிச் (மெக்சிகோ)", + "Spanish (Spain)": "ச்பானிச் (ச்பெயின்)", + "Telugu": "தெலுங்கு", + "Turkish (auto-generated)": "துருக்கிய (தானாக உருவாக்கிய)", + "Ukrainian": "உக்ரேனிய", + "Urdu": "உருது", + "Uzbek": "உச்பெக்", + "Vietnamese (auto-generated)": "வியட்நாமிய (தானாக உருவாக்கப்பட்ட)", + "Western Frisian": "மேற்கு ஃபிரிசியன்", + "Zulu": "சுலு", + "generic_count_years": "{{count}}} ஆண்டு", + "generic_count_years_plural": "{{count}} ஆண்டுகள்", + "generic_count_months": "{{count}} மாதம்", + "generic_count_months_plural": "{{count}} மாதங்கள்", + "generic_count_weeks": "{{count}}} வாரம்", + "generic_count_weeks_plural": "{{count}} வாரங்கள்", + "generic_count_days": "{{count}}} நாள்", + "generic_count_days_plural": "{{count}} நாட்கள்", + "generic_count_hours": "{{count}} மணிநேரம்", + "generic_count_hours_plural": "{{count}} மணிநேரம்", + "Search": "தேடல்", + "Rating: ": "மதிப்பீடு: ", + "preferences_locale_label": "மொழி: ", + "Default": "இயல்புநிலை", + "Music": "இசை", + "Download": "பதிவிறக்கம்", + "%A %B %-d, %Y": "%A %b %-d, %y", + "permalink": "பெர்மாலின்க்", + "Channel Sponsor": "சேனல் ஒப்புரவாளர்", + "Audio mode": "ஆடியோ பயன்முறை", + "search_filters_duration_option_short": "குறுகிய (<4 நிமிடங்கள்)", + "search_filters_title": "வடிப்பான்கள்", + "search_filters_date_label": "தேதி பதிவேற்றும் தேதி", + "search_filters_date_option_none": "எந்த தேதி", + "search_filters_date_option_hour": "கடைசி மணி", + "search_filters_date_option_year": "இந்த ஆண்டு", + "search_filters_type_label": "வகை", + "search_filters_type_option_all": "எந்த வகை", + "search_filters_type_option_video": "ஒளிதோற்றம்", + "search_filters_type_option_movie": "படம்", + "search_filters_type_option_show": "காட்டு", + "search_filters_features_option_vr180": "VR180", + "search_filters_features_option_purchased": "வாங்கப்பட்டது", + "search_filters_sort_label": "வரிசைப்படுத்தவும்", + "search_filters_sort_option_date": "பதிவேற்ற தேதி", + "search_filters_sort_option_views": "எண்ணிக்கை காண்க", + "search_filters_apply_button": "தேர்ந்தெடுக்கப்பட்ட வடிப்பான்களைப் பயன்படுத்துங்கள்", + "footer_documentation": "ஆவணப்படுத்துதல்", + "footer_source_code": "மூலக் குறியீடு", + "footer_original_source_code": "அசல் மூலக் குறியீடு", + "none": "எதுவுமில்லை", + "videoinfo_youTube_embed_link": "உட்பொதிக்கப்பட்டது", + "videoinfo_invidious_embed_link": "உட்பொதிப்பு இணைப்பு", + "Video unavailable": "வீடியோ கிடைக்கவில்லை", + "preferences_save_player_pos_label": "பிளேபேக் நிலையை சேமிக்கவும்: ", + "crash_page_you_found_a_bug": "நீங்கள் ஒரு பிழையை கண்டுபிடித்ததாகத் தெரிகிறது!", + "crash_page_refresh": "<a href = \"` x` \"> பக்கத்தை புதுப்பிக்க முயற்சித்தேன் </a>", + "crash_page_read_the_faq": "<a href = \"` x` \"> அடிக்கடி கேட்கப்படும் கேள்விகள் (கேள்விகள்) </a> ஐப் படியுங்கள்", + "crash_page_report_issue": "மேலே எதுவும் உதவவில்லை என்றால், தயவுசெய்து <a href = \"` x` \"> அறிவிலிமையம் </a> (முன்னுரிமை ஆங்கிலத்தில்) ஒரு புதிய சிக்கலைத் திறந்து உங்கள் செய்தியில் பின்வரும் உரையைச் சேர்க்கவும் (அந்த உரையை மொழிபெயர்க்க வேண்டாம்):", + "error_video_not_in_playlist": "கோரப்பட்ட வீடியோ இந்த பிளேலிச்ட்டில் இல்லை. <a href = \"` x` \"> பிளேலிச்ட் முகப்பு பக்கத்திற்கு இங்கே சொடுக்கு செய்க. </a>", + "channel_tab_videos_label": "வீடியோக்கள்", + "channel_tab_podcasts_label": "பாட்காச்ட்கள்", + "channel_tab_releases_label": "வெளியீடுகள்", + "channel_tab_playlists_label": "பிளேலிச்ட்கள்", + "channel_tab_community_label": "சமூகம்", + "channel_tab_channels_label": "சேனல்கள்", + "toggle_theme": "கருப்பொருளை மாற்றவும்", + "carousel_slide": "{{total}} இன் ச்லைடு {{current}}", + "carousel_skip": "கொணர்வி தவிர்க்கவும்" +} diff --git a/locales/tok.json b/locales/tok.json new file mode 100644 index 00000000..0967ef42 --- /dev/null +++ b/locales/tok.json @@ -0,0 +1 @@ +{} diff --git a/locales/tr.json b/locales/tr.json index 282cbf88..94c127db 100644 --- a/locales/tr.json +++ b/locales/tr.json @@ -496,5 +496,6 @@ "carousel_slide": "Sunum {{current}} / {{total}}", "carousel_skip": "Kayar menüyü atla", "carousel_go_to": "`x` sunumuna git", - "The Popular feed has been disabled by the administrator.": "Popüler akışı yönetici tarafından devre dışı bırakıldı." + "The Popular feed has been disabled by the administrator.": "Popüler akışı yönetici tarafından devre dışı bırakıldı.", + "preferences_preload_label": "Video verilerini önceden yükle: " } diff --git a/locales/uk.json b/locales/uk.json index 64329032..2472f247 100644 --- a/locales/uk.json +++ b/locales/uk.json @@ -513,5 +513,7 @@ "The Popular feed has been disabled by the administrator.": "Стрічка Популярні вимкнена адміністратором.", "carousel_slide": "Слайд {{current}} з {{total}}", "carousel_skip": "Пропустити карусель", - "carousel_go_to": "Перейти до слайда `x`" + "carousel_go_to": "Перейти до слайда `x`", + "preferences_preload_label": "Попереднє завантаження відеоданих: ", + "Filipino (auto-generated)": "Філіппінська (згенеровано автоматично)" } diff --git a/locales/zh-CN.json b/locales/zh-CN.json index 776c5ddb..2024bdd5 100644 --- a/locales/zh-CN.json +++ b/locales/zh-CN.json @@ -479,5 +479,7 @@ "The Popular feed has been disabled by the administrator.": "“流行”源已被管理员禁用。", "carousel_slide": "当前为第 {{current}} 张图,共 {{total}} 张图", "carousel_skip": "跳过图集", - "carousel_go_to": "转到图 `x`" + "carousel_go_to": "转到图 `x`", + "preferences_preload_label": "预加载视频数据: ", + "Filipino (auto-generated)": "菲律宾语 (自动生成)" } diff --git a/locales/zh-TW.json b/locales/zh-TW.json index 1e17deb6..b3d67130 100644 --- a/locales/zh-TW.json +++ b/locales/zh-TW.json @@ -479,5 +479,7 @@ "carousel_slide": "第 {{current}} 張投影片,共 {{total}} 張", "carousel_skip": "略過輪播", "carousel_go_to": "跳到投影片 `x`", - "The Popular feed has been disabled by the administrator.": "熱門 feed 已被管理員停用。" + "The Popular feed has been disabled by the administrator.": "熱門 feed 已被管理員停用。", + "preferences_preload_label": "預先載入影片資訊 ", + "Filipino (auto-generated)": "菲律賓語(自動產生)" } diff --git a/src/invidious.cr b/src/invidious.cr index b422dcbb..52df77be 100644 --- a/src/invidious.cr +++ b/src/invidious.cr @@ -192,8 +192,9 @@ if CONFIG.popular_enabled Invidious::Jobs.register Invidious::Jobs::PullPopularVideosJob.new(PG_DB) end -CONNECTION_CHANNEL = ::Channel({Bool, ::Channel(PQ::Notification)}).new(32) -Invidious::Jobs.register Invidious::Jobs::NotificationJob.new(CONNECTION_CHANNEL, CONFIG.database_url) +NOTIFICATION_CHANNEL = ::Channel(VideoNotification).new(32) +CONNECTION_CHANNEL = ::Channel({Bool, ::Channel(PQ::Notification)}).new(32) +Invidious::Jobs.register Invidious::Jobs::NotificationJob.new(NOTIFICATION_CHANNEL, CONNECTION_CHANNEL, CONFIG.database_url) Invidious::Jobs.register Invidious::Jobs::ClearExpiredItemsJob.new @@ -239,8 +240,6 @@ add_context_storage_type(Preferences) add_context_storage_type(Invidious::User) Kemal.config.logger = LOGGER -Kemal.config.host_binding = Kemal.config.host_binding != "0.0.0.0" ? Kemal.config.host_binding : CONFIG.host_binding -Kemal.config.port = Kemal.config.port != 3000 ? Kemal.config.port : CONFIG.port Kemal.config.app_name = "Invidious" # Use in kemal's production mode. @@ -249,4 +248,16 @@ Kemal.config.app_name = "Invidious" Kemal.config.env = "production" if !ENV.has_key?("KEMAL_ENV") {% end %} -Kemal.run +Kemal.run do |config| + if socket_binding = CONFIG.socket_binding +File.delete?(socket_binding.path) + # Create a socket and set its desired permissions + server = UNIXServer.new(socket_binding.path) + perms = socket_binding.permissions.to_i(base: 8) + File.chmod(socket_binding.path, perms) + config.server.not_nil!.bind server + else + Kemal.config.host_binding = Kemal.config.host_binding != "0.0.0.0" ? Kemal.config.host_binding : CONFIG.host_binding + Kemal.config.port = Kemal.config.port != 3000 ? Kemal.config.port : CONFIG.port + end +end diff --git a/src/invidious/channels/channels.cr b/src/invidious/channels/channels.cr index 1478c8fc..65982325 100644 --- a/src/invidious/channels/channels.cr +++ b/src/invidious/channels/channels.cr @@ -249,11 +249,7 @@ def fetch_channel(ucid, pull_all_videos : Bool) if was_insert LOGGER.trace("fetch_channel: #{ucid} : video #{video_id} : Inserted, updating subscriptions") - if CONFIG.enable_user_notifications - Invidious::Database::Users.add_notification(video) - else - Invidious::Database::Users.feed_needs_update(video) - end + NOTIFICATION_CHANNEL.send(VideoNotification.from_video(video)) else LOGGER.trace("fetch_channel: #{ucid} : video #{video_id} : Updated") end @@ -285,11 +281,7 @@ def fetch_channel(ucid, pull_all_videos : Bool) if Time.utc - video.published > 1.minute was_insert = Invidious::Database::ChannelVideos.insert(video) if was_insert - if CONFIG.enable_user_notifications - Invidious::Database::Users.add_notification(video) - else - Invidious::Database::Users.feed_needs_update(video) - end + NOTIFICATION_CHANNEL.send(VideoNotification.from_video(video)) end end end diff --git a/src/invidious/channels/playlists.cr b/src/invidious/channels/playlists.cr index 91029fe3..9b45d0c8 100644 --- a/src/invidious/channels/playlists.cr +++ b/src/invidious/channels/playlists.cr @@ -44,3 +44,12 @@ def fetch_channel_releases(ucid, author, continuation) end return extract_items(initial_data, author, ucid) end + +def fetch_channel_courses(ucid, author, continuation) + if continuation + initial_data = YoutubeAPI.browse(continuation) + else + initial_data = YoutubeAPI.browse(ucid, params: "Egdjb3Vyc2Vz8gYFCgPCAQA%3D") + end + return extract_items(initial_data, author, ucid) +end diff --git a/src/invidious/config.cr b/src/invidious/config.cr index 4b3bdafc..453256b5 100644 --- a/src/invidious/config.cr +++ b/src/invidious/config.cr @@ -8,6 +8,13 @@ struct DBConfig property dbname : String end +struct SocketBindingConfig + include YAML::Serializable + + property path : String + property permissions : String +end + struct ConfigPreferences include YAML::Serializable @@ -138,6 +145,8 @@ class Config property port : Int32 = 3000 # Host to bind (overridden by command line argument) property host_binding : String = "0.0.0.0" + # Path and permissions to make Invidious listen on a UNIX socket instead of a TCP port + property socket_binding : SocketBindingConfig? = nil # 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 @@ -255,6 +264,24 @@ class Config end end + # Check if the socket configuration is valid + if sb = config.socket_binding + if sb.path.ends_with?("/") || File.directory?(sb.path) + puts "Config: The socket path " + sb.path + " must not be a directory!" + exit(1) + end + d = File.dirname(sb.path) + if !File.directory?(d) + puts "Config: Socket directory " + sb.path + " does not exist or is not a directory!" + exit(1) + end + p = sb.permissions.to_i?(base: 8) + if !p || p < 0 || p > 0o777 + puts "Config: Socket permissions must be an octal between 0 and 777!" + exit(1) + end + end + return config end end diff --git a/src/invidious/database/users.cr b/src/invidious/database/users.cr index d54e6a76..4a3056ea 100644 --- a/src/invidious/database/users.cr +++ b/src/invidious/database/users.cr @@ -119,15 +119,15 @@ module Invidious::Database::Users # Update (notifs) # ------------------- - def add_notification(video : ChannelVideo) + def add_multiple_notifications(channel_id : String, video_ids : Array(String)) request = <<-SQL UPDATE users - SET notifications = array_append(notifications, $1), + SET notifications = array_cat(notifications, $1), feed_needs_update = true WHERE $2 = ANY(subscriptions) SQL - PG_DB.exec(request, video.id, video.ucid) + PG_DB.exec(request, video_ids, channel_id) end def remove_notification(user : User, vid : String) @@ -154,14 +154,14 @@ module Invidious::Database::Users # Update (misc) # ------------------- - def feed_needs_update(video : ChannelVideo) + def feed_needs_update(channel_id : String) request = <<-SQL UPDATE users SET feed_needs_update = true WHERE $1 = ANY(subscriptions) SQL - PG_DB.exec(request, video.ucid) + PG_DB.exec(request, channel_id) end def update_preferences(user : User) diff --git a/src/invidious/frontend/channel_page.cr b/src/invidious/frontend/channel_page.cr index 00f24568..4fe21b96 100644 --- a/src/invidious/frontend/channel_page.cr +++ b/src/invidious/frontend/channel_page.cr @@ -7,6 +7,7 @@ module Invidious::Frontend::ChannelPage Streams Podcasts Releases + Courses Playlists Posts Channels diff --git a/src/invidious/frontend/pagination.cr b/src/invidious/frontend/pagination.cr index 3f931f4e..a29f5936 100644 --- a/src/invidious/frontend/pagination.cr +++ b/src/invidious/frontend/pagination.cr @@ -3,6 +3,24 @@ require "uri" module Invidious::Frontend::Pagination extend self + private def first_page(str : String::Builder, locale : String?, url : String) + str << %(<a href=") << url << %(" class="pure-button pure-button-secondary">) + + if locale_is_rtl?(locale) + # Inverted arrow ("first" points to the right) + str << translate(locale, "First page") + str << " " + str << %(<i class="icon ion-ios-arrow-forward"></i>) + else + # Regular arrow ("first" points to the left) + str << %(<i class="icon ion-ios-arrow-back"></i>) + str << " " + str << translate(locale, "First page") + end + + str << "</a>" + end + private def previous_page(str : String::Builder, locale : String?, url : String) # Link str << %(<a href=") << url << %(" class="pure-button pure-button-secondary">) @@ -72,18 +90,24 @@ module Invidious::Frontend::Pagination end end - def nav_ctoken(locale : String?, *, base_url : String | URI, ctoken : String?) + def nav_ctoken(locale : String?, *, base_url : String | URI, ctoken : String?, first_page : Bool, params : URI::Params) return String.build do |str| str << %(<div class="h-box">\n) str << %(<div class="page-nav-container flexible">\n) - str << %(<div class="page-prev-container flex-left"></div>\n) + str << %(<div class="page-prev-container flex-left">) + + if !first_page + self.first_page(str, locale, base_url.to_s) + end + + str << %(</div>\n) str << %(<div class="page-next-container flex-right">) if !ctoken.nil? - params_next = URI::Params{"continuation" => ctoken} - url_next = HttpServer::Utils.add_params_to_url(base_url, params_next) + params["continuation"] = ctoken + url_next = HttpServer::Utils.add_params_to_url(base_url, params) self.next_page(str, locale, url_next.to_s) end diff --git a/src/invidious/helpers/i18n.cr b/src/invidious/helpers/i18n.cr index 1ba3ea61..bca2edda 100644 --- a/src/invidious/helpers/i18n.cr +++ b/src/invidious/helpers/i18n.cr @@ -54,6 +54,7 @@ LOCALES_LIST = { "sr" => "Srpski (latinica)", # Serbian (Latin) "sr_Cyrl" => "Српски (ћирилица)", # Serbian (Cyrillic) "sv-SE" => "Svenska", # Swedish + "ta" => "தமிழ்", # Tamil "tr" => "Türkçe", # Turkish "uk" => "Українська", # Ukrainian "vi" => "Tiếng Việt", # Vietnamese diff --git a/src/invidious/jobs/notification_job.cr b/src/invidious/jobs/notification_job.cr index b445107b..f2c9d4be 100644 --- a/src/invidious/jobs/notification_job.cr +++ b/src/invidious/jobs/notification_job.cr @@ -1,8 +1,32 @@ +struct VideoNotification + getter video_id : String + getter channel_id : String + getter published : Time + + def_hash @channel_id, @video_id + + def ==(other) + video_id == other.video_id + end + + def self.from_video(video : ChannelVideo) : self + VideoNotification.new(video.id, video.ucid, video.published) + end + + def initialize(@video_id, @channel_id, @published) + end + + def clone : VideoNotification + VideoNotification.new(video_id.clone, channel_id.clone, published.clone) + end +end + class Invidious::Jobs::NotificationJob < Invidious::Jobs::BaseJob + private getter notification_channel : ::Channel(VideoNotification) private getter connection_channel : ::Channel({Bool, ::Channel(PQ::Notification)}) private getter pg_url : URI - def initialize(@connection_channel, @pg_url) + def initialize(@notification_channel, @connection_channel, @pg_url) end def begin @@ -10,6 +34,70 @@ class Invidious::Jobs::NotificationJob < Invidious::Jobs::BaseJob PG.connect_listen(pg_url, "notifications") { |event| connections.each(&.send(event)) } + # hash of channels to their videos (id+published) that need notifying + to_notify = Hash(String, Set(VideoNotification)).new( + ->(hash : Hash(String, Set(VideoNotification)), key : String) { + hash[key] = Set(VideoNotification).new + } + ) + notify_mutex = Mutex.new() + + # fiber to locally cache all incoming notifications (from pubsub webhooks and refresh channels job) + spawn do + begin + loop do + notification = notification_channel.receive + notify_mutex.synchronize do + to_notify[notification.channel_id] << notification + end + end + end + end + # fiber to regularly persist all cached notifications + spawn do + loop do + begin + LOGGER.debug("NotificationJob: waking up") + cloned = {} of String => Set(VideoNotification) + notify_mutex.synchronize do + cloned = to_notify.clone + to_notify.clear + end + + cloned.each do |channel_id, notifications| + if notifications.empty? + next + end + + LOGGER.info("NotificationJob: updating channel #{channel_id} with #{notifications.size} notifications") + if CONFIG.enable_user_notifications + video_ids = notifications.map { |n| n.video_id } + Invidious::Database::Users.add_multiple_notifications(channel_id, video_ids) + PG_DB.using_connection do |conn| + notifications.each do |n| + # Deliver notifications to `/api/v1/auth/notifications` + payload = { + "topic" => n.channel_id, + "videoId" => n.video_id, + "published" => n.published.to_unix, + }.to_json + conn.exec("NOTIFY notifications, E'#{payload}'") + end + end + else + Invidious::Database::Users.feed_needs_update(channel_id) + end + end + + LOGGER.trace("NotificationJob: Done, sleeping") + rescue ex + LOGGER.error("NotificationJob: #{ex.message}") + end + sleep 1.minute + Fiber.yield + end + end + loop do action, connection = connection_channel.receive diff --git a/src/invidious/jsonify/api_v1/video_json.cr b/src/invidious/jsonify/api_v1/video_json.cr index 3439ae60..58805af2 100644 --- a/src/invidious/jsonify/api_v1/video_json.cr +++ b/src/invidious/jsonify/api_v1/video_json.cr @@ -268,7 +268,7 @@ module Invidious::JSONify::APIv1 json.field "viewCountText", rv["short_view_count"]? json.field "viewCount", rv["view_count"]?.try &.empty? ? nil : rv["view_count"].to_i64 json.field "published", rv["published"]? - if !rv["published"]?.nil? + if rv["published"]?.try &.presence json.field "publishedText", translate(locale, "`x` ago", recode_date(Time.parse_rfc3339(rv["published"].to_s), locale)) else json.field "publishedText", "" diff --git a/src/invidious/routes/api/v1/channels.cr b/src/invidious/routes/api/v1/channels.cr index 588bbc2a..a940ee68 100644 --- a/src/invidious/routes/api/v1/channels.cr +++ b/src/invidious/routes/api/v1/channels.cr @@ -368,6 +368,35 @@ module Invidious::Routes::API::V1::Channels end end + def self.courses(env) + locale = env.get("preferences").as(Preferences).locale + + env.response.content_type = "application/json" + + ucid = env.params.url["ucid"] + continuation = env.params.query["continuation"]? + + # Use the macro defined above + channel = nil # Make the compiler happy + get_channel() + + items, next_continuation = fetch_channel_courses(channel.ucid, channel.author, continuation) + + JSON.build do |json| + json.object do + json.field "playlists" do + json.array do + items.each do |item| + item.to_json(locale, json) if item.is_a?(SearchPlaylist) + end + end + end + + json.field "continuation", next_continuation if next_continuation + end + end + end + def self.community(env) locale = env.get("preferences").as(Preferences).locale diff --git a/src/invidious/routes/api/v1/videos.cr b/src/invidious/routes/api/v1/videos.cr index 368304ac..6a3eb8ae 100644 --- a/src/invidious/routes/api/v1/videos.cr +++ b/src/invidious/routes/api/v1/videos.cr @@ -429,4 +429,90 @@ module Invidious::Routes::API::V1::Videos end end end + + # Fetches transcripts from YouTube + # + # Use the `lang` and `autogen` query parameter to select which transcript to fetch + # Request without any URL parameters to see all the available transcripts. + def self.transcripts(env) + env.response.content_type = "application/json" + + id = env.params.url["id"] + lang = env.params.query["lang"]? + label = env.params.query["label"]? + auto_generated = env.params.query["autogen"]? ? true : false + + # Return all available transcript options when none is given + if !label && !lang + begin + video = get_video(id) + rescue ex : NotFoundException + return error_json(404, ex) + rescue ex + return error_json(500, ex) + end + + response = JSON.build do |json| + # The amount of transcripts available to fetch is the + # same as the amount of captions available. + available_transcripts = video.captions + + json.object do + json.field "transcripts" do + json.array do + available_transcripts.each do |transcript| + json.object do + json.field "label", transcript.name + json.field "languageCode", transcript.language_code + json.field "autoGenerated", transcript.auto_generated + + if transcript.auto_generated + json.field "url", "/api/v1/transcripts/#{id}?lang=#{URI.encode_www_form(transcript.language_code)}&autogen" + else + json.field "url", "/api/v1/transcripts/#{id}?lang=#{URI.encode_www_form(transcript.language_code)}" + end + end + end + end + end + end + end + + return response + end + + # If lang is not given then we attempt to fetch + # the transcript through the given label + if lang.nil? + begin + video = get_video(id) + rescue ex : NotFoundException + return error_json(404, ex) + rescue ex + return error_json(500, ex) + end + + target_transcript = video.captions.select(&.name.== label) + if target_transcript.empty? + return error_json(404, NotFoundException.new("Requested transcript does not exist")) + else + target_transcript = target_transcript[0] + lang, auto_generated = target_transcript.language_code, target_transcript.auto_generated + end + end + + params = Invidious::Videos::Transcript.generate_param(id, lang, auto_generated) + + begin + transcript = Invidious::Videos::Transcript.from_raw( + YoutubeAPI.get_transcript(params), lang, auto_generated + ) + rescue ex : NotFoundException + return error_json(404, ex) + rescue ex + return error_json(500, ex) + end + + return transcript.to_json + end end diff --git a/src/invidious/routes/channels.cr b/src/invidious/routes/channels.cr index 65e794bd..508aa3e4 100644 --- a/src/invidious/routes/channels.cr +++ b/src/invidious/routes/channels.cr @@ -197,6 +197,26 @@ module Invidious::Routes::Channels templated "channel" end + def self.courses(env) + data = self.fetch_basic_information(env) + return data if !data.is_a?(Tuple) + + locale, user, subscriptions, continuation, ucid, channel = data + + sort_by = "" + sort_options = [] of String + + items, next_continuation = fetch_channel_courses( + channel.ucid, channel.author, continuation + ) + + items = items.select(SearchPlaylist) + items.each(&.author = "") + + selected_tab = Frontend::ChannelPage::TabsAvailable::Courses + templated "channel" + end + def self.community(env) return env.redirect env.request.path.sub("posts", "community") if env.request.path.split("/").last == "posts" @@ -309,7 +329,7 @@ module Invidious::Routes::Channels private KNOWN_TABS = { "home", "videos", "shorts", "streams", "podcasts", - "releases", "playlists", "community", "channels", "about", + "releases", "courses", "playlists", "community", "channels", "about", "posts", } diff --git a/src/invidious/routes/feeds.cr b/src/invidious/routes/feeds.cr index 82c04994..7f9a0edb 100644 --- a/src/invidious/routes/feeds.cr +++ b/src/invidious/routes/feeds.cr @@ -143,32 +143,25 @@ module Invidious::Routes::Feeds # RSS feeds def self.rss_channel(env) - locale = env.get("preferences").as(Preferences).locale - env.response.headers["Content-Type"] = "application/atom+xml" env.response.content_type = "application/atom+xml" - ucid = env.params.url["ucid"] + if env.params.url["ucid"].matches?(/^[\w-]+$/) + ucid = env.params.url["ucid"] + else + return error_atom(400, InfoException.new("Invalid channel ucid provided.")) + end params = HTTP::Params.parse(env.params.query["params"]? || "") - begin - channel = get_about_info(ucid, locale) - rescue ex : ChannelRedirect - return env.redirect env.request.resource.gsub(ucid, ex.channel_id) - rescue ex : NotFoundException - return error_atom(404, ex) - rescue ex - return error_atom(500, ex) - end - namespaces = { "yt" => "http://www.youtube.com/xml/schemas/2015", "media" => "http://search.yahoo.com/mrss/", "default" => "http://www.w3.org/2005/Atom", } - response = YT_POOL.client &.get("/feeds/videos.xml?channel_id=#{channel.ucid}") + response = YT_POOL.client &.get("/feeds/videos.xml?channel_id=#{ucid}") + return error_atom(404, NotFoundException.new("Channel does not exist.")) if response.status_code == 404 rss = XML.parse(response.body) videos = rss.xpath_nodes("//default:feed/default:entry", namespaces).map do |entry| @@ -179,7 +172,7 @@ module Invidious::Routes::Feeds updated = Time.parse_rfc3339(entry.xpath_node("default:updated", namespaces).not_nil!.content) author = entry.xpath_node("default:author/default:name", namespaces).not_nil!.content - ucid = entry.xpath_node("yt:channelId", namespaces).not_nil!.content + video_ucid = entry.xpath_node("yt:channelId", namespaces).not_nil!.content description_html = entry.xpath_node("media:group/media:description", namespaces).not_nil!.to_s views = entry.xpath_node("media:group/media:community/media:statistics", namespaces).not_nil!.["views"].to_i64 @@ -187,7 +180,7 @@ module Invidious::Routes::Feeds title: title, id: video_id, author: author, - ucid: ucid, + ucid: video_ucid, published: published, views: views, description_html: description_html, @@ -199,30 +192,32 @@ module Invidious::Routes::Feeds }) end + author = "" + author = videos[0].author if videos.size > 0 + XML.build(indent: " ", encoding: "UTF-8") do |xml| xml.element("feed", "xmlns:yt": "http://www.youtube.com/xml/schemas/2015", "xmlns:media": "http://search.yahoo.com/mrss/", xmlns: "http://www.w3.org/2005/Atom", "xml:lang": "en-US") do xml.element("link", rel: "self", href: "#{HOST_URL}#{env.request.resource}") - xml.element("id") { xml.text "yt:channel:#{channel.ucid}" } - xml.element("yt:channelId") { xml.text channel.ucid } - xml.element("icon") { xml.text channel.author_thumbnail } - xml.element("title") { xml.text channel.author } - xml.element("link", rel: "alternate", href: "#{HOST_URL}/channel/#{channel.ucid}") + xml.element("id") { xml.text "yt:channel:#{ucid}" } + xml.element("yt:channelId") { xml.text ucid } + xml.element("title") { author } + xml.element("link", rel: "alternate", href: "#{HOST_URL}/channel/#{ucid}") xml.element("author") do - xml.element("name") { xml.text channel.author } - xml.element("uri") { xml.text "#{HOST_URL}/channel/#{channel.ucid}" } + xml.element("name") { xml.text author } + xml.element("uri") { xml.text "#{HOST_URL}/channel/#{ucid}" } end xml.element("image") do - xml.element("url") { xml.text channel.author_thumbnail } - xml.element("title") { xml.text channel.author } + xml.element("url") { xml.text "" } + xml.element("title") { xml.text author } xml.element("link", rel: "self", href: "#{HOST_URL}#{env.request.resource}") end videos.each do |video| - video.to_xml(channel.auto_generated, params, xml) + video.to_xml(false, params, xml) end end end @@ -310,8 +305,9 @@ module Invidious::Routes::Feeds end response = YT_POOL.client &.get("/feeds/videos.xml?playlist_id=#{plid}") - document = XML.parse(response.body) + return error_atom(404, NotFoundException.new("Playlist does not exist.")) if response.status_code == 404 + document = XML.parse(response.body) document.xpath_nodes(%q(//*[@href]|//*[@url])).each do |node| node.attributes.each do |attribute| case attribute.name @@ -424,16 +420,6 @@ module Invidious::Routes::Feeds next # skip this video since it raised an exception (e.g. it is a scheduled live event) end - if CONFIG.enable_user_notifications - # Deliver notifications to `/api/v1/auth/notifications` - payload = { - "topic" => video.ucid, - "videoId" => video.id, - "published" => published.to_unix, - }.to_json - PG_DB.exec("NOTIFY notifications, E'#{payload}'") - end - video = ChannelVideo.new({ id: id, title: video.title, @@ -449,11 +435,7 @@ module Invidious::Routes::Feeds was_insert = Invidious::Database::ChannelVideos.insert(video, with_premiere_timestamp: true) if was_insert - if CONFIG.enable_user_notifications - Invidious::Database::Users.add_notification(video) - else - Invidious::Database::Users.feed_needs_update(video) - end + NOTIFICATION_CHANNEL.send(VideoNotification.from_video(video)) end end end diff --git a/src/invidious/routes/misc.cr b/src/invidious/routes/misc.cr index 8b620d63..0b868755 100644 --- a/src/invidious/routes/misc.cr +++ b/src/invidious/routes/misc.cr @@ -42,12 +42,17 @@ module Invidious::Routes::Misc referer = get_referer(env) instance_list = Invidious::Jobs::InstanceListRefreshJob::INSTANCES["INSTANCES"] - if instance_list.empty? + # Filter out the current instance + other_available_instances = instance_list.reject { |_, domain| domain == CONFIG.domain } + + if other_available_instances.empty? + # If the current instance is the only one, use the redirect URL as fallback instance_url = "redirect.invidious.io" else + # Select other random instance # Sample returns an array # Instances are packaged as {region, domain} in the instance list - instance_url = instance_list.sample(1)[0][1] + instance_url = other_available_instances.sample(1)[0][1] end env.redirect "https://#{instance_url}#{referer}" diff --git a/src/invidious/routing.cr b/src/invidious/routing.cr index 975659eb..46b71f1f 100644 --- a/src/invidious/routing.cr +++ b/src/invidious/routing.cr @@ -120,6 +120,7 @@ module Invidious::Routing get "/channel/:ucid/streams", Routes::Channels, :streams get "/channel/:ucid/podcasts", Routes::Channels, :podcasts get "/channel/:ucid/releases", Routes::Channels, :releases + get "/channel/:ucid/courses", Routes::Channels, :courses get "/channel/:ucid/playlists", Routes::Channels, :playlists get "/channel/:ucid/community", Routes::Channels, :community get "/channel/:ucid/posts", Routes::Channels, :community @@ -237,6 +238,7 @@ module Invidious::Routing get "/api/v1/annotations/:id", {{namespace}}::Videos, :annotations get "/api/v1/comments/:id", {{namespace}}::Videos, :comments get "/api/v1/clips/:id", {{namespace}}::Videos, :clips + get "/api/v1/transcripts/:id", {{namespace}}::Videos, :transcripts # Feeds get "/api/v1/trending", {{namespace}}::Feeds, :trending @@ -250,6 +252,7 @@ module Invidious::Routing get "/api/v1/channels/:ucid/streams", {{namespace}}::Channels, :streams get "/api/v1/channels/:ucid/podcasts", {{namespace}}::Channels, :podcasts get "/api/v1/channels/:ucid/releases", {{namespace}}::Channels, :releases + get "/api/v1/channels/:ucid/courses", {{namespace}}::Channels, :courses get "/api/v1/channels/:ucid/playlists", {{namespace}}::Channels, :playlists get "/api/v1/channels/:ucid/community", {{namespace}}::Channels, :community get "/api/v1/channels/:ucid/posts", {{namespace}}::Channels, :community diff --git a/src/invidious/videos/transcript.cr b/src/invidious/videos/transcript.cr index 4bd9f820..ee1272d1 100644 --- a/src/invidious/videos/transcript.cr +++ b/src/invidious/videos/transcript.cr @@ -122,5 +122,40 @@ module Invidious::Videos return vtt end + + def to_json(json : JSON::Builder) + json.field "languageCode", @language_code + json.field "autoGenerated", @auto_generated + json.field "label", @label + json.field "body" do + json.array do + @lines.each do |line| + json.object do + if line.is_a? HeadingLine + json.field "type", "heading" + else + json.field "type", "regular" + end + + json.field "startMs", line.start_ms.total_milliseconds + json.field "endMs", line.end_ms.total_milliseconds + json.field "line", line.line + end + end + end + end + end + + def to_json + JSON.build do |json| + json.object do + json.field "transcript" do + json.object do + to_json(json) + end + end + end + end + end end end diff --git a/src/invidious/views/channel.ecr b/src/invidious/views/channel.ecr index a84e44bc..686de6bd 100644 --- a/src/invidious/views/channel.ecr +++ b/src/invidious/views/channel.ecr @@ -11,6 +11,7 @@ when .channels? then "/channel/#{ucid}/channels" when .podcasts? then "/channel/#{ucid}/podcasts" when .releases? then "/channel/#{ucid}/releases" + when .courses? then "/channel/#{ucid}/courses" else "/channel/#{ucid}" end @@ -20,7 +21,9 @@ page_nav_html = IV::Frontend::Pagination.nav_ctoken(locale, base_url: relative_url, - ctoken: next_continuation + ctoken: next_continuation, + first_page: continuation.nil?, + params: env.params.query, ) %> @@ -40,6 +43,8 @@ <link rel="alternate" type="application/rss+xml" title="RSS" href="/feed/channel/<%= ucid %>" /> <%- end -%> +<script src="/js/pagination.js?v=<%= ASSET_COMMIT %>"></script> + <link rel="alternate" href="<%= youtube_url %>"> <title><%= author %> - Invidious</title> <% end %> diff --git a/src/invidious/views/components/items_paginated.ecr b/src/invidious/views/components/items_paginated.ecr index 4534a0a3..f69df3fe 100644 --- a/src/invidious/views/components/items_paginated.ecr +++ b/src/invidious/views/components/items_paginated.ecr @@ -8,4 +8,14 @@ <%= page_nav_html %> +<script id="pagination-data" type="application/json"> +<%= +{ + "next_page" => translate(locale, "Next page"), + "prev_page" => translate(locale, "Previous page"), + "is_rtl" => locale_is_rtl?(locale) +}.to_pretty_json +%> +</script> + <script src="/js/watched_indicator.js"></script> |
