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; order: 6;
} }
.vjs-playback-rate > .vjs-menu {
width: 50px;
}
.vjs-control-bar { .vjs-control-bar {
display: flex; display: flex;
flex-direction: row; 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) { if (retries <= 0) {
console.log('Failed to pull playlist'); console.log('Failed to pull playlist');
return; return;

View File

@ -1,6 +1,8 @@
var notifications, delivered; var notifications, delivered;
function get_subscriptions(callback, retries = 5) { function get_subscriptions(callback, retries) {
if (retries == undefined) retries = 5;
if (retries <= 0) { if (retries <= 0) {
return; return;
} }

View File

@ -1,7 +1,7 @@
var options = { var options = {
preload: 'auto', preload: 'auto',
liveui: true, 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: { controlBar: {
children: [ children: [
'playToggle', 'playToggle',

View File

@ -109,7 +109,8 @@ function number_with_separator(val) {
return val; return val;
} }
function get_playlist(plid, retries = 5) { function get_playlist(plid, retries) {
if (retries == undefined) retries = 5;
playlist = document.getElementById('playlist'); playlist = document.getElementById('playlist');
if (retries <= 0) { if (retries <= 0) {
@ -194,7 +195,8 @@ function get_playlist(plid, retries = 5) {
xhr.send(); xhr.send();
} }
function get_reddit_comments(retries = 5) { function get_reddit_comments(retries) {
if (retries == undefined) retries = 5;
comments = document.getElementById('comments'); comments = document.getElementById('comments');
if (retries <= 0) { if (retries <= 0) {
@ -270,7 +272,8 @@ function get_reddit_comments(retries = 5) {
xhr.send(); xhr.send();
} }
function get_youtube_comments(retries = 5) { function get_youtube_comments(retries) {
if (retries == undefined) retries = 5;
comments = document.getElementById('comments'); comments = document.getElementById('comments');
if (retries <= 0) { if (retries <= 0) {

View File

@ -56,7 +56,7 @@
"Play next by default: ": "شغل الفيديو التالى تلقائيا", "Play next by default: ": "شغل الفيديو التالى تلقائيا",
"Autoplay next video: ": " شغل الفيديو التالى تلقائيا (فى قوائم التشغيل)", "Autoplay next video: ": " شغل الفيديو التالى تلقائيا (فى قوائم التشغيل)",
"Listen by default: ": "تشغيل النسخة السمعية تلقائى: ", "Listen by default: ": "تشغيل النسخة السمعية تلقائى: ",
"Proxy videos? ": "عرض الفيديوهات عن طريق الوكيل(proxy) ؟", "Proxy videos: ": "عرض الفيديوهات عن طريق الوكيل(proxy) ؟",
"Default speed: ": "السرعة الإفتراضية: ", "Default speed: ": "السرعة الإفتراضية: ",
"Preferred video quality: ": "الجودة المفضلة للفيديوهات: ", "Preferred video quality: ": "الجودة المفضلة للفيديوهات: ",
"Player volume: ": "صوت المشغل: ", "Player volume: ": "صوت المشغل: ",
@ -65,13 +65,13 @@
"reddit": "Reddit", "reddit": "Reddit",
"Default captions: ": "الترجمات الإفتراضية: ", "Default captions: ": "الترجمات الإفتراضية: ",
"Fallback captions: ": "الترجمات المصاحبة: ", "Fallback captions: ": "الترجمات المصاحبة: ",
"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: ": "عرض الملاحظات فى الفيديوهات تلقائيا فى القنوات المشترك بها فقط ؟",
"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: ": "ترتيب الفيديو بـ: ",
@ -99,11 +99,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: ": "إبلاغ الإحصائيات",
"Save preferences": "حفظ التفضيلات", "Save preferences": "حفظ التفضيلات",
"Subscription manager": "مدير الإشتراكات", "Subscription manager": "مدير الإشتراكات",
"Token manager": "إداره الرمز", "Token manager": "إداره الرمز",
@ -136,7 +136,7 @@
"Shared `x`": "شارك منذ `x`", "Shared `x`": "شارك منذ `x`",
"`x` views": "`x` مشاهدون", "`x` views": "`x` مشاهدون",
"Premieres in `x`": "يعرض فى `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.": "اهلا! يبدو ان الجافاسكريبت معطلة. اضغط هنا لعرض التعليقات, ضع فى إعتبارك انها ستأخذ وقت اطول للعرض.", "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 YouTube comments": "عرض تعليقات اليوتيوب",
"View more comments on Reddit": "عرض المزيد من التعليقات على\\من موقع Reddit", "View more comments on Reddit": "عرض المزيد من التعليقات على\\من موقع Reddit",
@ -310,12 +310,12 @@
"%A %B %-d, %Y": "", "%A %B %-d, %Y": "",
"(edited)": "(تم تعديلة)", "(edited)": "(تم تعديلة)",
"YouTube comment permalink": "رابط التعليق على اليوتيوب", "YouTube comment permalink": "رابط التعليق على اليوتيوب",
"permalink": "", "permalink": "الرابط",
"`x` marked it with a ❤": "`x` اعجب بهذا", "`x` marked it with a ❤": "`x` اعجب بهذا",
"Audio mode": "الوضع الصوتى", "Audio mode": "الوضع الصوتى",
"Video mode": "وضع الفيديو", "Video mode": "وضع الفيديو",
"Videos": "الفيديوهات", "Videos": "الفيديوهات",
"Playlists": "قوائم التشغيل", "Playlists": "قوائم التشغيل",
"Community": "", "Community": "المجتمع",
"Current version: ": "الإصدار الحالى" "Current version: ": "الإصدار الحالى"
} }

View File

@ -56,7 +56,7 @@
"Play next by default: ": "Standardmäßig als nächstes abspielen: ", "Play next by default: ": "Standardmäßig als nächstes abspielen: ",
"Autoplay next video: ": "nächstes Video automatisch abspielen: ", "Autoplay next video: ": "nächstes Video automatisch abspielen: ",
"Listen by default: ": "Nur Ton als Standard: ", "Listen by default: ": "Nur Ton als Standard: ",
"Proxy videos? ": "Proxy-Videos? ", "Proxy videos: ": "Proxy-Videos? ",
"Default speed: ": "Standardgeschwindigkeit: ", "Default speed: ": "Standardgeschwindigkeit: ",
"Preferred video quality: ": "Bevorzugte Videoqualität: ", "Preferred video quality: ": "Bevorzugte Videoqualität: ",
"Player volume: ": "Playerlautstärke: ", "Player volume: ": "Playerlautstärke: ",
@ -65,13 +65,13 @@
"reddit": "reddit", "reddit": "reddit",
"Default captions: ": "Standarduntertitel: ", "Default captions: ": "Standarduntertitel: ",
"Fallback captions: ": "Ersatzuntertitel: ", "Fallback captions: ": "Ersatzuntertitel: ",
"Show related videos? ": "Ähnliche Videos anzeigen? ", "Show related videos: ": "Ähnliche Videos anzeigen? ",
"Show annotations by default? ": "Standardmäßig Anmerkungen anzeigen? ", "Show annotations by default: ": "Standardmäßig Anmerkungen anzeigen? ",
"Visual preferences": "Anzeigeeinstellungen", "Visual preferences": "Anzeigeeinstellungen",
"Dark mode: ": "Nachtmodus: ", "Dark mode: ": "Nachtmodus: ",
"Thin mode: ": "Schlanker Modus: ", "Thin mode: ": "Schlanker Modus: ",
"Subscription preferences": "Abonnementeinstellungen", "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: ", "Redirect homepage to feed: ": "Startseite zu Feed umleiten: ",
"Number of videos shown in feed: ": "Anzahl von Videos die im Feed angezeigt werden: ", "Number of videos shown in feed: ": "Anzahl von Videos die im Feed angezeigt werden: ",
"Sort videos by: ": "Videos sortieren nach: ", "Sort videos by: ": "Videos sortieren nach: ",
@ -99,11 +99,11 @@
"Administrator preferences": "Administratoreinstellungen", "Administrator preferences": "Administratoreinstellungen",
"Default homepage: ": "Standard-Homepage: ", "Default homepage: ": "Standard-Homepage: ",
"Feed menu: ": "Feed-Menü: ", "Feed menu: ": "Feed-Menü: ",
"Top enabled? ": "Top aktiviert? ", "Top enabled: ": "Top aktiviert? ",
"CAPTCHA enabled? ": "CAPTCHA aktiviert? ", "CAPTCHA enabled: ": "CAPTCHA aktiviert? ",
"Login enabled? ": "Login aktiviert? ", "Login enabled: ": "Login aktiviert? ",
"Registration enabled? ": "Registrierung aktiviert? ", "Registration enabled: ": "Registrierung aktiviert? ",
"Report statistics? ": "Statistiken berichten? ", "Report statistics: ": "Statistiken berichten? ",
"Save preferences": "Einstellungen speichern", "Save preferences": "Einstellungen speichern",
"Subscription manager": "Abonnementverwaltung", "Subscription manager": "Abonnementverwaltung",
"Token manager": "Token-Manager", "Token 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): ", "Proxy videos: ": "Αναπαραγωγή με διακομιστή μεσολάβησης (proxy): ",
"Default speed: ": "Προεπιλεγμένη ταχύτητα: ", "Default speed: ": "Προεπιλεγμένη ταχύτητα: ",
"Preferred video quality: ": "Προτιμώμενη ανάλυση: ", "Preferred video quality: ": "Προτιμώμενη ανάλυση: ",
"Player volume: ": "Ένταση αναπαραγωγής: ", "Player volume: ": "Ένταση αναπαραγωγής: ",
@ -71,13 +71,13 @@
"reddit": "reddit", "reddit": "reddit",
"Default captions: ": "Προεπιλεγμένοι υπότιτλοι: ", "Default captions: ": "Προεπιλεγμένοι υπότιτλοι: ",
"Fallback captions: ": "Εναλλακτικοί υπότιτλοι: ", "Fallback captions: ": "Εναλλακτικοί υπότιτλοι: ",
"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: ": "Προβολή σημειώσεων μόνο για κανάλια στα οποία είστε συνδρομητής; ",
"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; ", "CAPTCHA enabled: ": "Ενεργοποίηση CAPTCHA; ",
"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

@ -62,7 +62,7 @@
"Play next by default: ": "Play next by default: ", "Play next by default: ": "Play next by default: ",
"Autoplay next video: ": "Autoplay next video: ", "Autoplay next video: ": "Autoplay next video: ",
"Listen by default: ": "Listen by default: ", "Listen by default: ": "Listen by default: ",
"Proxy videos? ": "Proxy videos? ", "Proxy videos: ": "Proxy videos: ",
"Default speed: ": "Default speed: ", "Default speed: ": "Default speed: ",
"Preferred video quality: ": "Preferred video quality: ", "Preferred video quality: ": "Preferred video quality: ",
"Player volume: ": "Player volume: ", "Player volume: ": "Player volume: ",
@ -71,13 +71,13 @@
"reddit": "reddit", "reddit": "reddit",
"Default captions: ": "Default captions: ", "Default captions: ": "Default captions: ",
"Fallback captions: ": "Fallback captions: ", "Fallback captions: ": "Fallback captions: ",
"Show related videos? ": "Show related videos? ", "Show related videos: ": "Show related videos: ",
"Show annotations by default? ": "Show annotations by default? ", "Show annotations by default: ": "Show annotations by default: ",
"Visual preferences": "Visual preferences", "Visual preferences": "Visual preferences",
"Dark mode: ": "Dark mode: ", "Dark mode: ": "Dark mode: ",
"Thin mode: ": "Thin mode: ", "Thin mode: ": "Thin mode: ",
"Subscription preferences": "Subscription preferences", "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: ", "Redirect homepage to feed: ": "Redirect homepage to feed: ",
"Number of videos shown in feed: ": "Number of videos shown in feed: ", "Number of videos shown in feed: ": "Number of videos shown in feed: ",
"Sort videos by: ": "Sort videos by: ", "Sort videos by: ": "Sort videos by: ",
@ -105,11 +105,11 @@
"Administrator preferences": "Administrator preferences", "Administrator preferences": "Administrator preferences",
"Default homepage: ": "Default homepage: ", "Default homepage: ": "Default homepage: ",
"Feed menu: ": "Feed menu: ", "Feed menu: ": "Feed menu: ",
"Top enabled? ": "Top enabled? ", "Top enabled: ": "Top enabled: ",
"CAPTCHA enabled? ": "CAPTCHA enabled? ", "CAPTCHA enabled: ": "CAPTCHA enabled: ",
"Login enabled? ": "Login enabled? ", "Login enabled: ": "Login enabled? ",
"Registration enabled? ": "Registration enabled? ", "Registration enabled: ": "Registration enabled? ",
"Report statistics? ": "Report statistics? ", "Report statistics: ": "Report statistics? ",
"Save preferences": "Save preferences", "Save preferences": "Save preferences",
"Subscription manager": "Subscription manager", "Subscription manager": "Subscription manager",
"Token manager": "Token manager", "Token manager": "Token manager",

View File

@ -56,7 +56,7 @@
"Play next by default: ": "Ludi sekvan defaŭlte: ", "Play next by default: ": "Ludi sekvan defaŭlte: ",
"Autoplay next video: ": "Aŭtomate ludi sekvan videon: ", "Autoplay next video: ": "Aŭtomate ludi sekvan videon: ",
"Listen by default: ": "Aŭskulti defaŭlte: ", "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: ", "Default speed: ": "Defaŭlta rapido: ",
"Preferred video quality: ": "Preferita videkvalito: ", "Preferred video quality: ": "Preferita videkvalito: ",
"Player volume: ": "Ludila sonforteco: ", "Player volume: ": "Ludila sonforteco: ",
@ -65,13 +65,13 @@
"reddit": "reddit", "reddit": "reddit",
"Default captions: ": "Defaŭltaj subtekstoj: ", "Default captions: ": "Defaŭltaj subtekstoj: ",
"Fallback captions: ": "Retrodefaŭltaj subtekstoj: ", "Fallback captions: ": "Retrodefaŭltaj subtekstoj: ",
"Show related videos? ": "Ĉu montri rilatajn videojn? ", "Show related videos: ": "Ĉu montri rilatajn videojn? ",
"Show annotations by default? ": "Ĉu montri prinotojn defaŭlte? ", "Show annotations by default: ": "Ĉu montri prinotojn defaŭlte? ",
"Visual preferences": "Vidaj preferoj", "Visual preferences": "Vidaj preferoj",
"Dark mode: ": "Malhela reĝimo: ", "Dark mode: ": "Malhela reĝimo: ",
"Thin mode: ": "Maldika reĝimo: ", "Thin mode: ": "Maldika reĝimo: ",
"Subscription preferences": "Abonaj agordoj", "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: ", "Redirect homepage to feed: ": "Alidirekti hejmpâgon al fluo: ",
"Number of videos shown in feed: ": "Nombro da videoj montritaj en fluo: ", "Number of videos shown in feed: ": "Nombro da videoj montritaj en fluo: ",
"Sort videos by: ": "Ordi videojn laŭ: ", "Sort videos by: ": "Ordi videojn laŭ: ",
@ -99,11 +99,11 @@
"Administrator preferences": "Agordoj de administranto", "Administrator preferences": "Agordoj de administranto",
"Default homepage: ": "Defaŭlta hejmpaĝo: ", "Default homepage: ": "Defaŭlta hejmpaĝo: ",
"Feed menu: ": "Flua menuo: ", "Feed menu: ": "Flua menuo: ",
"Top enabled? ": "Ĉu pli bonaj ŝaltitaj? ", "Top enabled: ": "Ĉu pli bonaj ŝaltitaj? ",
"CAPTCHA enabled? ": "Ĉu CAPTCHA ŝaltita? ", "CAPTCHA enabled: ": "Ĉu CAPTCHA ŝaltita? ",
"Login enabled? ": "Ĉu ensaluto aktivita? ", "Login enabled: ": "Ĉu ensaluto aktivita? ",
"Registration enabled? ": "Ĉu registriĝo aktivita? ", "Registration enabled: ": "Ĉu registriĝo aktivita? ",
"Report statistics? ": "Ĉu raporti statistikojn? ", "Report statistics: ": "Ĉu raporti statistikojn? ",
"Save preferences": "Konservi agordojn", "Save preferences": "Konservi agordojn",
"Subscription manager": "Administrilo de abonoj", "Subscription manager": "Administrilo de abonoj",
"Token manager": "Ĵetona administrilo", "Token manager": "Ĵetona administrilo",
@ -318,4 +318,4 @@
"Playlists": "Ludlistoj", "Playlists": "Ludlistoj",
"Community": "Komunumo", "Community": "Komunumo",
"Current version: ": "Nuna versio: " "Current version: ": "Nuna versio: "
} }

View File

@ -56,7 +56,7 @@
"Play next by default: ": "Reproducir siguiente por defecto: ", "Play next by default: ": "Reproducir siguiente por defecto: ",
"Autoplay next video: ": "Reproducir automáticamente el vídeo siguiente: ", "Autoplay next video: ": "Reproducir automáticamente el vídeo siguiente: ",
"Listen by default: ": "Activar el sonido por defecto: ", "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: ", "Default speed: ": "Velocidad por defecto: ",
"Preferred video quality: ": "Calidad de vídeo preferida: ", "Preferred video quality: ": "Calidad de vídeo preferida: ",
"Player volume: ": "Volumen del reproductor: ", "Player volume: ": "Volumen del reproductor: ",
@ -65,13 +65,13 @@
"reddit": "Reddit", "reddit": "Reddit",
"Default captions: ": "Subtítulos por defecto: ", "Default captions: ": "Subtítulos por defecto: ",
"Fallback captions: ": "Subtítulos alternativos: ", "Fallback captions: ": "Subtítulos alternativos: ",
"Show related videos? ": "¿Mostrar vídeos relacionados? ", "Show related videos: ": "¿Mostrar vídeos relacionados? ",
"Show annotations by default? ": "¿Mostrar anotaciones por defecto? ", "Show annotations by default: ": "¿Mostrar anotaciones por defecto? ",
"Visual preferences": "Preferencias visuales", "Visual preferences": "Preferencias visuales",
"Dark mode: ": "Modo oscuro: ", "Dark mode: ": "Modo oscuro: ",
"Thin mode: ": "Modo compacto: ", "Thin mode: ": "Modo compacto: ",
"Subscription preferences": "Preferencias de la suscripción", "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: ", "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: ", "Number of videos shown in feed: ": "Número de vídeos mostrados en la fuente: ",
"Sort videos by: ": "Ordenar los vídeos por: ", "Sort videos by: ": "Ordenar los vídeos por: ",
@ -99,11 +99,11 @@
"Administrator preferences": "Preferencias de administrador", "Administrator preferences": "Preferencias de administrador",
"Default homepage: ": "Página de inicio por defecto: ", "Default homepage: ": "Página de inicio por defecto: ",
"Feed menu: ": "Menú de fuentes: ", "Feed menu: ": "Menú de fuentes: ",
"Top enabled? ": "¿Habilitar los destacados? ", "Top enabled: ": "¿Habilitar los destacados? ",
"CAPTCHA enabled? ": "¿Habilitar los CAPTCHA? ", "CAPTCHA enabled: ": "¿Habilitar los CAPTCHA? ",
"Login enabled? ": "¿Habilitar el inicio de sesión? ", "Login enabled: ": "¿Habilitar el inicio de sesión? ",
"Registration enabled? ": "¿Habilitar el registro? ", "Registration enabled: ": "¿Habilitar el registro? ",
"Report statistics? ": "¿Enviar estadísticas? ", "Report statistics: ": "¿Enviar estadísticas? ",
"Save preferences": "Guardar las preferencias", "Save preferences": "Guardar las preferencias",
"Subscription manager": "Gestor de suscripciones", "Subscription manager": "Gestor de suscripciones",
"Token manager": "Gestor de tokens", "Token manager": "Gestor de tokens",

View File

@ -56,7 +56,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: ": "",
"Default speed: ": "", "Default speed: ": "",
"Preferred video quality: ": "", "Preferred video quality: ": "",
"Player volume: ": "", "Player volume: ": "",
@ -65,13 +65,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: ": "",
"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: ": "",
"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: ": "",
@ -99,11 +99,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: ": "",
"Save preferences": "", "Save preferences": "",
"Subscription manager": "", "Subscription manager": "",
"Token manager": "", "Token manager": "",

View File

@ -56,7 +56,7 @@
"Play next by default: ": "Jouer suirvante par défaut : ", "Play next by default: ": "Jouer suirvante par défaut : ",
"Autoplay next video: ": "Lire automatiquement la vidéo suivante : ", "Autoplay next video: ": "Lire automatiquement la vidéo suivante : ",
"Listen by default: ": "Audio uniquement : ", "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 : ", "Default speed: ": "Vitesse par défaut : ",
"Preferred video quality: ": "Qualité vidéo souhaitée : ", "Preferred video quality: ": "Qualité vidéo souhaitée : ",
"Player volume: ": "Volume du lecteur : ", "Player volume: ": "Volume du lecteur : ",
@ -65,13 +65,13 @@
"reddit": "Reddit", "reddit": "Reddit",
"Default captions: ": "Sous-titres par défaut : ", "Default captions: ": "Sous-titres par défaut : ",
"Fallback captions: ": "Sous-titres de repli : ", "Fallback captions: ": "Sous-titres de repli : ",
"Show related videos? ": "Voir les vidéos liées ? ", "Show related videos: ": "Voir les vidéos liées ? ",
"Show annotations by default? ": "Voir les annotations par défaut ? ", "Show annotations by default: ": "Voir les annotations par défaut ? ",
"Visual preferences": "Préférences du site", "Visual preferences": "Préférences du site",
"Dark mode: ": "Mode Sombre : ", "Dark mode: ": "Mode Sombre : ",
"Thin mode: ": "Mode Simplifié : ", "Thin mode: ": "Mode Simplifié : ",
"Subscription preferences": "Préférences de la page d'abonnements", "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 : ", "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 : ", "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 : ", "Sort videos by: ": "Trier les vidéos par : ",
@ -99,11 +99,11 @@
"Administrator preferences": "Préferences d'Administrateur", "Administrator preferences": "Préferences d'Administrateur",
"Default homepage: ": "Page d'accueil par défaut : ", "Default homepage: ": "Page d'accueil par défaut : ",
"Feed menu: ": "Menu des Flux : ", "Feed menu: ": "Menu des Flux : ",
"Top enabled? ": "Top activé ? ", "Top enabled: ": "Top activé ? ",
"CAPTCHA enabled? ": "CAPTCHA activé ? ", "CAPTCHA enabled: ": "CAPTCHA activé ? ",
"Login enabled? ": "Connexion activé ? ", "Login enabled: ": "Connexion activé ? ",
"Registration enabled? ": "Inscription activée ? ", "Registration enabled: ": "Inscription activée ? ",
"Report statistics? ": "Télémétrie activé ? ", "Report statistics: ": "Télémétrie activé ? ",
"Save preferences": "Enregistrer les préférences", "Save preferences": "Enregistrer les préférences",
"Subscription manager": "Gestionnaire d'abonnement", "Subscription manager": "Gestionnaire d'abonnement",
"Token manager": "Gestionnaire de tokens", "Token manager": "Gestionnaire de tokens",

View File

@ -4,7 +4,7 @@
"LIVE": "BEINT", "LIVE": "BEINT",
"Shared `x` ago": "Deilt `x` síðan", "Shared `x` ago": "Deilt `x` síðan",
"Unsubscribe": "Afskrá", "Unsubscribe": "Afskrá",
"Subscribe": "Gerast áskrifandi", "Subscribe": "Áskrifa",
"View channel on YouTube": "Skoða rás á YouTube", "View channel on YouTube": "Skoða rás á YouTube",
"View playlist on YouTube": "Skoða spilunarlisti á YouTube", "View playlist on YouTube": "Skoða spilunarlisti á YouTube",
"newest": "nýjasta", "newest": "nýjasta",
@ -56,7 +56,7 @@
"Play next by default: ": "Spila næst sjálfgefið: ", "Play next by default: ": "Spila næst sjálfgefið: ",
"Autoplay next video: ": "Spila næst sjálfkrafa: ", "Autoplay next video: ": "Spila næst sjálfkrafa: ",
"Listen by default: ": "Hlusta sjálfgefið: ", "Listen by default: ": "Hlusta sjálfgefið: ",
"Proxy videos? ": "", "Proxy videos: ": "Proxy myndbönd? ",
"Default speed: ": "Sjálfgefinn hraði: ", "Default speed: ": "Sjálfgefinn hraði: ",
"Preferred video quality: ": "Æskilegt myndbands gæði: ", "Preferred video quality: ": "Æskilegt myndbands gæði: ",
"Player volume: ": "Spilara bindi: ", "Player volume: ": "Spilara bindi: ",
@ -65,15 +65,15 @@
"reddit": "reddit", "reddit": "reddit",
"Default captions: ": "Sjálfgefin texti: ", "Default captions: ": "Sjálfgefin texti: ",
"Fallback captions: ": "Varatextar: ", "Fallback captions: ": "Varatextar: ",
"Show related videos? ": "Sýna tengd myndbönd? ", "Show related videos: ": "Sýna tengd myndbönd? ",
"Show annotations by default? ": "", "Show annotations by default: ": "Á að sýna glósur sjálfgefið? ",
"Visual preferences": "Sjónrænar stillingar", "Visual preferences": "Sjónrænar stillingar",
"Dark mode: ": "Myrkur ham: ", "Dark mode: ": "Myrkur ham: ",
"Thin mode: ": "Þunnt ham: ", "Thin mode: ": "Þunnt ham: ",
"Subscription preferences": "Áskriftarstillingar", "Subscription preferences": "Áskriftarstillingar",
"Show annotations by default for subscribed channels? ": "", "Show annotations by default for subscribed channels: ": "Á að sýna glósur sjálfgefið fyrir áskriftarrásir? ",
"Redirect homepage to feed: ": "", "Redirect homepage to feed: ": "Endurbeina heimasíðu að straumi: ",
"Number of videos shown in feed: ": "", "Number of videos shown in feed: ": "Fjöldi myndbanda sem sýndir eru í straumi: ",
"Sort videos by: ": "Raða myndbönd eftir: ", "Sort videos by: ": "Raða myndbönd eftir: ",
"published": "birt", "published": "birt",
"published - reverse": "birt - afturábak", "published - reverse": "birt - afturábak",
@ -86,234 +86,234 @@
"Only show unwatched: ": "Sýna aðeins óséð: ", "Only show unwatched: ": "Sýna aðeins óséð: ",
"Only show notifications (if there are any): ": "Sýna aðeins tilkynningar (ef einhverjar eru): ", "Only show notifications (if there are any): ": "Sýna aðeins tilkynningar (ef einhverjar eru): ",
"Enable web notifications": "Virkja veftilkynningar", "Enable web notifications": "Virkja veftilkynningar",
"`x` uploaded a video": "' x ' hlóð upp myndband", "`x` uploaded a video": "`x` hlóð upp myndband",
"`x` is live": "' x ' er í beinni", "`x` is live": "`x` er í beinni",
"Data preferences": "Gagnastillingar", "Data preferences": "Gagnastillingar",
"Clear watch history": "Hreinsa áhorfssögu", "Clear watch history": "Hreinsa áhorfssögu",
"Import/export data": "Flytja inn/út gögn", "Import/export data": "Flytja inn/út gögn",
"Change password": "Breyta lykilorði", "Change password": "Breyta lykilorði",
"Manage subscriptions": "Stjórna áskriftum", "Manage subscriptions": "Stjórna áskriftum",
"Manage tokens": "", "Manage tokens": "Stjórna tákn",
"Watch history": "", "Watch history": "Áhorfssögu",
"Delete account": "", "Delete account": "Eyða reikningi",
"Administrator preferences": "", "Administrator preferences": "Kjörstillingar stjórnanda",
"Default homepage: ": "", "Default homepage: ": "Sjálfgefin heimasíða: ",
"Feed menu: ": "", "Feed menu: ": "Straum valmynd: ",
"Top enabled? ": "", "Top enabled: ": "Toppur virkur? ",
"CAPTCHA enabled? ": "", "CAPTCHA enabled: ": "CAPTCHA virk? ",
"Login enabled? ": "", "Login enabled: ": "Innskráning virk? ",
"Registration enabled? ": "", "Registration enabled: ": "Nýskráning virkjuð? ",
"Report statistics? ": "", "Report statistics: ": "Skrá talnagögn? ",
"Save preferences": "", "Save preferences": "Vista stillingar",
"Subscription manager": "", "Subscription manager": "Áskriftarstjóri",
"Token manager": "", "Token manager": "Táknstjóri",
"Token": "", "Token": "Tákn",
"`x` subscriptions.": "", "`x` subscriptions.": "`x` áskriftir.",
"`x` tokens.": "", "`x` tokens.": "`x` tákn.",
"Import/export": "", "Import/export": "Flytja inn/út",
"unsubscribe": "", "unsubscribe": "afskrá",
"revoke": "", "revoke": "afturkalla",
"Subscriptions": "", "Subscriptions": "Áskriftir",
"`x` unseen notifications.": "", "`x` unseen notifications.": "`x` óséðar tilkynningar.",
"search": "", "search": "leita",
"Log out": "", "Log out": "Útskrá",
"Released under the AGPLv3 by Omar Roth.": "", "Released under the AGPLv3 by Omar Roth.": "Útgefið undir AGPLv3 eftir Omar Roth.",
"Source available here.": "", "Source available here.": "Frumkóði aðgengilegur hér.",
"View JavaScript license information.": "", "View JavaScript license information.": "Skoða JavaScript leyfisupplýsingar.",
"View privacy policy.": "", "View privacy policy.": "Skoða meðferð persónuupplýsinga.",
"Trending": "", "Trending": "Vinsælt",
"Unlisted": "", "Unlisted": "Óskráð",
"Watch on YouTube": "", "Watch on YouTube": "Horfa á YouTube",
"Hide annotations": "", "Hide annotations": "Fela glósur",
"Show annotations": "", "Show annotations": "Sýna glósur",
"Genre: ": "", "Genre: ": "Tegund: ",
"License: ": "", "License: ": "Notkunarleyfi: ",
"Family friendly? ": "", "Family friendly? ": "Fjölskylduvænt? ",
"Wilson score: ": "", "Wilson score: ": "Wilson stig: ",
"Engagement: ": "", "Engagement: ": "Þátttöku: ",
"Whitelisted regions: ": "", "Whitelisted regions: ": "Svæði á hvítum lista: ",
"Blacklisted regions: ": "", "Blacklisted regions: ": "Svæði á svörtum lista: ",
"Shared `x`": "", "Shared `x`": "Deilt `x`",
"`x` views.": "", "`x` views.": "`x` áhorf.",
"Premieres in `x`": "", "Premieres in `x`": "Frumflutt eftir `x`",
"Premieres `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.": "", "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": "", "View YouTube comments": "Skoða YouTube ummæli",
"View more comments on Reddit": "", "View more comments on Reddit": "Skoða fleiri ummæli á Reddit",
"View `x` comments": "", "View `x` comments": "Skoða `x` ummæli",
"View Reddit comments": "", "View Reddit comments": "Skoða Reddit ummæli",
"Hide replies": "", "Hide replies": "Fela svör",
"Show replies": "", "Show replies": "Sýna svör",
"Incorrect password": "", "Incorrect password": "Rangt lykilorð",
"Quota exceeded, try again in a few hours": "", "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.": "", "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": "", "Invalid TFA code": "Ógildur TFA kóði",
"Login failed. This may be because two-factor authentication is not turned on for your account.": "", "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": "", "Wrong answer": "Rangt svar",
"Erroneous CAPTCHA": "", "Erroneous CAPTCHA": "Rangt CAPTCHA",
"CAPTCHA is a required field": "", "CAPTCHA is a required field": "CAPTCHA er nauðsynlegur reitur",
"User ID is a required field": "", "User ID is a required field": "Notandakenni er nauðsynlegur reitur",
"Password is a required field": "", "Password is a required field": "Lykilorð er nauðsynlegur reitur",
"Wrong username or password": "", "Wrong username or password": "Rangt notandanafn eða lykilorð",
"Please sign in using 'Log in with Google'": "", "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": "", "Password cannot be empty": "Lykilorð má ekki vera autt",
"Password cannot be longer than 55 characters": "", "Password cannot be longer than 55 characters": "Lykilorð má ekki vera lengra en 55 stafir",
"Please log in": "", "Please log in": "Vinsamlegast skráðu þig inn",
"Invidious Private Feed for `x`": "", "Invidious Private Feed for `x`": "Invidious Persónulegur Straumur fyrir `x`",
"channel:`x`": "", "channel:`x`": "rás:`x`",
"Deleted or invalid channel": "", "Deleted or invalid channel": "Eytt eða ógild rás",
"This channel does not exist.": "", "This channel does not exist.": "Þessi rás er ekki til.",
"Could not get channel info.": "", "Could not get channel info.": "Ekki tókst að fá rásarupplýsingar.",
"Could not fetch comments": "", "Could not fetch comments": "Ekki tókst að sækja ummæli",
"View `x` replies.": "", "View `x` replies.": "Skoða `x` svör.",
"`x` ago": "", "`x` ago": "' x ' síðan",
"Load more": "", "Load more": "Hlaða meira",
"`x` points.": "", "`x` points.": "`x` stig.",
"Could not create mix.": "", "Could not create mix.": "Ekki tókst að búa til blöndu.",
"Empty playlist": "", "Empty playlist": "Tómur spilunarlisti",
"Not a playlist.": "", "Not a playlist.": "Ekki spilunarlisti.",
"Playlist does not exist.": "", "Playlist does not exist.": "Spilunarlisti er ekki til.",
"Could not pull trending pages.": "", "Could not pull trending pages.": "Ekki tókst að draga vinsællar síður.",
"Hidden field \"challenge\" is a required field": "", "Hidden field \"challenge\" is a required field": "Falinn reitur \"áskorun\" er nauðsynlegur reitur",
"Hidden field \"token\" is a required field": "", "Hidden field \"token\" is a required field": "Falinn reitur \"tákn\" er nauðsynlegur reitur",
"Erroneous challenge": "", "Erroneous challenge": "Röng áskorun",
"Erroneous token": "", "Erroneous token": "Rangt tákn",
"No such user": "", "No such user": "Enginn slíkur notandi",
"Token is expired, please try again": "", "Token is expired, please try again": "Tákn er útrunnið, vinsamlegast reyndu aftur",
"English": "", "English": "Enska",
"English (auto-generated)": "", "English (auto-generated)": "Enska (sjálfkrafa)",
"Afrikaans": "", "Afrikaans": "Afríkanska",
"Albanian": "", "Albanian": "Albanska",
"Amharic": "", "Amharic": "Amharíska",
"Arabic": "", "Arabic": "Arabíska",
"Armenian": "", "Armenian": "Armenska",
"Azerbaijani": "", "Azerbaijani": "Aserbaídsjanska",
"Bangla": "", "Bangla": "Bangla",
"Basque": "", "Basque": "Baskneska",
"Belarusian": "", "Belarusian": "Hvítrússneska",
"Bosnian": "", "Bosnian": "Bosníska",
"Bulgarian": "", "Bulgarian": "Búlgarska",
"Burmese": "", "Burmese": "Búrmíska",
"Catalan": "", "Catalan": "Katalónska",
"Cebuano": "", "Cebuano": "Cebúanó",
"Chinese (Simplified)": "", "Chinese (Simplified)": "Kínverska (Einfölduð)",
"Chinese (Traditional)": "", "Chinese (Traditional)": "Kínverska (Hefðbundin)",
"Corsican": "", "Corsican": "Korsíska",
"Croatian": "", "Croatian": "Króatíska",
"Czech": "", "Czech": "Tékkneska",
"Danish": "", "Danish": "Danska",
"Dutch": "", "Dutch": "Hollenska",
"Esperanto": "", "Esperanto": "Esperantó",
"Estonian": "", "Estonian": "Eistneska",
"Filipino": "", "Filipino": "Filippínska",
"Finnish": "", "Finnish": "Finnska",
"French": "", "French": "Franska",
"Galician": "", "Galician": "Galisíska",
"Georgian": "", "Georgian": "Georgíska",
"German": "", "German": "Þýska",
"Greek": "", "Greek": "Gríska",
"Gujarati": "", "Gujarati": "Gújaratí",
"Haitian Creole": "", "Haitian Creole": "Haítískt Kreólamál",
"Hausa": "", "Hausa": "Hausa",
"Hawaiian": "", "Hawaiian": "Havaíska",
"Hebrew": "", "Hebrew": "Hebreska",
"Hindi": "", "Hindi": "Hindí",
"Hmong": "", "Hmong": "Hmong",
"Hungarian": "", "Hungarian": "Ungverska",
"Icelandic": "", "Icelandic": "Íslenska",
"Igbo": "", "Igbo": "Igbo",
"Indonesian": "", "Indonesian": "Indónesíska",
"Irish": "", "Irish": "Írska",
"Italian": "", "Italian": "Ítalska",
"Japanese": "", "Japanese": "Japanska",
"Javanese": "", "Javanese": "Javanska",
"Kannada": "", "Kannada": "Kanaríska",
"Kazakh": "", "Kazakh": "Kasakíska",
"Khmer": "", "Khmer": "Khmeríska",
"Korean": "", "Korean": "Kóreska",
"Kurdish": "", "Kurdish": "Kúrdíska",
"Kyrgyz": "", "Kyrgyz": "Kirgisíska",
"Lao": "", "Lao": "Laó",
"Latin": "", "Latin": "Latína",
"Latvian": "", "Latvian": "Lettneska",
"Lithuanian": "", "Lithuanian": "Litháíska",
"Luxembourgish": "", "Luxembourgish": "Lúxemborgíska",
"Macedonian": "", "Macedonian": "Makedóníska",
"Malagasy": "", "Malagasy": "Malagasíska",
"Malay": "", "Malay": "Malaíska",
"Malayalam": "", "Malayalam": "Malaíalam",
"Maltese": "", "Maltese": "Maltneska",
"Maori": "", "Maori": "Maórí",
"Marathi": "", "Marathi": "Marathi",
"Mongolian": "", "Mongolian": "Mongólska",
"Nepali": "", "Nepali": "Nepalska",
"Norwegian Bokmål": "", "Norwegian Bokmål": "Norskt bókmál",
"Nyanja": "", "Nyanja": "Nyanja",
"Pashto": "", "Pashto": "Pashto",
"Persian": "", "Persian": "Persneska",
"Polish": "", "Polish": "Pólska",
"Portuguese": "", "Portuguese": "Portúgalska",
"Punjabi": "", "Punjabi": "Punjabi",
"Romanian": "", "Romanian": "Rúmenska",
"Russian": "", "Russian": "Rússneska",
"Samoan": "", "Samoan": "Samóíska",
"Scottish Gaelic": "", "Scottish Gaelic": "Skosk Gelíska",
"Serbian": "", "Serbian": "Serbneska",
"Shona": "", "Shona": "Shona",
"Sindhi": "", "Sindhi": "Sindí",
"Sinhala": "", "Sinhala": "Sinhala",
"Slovak": "", "Slovak": "Slóvakíska",
"Slovenian": "", "Slovenian": "Slóvenska",
"Somali": "", "Somali": "Sómalska",
"Southern Sotho": "", "Southern Sotho": "Suður Sótó",
"Spanish": "", "Spanish": "Spænska",
"Spanish (Latin America)": "", "Spanish (Latin America)": "Spænska (Rómönsku Ameríka)",
"Sundanese": "", "Sundanese": "Sundaneska",
"Swahili": "", "Swahili": "Svahílí",
"Swedish": "", "Swedish": "Sænska",
"Tajik": "", "Tajik": "Tadsikíska",
"Tamil": "", "Tamil": "Tamílska",
"Telugu": "", "Telugu": "Telúgú",
"Thai": "", "Thai": "Taílenska",
"Turkish": "", "Turkish": "Tyrkneska",
"Ukrainian": "", "Ukrainian": "Úkraníska",
"Urdu": "", "Urdu": "Úrdú",
"Uzbek": "", "Uzbek": "Úsbekíska",
"Vietnamese": "", "Vietnamese": "Víetnamska",
"Welsh": "", "Welsh": "Velska",
"Western Frisian": "", "Western Frisian": "Vestur Frísneska",
"Xhosa": "", "Xhosa": "Xhosa",
"Yiddish": "", "Yiddish": "Jiddíska",
"Yoruba": "", "Yoruba": "Jórúba",
"Zulu": "", "Zulu": "Zúlú",
"`x` years.": "", "`x` years.": "' x ' ár.",
"`x` months.": "", "`x` months.": "' x ' mánuði.",
"`x` weeks.": "", "`x` weeks.": "`x` vikur.",
"`x` days.": "", "`x` days.": "' x ' dagar.",
"`x` hours.": "", "`x` hours.": "`x` klukkustundir.",
"`x` minutes.": "", "`x` minutes.": "`x` mínútur.",
"`x` seconds.": "", "`x` seconds.": "`x` sekúndur.",
"Fallback comments: ": "", "Fallback comments: ": "Vara ummæli: ",
"Popular": "", "Popular": "Vinsællt",
"Top": "", "Top": "Topp",
"About": "", "About": "Um",
"Rating: ": "", "Rating: ": "Einkunn: ",
"Language: ": "", "Language: ": "Tungumál: ",
"View as playlist": "", "View as playlist": "Skoða sem spilunarlista",
"Default": "", "Default": "Sjálfgefið",
"Music": "", "Music": "Tónlist",
"Gaming": "", "Gaming": "Tólvuleikja",
"News": "", "News": "Fréttir",
"Movies": "", "Movies": "Kvikmyndir",
"Download": "", "Download": "Niðurhal",
"Download as: ": "", "Download as: ": "Niðurhala sem: ",
"%A %B %-d, %Y": "", "%A %B %-d, %Y": "%A %B %-d, %Y",
"(edited)": "", "(edited)": "(breytt)",
"YouTube comment permalink": "", "YouTube comment permalink": "YouTube ummæli varanlegur tengill",
"`x` marked it with a ❤": "", "`x` marked it with a ❤": "`x` merkti það með ❤",
"Audio mode": "", "Audio mode": "Hljóð ham",
"Video mode": "", "Video mode": "Myndband ham",
"Videos": "", "Videos": "Myndbönd",
"Playlists": "", "Playlists": "Spilunarlistar",
"Current version: ": "" "Current version: ": "Núverandi útgáfa: "
} }

View File

@ -56,7 +56,7 @@
"Play next by default: ": "Riproduzione successiva per impostazione predefinita: ", "Play next by default: ": "Riproduzione successiva per impostazione predefinita: ",
"Autoplay next video: ": "Riproduci automaticamente il prossimo video: ", "Autoplay next video: ": "Riproduci automaticamente il prossimo video: ",
"Listen by default: ": "Modalità solo audio come predefinita: ", "Listen by default: ": "Modalità solo audio come predefinita: ",
"Proxy videos? ": "", "Proxy videos: ": "",
"Default speed: ": "Velocità di riproduzione predefinita: ", "Default speed: ": "Velocità di riproduzione predefinita: ",
"Preferred video quality: ": "Preferenza sulla qualità video: ", "Preferred video quality: ": "Preferenza sulla qualità video: ",
"Player volume: ": "Volume di riproduzione: ", "Player volume: ": "Volume di riproduzione: ",
@ -65,13 +65,13 @@
"reddit": "", "reddit": "",
"Default captions: ": "Sottotitoli predefiniti: ", "Default captions: ": "Sottotitoli predefiniti: ",
"Fallback captions: ": "Sottotitoli alternativi: ", "Fallback captions: ": "Sottotitoli alternativi: ",
"Show related videos? ": "Mostra video correlati? ", "Show related videos: ": "Mostra video correlati? ",
"Show annotations by default? ": "Mostra le annotazioni per impostazione predefinita? ", "Show annotations by default: ": "Mostra le annotazioni per impostazione predefinita? ",
"Visual preferences": "Preferenze grafiche", "Visual preferences": "Preferenze grafiche",
"Dark mode: ": "Tema scuro: ", "Dark mode: ": "Tema scuro: ",
"Thin mode: ": "Modalità per connessioni lente: ", "Thin mode: ": "Modalità per connessioni lente: ",
"Subscription preferences": "Preferenze iscrizioni", "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: ", "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: ", "Number of videos shown in feed: ": "Numero di video da mostrare nelle iscrizioni: ",
"Sort videos by: ": "Ordinare i video per: ", "Sort videos by: ": "Ordinare i video per: ",
@ -99,11 +99,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: ": "",
"Save preferences": "Salva le preferenze", "Save preferences": "Salva le preferenze",
"Subscription manager": "Gestisci le iscrizioni", "Subscription manager": "Gestisci le iscrizioni",
"Token manager": "", "Token manager": "",

View File

@ -56,7 +56,7 @@
"Play next by default: ": "Spill neste som forvalg: ", "Play next by default: ": "Spill neste som forvalg: ",
"Autoplay next video: ": "Autospill neste video: ", "Autoplay next video: ": "Autospill neste video: ",
"Listen by default: ": "Lytt som forvalg: ", "Listen by default: ": "Lytt som forvalg: ",
"Proxy videos? ": "Mellomtjen videoer? ", "Proxy videos: ": "Mellomtjen videoer? ",
"Default speed: ": "Forvalgt hastighet: ", "Default speed: ": "Forvalgt hastighet: ",
"Preferred video quality: ": "Foretrukket videokvalitet: ", "Preferred video quality: ": "Foretrukket videokvalitet: ",
"Player volume: ": "Avspillerlydstyrke: ", "Player volume: ": "Avspillerlydstyrke: ",
@ -65,13 +65,13 @@
"reddit": "Reddit", "reddit": "Reddit",
"Default captions: ": "Forvalgte undertitler: ", "Default captions: ": "Forvalgte undertitler: ",
"Fallback captions: ": "Tilbakefallsundertitler: ", "Fallback captions: ": "Tilbakefallsundertitler: ",
"Show related videos? ": "Vis relaterte videoer? ", "Show related videos: ": "Vis relaterte videoer? ",
"Show annotations by default? ": "Vis merknader som forvalg? ", "Show annotations by default: ": "Vis merknader som forvalg? ",
"Visual preferences": "Visuelle innstillinger", "Visual preferences": "Visuelle innstillinger",
"Dark mode: ": "Mørk drakt: ", "Dark mode: ": "Mørk drakt: ",
"Thin mode: ": "Tynt modus: ", "Thin mode: ": "Tynt modus: ",
"Subscription preferences": "Abonnementsinnstillinger", "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: ", "Redirect homepage to feed: ": "Videresend hjemmeside til flyt: ",
"Number of videos shown in feed: ": "Antall videoer å vise i flyt: ", "Number of videos shown in feed: ": "Antall videoer å vise i flyt: ",
"Sort videos by: ": "Sorter videoer etter: ", "Sort videos by: ": "Sorter videoer etter: ",
@ -99,11 +99,11 @@
"Administrator preferences": "Administratorinnstillinger", "Administrator preferences": "Administratorinnstillinger",
"Default homepage: ": "Forvalgt hjemmeside: ", "Default homepage: ": "Forvalgt hjemmeside: ",
"Feed menu: ": "Flyt-meny: ", "Feed menu: ": "Flyt-meny: ",
"Top enabled? ": "Topp påskrudd? ", "Top enabled: ": "Topp påskrudd? ",
"CAPTCHA enabled? ": "CAPTCHA påskrudd? ", "CAPTCHA enabled: ": "CAPTCHA påskrudd? ",
"Login enabled? ": "Innlogging påskrudd? ", "Login enabled: ": "Innlogging påskrudd? ",
"Registration enabled? ": "Registrering påskrudd? ", "Registration enabled: ": "Registrering påskrudd? ",
"Report statistics? ": "Innrapporter statistikk? ", "Report statistics: ": "Innrapporter statistikk? ",
"Save preferences": "Lagre innstillinger", "Save preferences": "Lagre innstillinger",
"Subscription manager": "Abonnementsbehandler", "Subscription manager": "Abonnementsbehandler",
"Token manager": "Symbolbehandler", "Token manager": "Symbolbehandler",
@ -318,4 +318,4 @@
"Playlists": "Spillelister", "Playlists": "Spillelister",
"Community": "", "Community": "",
"Current version: ": "Nåværende versjon: " "Current version: ": "Nåværende versjon: "
} }

View File

@ -56,7 +56,7 @@
"Play next by default: ": "Standaard volgende video afspelen: ", "Play next by default: ": "Standaard volgende video afspelen: ",
"Autoplay next video: ": "Volgende video automatisch afspelen: ", "Autoplay next video: ": "Volgende video automatisch afspelen: ",
"Listen by default: ": "Standaard luisteren: ", "Listen by default: ": "Standaard luisteren: ",
"Proxy videos? ": "Video's afspelen via proxy? ", "Proxy videos: ": "Video's afspelen via proxy? ",
"Default speed: ": "Standaard afspeelsnelheid: ", "Default speed: ": "Standaard afspeelsnelheid: ",
"Preferred video quality: ": "Voorkeurskwaliteit: ", "Preferred video quality: ": "Voorkeurskwaliteit: ",
"Player volume: ": "Spelervolume: ", "Player volume: ": "Spelervolume: ",
@ -65,13 +65,13 @@
"reddit": "Reddit", "reddit": "Reddit",
"Default captions: ": "Standaard ondertiteling: ", "Default captions: ": "Standaard ondertiteling: ",
"Fallback captions: ": "Alternatieve ondertiteling: ", "Fallback captions: ": "Alternatieve ondertiteling: ",
"Show related videos? ": "Gerelateerde video's tonen? ", "Show related videos: ": "Gerelateerde video's tonen? ",
"Show annotations by default? ": "Standaard annotaties tonen? ", "Show annotations by default: ": "Standaard annotaties tonen? ",
"Visual preferences": "Visuele instellingen", "Visual preferences": "Visuele instellingen",
"Dark mode: ": "Donkere modus: ", "Dark mode: ": "Donkere modus: ",
"Thin mode: ": "Smalle modus: ", "Thin mode: ": "Smalle modus: ",
"Subscription preferences": "Abonnementsinstellingen", "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: ", "Redirect homepage to feed: ": "Startpagina omleiden naar feed: ",
"Number of videos shown in feed: ": "Aantal te tonen video's in feed: ", "Number of videos shown in feed: ": "Aantal te tonen video's in feed: ",
"Sort videos by: ": "Video's sorteren op: ", "Sort videos by: ": "Video's sorteren op: ",
@ -99,11 +99,11 @@
"Administrator preferences": "Beheerdersinstellingen", "Administrator preferences": "Beheerdersinstellingen",
"Default homepage: ": "Standaard startpagina: ", "Default homepage: ": "Standaard startpagina: ",
"Feed menu: ": "Feedmenu:", "Feed menu: ": "Feedmenu:",
"Top enabled? ": "Bovenkant inschakelen? ", "Top enabled: ": "Bovenkant inschakelen? ",
"CAPTCHA enabled? ": "CAPTCHA gebruiken? ", "CAPTCHA enabled: ": "CAPTCHA gebruiken? ",
"Login enabled? ": "Inloggen toestaan? ", "Login enabled: ": "Inloggen toestaan? ",
"Registration enabled? ": "Registratie toestaan? ", "Registration enabled: ": "Registratie toestaan? ",
"Report statistics? ": "Statistieken bijhouden? ", "Report statistics: ": "Statistieken bijhouden? ",
"Save preferences": "Instellingen opslaan", "Save preferences": "Instellingen opslaan",
"Subscription manager": "Abonnementen beheren", "Subscription manager": "Abonnementen beheren",
"Token manager": "Toegangssleutels beheren", "Token manager": "Toegangssleutels beheren",

View File

@ -56,7 +56,7 @@
"Play next by default: ": "", "Play next by default: ": "",
"Autoplay next video: ": "Odtwórz następny film: ", "Autoplay next video: ": "Odtwórz następny film: ",
"Listen by default: ": "Tryb dźwiękowy: ", "Listen by default: ": "Tryb dźwiękowy: ",
"Proxy videos? ": "Filmy przez proxy? ", "Proxy videos: ": "Filmy przez proxy? ",
"Default speed: ": "Domyślna prędkość: ", "Default speed: ": "Domyślna prędkość: ",
"Preferred video quality: ": "Preferowana jakość filmów: ", "Preferred video quality: ": "Preferowana jakość filmów: ",
"Player volume: ": "Głośność odtwarzacza: ", "Player volume: ": "Głośność odtwarzacza: ",
@ -65,13 +65,13 @@
"reddit": "", "reddit": "",
"Default captions: ": "Domyślne napisy: ", "Default captions: ": "Domyślne napisy: ",
"Fallback captions: ": "Zastępcze napisy: ", "Fallback captions: ": "Zastępcze napisy: ",
"Show related videos? ": "Pokaż powiązane filmy? ", "Show related videos: ": "Pokaż powiązane filmy? ",
"Show annotations by default? ": "", "Show annotations by default: ": "",
"Visual preferences": "Preferencje Wizualne", "Visual preferences": "Preferencje Wizualne",
"Dark mode: ": "Ciemny motyw: ", "Dark mode: ": "Ciemny motyw: ",
"Thin mode: ": "Tryb minimalny: ", "Thin mode: ": "Tryb minimalny: ",
"Subscription preferences": "Preferencje subskrybcji", "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: ", "Redirect homepage to feed: ": "Przekieruj stronę główną do subskrybcji: ",
"Number of videos shown in feed: ": "Liczba filmów widoczna na stronie subskrybcji: ", "Number of videos shown in feed: ": "Liczba filmów widoczna na stronie subskrybcji: ",
"Sort videos by: ": "Sortuj filmy: ", "Sort videos by: ": "Sortuj filmy: ",
@ -99,11 +99,11 @@
"Administrator preferences": "Preferencje administratora", "Administrator preferences": "Preferencje administratora",
"Default homepage: ": "Domyślna strona główna: ", "Default homepage: ": "Domyślna strona główna: ",
"Feed menu: ": "", "Feed menu: ": "",
"Top enabled? ": "", "Top enabled: ": "",
"CAPTCHA enabled? ": "CAPTCHA aktywna? ", "CAPTCHA enabled: ": "CAPTCHA aktywna? ",
"Login enabled? ": "Logowanie włączone? ", "Login enabled: ": "Logowanie włączone? ",
"Registration enabled? ": "Rejestracja włączona? ", "Registration enabled: ": "Rejestracja włączona? ",
"Report statistics? ": "Raportować statystyki? ", "Report statistics: ": "Raportować statystyki? ",
"Save preferences": "Zapisz preferencje", "Save preferences": "Zapisz preferencje",
"Subscription manager": "Manager subskrybcji", "Subscription manager": "Manager subskrybcji",
"Token manager": "", "Token manager": "",

View File

@ -56,7 +56,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: ": "Проигрывать видео через прокси? ",
"Default speed: ": "Скорость видео по умолчанию: ", "Default speed: ": "Скорость видео по умолчанию: ",
"Preferred video quality: ": "Предпочтительное качество видео: ", "Preferred video quality: ": "Предпочтительное качество видео: ",
"Player volume: ": "Громкость видео: ", "Player volume: ": "Громкость видео: ",
@ -65,13 +65,13 @@
"reddit": "Reddit", "reddit": "Reddit",
"Default captions: ": "Основной язык субтитров: ", "Default captions: ": "Основной язык субтитров: ",
"Fallback captions: ": "Дополнительный язык субтитров: ", "Fallback captions: ": "Дополнительный язык субтитров: ",
"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: ": "Всегда показывать аннотации в видео каналов, на которые вы подписаны? ",
"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: ": "Сортировать видео: ",
@ -99,11 +99,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: ": "Сообщать статистику? ",
"Save preferences": "Сохранить настройки", "Save preferences": "Сохранить настройки",
"Subscription manager": "Менеджер подписок", "Subscription manager": "Менеджер подписок",
"Token manager": "Менеджер токенов", "Token manager": "Менеджер токенов",

View File

@ -56,7 +56,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: ": "Програвати відео через проксі? ",
"Default speed: ": "Усталена швидкість відео: ", "Default speed: ": "Усталена швидкість відео: ",
"Preferred video quality: ": "Пріорітетна якість відео: ", "Preferred video quality: ": "Пріорітетна якість відео: ",
"Player volume: ": "Гучність відео: ", "Player volume: ": "Гучність відео: ",
@ -65,13 +65,13 @@
"reddit": "Reddit", "reddit": "Reddit",
"Default captions: ": "Основна мова субтитрів: ", "Default captions: ": "Основна мова субтитрів: ",
"Fallback captions: ": "Запасна мова субтитрів: ", "Fallback captions: ": "Запасна мова субтитрів: ",
"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: ": "Завжди показувати анотації у відео каналів, на які ви підписані? ",
"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: ": "Сортувати відео: ",
@ -99,11 +99,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: ": "Повідомляти статистику? ",
"Save preferences": "Зберегти налаштування", "Save preferences": "Зберегти налаштування",
"Subscription manager": "Менеджер підписок", "Subscription manager": "Менеджер підписок",
"Token manager": "Менеджер токенів", "Token manager": "Менеджер токенів",

View File

@ -56,7 +56,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: ": "代理视频?",
"Default speed: ": "默认速度:", "Default speed: ": "默认速度:",
"Preferred video quality: ": "视频质量偏好:", "Preferred video quality: ": "视频质量偏好:",
"Player volume: ": "播放器音量:", "Player volume: ": "播放器音量:",
@ -65,13 +65,13 @@
"reddit": "Reddit", "reddit": "Reddit",
"Default captions: ": "默认字幕语言:", "Default captions: ": "默认字幕语言:",
"Fallback captions: ": "后备字幕语言:", "Fallback captions: ": "后备字幕语言:",
"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: ": "在订阅频道的视频默认显示注释?",
"Redirect homepage to feed: ": "跳转主页到 feed: ", "Redirect homepage to feed: ": "跳转主页到 feed: ",
"Number of videos shown in feed: ": "Feed 中显示的视频数量:", "Number of videos shown in feed: ": "Feed 中显示的视频数量:",
"Sort videos by: ": "视频排序方式:", "Sort videos by: ": "视频排序方式:",
@ -99,11 +99,11 @@
"Administrator preferences": "管理员选项", "Administrator preferences": "管理员选项",
"Default homepage: ": "默认主页:", "Default homepage: ": "默认主页:",
"Feed menu: ": "Feed 菜单:", "Feed menu: ": "Feed 菜单:",
"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": "令牌管理器",

File diff suppressed because one or more lines are too long

View File

@ -532,6 +532,30 @@ get "/watch" do |env|
templated "watch" templated "watch"
end 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| get "/embed/:id" do |env|
locale = LOCALES[env.get("preferences").as(Preferences).locale]? locale = LOCALES[env.get("preferences").as(Preferences).locale]?
id = env.params.url["id"] id = env.params.url["id"]
@ -559,7 +583,8 @@ get "/embed/:id" do |env|
# YouTube embed supports `videoseries` with either `list=PLID` # YouTube embed supports `videoseries` with either `list=PLID`
# or `playlist=VIDEO_ID,VIDEO_ID` # or `playlist=VIDEO_ID,VIDEO_ID`
if id == "videoseries" case id
when "videoseries"
url = "" url = ""
if plid if plid
@ -584,7 +609,26 @@ get "/embed/:id" do |env|
end end
next env.redirect url 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]}" url = "/embed/#{id[0, 11]}"
if env.params.query.size > 0 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) count, videos = search(search_query, page, search_params, region).as(Tuple)
end end
env.set "search", query
templated "search" templated "search"
end end
@ -892,6 +937,7 @@ get "/login" do |env|
tfa = env.params.query["tfa"]? tfa = env.params.query["tfa"]?
tfa ||= false tfa ||= false
prompt = ""
templated "login" templated "login"
end end
@ -989,15 +1035,22 @@ post "/login" do |env|
next templated "error" next templated "error"
end end
if challenge_results[0][-1]?.try &.[0]?.try &.as_a? prompt_type = challenge_results[0][-1]?.try &.[0].as_a?.try &.[0][2]?
traceback << "User has 2FA.<br/>" 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 # 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 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| auth_type[8] == 6 || auth_type[8] == 9 }[0] 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]}..." 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] tl = challenge_results[1][2]
@ -1011,62 +1064,84 @@ post "/login" do |env|
tfa = challenge_results[0][-1][0][0] tfa = challenge_results[0][-1][0][0]
end end
if tfa[2] == "TWO_STEP_VERIFICATION" if tfa[5] == "QUOTA_EXCEEDED"
if tfa[5] == "QUOTA_EXCEEDED" error_message = translate(locale, "Quota exceeded, try again in a few hours")
error_message = translate(locale, "Quota exceeded, try again in a few hours") env.response.status_code = 423
env.response.status_code = 423 next templated "error"
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/>"
end 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 end
traceback << "Logging in..." traceback << "Logging in..."
@ -1080,8 +1155,7 @@ post "/login" do |env|
end end
# TODO: Occasionally there will be a second page after login confirming # TODO: Occasionally there will be a second page after login confirming
# the user's phone number, which we will likely choke on. # the user's phone number ("/b/0/SmsAuthInterstitial"), which we currently choke on.
# if location.includes? "SmsAuthInterstitial"
login = client.get(location, headers) login = client.get(location, headers)
headers = login.cookies.add_request_headers(headers) headers = login.cookies.add_request_headers(headers)
@ -1225,6 +1299,7 @@ post "/login" do |env|
account_type = "invidious" account_type = "invidious"
tfa = false tfa = false
prompt = ""
if captcha_type == "image" if captcha_type == "image"
captcha = generate_captcha(HMAC_KEY, PG_DB) 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 = items.map { |item| item.as(SearchPlaylist) }
items.each { |item| item.author = "" } items.each { |item| item.author = "" }
env.set "search", "channel:#{channel.ucid} "
templated "playlists" templated "playlists"
end end
@ -3036,6 +3112,7 @@ get "/channel/:ucid/community" do |env|
error_message = ex.message error_message = ex.message
end end
env.set "search", "channel:#{channel.ucid} "
templated "community" templated "community"
end end
@ -3572,7 +3649,7 @@ get "/api/v1/top" do |env|
generate_thumbnails(json, video.id, config, Kemal.config) generate_thumbnails(json, video.id, config, Kemal.config)
end end
json.field "lengthSeconds", video.info["length_seconds"].to_i json.field "lengthSeconds", video.length_seconds
json.field "viewCount", video.views json.field "viewCount", video.views
json.field "author", video.author 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.build(indent: " ", encoding: "UTF-8") do |xml|
xml.element("MPD", "xmlns": "urn:mpeg:dash:schema:mpd:2011", xml.element("MPD", "xmlns": "urn:mpeg:dash:schema:mpd:2011",
"profiles": "urn:mpeg:dash:profile:full:2011", minBufferTime: "PT1.5S", type: "static", "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 xml.element("Period") do
i = 0 i = 0

View File

@ -375,13 +375,14 @@ def fetch_channel_playlists(ucid, author, auto_generated, continuation, sort_by)
json = JSON.parse(response.body) json = JSON.parse(response.body)
if json["load_more_widget_html"].as_s.empty? if json["load_more_widget_html"].as_s.empty?
return [] of SearchItem, nil continuation = nil
end 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) if continuation
continuation = continuation.xpath_node(%q(//button[@data-uix-load-more-href])) continuation = extract_channel_playlists_cursor(continuation["data-uix-load-more-href"], auto_generated)
if continuation end
continuation = extract_channel_playlists_cursor(continuation["data-uix-load-more-href"], auto_generated)
end end
html = XML.parse_html(json["content_html"].as_s) 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 switch = 0x00
end end
meta = IO::Memory.new data = IO::Memory.new
meta.write(Bytes[0x12, 0x06]) data.write_byte 0x12
meta.print("videos") data.write_byte 0x06
data.print "videos"
meta.write(Bytes[0x30, 0x02]) data.write Bytes[0x30, 0x02]
meta.write(Bytes[0x38, 0x01]) data.write Bytes[0x38, 0x01]
meta.write(Bytes[0x60, 0x01]) data.write Bytes[0x60, 0x01]
meta.write(Bytes[0x6a, 0x00]) data.write Bytes[0x6a, 0x00]
meta.write(Bytes[0xb8, 0x01, 0x00]) data.write Bytes[0xb8, 0x01, 0x00]
meta.write(Bytes[0x20, switch]) data.write Bytes[0x20, switch]
meta.write(Bytes[0x7a, page.size]) data.write_byte 0x7a
meta.print(page) VarInt.to_io(data, page.bytesize)
data.print page
case sort_by case sort_by
when "newest" when "newest"
# Empty tags can be omitted # Empty tags can be omitted
# meta.write(Bytes[0x18,0x00]) # meta.write(Bytes[0x18,0x00])
when "popular" when "popular"
meta.write(Bytes[0x18, 0x01]) data.write Bytes[0x18, 0x01]
when "oldest" when "oldest"
meta.write(Bytes[0x18, 0x02]) data.write Bytes[0x18, 0x02]
end end
meta.rewind data = Base64.urlsafe_encode(data)
meta = Base64.urlsafe_encode(meta.to_slice) cursor = URI.escape(data)
meta = URI.escape(meta)
continuation = IO::Memory.new data = IO::Memory.new
continuation.write(Bytes[0x12, ucid.size])
continuation.print(ucid)
continuation.write(Bytes[0x1a, meta.size]) data.write_byte 0x12
continuation.print(meta) VarInt.to_io(data, ucid.bytesize)
data.print ucid
continuation.rewind data.write_byte 0x1a
continuation = continuation.gets_to_end VarInt.to_io(data, cursor.bytesize)
data.print cursor
wrapper = IO::Memory.new data.rewind
wrapper.write(Bytes[0xe2, 0xa9, 0x85, 0xb2, 0x02, continuation.size])
wrapper.print(continuation)
wrapper.rewind
wrapper = Base64.urlsafe_encode(wrapper.to_slice) buffer = IO::Memory.new
wrapper = URI.escape(wrapper) 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 return url
end end
@ -496,117 +501,108 @@ def produce_channel_playlists_url(ucid, cursor, sort = "newest", auto_generated
cursor = Base64.urlsafe_encode(cursor, false) cursor = Base64.urlsafe_encode(cursor, false)
end end
meta = IO::Memory.new data = IO::Memory.new
if auto_generated if auto_generated
meta.write(Bytes[0x08, 0x0a]) data.write Bytes[0x08, 0x0a]
end end
meta.write(Bytes[0x12, 0x09]) data.write Bytes[0x12, 0x09]
meta.print("playlists") data.print "playlists"
if auto_generated if auto_generated
meta.write(Bytes[0x20, 0x32]) data.write Bytes[0x20, 0x32]
else else
# TODO: Look at 0x01, 0x00 # TODO: Look at 0x01, 0x00
case sort case sort
when "oldest", "oldest_created" when "oldest", "oldest_created"
meta.write(Bytes[0x18, 0x02]) data.write Bytes[0x18, 0x02]
when "newest", "newest_created" when "newest", "newest_created"
meta.write(Bytes[0x18, 0x03]) data.write Bytes[0x18, 0x03]
when "last", "last_added" when "last", "last_added"
meta.write(Bytes[0x18, 0x04]) data.write Bytes[0x18, 0x04]
end end
meta.write(Bytes[0x20, 0x01]) data.write Bytes[0x20, 0x01]
end end
meta.write(Bytes[0x30, 0x02]) data.write Bytes[0x30, 0x02]
meta.write(Bytes[0x38, 0x01]) data.write Bytes[0x38, 0x01]
meta.write(Bytes[0x60, 0x01]) data.write Bytes[0x60, 0x01]
meta.write(Bytes[0x6a, 0x00]) data.write Bytes[0x6a, 0x00]
meta.write(Bytes[0x7a, cursor.size]) data.write_byte 0x7a
meta.print(cursor) VarInt.to_io(data, cursor.bytesize)
data.print cursor
meta.write(Bytes[0xb8, 0x01, 0x00]) data.write Bytes[0xb8, 0x01, 0x00]
meta.rewind data.rewind
meta = Base64.urlsafe_encode(meta.to_slice) data = Base64.urlsafe_encode(data)
meta = URI.escape(meta) continuation = URI.escape(data)
continuation = IO::Memory.new data = IO::Memory.new
continuation.write(Bytes[0x12, ucid.size])
continuation.print(ucid)
continuation.write(Bytes[0x1a]) data.write_byte 0x12
continuation.write(write_var_int(meta.size)) VarInt.to_io(data, ucid.bytesize)
continuation.print(meta) data.print ucid
continuation.rewind data.write_byte 0x1a
continuation = continuation.gets_to_end VarInt.to_io(data, continuation.bytesize)
data.print continuation
wrapper = IO::Memory.new data.rewind
wrapper.write(Bytes[0xe2, 0xa9, 0x85, 0xb2, 0x02])
wrapper.write(write_var_int(continuation.size))
wrapper.print(continuation)
wrapper.rewind
wrapper = Base64.urlsafe_encode(wrapper.to_slice) buffer = IO::Memory.new
wrapper = URI.escape(wrapper) 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 return url
end end
def extract_channel_playlists_cursor(url, auto_generated) 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) continuation = URI.unescape(continuation)
wrapper = Base64.decode(wrapper) data = IO::Memory.new(Base64.decode(continuation))
# 0xe2 0xa9 0x85 0xb2 0x02 # 0xe2 0xa9 0x85 0xb2 0x02
wrapper += 5 data.pos += 5
continuation_size = read_var_int(wrapper[0, 4]) continuation = Bytes.new(data.read_bytes(VarInt))
wrapper += write_var_int(continuation_size).size data.read continuation
continuation = wrapper[0, continuation_size] data = IO::Memory.new(continuation)
# 0x12 data.read_byte # => 0x12
continuation += 1 ucid = Bytes.new(data.read_bytes(VarInt))
ucid_size = continuation[0] data.read ucid
continuation += 1
ucid = continuation[0, ucid_size]
continuation += ucid_size
# 0x1a data.read_byte # => 0x1a
continuation += 1 inner_continuation = Bytes.new(data.read_bytes(VarInt))
meta_size = read_var_int(continuation[0, 4]) data.read inner_continuation
continuation += write_var_int(meta_size).size
meta = continuation[0, meta_size]
continuation += meta_size
meta = String.new(meta) continuation = String.new(inner_continuation)
meta = URI.unescape(meta) continuation = URI.unescape(continuation)
meta = Base64.decode(meta) data = IO::Memory.new(Base64.decode(continuation))
# 0x12 0x09 playlists # 0x12 0x09 playlists
meta += 11 data.pos += 11
until meta[0] == 0x7a until data.peek[0] == 0x7a
tag = read_var_int(meta[0, 4]) key = data.read_bytes(VarInt)
meta += write_var_int(tag).size value = data.read_bytes(VarInt)
value = meta[0]
meta += 1
end end
# 0x7a data.pos += 1 # => 0x7a
meta += 1 cursor = Bytes.new(data.read_bytes(VarInt))
cursor_size = meta[0] data.read cursor
meta += 1
cursor = meta[0, cursor_size]
cursor = String.new(cursor) cursor = String.new(cursor)
if !auto_generated if !auto_generated
@ -873,20 +869,26 @@ end
def produce_channel_community_continuation(ucid, cursor) def produce_channel_community_continuation(ucid, cursor)
cursor = URI.escape(cursor) cursor = URI.escape(cursor)
continuation = IO::Memory.new
continuation.write(Bytes[0xe2, 0xa9, 0x85, 0xb2, 0x02]) data = IO::Memory.new
continuation.write(write_var_int(3 + ucid.size + write_var_int(cursor.size).size + cursor.size))
continuation.write(Bytes[0x12, ucid.size]) data.write_byte 0x12
continuation.print(ucid) VarInt.to_io(data, ucid.bytesize)
data.print ucid
continuation.write(Bytes[0x1a]) data.write_byte 0x1a
continuation.write(write_var_int(cursor.size)) VarInt.to_io(data, cursor.bytesize)
continuation.print(cursor) data.print cursor
continuation.rewind
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) continuation = URI.escape(continuation)
return continuation return continuation
@ -894,28 +896,25 @@ end
def extract_channel_community_cursor(continuation) def extract_channel_community_cursor(continuation)
continuation = URI.unescape(continuation) continuation = URI.unescape(continuation)
continuation = Base64.decode(continuation) data = IO::Memory.new(Base64.decode(continuation))
# 0xe2 0xa9 0x85 0xb2 0x02 # 0xe2 0xa9 0x85 0xb2 0x02
continuation += 5 data.pos += 5
total_size = read_var_int(continuation[0, 4]) continuation = Bytes.new(data.read_bytes(VarInt))
continuation += write_var_int(total_size).size data.read continuation
data = IO::Memory.new(continuation)
# 0x12 data.read_byte # => 0x12
continuation += 1 ucid = Bytes.new(data.read_bytes(VarInt))
ucid_size = continuation[0] data.read ucid
continuation += 1
ucid = continuation[0, ucid_size]
continuation += ucid_size
# 0x1a data.read_byte # => 0x1a
continuation += 1 until data.peek[0] == 'E'.ord
until continuation[0] == 'E'.ord data.read_byte
continuation += 1
end end
return String.new(continuation) return URI.unescape(data.gets_to_end)
end end
def get_about_info(ucid, locale) def get_about_info(ucid, locale)

View File

@ -339,7 +339,7 @@ def template_youtube_comments(comments, locale, thin_mode)
END_HTML END_HTML
else else
html << <<-END_HTML 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_HTML
end end
@ -564,108 +564,105 @@ def content_to_comment_html(content)
end end
def produce_comment_continuation(video_id, cursor = "", sort_by = "top") 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]) data.write_byte 0x12
continuation.print(video_id) VarInt.to_io(data, video_id.bytesize)
data.print video_id
continuation.write(Bytes[0xc0, 0x01, 0x01]) data.write Bytes[0xc0, 0x01, 0x01]
continuation.write(Bytes[0xc8, 0x01, 0x01]) data.write Bytes[0xc8, 0x01, 0x01]
continuation.write(Bytes[0xe0, 0x01, 0x01]) data.write Bytes[0xe0, 0x01, 0x01]
continuation.write(Bytes[0xa2, 0x02, 0x0d]) data.write Bytes[0xa2, 0x02, 0x0d]
continuation.write(Bytes[0x28, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x01]) data.write Bytes[0x28, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x01]
continuation.write(Bytes[0x40, 0x00]) data.write Bytes[0x40, 0x00]
continuation.write(Bytes[0x18, 0x06]) data.write Bytes[0x18, 0x06]
if cursor.empty? if cursor.empty?
continuation.write(Bytes[0x32]) data.write Bytes[0x32]
continuation.write(write_var_int(video_id.size + 8)) VarInt.to_io(data, cursor.bytesize + video_id.bytesize + 8)
continuation.write(Bytes[0x22, video_id.size + 4]) data.write Bytes[0x22, video_id.bytesize + 4]
continuation.write(Bytes[0x22, video_id.size]) data.write Bytes[0x22, video_id.bytesize]
continuation.print(video_id) data.print video_id
case sort_by case sort_by
when "top" when "top"
continuation.write(Bytes[0x30, 0x00]) data.write Bytes[0x30, 0x00]
when "new", "newest" when "new", "newest"
continuation.write(Bytes[0x30, 0x01]) data.write Bytes[0x30, 0x01]
end end
continuation.write(Bytes[0x78, 0x02]) data.write(Bytes[0x78, 0x02])
else else
continuation.write(Bytes[0x32]) data.write Bytes[0x32]
continuation.write(write_var_int(cursor.size + video_id.size + 11)) VarInt.to_io(data, cursor.bytesize + video_id.bytesize + 11)
continuation.write(Bytes[0x0a]) data.write_byte 0x0a
continuation.write(write_var_int(cursor.size)) VarInt.to_io(data, cursor.bytesize)
continuation.print(cursor) data.print cursor
continuation.write(Bytes[0x22, video_id.size + 4]) data.write Bytes[0x22, video_id.bytesize + 4]
continuation.write(Bytes[0x22, video_id.size]) data.write Bytes[0x22, video_id.bytesize]
continuation.print(video_id) data.print video_id
case sort_by case sort_by
when "top" when "top"
continuation.write(Bytes[0x30, 0x00]) data.write Bytes[0x30, 0x00]
when "new", "newest" when "new", "newest"
continuation.write(Bytes[0x30, 0x01]) data.write Bytes[0x30, 0x01]
end end
continuation.write(Bytes[0x28, 0x14]) data.write Bytes[0x28, 0x14]
end end
continuation.rewind continuation = Base64.urlsafe_encode(data)
continuation = continuation.gets_to_end
continuation = Base64.urlsafe_encode(continuation.to_slice)
continuation = URI.escape(continuation) continuation = URI.escape(continuation)
return continuation return continuation
end end
def produce_comment_reply_continuation(video_id, ucid, comment_id) 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]) data.write_byte 0x12
continuation.print(video_id) VarInt.to_io(data, video_id.size)
data.print video_id
continuation.write(Bytes[0xc0, 0x01, 0x01]) data.write Bytes[0xc0, 0x01, 0x01]
continuation.write(Bytes[0xc8, 0x01, 0x01]) data.write Bytes[0xc8, 0x01, 0x01]
continuation.write(Bytes[0xe0, 0x01, 0x01]) data.write Bytes[0xe0, 0x01, 0x01]
continuation.write(Bytes[0xa2, 0x02, 0x0d]) data.write Bytes[0xa2, 0x02, 0x0d]
continuation.write(Bytes[0x28, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x01]) data.write Bytes[0x28, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x01]
continuation.write(Bytes[0x40, 0x00]) data.write Bytes[0x40, 0x00]
continuation.write(Bytes[0x18, 0x06]) data.write Bytes[0x18, 0x06]
continuation.write(Bytes[0x32, ucid.size + video_id.size + comment_id.size + 16]) data.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[0x1a, ucid.size + video_id.size + comment_id.size + 14])
continuation.write(Bytes[0x12, comment_id.size]) data.write_byte 0x12
continuation.print(comment_id) 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]) data.write(Bytes[ucid.size + video_id.size + 7])
continuation.write(Bytes[ucid.size]) data.write(Bytes[ucid.size])
continuation.print(ucid) data.print(ucid)
continuation.write(Bytes[0x32, video_id.size]) data.write(Bytes[0x32, video_id.size])
continuation.print(video_id) data.print(video_id)
continuation.write(Bytes[0x40, 0x01]) data.write(Bytes[0x40, 0x01])
continuation.write(Bytes[0x48, 0x0a]) data.write(Bytes[0x48, 0x0a])
continuation.rewind continuation = Base64.urlsafe_encode(data.to_slice)
continuation = continuation.gets_to_end
continuation = Base64.urlsafe_encode(continuation.to_slice)
continuation = URI.escape(continuation) continuation = URI.escape(continuation)
return continuation return continuation

View File

@ -87,12 +87,40 @@ end
struct Config struct Config
module ConfigPreferencesConverter 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 def self.from_yaml(ctx : YAML::ParseContext, node : YAML::Nodes::Node) : Preferences
Preferences.new(*ConfigPreferences.new(ctx, node).to_tuple) Preferences.new(*ConfigPreferences.new(ctx, node).to_tuple)
end end
end
def self.to_yaml(value : Preferences, yaml : YAML::Nodes::Builder) module FamilyConverter
value.to_yaml(yaml) 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
end end
@ -131,12 +159,13 @@ struct Config
default: Preferences.new(*ConfigPreferences.from_yaml("").to_tuple), default: Preferences.new(*ConfigPreferences.from_yaml("").to_tuple),
converter: ConfigPreferencesConverter, converter: ConfigPreferencesConverter,
}, },
dmca_content: {type: Array(String), default: [] of String}, # For compliance with DMCA, disables download widget using list of video IDs 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. 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 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. 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 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' 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 end
@ -650,48 +679,6 @@ def cache_annotation(db, id, annotations)
end end
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) def create_notification_stream(env, config, kemal_config, decrypt_function, topics, connection_channel)
connection = Channel(PQ::Notification).new(8) connection = Channel(PQ::Notification).new(8)
connection_channel.send({true, connection}) connection_channel.send({true, connection})
@ -834,3 +821,79 @@ def extract_initial_data(body)
return JSON.parse(initial_data) return JSON.parse(initial_data)
end end
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) def make_client(url : URI, region = nil)
client = HTTPClient.new(url) client = HTTPClient.new(url)
client.family = CONFIG.force_resolve
client.read_timeout = 15.seconds client.read_timeout = 15.seconds
client.connect_timeout = 15.seconds client.connect_timeout = 15.seconds
@ -265,50 +266,40 @@ def get_referer(env, fallback = "/", unroll = true)
return referer return referer
end end
def read_var_int(bytes) struct VarInt
num_read = 0 def self.from_io(io : IO, format = IO::ByteFormat::BigEndian) : Int32
result = 0 result = 0_i32
num_read = 0
read = bytes[num_read] loop do
byte = io.read_byte
if bytes.size == 1 raise "Invalid VarInt" if !byte
result = bytes[0].to_i32 value = byte & 0x7f
else
while ((read & 0b10000000) != 0)
read = bytes[num_read].to_u64
value = (read & 0b01111111)
result |= (value << (7 * num_read))
result |= value.to_i32 << (7 * num_read)
num_read += 1 num_read += 1
if num_read > 5
raise "VarInt is too big" break if byte & 0x80 == 0
end raise "Invalid VarInt" if num_read > 5
end end
result
end end
return result def self.to_io(io : IO, value : Int32)
end 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 while value != 0
temp = (value & 0b01111111).to_u8 byte = (value & 0x7f).to_u8
value = value >> 7 value >>= 7
if value != 0 if value != 0
temp |= 0b10000000 byte |= 0x80
end end
bytes << temp io.write_byte byte
end end
end end
return Slice.new(bytes.to_unsafe, bytes.size)
end end
def sha256(text) def sha256(text)

