Merge remote-tracking branch 'upstream/master'
Some checks failed
Invidious CI / build (push) Has been cancelled
Some checks failed
Invidious CI / build (push) Has been cancelled
This commit is contained in:
commit
50fa7de901
54 changed files with 1157 additions and 152 deletions
93
assets/js/pagination.js
Normal file
93
assets/js/pagination.js
Normal file
|
@ -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);
|
||||
}
|
||||
}
|
||||
});
|
|
@ -138,26 +138,32 @@ player.on('timeupdate', function () {
|
|||
// YouTube links
|
||||
|
||||
let elem_yt_watch = document.getElementById('link-yt-watch');
|
||||
let elem_yt_embed = document.getElementById('link-yt-embed');
|
||||
|
||||
if (elem_yt_watch) {
|
||||
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);
|
||||
}
|
||||
|
||||
let elem_yt_embed = document.getElementById('link-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');
|
||||
let elem_iv_other = document.getElementById('link-iv-other');
|
||||
|
||||
if (elem_iv_embed) {
|
||||
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);
|
||||
}
|
||||
|
||||
let elem_iv_other = document.getElementById('link-iv-other');
|
||||
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);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
|
|
|
@ -177,6 +177,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)
|
||||
|
|
|
@ -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)": "الفلبينية (المولدة تلقائيًا)"
|
||||
}
|
||||
|
|
|
@ -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)"
|
||||
}
|
||||
|
|
|
@ -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)"
|
||||
}
|
||||
|
|
|
@ -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)",
|
||||
|
@ -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": "Αποφυγή εμφάνισης εικόνων"
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
|
@ -512,6 +513,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",
|
||||
|
|
|
@ -516,5 +516,7 @@
|
|||
"carousel_slide": "Diapositiva {{current}} de {{total}}",
|
||||
"carousel_skip": "Saltar el carrusel",
|
||||
"carousel_go_to": "Ir a la diapositiva `x`",
|
||||
"footer_contact_url": "Contactar al Administrador"
|
||||
"footer_contact_url": "Contactar al Administrador",
|
||||
"preferences_preload_label": "Precargar datos del vídeo: ",
|
||||
"Filipino (auto-generated)": "Filipino (generado automáticamente)"
|
||||
}
|
||||
|
|
|
@ -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": "پیش بار کردن دادههای ویدیو: "
|
||||
}
|
||||
|
|
|
@ -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. "
|
||||
}
|
||||
|
|
|
@ -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 : "
|
||||
}
|
||||
|
|
|
@ -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: "
|
||||
}
|
||||
|
|
|
@ -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)"
|
||||
}
|
||||
|
|
|
@ -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)"
|
||||
}
|
||||
|
|
|
@ -479,5 +479,7 @@
|
|||
"carousel_go_to": "スライド`x`を表示",
|
||||
"carousel_slide": "スライド{{current}} / 全{{total}}個中",
|
||||
"carousel_skip": "画像のスライド表示をスキップ",
|
||||
"toggle_theme": "テーマの切り替え"
|
||||
"toggle_theme": "テーマの切り替え",
|
||||
"preferences_preload_label": "動画データを事前に読み込む: ",
|
||||
"Filipino (auto-generated)": "フィリピノ語 (自動生成)"
|
||||
}
|
||||
|
|
|
@ -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": "비디오 데이터 사전 로드: "
|
||||
}
|
||||
|
|
|
@ -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: "
|
||||
}
|
||||
|
|
|
@ -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)"
|
||||
}
|
||||
|
|
|
@ -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)"
|
||||
}
|
||||
|
|
|
@ -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)"
|
||||
}
|
||||
|
|
|
@ -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)"
|
||||
}
|
||||
|
|
|
@ -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": "Предзагрузка видеоданных: "
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
|
@ -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: "
|
||||
}
|
||||
|
|
|
@ -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: "
|
||||
}
|
||||
|
|
|
@ -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)"
|
||||
}
|
||||
|
|
|
@ -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)": "Филипински (аутоматски генерисано)"
|
||||
}
|
||||
|
|
|
@ -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)"
|
||||
}
|
||||
|
|
502
locales/ta.json
Normal file
502
locales/ta.json
Normal file
|
@ -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": "கொணர்வி தவிர்க்கவும்"
|
||||
}
|
1
locales/tok.json
Normal file
1
locales/tok.json
Normal file
|
@ -0,0 +1 @@
|
|||
{}
|
|
@ -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: "
|
||||
}
|
||||
|
|
|
@ -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)": "Філіппінська (згенеровано автоматично)"
|
||||
}
|
||||
|
|
|
@ -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)": "菲律宾语 (自动生成)"
|
||||
}
|
||||
|
|
|
@ -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)": "菲律賓語(自動產生)"
|
||||
}
|
||||
|
|
|
@ -201,8 +201,9 @@ if CONFIG.popular_enabled
|
|||
Invidious::Jobs.register Invidious::Jobs::PullPopularVideosJob.new(PG_DB)
|
||||
end
|
||||
|
||||
NOTIFICATION_CHANNEL = ::Channel(VideoNotification).new(32)
|
||||
CONNECTION_CHANNEL = ::Channel({Bool, ::Channel(PQ::Notification)}).new(32)
|
||||
Invidious::Jobs.register Invidious::Jobs::NotificationJob.new(CONNECTION_CHANNEL, CONFIG.database_url)
|
||||
Invidious::Jobs.register Invidious::Jobs::NotificationJob.new(NOTIFICATION_CHANNEL, CONNECTION_CHANNEL, CONFIG.database_url)
|
||||
|
||||
Invidious::Jobs.register Invidious::Jobs::ClearExpiredItemsJob.new
|
||||
|
||||
|
@ -262,8 +263,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.
|
||||
|
@ -272,4 +271,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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
@ -172,6 +179,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
|
||||
|
@ -410,6 +419,23 @@ class Config
|
|||
puts "0 (PostgreSQL)"
|
||||
puts "1 (Redis compatible DB) (Default)"
|
||||
puts "2 (In memory LRU)"
|
||||
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
|
||||
|
|
|
@ -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,17 +154,15 @@ module Invidious::Database::Users
|
|||
# Update (misc)
|
||||
# -------------------
|
||||
|
||||
# Feeds never need update. PubSubHubBub is the one that sends videos to
|
||||
# invidious.
|
||||
# def feed_needs_update(video : ChannelVideo)
|
||||
# request = <<-SQL
|
||||
# UPDATE users
|
||||
# SET feed_needs_update = true
|
||||
# WHERE $1 = ANY(subscriptions)
|
||||
# SQL
|
||||
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)
|
||||
# end
|
||||
PG_DB.exec(request, channel_id)
|
||||
end
|
||||
|
||||
def update_preferences(user : User)
|
||||
request = <<-SQL
|
||||
|
|
|
@ -7,6 +7,7 @@ module Invidious::Frontend::ChannelPage
|
|||
Streams
|
||||
Podcasts
|
||||
Releases
|
||||
Courses
|
||||
Playlists
|
||||
Posts
|
||||
Channels
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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(&.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
|
||||
|
||||
|
|
|
@ -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", ""
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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",
|
||||
}
|
||||
|
||||
|
|
|
@ -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"
|
||||
|
||||
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
|
||||
|
@ -428,16 +424,6 @@ module Invidious::Routes::Feeds
|
|||
end
|
||||
end
|
||||
|
||||
if CONFIG.enable_user_notifications
|
||||
# Deliver notifications to `/api/v1/auth/notifications`
|
||||
payload = {
|
||||
"topic" => ucid,
|
||||
"videoId" => id,
|
||||
"published" => published.to_unix,
|
||||
}.to_json
|
||||
PG_DB.exec("NOTIFY notifications, E'#{payload}'")
|
||||
end
|
||||
|
||||
video = ChannelVideo.new({
|
||||
id: id,
|
||||
title: title,
|
||||
|
@ -453,11 +439,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
|
||||
|
|
|
@ -48,12 +48,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}"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -12,6 +12,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
|
||||
|
@ -21,7 +22,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,
|
||||
)
|
||||
%>
|
||||
|
||||
|
@ -41,6 +44,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 %>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
<% if params.autoplay %>autoplay<% end %>
|
||||
<% if params.video_loop %>loop<% end %>
|
||||
<% if params.controls %>controls<% end %>>
|
||||
<% if (hlsvp = video.hls_manifest_url) && !CONFIG.disabled?("livestreams") %>
|
||||
<% if (hlsvp = video.hls_manifest_url) && video.live_now && !CONFIG.disabled?("livestreams") %>
|
||||
<source src="<%= URI.parse(hlsvp).request_target %><% if params.local %>?local=true<% end %>" type="application/x-mpegURL" label="livestream">
|
||||
<% else %>
|
||||
<% if params.listen %>
|
||||
|
|
Loading…
Add table
Reference in a new issue