Compare commits

...

20 Commits

Author SHA1 Message Date
ff5d79e3ee Update video extractor 2019-07-29 19:41:45 -05:00
4ee3ec09df Autofill search for playlists and communities page 2019-07-27 08:51:10 -05:00
cfe9d47fa0 Add support for '/embed/?list' 2019-07-25 10:36:35 -05:00
607d6125fc Add support for '/embed/live_stream' 2019-07-24 19:18:26 -05:00
6215259565 Add support for Google login verification 2019-07-22 13:28:36 -05:00
d034fecc89 Remove default arguments from function definitions 2019-07-20 20:33:44 -05:00
f18d8229c0 Refactor continuation protocol buffers 2019-07-20 20:18:08 -05:00
e736626953 Fix continuation for last page of playlists 2019-07-20 11:38:20 -05:00
c2c438637a Merge remote-tracking branch 'weblate/master' 2019-07-18 21:58:51 -05:00
94638fe42c Update translations 2019-07-18 21:52:25 -05:00
55ecfda39a Update Icelandic translation 2019-07-18 21:52:25 -05:00
d97a272aa5 Fix check for 2-step verification 2019-07-18 21:52:24 -05:00
80a1944b9d Update Icelandic translation 2019-07-19 01:52:11 +02:00
138cf943a9 Update Icelandic translation 2019-07-19 01:52:11 +02:00
c7e672e533 Update Icelandic translation 2019-07-19 01:52:11 +02:00
1b74a04efd Add 'force_resolve' to fix issues with rate limiting 2019-07-18 18:51:10 -05:00
290c7e6009 Disable autoplay in community tabs 2019-07-14 10:13:40 -05:00
e8a56e0fea Add '1.75' playback speed 2019-07-14 10:13:40 -05:00
1ae7b646b3 Merge pull request #633 from EsmailELBoBDev2/patch-4
Update ar.json
2019-07-14 10:13:04 -05:00
42e2d73ce2 Update ar.json 2019-07-14 06:07:02 +00:00
36 changed files with 994 additions and 796 deletions

View File

@ -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;

View File

@ -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;

View File

@ -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;
}

View File

@ -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',

View File

@ -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) {

View File

@ -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: ": "الإصدار الحالى"
}

View File

@ -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",

View File

@ -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": "Διαχειριστής διασυνδέσεων",

View File

@ -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",

View File

@ -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: "
}
}

View File

@ -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",

View File

@ -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": "",

View File

@ -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",

View File

@ -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: "
}

View File

@ -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": "",

View File

@ -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: "
}
}

View File

@ -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",

View File

@ -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": "",

View File

@ -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": "Менеджер токенов",

View File

@ -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": "Менеджер токенів",

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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 %>,

View File

@ -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>

View File

@ -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>

View File

@ -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 %>

View File

@ -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>

View File

@ -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")) %>',