forked from midou/invidious
Compare commits
20 Commits
Author | SHA1 | Date | |
---|---|---|---|
ff5d79e3ee | |||
4ee3ec09df | |||
cfe9d47fa0 | |||
607d6125fc | |||
6215259565 | |||
d034fecc89 | |||
f18d8229c0 | |||
e736626953 | |||
c2c438637a | |||
94638fe42c | |||
55ecfda39a | |||
d97a272aa5 | |||
80a1944b9d | |||
138cf943a9 | |||
c7e672e533 | |||
1b74a04efd | |||
290c7e6009 | |||
e8a56e0fea | |||
1ae7b646b3 | |||
42e2d73ce2 |
@ -322,6 +322,10 @@ input[type="search"]::-webkit-search-cancel-button {
|
||||
order: 6;
|
||||
}
|
||||
|
||||
.vjs-playback-rate > .vjs-menu {
|
||||
width: 50px;
|
||||
}
|
||||
|
||||
.vjs-control-bar {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
@ -1,4 +1,6 @@
|
||||
function get_playlist(plid, retries = 5) {
|
||||
function get_playlist(plid, retries) {
|
||||
if (retries == undefined) retries = 5;
|
||||
|
||||
if (retries <= 0) {
|
||||
console.log('Failed to pull playlist');
|
||||
return;
|
||||
|
@ -1,6 +1,8 @@
|
||||
var notifications, delivered;
|
||||
|
||||
function get_subscriptions(callback, retries = 5) {
|
||||
function get_subscriptions(callback, retries) {
|
||||
if (retries == undefined) retries = 5;
|
||||
|
||||
if (retries <= 0) {
|
||||
return;
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
var options = {
|
||||
preload: 'auto',
|
||||
liveui: true,
|
||||
playbackRates: [0.25, 0.5, 0.75, 1.0, 1.25, 1.5, 2.0],
|
||||
playbackRates: [0.25, 0.5, 0.75, 1.0, 1.25, 1.5, 1.75, 2.0],
|
||||
controlBar: {
|
||||
children: [
|
||||
'playToggle',
|
||||
|
@ -109,7 +109,8 @@ function number_with_separator(val) {
|
||||
return val;
|
||||
}
|
||||
|
||||
function get_playlist(plid, retries = 5) {
|
||||
function get_playlist(plid, retries) {
|
||||
if (retries == undefined) retries = 5;
|
||||
playlist = document.getElementById('playlist');
|
||||
|
||||
if (retries <= 0) {
|
||||
@ -194,7 +195,8 @@ function get_playlist(plid, retries = 5) {
|
||||
xhr.send();
|
||||
}
|
||||
|
||||
function get_reddit_comments(retries = 5) {
|
||||
function get_reddit_comments(retries) {
|
||||
if (retries == undefined) retries = 5;
|
||||
comments = document.getElementById('comments');
|
||||
|
||||
if (retries <= 0) {
|
||||
@ -270,7 +272,8 @@ function get_reddit_comments(retries = 5) {
|
||||
xhr.send();
|
||||
}
|
||||
|
||||
function get_youtube_comments(retries = 5) {
|
||||
function get_youtube_comments(retries) {
|
||||
if (retries == undefined) retries = 5;
|
||||
comments = document.getElementById('comments');
|
||||
|
||||
if (retries <= 0) {
|
||||
|
@ -56,7 +56,7 @@
|
||||
"Play next by default: ": "شغل الفيديو التالى تلقائيا",
|
||||
"Autoplay next video: ": " شغل الفيديو التالى تلقائيا (فى قوائم التشغيل)",
|
||||
"Listen by default: ": "تشغيل النسخة السمعية تلقائى: ",
|
||||
"Proxy videos? ": "عرض الفيديوهات عن طريق الوكيل(proxy) ؟",
|
||||
"Proxy videos: ": "عرض الفيديوهات عن طريق الوكيل(proxy) ؟",
|
||||
"Default speed: ": "السرعة الإفتراضية: ",
|
||||
"Preferred video quality: ": "الجودة المفضلة للفيديوهات: ",
|
||||
"Player volume: ": "صوت المشغل: ",
|
||||
@ -65,13 +65,13 @@
|
||||
"reddit": "Reddit",
|
||||
"Default captions: ": "الترجمات الإفتراضية: ",
|
||||
"Fallback captions: ": "الترجمات المصاحبة: ",
|
||||
"Show related videos? ": "عرض مقاطع الفيديو ذات الصلة؟",
|
||||
"Show annotations by default? ": "عرض الملاحظات فى الفيديو تلقائيا ؟",
|
||||
"Show related videos: ": "عرض مقاطع الفيديو ذات الصلة؟",
|
||||
"Show annotations by default: ": "عرض الملاحظات فى الفيديو تلقائيا ؟",
|
||||
"Visual preferences": "التفضيلات المرئية",
|
||||
"Dark mode: ": "الوضع الليلى: ",
|
||||
"Thin mode: ": "الوضع الخفيف: ",
|
||||
"Subscription preferences": "تفضيلات الإشتراك",
|
||||
"Show annotations by default for subscribed channels? ": "عرض الملاحظات فى الفيديوهات تلقائيا فى القنوات المشترك بها فقط ؟",
|
||||
"Show annotations by default for subscribed channels: ": "عرض الملاحظات فى الفيديوهات تلقائيا فى القنوات المشترك بها فقط ؟",
|
||||
"Redirect homepage to feed: ": "إعادة التوجية من الصفحة الرئيسية لصفحة المشتركين (لرؤية اخر فيديوهات المشتركين): ",
|
||||
"Number of videos shown in feed: ": "عدد الفيديوهات التى ستظهر فى صفحة المشتركين: ",
|
||||
"Sort videos by: ": "ترتيب الفيديو بـ: ",
|
||||
@ -99,11 +99,11 @@
|
||||
"Administrator preferences": "إعدادات المدير",
|
||||
"Default homepage: ": "الصفحة الرئيسية الافتراضية ",
|
||||
"Feed menu: ": "قائمة التغذية",
|
||||
"Top enabled? ": "تفعيل 'الأفضل' ؟ ",
|
||||
"CAPTCHA enabled? ": "تفعيل الكابتشا ؟",
|
||||
"Login enabled? ": "تفعيل تسجيل الدخول ؟",
|
||||
"Registration enabled? ": "تفعيل التسجيل ؟",
|
||||
"Report statistics? ": "إبلاغ الإحصائيات",
|
||||
"Top enabled: ": "تفعيل 'الأفضل' ؟ ",
|
||||
"CAPTCHA enabled: ": "تفعيل الكابتشا ؟",
|
||||
"Login enabled: ": "تفعيل تسجيل الدخول ؟",
|
||||
"Registration enabled: ": "تفعيل التسجيل ؟",
|
||||
"Report statistics: ": "إبلاغ الإحصائيات",
|
||||
"Save preferences": "حفظ التفضيلات",
|
||||
"Subscription manager": "مدير الإشتراكات",
|
||||
"Token manager": "إداره الرمز",
|
||||
@ -136,7 +136,7 @@
|
||||
"Shared `x`": "شارك منذ `x`",
|
||||
"`x` views": "`x` مشاهدون",
|
||||
"Premieres in `x`": "يعرض فى `x`",
|
||||
"Premieres `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": "عرض تعليقات اليوتيوب",
|
||||
"View more comments on Reddit": "عرض المزيد من التعليقات على\\من موقع Reddit",
|
||||
@ -310,12 +310,12 @@
|
||||
"%A %B %-d, %Y": "",
|
||||
"(edited)": "(تم تعديلة)",
|
||||
"YouTube comment permalink": "رابط التعليق على اليوتيوب",
|
||||
"permalink": "",
|
||||
"permalink": "الرابط",
|
||||
"`x` marked it with a ❤": "`x` اعجب بهذا",
|
||||
"Audio mode": "الوضع الصوتى",
|
||||
"Video mode": "وضع الفيديو",
|
||||
"Videos": "الفيديوهات",
|
||||
"Playlists": "قوائم التشغيل",
|
||||
"Community": "",
|
||||
"Community": "المجتمع",
|
||||
"Current version: ": "الإصدار الحالى"
|
||||
}
|
@ -56,7 +56,7 @@
|
||||
"Play next by default: ": "Standardmäßig als nächstes abspielen: ",
|
||||
"Autoplay next video: ": "nächstes Video automatisch abspielen: ",
|
||||
"Listen by default: ": "Nur Ton als Standard: ",
|
||||
"Proxy videos? ": "Proxy-Videos? ",
|
||||
"Proxy videos: ": "Proxy-Videos? ",
|
||||
"Default speed: ": "Standardgeschwindigkeit: ",
|
||||
"Preferred video quality: ": "Bevorzugte Videoqualität: ",
|
||||
"Player volume: ": "Playerlautstärke: ",
|
||||
@ -65,13 +65,13 @@
|
||||
"reddit": "reddit",
|
||||
"Default captions: ": "Standarduntertitel: ",
|
||||
"Fallback captions: ": "Ersatzuntertitel: ",
|
||||
"Show related videos? ": "Ähnliche Videos anzeigen? ",
|
||||
"Show annotations by default? ": "Standardmäßig Anmerkungen anzeigen? ",
|
||||
"Show related videos: ": "Ähnliche Videos anzeigen? ",
|
||||
"Show annotations by default: ": "Standardmäßig Anmerkungen anzeigen? ",
|
||||
"Visual preferences": "Anzeigeeinstellungen",
|
||||
"Dark mode: ": "Nachtmodus: ",
|
||||
"Thin mode: ": "Schlanker Modus: ",
|
||||
"Subscription preferences": "Abonnementeinstellungen",
|
||||
"Show annotations by default for subscribed channels? ": "Anmerkungen für abonnierte Kanäle standardmäßig anzeigen? ",
|
||||
"Show annotations by default for subscribed channels: ": "Anmerkungen für abonnierte Kanäle standardmäßig anzeigen? ",
|
||||
"Redirect homepage to feed: ": "Startseite zu Feed umleiten: ",
|
||||
"Number of videos shown in feed: ": "Anzahl von Videos die im Feed angezeigt werden: ",
|
||||
"Sort videos by: ": "Videos sortieren nach: ",
|
||||
@ -99,11 +99,11 @@
|
||||
"Administrator preferences": "Administratoreinstellungen",
|
||||
"Default homepage: ": "Standard-Homepage: ",
|
||||
"Feed menu: ": "Feed-Menü: ",
|
||||
"Top enabled? ": "Top aktiviert? ",
|
||||
"CAPTCHA enabled? ": "CAPTCHA aktiviert? ",
|
||||
"Login enabled? ": "Login aktiviert? ",
|
||||
"Registration enabled? ": "Registrierung aktiviert? ",
|
||||
"Report statistics? ": "Statistiken berichten? ",
|
||||
"Top enabled: ": "Top aktiviert? ",
|
||||
"CAPTCHA enabled: ": "CAPTCHA aktiviert? ",
|
||||
"Login enabled: ": "Login aktiviert? ",
|
||||
"Registration enabled: ": "Registrierung aktiviert? ",
|
||||
"Report statistics: ": "Statistiken berichten? ",
|
||||
"Save preferences": "Einstellungen speichern",
|
||||
"Subscription manager": "Abonnementverwaltung",
|
||||
"Token manager": "Token-Manager",
|
||||
|
@ -62,7 +62,7 @@
|
||||
"Play next by default: ": "Αναπαραγωγή επόμενου: ",
|
||||
"Autoplay next video: ": "Αυτόματη αναπαραγωγή επόμενου: ",
|
||||
"Listen by default: ": "Φόρτωση μόνο ήχου: ",
|
||||
"Proxy videos? ": "Αναπαραγωγή με διακομιστή μεσολάβησης (proxy): ",
|
||||
"Proxy videos: ": "Αναπαραγωγή με διακομιστή μεσολάβησης (proxy): ",
|
||||
"Default speed: ": "Προεπιλεγμένη ταχύτητα: ",
|
||||
"Preferred video quality: ": "Προτιμώμενη ανάλυση: ",
|
||||
"Player volume: ": "Ένταση αναπαραγωγής: ",
|
||||
@ -71,13 +71,13 @@
|
||||
"reddit": "reddit",
|
||||
"Default captions: ": "Προεπιλεγμένοι υπότιτλοι: ",
|
||||
"Fallback captions: ": "Εναλλακτικοί υπότιτλοι: ",
|
||||
"Show related videos? ": "Προβολή σχετικών βίντεο; ",
|
||||
"Show annotations by default? ": "Αυτόματη προβολή σημειώσεων; :",
|
||||
"Show related videos: ": "Προβολή σχετικών βίντεο; ",
|
||||
"Show annotations by default: ": "Αυτόματη προβολή σημειώσεων; :",
|
||||
"Visual preferences": "Προτιμήσεις εμφάνισης",
|
||||
"Dark mode: ": "Σκοτεινή λειτουργία: ",
|
||||
"Thin mode: ": "Ελαφριά λειτουργία: ",
|
||||
"Subscription preferences": "Προτιμήσεις συνδρομών",
|
||||
"Show annotations by default for subscribed channels? ": "Προβολή σημειώσεων μόνο για κανάλια στα οποία είστε συνδρομητής; ",
|
||||
"Show annotations by default for subscribed channels: ": "Προβολή σημειώσεων μόνο για κανάλια στα οποία είστε συνδρομητής; ",
|
||||
"Redirect homepage to feed: ": "Ανακατεύθυνση αρχικής στη ροή συνδρομών: ",
|
||||
"Number of videos shown in feed: ": "Αριθμός βίντεο ανά σελίδα ροής συνδρομών: ",
|
||||
"Sort videos by: ": "Ταξινόμηση ανά: ",
|
||||
@ -105,11 +105,11 @@
|
||||
"Administrator preferences": "Προτιμήσεις διαχειριστή",
|
||||
"Default homepage: ": "Προεπιλεγμένη αρχική: ",
|
||||
"Feed menu: ": "Μενού ροής συνδρομών: ",
|
||||
"Top enabled? ": "Ενεργοποίηση κορυφαίων; ",
|
||||
"CAPTCHA enabled? ": "Ενεργοποίηση CAPTCHA; ",
|
||||
"Login enabled? ": "Ενεργοποίηση σύνδεσης; ",
|
||||
"Registration enabled? ": "Ενεργοποίηση εγγραφής; ",
|
||||
"Report statistics? ": "Αναφορά στατιστικών; ",
|
||||
"Top enabled: ": "Ενεργοποίηση κορυφαίων; ",
|
||||
"CAPTCHA enabled: ": "Ενεργοποίηση CAPTCHA; ",
|
||||
"Login enabled: ": "Ενεργοποίηση σύνδεσης; ",
|
||||
"Registration enabled: ": "Ενεργοποίηση εγγραφής; ",
|
||||
"Report statistics: ": "Αναφορά στατιστικών; ",
|
||||
"Save preferences": "Αποθήκευση προτιμήσεων",
|
||||
"Subscription manager": "Διαχειριστής συνδρομών",
|
||||
"Token manager": "Διαχειριστής διασυνδέσεων",
|
||||
|
@ -62,7 +62,7 @@
|
||||
"Play next by default: ": "Play next by default: ",
|
||||
"Autoplay next video: ": "Autoplay next video: ",
|
||||
"Listen by default: ": "Listen by default: ",
|
||||
"Proxy videos? ": "Proxy videos? ",
|
||||
"Proxy videos: ": "Proxy videos: ",
|
||||
"Default speed: ": "Default speed: ",
|
||||
"Preferred video quality: ": "Preferred video quality: ",
|
||||
"Player volume: ": "Player volume: ",
|
||||
@ -71,13 +71,13 @@
|
||||
"reddit": "reddit",
|
||||
"Default captions: ": "Default captions: ",
|
||||
"Fallback captions: ": "Fallback captions: ",
|
||||
"Show related videos? ": "Show related videos? ",
|
||||
"Show annotations by default? ": "Show annotations by default? ",
|
||||
"Show related videos: ": "Show related videos: ",
|
||||
"Show annotations by default: ": "Show annotations by default: ",
|
||||
"Visual preferences": "Visual preferences",
|
||||
"Dark mode: ": "Dark mode: ",
|
||||
"Thin mode: ": "Thin mode: ",
|
||||
"Subscription preferences": "Subscription preferences",
|
||||
"Show annotations by default for subscribed channels? ": "Show annotations by default for subscribed channels? ",
|
||||
"Show annotations by default for subscribed channels: ": "Show annotations by default for subscribed channels? ",
|
||||
"Redirect homepage to feed: ": "Redirect homepage to feed: ",
|
||||
"Number of videos shown in feed: ": "Number of videos shown in feed: ",
|
||||
"Sort videos by: ": "Sort videos by: ",
|
||||
@ -105,11 +105,11 @@
|
||||
"Administrator preferences": "Administrator preferences",
|
||||
"Default homepage: ": "Default homepage: ",
|
||||
"Feed menu: ": "Feed menu: ",
|
||||
"Top enabled? ": "Top enabled? ",
|
||||
"CAPTCHA enabled? ": "CAPTCHA enabled? ",
|
||||
"Login enabled? ": "Login enabled? ",
|
||||
"Registration enabled? ": "Registration enabled? ",
|
||||
"Report statistics? ": "Report statistics? ",
|
||||
"Top enabled: ": "Top enabled: ",
|
||||
"CAPTCHA enabled: ": "CAPTCHA enabled: ",
|
||||
"Login enabled: ": "Login enabled? ",
|
||||
"Registration enabled: ": "Registration enabled? ",
|
||||
"Report statistics: ": "Report statistics? ",
|
||||
"Save preferences": "Save preferences",
|
||||
"Subscription manager": "Subscription manager",
|
||||
"Token manager": "Token manager",
|
||||
|
@ -56,7 +56,7 @@
|
||||
"Play next by default: ": "Ludi sekvan defaŭlte: ",
|
||||
"Autoplay next video: ": "Aŭtomate ludi sekvan videon: ",
|
||||
"Listen by default: ": "Aŭskulti defaŭlte: ",
|
||||
"Proxy videos? ": "Ĉu uzi prokuran servilon por videoj? ",
|
||||
"Proxy videos: ": "Ĉu uzi prokuran servilon por videoj? ",
|
||||
"Default speed: ": "Defaŭlta rapido: ",
|
||||
"Preferred video quality: ": "Preferita videkvalito: ",
|
||||
"Player volume: ": "Ludila sonforteco: ",
|
||||
@ -65,13 +65,13 @@
|
||||
"reddit": "reddit",
|
||||
"Default captions: ": "Defaŭltaj subtekstoj: ",
|
||||
"Fallback captions: ": "Retrodefaŭltaj subtekstoj: ",
|
||||
"Show related videos? ": "Ĉu montri rilatajn videojn? ",
|
||||
"Show annotations by default? ": "Ĉu montri prinotojn defaŭlte? ",
|
||||
"Show related videos: ": "Ĉu montri rilatajn videojn? ",
|
||||
"Show annotations by default: ": "Ĉu montri prinotojn defaŭlte? ",
|
||||
"Visual preferences": "Vidaj preferoj",
|
||||
"Dark mode: ": "Malhela reĝimo: ",
|
||||
"Thin mode: ": "Maldika reĝimo: ",
|
||||
"Subscription preferences": "Abonaj agordoj",
|
||||
"Show annotations by default for subscribed channels? ": "Ĉu montri prinotojn defaŭlte por abonitaj kanaloj? ",
|
||||
"Show annotations by default for subscribed channels: ": "Ĉu montri prinotojn defaŭlte por abonitaj kanaloj? ",
|
||||
"Redirect homepage to feed: ": "Alidirekti hejmpâgon al fluo: ",
|
||||
"Number of videos shown in feed: ": "Nombro da videoj montritaj en fluo: ",
|
||||
"Sort videos by: ": "Ordi videojn laŭ: ",
|
||||
@ -99,11 +99,11 @@
|
||||
"Administrator preferences": "Agordoj de administranto",
|
||||
"Default homepage: ": "Defaŭlta hejmpaĝo: ",
|
||||
"Feed menu: ": "Flua menuo: ",
|
||||
"Top enabled? ": "Ĉu pli bonaj ŝaltitaj? ",
|
||||
"CAPTCHA enabled? ": "Ĉu CAPTCHA ŝaltita? ",
|
||||
"Login enabled? ": "Ĉu ensaluto aktivita? ",
|
||||
"Registration enabled? ": "Ĉu registriĝo aktivita? ",
|
||||
"Report statistics? ": "Ĉu raporti statistikojn? ",
|
||||
"Top enabled: ": "Ĉu pli bonaj ŝaltitaj? ",
|
||||
"CAPTCHA enabled: ": "Ĉu CAPTCHA ŝaltita? ",
|
||||
"Login enabled: ": "Ĉu ensaluto aktivita? ",
|
||||
"Registration enabled: ": "Ĉu registriĝo aktivita? ",
|
||||
"Report statistics: ": "Ĉu raporti statistikojn? ",
|
||||
"Save preferences": "Konservi agordojn",
|
||||
"Subscription manager": "Administrilo de abonoj",
|
||||
"Token manager": "Ĵetona administrilo",
|
||||
@ -318,4 +318,4 @@
|
||||
"Playlists": "Ludlistoj",
|
||||
"Community": "Komunumo",
|
||||
"Current version: ": "Nuna versio: "
|
||||
}
|
||||
}
|
@ -56,7 +56,7 @@
|
||||
"Play next by default: ": "Reproducir siguiente por defecto: ",
|
||||
"Autoplay next video: ": "Reproducir automáticamente el vídeo siguiente: ",
|
||||
"Listen by default: ": "Activar el sonido por defecto: ",
|
||||
"Proxy videos? ": "¿Usar un proxy para los vídeos? ",
|
||||
"Proxy videos: ": "¿Usar un proxy para los vídeos? ",
|
||||
"Default speed: ": "Velocidad por defecto: ",
|
||||
"Preferred video quality: ": "Calidad de vídeo preferida: ",
|
||||
"Player volume: ": "Volumen del reproductor: ",
|
||||
@ -65,13 +65,13 @@
|
||||
"reddit": "Reddit",
|
||||
"Default captions: ": "Subtítulos por defecto: ",
|
||||
"Fallback captions: ": "Subtítulos alternativos: ",
|
||||
"Show related videos? ": "¿Mostrar vídeos relacionados? ",
|
||||
"Show annotations by default? ": "¿Mostrar anotaciones por defecto? ",
|
||||
"Show related videos: ": "¿Mostrar vídeos relacionados? ",
|
||||
"Show annotations by default: ": "¿Mostrar anotaciones por defecto? ",
|
||||
"Visual preferences": "Preferencias visuales",
|
||||
"Dark mode: ": "Modo oscuro: ",
|
||||
"Thin mode: ": "Modo compacto: ",
|
||||
"Subscription preferences": "Preferencias de la suscripción",
|
||||
"Show annotations by default for subscribed channels? ": "¿Mostrar anotaciones por defecto para los canales suscritos? ",
|
||||
"Show annotations by default for subscribed channels: ": "¿Mostrar anotaciones por defecto para los canales suscritos? ",
|
||||
"Redirect homepage to feed: ": "Redirigir la página de inicio a la fuente: ",
|
||||
"Number of videos shown in feed: ": "Número de vídeos mostrados en la fuente: ",
|
||||
"Sort videos by: ": "Ordenar los vídeos por: ",
|
||||
@ -99,11 +99,11 @@
|
||||
"Administrator preferences": "Preferencias de administrador",
|
||||
"Default homepage: ": "Página de inicio por defecto: ",
|
||||
"Feed menu: ": "Menú de fuentes: ",
|
||||
"Top enabled? ": "¿Habilitar los destacados? ",
|
||||
"CAPTCHA enabled? ": "¿Habilitar los CAPTCHA? ",
|
||||
"Login enabled? ": "¿Habilitar el inicio de sesión? ",
|
||||
"Registration enabled? ": "¿Habilitar el registro? ",
|
||||
"Report statistics? ": "¿Enviar estadísticas? ",
|
||||
"Top enabled: ": "¿Habilitar los destacados? ",
|
||||
"CAPTCHA enabled: ": "¿Habilitar los CAPTCHA? ",
|
||||
"Login enabled: ": "¿Habilitar el inicio de sesión? ",
|
||||
"Registration enabled: ": "¿Habilitar el registro? ",
|
||||
"Report statistics: ": "¿Enviar estadísticas? ",
|
||||
"Save preferences": "Guardar las preferencias",
|
||||
"Subscription manager": "Gestor de suscripciones",
|
||||
"Token manager": "Gestor de tokens",
|
||||
|
@ -56,7 +56,7 @@
|
||||
"Play next by default: ": "",
|
||||
"Autoplay next video: ": "",
|
||||
"Listen by default: ": "",
|
||||
"Proxy videos? ": "",
|
||||
"Proxy videos: ": "",
|
||||
"Default speed: ": "",
|
||||
"Preferred video quality: ": "",
|
||||
"Player volume: ": "",
|
||||
@ -65,13 +65,13 @@
|
||||
"reddit": "",
|
||||
"Default captions: ": "",
|
||||
"Fallback captions: ": "",
|
||||
"Show related videos? ": "",
|
||||
"Show annotations by default? ": "",
|
||||
"Show related videos: ": "",
|
||||
"Show annotations by default: ": "",
|
||||
"Visual preferences": "",
|
||||
"Dark mode: ": "",
|
||||
"Thin mode: ": "",
|
||||
"Subscription preferences": "",
|
||||
"Show annotations by default for subscribed channels? ": "",
|
||||
"Show annotations by default for subscribed channels: ": "",
|
||||
"Redirect homepage to feed: ": "",
|
||||
"Number of videos shown in feed: ": "",
|
||||
"Sort videos by: ": "",
|
||||
@ -99,11 +99,11 @@
|
||||
"Administrator preferences": "",
|
||||
"Default homepage: ": "",
|
||||
"Feed menu: ": "",
|
||||
"Top enabled? ": "",
|
||||
"CAPTCHA enabled? ": "",
|
||||
"Login enabled? ": "",
|
||||
"Registration enabled? ": "",
|
||||
"Report statistics? ": "",
|
||||
"Top enabled: ": "",
|
||||
"CAPTCHA enabled: ": "",
|
||||
"Login enabled: ": "",
|
||||
"Registration enabled: ": "",
|
||||
"Report statistics: ": "",
|
||||
"Save preferences": "",
|
||||
"Subscription manager": "",
|
||||
"Token manager": "",
|
||||
|
@ -56,7 +56,7 @@
|
||||
"Play next by default: ": "Jouer suirvante par défaut : ",
|
||||
"Autoplay next video: ": "Lire automatiquement la vidéo suivante : ",
|
||||
"Listen by default: ": "Audio uniquement : ",
|
||||
"Proxy videos? ": "Charger les vidéos à travers un proxy ? ",
|
||||
"Proxy videos: ": "Charger les vidéos à travers un proxy ? ",
|
||||
"Default speed: ": "Vitesse par défaut : ",
|
||||
"Preferred video quality: ": "Qualité vidéo souhaitée : ",
|
||||
"Player volume: ": "Volume du lecteur : ",
|
||||
@ -65,13 +65,13 @@
|
||||
"reddit": "Reddit",
|
||||
"Default captions: ": "Sous-titres par défaut : ",
|
||||
"Fallback captions: ": "Sous-titres de repli : ",
|
||||
"Show related videos? ": "Voir les vidéos liées ? ",
|
||||
"Show annotations by default? ": "Voir les annotations par défaut ? ",
|
||||
"Show related videos: ": "Voir les vidéos liées ? ",
|
||||
"Show annotations by default: ": "Voir les annotations par défaut ? ",
|
||||
"Visual preferences": "Préférences du site",
|
||||
"Dark mode: ": "Mode Sombre : ",
|
||||
"Thin mode: ": "Mode Simplifié : ",
|
||||
"Subscription preferences": "Préférences de la page d'abonnements",
|
||||
"Show annotations by default for subscribed channels? ": "Voir les annotations par défaut sur les chaînes suivies ? ",
|
||||
"Show annotations by default for subscribed channels: ": "Voir les annotations par défaut sur les chaînes suivies ? ",
|
||||
"Redirect homepage to feed: ": "Rediriger la page d'accueil vers la page d'abonnements : ",
|
||||
"Number of videos shown in feed: ": "Nombre de vidéos montrées dans la page d'abonnements : ",
|
||||
"Sort videos by: ": "Trier les vidéos par : ",
|
||||
@ -99,11 +99,11 @@
|
||||
"Administrator preferences": "Préferences d'Administrateur",
|
||||
"Default homepage: ": "Page d'accueil par défaut : ",
|
||||
"Feed menu: ": "Menu des Flux : ",
|
||||
"Top enabled? ": "Top activé ? ",
|
||||
"CAPTCHA enabled? ": "CAPTCHA activé ? ",
|
||||
"Login enabled? ": "Connexion activé ? ",
|
||||
"Registration enabled? ": "Inscription activée ? ",
|
||||
"Report statistics? ": "Télémétrie activé ? ",
|
||||
"Top enabled: ": "Top activé ? ",
|
||||
"CAPTCHA enabled: ": "CAPTCHA activé ? ",
|
||||
"Login enabled: ": "Connexion activé ? ",
|
||||
"Registration enabled: ": "Inscription activée ? ",
|
||||
"Report statistics: ": "Télémétrie activé ? ",
|
||||
"Save preferences": "Enregistrer les préférences",
|
||||
"Subscription manager": "Gestionnaire d'abonnement",
|
||||
"Token manager": "Gestionnaire de tokens",
|
||||
|
464
locales/is.json
464
locales/is.json
@ -4,7 +4,7 @@
|
||||
"LIVE": "BEINT",
|
||||
"Shared `x` ago": "Deilt `x` síðan",
|
||||
"Unsubscribe": "Afskrá",
|
||||
"Subscribe": "Gerast áskrifandi",
|
||||
"Subscribe": "Áskrifa",
|
||||
"View channel on YouTube": "Skoða rás á YouTube",
|
||||
"View playlist on YouTube": "Skoða spilunarlisti á YouTube",
|
||||
"newest": "nýjasta",
|
||||
@ -56,7 +56,7 @@
|
||||
"Play next by default: ": "Spila næst sjálfgefið: ",
|
||||
"Autoplay next video: ": "Spila næst sjálfkrafa: ",
|
||||
"Listen by default: ": "Hlusta sjálfgefið: ",
|
||||
"Proxy videos? ": "",
|
||||
"Proxy videos: ": "Proxy myndbönd? ",
|
||||
"Default speed: ": "Sjálfgefinn hraði: ",
|
||||
"Preferred video quality: ": "Æskilegt myndbands gæði: ",
|
||||
"Player volume: ": "Spilara bindi: ",
|
||||
@ -65,15 +65,15 @@
|
||||
"reddit": "reddit",
|
||||
"Default captions: ": "Sjálfgefin texti: ",
|
||||
"Fallback captions: ": "Varatextar: ",
|
||||
"Show related videos? ": "Sýna tengd myndbönd? ",
|
||||
"Show annotations by default? ": "",
|
||||
"Show related videos: ": "Sýna tengd myndbönd? ",
|
||||
"Show annotations by default: ": "Á að sýna glósur sjálfgefið? ",
|
||||
"Visual preferences": "Sjónrænar stillingar",
|
||||
"Dark mode: ": "Myrkur ham: ",
|
||||
"Thin mode: ": "Þunnt ham: ",
|
||||
"Subscription preferences": "Áskriftarstillingar",
|
||||
"Show annotations by default for subscribed channels? ": "",
|
||||
"Redirect homepage to feed: ": "",
|
||||
"Number of videos shown in feed: ": "",
|
||||
"Show annotations by default for subscribed channels: ": "Á að sýna glósur sjálfgefið fyrir áskriftarrásir? ",
|
||||
"Redirect homepage to feed: ": "Endurbeina heimasíðu að straumi: ",
|
||||
"Number of videos shown in feed: ": "Fjöldi myndbanda sem sýndir eru í straumi: ",
|
||||
"Sort videos by: ": "Raða myndbönd eftir: ",
|
||||
"published": "birt",
|
||||
"published - reverse": "birt - afturábak",
|
||||
@ -86,234 +86,234 @@
|
||||
"Only show unwatched: ": "Sýna aðeins óséð: ",
|
||||
"Only show notifications (if there are any): ": "Sýna aðeins tilkynningar (ef einhverjar eru): ",
|
||||
"Enable web notifications": "Virkja veftilkynningar",
|
||||
"`x` uploaded a video": "' x ' hlóð upp myndband",
|
||||
"`x` is live": "' x ' er í beinni",
|
||||
"`x` uploaded a video": "`x` hlóð upp myndband",
|
||||
"`x` is live": "`x` er í beinni",
|
||||
"Data preferences": "Gagnastillingar",
|
||||
"Clear watch history": "Hreinsa áhorfssögu",
|
||||
"Import/export data": "Flytja inn/út gögn",
|
||||
"Change password": "Breyta lykilorði",
|
||||
"Manage subscriptions": "Stjórna áskriftum",
|
||||
"Manage tokens": "",
|
||||
"Watch history": "",
|
||||
"Delete account": "",
|
||||
"Administrator preferences": "",
|
||||
"Default homepage: ": "",
|
||||
"Feed menu: ": "",
|
||||
"Top enabled? ": "",
|
||||
"CAPTCHA enabled? ": "",
|
||||
"Login enabled? ": "",
|
||||
"Registration enabled? ": "",
|
||||
"Report statistics? ": "",
|
||||
"Save preferences": "",
|
||||
"Subscription manager": "",
|
||||
"Token manager": "",
|
||||
"Token": "",
|
||||
"`x` subscriptions.": "",
|
||||
"`x` tokens.": "",
|
||||
"Import/export": "",
|
||||
"unsubscribe": "",
|
||||
"revoke": "",
|
||||
"Subscriptions": "",
|
||||
"`x` unseen notifications.": "",
|
||||
"search": "",
|
||||
"Log out": "",
|
||||
"Released under the AGPLv3 by Omar Roth.": "",
|
||||
"Source available here.": "",
|
||||
"View JavaScript license information.": "",
|
||||
"View privacy policy.": "",
|
||||
"Trending": "",
|
||||
"Unlisted": "",
|
||||
"Watch on YouTube": "",
|
||||
"Hide annotations": "",
|
||||
"Show annotations": "",
|
||||
"Genre: ": "",
|
||||
"License: ": "",
|
||||
"Family friendly? ": "",
|
||||
"Wilson score: ": "",
|
||||
"Engagement: ": "",
|
||||
"Whitelisted regions: ": "",
|
||||
"Blacklisted regions: ": "",
|
||||
"Shared `x`": "",
|
||||
"`x` views.": "",
|
||||
"Premieres in `x`": "",
|
||||
"Premieres `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": "",
|
||||
"View more comments on Reddit": "",
|
||||
"View `x` comments": "",
|
||||
"View Reddit comments": "",
|
||||
"Hide replies": "",
|
||||
"Show replies": "",
|
||||
"Incorrect password": "",
|
||||
"Quota exceeded, try again in a few hours": "",
|
||||
"Unable to log in, make sure two-factor authentication (Authenticator or SMS) is turned on.": "",
|
||||
"Invalid TFA code": "",
|
||||
"Login failed. This may be because two-factor authentication is not turned on for your account.": "",
|
||||
"Wrong answer": "",
|
||||
"Erroneous CAPTCHA": "",
|
||||
"CAPTCHA is a required field": "",
|
||||
"User ID is a required field": "",
|
||||
"Password is a required field": "",
|
||||
"Wrong username or password": "",
|
||||
"Please sign in using 'Log in with Google'": "",
|
||||
"Password cannot be empty": "",
|
||||
"Password cannot be longer than 55 characters": "",
|
||||
"Please log in": "",
|
||||
"Invidious Private Feed for `x`": "",
|
||||
"channel:`x`": "",
|
||||
"Deleted or invalid channel": "",
|
||||
"This channel does not exist.": "",
|
||||
"Could not get channel info.": "",
|
||||
"Could not fetch comments": "",
|
||||
"View `x` replies.": "",
|
||||
"`x` ago": "",
|
||||
"Load more": "",
|
||||
"`x` points.": "",
|
||||
"Could not create mix.": "",
|
||||
"Empty playlist": "",
|
||||
"Not a playlist.": "",
|
||||
"Playlist does not exist.": "",
|
||||
"Could not pull trending pages.": "",
|
||||
"Hidden field \"challenge\" is a required field": "",
|
||||
"Hidden field \"token\" is a required field": "",
|
||||
"Erroneous challenge": "",
|
||||
"Erroneous token": "",
|
||||
"No such user": "",
|
||||
"Token is expired, please try again": "",
|
||||
"English": "",
|
||||
"English (auto-generated)": "",
|
||||
"Afrikaans": "",
|
||||
"Albanian": "",
|
||||
"Amharic": "",
|
||||
"Arabic": "",
|
||||
"Armenian": "",
|
||||
"Azerbaijani": "",
|
||||
"Bangla": "",
|
||||
"Basque": "",
|
||||
"Belarusian": "",
|
||||
"Bosnian": "",
|
||||
"Bulgarian": "",
|
||||
"Burmese": "",
|
||||
"Catalan": "",
|
||||
"Cebuano": "",
|
||||
"Chinese (Simplified)": "",
|
||||
"Chinese (Traditional)": "",
|
||||
"Corsican": "",
|
||||
"Croatian": "",
|
||||
"Czech": "",
|
||||
"Danish": "",
|
||||
"Dutch": "",
|
||||
"Esperanto": "",
|
||||
"Estonian": "",
|
||||
"Filipino": "",
|
||||
"Finnish": "",
|
||||
"French": "",
|
||||
"Galician": "",
|
||||
"Georgian": "",
|
||||
"German": "",
|
||||
"Greek": "",
|
||||
"Gujarati": "",
|
||||
"Haitian Creole": "",
|
||||
"Hausa": "",
|
||||
"Hawaiian": "",
|
||||
"Hebrew": "",
|
||||
"Hindi": "",
|
||||
"Hmong": "",
|
||||
"Hungarian": "",
|
||||
"Icelandic": "",
|
||||
"Igbo": "",
|
||||
"Indonesian": "",
|
||||
"Irish": "",
|
||||
"Italian": "",
|
||||
"Japanese": "",
|
||||
"Javanese": "",
|
||||
"Kannada": "",
|
||||
"Kazakh": "",
|
||||
"Khmer": "",
|
||||
"Korean": "",
|
||||
"Kurdish": "",
|
||||
"Kyrgyz": "",
|
||||
"Lao": "",
|
||||
"Latin": "",
|
||||
"Latvian": "",
|
||||
"Lithuanian": "",
|
||||
"Luxembourgish": "",
|
||||
"Macedonian": "",
|
||||
"Malagasy": "",
|
||||
"Malay": "",
|
||||
"Malayalam": "",
|
||||
"Maltese": "",
|
||||
"Maori": "",
|
||||
"Marathi": "",
|
||||
"Mongolian": "",
|
||||
"Nepali": "",
|
||||
"Norwegian Bokmål": "",
|
||||
"Nyanja": "",
|
||||
"Pashto": "",
|
||||
"Persian": "",
|
||||
"Polish": "",
|
||||
"Portuguese": "",
|
||||
"Punjabi": "",
|
||||
"Romanian": "",
|
||||
"Russian": "",
|
||||
"Samoan": "",
|
||||
"Scottish Gaelic": "",
|
||||
"Serbian": "",
|
||||
"Shona": "",
|
||||
"Sindhi": "",
|
||||
"Sinhala": "",
|
||||
"Slovak": "",
|
||||
"Slovenian": "",
|
||||
"Somali": "",
|
||||
"Southern Sotho": "",
|
||||
"Spanish": "",
|
||||
"Spanish (Latin America)": "",
|
||||
"Sundanese": "",
|
||||
"Swahili": "",
|
||||
"Swedish": "",
|
||||
"Tajik": "",
|
||||
"Tamil": "",
|
||||
"Telugu": "",
|
||||
"Thai": "",
|
||||
"Turkish": "",
|
||||
"Ukrainian": "",
|
||||
"Urdu": "",
|
||||
"Uzbek": "",
|
||||
"Vietnamese": "",
|
||||
"Welsh": "",
|
||||
"Western Frisian": "",
|
||||
"Xhosa": "",
|
||||
"Yiddish": "",
|
||||
"Yoruba": "",
|
||||
"Zulu": "",
|
||||
"`x` years.": "",
|
||||
"`x` months.": "",
|
||||
"`x` weeks.": "",
|
||||
"`x` days.": "",
|
||||
"`x` hours.": "",
|
||||
"`x` minutes.": "",
|
||||
"`x` seconds.": "",
|
||||
"Fallback comments: ": "",
|
||||
"Popular": "",
|
||||
"Top": "",
|
||||
"About": "",
|
||||
"Rating: ": "",
|
||||
"Language: ": "",
|
||||
"View as playlist": "",
|
||||
"Default": "",
|
||||
"Music": "",
|
||||
"Gaming": "",
|
||||
"News": "",
|
||||
"Movies": "",
|
||||
"Download": "",
|
||||
"Download as: ": "",
|
||||
"%A %B %-d, %Y": "",
|
||||
"(edited)": "",
|
||||
"YouTube comment permalink": "",
|
||||
"`x` marked it with a ❤": "",
|
||||
"Audio mode": "",
|
||||
"Video mode": "",
|
||||
"Videos": "",
|
||||
"Playlists": "",
|
||||
"Current version: ": ""
|
||||
"Manage tokens": "Stjórna tákn",
|
||||
"Watch history": "Áhorfssögu",
|
||||
"Delete account": "Eyða reikningi",
|
||||
"Administrator preferences": "Kjörstillingar stjórnanda",
|
||||
"Default homepage: ": "Sjálfgefin heimasíða: ",
|
||||
"Feed menu: ": "Straum valmynd: ",
|
||||
"Top enabled: ": "Toppur virkur? ",
|
||||
"CAPTCHA enabled: ": "CAPTCHA virk? ",
|
||||
"Login enabled: ": "Innskráning virk? ",
|
||||
"Registration enabled: ": "Nýskráning virkjuð? ",
|
||||
"Report statistics: ": "Skrá talnagögn? ",
|
||||
"Save preferences": "Vista stillingar",
|
||||
"Subscription manager": "Áskriftarstjóri",
|
||||
"Token manager": "Táknstjóri",
|
||||
"Token": "Tákn",
|
||||
"`x` subscriptions.": "`x` áskriftir.",
|
||||
"`x` tokens.": "`x` tákn.",
|
||||
"Import/export": "Flytja inn/út",
|
||||
"unsubscribe": "afskrá",
|
||||
"revoke": "afturkalla",
|
||||
"Subscriptions": "Áskriftir",
|
||||
"`x` unseen notifications.": "`x` óséðar tilkynningar.",
|
||||
"search": "leita",
|
||||
"Log out": "Útskrá",
|
||||
"Released under the AGPLv3 by Omar Roth.": "Útgefið undir AGPLv3 eftir Omar Roth.",
|
||||
"Source available here.": "Frumkóði aðgengilegur hér.",
|
||||
"View JavaScript license information.": "Skoða JavaScript leyfisupplýsingar.",
|
||||
"View privacy policy.": "Skoða meðferð persónuupplýsinga.",
|
||||
"Trending": "Vinsælt",
|
||||
"Unlisted": "Óskráð",
|
||||
"Watch on YouTube": "Horfa á YouTube",
|
||||
"Hide annotations": "Fela glósur",
|
||||
"Show annotations": "Sýna glósur",
|
||||
"Genre: ": "Tegund: ",
|
||||
"License: ": "Notkunarleyfi: ",
|
||||
"Family friendly? ": "Fjölskylduvænt? ",
|
||||
"Wilson score: ": "Wilson stig: ",
|
||||
"Engagement: ": "Þátttöku: ",
|
||||
"Whitelisted regions: ": "Svæði á hvítum lista: ",
|
||||
"Blacklisted regions: ": "Svæði á svörtum lista: ",
|
||||
"Shared `x`": "Deilt `x`",
|
||||
"`x` views.": "`x` áhorf.",
|
||||
"Premieres in `x`": "Frumflutt eftir `x`",
|
||||
"Premieres `x`": "Frumflutt `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.": "Hæ! Lítur út eins og þú hafir slökkt á JavaScript. Smelltu hér til að skoða ummæli, hafðu í huga að þær geta tekið aðeins lengri tíma að hlaða.",
|
||||
"View YouTube comments": "Skoða YouTube ummæli",
|
||||
"View more comments on Reddit": "Skoða fleiri ummæli á Reddit",
|
||||
"View `x` comments": "Skoða `x` ummæli",
|
||||
"View Reddit comments": "Skoða Reddit ummæli",
|
||||
"Hide replies": "Fela svör",
|
||||
"Show replies": "Sýna svör",
|
||||
"Incorrect password": "Rangt lykilorð",
|
||||
"Quota exceeded, try again in a few hours": "Kvóti fór yfir, reyndu aftur eftir nokkrar klukkustundir",
|
||||
"Unable to log in, make sure two-factor authentication (Authenticator or SMS) is turned on.": "Ekki er hægt að skrá þig inn, vertu viss um að tvíþætt staðfesting (Authenticator eða SMS) sé kveikt á.",
|
||||
"Invalid TFA code": "Ógildur TFA kóði",
|
||||
"Login failed. This may be because two-factor authentication is not turned on for your account.": "Innskráning mistókst. Þetta gæti verið vegna þess að tvíþátta staðfesting er ekki kveikt á reikningnum þínum.",
|
||||
"Wrong answer": "Rangt svar",
|
||||
"Erroneous CAPTCHA": "Rangt CAPTCHA",
|
||||
"CAPTCHA is a required field": "CAPTCHA er nauðsynlegur reitur",
|
||||
"User ID is a required field": "Notandakenni er nauðsynlegur reitur",
|
||||
"Password is a required field": "Lykilorð er nauðsynlegur reitur",
|
||||
"Wrong username or password": "Rangt notandanafn eða lykilorð",
|
||||
"Please sign in using 'Log in with Google'": "Vinsamlegast skráðu þig inn með því að nota 'Innskráning með Google'",
|
||||
"Password cannot be empty": "Lykilorð má ekki vera autt",
|
||||
"Password cannot be longer than 55 characters": "Lykilorð má ekki vera lengra en 55 stafir",
|
||||
"Please log in": "Vinsamlegast skráðu þig inn",
|
||||
"Invidious Private Feed for `x`": "Invidious Persónulegur Straumur fyrir `x`",
|
||||
"channel:`x`": "rás:`x`",
|
||||
"Deleted or invalid channel": "Eytt eða ógild rás",
|
||||
"This channel does not exist.": "Þessi rás er ekki til.",
|
||||
"Could not get channel info.": "Ekki tókst að fá rásarupplýsingar.",
|
||||
"Could not fetch comments": "Ekki tókst að sækja ummæli",
|
||||
"View `x` replies.": "Skoða `x` svör.",
|
||||
"`x` ago": "' x ' síðan",
|
||||
"Load more": "Hlaða meira",
|
||||
"`x` points.": "`x` stig.",
|
||||
"Could not create mix.": "Ekki tókst að búa til blöndu.",
|
||||
"Empty playlist": "Tómur spilunarlisti",
|
||||
"Not a playlist.": "Ekki spilunarlisti.",
|
||||
"Playlist does not exist.": "Spilunarlisti er ekki til.",
|
||||
"Could not pull trending pages.": "Ekki tókst að draga vinsællar síður.",
|
||||
"Hidden field \"challenge\" is a required field": "Falinn reitur \"áskorun\" er nauðsynlegur reitur",
|
||||
"Hidden field \"token\" is a required field": "Falinn reitur \"tákn\" er nauðsynlegur reitur",
|
||||
"Erroneous challenge": "Röng áskorun",
|
||||
"Erroneous token": "Rangt tákn",
|
||||
"No such user": "Enginn slíkur notandi",
|
||||
"Token is expired, please try again": "Tákn er útrunnið, vinsamlegast reyndu aftur",
|
||||
"English": "Enska",
|
||||
"English (auto-generated)": "Enska (sjálfkrafa)",
|
||||
"Afrikaans": "Afríkanska",
|
||||
"Albanian": "Albanska",
|
||||
"Amharic": "Amharíska",
|
||||
"Arabic": "Arabíska",
|
||||
"Armenian": "Armenska",
|
||||
"Azerbaijani": "Aserbaídsjanska",
|
||||
"Bangla": "Bangla",
|
||||
"Basque": "Baskneska",
|
||||
"Belarusian": "Hvítrússneska",
|
||||
"Bosnian": "Bosníska",
|
||||
"Bulgarian": "Búlgarska",
|
||||
"Burmese": "Búrmíska",
|
||||
"Catalan": "Katalónska",
|
||||
"Cebuano": "Cebúanó",
|
||||
"Chinese (Simplified)": "Kínverska (Einfölduð)",
|
||||
"Chinese (Traditional)": "Kínverska (Hefðbundin)",
|
||||
"Corsican": "Korsíska",
|
||||
"Croatian": "Króatíska",
|
||||
"Czech": "Tékkneska",
|
||||
"Danish": "Danska",
|
||||
"Dutch": "Hollenska",
|
||||
"Esperanto": "Esperantó",
|
||||
"Estonian": "Eistneska",
|
||||
"Filipino": "Filippínska",
|
||||
"Finnish": "Finnska",
|
||||
"French": "Franska",
|
||||
"Galician": "Galisíska",
|
||||
"Georgian": "Georgíska",
|
||||
"German": "Þýska",
|
||||
"Greek": "Gríska",
|
||||
"Gujarati": "Gújaratí",
|
||||
"Haitian Creole": "Haítískt Kreólamál",
|
||||
"Hausa": "Hausa",
|
||||
"Hawaiian": "Havaíska",
|
||||
"Hebrew": "Hebreska",
|
||||
"Hindi": "Hindí",
|
||||
"Hmong": "Hmong",
|
||||
"Hungarian": "Ungverska",
|
||||
"Icelandic": "Íslenska",
|
||||
"Igbo": "Igbo",
|
||||
"Indonesian": "Indónesíska",
|
||||
"Irish": "Írska",
|
||||
"Italian": "Ítalska",
|
||||
"Japanese": "Japanska",
|
||||
"Javanese": "Javanska",
|
||||
"Kannada": "Kanaríska",
|
||||
"Kazakh": "Kasakíska",
|
||||
"Khmer": "Khmeríska",
|
||||
"Korean": "Kóreska",
|
||||
"Kurdish": "Kúrdíska",
|
||||
"Kyrgyz": "Kirgisíska",
|
||||
"Lao": "Laó",
|
||||
"Latin": "Latína",
|
||||
"Latvian": "Lettneska",
|
||||
"Lithuanian": "Litháíska",
|
||||
"Luxembourgish": "Lúxemborgíska",
|
||||
"Macedonian": "Makedóníska",
|
||||
"Malagasy": "Malagasíska",
|
||||
"Malay": "Malaíska",
|
||||
"Malayalam": "Malaíalam",
|
||||
"Maltese": "Maltneska",
|
||||
"Maori": "Maórí",
|
||||
"Marathi": "Marathi",
|
||||
"Mongolian": "Mongólska",
|
||||
"Nepali": "Nepalska",
|
||||
"Norwegian Bokmål": "Norskt bókmál",
|
||||
"Nyanja": "Nyanja",
|
||||
"Pashto": "Pashto",
|
||||
"Persian": "Persneska",
|
||||
"Polish": "Pólska",
|
||||
"Portuguese": "Portúgalska",
|
||||
"Punjabi": "Punjabi",
|
||||
"Romanian": "Rúmenska",
|
||||
"Russian": "Rússneska",
|
||||
"Samoan": "Samóíska",
|
||||
"Scottish Gaelic": "Skosk Gelíska",
|
||||
"Serbian": "Serbneska",
|
||||
"Shona": "Shona",
|
||||
"Sindhi": "Sindí",
|
||||
"Sinhala": "Sinhala",
|
||||
"Slovak": "Slóvakíska",
|
||||
"Slovenian": "Slóvenska",
|
||||
"Somali": "Sómalska",
|
||||
"Southern Sotho": "Suður Sótó",
|
||||
"Spanish": "Spænska",
|
||||
"Spanish (Latin America)": "Spænska (Rómönsku Ameríka)",
|
||||
"Sundanese": "Sundaneska",
|
||||
"Swahili": "Svahílí",
|
||||
"Swedish": "Sænska",
|
||||
"Tajik": "Tadsikíska",
|
||||
"Tamil": "Tamílska",
|
||||
"Telugu": "Telúgú",
|
||||
"Thai": "Taílenska",
|
||||
"Turkish": "Tyrkneska",
|
||||
"Ukrainian": "Úkraníska",
|
||||
"Urdu": "Úrdú",
|
||||
"Uzbek": "Úsbekíska",
|
||||
"Vietnamese": "Víetnamska",
|
||||
"Welsh": "Velska",
|
||||
"Western Frisian": "Vestur Frísneska",
|
||||
"Xhosa": "Xhosa",
|
||||
"Yiddish": "Jiddíska",
|
||||
"Yoruba": "Jórúba",
|
||||
"Zulu": "Zúlú",
|
||||
"`x` years.": "' x ' ár.",
|
||||
"`x` months.": "' x ' mánuði.",
|
||||
"`x` weeks.": "`x` vikur.",
|
||||
"`x` days.": "' x ' dagar.",
|
||||
"`x` hours.": "`x` klukkustundir.",
|
||||
"`x` minutes.": "`x` mínútur.",
|
||||
"`x` seconds.": "`x` sekúndur.",
|
||||
"Fallback comments: ": "Vara ummæli: ",
|
||||
"Popular": "Vinsællt",
|
||||
"Top": "Topp",
|
||||
"About": "Um",
|
||||
"Rating: ": "Einkunn: ",
|
||||
"Language: ": "Tungumál: ",
|
||||
"View as playlist": "Skoða sem spilunarlista",
|
||||
"Default": "Sjálfgefið",
|
||||
"Music": "Tónlist",
|
||||
"Gaming": "Tólvuleikja",
|
||||
"News": "Fréttir",
|
||||
"Movies": "Kvikmyndir",
|
||||
"Download": "Niðurhal",
|
||||
"Download as: ": "Niðurhala sem: ",
|
||||
"%A %B %-d, %Y": "%A %B %-d, %Y",
|
||||
"(edited)": "(breytt)",
|
||||
"YouTube comment permalink": "YouTube ummæli varanlegur tengill",
|
||||
"`x` marked it with a ❤": "`x` merkti það með ❤",
|
||||
"Audio mode": "Hljóð ham",
|
||||
"Video mode": "Myndband ham",
|
||||
"Videos": "Myndbönd",
|
||||
"Playlists": "Spilunarlistar",
|
||||
"Current version: ": "Núverandi útgáfa: "
|
||||
}
|
@ -56,7 +56,7 @@
|
||||
"Play next by default: ": "Riproduzione successiva per impostazione predefinita: ",
|
||||
"Autoplay next video: ": "Riproduci automaticamente il prossimo video: ",
|
||||
"Listen by default: ": "Modalità solo audio come predefinita: ",
|
||||
"Proxy videos? ": "",
|
||||
"Proxy videos: ": "",
|
||||
"Default speed: ": "Velocità di riproduzione predefinita: ",
|
||||
"Preferred video quality: ": "Preferenza sulla qualità video: ",
|
||||
"Player volume: ": "Volume di riproduzione: ",
|
||||
@ -65,13 +65,13 @@
|
||||
"reddit": "",
|
||||
"Default captions: ": "Sottotitoli predefiniti: ",
|
||||
"Fallback captions: ": "Sottotitoli alternativi: ",
|
||||
"Show related videos? ": "Mostra video correlati? ",
|
||||
"Show annotations by default? ": "Mostra le annotazioni per impostazione predefinita? ",
|
||||
"Show related videos: ": "Mostra video correlati? ",
|
||||
"Show annotations by default: ": "Mostra le annotazioni per impostazione predefinita? ",
|
||||
"Visual preferences": "Preferenze grafiche",
|
||||
"Dark mode: ": "Tema scuro: ",
|
||||
"Thin mode: ": "Modalità per connessioni lente: ",
|
||||
"Subscription preferences": "Preferenze iscrizioni",
|
||||
"Show annotations by default for subscribed channels? ": "",
|
||||
"Show annotations by default for subscribed channels: ": "",
|
||||
"Redirect homepage to feed: ": "Reindirizza la pagina principale a quella delle iscrizioni: ",
|
||||
"Number of videos shown in feed: ": "Numero di video da mostrare nelle iscrizioni: ",
|
||||
"Sort videos by: ": "Ordinare i video per: ",
|
||||
@ -99,11 +99,11 @@
|
||||
"Administrator preferences": "",
|
||||
"Default homepage: ": "",
|
||||
"Feed menu: ": "",
|
||||
"Top enabled? ": "",
|
||||
"CAPTCHA enabled? ": "",
|
||||
"Login enabled? ": "",
|
||||
"Registration enabled? ": "",
|
||||
"Report statistics? ": "",
|
||||
"Top enabled: ": "",
|
||||
"CAPTCHA enabled: ": "",
|
||||
"Login enabled: ": "",
|
||||
"Registration enabled: ": "",
|
||||
"Report statistics: ": "",
|
||||
"Save preferences": "Salva le preferenze",
|
||||
"Subscription manager": "Gestisci le iscrizioni",
|
||||
"Token manager": "",
|
||||
|
@ -56,7 +56,7 @@
|
||||
"Play next by default: ": "Spill neste som forvalg: ",
|
||||
"Autoplay next video: ": "Autospill neste video: ",
|
||||
"Listen by default: ": "Lytt som forvalg: ",
|
||||
"Proxy videos? ": "Mellomtjen videoer? ",
|
||||
"Proxy videos: ": "Mellomtjen videoer? ",
|
||||
"Default speed: ": "Forvalgt hastighet: ",
|
||||
"Preferred video quality: ": "Foretrukket videokvalitet: ",
|
||||
"Player volume: ": "Avspillerlydstyrke: ",
|
||||
@ -65,13 +65,13 @@
|
||||
"reddit": "Reddit",
|
||||
"Default captions: ": "Forvalgte undertitler: ",
|
||||
"Fallback captions: ": "Tilbakefallsundertitler: ",
|
||||
"Show related videos? ": "Vis relaterte videoer? ",
|
||||
"Show annotations by default? ": "Vis merknader som forvalg? ",
|
||||
"Show related videos: ": "Vis relaterte videoer? ",
|
||||
"Show annotations by default: ": "Vis merknader som forvalg? ",
|
||||
"Visual preferences": "Visuelle innstillinger",
|
||||
"Dark mode: ": "Mørk drakt: ",
|
||||
"Thin mode: ": "Tynt modus: ",
|
||||
"Subscription preferences": "Abonnementsinnstillinger",
|
||||
"Show annotations by default for subscribed channels? ": "Vis merknader som forvalg for kanaler det abonneres på? ",
|
||||
"Show annotations by default for subscribed channels: ": "Vis merknader som forvalg for kanaler det abonneres på? ",
|
||||
"Redirect homepage to feed: ": "Videresend hjemmeside til flyt: ",
|
||||
"Number of videos shown in feed: ": "Antall videoer å vise i flyt: ",
|
||||
"Sort videos by: ": "Sorter videoer etter: ",
|
||||
@ -99,11 +99,11 @@
|
||||
"Administrator preferences": "Administratorinnstillinger",
|
||||
"Default homepage: ": "Forvalgt hjemmeside: ",
|
||||
"Feed menu: ": "Flyt-meny: ",
|
||||
"Top enabled? ": "Topp påskrudd? ",
|
||||
"CAPTCHA enabled? ": "CAPTCHA påskrudd? ",
|
||||
"Login enabled? ": "Innlogging påskrudd? ",
|
||||
"Registration enabled? ": "Registrering påskrudd? ",
|
||||
"Report statistics? ": "Innrapporter statistikk? ",
|
||||
"Top enabled: ": "Topp påskrudd? ",
|
||||
"CAPTCHA enabled: ": "CAPTCHA påskrudd? ",
|
||||
"Login enabled: ": "Innlogging påskrudd? ",
|
||||
"Registration enabled: ": "Registrering påskrudd? ",
|
||||
"Report statistics: ": "Innrapporter statistikk? ",
|
||||
"Save preferences": "Lagre innstillinger",
|
||||
"Subscription manager": "Abonnementsbehandler",
|
||||
"Token manager": "Symbolbehandler",
|
||||
@ -318,4 +318,4 @@
|
||||
"Playlists": "Spillelister",
|
||||
"Community": "",
|
||||
"Current version: ": "Nåværende versjon: "
|
||||
}
|
||||
}
|
@ -56,7 +56,7 @@
|
||||
"Play next by default: ": "Standaard volgende video afspelen: ",
|
||||
"Autoplay next video: ": "Volgende video automatisch afspelen: ",
|
||||
"Listen by default: ": "Standaard luisteren: ",
|
||||
"Proxy videos? ": "Video's afspelen via proxy? ",
|
||||
"Proxy videos: ": "Video's afspelen via proxy? ",
|
||||
"Default speed: ": "Standaard afspeelsnelheid: ",
|
||||
"Preferred video quality: ": "Voorkeurskwaliteit: ",
|
||||
"Player volume: ": "Spelervolume: ",
|
||||
@ -65,13 +65,13 @@
|
||||
"reddit": "Reddit",
|
||||
"Default captions: ": "Standaard ondertiteling: ",
|
||||
"Fallback captions: ": "Alternatieve ondertiteling: ",
|
||||
"Show related videos? ": "Gerelateerde video's tonen? ",
|
||||
"Show annotations by default? ": "Standaard annotaties tonen? ",
|
||||
"Show related videos: ": "Gerelateerde video's tonen? ",
|
||||
"Show annotations by default: ": "Standaard annotaties tonen? ",
|
||||
"Visual preferences": "Visuele instellingen",
|
||||
"Dark mode: ": "Donkere modus: ",
|
||||
"Thin mode: ": "Smalle modus: ",
|
||||
"Subscription preferences": "Abonnementsinstellingen",
|
||||
"Show annotations by default for subscribed channels? ": "Standaard annotaties tonen voor geabonneerde kanalen? ",
|
||||
"Show annotations by default for subscribed channels: ": "Standaard annotaties tonen voor geabonneerde kanalen? ",
|
||||
"Redirect homepage to feed: ": "Startpagina omleiden naar feed: ",
|
||||
"Number of videos shown in feed: ": "Aantal te tonen video's in feed: ",
|
||||
"Sort videos by: ": "Video's sorteren op: ",
|
||||
@ -99,11 +99,11 @@
|
||||
"Administrator preferences": "Beheerdersinstellingen",
|
||||
"Default homepage: ": "Standaard startpagina: ",
|
||||
"Feed menu: ": "Feedmenu:",
|
||||
"Top enabled? ": "Bovenkant inschakelen? ",
|
||||
"CAPTCHA enabled? ": "CAPTCHA gebruiken? ",
|
||||
"Login enabled? ": "Inloggen toestaan? ",
|
||||
"Registration enabled? ": "Registratie toestaan? ",
|
||||
"Report statistics? ": "Statistieken bijhouden? ",
|
||||
"Top enabled: ": "Bovenkant inschakelen? ",
|
||||
"CAPTCHA enabled: ": "CAPTCHA gebruiken? ",
|
||||
"Login enabled: ": "Inloggen toestaan? ",
|
||||
"Registration enabled: ": "Registratie toestaan? ",
|
||||
"Report statistics: ": "Statistieken bijhouden? ",
|
||||
"Save preferences": "Instellingen opslaan",
|
||||
"Subscription manager": "Abonnementen beheren",
|
||||
"Token manager": "Toegangssleutels beheren",
|
||||
|
@ -56,7 +56,7 @@
|
||||
"Play next by default: ": "",
|
||||
"Autoplay next video: ": "Odtwórz następny film: ",
|
||||
"Listen by default: ": "Tryb dźwiękowy: ",
|
||||
"Proxy videos? ": "Filmy przez proxy? ",
|
||||
"Proxy videos: ": "Filmy przez proxy? ",
|
||||
"Default speed: ": "Domyślna prędkość: ",
|
||||
"Preferred video quality: ": "Preferowana jakość filmów: ",
|
||||
"Player volume: ": "Głośność odtwarzacza: ",
|
||||
@ -65,13 +65,13 @@
|
||||
"reddit": "",
|
||||
"Default captions: ": "Domyślne napisy: ",
|
||||
"Fallback captions: ": "Zastępcze napisy: ",
|
||||
"Show related videos? ": "Pokaż powiązane filmy? ",
|
||||
"Show annotations by default? ": "",
|
||||
"Show related videos: ": "Pokaż powiązane filmy? ",
|
||||
"Show annotations by default: ": "",
|
||||
"Visual preferences": "Preferencje Wizualne",
|
||||
"Dark mode: ": "Ciemny motyw: ",
|
||||
"Thin mode: ": "Tryb minimalny: ",
|
||||
"Subscription preferences": "Preferencje subskrybcji",
|
||||
"Show annotations by default for subscribed channels? ": "",
|
||||
"Show annotations by default for subscribed channels: ": "",
|
||||
"Redirect homepage to feed: ": "Przekieruj stronę główną do subskrybcji: ",
|
||||
"Number of videos shown in feed: ": "Liczba filmów widoczna na stronie subskrybcji: ",
|
||||
"Sort videos by: ": "Sortuj filmy: ",
|
||||
@ -99,11 +99,11 @@
|
||||
"Administrator preferences": "Preferencje administratora",
|
||||
"Default homepage: ": "Domyślna strona główna: ",
|
||||
"Feed menu: ": "",
|
||||
"Top enabled? ": "",
|
||||
"CAPTCHA enabled? ": "CAPTCHA aktywna? ",
|
||||
"Login enabled? ": "Logowanie włączone? ",
|
||||
"Registration enabled? ": "Rejestracja włączona? ",
|
||||
"Report statistics? ": "Raportować statystyki? ",
|
||||
"Top enabled: ": "",
|
||||
"CAPTCHA enabled: ": "CAPTCHA aktywna? ",
|
||||
"Login enabled: ": "Logowanie włączone? ",
|
||||
"Registration enabled: ": "Rejestracja włączona? ",
|
||||
"Report statistics: ": "Raportować statystyki? ",
|
||||
"Save preferences": "Zapisz preferencje",
|
||||
"Subscription manager": "Manager subskrybcji",
|
||||
"Token manager": "",
|
||||
|
@ -56,7 +56,7 @@
|
||||
"Play next by default: ": "Всегда включать следующее видео? ",
|
||||
"Autoplay next video: ": "Автопроигрывание следующего видео: ",
|
||||
"Listen by default: ": "Режим «только аудио» по умолчанию: ",
|
||||
"Proxy videos? ": "Проигрывать видео через прокси? ",
|
||||
"Proxy videos: ": "Проигрывать видео через прокси? ",
|
||||
"Default speed: ": "Скорость видео по умолчанию: ",
|
||||
"Preferred video quality: ": "Предпочтительное качество видео: ",
|
||||
"Player volume: ": "Громкость видео: ",
|
||||
@ -65,13 +65,13 @@
|
||||
"reddit": "Reddit",
|
||||
"Default captions: ": "Основной язык субтитров: ",
|
||||
"Fallback captions: ": "Дополнительный язык субтитров: ",
|
||||
"Show related videos? ": "Показывать похожие видео? ",
|
||||
"Show annotations by default? ": "Всегда показывать аннотации? ",
|
||||
"Show related videos: ": "Показывать похожие видео? ",
|
||||
"Show annotations by default: ": "Всегда показывать аннотации? ",
|
||||
"Visual preferences": "Настройки сайта",
|
||||
"Dark mode: ": "Тёмное оформление: ",
|
||||
"Thin mode: ": "Облегчённое оформление: ",
|
||||
"Subscription preferences": "Настройки подписок",
|
||||
"Show annotations by default for subscribed channels? ": "Всегда показывать аннотации в видео каналов, на которые вы подписаны? ",
|
||||
"Show annotations by default for subscribed channels: ": "Всегда показывать аннотации в видео каналов, на которые вы подписаны? ",
|
||||
"Redirect homepage to feed: ": "Отображать видео с каналов, на которые вы подписаны, как главную страницу: ",
|
||||
"Number of videos shown in feed: ": "Число видео, на которые вы подписаны, в ленте: ",
|
||||
"Sort videos by: ": "Сортировать видео: ",
|
||||
@ -99,11 +99,11 @@
|
||||
"Administrator preferences": "Администраторские настройки",
|
||||
"Default homepage: ": "Главная страница по умолчанию: ",
|
||||
"Feed menu: ": "Меню ленты видео: ",
|
||||
"Top enabled? ": "Включить топ видео? ",
|
||||
"CAPTCHA enabled? ": "Включить капчу? ",
|
||||
"Login enabled? ": "Включить авторизацию? ",
|
||||
"Registration enabled? ": "Включить регистрацию? ",
|
||||
"Report statistics? ": "Сообщать статистику? ",
|
||||
"Top enabled: ": "Включить топ видео? ",
|
||||
"CAPTCHA enabled: ": "Включить капчу? ",
|
||||
"Login enabled: ": "Включить авторизацию? ",
|
||||
"Registration enabled: ": "Включить регистрацию? ",
|
||||
"Report statistics: ": "Сообщать статистику? ",
|
||||
"Save preferences": "Сохранить настройки",
|
||||
"Subscription manager": "Менеджер подписок",
|
||||
"Token manager": "Менеджер токенов",
|
||||
|
@ -56,7 +56,7 @@
|
||||
"Play next by default: ": "Завжди вмикати наступне відео: ",
|
||||
"Autoplay next video: ": "Автовідтворення наступного відео: ",
|
||||
"Listen by default: ": "Режим «тільки звук» як усталений: ",
|
||||
"Proxy videos? ": "Програвати відео через проксі? ",
|
||||
"Proxy videos: ": "Програвати відео через проксі? ",
|
||||
"Default speed: ": "Усталена швидкість відео: ",
|
||||
"Preferred video quality: ": "Пріорітетна якість відео: ",
|
||||
"Player volume: ": "Гучність відео: ",
|
||||
@ -65,13 +65,13 @@
|
||||
"reddit": "Reddit",
|
||||
"Default captions: ": "Основна мова субтитрів: ",
|
||||
"Fallback captions: ": "Запасна мова субтитрів: ",
|
||||
"Show related videos? ": "Показувати схожі відео? ",
|
||||
"Show annotations by default? ": "Завжди показувати анотації? ",
|
||||
"Show related videos: ": "Показувати схожі відео? ",
|
||||
"Show annotations by default: ": "Завжди показувати анотації? ",
|
||||
"Visual preferences": "Налаштування сайту",
|
||||
"Dark mode: ": "Темне оформлення: ",
|
||||
"Thin mode: ": "Полегшене оформлення: ",
|
||||
"Subscription preferences": "Налаштування підписок",
|
||||
"Show annotations by default for subscribed channels? ": "Завжди показувати анотації у відео каналів, на які ви підписані? ",
|
||||
"Show annotations by default for subscribed channels: ": "Завжди показувати анотації у відео каналів, на які ви підписані? ",
|
||||
"Redirect homepage to feed: ": "Показувати відео з каналів, на які підписані, як головну сторінку: ",
|
||||
"Number of videos shown in feed: ": "Кількість відео з каналів, на які підписані, у потоці: ",
|
||||
"Sort videos by: ": "Сортувати відео: ",
|
||||
@ -99,11 +99,11 @@
|
||||
"Administrator preferences": "Адміністраторські налаштування",
|
||||
"Default homepage: ": "Усталена домашня сторінка: ",
|
||||
"Feed menu: ": "Меню потоку з відео: ",
|
||||
"Top enabled? ": "Увімкнути топ відео? ",
|
||||
"CAPTCHA enabled? ": "Увімкнути капчу? ",
|
||||
"Login enabled? ": "Увімкнути авторизацію? ",
|
||||
"Registration enabled? ": "Увімкнути реєстрацію? ",
|
||||
"Report statistics? ": "Повідомляти статистику? ",
|
||||
"Top enabled: ": "Увімкнути топ відео? ",
|
||||
"CAPTCHA enabled: ": "Увімкнути капчу? ",
|
||||
"Login enabled: ": "Увімкнути авторизацію? ",
|
||||
"Registration enabled: ": "Увімкнути реєстрацію? ",
|
||||
"Report statistics: ": "Повідомляти статистику? ",
|
||||
"Save preferences": "Зберегти налаштування",
|
||||
"Subscription manager": "Менеджер підписок",
|
||||
"Token manager": "Менеджер токенів",
|
||||
|
@ -56,7 +56,7 @@
|
||||
"Play next by default: ": "默认自动播放下一个视频:",
|
||||
"Autoplay next video: ": "自动播放下一个视频:",
|
||||
"Listen by default: ": "默认只聆听声音:",
|
||||
"Proxy videos? ": "代理视频?",
|
||||
"Proxy videos: ": "代理视频?",
|
||||
"Default speed: ": "默认速度:",
|
||||
"Preferred video quality: ": "视频质量偏好:",
|
||||
"Player volume: ": "播放器音量:",
|
||||
@ -65,13 +65,13 @@
|
||||
"reddit": "Reddit",
|
||||
"Default captions: ": "默认字幕语言:",
|
||||
"Fallback captions: ": "后备字幕语言:",
|
||||
"Show related videos? ": "显示相关视频?",
|
||||
"Show annotations by default? ": "默认显示视频注释?",
|
||||
"Show related videos: ": "显示相关视频?",
|
||||
"Show annotations by default: ": "默认显示视频注释?",
|
||||
"Visual preferences": "视觉选项",
|
||||
"Dark mode: ": "暗色模式:",
|
||||
"Thin mode: ": "窄页模式:",
|
||||
"Subscription preferences": "订阅设置",
|
||||
"Show annotations by default for subscribed channels? ": "在订阅频道的视频默认显示注释?",
|
||||
"Show annotations by default for subscribed channels: ": "在订阅频道的视频默认显示注释?",
|
||||
"Redirect homepage to feed: ": "跳转主页到 feed: ",
|
||||
"Number of videos shown in feed: ": "Feed 中显示的视频数量:",
|
||||
"Sort videos by: ": "视频排序方式:",
|
||||
@ -99,11 +99,11 @@
|
||||
"Administrator preferences": "管理员选项",
|
||||
"Default homepage: ": "默认主页:",
|
||||
"Feed menu: ": "Feed 菜单:",
|
||||
"Top enabled? ": "启用“热门视频”页?",
|
||||
"CAPTCHA enabled? ": "启用验证码?",
|
||||
"Login enabled? ": "启用登录?",
|
||||
"Registration enabled? ": "启用注册?",
|
||||
"Report statistics? ": "报告统计信息?",
|
||||
"Top enabled: ": "启用“热门视频”页?",
|
||||
"CAPTCHA enabled: ": "启用验证码?",
|
||||
"Login enabled: ": "启用登录?",
|
||||
"Registration enabled: ": "启用注册?",
|
||||
"Report statistics: ": "报告统计信息?",
|
||||
"Save preferences": "保存选项",
|
||||
"Subscription manager": "订阅管理器",
|
||||
"Token manager": "令牌管理器",
|
||||
|
File diff suppressed because one or more lines are too long
209
src/invidious.cr
209
src/invidious.cr
@ -532,6 +532,30 @@ get "/watch" do |env|
|
||||
templated "watch"
|
||||
end
|
||||
|
||||
get "/embed/" do |env|
|
||||
locale = LOCALES[env.get("preferences").as(Preferences).locale]?
|
||||
|
||||
if plid = env.params.query["list"]?
|
||||
begin
|
||||
videos = fetch_playlist_videos(plid, 1, 1, locale: locale)
|
||||
rescue ex
|
||||
error_message = ex.message
|
||||
env.response.status_code = 500
|
||||
next templated "error"
|
||||
end
|
||||
|
||||
url = "/embed/#{videos[0].id}?#{env.params.query}"
|
||||
|
||||
if env.params.query.size > 0
|
||||
url += "?#{env.params.query}"
|
||||
end
|
||||
else
|
||||
url = "/"
|
||||
end
|
||||
|
||||
env.redirect url
|
||||
end
|
||||
|
||||
get "/embed/:id" do |env|
|
||||
locale = LOCALES[env.get("preferences").as(Preferences).locale]?
|
||||
id = env.params.url["id"]
|
||||
@ -559,7 +583,8 @@ get "/embed/:id" do |env|
|
||||
|
||||
# YouTube embed supports `videoseries` with either `list=PLID`
|
||||
# or `playlist=VIDEO_ID,VIDEO_ID`
|
||||
if id == "videoseries"
|
||||
case id
|
||||
when "videoseries"
|
||||
url = ""
|
||||
|
||||
if plid
|
||||
@ -584,7 +609,26 @@ get "/embed/:id" do |env|
|
||||
end
|
||||
|
||||
next env.redirect url
|
||||
elsif id.size > 11
|
||||
when "live_stream"
|
||||
client = make_client(YT_URL)
|
||||
response = client.get("/embed/live_stream?channel=#{env.params.query["channel"]? || ""}")
|
||||
video_id = response.body.match(/"video_id":"(?<video_id>[a-zA-Z0-9_-]{11})"/).try &.["video_id"]
|
||||
|
||||
env.params.query.delete_all("channel")
|
||||
|
||||
if !video_id || video_id == "live_stream"
|
||||
error_message = "Video is unavailable."
|
||||
next templated "error"
|
||||
end
|
||||
|
||||
url = "/embed/#{video_id}"
|
||||
|
||||
if env.params.query.size > 0
|
||||
url += "?#{env.params.query}"
|
||||
end
|
||||
|
||||
next env.redirect url
|
||||
when id.size > 11
|
||||
url = "/embed/#{id[0, 11]}"
|
||||
|
||||
if env.params.query.size > 0
|
||||
@ -859,6 +903,7 @@ get "/search" do |env|
|
||||
count, videos = search(search_query, page, search_params, region).as(Tuple)
|
||||
end
|
||||
|
||||
env.set "search", query
|
||||
templated "search"
|
||||
end
|
||||
|
||||
@ -892,6 +937,7 @@ get "/login" do |env|
|
||||
|
||||
tfa = env.params.query["tfa"]?
|
||||
tfa ||= false
|
||||
prompt = ""
|
||||
|
||||
templated "login"
|
||||
end
|
||||
@ -989,15 +1035,22 @@ post "/login" do |env|
|
||||
next templated "error"
|
||||
end
|
||||
|
||||
if challenge_results[0][-1]?.try &.[0]?.try &.as_a?
|
||||
traceback << "User has 2FA.<br/>"
|
||||
prompt_type = challenge_results[0][-1]?.try &.[0].as_a?.try &.[0][2]?
|
||||
if {"TWO_STEP_VERIFICATION", "LOGIN_CHALLENGE"}.includes? prompt_type
|
||||
traceback << "Handling prompt #{prompt_type}.<br/>"
|
||||
case prompt_type
|
||||
when "TWO_STEP_VERIFICATION"
|
||||
prompt_type = 2
|
||||
when "LOGIN_CHALLENGE"
|
||||
prompt_type = 4
|
||||
end
|
||||
|
||||
# Prefer Authenticator app and SMS over unsupported protocols
|
||||
if challenge_results[0][-1][0][0][8] != 6 && challenge_results[0][-1][0][0][8] != 9
|
||||
tfa = challenge_results[0][-1][0].as_a.select { |auth_type| auth_type[8] == 6 || auth_type[8] == 9 }[0]
|
||||
if !{6, 9, 12, 15}.includes?(challenge_results[0][-1][0][0][8]) && prompt_type == 4
|
||||
tfa = challenge_results[0][-1][0].as_a.select { |auth_type| {6, 9, 12, 15}.includes? auth_type[8] }[0]
|
||||
|
||||
traceback << "Selecting challenge #{tfa[8]}..."
|
||||
select_challenge = {2, nil, nil, nil, {tfa[8]}}.to_json
|
||||
select_challenge = {prompt_type, nil, nil, nil, {tfa[8]}}.to_json
|
||||
|
||||
tl = challenge_results[1][2]
|
||||
|
||||
@ -1011,62 +1064,84 @@ post "/login" do |env|
|
||||
tfa = challenge_results[0][-1][0][0]
|
||||
end
|
||||
|
||||
if tfa[2] == "TWO_STEP_VERIFICATION"
|
||||
if tfa[5] == "QUOTA_EXCEEDED"
|
||||
error_message = translate(locale, "Quota exceeded, try again in a few hours")
|
||||
env.response.status_code = 423
|
||||
next templated "error"
|
||||
end
|
||||
|
||||
if !tfa_code
|
||||
account_type = "google"
|
||||
captcha_type = "image"
|
||||
tfa = true
|
||||
captcha = nil
|
||||
next templated "login"
|
||||
end
|
||||
|
||||
tl = challenge_results[1][2]
|
||||
|
||||
request_type = tfa[8]
|
||||
case request_type
|
||||
when 6
|
||||
# Authenticator app
|
||||
tfa_req = {
|
||||
user_hash, nil, 2, nil,
|
||||
{6, nil, nil, nil, nil,
|
||||
{tfa_code, false},
|
||||
},
|
||||
}.to_json
|
||||
when 9
|
||||
# Voice or text message
|
||||
tfa_req = {
|
||||
user_hash, nil, 2, nil,
|
||||
{9, nil, nil, nil, nil, nil, nil, nil,
|
||||
{nil, tfa_code, false, 2},
|
||||
},
|
||||
}.to_json
|
||||
else
|
||||
error_message = translate(locale, "Unable to log in, make sure two-factor authentication (Authenticator or SMS) is turned on.")
|
||||
env.response.status_code = 500
|
||||
next templated "error"
|
||||
end
|
||||
|
||||
traceback << "Submitting challenge..."
|
||||
|
||||
response = client.post("/_/signin/challenge?hl=en&TL=#{tl}", headers, login_req(tfa_req))
|
||||
headers = response.cookies.add_request_headers(headers)
|
||||
challenge_results = JSON.parse(response.body[5..-1])
|
||||
|
||||
if (challenge_results[0][-1]?.try &.[5] == "INCORRECT_ANSWER_ENTERED") ||
|
||||
(challenge_results[0][-1]?.try &.[5] == "INVALID_INPUT")
|
||||
error_message = translate(locale, "Invalid TFA code")
|
||||
env.response.status_code = 401
|
||||
next templated "error"
|
||||
end
|
||||
|
||||
traceback << "done.<br/>"
|
||||
if tfa[5] == "QUOTA_EXCEEDED"
|
||||
error_message = translate(locale, "Quota exceeded, try again in a few hours")
|
||||
env.response.status_code = 423
|
||||
next templated "error"
|
||||
end
|
||||
|
||||
if !tfa_code
|
||||
account_type = "google"
|
||||
captcha_type = "image"
|
||||
|
||||
case tfa[8]
|
||||
when 6, 9
|
||||
prompt = "Google verification code"
|
||||
when 12
|
||||
prompt = "Login verification, recovery email: #{tfa[-1][tfa[-1].as_h.keys[0]][0]}"
|
||||
when 15
|
||||
prompt = "Login verification, security question: #{tfa[-1][tfa[-1].as_h.keys[0]][0]}"
|
||||
else
|
||||
prompt = "Google verification code"
|
||||
end
|
||||
|
||||
tfa = true
|
||||
captcha = nil
|
||||
next templated "login"
|
||||
end
|
||||
|
||||
tl = challenge_results[1][2]
|
||||
|
||||
request_type = tfa[8]
|
||||
case request_type
|
||||
when 6 # Authenticator app
|
||||
tfa_req = {
|
||||
user_hash, nil, 2, nil,
|
||||
{6, nil, nil, nil, nil,
|
||||
{tfa_code, false},
|
||||
},
|
||||
}.to_json
|
||||
when 9 # Voice or text message
|
||||
tfa_req = {
|
||||
user_hash, nil, 2, nil,
|
||||
{9, nil, nil, nil, nil, nil, nil, nil,
|
||||
{nil, tfa_code, false, 2},
|
||||
},
|
||||
}.to_json
|
||||
when 12 # Recovery email
|
||||
tfa_req = {
|
||||
user_hash, nil, 4, nil,
|
||||
{12, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil,
|
||||
{tfa_code},
|
||||
},
|
||||
}.to_json
|
||||
when 15 # Security question
|
||||
tfa_req = {
|
||||
user_hash, nil, 5, nil,
|
||||
{15, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil,
|
||||
{tfa_code},
|
||||
},
|
||||
}.to_json
|
||||
else
|
||||
error_message = translate(locale, "Unable to log in, make sure two-factor authentication (Authenticator or SMS) is turned on.")
|
||||
env.response.status_code = 500
|
||||
next templated "error"
|
||||
end
|
||||
|
||||
traceback << "Submitting challenge..."
|
||||
|
||||
response = client.post("/_/signin/challenge?hl=en&TL=#{tl}", headers, login_req(tfa_req))
|
||||
headers = response.cookies.add_request_headers(headers)
|
||||
challenge_results = JSON.parse(response.body[5..-1])
|
||||
|
||||
if (challenge_results[0][-1]?.try &.[5] == "INCORRECT_ANSWER_ENTERED") ||
|
||||
(challenge_results[0][-1]?.try &.[5] == "INVALID_INPUT")
|
||||
error_message = translate(locale, "Invalid TFA code")
|
||||
env.response.status_code = 401
|
||||
next templated "error"
|
||||
end
|
||||
|
||||
traceback << "done.<br/>"
|
||||
end
|
||||
|
||||
traceback << "Logging in..."
|
||||
@ -1080,8 +1155,7 @@ post "/login" do |env|
|
||||
end
|
||||
|
||||
# TODO: Occasionally there will be a second page after login confirming
|
||||
# the user's phone number, which we will likely choke on.
|
||||
# if location.includes? "SmsAuthInterstitial"
|
||||
# the user's phone number ("/b/0/SmsAuthInterstitial"), which we currently choke on.
|
||||
|
||||
login = client.get(location, headers)
|
||||
headers = login.cookies.add_request_headers(headers)
|
||||
@ -1225,6 +1299,7 @@ post "/login" do |env|
|
||||
|
||||
account_type = "invidious"
|
||||
tfa = false
|
||||
prompt = ""
|
||||
|
||||
if captcha_type == "image"
|
||||
captcha = generate_captcha(HMAC_KEY, PG_DB)
|
||||
@ -2996,6 +3071,7 @@ get "/channel/:ucid/playlists" do |env|
|
||||
items = items.map { |item| item.as(SearchPlaylist) }
|
||||
items.each { |item| item.author = "" }
|
||||
|
||||
env.set "search", "channel:#{channel.ucid} "
|
||||
templated "playlists"
|
||||
end
|
||||
|
||||
@ -3036,6 +3112,7 @@ get "/channel/:ucid/community" do |env|
|
||||
error_message = ex.message
|
||||
end
|
||||
|
||||
env.set "search", "channel:#{channel.ucid} "
|
||||
templated "community"
|
||||
end
|
||||
|
||||
@ -3572,7 +3649,7 @@ get "/api/v1/top" do |env|
|
||||
generate_thumbnails(json, video.id, config, Kemal.config)
|
||||
end
|
||||
|
||||
json.field "lengthSeconds", video.info["length_seconds"].to_i
|
||||
json.field "lengthSeconds", video.length_seconds
|
||||
json.field "viewCount", video.views
|
||||
|
||||
json.field "author", video.author
|
||||
@ -4417,7 +4494,7 @@ get "/api/manifest/dash/id/:id" do |env|
|
||||
XML.build(indent: " ", encoding: "UTF-8") do |xml|
|
||||
xml.element("MPD", "xmlns": "urn:mpeg:dash:schema:mpd:2011",
|
||||
"profiles": "urn:mpeg:dash:profile:full:2011", minBufferTime: "PT1.5S", type: "static",
|
||||
mediaPresentationDuration: "PT#{video.info["length_seconds"]}S") do
|
||||
mediaPresentationDuration: "PT#{video.length_seconds}S") do
|
||||
xml.element("Period") do
|
||||
i = 0
|
||||
|
||||
|
@ -375,13 +375,14 @@ def fetch_channel_playlists(ucid, author, auto_generated, continuation, sort_by)
|
||||
json = JSON.parse(response.body)
|
||||
|
||||
if json["load_more_widget_html"].as_s.empty?
|
||||
return [] of SearchItem, nil
|
||||
end
|
||||
continuation = nil
|
||||
else
|
||||
continuation = XML.parse_html(json["load_more_widget_html"].as_s)
|
||||
continuation = continuation.xpath_node(%q(//button[@data-uix-load-more-href]))
|
||||
|
||||
continuation = XML.parse_html(json["load_more_widget_html"].as_s)
|
||||
continuation = continuation.xpath_node(%q(//button[@data-uix-load-more-href]))
|
||||
if continuation
|
||||
continuation = extract_channel_playlists_cursor(continuation["data-uix-load-more-href"], auto_generated)
|
||||
if continuation
|
||||
continuation = extract_channel_playlists_cursor(continuation["data-uix-load-more-href"], auto_generated)
|
||||
end
|
||||
end
|
||||
|
||||
html = XML.parse_html(json["content_html"].as_s)
|
||||
@ -440,53 +441,57 @@ def produce_channel_videos_url(ucid, page = 1, auto_generated = nil, sort_by = "
|
||||
switch = 0x00
|
||||
end
|
||||
|
||||
meta = IO::Memory.new
|
||||
meta.write(Bytes[0x12, 0x06])
|
||||
meta.print("videos")
|
||||
data = IO::Memory.new
|
||||
data.write_byte 0x12
|
||||
data.write_byte 0x06
|
||||
data.print "videos"
|
||||
|
||||
meta.write(Bytes[0x30, 0x02])
|
||||
meta.write(Bytes[0x38, 0x01])
|
||||
meta.write(Bytes[0x60, 0x01])
|
||||
meta.write(Bytes[0x6a, 0x00])
|
||||
meta.write(Bytes[0xb8, 0x01, 0x00])
|
||||
data.write Bytes[0x30, 0x02]
|
||||
data.write Bytes[0x38, 0x01]
|
||||
data.write Bytes[0x60, 0x01]
|
||||
data.write Bytes[0x6a, 0x00]
|
||||
data.write Bytes[0xb8, 0x01, 0x00]
|
||||
|
||||
meta.write(Bytes[0x20, switch])
|
||||
meta.write(Bytes[0x7a, page.size])
|
||||
meta.print(page)
|
||||
data.write Bytes[0x20, switch]
|
||||
data.write_byte 0x7a
|
||||
VarInt.to_io(data, page.bytesize)
|
||||
data.print page
|
||||
|
||||
case sort_by
|
||||
when "newest"
|
||||
# Empty tags can be omitted
|
||||
# meta.write(Bytes[0x18,0x00])
|
||||
when "popular"
|
||||
meta.write(Bytes[0x18, 0x01])
|
||||
data.write Bytes[0x18, 0x01]
|
||||
when "oldest"
|
||||
meta.write(Bytes[0x18, 0x02])
|
||||
data.write Bytes[0x18, 0x02]
|
||||
end
|
||||
|
||||
meta.rewind
|
||||
meta = Base64.urlsafe_encode(meta.to_slice)
|
||||
meta = URI.escape(meta)
|
||||
data = Base64.urlsafe_encode(data)
|
||||
cursor = URI.escape(data)
|
||||
|
||||
continuation = IO::Memory.new
|
||||
continuation.write(Bytes[0x12, ucid.size])
|
||||
continuation.print(ucid)
|
||||
data = IO::Memory.new
|
||||
|
||||
continuation.write(Bytes[0x1a, meta.size])
|
||||
continuation.print(meta)
|
||||
data.write_byte 0x12
|
||||
VarInt.to_io(data, ucid.bytesize)
|
||||
data.print ucid
|
||||
|
||||
continuation.rewind
|
||||
continuation = continuation.gets_to_end
|
||||
data.write_byte 0x1a
|
||||
VarInt.to_io(data, cursor.bytesize)
|
||||
data.print cursor
|
||||
|
||||
wrapper = IO::Memory.new
|
||||
wrapper.write(Bytes[0xe2, 0xa9, 0x85, 0xb2, 0x02, continuation.size])
|
||||
wrapper.print(continuation)
|
||||
wrapper.rewind
|
||||
data.rewind
|
||||
|
||||
wrapper = Base64.urlsafe_encode(wrapper.to_slice)
|
||||
wrapper = URI.escape(wrapper)
|
||||
buffer = IO::Memory.new
|
||||
buffer.write Bytes[0xe2, 0xa9, 0x85, 0xb2, 0x02]
|
||||
VarInt.to_io(buffer, data.bytesize)
|
||||
|
||||
url = "/browse_ajax?continuation=#{wrapper}&gl=US&hl=en"
|
||||
IO.copy data, buffer
|
||||
|
||||
continuation = Base64.urlsafe_encode(buffer)
|
||||
continuation = URI.escape(continuation)
|
||||
|
||||
url = "/browse_ajax?continuation=#{continuation}&gl=US&hl=en"
|
||||
|
||||
return url
|
||||
end
|
||||
@ -496,117 +501,108 @@ def produce_channel_playlists_url(ucid, cursor, sort = "newest", auto_generated
|
||||
cursor = Base64.urlsafe_encode(cursor, false)
|
||||
end
|
||||
|
||||
meta = IO::Memory.new
|
||||
data = IO::Memory.new
|
||||
|
||||
if auto_generated
|
||||
meta.write(Bytes[0x08, 0x0a])
|
||||
data.write Bytes[0x08, 0x0a]
|
||||
end
|
||||
|
||||
meta.write(Bytes[0x12, 0x09])
|
||||
meta.print("playlists")
|
||||
data.write Bytes[0x12, 0x09]
|
||||
data.print "playlists"
|
||||
|
||||
if auto_generated
|
||||
meta.write(Bytes[0x20, 0x32])
|
||||
data.write Bytes[0x20, 0x32]
|
||||
else
|
||||
# TODO: Look at 0x01, 0x00
|
||||
case sort
|
||||
when "oldest", "oldest_created"
|
||||
meta.write(Bytes[0x18, 0x02])
|
||||
data.write Bytes[0x18, 0x02]
|
||||
when "newest", "newest_created"
|
||||
meta.write(Bytes[0x18, 0x03])
|
||||
data.write Bytes[0x18, 0x03]
|
||||
when "last", "last_added"
|
||||
meta.write(Bytes[0x18, 0x04])
|
||||
data.write Bytes[0x18, 0x04]
|
||||
end
|
||||
|
||||
meta.write(Bytes[0x20, 0x01])
|
||||
data.write Bytes[0x20, 0x01]
|
||||
end
|
||||
|
||||
meta.write(Bytes[0x30, 0x02])
|
||||
meta.write(Bytes[0x38, 0x01])
|
||||
meta.write(Bytes[0x60, 0x01])
|
||||
meta.write(Bytes[0x6a, 0x00])
|
||||
data.write Bytes[0x30, 0x02]
|
||||
data.write Bytes[0x38, 0x01]
|
||||
data.write Bytes[0x60, 0x01]
|
||||
data.write Bytes[0x6a, 0x00]
|
||||
|
||||
meta.write(Bytes[0x7a, cursor.size])
|
||||
meta.print(cursor)
|
||||
data.write_byte 0x7a
|
||||
VarInt.to_io(data, cursor.bytesize)
|
||||
data.print cursor
|
||||
|
||||
meta.write(Bytes[0xb8, 0x01, 0x00])
|
||||
data.write Bytes[0xb8, 0x01, 0x00]
|
||||
|
||||
meta.rewind
|
||||
meta = Base64.urlsafe_encode(meta.to_slice)
|
||||
meta = URI.escape(meta)
|
||||
data.rewind
|
||||
data = Base64.urlsafe_encode(data)
|
||||
continuation = URI.escape(data)
|
||||
|
||||
continuation = IO::Memory.new
|
||||
continuation.write(Bytes[0x12, ucid.size])
|
||||
continuation.print(ucid)
|
||||
data = IO::Memory.new
|
||||
|
||||
continuation.write(Bytes[0x1a])
|
||||
continuation.write(write_var_int(meta.size))
|
||||
continuation.print(meta)
|
||||
data.write_byte 0x12
|
||||
VarInt.to_io(data, ucid.bytesize)
|
||||
data.print ucid
|
||||
|
||||
continuation.rewind
|
||||
continuation = continuation.gets_to_end
|
||||
data.write_byte 0x1a
|
||||
VarInt.to_io(data, continuation.bytesize)
|
||||
data.print continuation
|
||||
|
||||
wrapper = IO::Memory.new
|
||||
wrapper.write(Bytes[0xe2, 0xa9, 0x85, 0xb2, 0x02])
|
||||
wrapper.write(write_var_int(continuation.size))
|
||||
wrapper.print(continuation)
|
||||
wrapper.rewind
|
||||
data.rewind
|
||||
|
||||
wrapper = Base64.urlsafe_encode(wrapper.to_slice)
|
||||
wrapper = URI.escape(wrapper)
|
||||
buffer = IO::Memory.new
|
||||
buffer.write Bytes[0xe2, 0xa9, 0x85, 0xb2, 0x02]
|
||||
VarInt.to_io(buffer, data.bytesize)
|
||||
|
||||
url = "/browse_ajax?continuation=#{wrapper}&gl=US&hl=en"
|
||||
IO.copy data, buffer
|
||||
|
||||
continuation = Base64.urlsafe_encode(buffer)
|
||||
continuation = URI.escape(continuation)
|
||||
|
||||
url = "/browse_ajax?continuation=#{continuation}&gl=US&hl=en"
|
||||
|
||||
return url
|
||||
end
|
||||
|
||||
def extract_channel_playlists_cursor(url, auto_generated)
|
||||
wrapper = HTTP::Params.parse(URI.parse(url).query.not_nil!)["continuation"]
|
||||
continuation = HTTP::Params.parse(URI.parse(url).query.not_nil!)["continuation"]
|
||||
|
||||
wrapper = URI.unescape(wrapper)
|
||||
wrapper = Base64.decode(wrapper)
|
||||
continuation = URI.unescape(continuation)
|
||||
data = IO::Memory.new(Base64.decode(continuation))
|
||||
|
||||
# 0xe2 0xa9 0x85 0xb2 0x02
|
||||
wrapper += 5
|
||||
data.pos += 5
|
||||
|
||||
continuation_size = read_var_int(wrapper[0, 4])
|
||||
wrapper += write_var_int(continuation_size).size
|
||||
continuation = wrapper[0, continuation_size]
|
||||
continuation = Bytes.new(data.read_bytes(VarInt))
|
||||
data.read continuation
|
||||
data = IO::Memory.new(continuation)
|
||||
|
||||
# 0x12
|
||||
continuation += 1
|
||||
ucid_size = continuation[0]
|
||||
continuation += 1
|
||||
ucid = continuation[0, ucid_size]
|
||||
continuation += ucid_size
|
||||
data.read_byte # => 0x12
|
||||
ucid = Bytes.new(data.read_bytes(VarInt))
|
||||
data.read ucid
|
||||
|
||||
# 0x1a
|
||||
continuation += 1
|
||||
meta_size = read_var_int(continuation[0, 4])
|
||||
continuation += write_var_int(meta_size).size
|
||||
meta = continuation[0, meta_size]
|
||||
continuation += meta_size
|
||||
data.read_byte # => 0x1a
|
||||
inner_continuation = Bytes.new(data.read_bytes(VarInt))
|
||||
data.read inner_continuation
|
||||
|
||||
meta = String.new(meta)
|
||||
meta = URI.unescape(meta)
|
||||
meta = Base64.decode(meta)
|
||||
continuation = String.new(inner_continuation)
|
||||
continuation = URI.unescape(continuation)
|
||||
data = IO::Memory.new(Base64.decode(continuation))
|
||||
|
||||
# 0x12 0x09 playlists
|
||||
meta += 11
|
||||
data.pos += 11
|
||||
|
||||
until meta[0] == 0x7a
|
||||
tag = read_var_int(meta[0, 4])
|
||||
meta += write_var_int(tag).size
|
||||
value = meta[0]
|
||||
meta += 1
|
||||
until data.peek[0] == 0x7a
|
||||
key = data.read_bytes(VarInt)
|
||||
value = data.read_bytes(VarInt)
|
||||
end
|
||||
|
||||
# 0x7a
|
||||
meta += 1
|
||||
cursor_size = meta[0]
|
||||
meta += 1
|
||||
cursor = meta[0, cursor_size]
|
||||
|
||||
data.pos += 1 # => 0x7a
|
||||
cursor = Bytes.new(data.read_bytes(VarInt))
|
||||
data.read cursor
|
||||
cursor = String.new(cursor)
|
||||
|
||||
if !auto_generated
|
||||
@ -873,20 +869,26 @@ end
|
||||
|
||||
def produce_channel_community_continuation(ucid, cursor)
|
||||
cursor = URI.escape(cursor)
|
||||
continuation = IO::Memory.new
|
||||
|
||||
continuation.write(Bytes[0xe2, 0xa9, 0x85, 0xb2, 0x02])
|
||||
continuation.write(write_var_int(3 + ucid.size + write_var_int(cursor.size).size + cursor.size))
|
||||
data = IO::Memory.new
|
||||
|
||||
continuation.write(Bytes[0x12, ucid.size])
|
||||
continuation.print(ucid)
|
||||
data.write_byte 0x12
|
||||
VarInt.to_io(data, ucid.bytesize)
|
||||
data.print ucid
|
||||
|
||||
continuation.write(Bytes[0x1a])
|
||||
continuation.write(write_var_int(cursor.size))
|
||||
continuation.print(cursor)
|
||||
continuation.rewind
|
||||
data.write_byte 0x1a
|
||||
VarInt.to_io(data, cursor.bytesize)
|
||||
data.print cursor
|
||||
|
||||
continuation = Base64.urlsafe_encode(continuation.to_slice)
|
||||
data.rewind
|
||||
|
||||
buffer = IO::Memory.new
|
||||
buffer.write Bytes[0xe2, 0xa9, 0x85, 0xb2, 0x02]
|
||||
VarInt.to_io(buffer, data.size)
|
||||
|
||||
IO.copy data, buffer
|
||||
|
||||
continuation = Base64.urlsafe_encode(buffer)
|
||||
continuation = URI.escape(continuation)
|
||||
|
||||
return continuation
|
||||
@ -894,28 +896,25 @@ end
|
||||
|
||||
def extract_channel_community_cursor(continuation)
|
||||
continuation = URI.unescape(continuation)
|
||||
continuation = Base64.decode(continuation)
|
||||
data = IO::Memory.new(Base64.decode(continuation))
|
||||
|
||||
# 0xe2 0xa9 0x85 0xb2 0x02
|
||||
continuation += 5
|
||||
data.pos += 5
|
||||
|
||||
total_size = read_var_int(continuation[0, 4])
|
||||
continuation += write_var_int(total_size).size
|
||||
continuation = Bytes.new(data.read_bytes(VarInt))
|
||||
data.read continuation
|
||||
data = IO::Memory.new(continuation)
|
||||
|
||||
# 0x12
|
||||
continuation += 1
|
||||
ucid_size = continuation[0]
|
||||
continuation += 1
|
||||
ucid = continuation[0, ucid_size]
|
||||
continuation += ucid_size
|
||||
data.read_byte # => 0x12
|
||||
ucid = Bytes.new(data.read_bytes(VarInt))
|
||||
data.read ucid
|
||||
|
||||
# 0x1a
|
||||
continuation += 1
|
||||
until continuation[0] == 'E'.ord
|
||||
continuation += 1
|
||||
data.read_byte # => 0x1a
|
||||
until data.peek[0] == 'E'.ord
|
||||
data.read_byte
|
||||
end
|
||||
|
||||
return String.new(continuation)
|
||||
return URI.unescape(data.gets_to_end)
|
||||
end
|
||||
|
||||
def get_about_info(ucid, locale)
|
||||
|
@ -339,7 +339,7 @@ def template_youtube_comments(comments, locale, thin_mode)
|
||||
END_HTML
|
||||
else
|
||||
html << <<-END_HTML
|
||||
<iframe id='ivplayer' type='text/html' style='position:absolute;width:100%;height:100%;left:0;top:0' src='/embed/#{attachment["videoId"]?}' frameborder='0'></iframe>
|
||||
<iframe id='ivplayer' type='text/html' style='position:absolute;width:100%;height:100%;left:0;top:0' src='/embed/#{attachment["videoId"]?}?autoplay=0' frameborder='0'></iframe>
|
||||
END_HTML
|
||||
end
|
||||
|
||||
@ -564,108 +564,105 @@ def content_to_comment_html(content)
|
||||
end
|
||||
|
||||
def produce_comment_continuation(video_id, cursor = "", sort_by = "top")
|
||||
continuation = IO::Memory.new
|
||||
data = IO::Memory.new
|
||||
|
||||
continuation.write(Bytes[0x12, 0x26])
|
||||
data.write Bytes[0x12, 0x26]
|
||||
|
||||
continuation.write(Bytes[0x12, video_id.size])
|
||||
continuation.print(video_id)
|
||||
data.write_byte 0x12
|
||||
VarInt.to_io(data, video_id.bytesize)
|
||||
data.print video_id
|
||||
|
||||
continuation.write(Bytes[0xc0, 0x01, 0x01])
|
||||
continuation.write(Bytes[0xc8, 0x01, 0x01])
|
||||
continuation.write(Bytes[0xe0, 0x01, 0x01])
|
||||
data.write Bytes[0xc0, 0x01, 0x01]
|
||||
data.write Bytes[0xc8, 0x01, 0x01]
|
||||
data.write Bytes[0xe0, 0x01, 0x01]
|
||||
|
||||
continuation.write(Bytes[0xa2, 0x02, 0x0d])
|
||||
continuation.write(Bytes[0x28, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x01])
|
||||
data.write Bytes[0xa2, 0x02, 0x0d]
|
||||
data.write Bytes[0x28, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x01]
|
||||
|
||||
continuation.write(Bytes[0x40, 0x00])
|
||||
continuation.write(Bytes[0x18, 0x06])
|
||||
data.write Bytes[0x40, 0x00]
|
||||
data.write Bytes[0x18, 0x06]
|
||||
|
||||
if cursor.empty?
|
||||
continuation.write(Bytes[0x32])
|
||||
continuation.write(write_var_int(video_id.size + 8))
|
||||
data.write Bytes[0x32]
|
||||
VarInt.to_io(data, cursor.bytesize + video_id.bytesize + 8)
|
||||
|
||||
continuation.write(Bytes[0x22, video_id.size + 4])
|
||||
continuation.write(Bytes[0x22, video_id.size])
|
||||
continuation.print(video_id)
|
||||
data.write Bytes[0x22, video_id.bytesize + 4]
|
||||
data.write Bytes[0x22, video_id.bytesize]
|
||||
data.print video_id
|
||||
|
||||
case sort_by
|
||||
when "top"
|
||||
continuation.write(Bytes[0x30, 0x00])
|
||||
data.write Bytes[0x30, 0x00]
|
||||
when "new", "newest"
|
||||
continuation.write(Bytes[0x30, 0x01])
|
||||
data.write Bytes[0x30, 0x01]
|
||||
end
|
||||
|
||||
continuation.write(Bytes[0x78, 0x02])
|
||||
data.write(Bytes[0x78, 0x02])
|
||||
else
|
||||
continuation.write(Bytes[0x32])
|
||||
continuation.write(write_var_int(cursor.size + video_id.size + 11))
|
||||
data.write Bytes[0x32]
|
||||
VarInt.to_io(data, cursor.bytesize + video_id.bytesize + 11)
|
||||
|
||||
continuation.write(Bytes[0x0a])
|
||||
continuation.write(write_var_int(cursor.size))
|
||||
continuation.print(cursor)
|
||||
data.write_byte 0x0a
|
||||
VarInt.to_io(data, cursor.bytesize)
|
||||
data.print cursor
|
||||
|
||||
continuation.write(Bytes[0x22, video_id.size + 4])
|
||||
continuation.write(Bytes[0x22, video_id.size])
|
||||
continuation.print(video_id)
|
||||
data.write Bytes[0x22, video_id.bytesize + 4]
|
||||
data.write Bytes[0x22, video_id.bytesize]
|
||||
data.print video_id
|
||||
|
||||
case sort_by
|
||||
when "top"
|
||||
continuation.write(Bytes[0x30, 0x00])
|
||||
data.write Bytes[0x30, 0x00]
|
||||
when "new", "newest"
|
||||
continuation.write(Bytes[0x30, 0x01])
|
||||
data.write Bytes[0x30, 0x01]
|
||||
end
|
||||
|
||||
continuation.write(Bytes[0x28, 0x14])
|
||||
data.write Bytes[0x28, 0x14]
|
||||
end
|
||||
|
||||
continuation.rewind
|
||||
continuation = continuation.gets_to_end
|
||||
|
||||
continuation = Base64.urlsafe_encode(continuation.to_slice)
|
||||
continuation = Base64.urlsafe_encode(data)
|
||||
continuation = URI.escape(continuation)
|
||||
|
||||
return continuation
|
||||
end
|
||||
|
||||
def produce_comment_reply_continuation(video_id, ucid, comment_id)
|
||||
continuation = IO::Memory.new
|
||||
data = IO::Memory.new
|
||||
|
||||
continuation.write(Bytes[0x12, 0x26])
|
||||
data.write Bytes[0x12, 0x26]
|
||||
|
||||
continuation.write(Bytes[0x12, video_id.size])
|
||||
continuation.print(video_id)
|
||||
data.write_byte 0x12
|
||||
VarInt.to_io(data, video_id.size)
|
||||
data.print video_id
|
||||
|
||||
continuation.write(Bytes[0xc0, 0x01, 0x01])
|
||||
continuation.write(Bytes[0xc8, 0x01, 0x01])
|
||||
continuation.write(Bytes[0xe0, 0x01, 0x01])
|
||||
data.write Bytes[0xc0, 0x01, 0x01]
|
||||
data.write Bytes[0xc8, 0x01, 0x01]
|
||||
data.write Bytes[0xe0, 0x01, 0x01]
|
||||
|
||||
continuation.write(Bytes[0xa2, 0x02, 0x0d])
|
||||
continuation.write(Bytes[0x28, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x01])
|
||||
data.write Bytes[0xa2, 0x02, 0x0d]
|
||||
data.write Bytes[0x28, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x01]
|
||||
|
||||
continuation.write(Bytes[0x40, 0x00])
|
||||
continuation.write(Bytes[0x18, 0x06])
|
||||
data.write Bytes[0x40, 0x00]
|
||||
data.write Bytes[0x18, 0x06]
|
||||
|
||||
continuation.write(Bytes[0x32, ucid.size + video_id.size + comment_id.size + 16])
|
||||
continuation.write(Bytes[0x1a, ucid.size + video_id.size + comment_id.size + 14])
|
||||
data.write(Bytes[0x32, ucid.size + video_id.size + comment_id.size + 16])
|
||||
data.write(Bytes[0x1a, ucid.size + video_id.size + comment_id.size + 14])
|
||||
|
||||
continuation.write(Bytes[0x12, comment_id.size])
|
||||
continuation.print(comment_id)
|
||||
data.write_byte 0x12
|
||||
VarInt.to_io(data, comment_id.size)
|
||||
data.print comment_id
|
||||
|
||||
continuation.write(Bytes[0x22, 0x02, 0x08, 0x00]) # ??
|
||||
data.write(Bytes[0x22, 0x02, 0x08, 0x00]) # ??
|
||||
|
||||
continuation.write(Bytes[ucid.size + video_id.size + 7])
|
||||
continuation.write(Bytes[ucid.size])
|
||||
continuation.print(ucid)
|
||||
continuation.write(Bytes[0x32, video_id.size])
|
||||
continuation.print(video_id)
|
||||
continuation.write(Bytes[0x40, 0x01])
|
||||
continuation.write(Bytes[0x48, 0x0a])
|
||||
data.write(Bytes[ucid.size + video_id.size + 7])
|
||||
data.write(Bytes[ucid.size])
|
||||
data.print(ucid)
|
||||
data.write(Bytes[0x32, video_id.size])
|
||||
data.print(video_id)
|
||||
data.write(Bytes[0x40, 0x01])
|
||||
data.write(Bytes[0x48, 0x0a])
|
||||
|
||||
continuation.rewind
|
||||
continuation = continuation.gets_to_end
|
||||
|
||||
continuation = Base64.urlsafe_encode(continuation.to_slice)
|
||||
continuation = Base64.urlsafe_encode(data.to_slice)
|
||||
continuation = URI.escape(continuation)
|
||||
|
||||
return continuation
|
||||
|
@ -87,12 +87,40 @@ end
|
||||
|
||||
struct Config
|
||||
module ConfigPreferencesConverter
|
||||
def self.to_yaml(value : Preferences, yaml : YAML::Nodes::Builder)
|
||||
value.to_yaml(yaml)
|
||||
end
|
||||
|
||||
def self.from_yaml(ctx : YAML::ParseContext, node : YAML::Nodes::Node) : Preferences
|
||||
Preferences.new(*ConfigPreferences.new(ctx, node).to_tuple)
|
||||
end
|
||||
end
|
||||
|
||||
def self.to_yaml(value : Preferences, yaml : YAML::Nodes::Builder)
|
||||
value.to_yaml(yaml)
|
||||
module FamilyConverter
|
||||
def self.to_yaml(value : Socket::Family, yaml : YAML::Nodes::Builder)
|
||||
case value
|
||||
when Socket::Family::UNSPEC
|
||||
yaml.scalar nil
|
||||
when Socket::Family::INET
|
||||
yaml.scalar "ipv4"
|
||||
when Socket::Family::INET6
|
||||
yaml.scalar "ipv6"
|
||||
end
|
||||
end
|
||||
|
||||
def self.from_yaml(ctx : YAML::ParseContext, node : YAML::Nodes::Node) : Socket::Family
|
||||
if node.is_a?(YAML::Nodes::Scalar)
|
||||
case node.value.downcase
|
||||
when "ipv4"
|
||||
Socket::Family::INET
|
||||
when "ipv6"
|
||||
Socket::Family::INET6
|
||||
else
|
||||
Socket::Family::UNSPEC
|
||||
end
|
||||
else
|
||||
node.raise "Expected scalar, not #{node.class}"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@ -131,12 +159,13 @@ struct Config
|
||||
default: Preferences.new(*ConfigPreferences.from_yaml("").to_tuple),
|
||||
converter: ConfigPreferencesConverter,
|
||||
},
|
||||
dmca_content: {type: Array(String), default: [] of String}, # For compliance with DMCA, disables download widget using list of video IDs
|
||||
check_tables: {type: Bool, default: false}, # Check table integrity, automatically try to add any missing columns, create tables, etc.
|
||||
cache_annotations: {type: Bool, default: false}, # Cache annotations requested from IA, will not cache empty annotations or annotations that only contain cards
|
||||
banner: {type: String?, default: nil}, # Optional banner to be displayed along top of page for announcements, etc.
|
||||
hsts: {type: Bool?, default: true}, # Enables 'Strict-Transport-Security'. Ensure that `domain` and all subdomains are served securely
|
||||
disable_proxy: {type: Bool? | Array(String)?, default: false}, # Disable proxying server-wide: options: 'dash', 'livestreams', 'downloads', 'local'
|
||||
dmca_content: {type: Array(String), default: [] of String}, # For compliance with DMCA, disables download widget using list of video IDs
|
||||
check_tables: {type: Bool, default: false}, # Check table integrity, automatically try to add any missing columns, create tables, etc.
|
||||
cache_annotations: {type: Bool, default: false}, # Cache annotations requested from IA, will not cache empty annotations or annotations that only contain cards
|
||||
banner: {type: String?, default: nil}, # Optional banner to be displayed along top of page for announcements, etc.
|
||||
hsts: {type: Bool?, default: true}, # Enables 'Strict-Transport-Security'. Ensure that `domain` and all subdomains are served securely
|
||||
disable_proxy: {type: Bool? | Array(String)?, default: false}, # Disable proxying server-wide: options: 'dash', 'livestreams', 'downloads', 'local'
|
||||
force_resolve: {type: Socket::Family, default: Socket::Family::UNSPEC, converter: FamilyConverter}, # Connect to YouTube over 'ipv6', 'ipv4'. Will sometimes resolve fix issues with rate-limiting (see https://github.com/ytdl-org/youtube-dl/issues/21729)
|
||||
})
|
||||
end
|
||||
|
||||
@ -650,48 +679,6 @@ def cache_annotation(db, id, annotations)
|
||||
end
|
||||
end
|
||||
|
||||
def proxy_file(response, env)
|
||||
if response.headers.includes_word?("Content-Encoding", "gzip")
|
||||
Gzip::Writer.open(env.response) do |deflate|
|
||||
response.pipe(deflate)
|
||||
end
|
||||
elsif response.headers.includes_word?("Content-Encoding", "deflate")
|
||||
Flate::Writer.open(env.response) do |deflate|
|
||||
response.pipe(deflate)
|
||||
end
|
||||
else
|
||||
response.pipe(env.response)
|
||||
end
|
||||
end
|
||||
|
||||
class HTTP::Client::Response
|
||||
def pipe(io)
|
||||
HTTP.serialize_body(io, headers, @body, @body_io, @version)
|
||||
end
|
||||
end
|
||||
|
||||
# Supports serialize_body without first writing headers
|
||||
module HTTP
|
||||
def self.serialize_body(io, headers, body, body_io, version)
|
||||
if body
|
||||
io << body
|
||||
elsif body_io
|
||||
content_length = content_length(headers)
|
||||
if content_length
|
||||
copied = IO.copy(body_io, io)
|
||||
if copied != content_length
|
||||
raise ArgumentError.new("Content-Length header is #{content_length} but body had #{copied} bytes")
|
||||
end
|
||||
elsif Client::Response.supports_chunked?(version)
|
||||
headers["Transfer-Encoding"] = "chunked"
|
||||
serialize_chunked_body(io, body_io)
|
||||
else
|
||||
io << body
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def create_notification_stream(env, config, kemal_config, decrypt_function, topics, connection_channel)
|
||||
connection = Channel(PQ::Notification).new(8)
|
||||
connection_channel.send({true, connection})
|
||||
@ -834,3 +821,79 @@ def extract_initial_data(body)
|
||||
return JSON.parse(initial_data)
|
||||
end
|
||||
end
|
||||
|
||||
def proxy_file(response, env)
|
||||
if response.headers.includes_word?("Content-Encoding", "gzip")
|
||||
Gzip::Writer.open(env.response) do |deflate|
|
||||
response.pipe(deflate)
|
||||
end
|
||||
elsif response.headers.includes_word?("Content-Encoding", "deflate")
|
||||
Flate::Writer.open(env.response) do |deflate|
|
||||
response.pipe(deflate)
|
||||
end
|
||||
else
|
||||
response.pipe(env.response)
|
||||
end
|
||||
end
|
||||
|
||||
class HTTP::Client::Response
|
||||
def pipe(io)
|
||||
HTTP.serialize_body(io, headers, @body, @body_io, @version)
|
||||
end
|
||||
end
|
||||
|
||||
# Supports serialize_body without first writing headers
|
||||
module HTTP
|
||||
def self.serialize_body(io, headers, body, body_io, version)
|
||||
if body
|
||||
io << body
|
||||
elsif body_io
|
||||
content_length = content_length(headers)
|
||||
if content_length
|
||||
copied = IO.copy(body_io, io)
|
||||
if copied != content_length
|
||||
raise ArgumentError.new("Content-Length header is #{content_length} but body had #{copied} bytes")
|
||||
end
|
||||
elsif Client::Response.supports_chunked?(version)
|
||||
headers["Transfer-Encoding"] = "chunked"
|
||||
serialize_chunked_body(io, body_io)
|
||||
else
|
||||
io << body
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
class HTTP::Client
|
||||
property family : Socket::Family = Socket::Family::UNSPEC
|
||||
|
||||
private def socket
|
||||
socket = @socket
|
||||
return socket if socket
|
||||
|
||||
hostname = @host.starts_with?('[') && @host.ends_with?(']') ? @host[1..-2] : @host
|
||||
socket = TCPSocket.new hostname, @port, @dns_timeout, @connect_timeout, @family
|
||||
socket.read_timeout = @read_timeout if @read_timeout
|
||||
socket.sync = false
|
||||
|
||||
{% if !flag?(:without_openssl) %}
|
||||
if tls = @tls
|
||||
socket = OpenSSL::SSL::Socket::Client.new(socket, context: tls, sync_close: true, hostname: @host)
|
||||
end
|
||||
{% end %}
|
||||
|
||||
@socket = socket
|
||||
end
|
||||
end
|
||||
|
||||
class TCPSocket
|
||||
def initialize(host, port, dns_timeout = nil, connect_timeout = nil, family = Socket::Family::UNSPEC)
|
||||
Addrinfo.tcp(host, port, timeout: dns_timeout, family: family) do |addrinfo|
|
||||
super(addrinfo.family, addrinfo.type, addrinfo.protocol)
|
||||
connect(addrinfo, timeout: connect_timeout) do |error|
|
||||
close
|
||||
error
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -20,6 +20,7 @@ end
|
||||
|
||||
def make_client(url : URI, region = nil)
|
||||
client = HTTPClient.new(url)
|
||||
client.family = CONFIG.force_resolve
|
||||
client.read_timeout = 15.seconds
|
||||
client.connect_timeout = 15.seconds
|
||||
|
||||
@ -265,50 +266,40 @@ def get_referer(env, fallback = "/", unroll = true)
|
||||
return referer
|
||||
end
|
||||
|
||||
def read_var_int(bytes)
|
||||
num_read = 0
|
||||
result = 0
|
||||
struct VarInt
|
||||
def self.from_io(io : IO, format = IO::ByteFormat::BigEndian) : Int32
|
||||
result = 0_i32
|
||||
num_read = 0
|
||||
|
||||
read = bytes[num_read]
|
||||
|
||||
if bytes.size == 1
|
||||
result = bytes[0].to_i32
|
||||
else
|
||||
while ((read & 0b10000000) != 0)
|
||||
read = bytes[num_read].to_u64
|
||||
value = (read & 0b01111111)
|
||||
result |= (value << (7 * num_read))
|
||||
loop do
|
||||
byte = io.read_byte
|
||||
raise "Invalid VarInt" if !byte
|
||||
value = byte & 0x7f
|
||||
|
||||
result |= value.to_i32 << (7 * num_read)
|
||||
num_read += 1
|
||||
if num_read > 5
|
||||
raise "VarInt is too big"
|
||||
end
|
||||
|
||||
break if byte & 0x80 == 0
|
||||
raise "Invalid VarInt" if num_read > 5
|
||||
end
|
||||
|
||||
result
|
||||
end
|
||||
|
||||
return result
|
||||
end
|
||||
def self.to_io(io : IO, value : Int32)
|
||||
io.write_byte 0x00 if value == 0x00
|
||||
|
||||
def write_var_int(value : Int)
|
||||
bytes = [] of UInt8
|
||||
value = value.to_u32
|
||||
|
||||
if value == 0
|
||||
bytes = [0_u8]
|
||||
else
|
||||
while value != 0
|
||||
temp = (value & 0b01111111).to_u8
|
||||
value = value >> 7
|
||||
byte = (value & 0x7f).to_u8
|
||||
value >>= 7
|
||||
|
||||
if value != 0
|
||||
temp |= 0b10000000
|
||||
byte |= 0x80
|
||||
end
|
||||
|
||||
bytes << temp
|
||||
io.write_byte byte
|
||||
end
|
||||
end
|
||||
|
||||
return Slice.new(bytes.to_unsafe, bytes.size)
|
||||
end
|
||||
|
||||
def sha256(text)
|
||||
|
@ -157,37 +157,44 @@ def produce_playlist_url(id, index)
|
||||
end
|
||||
ucid = "VL" + id
|
||||
|
||||
meta = IO::Memory.new
|
||||
meta.write(Bytes[0x08])
|
||||
meta.write(write_var_int(index))
|
||||
data = IO::Memory.new
|
||||
data.write_byte 0x08
|
||||
VarInt.to_io(data, index)
|
||||
|
||||
meta.rewind
|
||||
meta = Base64.urlsafe_encode(meta.to_slice, false)
|
||||
meta = "PT:#{meta}"
|
||||
data.rewind
|
||||
data = Base64.urlsafe_encode(data, false)
|
||||
data = "PT:#{data}"
|
||||
|
||||
continuation = IO::Memory.new
|
||||
continuation.write(Bytes[0x7a, meta.size])
|
||||
continuation.print(meta)
|
||||
continuation.write_byte 0x7a
|
||||
VarInt.to_io(continuation, data.bytesize)
|
||||
continuation.print data
|
||||
|
||||
continuation.rewind
|
||||
meta = Base64.urlsafe_encode(continuation.to_slice)
|
||||
meta = URI.escape(meta)
|
||||
data = Base64.urlsafe_encode(continuation)
|
||||
cursor = URI.escape(data)
|
||||
|
||||
continuation = IO::Memory.new
|
||||
continuation.write(Bytes[0x12, ucid.size])
|
||||
continuation.print(ucid)
|
||||
continuation.write(Bytes[0x1a, meta.size])
|
||||
continuation.print(meta)
|
||||
data = IO::Memory.new
|
||||
|
||||
wrapper = IO::Memory.new
|
||||
wrapper.write(Bytes[0xe2, 0xa9, 0x85, 0xb2, 0x02, continuation.size])
|
||||
wrapper.print(continuation)
|
||||
wrapper.rewind
|
||||
data.write_byte 0x12
|
||||
VarInt.to_io(data, ucid.bytesize)
|
||||
data.print ucid
|
||||
|
||||
wrapper = Base64.urlsafe_encode(wrapper.to_slice)
|
||||
wrapper = URI.escape(wrapper)
|
||||
data.write_byte 0x1a
|
||||
VarInt.to_io(data, cursor.bytesize)
|
||||
data.print cursor
|
||||
|
||||
url = "/browse_ajax?continuation=#{wrapper}&gl=US&hl=en"
|
||||
data.rewind
|
||||
|
||||
buffer = IO::Memory.new
|
||||
buffer.write Bytes[0xe2, 0xa9, 0x85, 0xb2, 0x02]
|
||||
VarInt.to_io(buffer, data.bytesize)
|
||||
|
||||
IO.copy data, buffer
|
||||
|
||||
continuation = Base64.urlsafe_encode(buffer)
|
||||
continuation = URI.escape(continuation)
|
||||
|
||||
url = "/browse_ajax?continuation=#{continuation}&gl=US&hl=en"
|
||||
|
||||
return url
|
||||
end
|
||||
|
@ -374,45 +374,51 @@ end
|
||||
def produce_channel_search_url(ucid, query, page)
|
||||
page = "#{page}"
|
||||
|
||||
meta = IO::Memory.new
|
||||
meta.write(Bytes[0x12, 0x06])
|
||||
meta.print("search")
|
||||
data = IO::Memory.new
|
||||
data.write_byte 0x12
|
||||
data.write_byte 0x06
|
||||
data.print "search"
|
||||
|
||||
meta.write(Bytes[0x30, 0x02])
|
||||
meta.write(Bytes[0x38, 0x01])
|
||||
meta.write(Bytes[0x60, 0x01])
|
||||
meta.write(Bytes[0x6a, 0x00])
|
||||
meta.write(Bytes[0xb8, 0x01, 0x00])
|
||||
data.write Bytes[0x30, 0x02]
|
||||
data.write Bytes[0x38, 0x01]
|
||||
data.write Bytes[0x60, 0x01]
|
||||
data.write Bytes[0x6a, 0x00]
|
||||
data.write Bytes[0xb8, 0x01, 0x00]
|
||||
|
||||
meta.write(Bytes[0x7a, page.size])
|
||||
meta.print(page)
|
||||
data.write_byte 0x7a
|
||||
VarInt.to_io(data, page.bytesize)
|
||||
data.print page
|
||||
|
||||
meta.rewind
|
||||
meta = Base64.urlsafe_encode(meta.to_slice)
|
||||
meta = URI.escape(meta)
|
||||
data.rewind
|
||||
data = Base64.urlsafe_encode(data)
|
||||
continuation = URI.escape(data)
|
||||
|
||||
continuation = IO::Memory.new
|
||||
continuation.write(Bytes[0x12, ucid.size])
|
||||
continuation.print(ucid)
|
||||
data = IO::Memory.new
|
||||
|
||||
continuation.write(Bytes[0x1a, meta.size])
|
||||
continuation.print(meta)
|
||||
data.write_byte 0x12
|
||||
VarInt.to_io(data, ucid.bytesize)
|
||||
data.print ucid
|
||||
|
||||
continuation.write(Bytes[0x5a, query.size])
|
||||
continuation.print(query)
|
||||
data.write_byte 0x1a
|
||||
VarInt.to_io(data, continuation.bytesize)
|
||||
data.print continuation
|
||||
|
||||
continuation.rewind
|
||||
continuation = continuation.gets_to_end
|
||||
data.write_byte 0x5a
|
||||
VarInt.to_io(data, query.bytesize)
|
||||
data.print query
|
||||
|
||||
wrapper = IO::Memory.new
|
||||
wrapper.write(Bytes[0xe2, 0xa9, 0x85, 0xb2, 0x02, continuation.size])
|
||||
wrapper.print(continuation)
|
||||
wrapper.rewind
|
||||
data.rewind
|
||||
|
||||
wrapper = Base64.urlsafe_encode(wrapper.to_slice)
|
||||
wrapper = URI.escape(wrapper)
|
||||
buffer = IO::Memory.new
|
||||
buffer.write Bytes[0xe2, 0xa9, 0x85, 0xb2, 0x02]
|
||||
VarInt.to_io(buffer, data.bytesize)
|
||||
|
||||
url = "/browse_ajax?continuation=#{wrapper}&gl=US&hl=en"
|
||||
IO.copy data, buffer
|
||||
|
||||
continuation = Base64.urlsafe_encode(buffer)
|
||||
continuation = URI.escape(continuation)
|
||||
|
||||
url = "/browse_ajax?continuation=#{continuation}&gl=US&hl=en"
|
||||
|
||||
return url
|
||||
end
|
||||
|
@ -329,7 +329,7 @@ struct Video
|
||||
|
||||
json.field "subCountText", self.sub_count_text
|
||||
|
||||
json.field "lengthSeconds", self.info["length_seconds"].to_i
|
||||
json.field "lengthSeconds", self.length_seconds
|
||||
json.field "allowRatings", self.allow_ratings
|
||||
json.field "rating", self.info["avg_rating"].to_f32
|
||||
json.field "isListed", self.is_listed
|
||||
@ -563,7 +563,14 @@ struct Video
|
||||
fmt["clen"] = fmt_stream["contentLength"]?.try &.as_s || "0"
|
||||
fmt["bitrate"] = fmt_stream["bitrate"]?.try &.as_i.to_s || "0"
|
||||
fmt["itag"] = fmt_stream["itag"].as_i.to_s
|
||||
fmt["url"] = fmt_stream["url"].as_s
|
||||
if fmt_stream["url"]?
|
||||
fmt["url"] = fmt_stream["url"].as_s
|
||||
end
|
||||
if fmt_stream["cipher"]?
|
||||
HTTP::Params.parse(fmt_stream["cipher"].as_s).each do |key, value|
|
||||
fmt[key] = value
|
||||
end
|
||||
end
|
||||
fmt["quality"] = fmt_stream["quality"].as_s
|
||||
|
||||
if fmt_stream["width"]?
|
||||
@ -635,8 +642,14 @@ struct Video
|
||||
fmt["clen"] = adaptive_fmt["contentLength"]?.try &.as_s || "0"
|
||||
fmt["bitrate"] = adaptive_fmt["bitrate"]?.try &.as_i.to_s || "0"
|
||||
fmt["itag"] = adaptive_fmt["itag"].as_i.to_s
|
||||
fmt["url"] = adaptive_fmt["url"].as_s
|
||||
|
||||
if adaptive_fmt["url"]?
|
||||
fmt["url"] = adaptive_fmt["url"].as_s
|
||||
end
|
||||
if adaptive_fmt["cipher"]?
|
||||
HTTP::Params.parse(adaptive_fmt["cipher"].as_s).each do |key, value|
|
||||
fmt[key] = value
|
||||
end
|
||||
end
|
||||
if index = adaptive_fmt["indexRange"]?
|
||||
fmt["index"] = "#{index["start"]}-#{index["end"]}"
|
||||
end
|
||||
@ -827,7 +840,7 @@ struct Video
|
||||
end
|
||||
|
||||
def length_seconds
|
||||
return self.info["length_seconds"].to_i
|
||||
self.player_response["videoDetails"]["lengthSeconds"].as_s.to_i
|
||||
end
|
||||
|
||||
db_mapping({
|
||||
@ -1162,17 +1175,19 @@ def fetch_video(id, region)
|
||||
end
|
||||
end
|
||||
|
||||
if info["errorcode"]?.try &.== "2"
|
||||
if info["errorcode"]?.try &.== "2" || !info["player_response"]
|
||||
raise "Video unavailable."
|
||||
end
|
||||
|
||||
if !info["title"]? || info["title"].empty?
|
||||
raise "Video unavailable."
|
||||
if info["reason"]?
|
||||
raise info["reason"]
|
||||
end
|
||||
|
||||
title = info["title"]
|
||||
author = info["author"]? || ""
|
||||
ucid = info["ucid"]? || ""
|
||||
player_json = JSON.parse(info["player_response"])
|
||||
|
||||
title = player_json["videoDetails"]["title"].as_s
|
||||
author = player_json["videoDetails"]["author"]?.try &.as_s || ""
|
||||
ucid = player_json["videoDetails"]["ucid"]?.try &.as_s || ""
|
||||
|
||||
views = html.xpath_node(%q(//meta[@itemprop="interactionCount"]))
|
||||
.try &.["content"].to_i64? || 0_i64
|
||||
|
@ -30,7 +30,7 @@
|
||||
var video_data = {
|
||||
id: '<%= video.id %>',
|
||||
plid: '<%= plid %>',
|
||||
length_seconds: '<%= video.info["length_seconds"].to_f %>',
|
||||
length_seconds: '<%= video.length_seconds.to_f %>',
|
||||
video_series: <%= video_series.to_json %>,
|
||||
params: <%= params.to_json %>,
|
||||
preferences: <%= preferences.to_json %>,
|
||||
|
@ -102,8 +102,8 @@
|
||||
<% end %>
|
||||
|
||||
<% if tfa %>
|
||||
<label for="tfa"><%= translate(locale, "Google verification code") %> :</label>
|
||||
<input required class="pure-input-1" name="tfa" type="text" placeholder="<%= translate(locale, "Google verification code") %>">
|
||||
<label for="tfa"><%= translate(locale, prompt) %> :</label>
|
||||
<input required class="pure-input-1" name="tfa" type="text" placeholder="<%= translate(locale, prompt) %>">
|
||||
<% end %>
|
||||
|
||||
<button type="submit" class="pure-button pure-button-primary"><%= translate(locale, "Sign In") %></button>
|
||||
|
@ -85,7 +85,7 @@
|
||||
<div class="pure-g h-box">
|
||||
<div class="pure-u-1 pure-u-md-4-5"></div>
|
||||
<div class="pure-u-1 pure-u-lg-1-5" style="text-align:right">
|
||||
<% if items.size >= 28 %>
|
||||
<% if continuation %>
|
||||
<a href="/channel/<%= channel.ucid %>/playlists?continuation=<%= continuation %><% if sort_by != "last" %>&sort_by=<%= sort_by %><% end %>">
|
||||
<%= translate(locale, "Next page") %>
|
||||
</a>
|
||||
|
@ -34,7 +34,7 @@ function update_value(element) {
|
||||
</div>
|
||||
|
||||
<div class="pure-control-group">
|
||||
<label for="local"><%= translate(locale, "Proxy videos? ") %></label>
|
||||
<label for="local"><%= translate(locale, "Proxy videos: ") %></label>
|
||||
<input name="local" id="local" type="checkbox" <% if preferences.local && !CONFIG.disabled?("local") %>checked<% end %> <% if CONFIG.disabled?("local") %>disabled<% end %>>
|
||||
</div>
|
||||
|
||||
@ -46,7 +46,7 @@ function update_value(element) {
|
||||
<div class="pure-control-group">
|
||||
<label for="speed"><%= translate(locale, "Default speed: ") %></label>
|
||||
<select name="speed" id="speed">
|
||||
<% {2.0, 1.5, 1.25, 1.0, 0.75, 0.5, 0.25}.each do |option| %>
|
||||
<% {2.0, 1.75, 1.5, 1.25, 1.0, 0.75, 0.5, 0.25}.each do |option| %>
|
||||
<option <% if preferences.speed == option %> selected <% end %>><%= option %></option>
|
||||
<% end %>
|
||||
</select>
|
||||
@ -92,12 +92,12 @@ function update_value(element) {
|
||||
</div>
|
||||
|
||||
<div class="pure-control-group">
|
||||
<label for="related_videos"><%= translate(locale, "Show related videos? ") %></label>
|
||||
<label for="related_videos"><%= translate(locale, "Show related videos: ") %></label>
|
||||
<input name="related_videos" id="related_videos" type="checkbox" <% if preferences.related_videos %>checked<% end %>>
|
||||
</div>
|
||||
|
||||
<div class="pure-control-group">
|
||||
<label for="annotations"><%= translate(locale, "Show annotations by default? ") %></label>
|
||||
<label for="annotations"><%= translate(locale, "Show annotations by default: ") %></label>
|
||||
<input name="annotations" id="annotations" type="checkbox" <% if preferences.annotations %>checked<% end %>>
|
||||
</div>
|
||||
|
||||
@ -126,7 +126,7 @@ function update_value(element) {
|
||||
<legend><%= translate(locale, "Subscription preferences") %></legend>
|
||||
|
||||
<div class="pure-control-group">
|
||||
<label for="annotations_subscribed"><%= translate(locale, "Show annotations by default for subscribed channels? ") %></label>
|
||||
<label for="annotations_subscribed"><%= translate(locale, "Show annotations by default for subscribed channels: ") %></label>
|
||||
<input name="annotations_subscribed" id="annotations_subscribed" type="checkbox" <% if preferences.annotations_subscribed %>checked<% end %>>
|
||||
</div>
|
||||
|
||||
@ -200,27 +200,27 @@ function update_value(element) {
|
||||
</div>
|
||||
|
||||
<div class="pure-control-group">
|
||||
<label for="top_enabled"><%= translate(locale, "Top enabled? ") %></label>
|
||||
<label for="top_enabled"><%= translate(locale, "Top enabled: ") %></label>
|
||||
<input name="top_enabled" id="top_enabled" type="checkbox" <% if config.top_enabled %>checked<% end %>>
|
||||
</div>
|
||||
|
||||
<div class="pure-control-group">
|
||||
<label for="captcha_enabled"><%= translate(locale, "CAPTCHA enabled? ") %></label>
|
||||
<label for="captcha_enabled"><%= translate(locale, "CAPTCHA enabled: ") %></label>
|
||||
<input name="captcha_enabled" id="captcha_enabled" type="checkbox" <% if config.captcha_enabled %>checked<% end %>>
|
||||
</div>
|
||||
|
||||
<div class="pure-control-group">
|
||||
<label for="login_enabled"><%= translate(locale, "Login enabled? ") %></label>
|
||||
<label for="login_enabled"><%= translate(locale, "Login enabled: ") %></label>
|
||||
<input name="login_enabled" id="login_enabled" type="checkbox" <% if config.login_enabled %>checked<% end %>>
|
||||
</div>
|
||||
|
||||
<div class="pure-control-group">
|
||||
<label for="registration_enabled"><%= translate(locale, "Registration enabled? ") %></label>
|
||||
<label for="registration_enabled"><%= translate(locale, "Registration enabled: ") %></label>
|
||||
<input name="registration_enabled" id="registration_enabled" type="checkbox" <% if config.registration_enabled %>checked<% end %>>
|
||||
</div>
|
||||
|
||||
<div class="pure-control-group">
|
||||
<label for="statistics_enabled"><%= translate(locale, "Report statistics? ") %></label>
|
||||
<label for="statistics_enabled"><%= translate(locale, "Report statistics: ") %></label>
|
||||
<input name="statistics_enabled" id="statistics_enabled" type="checkbox" <% if config.statistics_enabled %>checked<% end %>>
|
||||
</div>
|
||||
<% end %>
|
||||
|
@ -35,7 +35,7 @@
|
||||
<div class="pure-u-1 pure-u-md-12-24 searchbar">
|
||||
<form class="pure-form" action="/search" method="get">
|
||||
<fieldset>
|
||||
<input type="search" style="width:100%" name="q" placeholder="<%= translate(locale, "search") %>" value="<%= env.get?("search").try {|x| HTML.escape(x.as(String)) } || env.params.query["q"]?.try {|x| HTML.escape(x)} %>">
|
||||
<input type="search" style="width:100%" name="q" placeholder="<%= translate(locale, "search") %>" value="<%= env.get?("search").try {|x| HTML.escape(x.as(String)) } %>">
|
||||
</fieldset>
|
||||
</form>
|
||||
</div>
|
||||
|
@ -30,7 +30,7 @@
|
||||
var video_data = {
|
||||
id: '<%= video.id %>',
|
||||
plid: '<%= plid %>',
|
||||
length_seconds: <%= video.info["length_seconds"].to_f %>,
|
||||
length_seconds: <%= video.length_seconds.to_f %>,
|
||||
play_next: <%= !rvs.empty? && !plid && params.continue %>,
|
||||
next_video: '<%= rvs.select { |rv| rv["id"]? }[0]?.try &.["id"] %>',
|
||||
youtube_comments_text: '<%= HTML.escape(translate(locale, "View YouTube comments")) %>',
|
||||
|
Reference in New Issue
Block a user