View File

@ -157,37 +157,44 @@ def produce_playlist_url(id, index)
end end
ucid = "VL" + id ucid = "VL" + id
meta = IO::Memory.new data = IO::Memory.new
meta.write(Bytes[0x08]) data.write_byte 0x08
meta.write(write_var_int(index)) VarInt.to_io(data, index)
meta.rewind data.rewind
meta = Base64.urlsafe_encode(meta.to_slice, false) data = Base64.urlsafe_encode(data, false)
meta = "PT:#{meta}" data = "PT:#{data}"
continuation = IO::Memory.new continuation = IO::Memory.new
continuation.write(Bytes[0x7a, meta.size]) continuation.write_byte 0x7a
continuation.print(meta) VarInt.to_io(continuation, data.bytesize)
continuation.print data
continuation.rewind data = Base64.urlsafe_encode(continuation)
meta = Base64.urlsafe_encode(continuation.to_slice) cursor = URI.escape(data)
meta = URI.escape(meta)
continuation = IO::Memory.new data = IO::Memory.new
continuation.write(Bytes[0x12, ucid.size])
continuation.print(ucid)
continuation.write(Bytes[0x1a, meta.size])
continuation.print(meta)
wrapper = IO::Memory.new data.write_byte 0x12
wrapper.write(Bytes[0xe2, 0xa9, 0x85, 0xb2, 0x02, continuation.size]) VarInt.to_io(data, ucid.bytesize)
wrapper.print(continuation) data.print ucid
wrapper.rewind
wrapper = Base64.urlsafe_encode(wrapper.to_slice) data.write_byte 0x1a
wrapper = URI.escape(wrapper) 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 return url
end end

