diff options
52 files changed, 868 insertions, 640 deletions
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 66aacff9..99aac794 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -7,6 +7,19 @@ on: - "api-only" pull_request: branches: "*" + paths-ignore: + - "*.md" + - LICENCE + - TRANSLATION + - invidious.service + - .git* + - .editorconfig + + - screenshots/* + - assets/** + - config/** + - .github/ISSUE_TEMPLATE/* + - kubernetes/** jobs: build: @@ -19,7 +32,7 @@ jobs: - name: Install Crystal uses: oprypin/install-crystal@v1.2.4 with: - crystal: 0.36.1 + crystal: 1.0.0 - name: Cache Shards uses: actions/cache@v2 diff --git a/.github/workflows/lock.yml b/.github/workflows/lock.yml new file mode 100644 index 00000000..afed2240 --- /dev/null +++ b/.github/workflows/lock.yml @@ -0,0 +1,22 @@ +# Documentation: https://github.com/marketplace/actions/lock-threads + +name: 'Lock Threads' +on: + workflow_dispatch: + schedule: + - cron: "0 */12 * * *" + +jobs: + lock: + runs-on: ubuntu-latest + steps: + - uses: dessant/lock-threads@v2 + with: + github-token: ${{ github.token }} + issue-lock-inactive-days: '30' + pr-lock-inactive-days: '30' + issue-lock-reason: 'resolved' + pr-lock-reason: 'resolved' + + # issue-lock-comment: 'This issue has been automatically locked since there has not been any activity in it in the last 30 days. If this is still applicable to the current version of Invidious feel free to open a new issue.' + # pr-lock-comment: 'This pull request has been automatically locked since there has not been any activity in it in the last 30 days. If you want to tell us about needed or wanted changes or if problems related to this code are discovered, feel free to open an issue or a new pull request.' diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index e452274b..22831ea2 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -14,9 +14,10 @@ jobs: with: repo-token: ${{ secrets.GITHUB_TOKEN }} days-before-stale: 365 + days-before-pr-stale: 45 # PRs should be active. Anything that hasn't had activity in more than 45 days should be considered abandoned. days-before-close: 30 stale-issue-message: 'This issue has been automatically marked as stale and will be closed in 30 days because it has not had recent activity and is much likely outdated. If you think this issue is still relevant and applicable, you just have to post a comment and it will be unmarked.' - stale-pr-message: 'This pull request has been automatically marked as stale and will be closed in 30 days because it has not had recent activity and is much likely outdated. If you think this pull request is still relevant and applicable, you just have to post a comment and it will be unmarked.' + stale-pr-message: 'This pull request has been automatically marked as stale and will be closed in 30 days because it has not had recent activity and is much likely abandoned or outdated. If you think this pull request is still relevant and applicable, you just have to post a comment and it will be unmarked.' stale-issue-label: "stale" stale-pr-label: "stale" ascending: true diff --git a/assets/css/default.css b/assets/css/default.css index 07a879bb..1d62bc01 100644 --- a/assets/css/default.css +++ b/assets/css/default.css @@ -488,7 +488,7 @@ body.dark-theme { the hr element is rendered improperly within one. See https://stackoverflow.com/a/34372979 for more info */ hr { - margin: auto 0 auto 0; + margin: 10px 0 10px 0; } /* Description Expansion Styling*/ diff --git a/assets/robots.txt b/assets/robots.txt index 316bd423..b7e8f5a9 100644 --- a/assets/robots.txt +++ b/assets/robots.txt @@ -1,3 +1,4 @@ User-agent: * Disallow: /search -Disallow: /login
\ No newline at end of file +Disallow: /login +Disallow: /watch
\ No newline at end of file diff --git a/docker/Dockerfile b/docker/Dockerfile index 87884403..b8e5af8a 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -1,4 +1,4 @@ -FROM crystallang/crystal:0.36.1-alpine AS builder +FROM crystallang/crystal:1.0.0-alpine AS builder RUN apk add --no-cache curl sqlite-static yaml-static WORKDIR /invidious COPY ./shard.yml ./shard.yml diff --git a/locales/ar.json b/locales/ar.json index f1248b03..779ea61d 100644 --- a/locales/ar.json +++ b/locales/ar.json @@ -1,15 +1,15 @@ { "`x` subscribers": { "([^.,0-9]|^)1([^.,0-9]|$)": "`x` المشتركين", - "": "`x` المشتركين" + "": "`x` المشتركين." }, "`x` videos": { "([^.,0-9]|^)1([^.,0-9]|$)": "`x` المقاطع المرئيَّة", - "": "`x` المقاطع المرئيَّة" + "": "`x` المقاطع المرئيَّة." }, "`x` playlists": { "([^.,0-9]|^)1([^.,0-9]|$)": "`x` قوائم التشغيل", - "": "`x` قوائم التشغيل" + "": "`x` قوائم التشغيل." }, "LIVE": "مُباشِر", "Shared `x` ago": "تمَّ رفع المقطع المرئيّ مُنذ `x`", @@ -39,8 +39,8 @@ "Import NewPipe subscriptions (.json)": "استيراد اشتراكات نيو بايب (.json)", "Import NewPipe data (.zip)": "استيراد بيانات نيو بايب (.zip)", "Export": "تصدير", - "Export subscriptions as OPML": "تصدير الاشتراكات كَـ OPML", - "Export subscriptions as OPML (for NewPipe & FreeTube)": "تصدير الاشتراكات كَـ OPML (لِنيو بايب و فريتيوب)", + "Export subscriptions as OPML": "تصدير الاشتراكات كَ OPML", + "Export subscriptions as OPML (for NewPipe & FreeTube)": "تصدير الاشتراكات كَ OPML (لِنيو بايب و فريتيوب)", "Export data as JSON": "تصدير البيانات بتنسيق JSON", "Delete account?": "حذف الحساب؟", "History": "السِّجل", @@ -77,8 +77,8 @@ "Fallback captions: ": "التسميات التوضيحية الاحتياطيَّة: ", "Show related videos: ": "اعرض الفيديوهات ذات الصلة: ", "Show annotations by default: ": "اعرض الملاحظات في الفيديو تلقائيا: ", - "Automatically extend video description: ": "", - "Interactive 360 degree videos: ": "", + "Automatically extend video description: ": "توسيع وصف الفيديو تلقائيا: ", + "Interactive 360 degree videos: ": "مقاطع فيديو تفاعلية ب درجة 360: ", "Visual preferences": "التفضيلات المرئية", "Player style: ": "شكل مشغل الفيديوهات: ", "Dark mode: ": "الوضع الليلى: ", @@ -90,7 +90,7 @@ "Show annotations by default for subscribed channels: ": "عرض الملاحظات في الفيديوهات تلقائيا في القنوات المشترك بها فقط: ", "Redirect homepage to feed: ": "إعادة التوجية من الصفحة الرئيسية لصفحة المشتركين (لرؤية اخر فيديوهات المشتركين): ", "Number of videos shown in feed: ": "عدد الفيديوهات التى ستظهر فى صفحة المشتركين: ", - "Sort videos by: ": "ترتيب الفيديو بـ: ", + "Sort videos by: ": "ترتيب الفيديو ب: ", "published": "احدث فيديو", "published - reverse": "احدث فيديو - عكسى", "alphabetically": "ترتيب ابجدى", @@ -126,11 +126,11 @@ "Token": "الرمز", "`x` subscriptions": { "([^.,0-9]|^)1([^.,0-9]|$)": "`x` مشتركين", - "": "`x` مشتركين" + "": "`x` مشتركين." }, "`x` tokens": { "([^.,0-9]|^)1([^.,0-9]|$)": "`x` رموز", - "": "`x` رموز" + "": "`x` رموز." }, "Import/export": "إضافة\\إستخراج", "unsubscribe": "إلغاء الإشتراك", @@ -158,8 +158,8 @@ "Title": "العنوان", "Playlist privacy": "إعدادات الخصوصيه", "Editing playlist `x`": "تعديل قائمه التشفيل `x`", - "Show more": "", - "Show less": "", + "Show more": "أظهر المزيد", + "Show less": "عرض اقل", "Watch on YouTube": "مشاهدة الفيديو على اليوتيوب", "Hide annotations": "إخفاء الملاحظات فى الفيديو", "Show annotations": "عرض الملاحظات فى الفيديو", @@ -173,7 +173,7 @@ "Shared `x`": "شارك منذ `x`", "`x` views": { "([^.,0-9]|^)1([^.,0-9]|$)": "`x` مشاهدات", - "": "`x` مشاهدات" + "": "`x` مشاهدات." }, "Premieres in `x`": "يعرض فى `x`", "Premieres `x`": "يعرض `x`", @@ -182,7 +182,7 @@ "View more comments on Reddit": "عرض المزيد من التعليقات على\\من موقع Reddit", "View `x` comments": { "([^.,0-9]|^)1([^.,0-9]|$)": "عرض `x` تعليقات", - "": "عرض `x` تعليقات" + "": "عرض `x` تعليقات." }, "View Reddit comments": "عرض تعليقات ريدإت Reddit", "Hide replies": "إخفاء الردود", @@ -210,13 +210,13 @@ "Could not fetch comments": "لم يتمكن من إحضار التعليقات", "View `x` replies": { "([^.,0-9]|^)1([^.,0-9]|$)": "عرض `x` ردود", - "": "عرض `x` ردود" + "": "عرض `x` ردود." }, "`x` ago": "`x` منذ", "Load more": "عرض المزيد", "`x` points": { "([^.,0-9]|^)1([^.,0-9]|$)": "`x` نقاط", - "": "`x` نقاط" + "": "`x` نقاط." }, "Could not create mix.": "لم يستطع عمل خلط.", "Empty playlist": "قائمة التشغيل فارغة", @@ -337,35 +337,35 @@ "Zulu": "الزولو", "`x` years": { "([^.,0-9]|^)1([^.,0-9]|$)": "`x` سنوات", - "": "`x` سنوات" + "": "`x` سنوات." }, "`x` months": { "([^.,0-9]|^)1([^.,0-9]|$)": "`x` شهور", - "": "`x` شهور" + "": "`x` شهور." }, "`x` weeks": { "([^.,0-9]|^)1([^.,0-9]|$)": "`x` اسابيع", - "": "`x` اسابيع" + "": "`x` اسابيع." }, "`x` days": { "([^.,0-9]|^)1([^.,0-9]|$)": "`x` ايام", - "": "`x` ايام" + "": "`x` ايام." }, "`x` hours": { "([^.,0-9]|^)1([^.,0-9]|$)": "`x` ساعات", - "": "`x` ساعات" + "": "`x` ساعات." }, "`x` minutes": { "([^.,0-9]|^)1([^.,0-9]|$)": "`x` دقائق", - "": "`x` دقائق" + "": "`x` دقائق." }, "`x` seconds": { "([^.,0-9]|^)1([^.,0-9]|$)": "`x` ثوانى", - "": "`x` ثوانى" + "": "`x` ثوانى." }, "Fallback comments: ": "التعليقات البديلة: ", "Popular": "الأكثر شعبية", - "Search": "", + "Search": "بحث", "Top": "الأفضل", "About": "حول", "Rating: ": "التقييم: ", @@ -388,32 +388,32 @@ "Videos": "الفيديوهات", "Playlists": "قوائم التشغيل", "Community": "المجتمع", - "relevance": "", - "rating": "", - "date": "", - "views": "", - "content_type": "", - "duration": "", - "features": "", - "sort": "", - "hour": "", - "today": "", - "week": "", - "month": "", - "year": "", - "video": "", - "channel": "", - "playlist": "", - "movie": "", - "show": "", - "hd": "", - "subtitles": "", - "creative_commons": "", - "3d": "", - "live": "", - "4k": "", - "location": "", - "hdr": "", - "filter": "", + "relevance": "ملاءم", + "rating": "تقييم", + "date": "التاريخ", + "views": "مشاهدات", + "content_type": "نوع المحتوى", + "duration": "المدة الزمنية", + "features": "الميزات", + "sort": "فرز", + "hour": "ساعة", + "today": "اليوم", + "week": "إسبوع", + "month": "شهر", + "year": "سنة", + "video": "فيديو", + "channel": "قناة", + "playlist": "قائمة التشغيل", + "movie": "فيلم", + "show": "عرض", + "hd": "عالية الدقة", + "subtitles": "ترجمات", + "creative_commons": "المشاع الإبداعي", + "3d": "ثلاثي الأبعاد", + "live": "مباشر", + "4k": "4k", + "location": "الاماكن", + "hdr": "وضع التباين العالي", + "filter": "معامل الفرز", "Current version: ": "الإصدار الحالي: " } diff --git a/locales/bn_BD.json b/locales/bn_BD.json index 01a0711f..2d3ace82 100644 --- a/locales/bn_BD.json +++ b/locales/bn_BD.json @@ -1,10 +1,10 @@ { - "`x` subscribers.([^.,0-9]|^)1([^.,0-9]|$)": "`x` সাবস্ক্রাইবার।([^.,0-9]|^)1([^.,0-9]|$)", + "`x` subscribers.([^.,0-9]|^)1([^.,0-9]|$)": "`x` সাবস্ক্রাইবার", "`x` subscribers.": "`x` সাবস্ক্রাইবার।", - "`x` videos.([^.,0-9]|^)1([^.,0-9]|$)": "`x` ভিডিও।([^.,0-9]|^)1([^.,0-9]|$)", - "`x` videos.": "`x` ভিডিও।", - "`x` playlists.([^.,0-9]|^)1([^.,0-9]|$)": "`x` প্লেলিস্ট।[^.,0-9]|^)1([^.,0-9]|$)", - "`x` playlists.": "`x` প্লেলিস্ট।", + "`x` videos.([^.,0-9]|^)1([^.,0-9]|$)": "`x` ভিডিও", + "`x` videos.": "`x` ভিডিও", + "`x` playlists.([^.,0-9]|^)1([^.,0-9]|$)": "`x` প্লেলিস্ট", + "`x` playlists.": "`x` প্লেলিস্ট", "LIVE": "লাইভ", "Shared `x` ago": "`x` আগে শেয়ার করা হয়েছে", "Unsubscribe": "আনসাবস্ক্রাইব", @@ -355,4 +355,4 @@ "Playlists": "", "Community": "", "Current version: ": "" -}
\ No newline at end of file +} diff --git a/locales/cs.json b/locales/cs.json index ab1ee368..4cacf6b9 100644 --- a/locales/cs.json +++ b/locales/cs.json @@ -1,11 +1,11 @@ { "`x` subscribers": { - "([^.,0-9]|^)1([^.,0-9]|$)": "`x` odběratelů.([^.,0-9]|^)1([^.,0-9]|$)", - "": "`x` odběratelů." + "([^.,0-9]|^)1([^.,0-9]|$)": "`x` odběratelů", + "": "`x` odběratelů" }, "`x` videos": { - "([^.,0-9]|^)1([^.,0-9]|$)": "`x` videí.([^.,0-9]|^)1([^.,0-9]|$)", - "": "`x` videí." + "([^.,0-9]|^)1([^.,0-9]|$)": "`x` videí", + "": "`x` videí" }, "`x` playlists": { "([^.,0-9]|^)1([^.,0-9]|$)": "`x` playlist", @@ -125,12 +125,12 @@ "Token manager": "Správa klíčů", "Token": "Klíč", "`x` subscriptions": { - "([^.,0-9]|^)1([^.,0-9]|$)": "`x` Odběry.([^.,0-9]|^)1([^.,0-9]|$)", - "": "`x` Odebíraných kanálů." + "([^.,0-9]|^)1([^.,0-9]|$)": "`x` Odběry", + "": "`x` Odebíraných kanálů" }, "`x` tokens": { "([^.,0-9]|^)1([^.,0-9]|$)": "`x` Klíčů", - "": "`x` klíčů." + "": "`x` klíčů" }, "Import/export": "Importovat/exportovat", "unsubscribe": "odhlásit odběr", @@ -138,7 +138,7 @@ "Subscriptions": "Odběry", "`x` unseen notifications": { "([^.,0-9]|^)1([^.,0-9]|$)": "`x` nezhlédnutých oznámení", - "": "`x` nezhlédnutých oznámení." + "": "`x` nezhlédnutých oznámení" }, "search": "hledat", "Log out": "Odhlásit se", diff --git a/locales/da.json b/locales/da.json index 246fbe4f..cf997bb9 100644 --- a/locales/da.json +++ b/locales/da.json @@ -1,15 +1,15 @@ { "`x` subscribers": { - "([^.,0-9]|^)1([^.,0-9]|$)": "`x` abonnenter.([^.,0-9]|^)1([^.,0-9]|$)", - "": "`x` abonnenter." + "([^.,0-9]|^)1([^.,0-9]|$)": "`x` abonnenter", + "": "`x` abonnenter" }, "`x` videos": { - "([^.,0-9]|^)1([^.,0-9]|$)": "`x` videoer.([^.,0-9]|^)1([^.,0-9]|$)", - "": "`x` videoer." + "([^.,0-9]|^)1([^.,0-9]|$)": "`x` videoer", + "": "`x` videoer" }, "`x` playlists": { - "([^.,0-9]|^)1([^.,0-9]|$)": "`x` afspilningslister.([^.,0-9]|^)1([^.,0-9]|$)", - "": "`x` afspilningslister." + "([^.,0-9]|^)1([^.,0-9]|$)": "`x` afspilningslister", + "": "`x` afspilningslister" }, "LIVE": "DIREKTE", "Shared `x` ago": "Delt for `x` siden", @@ -77,8 +77,8 @@ "Fallback captions: ": "Alternative undertekster: ", "Show related videos: ": "Vis relaterede videoer: ", "Show annotations by default: ": "Vis annotationer som standard: ", - "Automatically extend video description: ": "", - "Interactive 360 degree videos: ": "", + "Automatically extend video description: ": "Automatisk udvid videoens beskrivelse: ", + "Interactive 360 degree videos: ": "Interaktiv 360 graders videoer: ", "Visual preferences": "Visuelle præferencer", "Player style: ": "Afspiller stil: ", "Dark mode: ": "Mørk tilstand: ", @@ -125,20 +125,20 @@ "Token manager": "Tokenmanager", "Token": "Token", "`x` subscriptions": { - "([^.,0-9]|^)1([^.,0-9]|$)": "`x` abonnementer.([^.,0-9]|^)1([^.,0-9]|$)", + "([^.,0-9]|^)1([^.,0-9]|$)": "`x` abonnementer", "": "`x`" }, "`x` tokens": { - "([^.,0-9]|^)1([^.,0-9]|$)": "`x` tokens.([^.,0-9]|^)1([^.,0-9]|$)", - "": "`x` tokens." + "([^.,0-9]|^)1([^.,0-9]|$)": "`x` tokens", + "": "`x` tokens" }, "Import/export": "Importer/eksporter", "unsubscribe": "opsig abonnement", "revoke": "tilbagekald", "Subscriptions": "Abonnementer", "`x` unseen notifications": { - "([^.,0-9]|^)1([^.,0-9]|$)": "`x` usete notifikationer.([^.,0-9]|^)1([^.,0-9]|$)", - "": "`x` usete notifikationer." + "([^.,0-9]|^)1([^.,0-9]|$)": "`x` usete notifikationer", + "": "`x` usete notifikationer" }, "search": "søg", "Log out": "Log ud", @@ -151,15 +151,15 @@ "Unlisted": "Skjult", "Private": "Privat", "View all playlists": "Vis alle afspilningslister", - "Updated `x` ago": "", + "Updated `x` ago": "Opdateret for 'x' siden", "Delete playlist `x`?": "Opdateret `x` siden", "Delete playlist": "Slet afspilningsliste", "Create playlist": "Opret afspilningsliste", "Title": "Titel", "Playlist privacy": "Privatlivsindstillinger for afspilningsliste", "Editing playlist `x`": "Redigerer afspilningsliste `x`", - "Show more": "", - "Show less": "", + "Show more": "Vis mere", + "Show less": "Vis mindre", "Watch on YouTube": "Se på YouTube", "Hide annotations": "Skjul annotationer", "Show annotations": "Vis annotationer", @@ -190,38 +190,38 @@ "Incorrect password": "Forkert adgangskode", "Quota exceeded, try again in a few hours": "Kvota overskredet, prøv igen om et par timer", "Unable to log in, make sure two-factor authentication (Authenticator or SMS) is turned on.": "Login fejlet, tjek at totrinsbekræftelse (Authenticator eller SMS) er slået til.", - "Invalid TFA code": "", - "Login failed. This may be because two-factor authentication is not turned on for your account.": "", + "Invalid TFA code": "Ugyldig TFA kode", + "Login failed. This may be because two-factor authentication is not turned on for your account.": "Login fejlede. Det er måske på grund af to-faktor-autentisering ikk er slået til for din konto.", "Wrong answer": "Forkert svar", "Erroneous CAPTCHA": "Fejlagtig CAPTCHA", - "CAPTCHA is a required field": "", - "User ID is a required field": "", - "Password is a required field": "", + "CAPTCHA is a required field": "CAPTCHA er et krævet felt", + "User ID is a required field": "Bruger ID er et krævet felt", + "Password is a required field": "Adgangskode er et krævet felt", "Wrong username or password": "Forkert brugernavn eller adgangskode", - "Please sign in using 'Log in with Google'": "", - "Password cannot be empty": "", + "Please sign in using 'Log in with Google'": "Venligst tjek in via 'Log in med Google'", + "Password cannot be empty": "Adgangskode kan ikke være tom", "Password cannot be longer than 55 characters": "Adgangskoden må ikke være længere end 55 tegn", - "Please log in": "", + "Please log in": "Venligst log in", "Invidious Private Feed for `x`": "", - "channel:`x`": "", - "Deleted or invalid channel": "", - "This channel does not exist.": "", - "Could not get channel info.": "", - "Could not fetch comments": "", + "channel:`x`": "kanal: 'x'", + "Deleted or invalid channel": "Slettet eller invalid kanal", + "This channel does not exist.": "Denne kanal eksisterer ikke.", + "Could not get channel info.": "Kunne ikke hente kanal info.", + "Could not fetch comments": "Kunne ikke hente kommentarer", "View `x` replies": { - "([^.,0-9]|^)1([^.,0-9]|$)": "", - "": "" + "([^.,0-9]|^)1([^.,0-9]|$)": "Vis `x` besvarelser.([^.,0-9]|^)1([^.,0-9]|$)", + "": "Vis 'x' besvarelser." }, - "`x` ago": "", - "Load more": "", + "`x` ago": "'x' siden", + "Load more": "Hent flere", "`x` points": { - "([^.,0-9]|^)1([^.,0-9]|$)": "", - "": "" + "([^.,0-9]|^)1([^.,0-9]|$)": "`x` point.([^.,0-9]|^)1([^.,0-9]|$)", + "": "'x' point." }, - "Could not create mix.": "", - "Empty playlist": "", - "Not a playlist.": "", - "Playlist does not exist.": "", + "Could not create mix.": "Kunne ikke skabe blanding.", + "Empty playlist": "Tom playliste", + "Not a playlist.": "Ikke en playliste.", + "Playlist does not exist.": "Playlist eksisterer ikke.", "Could not pull trending pages.": "", "Hidden field \"challenge\" is a required field": "", "Hidden field \"token\" is a required field": "", @@ -416,4 +416,4 @@ "hdr": "", "filter": "", "Current version: ": "" -}
\ No newline at end of file +} diff --git a/locales/el.json b/locales/el.json index 26e7fcaa..c55db8ef 100644 --- a/locales/el.json +++ b/locales/el.json @@ -416,4 +416,4 @@ "hdr": "", "filter": "", "Current version: ": "Τρέχουσα έκδοση: " -}
\ No newline at end of file +} diff --git a/locales/eo.json b/locales/eo.json index 27ad34d2..23da15ab 100644 --- a/locales/eo.json +++ b/locales/eo.json @@ -1,15 +1,15 @@ { "`x` subscribers": { - "([^.,0-9]|^)1([^.,0-9]|$)": "`x` abonantoj.([^.,0-9]|^)1([^.,0-9]|$)", - "": "`x` abonantoj." + "([^.,0-9]|^)1([^.,0-9]|$)": "`x` abonantoj", + "": "`x` abonantoj" }, "`x` videos": { - "([^.,0-9]|^)1([^.,0-9]|$)": "`x` filmetoj.([^.,0-9]|^)1([^.,0-9]|$)", - "": "`x` filmetoj." + "([^.,0-9]|^)1([^.,0-9]|$)": "`x` filmetoj", + "": "`x` filmetoj" }, "`x` playlists": { - "([^.,0-9]|^)1([^.,0-9]|$)": "`x` ludlistoj.([^.,0-9]|^)1([^.,0-9]|$)", - "": "`x` ludlistoj." + "([^.,0-9]|^)1([^.,0-9]|$)": "`x` ludlistoj", + "": "`x` ludlistoj" }, "LIVE": "NUNA", "Shared `x` ago": "Konigita antaŭ `x`", @@ -126,11 +126,11 @@ "Token": "Ĵetono", "`x` subscriptions": { "([^.,0-9]|^)1([^.,0-9]|$)": "`x` abonoj", - "": "`x` abonoj." + "": "`x` abonoj" }, "`x` tokens": { "([^.,0-9]|^)1([^.,0-9]|$)": "`x` ĵetonoj", - "": "`x` ĵetonoj." + "": "`x` ĵetonoj" }, "Import/export": "Importi/Eksporti", "unsubscribe": "malabonu", @@ -138,7 +138,7 @@ "Subscriptions": "Abonoj", "`x` unseen notifications": { "([^.,0-9]|^)1([^.,0-9]|$)": "`x` neviditaj sciigoj", - "": "`x` neviditaj sciigoj." + "": "`x` neviditaj sciigoj" }, "search": "serĉi", "Log out": "Elsaluti", diff --git a/locales/es.json b/locales/es.json index 85d42a59..3727cb15 100644 --- a/locales/es.json +++ b/locales/es.json @@ -1,7 +1,7 @@ { "`x` subscribers": { "([^.,0-9]|^)1([^.,0-9]|$)": "`x` suscriptores", - "": "`x` suscriptores." + "": "`x` suscriptores" }, "`x` videos": { "([^.,0-9]|^)1([^.,0-9]|$)": "`x` vídeos", @@ -9,7 +9,7 @@ }, "`x` playlists": { "([^.,0-9]|^)1([^.,0-9]|$)": "`x` listas de reproducción", - "": "`x` listas de reproducción." + "": "`x` listas de reproducción" }, "LIVE": "DIRECTO", "Shared `x` ago": "Compartido hace `x`", @@ -78,7 +78,7 @@ "Show related videos: ": "¿Mostrar vídeos relacionados? ", "Show annotations by default: ": "¿Mostrar anotaciones por defecto? ", "Automatically extend video description: ": "Extender automáticamente la descripción del vídeo: ", - "Interactive 360 degree videos: ": "", + "Interactive 360 degree videos: ": "Vídeos interactivos de 360 grados: ", "Visual preferences": "Preferencias visuales", "Player style: ": "Estilo de reproductor: ", "Dark mode: ": "Modo oscuro: ", @@ -126,11 +126,11 @@ "Token": "Token", "`x` subscriptions": { "([^.,0-9]|^)1([^.,0-9]|$)": "`x` suscripciones", - "": "`x` suscripciones." + "": "`x` suscripciones" }, "`x` tokens": { "([^.,0-9]|^)1([^.,0-9]|$)": "`x` tokens", - "": "`x` tokens." + "": "`x` tokens" }, "Import/export": "Importar/Exportar", "unsubscribe": "Desuscribirse", @@ -138,7 +138,7 @@ "Subscriptions": "Suscripciones", "`x` unseen notifications": { "([^.,0-9]|^)1([^.,0-9]|$)": "`x` notificaciones sin ver", - "": "`x` notificaciones sin ver." + "": "`x` notificaciones sin ver" }, "search": "buscar", "Log out": "Cerrar la sesión", diff --git a/locales/eu.json b/locales/eu.json index 8381f496..ff1c67b7 100644 --- a/locales/eu.json +++ b/locales/eu.json @@ -338,4 +338,4 @@ "Playlists": "", "Community": "", "Current version: ": "" -}
\ No newline at end of file +} diff --git a/locales/fa.json b/locales/fa.json index 5e540b7d..f8c33b8f 100644 --- a/locales/fa.json +++ b/locales/fa.json @@ -416,4 +416,4 @@ "hdr": "", "filter": "", "Current version: ": "نسخه فعلی: " -}
\ No newline at end of file +} diff --git a/locales/fi.json b/locales/fi.json index 62e96639..2092e994 100644 --- a/locales/fi.json +++ b/locales/fi.json @@ -416,4 +416,4 @@ "hdr": "", "filter": "", "Current version: ": "Tämänhetkinen versio: " -}
\ No newline at end of file +} diff --git a/locales/fr.json b/locales/fr.json index 594eda47..e912f913 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -53,8 +53,8 @@ "User ID": "Identifiant utilisateur", "Password": "Mot de passe", "Time (h:mm:ss):": "Heure (h:mm:ss) :", - "Text CAPTCHA": "CAPTCHA Texte", - "Image CAPTCHA": "CAPTCHA Image", + "Text CAPTCHA": "CAPTCHA textuel", + "Image CAPTCHA": "CAPTCHA graphique", "Sign In": "Se connecter", "Register": "S'inscrire", "E-mail": "E-mail", @@ -64,7 +64,7 @@ "Always loop: ": "Lire en boucle : ", "Autoplay: ": "Lancer la lecture automatiquement : ", "Play next by default: ": "Lire les vidéos suivantes par défaut : ", - "Autoplay next video: ": "Lancer la lecture automatiquement pour la vidéo suivant la vidéo regardée : ", + "Autoplay next video: ": "Lire automatiquement la vidéo suivante : ", "Listen by default: ": "Audio uniquement : ", "Proxy videos: ": "Charger les vidéos à travers un proxy : ", "Default speed: ": "Vitesse par défaut : ", @@ -86,15 +86,15 @@ "dark": "sombre", "light": "clair", "Thin mode: ": "Mode léger : ", - "Subscription preferences": "Préférences de la page d'abonnements", + "Subscription preferences": "Préférences des abonnements", "Show annotations by default for subscribed channels: ": "Afficher les annotations par défaut sur les chaînes auxquelles vous êtes abonnés : ", "Redirect homepage to feed: ": "Rediriger la page d'accueil vers la page d'abonnements : ", "Number of videos shown in feed: ": "Nombre de vidéos affichées dans la page d'abonnements : ", "Sort videos by: ": "Trier les vidéos par : ", "published": "date de publication", "published - reverse": "date de publication - inversé", - "alphabetically": "alphabétiquement", - "alphabetically - reverse": "alphabétiquement - inversé", + "alphabetically": "ordre alphabétique", + "alphabetically - reverse": "ordre alphabétique - inversé", "channel name": "nom de la chaîne", "channel name - reverse": "nom de la chaîne - inversé", "Only show latest video from channel: ": "Afficher uniquement la dernière vidéo des chaînes auxquelles vous êtes abonnés : ", @@ -117,9 +117,9 @@ "Feed menu: ": "Préferences des abonnements : ", "Top enabled: ": "Top activé : ", "CAPTCHA enabled: ": "CAPTCHA activé : ", - "Login enabled: ": "Connexion activée : ", - "Registration enabled: ": "Inscription activée : ", - "Report statistics: ": "Télémétrie activé : ", + "Login enabled: ": "Autoriser l'ouverture de sessions utilisateur : ", + "Registration enabled: ": "Autoriser la création de comptes utilisateur : ", + "Report statistics: ": "Activer les statistiques d'instance : ", "Save preferences": "Enregistrer les préférences", "Subscription manager": "Gestionnaire d'abonnement", "Token manager": "Gestionnaire de token", diff --git a/locales/he.json b/locales/he.json index 129a451a..d924e11b 100644 --- a/locales/he.json +++ b/locales/he.json @@ -1,15 +1,15 @@ { "`x` subscribers": { - "([^.,0-9]|^)1([^.,0-9]|$)": "`x` רשומים.([^.,0-9]|^)1([^.,0-9]|$)", - "": "`x` רשומים." + "([^.,0-9]|^)1([^.,0-9]|$)": "`x` רשומים", + "": "`x` רשומים" }, "`x` videos": { - "([^.,0-9]|^)1([^.,0-9]|$)": "`x` סרטונים.([^.,0-9]|^)1([^.,0-9]|$)", - "": "`x` סרטונים." + "([^.,0-9]|^)1([^.,0-9]|$)": "`x` סרטונים", + "": "`x` סרטונים" }, "`x` playlists": { - "([^.,0-9]|^)1([^.,0-9]|$)": "`x` פלייליסטים.([^.,0-9]|^)1([^.,0-9]|$)", - "": "`x` פלייליסטים." + "([^.,0-9]|^)1([^.,0-9]|$)": "`x` פלייליסטים", + "": "`x` פלייליסטים" }, "LIVE": "שידור חי", "Shared `x` ago": "שותף לפני `x`", @@ -125,8 +125,8 @@ "Token manager": "Token manager", "Token": "Token", "`x` subscriptions": { - "([^.,0-9]|^)1([^.,0-9]|$)": "`x` מינויים.([^.,0-9]|^)1([^.,0-9]|$)", - "": "`x` מינויים." + "([^.,0-9]|^)1([^.,0-9]|$)": "`x` מינויים", + "": "`x` מינויים" }, "`x` tokens": { "([^.,0-9]|^)1([^.,0-9]|$)": "", @@ -137,8 +137,8 @@ "revoke": "", "Subscriptions": "מינויים", "`x` unseen notifications": { - "([^.,0-9]|^)1([^.,0-9]|$)": "`x` הודעות שלא נראו.([^.,0-9]|^)1([^.,0-9]|$)", - "": "`x` הודעות שלא נראו." + "([^.,0-9]|^)1([^.,0-9]|$)": "`x` הודעות שלא נראו", + "": "`x` הודעות שלא נראו" }, "search": "חיפוש", "Log out": "יציאה", @@ -416,4 +416,4 @@ "hdr": "HDR", "filter": "סינון", "Current version: ": "הגרסה הנוכחית: " -}
\ No newline at end of file +} diff --git a/locales/hr.json b/locales/hr.json index 744c4d0e..bebd3859 100644 --- a/locales/hr.json +++ b/locales/hr.json @@ -1,15 +1,15 @@ { "`x` subscribers": { - "([^.,0-9]|^)1([^.,0-9]|$)": "`x` pretplatnika.([^.,0-9]|^)1([^.,0-9]|$)", - "": "`x` pretplatnika." + "([^.,0-9]|^)1([^.,0-9]|$)": "`x` pretplatnika", + "": "`x` pretplatnika" }, "`x` videos": { - "([^.,0-9]|^)1([^.,0-9]|$)": "`x` videa.([^.,0-9]|^)1([^.,0-9]|$)", - "": "`x` videa." + "([^.,0-9]|^)1([^.,0-9]|$)": "`x` videa", + "": "`x` videa" }, "`x` playlists": { - "([^.,0-9]|^)1([^.,0-9]|$)": "`x` playliste.([^.,0-9]|^)1([^.,0-9]|$)", - "": "`x` playliste." + "([^.,0-9]|^)1([^.,0-9]|$)": "`x` playliste", + "": "`x` playliste" }, "LIVE": "UŽIVO", "Shared `x` ago": "Dijeljeno prije `x`", @@ -125,20 +125,20 @@ "Token manager": "Upravljanje tokenima", "Token": "Token", "`x` subscriptions": { - "([^.,0-9]|^)1([^.,0-9]|$)": "`x` pretplate.([^.,0-9]|^)1([^.,0-9]|$)", - "": "`x` pretplate." + "([^.,0-9]|^)1([^.,0-9]|$)": "`x` pretplate", + "": "`x` pretplate" }, "`x` tokens": { - "([^.,0-9]|^)1([^.,0-9]|$)": "`x` tokena.([^.,0-9]|^)1([^.,0-9]|$)", - "": "`x` tokena." + "([^.,0-9]|^)1([^.,0-9]|$)": "`x` tokena", + "": "`x` tokena" }, "Import/export": "Uvezi/izvezi", "unsubscribe": "odjavi pretplatu", "revoke": "opozovi", "Subscriptions": "Pretplate", "`x` unseen notifications": { - "([^.,0-9]|^)1([^.,0-9]|$)": "`x` neviđene obavijesti.([^.,0-9]|^)1([^.,0-9]|$)", - "": "`x` neviđene obavijesti." + "([^.,0-9]|^)1([^.,0-9]|$)": "`x` neviđene obavijesti", + "": "`x` neviđene obavijesti" }, "search": "traži", "Log out": "Odjavi se", diff --git a/locales/hu-HU.json b/locales/hu-HU.json index 29817cc2..859fdef2 100644 --- a/locales/hu-HU.json +++ b/locales/hu-HU.json @@ -6,8 +6,8 @@ "Shared `x` ago": "`x` óta megosztva", "Unsubscribe": "Leiratkozás", "Subscribe": "Feliratkozás", - "View channel on YouTube": "Csatokrna megtekintése a YouTube-on", - "View playlist on YouTube": "Playlist megtekintése a YouTube-on", + "View channel on YouTube": "csatorna megtekintése a YouTube-on", + "View playlist on YouTube": "lejátszási lista megtekintése a YouTube-on", "newest": "legújabb", "oldest": "legrégibb", "popular": "népszerű", @@ -17,7 +17,7 @@ "Clear watch history?": "Megtekintési napló törlése?", "New password": "Új jelszó", "New passwords must match": "Az új jelszavaknak egyezniük kell", - "Cannot change password for Google accounts": "Google fiók jelszavát nem lehet cserélni", + "Cannot change password for Google accounts": "Google fiók jelszavát nem lehet megváltoztatni", "Authorize token?": "Token felhatalmazása?", "Authorize token for `x`?": "Token felhatalmazása `x`-ra?", "Yes": "Igen", @@ -57,37 +57,37 @@ "Play next by default: ": "Következő lejátszása alapértelmezésben: ", "Autoplay next video: ": "Következő automatikus lejátszása: ", "Listen by default: ": "Hallgatás alapértelmezésben: ", - "Proxy videos: ": "Proxy videók: ", + "Proxy videos: ": "Videók proxyzása: ", "Default speed: ": "Alapértelmezett sebesség: ", "Preferred video quality: ": "Kívánt video minőség: ", "Player volume: ": "Hangerő: ", "Default comments: ": "Alapértelmezett kommentek: ", - "youtube": "YouTube", - "reddit": "Reddit", + "youtube": "youtube", + "reddit": "reddit", "Default captions: ": "Alapértelmezett feliratok: ", "Fallback captions: ": "Másodlagos feliratok: ", - "Show related videos: ": "Kapcsolódó videók mutatása: ", - "Show annotations by default: ": "Annotációk mutatása alapértelmetésben: ", - "Automatically extend video description: ": "", - "Interactive 360 degree videos: ": "", - "Visual preferences": "Vizuális preferenciák", + "Show related videos: ": "Hasonló videók mutatása: ", + "Show annotations by default: ": "Szövegmagyarázatok mutatása alapértelmezésben: ", + "Automatically extend video description: ": "Automatikusan növelje meg a videó leírását", + "Interactive 360 degree videos: ": "Interaktív 360° videók", + "Visual preferences": "Kinézeti beállítások", "Player style: ": "Lejátszó stílusa: ", "Dark mode: ": "Sötét mód: ", "Theme: ": "Téma: ", - "dark": "Sötét", - "light": "Világos", + "dark": "sötét", + "light": "világos", "Thin mode: ": "Vékony mód: ", "Subscription preferences": "Feliratkozási beállítások", - "Show annotations by default for subscribed channels: ": "Annotációk mutatása alapértelmezésben feliratkozott csatornák esetében: ", + "Show annotations by default for subscribed channels: ": "Szövegmagyarázatok mutatása alapértelmezésben feliratkozott csatornák esetében: ", "Redirect homepage to feed: ": "Kezdő oldal átirányitása a feed-re: ", "Number of videos shown in feed: ": "Feed-ben mutatott videók száma: ", "Sort videos by: ": "Videók sorrendje: ", "published": "közzétéve", - "published - reverse": "közzétéve (ford.)", + "published - reverse": "közzétéve - fordítva", "alphabetically": "ABC sorrend", - "alphabetically - reverse": "ABC sorrend (ford.)", + "alphabetically - reverse": "ABC sorrend - fordítva", "channel name": "csatorna neve", - "channel name - reverse": "csatorna neve (ford.)", + "channel name - reverse": "csatorna neve - fordítva", "Only show latest video from channel: ": "Csak a legutolsó videó mutatása a csatornából: ", "Only show latest unwatched video from channel: ": "Csak a legutolsó nem megtekintett videó mutatása a csatornából: ", "Only show unwatched: ": "Csak a nem megtekintettek mutatása: ", @@ -104,7 +104,7 @@ "Watch history": "Megtekintési napló", "Delete account": "Fiók törlése", "Administrator preferences": "Adminisztrátor beállítások", - "Default homepage: ": "Alapértelmezett honlap: ", + "Default homepage: ": "Alapértelmezett oldal: ", "Feed menu: ": "Feed menü: ", "Top enabled: ": "Top lista engedélyezve: ", "CAPTCHA enabled: ": "CAPTCHA engedélyezve: ", @@ -125,48 +125,48 @@ "search": "keresés", "Log out": "Kijelentkezés", "Released under the AGPLv3 by Omar Roth.": "Omar Roth által kiadva AGPLv3 licensz alatt.", - "Source available here.": "Forrás elérhető itt.", + "Source available here.": "A forráskód itt érhető el.", "View JavaScript license information.": "JavaScript licensz inforkációk megtekintése.", "View privacy policy.": "Adatvédelmi irányelvek megtekintése.", "Trending": "Felkapott", "Public": "Nyilvános", "Unlisted": "Nem nyilvános", "Private": "Privát", - "View all playlists": "Minden playlist megtekintése", - "Updated `x` ago": "Frissitve `x`", + "View all playlists": "Minden lejátszási lista megtekintése", + "Updated `x` ago": "Frissitve: `x`", "Delete playlist `x`?": "`x` playlist törlése?", "Delete playlist": "Lejátszási lista törlése", "Create playlist": "Lejátszási lista létrehozása", - "Title": "Címe", + "Title": "Cím", "Playlist privacy": "Lejátszási lista láthatósága", "Editing playlist `x`": "`x` lista szerkesztése", - "Show more": "", - "Show less": "", + "Show more": "Mutass többet", + "Show less": "Mutass kevesebbet", "Watch on YouTube": "Megtekintés a YouTube-on", - "Hide annotations": "Annotációk elrejtése", - "Show annotations": "Annotációk mutatása", + "Hide annotations": "Szövegmagyarázat elrejtése", + "Show annotations": "Szövegmagyarázat mutatása", "Genre: ": "Műfaj: ", "License: ": "Licensz: ", "Family friendly? ": "Családbarát? ", - "Wilson score: ": "Wilson-ponstszám: ", - "Engagement: ": "Engagement: ", + "Wilson score: ": "Wilson-pontszám: ", + "Engagement: ": "elkötelezettség: ", "Whitelisted regions: ": "Engedélyezett régiók: ", "Blacklisted regions: ": "Tiltott régiók: ", "Shared `x`": "Megosztva `x`", "`x` views": "`x` megtekintés", - "Premieres in `x`": "Premier `x`", - "Premieres `x`": "Premier `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.", + "Premieres in `x`": "premierel `x` múlva", + "Premieres `x`": "`x`-t premierel", + "Hi! Looks like you have JavaScript turned off. Click here to view comments, keep in mind they may take a bit longer to load.": "Úgy látszik, hogy a JavaScript ki van kapcsolva a böngésződben. Kattints ide hogy megtekintsd a kommenteket, de tudd, hogy így kicsit tovább tarthat a betöltés.", "View YouTube comments": "YouTube kommentek megtekintése", - "View more comments on Reddit": "További Reddit kommentek megtekintése", + "View more comments on Reddit": "További kommentek megtekintése Redditen", "View `x` comments": "`x` komment megtekintése", "View Reddit comments": "Reddit kommentek megtekintése", "Hide replies": "Válaszok elrejtése", "Show replies": "Válaszok mutatása", "Incorrect password": "Helytelen jelszó", "Quota exceeded, try again in a few hours": "Kvóta túllépve, próbálkozz pár órával később", - "Unable to log in, make sure two-factor authentication (Authenticator or SMS) is turned on.": "Sikertelen belépés, győződj meg róla hogy a 2FA (Authenticator vagy SMS) engedélyezve van.", - "Login failed. This may be because two-factor authentication is not turned on for your account.": "Sikertelen belépés, győződj meg róla hogy a 2FA (Authenticator vagy SMS) engedélyezve van.", + "Unable to log in, make sure two-factor authentication (Authenticator or SMS) is turned on.": "Sikertelen bejelentkezés. Győződj meg róla, hogy a kétfaktoros hitelesítés (hitelesítő vagy SMS) engedélyezve van.", + "Login failed. This may be because two-factor authentication is not turned on for your account.": "Sikertelen bejelentkezés. Győződj meg róla, hogy a kétfaktoros hitelesítés engedélyezve van.", "Wrong answer": "Rossz válasz", "Erroneous CAPTCHA": "Hibás CAPTCHA", "CAPTCHA is a required field": "A CAPTCHA kötelező", @@ -175,23 +175,23 @@ "Wrong username or password": "Rossz felhasználónév vagy jelszó", "Please sign in using 'Log in with Google'": "Kérem, jelentkezzen be a \"Bejelentkezés Google-el\"", "Password cannot be empty": "A jelszó nem lehet üres", - "Password cannot be longer than 55 characters": "A jelszó nem lehet hosszabb 55 betűnél", + "Password cannot be longer than 55 characters": "A jelszó nem lehet hosszabb 55 karakternél", "Please log in": "Kérem lépjen be", "Invidious Private Feed for `x`": "`x` Invidious privát feed-je", "channel:`x`": "`x` csatorna", "Deleted or invalid channel": "Törölt vagy nemlétező csatorna", "This channel does not exist.": "Ez a csatorna nem létezik.", - "Could not get channel info.": "Nem megszerezhető a csatorna információ.", - "Could not fetch comments": "Nem megszerezhetőek a kommentek", + "Could not get channel info.": "Nem sikerült lekérni a csatorna adatokat.", + "Could not fetch comments": "Nem sikerült lekérni a kommenteket", "View `x` replies": "`x` válasz megtekintése", "`x` ago": "`x` óta", "Load more": "További betöltése", "`x` points": "`x` pont", "Could not create mix.": "Nem tudok mix-et készíteni.", - "Empty playlist": "Üres playlist", - "Not a playlist.": "Nem playlist.", - "Playlist does not exist.": "Nem létező playlist.", - "Could not pull trending pages.": "Nem tudom letölteni a trendek adatait.", + "Empty playlist": "Üres lejátszási lista", + "Not a playlist.": "Nem lejátszási lista.", + "Playlist does not exist.": "Nincs ilyen lejátszási lista.", + "Could not pull trending pages.": "Nem sikerült lekérni a felkapott oldalt.", "Hidden field \"challenge\" is a required field": "A rejtett \"challenge\" mező kötelező", "Hidden field \"token\" is a required field": "A rejtett \"token\" mező kötelező", "Erroneous challenge": "Hibás challenge", @@ -200,110 +200,110 @@ "Token is expired, please try again": "Lejárt token, kérem próbáld újra", "English": "angol", "English (auto-generated)": "angol (automatikusan generált)", - "Afrikaans": "", - "Albanian": "", - "Amharic": "", - "Arabic": "", - "Armenian": "", - "Azerbaijani": "", - "Bangla": "", - "Basque": "", - "Belarusian": "", - "Bosnian": "", - "Bulgarian": "", - "Burmese": "", - "Catalan": "", - "Cebuano": "", - "Chinese (Simplified)": "", - "Chinese (Traditional)": "", - "Corsican": "", - "Croatian": "", - "Czech": "", - "Danish": "", - "Dutch": "", - "Esperanto": "", - "Estonian": "", - "Filipino": "", - "Finnish": "", - "French": "", - "Galician": "", - "Georgian": "", - "German": "", - "Greek": "", - "Gujarati": "", - "Haitian Creole": "", - "Hausa": "", - "Hawaiian": "", - "Hebrew": "", - "Hindi": "", - "Hmong": "", - "Hungarian": "", - "Icelandic": "", - "Igbo": "", - "Indonesian": "", - "Irish": "", - "Italian": "", - "Japanese": "", - "Javanese": "", - "Kannada": "", - "Kazakh": "", - "Khmer": "", - "Korean": "", - "Kurdish": "", - "Kyrgyz": "", - "Lao": "", - "Latin": "", - "Latvian": "", - "Lithuanian": "", - "Luxembourgish": "", - "Macedonian": "", - "Malagasy": "", - "Malay": "", - "Malayalam": "", - "Maltese": "", - "Maori": "", - "Marathi": "", - "Mongolian": "", - "Nepali": "", - "Norwegian Bokmål": "", - "Nyanja": "", - "Pashto": "", - "Persian": "", - "Polish": "", - "Portuguese": "", - "Punjabi": "", - "Romanian": "", - "Russian": "", - "Samoan": "", - "Scottish Gaelic": "", - "Serbian": "", - "Shona": "", - "Sindhi": "", - "Sinhala": "", - "Slovak": "", - "Slovenian": "", - "Somali": "", - "Southern Sotho": "", - "Spanish": "", - "Spanish (Latin America)": "", - "Sundanese": "", - "Swahili": "", - "Swedish": "", - "Tajik": "", - "Tamil": "", - "Telugu": "", - "Thai": "", - "Turkish": "", - "Ukrainian": "", + "Afrikaans": "afrikaans", + "Albanian": "albán", + "Amharic": "amhara", + "Arabic": "arab", + "Armenian": "örmény", + "Azerbaijani": "azerbajdzsáni", + "Bangla": "bengáli", + "Basque": "baszk", + "Belarusian": "fehérorosz", + "Bosnian": "bosnyák", + "Bulgarian": "bolgár", + "Burmese": "burmai", + "Catalan": "katalán", + "Cebuano": "szebuano", + "Chinese (Simplified)": "kínai (egyszerűsített)", + "Chinese (Traditional)": "kínai (hagyományos)", + "Corsican": "korzikai", + "Croatian": "horvát", + "Czech": "cseh", + "Danish": "dán", + "Dutch": "holland", + "Esperanto": "eszperantó", + "Estonian": "észt", + "Filipino": "filippínó", + "Finnish": "finn", + "French": "francia", + "Galician": "galíciai", + "Georgian": "grúz", + "German": "német", + "Greek": "görök", + "Gujarati": "gudzsaráti", + "Haitian Creole": "haiti kreol", + "Hausa": "hausza", + "Hawaiian": "hawaii", + "Hebrew": "héber", + "Hindi": "hindi", + "Hmong": "hmong", + "Hungarian": "magyar", + "Icelandic": "izlandi", + "Igbo": "igbo", + "Indonesian": "indonéziai", + "Irish": "ír", + "Italian": "olasz", + "Japanese": "japán", + "Javanese": "jávai", + "Kannada": "kannada", + "Kazakh": "kazah", + "Khmer": "khmer", + "Korean": "koreai", + "Kurdish": "kurd", + "Kyrgyz": "kirgiz", + "Lao": "lao", + "Latin": "latin", + "Latvian": "lett", + "Lithuanian": "litván", + "Luxembourgish": "luxemburgi", + "Macedonian": "macedóniai", + "Malagasy": "madagaszkári", + "Malay": "maláj", + "Malayalam": "malajálam", + "Maltese": "máltai", + "Maori": "maori", + "Marathi": "Maráthi", + "Mongolian": "mongol", + "Nepali": "nepáli", + "Norwegian Bokmål": "bokmål", + "Nyanja": "nyánja", + "Pashto": "pastu", + "Persian": "perzsa", + "Polish": "lengyel", + "Portuguese": "portugál", + "Punjabi": "pandzsábi", + "Romanian": "román", + "Russian": "orosz", + "Samoan": "szamoai", + "Scottish Gaelic": "skót gael", + "Serbian": "szerb", + "Shona": "shona", + "Sindhi": "szindhi", + "Sinhala": "szingaléz", + "Slovak": "szlovák", + "Slovenian": "szlovén", + "Somali": "szomáliai", + "Southern Sotho": "déli szothó", + "Spanish": "spanyol", + "Spanish (Latin America)": "spanyol (Latin-Amerika)", + "Sundanese": "szunda", + "Swahili": "szuahéli", + "Swedish": "svld", + "Tajik": "tadzsik", + "Tamil": "tamil", + "Telugu": "telugu", + "Thai": "thai", + "Turkish": "török", + "Ukrainian": "ukrán", "Urdu": "", - "Uzbek": "", - "Vietnamese": "", - "Welsh": "", - "Western Frisian": "", + "Uzbek": "üzbég", + "Vietnamese": "vietnámi", + "Welsh": "walesi", + "Western Frisian": "nyugati fríz", "Xhosa": "", - "Yiddish": "", - "Yoruba": "", - "Zulu": "", + "Yiddish": "jiddis", + "Yoruba": "joruba", + "Zulu": "zulu", "`x` years": "`x` év", "`x` months": "`x` hónap", "`x` weeks": "`x` hét", @@ -318,7 +318,7 @@ "About": "Leírás", "Rating: ": "Besorolás: ", "Language: ": "Nyelv: ", - "View as playlist": "Megtekintés playlist-ként", + "View as playlist": "Megtekintés lejátszási listaként", "Default": "Alapértelmezett", "Music": "Zene", "Gaming": "Játékok", @@ -331,10 +331,10 @@ "YouTube comment permalink": "YouTube komment permalink", "permalink": "permalink", "`x` marked it with a ❤": "`x` jelölte ❤-vel", - "Audio mode": "Audio mód", - "Video mode": "Video mód", + "Audio mode": "Audió mód", + "Video mode": "Hang mód", "Videos": "Videók", - "Playlists": "Playlistek", + "Playlists": "Lejátszási listák", "Community": "Közösség", "Current version: ": "Jelenlegi verzió: " } diff --git a/locales/id.json b/locales/id.json index ee1dea1a..ce98f87b 100644 --- a/locales/id.json +++ b/locales/id.json @@ -1,14 +1,14 @@ { "`x` subscribers": { - "([^.,0-9]|^)1([^.,0-9]|$)": "`x` pelanggan.([^.,0-9]|^)1([^.,0-9]|$)", + "([^.,0-9]|^)1([^.,0-9]|$)": "`x` pelanggan", "": "`x` pelanggan." }, "`x` videos": { - "([^.,0-9]|^)1([^.,0-9]|$)": "`x` video.([^.,0-9]|^)1([^.,0-9]|$)", + "([^.,0-9]|^)1([^.,0-9]|$)": "`x` video", "": "`x` video." }, "`x` playlists": { - "([^.,0-9]|^)1([^.,0-9]|$)": "`x` daftar putar.([^.,0-9]|^)1([^.,0-9]|$)", + "([^.,0-9]|^)1([^.,0-9]|$)": "`x` daftar putar", "": "`x` daftar putar." }, "LIVE": "SIARAN LANGSUNG", @@ -77,8 +77,8 @@ "Fallback captions: ": "Subtitel fallback: ", "Show related videos: ": "Tampilkan video terkait: ", "Show annotations by default: ": "Tampilkan anotasi secara default: ", - "Automatically extend video description: ": "", - "Interactive 360 degree videos: ": "", + "Automatically extend video description: ": "Perluas deskripsi video secara otomatis: ", + "Interactive 360 degree videos: ": "Video interaktif 360°: ", "Visual preferences": "Preferensi visual", "Player style: ": "Gaya pemutar: ", "Dark mode: ": "Mode gelap: ", @@ -125,11 +125,11 @@ "Token manager": "Pengatur token", "Token": "Token", "`x` subscriptions": { - "([^.,0-9]|^)1([^.,0-9]|$)": "`x` langganan.([^.,0-9]|^)1([^.,0-9]|$)", + "([^.,0-9]|^)1([^.,0-9]|$)": "`x` langganan", "": "`x` langganan." }, "`x` tokens": { - "([^.,0-9]|^)1([^.,0-9]|$)": "`x` token.([^.,0-9]|^)1([^.,0-9]|$)", + "([^.,0-9]|^)1([^.,0-9]|$)": "`x` token", "": "`x` token." }, "Import/export": "Impor/ekspor", @@ -137,7 +137,7 @@ "revoke": "cabut", "Subscriptions": "Langganan", "`x` unseen notifications": { - "([^.,0-9]|^)1([^.,0-9]|$)": "`x` pemberitahuan belum dilihat.([^.,0-9]|^)1([^.,0-9]|$)", + "([^.,0-9]|^)1([^.,0-9]|$)": "`x` pemberitahuan belum dilihat", "": "`x` pemberitahuan belum dilihat." }, "search": "cari", @@ -158,8 +158,8 @@ "Title": "Judul", "Playlist privacy": "Privasi daftar putar", "Editing playlist `x`": "Menyunting daftar putar `x`", - "Show more": "", - "Show less": "", + "Show more": "Tampilkan lainnya", + "Show less": "Tampilkan lebih sedikit", "Watch on YouTube": "Tonton di YouTube", "Hide annotations": "Sembunyikan anotasi", "Show annotations": "Tampilkan anotasi", @@ -363,7 +363,7 @@ "([^.,0-9]|^)1([^.,0-9]|$)": "`x` detik.([^.,0-9]|^)1([^.,0-9]|$)", "": "`x` detik." }, - "Fallback comments: ": "", + "Fallback comments: ": "Komentar mundur: ", "Popular": "Populer", "Search": "Cari", "Top": "Teratas", @@ -373,7 +373,7 @@ "View as playlist": "Tampilkan sebagai daftar putar", "Default": "Asali", "Music": "Musik", - "Gaming": "Gaming", + "Gaming": "Permainan", "News": "Berita", "Movies": "Film", "Download": "Unduh", @@ -416,4 +416,4 @@ "hdr": "hdr", "filter": "saring", "Current version: ": "Versi saat ini: " -}
\ No newline at end of file +} diff --git a/locales/is.json b/locales/is.json index 00d50ad1..a847080a 100644 --- a/locales/is.json +++ b/locales/is.json @@ -416,4 +416,4 @@ "hdr": "", "filter": "", "Current version: ": "Núverandi útgáfa: " -}
\ No newline at end of file +} diff --git a/locales/nb-NO.json b/locales/nb-NO.json index 5e57c43b..cb95c2dc 100644 --- a/locales/nb-NO.json +++ b/locales/nb-NO.json @@ -1,7 +1,7 @@ { "`x` subscribers": { "([^.,0-9]|^)1([^.,0-9]|$)": "`x` abonnenter", - "": "`x` abonnenter." + "": "`x` abonnenter" }, "`x` videos": { "([^.,0-9]|^)1([^.,0-9]|$)": "`x` videoer", @@ -126,11 +126,11 @@ "Token": "Symbol", "`x` subscriptions": { "([^.,0-9]|^)1([^.,0-9]|$)": "`x` abonnementer", - "": "`x` abonnementer." + "": "`x` abonnementer" }, "`x` tokens": { "([^.,0-9]|^)1([^.,0-9]|$)": "`x` symboler", - "": "`x` symboler." + "": "`x` symboler" }, "Import/export": "Importer/eksporter", "unsubscribe": "opphev abonnement", @@ -138,7 +138,7 @@ "Subscriptions": "Abonnement", "`x` unseen notifications": { "([^.,0-9]|^)1([^.,0-9]|$)": "`x` usette merknader", - "": "`x` usette merknader." + "": "`x` usette merknader" }, "search": "søk", "Log out": "Logg ut", diff --git a/locales/nl.json b/locales/nl.json index d62a43f0..c9da1875 100644 --- a/locales/nl.json +++ b/locales/nl.json @@ -1,15 +1,15 @@ { "`x` subscribers": { "([^.,0-9]|^)1([^.,0-9]|$)": "`x` abonnees", - "": "`x` abonnees" + "": "`x` abonnees." }, "`x` videos": { "([^.,0-9]|^)1([^.,0-9]|$)": "`x` video's", - "": "`x` video's" + "": "`x` video's." }, "`x` playlists": { "([^.,0-9]|^)1([^.,0-9]|$)": "`x` afspeellijsten", - "": "`x` afspeellijsten" + "": "`x` afspeellijsten." }, "LIVE": "LIVE", "Shared `x` ago": "Gedeeld: `x` geleden", @@ -77,8 +77,8 @@ "Fallback captions: ": "Alternatieve ondertiteling: ", "Show related videos: ": "Gerelateerde video's tonen? ", "Show annotations by default: ": "Standaard annotaties tonen? ", - "Automatically extend video description: ": "", - "Interactive 360 degree videos: ": "", + "Automatically extend video description: ": "Breid videobeschrijving automatisch uit: ", + "Interactive 360 degree videos: ": "Interactieve 360-graden-video's ", "Visual preferences": "Visuele instellingen", "Player style: ": "Speler vormgeving ", "Dark mode: ": "Donkere modus: ", @@ -126,11 +126,11 @@ "Token": "Toegangssleutel", "`x` subscriptions": { "([^.,0-9]|^)1([^.,0-9]|$)": "`x` abonnementen", - "": "`x` abonnementen" + "": "`x` abonnementen." }, "`x` tokens": { "([^.,0-9]|^)1([^.,0-9]|$)": "`x` toegangssleutels", - "": "`x` toegangssleutels" + "": "`x` toegangssleutels." }, "Import/export": "Importeren/Exporteren", "unsubscribe": "Deabonneren", @@ -158,8 +158,8 @@ "Title": "Titel", "Playlist privacy": "Afspeellijst privacy", "Editing playlist `x`": "Afspeellijst `x` wijzigen", - "Show more": "", - "Show less": "", + "Show more": "Toon meer", + "Show less": "Toon minder", "Watch on YouTube": "Video bekijken op YouTube", "Hide annotations": "Annotaties verbergen", "Show annotations": "Annotaties tonen", @@ -173,7 +173,7 @@ "Shared `x`": "`x` gedeeld", "`x` views": { "([^.,0-9]|^)1([^.,0-9]|$)": "`x` weergaven", - "": "`x` weergaven" + "": "`x` weergaven." }, "Premieres in `x`": "Verschijnt over `x`", "Premieres `x`": "Verschijnt op `x`", @@ -182,7 +182,7 @@ "View more comments on Reddit": "Meer reacties bekijken op Reddit", "View `x` comments": { "([^.,0-9]|^)1([^.,0-9]|$)": "`x` reacties tonen", - "": "`x` reacties tonen" + "": "`x` reacties tonen." }, "View Reddit comments": "Reddit-reacties tonen", "Hide replies": "Antwoorden verbergen", @@ -210,13 +210,13 @@ "Could not fetch comments": "Kan reacties niet ophalen", "View `x` replies": { "([^.,0-9]|^)1([^.,0-9]|$)": "`x` antwoorden tonen", - "": "`x` antwoorden tonen" + "": "`x` antwoorden tonen." }, "`x` ago": "`x` geleden", "Load more": "Meer laden", "`x` points": { "([^.,0-9]|^)1([^.,0-9]|$)": "`x` punten", - "": "`x` punten" + "": "`x` punten." }, "Could not create mix.": "Kan geen mix maken.", "Empty playlist": "Lege afspeellijst", @@ -337,35 +337,35 @@ "Zulu": "Zulu", "`x` years": { "([^.,0-9]|^)1([^.,0-9]|$)": "`x` jaar", - "": "`x` jaar" + "": "`x` jaren." }, "`x` months": { "([^.,0-9]|^)1([^.,0-9]|$)": "`x` maanden", - "": "`x` maanden" + "": "`x` maanden." }, "`x` weeks": { "([^.,0-9]|^)1([^.,0-9]|$)": "`x` weken", - "": "`x` weken" + "": "`x` weken." }, "`x` days": { "([^.,0-9]|^)1([^.,0-9]|$)": "`x` dagen", - "": "`x` dagen" + "": "`x` dagen." }, "`x` hours": { "([^.,0-9]|^)1([^.,0-9]|$)": "`x` uur", - "": "`x` uur" + "": "`x` uren." }, "`x` minutes": { "([^.,0-9]|^)1([^.,0-9]|$)": "`x` minuten", - "": "`x` minuten" + "": "`x` minuten." }, "`x` seconds": { "([^.,0-9]|^)1([^.,0-9]|$)": "`x` seconden", - "": "`x` seconden" + "": "`x` seconden." }, "Fallback comments: ": "Terugvallen op ", "Popular": "Populair", - "Search": "", + "Search": "Zoeken", "Top": "Top", "About": "Over", "Rating: ": "Waardering: ", @@ -388,32 +388,32 @@ "Videos": "Video's", "Playlists": "Afspeellijsten", "Community": "Gemeenschap", - "relevance": "", - "rating": "", - "date": "", - "views": "", - "content_type": "", - "duration": "", - "features": "", - "sort": "", - "hour": "", - "today": "", - "week": "", - "month": "", - "year": "", - "video": "", - "channel": "", - "playlist": "", - "movie": "", - "show": "", - "hd": "", - "subtitles": "", - "creative_commons": "", - "3d": "", - "live": "", - "4k": "", - "location": "", - "hdr": "", - "filter": "", + "relevance": "relevantie", + "rating": "beoordeling", + "date": "datum", + "views": "keren bekeken", + "content_type": "Type inhoud", + "duration": "duur", + "features": "eigenschappen", + "sort": "sorteren", + "hour": "uur", + "today": "vandaag", + "week": "week", + "month": "maand", + "year": "jaar", + "video": "video", + "channel": "kanaal", + "playlist": "afspeellijst", + "movie": "film", + "show": "show", + "hd": "HD", + "subtitles": "ondertitels", + "creative_commons": "Creative Commons", + "3d": "3D", + "live": "Live", + "4k": "4K", + "location": "locatie", + "hdr": "HDR", + "filter": "verfijnen", "Current version: ": "Huidige versie: " } diff --git a/locales/pl.json b/locales/pl.json index 30291a9e..f31293d3 100644 --- a/locales/pl.json +++ b/locales/pl.json @@ -77,8 +77,8 @@ "Fallback captions: ": "Zastępcze napisy: ", "Show related videos: ": "Pokaż powiązane filmy? ", "Show annotations by default: ": "Domyślnie pokazuj adnotacje: ", - "Automatically extend video description: ": "", - "Interactive 360 degree videos: ": "", + "Automatically extend video description: ": "Automatycznie rozwijaj opisy filmów: ", + "Interactive 360 degree videos: ": "Interaktywne filmy 360 stopni: ", "Visual preferences": "Preferencje Wizualne", "Player style: ": "Styl odtwarzacza: ", "Dark mode: ": "Ciemny motyw: ", @@ -114,7 +114,7 @@ "Delete account": "Usuń konto", "Administrator preferences": "Preferencje administratora", "Default homepage: ": "Domyślna strona główna: ", - "Feed menu: ": "", + "Feed menu: ": "Menu aktualności: ", "Top enabled: ": "\"Top\" aktywne: ", "CAPTCHA enabled: ": "CAPTCHA aktywna? ", "Login enabled: ": "Logowanie włączone? ", @@ -158,8 +158,8 @@ "Title": "Tytuł", "Playlist privacy": "Widoczność playlisty", "Editing playlist `x`": "Edycja playlisty `x`", - "Show more": "", - "Show less": "", + "Show more": "Pokaż więcej", + "Show less": "Pokaż mniej", "Watch on YouTube": "Zobacz film na YouTube", "Hide annotations": "Ukryj adnotacje", "Show annotations": "Pokaż adnotacje", @@ -202,7 +202,7 @@ "Password cannot be empty": "Hasło nie może być puste", "Password cannot be longer than 55 characters": "Hasło nie może być dłuższe niż 55 znaków", "Please log in": "Proszę się zalogować", - "Invidious Private Feed for `x`": "", + "Invidious Private Feed for `x`": "Prywatne aktualności dla `x`", "channel:`x`": "kanał:`x", "Deleted or invalid channel": "Usunięty lub niepoprawny kanał", "This channel does not exist.": "Ten kanał nie istnieje.", @@ -365,7 +365,7 @@ }, "Fallback comments: ": "Zastępcze komentarze: ", "Popular": "Popularne", - "Search": "", + "Search": "Szukaj", "Top": "Top", "About": "Informacje", "Rating: ": "Ocena: ", @@ -390,30 +390,30 @@ "Community": "Społeczność", "relevance": "", "rating": "", - "date": "", + "date": "data", "views": "", "content_type": "", "duration": "", "features": "", - "sort": "", - "hour": "", - "today": "", - "week": "", - "month": "", - "year": "", + "sort": "sortuj", + "hour": "godzina", + "today": "dzisiaj", + "week": "tydzień", + "month": "miesiąc", + "year": "rok", "video": "", - "channel": "", - "playlist": "", - "movie": "", - "show": "", - "hd": "", - "subtitles": "", - "creative_commons": "", - "3d": "", + "channel": "kanał", + "playlist": "playlista", + "movie": "film", + "show": "pokaż", + "hd": "hd", + "subtitles": "napisy", + "creative_commons": "creative_commons", + "3d": "3d", "live": "", - "4k": "", + "4k": "4k", "location": "", - "hdr": "", - "filter": "", + "hdr": "hdr", + "filter": "filtr", "Current version: ": "Aktualna wersja: " -}
\ No newline at end of file +} diff --git a/locales/pt-BR.json b/locales/pt-BR.json index 4ab87c7a..04971d6c 100644 --- a/locales/pt-BR.json +++ b/locales/pt-BR.json @@ -77,8 +77,8 @@ "Fallback captions: ": "Legendas alternativas: ", "Show related videos: ": "Mostrar vídeos relacionados: ", "Show annotations by default: ": "Sempre mostrar anotações: ", - "Automatically extend video description: ": "", - "Interactive 360 degree videos: ": "", + "Automatically extend video description: ": "Estenda automaticamente a descrição do vídeo: ", + "Interactive 360 degree videos: ": "Vídeos interativos de 360 graus: ", "Visual preferences": "Preferências visuais", "Player style: ": "Estilo do tocador: ", "Dark mode: ": "Modo escuro: ", @@ -392,28 +392,28 @@ "rating": "avaliação", "date": "data", "views": "visualizações", - "content_type": "", + "content_type": "content_type", "duration": "duração", - "features": "", - "sort": "", - "hour": "", - "today": "", - "week": "", - "month": "", - "year": "", - "video": "", - "channel": "", - "playlist": "", - "movie": "", - "show": "", - "hd": "", - "subtitles": "", - "creative_commons": "", - "3d": "", - "live": "", - "4k": "", - "location": "", - "hdr": "", - "filter": "", + "features": "recursos", + "sort": "ordenar", + "hour": "hora", + "today": "hoje", + "week": "semana", + "month": "mês", + "year": "ano", + "video": "vídeo", + "channel": "Canal", + "playlist": "playlist", + "movie": "filme", + "show": "show", + "hd": "hd", + "subtitles": "legendas", + "creative_commons": "creative_commons", + "3d": "3d", + "live": "ao vivo", + "4k": "4k", + "location": "localização", + "hdr": "hdr", + "filter": "filtro", "Current version: ": "Versão atual: " } diff --git a/locales/pt-PT.json b/locales/pt-PT.json index a8569f18..43ffc7d8 100644 --- a/locales/pt-PT.json +++ b/locales/pt-PT.json @@ -416,4 +416,4 @@ "hdr": "", "filter": "", "Current version: ": "Versão atual: " -}
\ No newline at end of file +} diff --git a/locales/si.json b/locales/si.json index a9889672..57ed22a3 100644 --- a/locales/si.json +++ b/locales/si.json @@ -416,4 +416,4 @@ "hdr": "", "filter": "", "Current version: ": "" -}
\ No newline at end of file +} diff --git a/locales/sk.json b/locales/sk.json index 0cf23fa0..30495e9a 100644 --- a/locales/sk.json +++ b/locales/sk.json @@ -1,6 +1,6 @@ { "`x` subscribers.([^.,0-9]|^)1([^.,0-9]|$)": "", - "`x` subscribers.": "`x` odberateľov.", + "`x` subscribers.": "`x` odberateľov", "`x` videos.([^.,0-9]|^)1([^.,0-9]|$)": "", "`x` videos.": "", "`x` playlists.([^.,0-9]|^)1([^.,0-9]|$)": "", @@ -355,4 +355,4 @@ "Playlists": "", "Community": "", "Current version: ": "" -}
\ No newline at end of file +} diff --git a/locales/sr.json b/locales/sr.json index a9ec697b..76f9fab6 100644 --- a/locales/sr.json +++ b/locales/sr.json @@ -1,15 +1,15 @@ { "`x` subscribers": { - "([^.,0-9]|^)1([^.,0-9]|$)": "`x` пратилаца.([^.,0-9]|^)1([^.,0-9]|$)", - "": "`x` пратилаца." + "([^.,0-9]|^)1([^.,0-9]|$)": "`x` пратилаца", + "": "`x` пратилаца" }, "`x` videos": { - "([^.,0-9]|^)1([^.,0-9]|$)": "`x` видео записа.([^.,0-9]|^)1([^.,0-9]|$)", - "": "`x` видео записа." + "([^.,0-9]|^)1([^.,0-9]|$)": "`x` видео записа", + "": "`x` видео записа" }, "`x` playlists": { - "([^.,0-9]|^)1([^.,0-9]|$)": "`x` списака извођења.([^.,0-9]|^)1([^.,0-9]|$)", - "": "`x` списака извођења." + "([^.,0-9]|^)1([^.,0-9]|$)": "`x` списака извођења", + "": "`x` списака извођења" }, "LIVE": "УЖИВО", "Shared `x` ago": "Подељено пре `x`", @@ -416,4 +416,4 @@ "hdr": "", "filter": "", "Current version: ": "" -}
\ No newline at end of file +} diff --git a/locales/sr_Cyrl.json b/locales/sr_Cyrl.json index fb1dc349..6c72c9ed 100644 --- a/locales/sr_Cyrl.json +++ b/locales/sr_Cyrl.json @@ -1,7 +1,7 @@ { - "`x` subscribers.": "%(count)s пратилац.", - "`x` videos.": "`x` видеа.", - "`x` playlists.": "`x` плејлиста/е.", + "`x` subscribers.": "`x` пратилац", + "`x` videos.": "`x` видеа", + "`x` playlists.": "`x` плејлиста/е", "LIVE": "УЖИВО", "Shared `x` ago": "Објављено пре `x`", "Unsubscribe": "Прекините праћење", @@ -115,13 +115,13 @@ "Subscription manager": "Управљање праћењима", "Token manager": "Управљање токенима", "Token": "Токен", - "`x` subscriptions.": "`x`праћења.", - "`x` tokens.": "`x`токена.", + "`x` subscriptions.": "`x`праћења", + "`x` tokens.": "`x`токена", "Import/export": "Увези/извези", "unsubscribe": "укини праћење", "revoke": "опозови", "Subscriptions": "Праћења", - "`x` unseen notifications.": "`x` непрочитаних обавештења.", + "`x` unseen notifications.": "`x` непрочитаних обавештења", "search": "претрага", "Log out": "Одјавите се", "Released under the AGPLv3 by Omar Roth.": "Издао Омар Рот (Omar Roth) под условима AGPLv3 лиценце.", @@ -338,4 +338,4 @@ "Playlists": "", "Community": "", "Current version: ": "Тренутна верзија: " -}
\ No newline at end of file +} diff --git a/locales/tr.json b/locales/tr.json index 8b69a6d0..e16c1217 100644 --- a/locales/tr.json +++ b/locales/tr.json @@ -1,15 +1,15 @@ { "`x` subscribers": { - "([^.,0-9]|^)1([^.,0-9]|$)": "`x` abone.([^.,0-9]|^)1([^.,0-9]|$)", - "": "`x` abone." + "([^.,0-9]|^)1([^.,0-9]|$)": "`x` abone", + "": "`x` abone" }, "`x` videos": { - "([^.,0-9]|^)1([^.,0-9]|$)": "`x` video.([^.,0-9]|^)1([^.,0-9]|$)", - "": "`x` video." + "([^.,0-9]|^)1([^.,0-9]|$)": "`x` video", + "": "`x` video" }, "`x` playlists": { - "([^.,0-9]|^)1([^.,0-9]|$)": "`x` oynatma listesi.([^.,0-9]|^)1([^.,0-9]|$)", - "": "`x` oynatma listesi." + "([^.,0-9]|^)1([^.,0-9]|$)": "`x` oynatma listesi", + "": "`x` oynatma listesi" }, "LIVE": "CANLI", "Shared `x` ago": "`x` önce paylaşıldı", @@ -125,20 +125,20 @@ "Token manager": "Belirteç yöneticisi", "Token": "Belirteç", "`x` subscriptions": { - "([^.,0-9]|^)1([^.,0-9]|$)": "`x` abonelik.([^.,0-9]|^)1([^.,0-9]|$)", - "": "`x` abonelik." + "([^.,0-9]|^)1([^.,0-9]|$)": "`x` abonelik", + "": "`x` abonelik" }, "`x` tokens": { - "([^.,0-9]|^)1([^.,0-9]|$)": "`x` belirteç.([^.,0-9]|^)1([^.,0-9]|$)", - "": "`x` belirteç." + "([^.,0-9]|^)1([^.,0-9]|$)": "`x` belirteç", + "": "`x` belirteç" }, "Import/export": "İçe/dışa aktar", "unsubscribe": "abonelikten çık", "revoke": "geri al", "Subscriptions": "Abonelikler", "`x` unseen notifications": { - "([^.,0-9]|^)1([^.,0-9]|$)": "`x` okunmamış bildirim.([^.,0-9]|^)1([^.,0-9]|$)", - "": "`x` okunmamış bildirim." + "([^.,0-9]|^)1([^.,0-9]|$)": "`x` okunmamış bildirim", + "": "`x` okunmamış bildirim" }, "search": "ara", "Log out": "Çıkış yap", diff --git a/locales/zh-CN.json b/locales/zh-CN.json index b413d116..050e3071 100644 --- a/locales/zh-CN.json +++ b/locales/zh-CN.json @@ -1,11 +1,11 @@ { "`x` subscribers": { "([^.,0-9]|^)1([^.,0-9]|$)": "`x` 位订阅者", - "": "`x` 位订阅者" + "": "`x` 位订阅者." }, "`x` videos": { "([^.,0-9]|^)1([^.,0-9]|$)": "`x` 个视频", - "": "`x` 个视频" + "": "`x` 个视频." }, "`x` playlists": { "([^.,0-9]|^)1([^.,0-9]|$)": "`x` 个播放列表", @@ -20,7 +20,7 @@ "newest": "最新", "oldest": "最老", "popular": "时下流行", - "last": "", + "last": "持续", "Next page": "下一页", "Previous page": "上一页", "Clear watch history?": "清除观看历史?", @@ -77,8 +77,8 @@ "Fallback captions: ": "后备字幕语言: ", "Show related videos: ": "是否显示相关视频: ", "Show annotations by default: ": "是否默认显示视频注释: ", - "Automatically extend video description: ": "", - "Interactive 360 degree videos: ": "", + "Automatically extend video description: ": "自动展开视频描述: ", + "Interactive 360 degree videos: ": "互动式 360 度视频: ", "Visual preferences": "视觉选项", "Player style: ": "播放器样式: ", "Dark mode: ": "深色模式: ", @@ -126,7 +126,7 @@ "Token": "令牌", "`x` subscriptions": { "([^.,0-9]|^)1([^.,0-9]|$)": "`x` 个订阅", - "": "`x` 个订阅" + "": "`x` 个订阅." }, "`x` tokens": { "([^.,0-9]|^)1([^.,0-9]|$)": "`x` 个令牌", @@ -158,8 +158,8 @@ "Title": "标题", "Playlist privacy": "播放列表隐私设置", "Editing playlist `x`": "正在编辑播放列表 `x`", - "Show more": "", - "Show less": "", + "Show more": "显示更多", + "Show less": "显示较少", "Watch on YouTube": "在 YouTube 观看", "Hide annotations": "隐藏注释", "Show annotations": "显示注释", @@ -173,7 +173,7 @@ "Shared `x`": "`x`发布", "`x` views": { "([^.,0-9]|^)1([^.,0-9]|$)": "`x` 播放", - "": "`x` 播放" + "": "`x` 次观看." }, "Premieres in `x`": "首映于 `x` 后", "Premieres `x`": "首映于 `x`", @@ -182,7 +182,7 @@ "View more comments on Reddit": "在 Reddit 查看更多评论", "View `x` comments": { "([^.,0-9]|^)1([^.,0-9]|$)": "查看 `x` 条评论", - "": "查看 `x` 条评论" + "": "查看 `x` 条评论." }, "View Reddit comments": "查看 Reddit 评论", "Hide replies": "隐藏回复", @@ -210,13 +210,13 @@ "Could not fetch comments": "无法获取评论", "View `x` replies": { "([^.,0-9]|^)1([^.,0-9]|$)": "查看 `x` 条回复", - "": "查看 `x` 条回复" + "": "查看 `x` 条回复." }, "`x` ago": "`x` 前", "Load more": "加载更多", "`x` points": { "([^.,0-9]|^)1([^.,0-9]|$)": "`x` 分", - "": "`x` 分" + "": "`x` 分." }, "Could not create mix.": "无法创建合集。", "Empty playlist": "空播放列表", @@ -337,35 +337,35 @@ "Zulu": "祖鲁语", "`x` years": { "([^.,0-9]|^)1([^.,0-9]|$)": "`x` 年", - "": "`x` 年" + "": "`x` 年." }, "`x` months": { "([^.,0-9]|^)1([^.,0-9]|$)": "`x` 月", - "": "`x` 月" + "": "`x` 个月." }, "`x` weeks": { "([^.,0-9]|^)1([^.,0-9]|$)": "`x` 周", - "": "`x` 周" + "": "`x` 周." }, "`x` days": { "([^.,0-9]|^)1([^.,0-9]|$)": "`x` 天", - "": "`x` 天" + "": "`x` 天." }, "`x` hours": { "([^.,0-9]|^)1([^.,0-9]|$)": "`x` 小时", - "": "`x` 小时" + "": "`x` 小时." }, "`x` minutes": { "([^.,0-9]|^)1([^.,0-9]|$)": "`x` 分钟", - "": "`x` 分钟" + "": "`x` 分钟." }, "`x` seconds": { "([^.,0-9]|^)1([^.,0-9]|$)": "`x` 秒", - "": "`x` 秒" + "": "`x` 秒." }, "Fallback comments: ": "后备评论: ", "Popular": "热门频道", - "Search": "", + "Search": "搜索", "Top": "热门视频", "About": "关于", "Rating: ": "评分: ", @@ -388,32 +388,32 @@ "Videos": "视频", "Playlists": "播放列表", "Community": "社区", - "relevance": "", - "rating": "", - "date": "", - "views": "", - "content_type": "", - "duration": "", - "features": "", - "sort": "", - "hour": "", - "today": "", - "week": "", - "month": "", - "year": "", - "video": "", - "channel": "", - "playlist": "", - "movie": "", - "show": "", - "hd": "", - "subtitles": "", - "creative_commons": "", - "3d": "", - "live": "", - "4k": "", - "location": "", - "hdr": "", - "filter": "", + "relevance": "相关度", + "rating": "评分", + "date": "日期", + "views": "观看次数", + "content_type": "content_type", + "duration": "持续时间", + "features": "功能", + "sort": "排序", + "hour": "小时", + "today": "今日", + "week": "周", + "month": "月", + "year": "年份", + "video": "视频", + "channel": "频道", + "playlist": "播放列表", + "movie": "电影", + "show": "真人秀", + "hd": "高清", + "subtitles": "字幕", + "creative_commons": "creative_commons 许可", + "3d": "3d", + "live": "直播", + "4k": "4k", + "location": "位置", + "hdr": "hdr", + "filter": "过滤器", "Current version: ": "当前版本: " -}
\ No newline at end of file +} diff --git a/locales/zh-TW.json b/locales/zh-TW.json index 085ffa30..1affeb3f 100644 --- a/locales/zh-TW.json +++ b/locales/zh-TW.json @@ -9,7 +9,7 @@ }, "`x` playlists": { "([^.,0-9]|^)1([^.,0-9]|$)": "`x` 播放清單", - "": "`x` 播放清單。" + "": "`x` 播放清單" }, "LIVE": "直播", "Shared `x` ago": "`x` 前分享", diff --git a/scripts/git/pre-commit b/scripts/git/pre-commit new file mode 100644 index 00000000..e4a27750 --- /dev/null +++ b/scripts/git/pre-commit @@ -0,0 +1,23 @@ +# Useful precomit hooks +# Please see https://git-scm.com/book/en/v2/Customizing-Git-Git-Hooks for instructions on installation. + +# Crystal linter +# This is a modified version of the pre-commit hook from the crystal repo. https://github.com/crystal-lang/crystal/blob/master/scripts/git/pre-commit +# Please refer to that if you'd like an version that doesn't automatically format staged files. +changed_cr_files=$(git diff --cached --name-only --diff-filter=ACM | grep '\.cr$') +if [ ! -z "$changed_cr_files" ]; then + if [ -x bin/crystal ]; then + # use bin/crystal wrapper when available to run local compiler build + bin/crystal tool format $changed_cr_files >&2 + else + crystal tool format $changed_cr_files >&2 + fi + + git add $changed_cr_files +fi + +# Locale equalizer +if [ ! -z $(git diff --name-only --cached -- locales/) ]; then + crystal run scripts/propagate-new-locale-keys.cr + git add locales > /dev/null +fi
\ No newline at end of file diff --git a/scripts/propagate-new-locale-keys.cr b/scripts/propagate-new-locale-keys.cr new file mode 100644 index 00000000..570b408a --- /dev/null +++ b/scripts/propagate-new-locale-keys.cr @@ -0,0 +1,95 @@ +require "json" +require "../src/invidious/helpers/i18n.cr" + +def locale_to_array(locale_name) + arrayifed_locale_data = [] of Tuple(String, JSON::Any | String) + keys_only_array = [] of String + LOCALES[locale_name].each do |k, v| + if v.as_h? + arrayifed_locale_data << {k, JSON.parse(v.as_h.to_json)} + elsif v.as_s? + arrayifed_locale_data << {k, v.as_s} + end + + keys_only_array << k + end + + return arrayifed_locale_data, keys_only_array +end + +# Invidious currently has some unloaded localization files. We shouldn't need to propagate new keys onto those. +# We'll also remove the reference locale (english) from the list to process. +loaded_locales = LOCALES.keys.select! { |key| key != "en-US" } +english_locale, english_locale_keys = locale_to_array("en-US") + +# In order to automatically propagate locale keys we're going to be needing two arrays. +# One is an array containing each locale data encoded as tuples. The other would contain +# sets of only the keys of each locale files. +# +# The second array is to make sure that an key from the english reference file is present +# in whatever the current locale we're scanning is. +locale_list = [] of Array(Tuple(String, JSON::Any | String)) +locale_list_with_only_keys = [] of Array(String) + +# Populates the created arrays from above +loaded_locales.each do |name| + arrayifed_locale_data, keys_only_locale = locale_to_array(name) + + locale_list << arrayifed_locale_data + locale_list_with_only_keys << keys_only_locale +end + +# Propagate additions +locale_list_with_only_keys.dup.each_with_index do |keys_of_locale_in_processing, index_of_locale_in_processing| + insert_at = {} of Int32 => Tuple(String, JSON::Any | String) + + LOCALES["en-US"].each_with_index do |ref_locale_data, ref_locale_key_index| + ref_locale_key, ref_locale_value = ref_locale_data + + # Found an new key that isn't present in the current locale.. + if !keys_of_locale_in_processing.includes? ref_locale_key + # In terms of structure there's currently only two types; one for plural and the other for singular translations. + if ref_locale_value.as_h? + insert_at[ref_locale_key_index] = {ref_locale_key, JSON.parse({"([^.,0-9]|^)1([^.,0-9]|$)" => "", "" => ""}.to_json)} + else + insert_at[ref_locale_key_index] = {ref_locale_key, ""} + end + end + end + + insert_at.each do |location_to_insert, data| + locale_list_with_only_keys[index_of_locale_in_processing].insert(location_to_insert, data[0]) + locale_list[index_of_locale_in_processing].insert(location_to_insert, data) + end +end + +# Propagate removals +locale_list_with_only_keys.dup.each_with_index do |keys_of_locale_in_processing, index_of_locale_in_processing| + remove_at = [] of Int32 + + keys_of_locale_in_processing.each_with_index do |current_key, current_key_index| + if !english_locale_keys.includes? current_key + remove_at << current_key_index + end + end + + remove_at.each do |index_to_remove_at| + locale_list_with_only_keys[index_of_locale_in_processing].delete_at(index_to_remove_at) + locale_list[index_of_locale_in_processing].delete_at(index_to_remove_at) + end +end + +# Now we convert back to our original format. +final_locale_list = [] of String +locale_list.each do |locale| + intermediate_hash = {} of String => (JSON::Any | String) + locale.each { |k, v| intermediate_hash[k] = v } + final_locale_list << intermediate_hash.to_pretty_json(indent = " ") +end + +locale_map = Hash.zip(loaded_locales, final_locale_list) + +# Export +locale_map.each do |locale_name, locale_contents| + File.write("locales/#{locale_name}.json", "#{locale_contents}\n") +end @@ -2,39 +2,35 @@ version: 2.0 shards: db: git: https://github.com/crystal-lang/crystal-db.git - version: 0.10.0 + version: 0.10.1 exception_page: git: https://github.com/crystal-loot/exception_page.git - version: 0.1.4 + version: 0.1.5 kemal: git: https://github.com/kemalcr/kemal.git - version: 0.27.0 + version: 1.0.0 kilt: git: https://github.com/jeromegn/kilt.git - version: 0.4.0 + version: 0.4.1 lsquic: git: https://github.com/iv-org/lsquic.cr.git - version: 2.18.1-1 + version: 2.18.1-2 pg: git: https://github.com/will/crystal-pg.git - version: 0.23.1 - - pool: - git: https://github.com/ysbaddaden/pool.git - version: 0.2.3 + version: 0.23.2 protodec: git: https://github.com/iv-org/protodec.git - version: 0.1.3 + version: 0.1.4 radix: git: https://github.com/luislavena/radix.git - version: 0.3.9 + version: 0.4.1 sqlite3: git: https://github.com/crystal-lang/crystal-sqlite3.git @@ -12,23 +12,20 @@ targets: dependencies: pg: github: will/crystal-pg - version: ~> 0.23.1 + version: ~> 0.23.2 sqlite3: github: crystal-lang/crystal-sqlite3 version: ~> 0.18.0 kemal: github: kemalcr/kemal - version: ~> 0.27.0 - pool: - github: ysbaddaden/pool - version: ~> 0.2.3 + version: ~> 1.0.0 protodec: github: iv-org/protodec - version: ~> 0.1.3 + version: ~> 0.1.4 lsquic: github: iv-org/lsquic.cr - version: ~> 2.18.1-1 + version: ~> 2.18.1-2 -crystal: 0.36.1 +crystal: 1.0.0 license: AGPLv3 diff --git a/src/invidious.cr b/src/invidious.cr index ae20e13e..7037ecfe 100644 --- a/src/invidious.cr +++ b/src/invidious.cr @@ -166,7 +166,7 @@ end before_all do |env| preferences = begin - Preferences.from_json(env.request.cookies["PREFS"]?.try &.value || "{}") + Preferences.from_json(URI.decode_www_form(env.request.cookies["PREFS"]?.try &.value || "{}")) rescue Preferences.from_json("{}") end @@ -174,15 +174,44 @@ before_all do |env| env.set "preferences", preferences env.response.headers["X-XSS-Protection"] = "1; mode=block" env.response.headers["X-Content-Type-Options"] = "nosniff" - extra_media_csp = "" + + # Allow media resources to be loaded from google servers + # TODO: check if *.youtube.com can be removed if CONFIG.disabled?("local") || !preferences.local - extra_media_csp += " https://*.googlevideo.com:443" - extra_media_csp += " https://*.youtube.com:443" + extra_media_csp = " https://*.googlevideo.com:443 https://*.youtube.com:443" + else + extra_media_csp = "" end - # TODO: Remove style-src's 'unsafe-inline', requires to remove all inline styles (<style> [..] </style>, style=" [..] ") - env.response.headers["Content-Security-Policy"] = "default-src 'none'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self' data:; connect-src 'self'; manifest-src 'self'; media-src 'self' blob:#{extra_media_csp}; child-src blob:" + + # Only allow the pages at /embed/* to be embedded + if env.request.resource.starts_with?("/embed") + frame_ancestors = "'self' http: https:" + else + frame_ancestors = "none" + end + + # TODO: Remove style-src's 'unsafe-inline', requires to remove all + # inline styles (<style> [..] </style>, style=" [..] ") + env.response.headers["Content-Security-Policy"] = { + "default-src 'none'", + "script-src 'self'", + "style-src 'self' 'unsafe-inline'", + "img-src 'self' data:", + "font-src 'self' data:", + "connect-src 'self'", + "manifest-src 'self'", + "media-src 'self' blob:" + extra_media_csp, + "child-src 'self' blob:", + "frame-src 'self'", + "frame-ancestors " + frame_ancestors, + }.join("; ") + env.response.headers["Referrer-Policy"] = "same-origin" + # Ask the chrom*-based browsers to disable FLoC + # See: https://blog.runcloud.io/google-floc/ + env.response.headers["Permissions-Policy"] = "interest-cohort=()" + if (Kemal.config.ssl || CONFIG.https_only) && CONFIG.hsts env.response.headers["Strict-Transport-Security"] = "max-age=31536000; includeSubDomains; preload" end @@ -421,7 +450,7 @@ get "/modify_notifications" do |env| html = YT_POOL.client &.get("/subscription_manager?disable_polymer=1", headers) - cookies = HTTP::Cookies.from_headers(headers) + cookies = HTTP::Cookies.from_client_headers(headers) html.cookies.each do |cookie| if {"VISITOR_INFO1_LIVE", "YSC", "SIDCC"}.includes? cookie.name if cookies[cookie.name]? diff --git a/src/invidious/channels.cr b/src/invidious/channels.cr index 3109b508..bbef3d4f 100644 --- a/src/invidious/channels.cr +++ b/src/invidious/channels.cr @@ -229,22 +229,8 @@ def fetch_channel(ucid, db, pull_all_videos = true, locale = nil) page = 1 LOGGER.trace("fetch_channel: #{ucid} : Downloading channel videos page") - response_body = get_channel_videos_response(ucid, page, auto_generated: auto_generated) - - videos = [] of SearchVideo - begin - initial_data = JSON.parse(response_body) - raise InfoException.new("Could not extract channel JSON") if !initial_data - - LOGGER.trace("fetch_channel: #{ucid} : Extracting videos from channel videos page initial_data") - videos = extract_videos(initial_data.as_h, author, ucid) - rescue ex - if response_body.includes?("To continue with your YouTube experience, please fill out the form below.") || - response_body.includes?("https://www.google.com/sorry/index") - raise InfoException.new("Could not extract channel info. Instance is likely blocked.") - end - raise ex - end + initial_data = get_channel_videos_response(ucid, page, auto_generated: auto_generated) + videos = extract_videos(initial_data, author, ucid) LOGGER.trace("fetch_channel: #{ucid} : Extracting videos from channel RSS feed") rss.xpath_nodes("//feed/entry").each do |entry| @@ -304,10 +290,8 @@ def fetch_channel(ucid, db, pull_all_videos = true, locale = nil) ids = [] of String loop do - response_body = get_channel_videos_response(ucid, page, auto_generated: auto_generated) - initial_data = JSON.parse(response_body) - raise InfoException.new("Could not extract channel JSON") if !initial_data - videos = extract_videos(initial_data.as_h, author, ucid) + initial_data = get_channel_videos_response(ucid, page, auto_generated: auto_generated) + videos = extract_videos(initial_data, author, ucid) count = videos.size videos = videos.map { |video| ChannelVideo.new({ @@ -358,8 +342,7 @@ end def fetch_channel_playlists(ucid, author, continuation, sort_by) if continuation response_json = request_youtube_api_browse(continuation) - result = JSON.parse(response_json) - continuationItems = result["onResponseReceivedActions"]? + continuationItems = response_json["onResponseReceivedActions"]? .try &.[0]["appendContinuationItemsAction"]["continuationItems"] return [] of SearchItem, nil if !continuationItems @@ -964,21 +947,16 @@ def get_60_videos(ucid, author, page, auto_generated, sort_by = "newest") videos = [] of SearchVideo 2.times do |i| - response_json = get_channel_videos_response(ucid, page * 2 + (i - 1), auto_generated: auto_generated, sort_by: sort_by) - initial_data = JSON.parse(response_json) - break if !initial_data - videos.concat extract_videos(initial_data.as_h, author, ucid) + initial_data = get_channel_videos_response(ucid, page * 2 + (i - 1), auto_generated: auto_generated, sort_by: sort_by) + videos.concat extract_videos(initial_data, author, ucid) end return videos.size, videos end def get_latest_videos(ucid) - response_json = get_channel_videos_response(ucid) - initial_data = JSON.parse(response_json) - return [] of SearchVideo if !initial_data + initial_data = get_channel_videos_response(ucid) author = initial_data["metadata"]?.try &.["channelMetadataRenderer"]?.try &.["title"]?.try &.as_s - items = extract_videos(initial_data.as_h, author, ucid) - return items + return extract_videos(initial_data, author, ucid) end diff --git a/src/invidious/helpers/i18n.cr b/src/invidious/helpers/i18n.cr index 45a3f1ae..dd46feab 100644 --- a/src/invidious/helpers/i18n.cr +++ b/src/invidious/helpers/i18n.cr @@ -21,7 +21,7 @@ LOCALES = { "pt-PT" => load_locale("pt-PT"), "ro" => load_locale("ro"), "ru" => load_locale("ru"), - "sv" => load_locale("sv-SE"), + "sv-SE" => load_locale("sv-SE"), "tr" => load_locale("tr"), "uk" => load_locale("uk"), "zh-CN" => load_locale("zh-CN"), diff --git a/src/invidious/helpers/utils.cr b/src/invidious/helpers/utils.cr index 6ce457b9..66ad6961 100644 --- a/src/invidious/helpers/utils.cr +++ b/src/invidious/helpers/utils.cr @@ -1,5 +1,5 @@ require "lsquic" -require "pool/connection" +require "db" def add_yt_headers(request) request.headers["user-agent"] ||= "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.97 Safari/537.36" @@ -20,7 +20,7 @@ struct YoutubeConnectionPool property! url : URI property! capacity : Int32 property! timeout : Float64 - property pool : ConnectionPool(QUIC::Client | HTTP::Client) + property pool : DB::Pool(QUIC::Client | HTTP::Client) def initialize(url : URI, @capacity = 5, @timeout = 5.0, use_quic = true) @url = url @@ -43,7 +43,7 @@ struct YoutubeConnectionPool conn.before_request { |r| add_yt_headers(r) } if url.host == "www.youtube.com" response = yield conn ensure - pool.checkin(conn) + pool.release(conn) end end @@ -51,7 +51,7 @@ struct YoutubeConnectionPool end private def build_pool(use_quic) - ConnectionPool(QUIC::Client | HTTP::Client).new(capacity: capacity, timeout: timeout) do + DB::Pool(QUIC::Client | HTTP::Client).new(initial_pool_size: 0, max_pool_size: capacity, max_idle_pool_size: capacity, checkout_timeout: timeout) do if use_quic conn = QUIC::Client.new(url) else diff --git a/src/invidious/helpers/youtube_api.cr b/src/invidious/helpers/youtube_api.cr index 30413532..e27d4088 100644 --- a/src/invidious/helpers/youtube_api.cr +++ b/src/invidious/helpers/youtube_api.cr @@ -4,28 +4,116 @@ # Hard-coded constants required by the API HARDCODED_API_KEY = "AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8" -HARDCODED_CLIENT_VERS = "2.20210318.08.00" +HARDCODED_CLIENT_VERS = "2.20210330.08.00" -def request_youtube_api_browse(continuation) +#################################################################### +# make_youtube_api_context(region) +# +# Return, as a Hash, the "context" data required to request the +# youtube API endpoints. +# +def make_youtube_api_context(region : String | Nil) : Hash + return { + "client" => { + "hl" => "en", + "gl" => region || "US", # Can't be empty! + "clientName" => "WEB", + "clientVersion" => HARDCODED_CLIENT_VERS, + }, + } +end + +#################################################################### +# request_youtube_api_browse(continuation) +# request_youtube_api_browse(browse_id, params) +# +# Requests the youtubei/v1/browse endpoint with the required headers +# and POST data in order to get a JSON reply in english US that can +# be easily parsed. +# +# The requested data can either be: +# +# - A continuation token (ctoken). Depending on this token's +# contents, the returned data can be comments, playlist videos, +# search results, channel community tab, ... +# +# - A playlist ID (parameters MUST be an empty string) +# +def request_youtube_api_browse(continuation : String) # JSON Request data, required by the API data = { - "context": { - "client": { - "hl": "en", - "gl": "US", - "clientName": "WEB", - "clientVersion": HARDCODED_CLIENT_VERS, - }, - }, - "continuation": continuation, + "context" => make_youtube_api_context("US"), + "continuation" => continuation, + } + + return _youtube_api_post_json("/youtubei/v1/browse", data) +end + +def request_youtube_api_browse(browse_id : String, params : String) + # JSON Request data, required by the API + data = { + "browseId" => browse_id, + "context" => make_youtube_api_context("US"), } - # Send the POST request and return result + # Append the additionnal parameters if those were provided + # (this is required for channel info, playlist and community, e.g) + if params != "" + data["params"] = params + end + + return _youtube_api_post_json("/youtubei/v1/browse", data) +end + +#################################################################### +# request_youtube_api_search(search_query, params, region) +# +# Requests the youtubei/v1/search endpoint with the required headers +# and POST data in order to get a JSON reply. As the search results +# vary depending on the region, a region code can be specified in +# order to get non-US results. +# +# The requested data is a search string, with some additional +# paramters, formatted as a base64 string. +# +def request_youtube_api_search(search_query : String, params : String, region = nil) + # JSON Request data, required by the API + data = { + "query" => search_query, + "context" => make_youtube_api_context(region), + "params" => params, + } + + return _youtube_api_post_json("/youtubei/v1/search", data) +end + +#################################################################### +# _youtube_api_post_json(endpoint, data) +# +# Internal function that does the actual request to youtube servers +# and handles errors. +# +# The requested data is an endpoint (URL without the domain part) +# and the data as a Hash object. +# +def _youtube_api_post_json(endpoint, data) + # Send the POST request and parse result response = YT_POOL.client &.post( - "/youtubei/v1/browse?key=#{HARDCODED_API_KEY}", - headers: HTTP::Headers{"content-type" => "application/json"}, + "#{endpoint}?key=#{HARDCODED_API_KEY}", + headers: HTTP::Headers{"content-type" => "application/json; charset=UTF-8"}, body: data.to_json ) - return response.body + initial_data = JSON.parse(response.body).as_h + + # Error handling + if initial_data.has_key?("error") + code = initial_data["error"]["code"] + message = initial_data["error"]["message"].to_s.sub(/(\\n)+\^$/, "") + + raise InfoException.new("Could not extract JSON. Youtube API returned \ + error #{code} with message:<br>\"#{message}\"") + end + + return initial_data end diff --git a/src/invidious/jobs/bypass_captcha_job.cr b/src/invidious/jobs/bypass_captcha_job.cr index 4269e123..e68b81e6 100644 --- a/src/invidious/jobs/bypass_captcha_job.cr +++ b/src/invidious/jobs/bypass_captcha_job.cr @@ -112,7 +112,7 @@ class Invidious::Jobs::BypassCaptchaJob < Invidious::Jobs::BaseJob headers = HTTP::Headers{ "Cookie" => URI.parse(response.headers["location"]).query_params["google_abuse"].split(";")[0], } - cookies = HTTP::Cookies.from_headers(headers) + cookies = HTTP::Cookies.from_client_headers(headers) cookies.each { |cookie| CONFIG.cookies << cookie } diff --git a/src/invidious/playlists.cr b/src/invidious/playlists.cr index 073a9986..fe7f82f3 100644 --- a/src/invidious/playlists.cr +++ b/src/invidious/playlists.cr @@ -361,16 +361,7 @@ def fetch_playlist(plid, locale) plid = "UU#{plid.lchop("UC")}" end - response = YT_POOL.client &.get("/playlist?list=#{plid}&hl=en") - if response.status_code != 200 - if response.headers["location"]?.try &.includes? "/sorry/index" - raise InfoException.new("Could not extract playlist info. Instance is likely blocked.") - else - raise InfoException.new("Not a playlist.") - end - end - - initial_data = extract_initial_data(response.body) + initial_data = request_youtube_api_browse("VL" + plid, params: "") playlist_sidebar_renderer = initial_data["sidebar"]?.try &.["playlistSidebarRenderer"]?.try &.["items"]? raise InfoException.new("Could not extract playlistSidebarRenderer.") if !playlist_sidebar_renderer @@ -451,17 +442,12 @@ def get_playlist_videos(db, playlist, offset, locale = nil, continuation = nil) offset = (offset / 100).to_i64 * 100_i64 ctoken = produce_playlist_continuation(playlist.id, offset) - initial_data = JSON.parse(request_youtube_api_browse(ctoken)).as_h + initial_data = request_youtube_api_browse(ctoken) else - response = YT_POOL.client &.get("/playlist?list=#{playlist.id}&gl=US&hl=en") - initial_data = extract_initial_data(response.body) + initial_data = request_youtube_api_browse("VL" + playlist.id, params: "") end - if initial_data - return extract_playlist_videos(initial_data) - else - return [] of PlaylistVideo - end + return extract_playlist_videos(initial_data) end end diff --git a/src/invidious/routes/login.cr b/src/invidious/routes/login.cr index ffe5f568..f9e6ea6c 100644 --- a/src/invidious/routes/login.cr +++ b/src/invidious/routes/login.cr @@ -238,7 +238,7 @@ class Invidious::Routes::Login < Invidious::Routes::BaseRoute traceback << "Logging in..." location = URI.parse(challenge_results[0][-1][2].to_s) - cookies = HTTP::Cookies.from_headers(headers) + cookies = HTTP::Cookies.from_client_headers(headers) headers.delete("Content-Type") headers.delete("Google-Accounts-XSRF") @@ -261,7 +261,7 @@ class Invidious::Routes::Login < Invidious::Routes::BaseRoute location = login.headers["Location"]?.try { |u| URI.parse(u) } end - cookies = HTTP::Cookies.from_headers(headers) + cookies = HTTP::Cookies.from_client_headers(headers) sid = cookies["SID"]?.try &.value if !sid raise "Couldn't get SID." diff --git a/src/invidious/routes/preferences.cr b/src/invidious/routes/preferences.cr index cfdad443..f98c7a5e 100644 --- a/src/invidious/routes/preferences.cr +++ b/src/invidious/routes/preferences.cr @@ -198,10 +198,10 @@ class Invidious::Routes::PreferencesRoute < Invidious::Routes::BaseRoute end if CONFIG.domain - env.response.cookies["PREFS"] = HTTP::Cookie.new(name: "PREFS", domain: "#{CONFIG.domain}", value: preferences, expires: Time.utc + 2.years, + env.response.cookies["PREFS"] = HTTP::Cookie.new(name: "PREFS", domain: "#{CONFIG.domain}", value: URI.encode_www_form(preferences), expires: Time.utc + 2.years, secure: secure, http_only: true) else - env.response.cookies["PREFS"] = HTTP::Cookie.new(name: "PREFS", value: preferences, expires: Time.utc + 2.years, + env.response.cookies["PREFS"] = HTTP::Cookie.new(name: "PREFS", value: URI.encode_www_form(preferences), expires: Time.utc + 2.years, secure: secure, http_only: true) end end @@ -250,10 +250,10 @@ class Invidious::Routes::PreferencesRoute < Invidious::Routes::BaseRoute end if CONFIG.domain - env.response.cookies["PREFS"] = HTTP::Cookie.new(name: "PREFS", domain: "#{CONFIG.domain}", value: preferences, expires: Time.utc + 2.years, + env.response.cookies["PREFS"] = HTTP::Cookie.new(name: "PREFS", domain: "#{CONFIG.domain}", value: URI.encode_www_form(preferences), expires: Time.utc + 2.years, secure: secure, http_only: true) else - env.response.cookies["PREFS"] = HTTP::Cookie.new(name: "PREFS", value: preferences, expires: Time.utc + 2.years, + env.response.cookies["PREFS"] = HTTP::Cookie.new(name: "PREFS", value: URI.encode_www_form(preferences), expires: Time.utc + 2.years, secure: secure, http_only: true) end end diff --git a/src/invidious/routes/search.cr b/src/invidious/routes/search.cr index a993a17a..513904b8 100644 --- a/src/invidious/routes/search.cr +++ b/src/invidious/routes/search.cr @@ -20,15 +20,17 @@ class Invidious::Routes::Search < Invidious::Routes::BaseRoute query = env.params.query["search_query"]? query ||= env.params.query["q"]? - query ||= "" - page = env.params.query["page"]?.try &.to_i? - page ||= 1 + page = env.params.query["page"]? - if query - env.redirect "/search?q=#{URI.encode_www_form(query)}&page=#{page}" + if query && !query.empty? + if page && !page.empty? + env.redirect "/search?q=" + URI.encode_www_form(query) + "&page=" + page + else + env.redirect "/search?q=" + URI.encode_www_form(query) + end else - env.redirect "/" + env.redirect "/search" end end @@ -38,28 +40,31 @@ class Invidious::Routes::Search < Invidious::Routes::BaseRoute query = env.params.query["search_query"]? query ||= env.params.query["q"]? - query ||= "" - return env.redirect "/" if query.empty? + if !query || query.empty? + # Display the full page search box implemented in #1977 + env.set "search", "" + templated "search_homepage", navbar_search: false + else + page = env.params.query["page"]?.try &.to_i? + page ||= 1 - page = env.params.query["page"]?.try &.to_i? - page ||= 1 + user = env.get? "user" - user = env.get? "user" + begin + search_query, count, videos, operators = process_search_query(query, page, user, region: region) + rescue ex + return error_template(500, ex) + end - begin - search_query, count, videos, operators = process_search_query(query, page, user, region: nil) - rescue ex - return error_template(500, ex) - end + operator_hash = {} of String => String + operators.each do |operator| + key, value = operator.downcase.split(":") + operator_hash[key] = value + end - operator_hash = {} of String => String - operators.each do |operator| - key, value = operator.downcase.split(":") - operator_hash[key] = value + env.set "search", query + templated "search" end - - env.set "search", query - templated "search" end end diff --git a/src/invidious/search.cr b/src/invidious/search.cr index 4b216613..662173a0 100644 --- a/src/invidious/search.cr +++ b/src/invidious/search.cr @@ -246,8 +246,7 @@ def channel_search(query, page, channel) continuation = produce_channel_search_continuation(ucid, query, page) response_json = request_youtube_api_browse(continuation) - result = JSON.parse(response_json) - continuationItems = result["onResponseReceivedActions"]? + continuationItems = response_json["onResponseReceivedActions"]? .try &.[0]["appendContinuationItemsAction"]["continuationItems"] return 0, [] of SearchItem if !continuationItems @@ -264,14 +263,9 @@ end def search(query, search_params = produce_search_params(content_type: "all"), region = nil) return 0, [] of SearchItem if query.empty? - body = YT_POOL.client(region, &.get("/results?search_query=#{URI.encode_www_form(query)}&sp=#{search_params}&hl=en").body) - return 0, [] of SearchItem if body.empty? - - initial_data = extract_initial_data(body) + initial_data = request_youtube_api_search(query, search_params, region) items = extract_items(initial_data) - # initial_data["estimatedResults"]?.try &.as_s.to_i64 - return items.size, items end diff --git a/src/invidious/users.cr b/src/invidious/users.cr index 5dfd80bb..d774ee12 100644 --- a/src/invidious/users.cr +++ b/src/invidious/users.cr @@ -462,7 +462,7 @@ def subscribe_ajax(channel_id, action, env_headers) html = YT_POOL.client &.get("/subscription_manager?disable_polymer=1", headers) - cookies = HTTP::Cookies.from_headers(headers) + cookies = HTTP::Cookies.from_client_headers(headers) html.cookies.each do |cookie| if {"VISITOR_INFO1_LIVE", "YSC", "SIDCC"}.includes? cookie.name if cookies[cookie.name]? diff --git a/src/invidious/views/search_homepage.ecr b/src/invidious/views/search_homepage.ecr index 8927c3f1..7d2dab83 100644 --- a/src/invidious/views/search_homepage.ecr +++ b/src/invidious/views/search_homepage.ecr @@ -1,7 +1,7 @@ <% content_for "header" do %> <meta name="description" content="<%= translate(locale, "An alternative front-end to YouTube") %>"> <title> - Invidious + Invidious - <%= translate(locale, "search") %> </title> <link rel="stylesheet" href="/css/empty.css?v=<%= ASSET_COMMIT %>"> <% end %> diff --git a/src/invidious/views/template.ecr b/src/invidious/views/template.ecr index 5b63bf1f..a13d3928 100644 --- a/src/invidious/views/template.ecr +++ b/src/invidious/views/template.ecr @@ -26,7 +26,7 @@ <span style="display:none" id="dark_mode_pref"><%= env.get("preferences").as(Preferences).dark_mode %></span> <div class="pure-g"> <div class="pure-u-1 pure-u-md-2-24"></div> - <div class="pure-u-1 pure-u-md-20-24", id="contents"> + <div class="pure-u-1 pure-u-md-20-24" id="contents"> <div class="pure-g navbar h-box"> <% if navbar_search %> <div class="pure-u-1 pure-u-md-4-24"> |