View File

@ -374,45 +374,51 @@ end
def produce_channel_search_url(ucid, query, page) def produce_channel_search_url(ucid, query, page)
page = "#{page}" page = "#{page}"
meta = IO::Memory.new data = IO::Memory.new
meta.write(Bytes[0x12, 0x06]) data.write_byte 0x12
meta.print("search") data.write_byte 0x06
data.print "search"
meta.write(Bytes[0x30, 0x02]) data.write Bytes[0x30, 0x02]
meta.write(Bytes[0x38, 0x01]) data.write Bytes[0x38, 0x01]
meta.write(Bytes[0x60, 0x01]) data.write Bytes[0x60, 0x01]
meta.write(Bytes[0x6a, 0x00]) data.write Bytes[0x6a, 0x00]
meta.write(Bytes[0xb8, 0x01, 0x00]) data.write Bytes[0xb8, 0x01, 0x00]
meta.write(Bytes[0x7a, page.size]) data.write_byte 0x7a
meta.print(page) VarInt.to_io(data, page.bytesize)
data.print page
meta.rewind data.rewind
meta = Base64.urlsafe_encode(meta.to_slice) data = Base64.urlsafe_encode(data)
meta = URI.escape(meta) continuation = URI.escape(data)
continuation = IO::Memory.new data = IO::Memory.new
continuation.write(Bytes[0x12, ucid.size])
continuation.print(ucid)
continuation.write(Bytes[0x1a, meta.size]) data.write_byte 0x12
continuation.print(meta) VarInt.to_io(data, ucid.bytesize)
data.print ucid
continuation.write(Bytes[0x5a, query.size]) data.write_byte 0x1a
continuation.print(query) VarInt.to_io(data, continuation.bytesize)
data.print continuation
continuation.rewind data.write_byte 0x5a
continuation = continuation.gets_to_end VarInt.to_io(data, query.bytesize)
data.print query
wrapper = IO::Memory.new data.rewind
wrapper.write(Bytes[0xe2, 0xa9, 0x85, 0xb2, 0x02, continuation.size])
wrapper.print(continuation)
wrapper.rewind
wrapper = Base64.urlsafe_encode(wrapper.to_slice) buffer = IO::Memory.new
wrapper = URI.escape(wrapper) 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 return url
end end

View File

@ -329,7 +329,7 @@ struct Video
json.field "subCountText", self.sub_count_text 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 "allowRatings", self.allow_ratings
json.field "rating", self.info["avg_rating"].to_f32 json.field "rating", self.info["avg_rating"].to_f32
json.field "isListed", self.is_listed json.field "isListed", self.is_listed
@ -563,7 +563,14 @@ struct Video
fmt["clen"] = fmt_stream["contentLength"]?.try &.as_s || "0" fmt["clen"] = fmt_stream["contentLength"]?.try &.as_s || "0"
fmt["bitrate"] = fmt_stream["bitrate"]?.try &.as_i.to_s || "0" fmt["bitrate"] = fmt_stream["bitrate"]?.try &.as_i.to_s || "0"
fmt["itag"] = fmt_stream["itag"].as_i.to_s 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 fmt["quality"] = fmt_stream["quality"].as_s
if fmt_stream["width"]? if fmt_stream["width"]?
@ -635,8 +642,14 @@ struct Video
fmt["clen"] = adaptive_fmt["contentLength"]?.try &.as_s || "0" fmt["clen"] = adaptive_fmt["contentLength"]?.try &.as_s || "0"
fmt["bitrate"] = adaptive_fmt["bitrate"]?.try &.as_i.to_s || "0" fmt["bitrate"] = adaptive_fmt["bitrate"]?.try &.as_i.to_s || "0"
fmt["itag"] = adaptive_fmt["itag"].as_i.to_s 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"]? if index = adaptive_fmt["indexRange"]?
fmt["index"] = "#{index["start"]}-#{index["end"]}" fmt["index"] = "#{index["start"]}-#{index["end"]}"
end end
@ -827,7 +840,7 @@ struct Video
end end
def length_seconds def length_seconds
return self.info["length_seconds"].to_i self.player_response["videoDetails"]["lengthSeconds"].as_s.to_i
end end
db_mapping({ db_mapping({
@ -1162,17 +1175,19 @@ def fetch_video(id, region)
end end
end end
if info["errorcode"]?.try &.== "2" if info["errorcode"]?.try &.== "2" || !info["player_response"]
raise "Video unavailable." raise "Video unavailable."
end end
if !info["title"]? || info["title"].empty? if info["reason"]?
raise "Video unavailable." raise info["reason"]
end end
title = info["title"] player_json = JSON.parse(info["player_response"])
author = info["author"]? || ""
ucid = info["ucid"]? || "" 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"])) views = html.xpath_node(%q(//meta[@itemprop="interactionCount"]))
.try &.["content"].to_i64? || 0_i64 .try &.["content"].to_i64? || 0_i64

View File

@ -30,7 +30,7 @@
var video_data = { var video_data = {
id: '<%= video.id %>', id: '<%= video.id %>',
plid: '<%= plid %>', plid: '<%= plid %>',
length_seconds: '<%= video.info["length_seconds"].to_f %>', length_seconds: '<%= video.length_seconds.to_f %>',
video_series: <%= video_series.to_json %>, video_series: <%= video_series.to_json %>,
params: <%= params.to_json %>, params: <%= params.to_json %>,
preferences: <%= preferences.to_json %>, preferences: <%= preferences.to_json %>,

View File

@ -102,8 +102,8 @@
<% end %> <% end %>
<% if tfa %> <% if tfa %>
<label for="tfa"><%= translate(locale, "Google verification code") %> :</label> <label for="tfa"><%= translate(locale, prompt) %> :</label>
<input required class="pure-input-1" name="tfa" type="text" placeholder="<%= translate(locale, "Google verification code") %>"> <input required class="pure-input-1" name="tfa" type="text" placeholder="<%= translate(locale, prompt) %>">
<% end %> <% end %>
<button type="submit" class="pure-button pure-button-primary"><%= translate(locale, "Sign In") %></button> <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-g h-box">
<div class="pure-u-1 pure-u-md-4-5"></div> <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"> <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 %>"> <a href="/channel/<%= channel.ucid %>/playlists?continuation=<%= continuation %><% if sort_by != "last" %>&sort_by=<%= sort_by %><% end %>">
<%= translate(locale, "Next page") %> <%= translate(locale, "Next page") %>
</a> </a>

View File

@ -34,7 +34,7 @@ function update_value(element) {
</div> </div>
<div class="pure-control-group"> <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 %>> <input name="local" id="local" type="checkbox" <% if preferences.local && !CONFIG.disabled?("local") %>checked<% end %> <% if CONFIG.disabled?("local") %>disabled<% end %>>
</div> </div>
@ -46,7 +46,7 @@ function update_value(element) {
<div class="pure-control-group"> <div class="pure-control-group">
<label for="speed"><%= translate(locale, "Default speed: ") %></label> <label for="speed"><%= translate(locale, "Default speed: ") %></label>
<select name="speed" id="speed"> <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> <option <% if preferences.speed == option %> selected <% end %>><%= option %></option>
<% end %> <% end %>
</select> </select>
@ -92,12 +92,12 @@ function update_value(element) {
</div> </div>
<div class="pure-control-group"> <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 %>> <input name="related_videos" id="related_videos" type="checkbox" <% if preferences.related_videos %>checked<% end %>>
</div> </div>
<div class="pure-control-group"> <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 %>> <input name="annotations" id="annotations" type="checkbox" <% if preferences.annotations %>checked<% end %>>
</div> </div>
@ -126,7 +126,7 @@ function update_value(element) {
<legend><%= translate(locale, "Subscription preferences") %></legend> <legend><%= translate(locale, "Subscription preferences") %></legend>
<div class="pure-control-group"> <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 %>> <input name="annotations_subscribed" id="annotations_subscribed" type="checkbox" <% if preferences.annotations_subscribed %>checked<% end %>>
</div> </div>
@ -200,27 +200,27 @@ function update_value(element) {
</div> </div>
<div class="pure-control-group"> <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 %>> <input name="top_enabled" id="top_enabled" type="checkbox" <% if config.top_enabled %>checked<% end %>>
</div> </div>
<div class="pure-control-group"> <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 %>> <input name="captcha_enabled" id="captcha_enabled" type="checkbox" <% if config.captcha_enabled %>checked<% end %>>
</div> </div>
<div class="pure-control-group"> <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 %>> <input name="login_enabled" id="login_enabled" type="checkbox" <% if config.login_enabled %>checked<% end %>>
</div> </div>
<div class="pure-control-group"> <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 %>> <input name="registration_enabled" id="registration_enabled" type="checkbox" <% if config.registration_enabled %>checked<% end %>>
</div> </div>
<div class="pure-control-group"> <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 %>> <input name="statistics_enabled" id="statistics_enabled" type="checkbox" <% if config.statistics_enabled %>checked<% end %>>
</div> </div>
<% end %> <% end %>

View File

@ -35,7 +35,7 @@
<div class="pure-u-1 pure-u-md-12-24 searchbar"> <div class="pure-u-1 pure-u-md-12-24 searchbar">
<form class="pure-form" action="/search" method="get"> <form class="pure-form" action="/search" method="get">
<fieldset> <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> </fieldset>
</form> </form>
</div> </div>

View File

@ -30,7 +30,7 @@
var video_data = { var video_data = {
id: '<%= video.id %>', id: '<%= video.id %>',
plid: '<%= plid %>', plid: '<%= plid %>',
length_seconds: <%= video.info["length_seconds"].to_f %>, length_seconds: <%= video.length_seconds.to_f %>,
play_next: <%= !rvs.empty? && !plid && params.continue %>, play_next: <%= !rvs.empty? && !plid && params.continue %>,
next_video: '<%= rvs.select { |rv| rv["id"]? }[0]?.try &.["id"] %>', next_video: '<%= rvs.select { |rv| rv["id"]? }[0]?.try &.["id"] %>',
youtube_comments_text: '<%= HTML.escape(translate(locale, "View YouTube comments")) %>', youtube_comments_text: '<%= HTML.escape(translate(locale, "View YouTube comments")) %>',