73 Commits

Author SHA1 Message Date
df8839d1f0 Make base-Invidious video info extraction more resilient (#5312)
Try next fallback client if one raises

Convert `dig` to `dig?`

Optimize companionless stream retrieval
2025-05-28 20:18:51 +00:00
b859faebf0 Remove @iv-org/developers from codeowners (#5314) 2025-05-28 17:34:49 +02:00
4b37d47ebb Add missing xml.text on "title" element for channels RSS (#5320) 2025-05-22 20:12:54 -04:00
2c857b5ab6 Remove text captchas from Invidious (#5308)
textcaptcha.com seems to be down since April and it does not appear that
service will be restored.

Text captchas can be easily automated using free LLMs, so keeping the
text captcha is more like a gate to create accounts in mass on public
Invidious instances.

It also gives headaches like bots automating account creation to modify
the videos that appear popular page of each instance (since the popular
page is based on the subscriptions of the registered users).
2025-05-17 16:37:55 -07:00
00299ca4a0 Remove Image CAPTCHA and Text CAPTCHA from locale 2025-05-17 19:23:40 -04:00
9d18c8699f Release versioning maintenance (#5310) 2025-05-17 16:22:32 -07:00
475bf7448a Add Javascript licence information automatically (#5297) 2025-05-17 16:20:38 -07:00
50e0a4361b Add missing javascript licenses (#5292) 2025-05-17 16:19:21 -07:00
6bfb61e9b4 fix: safely access "label" key (#5282) 2025-05-17 16:18:58 -07:00
ef07c542dc fix: pass user to query.process if present (#5277) 2025-05-17 16:18:37 -07:00
a9180aa6c1 fix: do not strip '+' character from referer (#5276) 2025-05-17 16:18:15 -07:00
4b2f9ffffc fix: set CSP header after setting preferences of registered users (#5275) 2025-05-17 16:17:43 -07:00
64ad97f308 fix(typo): 'Salect' -> 'Select' (#5242) 2025-05-17 16:17:08 -07:00
d5cb653fd1 Handle parse errors gracefully on timeline items (#5196) 2025-05-17 16:16:20 -07:00
0b23dd12e1 require base_job before the other jobs (#5194) 2025-05-17 16:15:32 -07:00
23d66338cd Translations update from Hosted Weblate (#5192) 2025-05-17 16:15:03 -07:00
df41cb9588 Update Kemal to 1.6.0 and remove Kilt (#5120) 2025-05-17 16:14:40 -07:00
49ada0aae9 Fix incorrect PR link for v2.20250504.0 2025-05-17 15:49:03 -07:00
f6a41ce90d Bump shard.yml version 2025-05-17 15:42:48 -07:00
f7aefd5fb1 Release v2.20250517.0 2025-05-17 15:41:33 -07:00
6376fd55db Remove text captcha due to textcaptcha.com being down
Fixes https://github.com/iv-org/invidious/issues/5295

textcaptcha.com seems to be down since April and it does not appear that service will be restored.

Text captchas can be easily automated using free LLMs, so keeping the text captcha is more like a gate to create accounts in mass on public Invidious instances.

It also gives headaches like bots automating account creation to modify the videos that appear popular page of each instance (since the popular page is based on the subscriptions of the registered users).
2025-05-17 13:17:26 -04:00
9e172d8371 Update translation files
Updated by "Remove blank strings" hook in Weblate.

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/invidious/translations/
Translation: Invidious/Invidious Translations
2025-05-14 07:51:08 +02:00
8d0834005f Update translation files
Updated by "Remove blank strings" hook in Weblate.

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/invidious/translations/
Translation: Invidious/Invidious Translations
2025-05-14 07:51:08 +02:00
9f192d4f74 Update translation files
Updated by "Remove blank strings" hook in Weblate.

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/invidious/translations/
Translation: Invidious/Invidious Translations
2025-05-14 07:51:08 +02:00
ee7b8b6c61 Update translation files
Updated by "Remove blank strings" hook in Weblate.

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/invidious/translations/
Translation: Invidious/Invidious Translations
2025-05-14 07:51:08 +02:00
b9097d0a3b Update translation files
Updated by "Remove blank strings" hook in Weblate.

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/invidious/translations/
Translation: Invidious/Invidious Translations
2025-05-14 07:51:08 +02:00
be469304de Update translation files
Updated by "Remove blank strings" hook in Weblate.

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/invidious/translations/
Translation: Invidious/Invidious Translations
2025-05-14 07:51:08 +02:00
b6b245586a Update translation files
Updated by "Remove blank strings" hook in Weblate.

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/invidious/translations/
Translation: Invidious/Invidious Translations
2025-05-14 07:51:08 +02:00
88195113bf Update translation files
Updated by "Remove blank strings" hook in Weblate.

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/invidious/translations/
Translation: Invidious/Invidious Translations
2025-05-14 07:51:08 +02:00
4d381aca60 Update translation files
Updated by "Remove blank strings" hook in Weblate.

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/invidious/translations/
Translation: Invidious/Invidious Translations
2025-05-14 07:51:08 +02:00
a5904ecce2 Update translation files
Updated by "Remove blank strings" hook in Weblate.

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/invidious/translations/
Translation: Invidious/Invidious Translations
2025-05-14 07:51:08 +02:00
42125dfadd Update translation files
Updated by "Remove blank strings" hook in Weblate.

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/invidious/translations/
Translation: Invidious/Invidious Translations
2025-05-14 07:51:08 +02:00
5953f7286f Update translation files
Updated by "Remove blank strings" hook in Weblate.

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/invidious/translations/
Translation: Invidious/Invidious Translations
2025-05-14 07:51:08 +02:00
31556d0f88 Update translation files
Updated by "Remove blank strings" hook in Weblate.

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/invidious/translations/
Translation: Invidious/Invidious Translations
2025-05-14 07:51:08 +02:00
7bd1abecde Update translation files
Updated by "Remove blank strings" hook in Weblate.

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/invidious/translations/
Translation: Invidious/Invidious Translations
2025-05-14 07:51:07 +02:00
583195ccbd Update translation files
Updated by "Remove blank strings" hook in Weblate.

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/invidious/translations/
Translation: Invidious/Invidious Translations
2025-05-14 07:51:07 +02:00
f96e476ed9 Update Vietnamese translation
Update translation files

Updated by "Remove blank strings" hook in Weblate.

Co-authored-by: Abc's Noob <abcsspprt@gmail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/invidious/translations/
Translation: Invidious/Invidious Translations
2025-05-14 07:51:07 +02:00
9186020f94 Update translation files
Updated by "Remove blank strings" hook in Weblate.

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/invidious/translations/
Translation: Invidious/Invidious Translations
2025-05-14 07:51:07 +02:00
546a799f0b Update translation files
Updated by "Remove blank strings" hook in Weblate.

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/invidious/translations/
Translation: Invidious/Invidious Translations
2025-05-14 07:51:07 +02:00
2d8326c63d Update translation files
Updated by "Remove blank strings" hook in Weblate.

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/invidious/translations/
Translation: Invidious/Invidious Translations
2025-05-14 07:51:07 +02:00
9c9a8592e0 Update translation files
Updated by "Remove blank strings" hook in Weblate.

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/invidious/translations/
Translation: Invidious/Invidious Translations
2025-05-14 07:51:07 +02:00
435106b7de Update translation files
Updated by "Remove blank strings" hook in Weblate.

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/invidious/translations/
Translation: Invidious/Invidious Translations
2025-05-14 07:51:07 +02:00
6a9ed48d5d Update translation files
Updated by "Remove blank strings" hook in Weblate.

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/invidious/translations/
Translation: Invidious/Invidious Translations
2025-05-14 07:51:07 +02:00
a5b97a5850 Update translation files
Updated by "Remove blank strings" hook in Weblate.

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/invidious/translations/
Translation: Invidious/Invidious Translations
2025-05-14 07:51:07 +02:00
f8f6eb74f5 Update translation files
Updated by "Remove blank strings" hook in Weblate.

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/invidious/translations/
Translation: Invidious/Invidious Translations
2025-05-14 07:51:07 +02:00
1e73f4e382 Update translation files
Updated by "Remove blank strings" hook in Weblate.

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/invidious/translations/
Translation: Invidious/Invidious Translations
2025-05-14 07:51:07 +02:00
476bc51b0e Update translation files
Updated by "Remove blank strings" hook in Weblate.

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/invidious/translations/
Translation: Invidious/Invidious Translations
2025-05-14 07:51:07 +02:00
96b226b130 Update translation files
Updated by "Remove blank strings" hook in Weblate.

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/invidious/translations/
Translation: Invidious/Invidious Translations
2025-05-14 07:51:07 +02:00
3b87bf2675 Update Latvian translation
Add Latvian translation

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: ℂ𝕠𝕠𝕠𝕝 (𝕘𝕚𝕥𝕙𝕦𝕓.𝕔𝕠𝕞/ℂ𝕠𝕠𝕠𝕝) <coool@mail.lv>
2025-05-14 07:51:07 +02:00
0dff773a07 Update translation files
Updated by "Remove blank strings" hook in Weblate.

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/invidious/translations/
Translation: Invidious/Invidious Translations
2025-05-14 07:51:07 +02:00
6fe21a7523 Revert "Update src/invidious/routes/before_all.cr"
This reverts commit aab6ff4bb6.
2025-05-10 23:08:48 -04:00
aab6ff4bb6 Update src/invidious/routes/before_all.cr
Co-authored-by: syeopite <70992037+syeopite@users.noreply.github.com>
2025-05-10 23:02:34 -04:00
20cf913a4e Add Javascript licence information automatically
This commit automates the process of documenting the licenses of
Invidious Javascript files through a compile time macro in the
licenses.ecr template file.

This should hopefully help keep the license documentation up-to-date
and allow extensions like LibreJS to always be able to load the latest
Javascript files of Invidious.

Currently only Invidious's first-party Javascript files are supported.
In the future it should be possible to leverage videojs-dependencies.yml
to automatically document the Javascript licenses for
VideoJS and co. as well.
2025-05-10 18:44:53 -07:00
1492453c60 update comment 2025-05-10 16:31:14 -04:00
401bc110d6 fix: set CSP header after setting preferences of registered users
Fixes https://github.com/iv-org/invidious/issues/5142

add reason why extra_media_csp is after reading user preferences from the database and cookies

set media-src after loading database user preferences
2025-05-10 13:26:30 -04:00
30ae222bf2 Add missing javascript licenses 2025-05-09 23:02:19 -04:00
b120abdcc5 fix: safely access "label" key
Fixes https://github.com/iv-org/invidious/issues/5095

On some videos, `label` is missing from the video information. Invidious
assumed that the `label` key existed.

Videos with label have this inside `metadataBadgeRenderer`:

```
{"style" => "BADGE_STYLE_TYPE_SIMPLE",
 "label" => "4K",
 "trackingParams" => "COMDENwwGAoiEwiCrebe6JWNAxWIxz8EHSQRFTU="}
```

but other videos, for some reason, look like this:

```
{"icon" => {"iconType" => "PERSON_RADAR"},
 "style" => "BADGE_STYLE_TYPE_SIMPLE",
 "trackingParams" => "CM4DENwwGAsiEwiCrebe6JWNAxWIxz8EHSQRFTU="}
```
2025-05-09 02:58:29 -04:00
25eade589f fix: pass user to query.process if present.
Fixes https://github.com/iv-org/invidious/issues/5097
2025-05-08 03:12:00 -04:00
35896d086b fix: do not strip '+' character from referer
Fix that a user of my instance (https://inv.nadeko.net) sent me by email.
2025-05-08 01:00:46 -04:00
05b99df49a fix(typo): 'Salect' -> 'Select' 2025-04-17 16:55:30 -04:00
6c063436d4 Fix issues raised by code review
Remove explicit `self.` from #process of parsers

Remove explicit return tuple in get_issue_template

Fix formatting

Move inline issue template style to stylesheet

Use @id in ProblematicTimelineItem xml repr

Fix naming
2025-04-05 12:40:38 -07:00
7b27585454 Support ProblematicTimelineItem in trending feed 2025-03-19 23:50:41 -07:00
f7810ba007 Use ProblematicTimelineItem as needed in playlists 2025-03-19 23:32:46 -07:00
c288005bfd Make "show technical details" btn translatable 2025-03-19 22:52:04 -07:00
aae5ba01c2 Fix formatting 2025-03-19 22:52:04 -07:00
dd16f15aae Improve error card border color on dark theme 2025-03-19 22:52:04 -07:00
180d77276b Emphasise error card icon 2025-03-19 22:52:04 -07:00
0e0a95430a Improve JSON repr of ProblematicTimelineItem 2025-03-19 22:52:03 -07:00
9de69c0052 Improve design of placeholder item
Also makes it show the error backtrace
2025-03-19 22:52:03 -07:00
dbeee71457 Apply search filters details css only to itself
The CSS for the search filters details box was applied to every
detail element when search.css is loaded
2025-03-19 22:52:03 -07:00
94cb80ea81 Handle parse errors gracefully on timeline items
Prior to this commit, if even a single item fails to parse Invidious
will throw out an error. This means that even if everything else
on a page can be parsed and rendered without issues, the single
problematic item will cause the entire page to be unusable.

This commit gracefully handles parse errors by catching and then
replacing the problematic item with a new "timeline error" object
that represents the parse error. This will allow the rest of the page
to be rendered and an error card that will replace the location of the
problematic item.
2025-03-19 22:52:03 -07:00
dd2e999402 require base_job before the other jobs
The crystal compiler seems to evaluate `require` in an alphabetical way,
so if anyone in the future, wants to add another job and that job is
above `base_job.cr` in alphabetical order, the compiler is going to fail
with `Error: undefined constant: Invidious::Jobs::BaseJob`.

This doesn't fix anything, but it will prevent a future headache.
2025-02-28 19:47:22 -03:00
05c5448bc1 Update Kemal to 1.6.0 and remove Kilt
Kilt is unmaintained and the ECR templating logic has been
natively integrated into Kemal with the issues previously seen
having been resolved.

This commit is mostly a precursor to support the next Kemal
release which will add the ability to create error handlers for
raised exceptions.

See https://github.com/kemalcr/kemal/pull/688
2025-01-29 11:49:45 -08:00
58 changed files with 803 additions and 529 deletions

3
.github/CODEOWNERS vendored
View File

@ -1,6 +1,3 @@
# Default and lowest precedence. If none of the below matches, @iv-org/developers would be requested for review.
* @iv-org/developers
docker-compose.yml @unixfox
docker/ @unixfox
kubernetes/ @unixfox

View File

@ -2,11 +2,15 @@
## vX.Y.0 (future)
## v2.20250517.0
Inverse fallback for the YouTube client from TVHTML then MWEB. Fixes https://github.com/iv-org/invidious/issues/5273
## v2.20250504.0
Small release with quick workaround fix for issue #4251 (Nil assertion failed).
PR: https://github.com/iv-org/invidious/issues/5263
PR: https://github.com/iv-org/invidious/issues/5262
## v2.20250314.0

View File

@ -550,6 +550,10 @@ span > select {
color: #565d64;
}
.light-theme .error-card {
border: 1px solid black;
}
@media (prefers-color-scheme: light) {
.no-theme a:hover,
.no-theme a:active,
@ -596,6 +600,10 @@ span > select {
.light-theme .pure-menu-heading {
color: #565d64;
}
.no-theme .error-card {
border: 1px solid black;
}
}
@ -658,6 +666,10 @@ body.dark-theme {
color: inherit;
}
.dark-theme .error-card {
border: 1px solid #5e5e5e;
}
@media (prefers-color-scheme: dark) {
.no-theme a:hover,
.no-theme a:active,
@ -719,6 +731,10 @@ body.dark-theme {
.no-theme footer a {
color: #adadad !important;
}
.no-theme .error-card {
border: 1px solid #5e5e5e;
}
}
@ -816,3 +832,57 @@ h1, h2, h3, h4, h5, p,
#download_widget {
width: 100%;
}
.error-card {
display: flex;
flex-direction: column;
align-items: center;
padding: 25px;
margin-bottom: 1em;
border-radius: 10px;
box-sizing: border-box;
height: 100%;
}
.error-card > .explanation {
display: grid;
grid-template-columns: max-content 1fr;
grid-template-rows: 1fr max-content;
align-items: center;
column-gap: 10px;
row-gap: 4px;
}
.error-card > .explanation > i {
color: #f44;
font-size: 24px;
grid-area: 1 / 1 / 2 / 2;
}
.error-card > .explanation > h4 {
grid-area: 1 / 2 / 2 / 3;
margin: 0;
}
.error-card > .explanation > p {
grid-area: 2 / 2 / 3 / 3;
margin: 0;
}
.error-card details {
margin-top: 10px;
width: 100%;
}
.error-card summary {
width: 100%;
}
.error-card pre {
height: 300px;
}
.error-issue-template {
padding: 20px;
background: rgba(0, 0, 0, 0.12345);
}

View File

@ -1,4 +1,4 @@
summary {
#filters-collapse summary {
/* This should hide the marker */
display: block;
@ -8,10 +8,10 @@ summary {
cursor: pointer;
}
summary::-webkit-details-marker,
summary::marker { display: none; }
#filters-collapse summary::-webkit-details-marker,
#filters-collapse summary::marker { display: none; }
summary:before {
#filters-collapse summary:before {
border-radius: 5px;
content: "[ + ]";
margin: -2px 10px 0 10px;
@ -20,7 +20,7 @@ summary:before {
width: 40px;
}
details[open] > summary:before { content: "[ ]"; }
#filters-collapse details[open] > summary:before { content: "[ ]"; }
#filters-box {

View File

@ -154,8 +154,8 @@
"View YouTube comments": "عرض تعليقات اليوتيوب",
"View more comments on Reddit": "عرض المزيد من التعليقات على\\من موقع ريديت",
"View `x` comments": {
"([^.,0-9]|^)1([^.,0-9]|$)": "عرض `x` تعليقات",
"": "عرض `x` تعليقات."
"([^.,0-9]|^)1([^.,0-9]|$)": "عرض `x` تعليق",
"": "عرض `x` تعليقات"
},
"View Reddit comments": "عرض تعليقات ريديت",
"Hide replies": "إخفاء الردود",
@ -566,5 +566,8 @@
"carousel_skip": "تخطي الكاروسيل",
"carousel_go_to": "انتقل إلى الشريحة `x`",
"preferences_preload_label": "التحميل المسبق لبيانات الفيديو: ",
"Filipino (auto-generated)": "الفلبينية (المولدة تلقائيًا)"
"Filipino (auto-generated)": "الفلبينية (المولدة تلقائيًا)",
"channel_tab_courses_label": "الدورات",
"channel_tab_posts_label": "المنشورات",
"First page": "الصفحة الأولى"
}

View File

@ -403,7 +403,7 @@
"comments_view_x_replies": "Виж {{count}} отговор",
"comments_view_x_replies_plural": "Виж {{count}} отговора",
"footer_original_source_code": "Оригинален изходен код",
"Import YouTube subscriptions": "Импортиране на YouTube/OPML абонаменти",
"Import YouTube subscriptions": "Импортиране на YouTube-CSV/OPML абонаменти",
"Lithuanian": "Литовски",
"Nyanja": "Нянджа",
"Updated `x` ago": "Актуализирано преди `x`",
@ -493,5 +493,8 @@
"Add to playlist: ": "Добави към плейлист: ",
"Answer": "Отговор",
"Search for videos": "Търсене на видеа",
"The Popular feed has been disabled by the administrator.": "Популярната страница е деактивирана от администратора."
"The Popular feed has been disabled by the administrator.": "Популярната страница е деактивирана от администратора.",
"Filipino (auto-generated)": "Филипински (автоматично генериран)",
"preferences_preload_label": "Предварително заредете видео данни: ",
"First page": "Първа страница"
}

View File

@ -204,7 +204,7 @@
"View JavaScript license information.": "Consulta la informació de la llicència de JavaScript.",
"Playlist privacy": "Privacitat de la llista de reproducció",
"search_message_no_results": "No s'han trobat resultats.",
"search_message_use_another_instance": " També es pot <a href=\"`x`\">buscar en una altra instància</a>.",
"search_message_use_another_instance": "També es pot <a href=\"`x`\">cercar en una altra instància</a>.",
"Genre: ": "Gènere: ",
"Hidden field \"challenge\" is a required field": "El camp ocult \"repte\" és un camp obligatori",
"Burmese": "Birmà",
@ -489,5 +489,16 @@
"generic_button_delete": "Suprimeix",
"Import YouTube watch history (.json)": "Importa l'historial de visualitzacions de YouTube (.json)",
"Answer": "Resposta",
"toggle_theme": "Commuta el tema"
"toggle_theme": "Commuta el tema",
"Add to playlist": "Afegeix a la llista de reproducció",
"Add to playlist: ": "Afegeix a la llista de reproducció: ",
"Search for videos": "Cercar vídeos",
"carousel_slide": "Diapositiva {{current}} de {{total}}",
"preferences_preload_label": "Precarregar dades del vídeo: ",
"carousel_go_to": "Anar a la diapositiva `x`",
"First page": "Primera pàgina",
"Filipino (auto-generated)": "Filipí (generat automàticament)",
"channel_tab_courses_label": "Cursos",
"channel_tab_posts_label": "Missatges",
"carousel_skip": "Saltar l'exhibició"
}

View File

@ -515,5 +515,8 @@
"carousel_skip": "Přeskočit galerii",
"carousel_go_to": "Přejít na snímek `x`",
"preferences_preload_label": "Předem načíst data videa: ",
"Filipino (auto-generated)": "Filipínština (vytvořeno automaticky)"
"Filipino (auto-generated)": "Filipínština (vytvořeno automaticky)",
"First page": "První stránka",
"channel_tab_courses_label": "Kurzy",
"channel_tab_posts_label": "Příspěvky"
}

View File

@ -141,7 +141,7 @@
"An alternative front-end to YouTube": "Pen blaen amgen i YouTube",
"source": "ffynhonnell",
"Log in": "Mewngofnodi",
"Log in/register": "Mewngofnodi/Cofrestru",
"Log in/register": "Mewngofnodi/cofrestru",
"User ID": "Enw defnyddiwr",
"preferences_quality_option_dash": "DASH (ansawdd addasol)",
"Sign In": "Mewngofnodi",
@ -381,5 +381,32 @@
"channel_tab_channels_label": "Sianeli",
"channel_tab_community_label": "Cymuned",
"channel_tab_shorts_label": "Fideos byrion",
"channel_tab_videos_label": "Fideos"
"channel_tab_videos_label": "Fideos",
"generic_playlists_count_0": "{{count}} rhestr chwarae",
"generic_playlists_count_1": "{{count}} rhestr chwarae",
"generic_playlists_count_2": "{{count}} rhestri chwarae",
"generic_playlists_count_3": "{{count}} rhestri chwarae",
"generic_playlists_count_4": "{{count}} rhestri chwarae",
"generic_playlists_count_5": "{{count}} rhestri chwarae",
"New passwords must match": "Rhaid i'r cyfrineiriau newydd cyfateb â'i gilydd",
"last": "diwethaf",
"First page": "Tudalen gyntaf",
"preferences_preload_label": "Cynlwytho data fideo: ",
"preferences_extend_desc_label": "Ymestyn disgrifiad fideo'n awtomatig: ",
"preferences_vr_mode_label": "Fideos rhyngweithiol 360 gradd (angen WebGL): ",
"preferences_video_loop_label": "Doleniwch bob amser: ",
"Top enabled: ": "Tudalen fideos brig wedi'i alluogi: ",
"Export subscriptions as OPML (for NewPipe & FreeTube)": "Allforio tanysgrifiadau ar fformat OPML (i NewPipe a FreeTube)",
"Export subscriptions as OPML": "Allforio tanysgrifiadau ar fformat OPML",
"preferences_annotations_subscribed_label": "Ddangos nodiadau sianeli tanysgrifiwyd fel rhagosodiad? ",
"Redirect homepage to feed: ": "Ailgyfeirio tudalen gartref i'r borthiant: ",
"preferences_feed_menu_label": "Dewislen porthiant: ",
"Login enabled: ": "Mewngofnodi wedi'i alluogi: ",
"tokens_count_0": "",
"tokens_count_1": "tocyn",
"tokens_count_2": "",
"tokens_count_3": "",
"tokens_count_4": "tocynnau",
"tokens_count_5": "",
"Source available here.": "Tarddle ar gael yma."
}

View File

@ -499,5 +499,7 @@
"carousel_go_to": "Zu Element `x` springen",
"carousel_slide": "Seite {{current}} von {{total}}",
"carousel_skip": "Galerie überspringen",
"Filipino (auto-generated)": "Philippinisch (automatisch generiert)"
"Filipino (auto-generated)": "Philippinisch (automatisch generiert)",
"channel_tab_courses_label": "Kurse",
"channel_tab_posts_label": "Beiträge"
}

View File

@ -490,7 +490,7 @@
"Search for videos": "Αναζήτηση βίντεο",
"The Popular feed has been disabled by the administrator.": "Η δημοφιλής ροή έχει απενεργοποιηθεί από τον διαχειριστή.",
"Answer": "Απάντηση",
"Add to playlist": "Προσθήκη στην λίιστα αναπαραγωγής",
"Add to playlist": "Προσθήκη στην λίστα αναπαραγωγής",
"Add to playlist: ": "Προσθήκη στην λίστα αναπαραγωγής : ",
"carousel_slide": "Εικόνα {{current}}απο {{total}}",
"carousel_go_to": "Πήγαινε στην εικόνα`x`",
@ -498,5 +498,8 @@
"Import YouTube watch history (.json)": "Εισαγωγή ιστορικού προβολής YouTube (.json)",
"Filipino (auto-generated)": "Φιλιππινέζικα (αυτόματη παραγωγή)",
"preferences_preload_label": "Προφόρτιση δεδομένων βίντεο: ",
"carousel_skip": "Αποφυγή εμφάνισης εικόνων"
"carousel_skip": "Αποφυγή εμφάνισης εικόνων",
"First page": "Πρώτη σελίδα",
"channel_tab_courses_label": "Μαθήματα",
"channel_tab_posts_label": "Δημοσιεύσεις"
}

View File

@ -64,8 +64,6 @@
"User ID": "User ID",
"Password": "Password",
"Time (h:mm:ss):": "Time (h:mm:ss):",
"Text CAPTCHA": "Text CAPTCHA",
"Image CAPTCHA": "Image CAPTCHA",
"Sign In": "Sign In",
"Register": "Register",
"E-mail": "E-mail",
@ -501,5 +499,8 @@
"toggle_theme": "Toggle Theme",
"carousel_slide": "Slide {{current}} of {{total}}",
"carousel_skip": "Skip the Carousel",
"carousel_go_to": "Go to slide `x`"
"carousel_go_to": "Go to slide `x`",
"timeline_parse_error_placeholder_heading": "Unable to parse item",
"timeline_parse_error_placeholder_message": "Invidious encountered an error while trying to parse this item. For more information see below:",
"timeline_parse_error_show_technical_details": "Show technical details"
}

View File

@ -187,10 +187,10 @@
"Hidden field \"token\" is a required field": "El campo oculto «símbolo» es un campo obligatorio",
"Erroneous challenge": "Desafío no válido",
"Erroneous token": "Símbolo no válido",
"No such user": "Usuario no existe",
"No such user": "El usuario no existe",
"Token is expired, please try again": "El símbolo ha caducado, inténtelo de nuevo",
"English": "Inglés",
"English (auto-generated)": "Inglés (generado automáticamente)",
"English (auto-generated)": "Inglés (generados automáticamente)",
"Afrikaans": "Afrikáans",
"Albanian": "Albanés",
"Amharic": "Amárico",
@ -276,7 +276,7 @@
"Somali": "Somalí",
"Southern Sotho": "Sesoto",
"Spanish": "Español",
"Spanish (Latin America)": "Español (Hispanoamérica)",
"Spanish (Latin America)": "Español (Latinoamérica)",
"Sundanese": "Sondanés",
"Swahili": "Suajili",
"Swedish": "Sueco",
@ -412,8 +412,8 @@
"generic_count_weeks_1": "{{count}} semanas",
"generic_count_weeks_2": "{{count}} semanas",
"generic_playlists_count_0": "{{count}} lista de reproducción",
"generic_playlists_count_1": "{{count}} listas de reproducciones",
"generic_playlists_count_2": "{{count}} listas de reproducciones",
"generic_playlists_count_1": "{{count}} listas de reproducción",
"generic_playlists_count_2": "{{count}} listas de reproducción",
"generic_videos_count_0": "{{count}} video",
"generic_videos_count_1": "{{count}} videos",
"generic_videos_count_2": "{{count}} videos",
@ -463,7 +463,7 @@
"Chinese (Hong Kong)": "Chino (Hong Kong)",
"Chinese (China)": "Chino (China)",
"Korean (auto-generated)": "Coreano (generados automáticamente)",
"Spanish (Mexico)": "Español (Méjico)",
"Spanish (Mexico)": "Español (México)",
"Spanish (auto-generated)": "Español (generados automáticamente)",
"preferences_watch_history_label": "Habilitar historial de reproducciones: ",
"search_message_no_results": "No se han encontrado resultados.",
@ -500,7 +500,7 @@
"generic_button_cancel": "Cancelar",
"generic_button_rss": "RSS",
"channel_tab_podcasts_label": "Podcasts",
"channel_tab_releases_label": "Publicaciones",
"channel_tab_releases_label": "Lanzamientos",
"generic_channels_count_0": "{{count}} canal",
"generic_channels_count_1": "{{count}} canales",
"generic_channels_count_2": "{{count}} canales",
@ -515,5 +515,8 @@
"carousel_skip": "Saltar el carrusel",
"carousel_go_to": "Ir a la diapositiva `x`",
"preferences_preload_label": "Precargar datos del vídeo: ",
"Filipino (auto-generated)": "Filipino (generado automáticamente)"
"Filipino (auto-generated)": "Filipino (generados automáticamente)",
"channel_tab_posts_label": "Publicaciones",
"First page": "Primera página",
"channel_tab_courses_label": "Cursos"
}

View File

@ -515,5 +515,8 @@
"carousel_go_to": "Aller à la diapositive `x`",
"toggle_theme": "Changer le Thème",
"Filipino (auto-generated)": "Philippines (automatiquement générer)",
"preferences_preload_label": "Précharger les données de la vidéo : "
"preferences_preload_label": "Précharger les données de la vidéo : ",
"First page": "Première page",
"channel_tab_courses_label": "Cours",
"channel_tab_posts_label": "Messages"
}

View File

@ -2,7 +2,7 @@
"LIVE": "BEINT",
"Shared `x` ago": "Deilt fyrir `x` síðan",
"Unsubscribe": "Afskrá",
"Subscribe": "Áskrifa",
"Subscribe": "Setja í áskrift",
"View channel on YouTube": "Skoða rás á YouTube",
"View playlist on YouTube": "Skoða spilunarlista á YouTube",
"newest": "nýjasta",
@ -14,8 +14,8 @@
"Clear watch history?": "Hreinsa áhorfsferil?",
"New password": "Nýtt lykilorð",
"New passwords must match": "Nýtt lykilorð verður að passa",
"Authorize token?": "Leyfa teikn?",
"Authorize token for `x`?": "Leyfa teikn fyrir `x`?",
"Authorize token?": "Auðkenna teikn?",
"Authorize token for `x`?": "Auðkenna teikn fyrir `x`?",
"Yes": "Já",
"No": "Nei",
"Import and Export Data": "Inn- og útflutningur gagna",
@ -36,17 +36,17 @@
"source": "uppruni",
"Log in": "Skrá inn",
"Log in/register": "Innskráning/nýskráning",
"User ID": "Notandakenni",
"User ID": "Auðkenni notanda",
"Password": "Lykilorð",
"Time (h:mm:ss):": "Tími (h:mm: ss):",
"Text CAPTCHA": "Texta CAPTCHA",
"Image CAPTCHA": "Mynd CAPTCHA",
"Text CAPTCHA": "CAPTCHA-texti",
"Image CAPTCHA": "CAPTCHA-mynd",
"Sign In": "Skrá inn",
"Register": "Nýskrá",
"E-mail": "Tölvupóstur",
"Preferences": "Kjörstillingar",
"preferences_category_player": "Kjörstillingar spilara",
"preferences_video_loop_label": "Alltaf lykkja: ",
"preferences_video_loop_label": "Alltaf endurtaka: ",
"preferences_autoplay_label": "Sjálfvirk spilun: ",
"preferences_continue_label": "Spila næst sjálfgefið: ",
"preferences_continue_autoplay_label": "Spila næsta myndskeið sjálfkrafa: ",
@ -85,7 +85,7 @@
"preferences_unseen_only_label": "Sýna aðeins óséð: ",
"preferences_notifications_only_label": "Sýna aðeins tilkynningar (ef einhverjar eru): ",
"Enable web notifications": "Virkja veftilkynningar",
"`x` uploaded a video": "`x` hlóð upp myndband",
"`x` uploaded a video": "`x` sendi inn myndskeið",
"`x` is live": "`x` er í beinni",
"preferences_category_data": "Gagnastillingar",
"Clear watch history": "Hreinsa áhorfsferil",
@ -104,8 +104,8 @@
"Registration enabled: ": "Nýskráning virkjuð? ",
"Report statistics: ": "Skrá tölfræði? ",
"Save preferences": "Vista stillingar",
"Subscription manager": "Áskriftarstjóri",
"Token manager": "Teiknastjórnun",
"Subscription manager": "Áskriftastýring",
"Token manager": "Teiknastýring",
"Token": "Teikn",
"Import/export": "Flytja inn/út",
"unsubscribe": "afskrá",
@ -233,7 +233,7 @@
"Korean": "Kóreska",
"Kurdish": "Kúrdíska",
"Kyrgyz": "Kirgisíska",
"Lao": "Laó",
"Lao": "Laóska",
"Latin": "Latína",
"Latvian": "Lettneska",
"Lithuanian": "Litháíska",
@ -295,18 +295,18 @@
"View as playlist": "Skoða sem spilunarlista",
"Default": "Sjálfgefið",
"Music": "Tónlist",
"Gaming": "Tólvuleikja",
"Gaming": "Spilun leikja",
"News": "Fréttir",
"Movies": "Kvikmyndir",
"Download": "Niðurhal",
"Download as: ": "Niðurhala sem: ",
"Download as: ": "Sækja sem: ",
"%A %B %-d, %Y": "%A %B %-d, %Y",
"(edited)": "(breytt)",
"YouTube comment permalink": "YouTube ummæli varanlegur tengill",
"YouTube comment permalink": "Varanlegur tengill á YouTube-ummæli",
"permalink": "Varanlegur tengill",
"`x` marked it with a ❤": "`x` merkti það með ❤",
"Audio mode": "Hljóð ham",
"Video mode": "Myndband ham",
"Audio mode": "Hljóðhamur",
"Video mode": "Myndhamur",
"channel_tab_videos_label": "Myndskeið",
"Playlists": "Spilunarlistar",
"channel_tab_community_label": "Samfélag",
@ -388,7 +388,7 @@
"crash_page_before_reporting": "Áður en þú tilkynnir villu, gakktu úr skugga um að þú hafir:",
"crash_page_switch_instance": "reynt að <a href=\"`x`\">nota annað tilvik</a>",
"crash_page_report_issue": "Ef ekkert af ofantöldu hjálpaði, ættirðu að <a href=\"`x`\">opna nýja verkbeiðni (issue) á GitHub</a> (helst á ensku) og láta fylgja eftirfarandi texta í skilaboðunum þínum (alls EKKI þýða þennan texta):",
"channel_tab_shorts_label": "Stuttmyndir",
"channel_tab_shorts_label": "Símamyndir",
"carousel_slide": "Skyggna {{current}} af {{total}}",
"carousel_go_to": "Fara á skyggnu `x`",
"channel_tab_streams_label": "Bein streymi",
@ -401,8 +401,8 @@
"English (United Kingdom)": "Enska (Bretland)",
"English (United States)": "Enska (Bandarísk)",
"Vietnamese (auto-generated)": "Víetnamska (sjálfvirkt útbúið)",
"generic_count_months": "{{count}} mánuður",
"generic_count_months_plural": "{{count}} mánuðir",
"generic_count_months": "{{count}} mánuði",
"generic_count_months_plural": "{{count}} mánuðum",
"search_filters_sort_option_rating": "Einkunn",
"videoinfo_youTube_embed_link": "Ívefja",
"error_video_not_in_playlist": "Umbeðið myndskeið fyrirfinnst ekki í þessum spilunarlista. <a href=\"`x`\">Smelltu hér til að fara á heimasíðu spilunarlistans.</a>",
@ -429,11 +429,11 @@
"Spanish (auto-generated)": "Spænska (sjálfvirkt útbúið)",
"Spanish (Mexico)": "Spænska (Mexíkó)",
"generic_count_hours": "{{count}} klukkustund",
"generic_count_hours_plural": "{{count}} klukkustundir",
"generic_count_years": "{{count}} ár",
"generic_count_years_plural": "{{count}} ár",
"generic_count_weeks": "{{count}} vika",
"generic_count_weeks_plural": "{{count}} vikur",
"generic_count_hours_plural": "{{count}} klukkustundum",
"generic_count_years": "{{count}} ári",
"generic_count_years_plural": "{{count}} árum",
"generic_count_weeks": "{{count}} viku",
"generic_count_weeks_plural": "{{count}} vikum",
"search_filters_date_option_none": "Hvaða dagsetning sem er",
"Channel Sponsor": "Styrktaraðili rásar",
"search_filters_date_option_week": "Í þessari viku",
@ -476,8 +476,8 @@
"preferences_quality_dash_option_144p": "144p",
"invidious": "Invidious",
"Korean (auto-generated)": "Kóreska (sjálfvirkt útbúið)",
"generic_count_days": "{{count}} dagur",
"generic_count_days_plural": "{{count}} dagar",
"generic_count_days": "{{count}} degi",
"generic_count_days_plural": "{{count}} dögum",
"search_filters_date_option_today": "Í dag",
"search_filters_type_label": "Tegund",
"search_filters_type_option_all": "Hvaða tegund sem er",
@ -498,5 +498,8 @@
"Import YouTube playlist (.csv)": "Flytja inn YouTube spilunarlista (.csv)",
"preferences_quality_option_dash": "DASH (aðlaganleg gæði)",
"preferences_preload_label": "Forhlaða gögnum myndskeiðs: ",
"Filipino (auto-generated)": "Filippínska (sjálfvirkt útbúin)"
"Filipino (auto-generated)": "Filippínska (sjálfvirkt útbúin)",
"channel_tab_posts_label": "Færslur",
"First page": "Fyrsta síða",
"channel_tab_courses_label": "Kennsluefni"
}

View File

@ -515,5 +515,8 @@
"carousel_skip": "Salta la galleria",
"carousel_go_to": "Vai al fotogramma `x`",
"preferences_preload_label": "Precarica dati video: ",
"Filipino (auto-generated)": "Filippino (generati automaticamente)"
"Filipino (auto-generated)": "Filippino (generati automaticamente)",
"First page": "Prima pagina",
"channel_tab_courses_label": "Corsi",
"channel_tab_posts_label": "Post"
}

View File

@ -25,7 +25,7 @@
"No": "いいえ",
"Import and Export Data": "データのインポートとエクスポート",
"Import": "インポート",
"Import Invidious data": "Invidious JSONデータをインポート",
"Import Invidious data": "Invidious JSON データをインポート",
"Import YouTube subscriptions": "YouTube/OPML 登録チャンネルをインポート",
"Import FreeTube subscriptions (.db)": "FreeTube 登録チャンネルをインポート (.db)",
"Import NewPipe subscriptions (.json)": "NewPipe 登録チャンネルをインポート (.json)",
@ -68,7 +68,7 @@
"preferences_related_videos_label": "関連動画を表示: ",
"preferences_annotations_label": "最初からアノテーションを表示: ",
"preferences_extend_desc_label": "動画の説明文を自動的に拡張: ",
"preferences_vr_mode_label": "対話的な360°動画 (WebGLが必要): ",
"preferences_vr_mode_label": "対話的な 360° 動画 (WebGL が必要): ",
"preferences_category_visual": "外観設定",
"preferences_player_style_label": "プレイヤーのスタイル: ",
"Dark mode: ": "ダークモード: ",
@ -77,7 +77,7 @@
"light": "ライト",
"preferences_thin_mode_label": "最小モード: ",
"preferences_category_misc": "ほかの設定",
"preferences_automatic_instance_redirect_label": "インスタンスの自動転送 (redirect.invidious.ioにフォールバック): ",
"preferences_automatic_instance_redirect_label": "インスタンスの自動転送 (redirect.invidious.io にフォールバック): ",
"preferences_category_subscription": "登録チャンネル設定",
"preferences_annotations_subscribed_label": "最初から登録チャンネルのアノテーションを表示 ",
"Redirect homepage to feed: ": "ホームからフィードにリダイレクト: ",
@ -125,7 +125,7 @@
"subscriptions_unseen_notifs_count_0": "{{count}}件の未読通知",
"search": "検索",
"Log out": "ログアウト",
"Released under the AGPLv3 on Github.": "GitHub上でAGPLv3の元で公開",
"Released under the AGPLv3 on Github.": "GitHub 上で AGPLv3 の元で公開",
"Source available here.": "ソースはここで閲覧可能です。",
"View JavaScript license information.": "JavaScriptライセンス情報",
"View privacy policy.": "個人情報保護方針",
@ -143,8 +143,8 @@
"Editing playlist `x`": "再生リスト `x` を編集中",
"Show more": "もっと見る",
"Show less": "表示を少なく",
"Watch on YouTube": "YouTubeで視聴",
"Switch Invidious Instance": "Invidiousインスタンスの変更",
"Watch on YouTube": "YouTube で視聴",
"Switch Invidious Instance": "Invidious インスタンスの変更",
"Hide annotations": "アノテーションを隠す",
"Show annotations": "アノテーションを表示",
"Genre: ": "ジャンル: ",
@ -330,7 +330,7 @@
"(edited)": "(編集済み)",
"YouTube comment permalink": "YouTube コメントのパーマリンク",
"permalink": "パーマリンク",
"`x` marked it with a ❤": "`x` がを送りました",
"`x` marked it with a ❤": "`x` がを送りました",
"Audio mode": "音声モード",
"Video mode": "動画モード",
"channel_tab_videos_label": "動画",
@ -343,7 +343,7 @@
"search_filters_type_label": "種類",
"search_filters_duration_label": "再生時間",
"search_filters_features_label": "特徴",
"search_filters_sort_label": "順番",
"search_filters_sort_label": "並べ替え",
"search_filters_date_option_hour": "1時間以内",
"search_filters_date_option_today": "今日",
"search_filters_date_option_week": "今週",
@ -365,13 +365,13 @@
"Current version: ": "現在のバージョン: ",
"next_steps_error_message": "以下をお試しください: ",
"next_steps_error_message_refresh": "再読み込み",
"next_steps_error_message_go_to_youtube": "YouTubeを開く",
"next_steps_error_message_go_to_youtube": "YouTube を開く",
"search_filters_duration_option_short": "4分未満",
"footer_documentation": "説明書",
"footer_source_code": "ソースコード",
"footer_original_source_code": "元のソースコード",
"footer_modfied_source_code": "改変し使用",
"adminprefs_modified_source_code_url_label": "改変されたソースコードのレポジトリのURL",
"footer_modfied_source_code": "改変し使用",
"adminprefs_modified_source_code_url_label": "改変されたソースコードのレポジトリの URL",
"search_filters_duration_option_long": "20分以上",
"preferences_region_label": "地域: ",
"footer_donate_page": "寄付する",
@ -399,7 +399,7 @@
"preferences_quality_dash_option_worst": "最低",
"preferences_quality_dash_option_best": "最高",
"videoinfo_started_streaming_x_ago": "`x`前に配信を開始",
"videoinfo_watch_on_youTube": "YouTubeで視聴",
"videoinfo_watch_on_youTube": "YouTube で視聴",
"user_created_playlists": "`x`個の作成した再生リスト",
"Video unavailable": "動画は利用できません",
"Chinese": "中国語",
@ -446,7 +446,7 @@
"search_filters_duration_option_medium": "4 20分",
"preferences_save_player_pos_label": "再生位置を保存: ",
"crash_page_before_reporting": "バグを報告する前に、次のことを確認してください。",
"crash_page_report_issue": "上記が助けにならないなら、<a href=\"`x`\">GitHub</a> に新しい issue を作成し(英語が好ましい)、メッセージに次のテキストを含めてくださいテキストは翻訳しない。",
"crash_page_report_issue": "上記が助けにならない場合、<a href=\"`x`\">GitHub</a> に新しい issue を作成し (できれば英語で) 、メッセージに次のテキストを含めてください (テキストは翻訳しない) 。",
"crash_page_search_issue": "<a href=\"`x`\">GitHub の既存の問題 (issue)</a> を検索",
"channel_tab_streams_label": "ライブ",
"channel_tab_playlists_label": "再生リスト",
@ -481,5 +481,8 @@
"carousel_skip": "画像のスライド表示をスキップ",
"toggle_theme": "テーマの切り替え",
"preferences_preload_label": "動画データを事前に読み込む: ",
"Filipino (auto-generated)": "フィリピノ語 (自動生成)"
"Filipino (auto-generated)": "フィリピノ語 (自動生成)",
"First page": "最初のページ",
"channel_tab_posts_label": "投稿",
"channel_tab_courses_label": "コース"
}

View File

@ -419,7 +419,7 @@
"Portuguese (Brazil)": "포르투갈어 (브라질)",
"search_message_no_results": "결과가 없습니다.",
"search_message_change_filters_or_query": "필터를 변경하시거나 검색어를 넓게 시도해보세요.",
"search_message_use_another_instance": " <a href=\"`x`\">다른 인스턴스에서 검색</a>할 수도 있습니다.",
"search_message_use_another_instance": "<a href=\"`x`\">다른 인스턴스에서 검색</a>할 수도 있습니다.",
"English (United States)": "영어 (미국)",
"Chinese": "중국어",
"Chinese (China)": "중국어 (중국)",
@ -480,5 +480,9 @@
"Search for videos": "비디오 검색",
"toggle_theme": "테마 전환",
"carousel_slide": "{{total}}의 슬라이드 {{current}}",
"preferences_preload_label": "비디오 데이터 사전 로드: "
"preferences_preload_label": "비디오 데이터 사전 로드: ",
"First page": "첫 페이지",
"Filipino (auto-generated)": "Filipino (auto-generated)",
"channel_tab_posts_label": "게시글",
"channel_tab_courses_label": "코스"
}

69
locales/lv.json Normal file
View File

@ -0,0 +1,69 @@
{
"generic_channels_count_0": "{{count}} kanāli",
"generic_channels_count_1": "{{count}} kanāls",
"generic_channels_count_2": "{{count}} kanāli",
"Add to playlist": "Pievienot atskaņošanas sarakstam",
"Answer": "Atbildēt",
"generic_subscribers_count_0": "{{count}} abonenti",
"generic_subscribers_count_1": "{{count}} abonents",
"generic_subscribers_count_2": "{{count}} abonenti",
"generic_button_delete": "Dzēst",
"generic_button_edit": "Rediģēt",
"generic_button_save": "Saglabāt",
"generic_button_cancel": "Atcelt",
"generic_button_rss": "RSS",
"Unsubscribe": "Pārtraukt abonementu",
"View playlist on YouTube": "Skatīt atskaņošanas sarakstu YouTube vietnē",
"New password": "Jaunā parole",
"Yes": "Jā",
"No": "Nē",
"Import and Export Data": "Ievietot un izgūt datus",
"Import": "Ievietot",
"Import Invidious data": "Ievietot Invidious JSON datus",
"Delete account?": "Vai dzēst kontu?",
"History": "Vēsture",
"User ID": "Lietotāja ID",
"Password": "Parole",
"Import YouTube subscriptions": "Ievietot YouTube CSV vai OPML abonementus",
"E-mail": "E-pasts",
"Preferences": "Iestatījumi",
"preferences_category_player": "Atskaņotāja iestatījumi",
"preferences_quality_option_hd720": "HD - 720p",
"preferences_quality_option_medium": "Vidēja",
"preferences_quality_dash_option_worst": "Vissliktākā",
"preferences_quality_dash_option_2160p": "2160p (4K)",
"preferences_quality_dash_option_1080p": "1080p (Full HD)",
"preferences_quality_dash_option_720p": "720p (HD)",
"preferences_quality_dash_option_1440p": "1440p (2.5K, QHD)",
"preferences_quality_dash_option_480p": "480p (SD)",
"preferences_quality_dash_option_360p": "360p",
"preferences_quality_dash_option_240p": "240p",
"preferences_quality_dash_option_144p": "144p",
"preferences_volume_label": "Atskaņošanas skaļums: ",
"reddit": "Reddit",
"invidious": "Invidious",
"Bangla": "Bengāļu",
"Basque": "Basku",
"Cebuano": "Sebuāņu",
"Chinese (Traditional)": "Ķīniešu (tradicionālā)",
"Corsican": "Korsikāņu",
"Croatian": "Horvātu",
"Galician": "Galisiešu",
"Georgian": "Gruzīnu",
"Gujarati": "Gudžaratu",
"German": "Vācu",
"Greek": "Grieķu",
"Haitian Creole": "Haitiešu",
"Hausa": "Hausu",
"Hawaiian": "Havajiešu",
"Export data as JSON": "Izgūt Invidious datus JSON formātā",
"preferences_quality_dash_option_4320p": "4320p (8K)",
"Time (h:mm:ss):": "Laiks (h:mm:ss):",
"Chinese (Simplified)": "Ķīniešu (vienkāršotā)",
"preferences_quality_dash_option_best": "Vislabākā",
"preferences_quality_option_small": "Zema",
"youtube": "YouTube",
"Add to playlist: ": "Pievienot atskaņošanas sarakstam: ",
"Subscribe": "Abonēt",
"View channel on YouTube": "Skatīt kanālu YouTube vietnē"
}

View File

@ -498,5 +498,8 @@
"carousel_skip": "Carousel overslaan",
"toggle_theme": "Thema omschakelen",
"preferences_preload_label": "Videogegevens vooraf laden: ",
"Filipino (auto-generated)": "Filipijns (automatisch gegenereerd)"
"Filipino (auto-generated)": "Filipijns (automatisch gegenereerd)",
"channel_tab_courses_label": "Cursussen",
"First page": "Eerste pagina",
"channel_tab_posts_label": "Gepost"
}

View File

@ -515,5 +515,8 @@
"carousel_skip": "Pomiń karuzelę",
"carousel_go_to": "Przejdź do slajdu `x`",
"preferences_preload_label": "Wstępne ładowanie danych wideo: ",
"Filipino (auto-generated)": "filipiński (wygenerowany automatycznie)"
"Filipino (auto-generated)": "filipiński (wygenerowany automatycznie)",
"First page": "Pierwsza strona",
"channel_tab_posts_label": "Posty",
"channel_tab_courses_label": "Kursy"
}

View File

@ -515,5 +515,8 @@
"carousel_skip": "Ignorar carrossel",
"carousel_go_to": "Ir ao slide `x`",
"preferences_preload_label": "Pré-carregar dados do vídeo: ",
"Filipino (auto-generated)": "Filipino (gerado automaticamente)"
"Filipino (auto-generated)": "Filipino (gerado automaticamente)",
"channel_tab_posts_label": "Postagens",
"First page": "Primeira página",
"channel_tab_courses_label": "Cursos"
}

View File

@ -1,27 +1,27 @@
{
"LIVE": "Em direto",
"LIVE": "Direto",
"Shared `x` ago": "Partilhado `x` atrás",
"Unsubscribe": "Anular subscrição",
"Subscribe": "Subscrever",
"View channel on YouTube": "Ver canal no YouTube",
"View playlist on YouTube": "Ver lista de reprodução no YouTube",
"newest": "mais recentes",
"oldest": "mais antigos",
"popular": "popular",
"newest": "recentes",
"oldest": "antigos",
"popular": "populares",
"last": "últimos",
"Next page": "Próxima página",
"Next page": "Página seguinte",
"Previous page": "Página anterior",
"Clear watch history?": "Limpar histórico de reprodução?",
"New password": "Nova palavra-chave",
"New passwords must match": "As novas palavra-chaves devem corresponder",
"Authorize token?": "Autorizar token?",
"Authorize token for `x`?": "Autorizar token para `x`?",
"New password": "Nova palavra-passe",
"New passwords must match": "As novas palavras-passe devem ser iguais",
"Authorize token?": "Autorizar 'token'?",
"Authorize token for `x`?": "Autorizar 'token' para `x`?",
"Yes": "Sim",
"No": "Não",
"Import and Export Data": "Importar e exportar dados",
"Import": "Importar",
"Import Invidious data": "Importar dados JSON do Invidious",
"Import YouTube subscriptions": "Importar subscrições do YouTube/OPML",
"Import YouTube subscriptions": "Importar via YouTube csv ou subscrição OPML",
"Import FreeTube subscriptions (.db)": "Importar subscrições do FreeTube (.db)",
"Import NewPipe subscriptions (.json)": "Importar subscrições do NewPipe (.json)",
"Import NewPipe data (.zip)": "Importar dados do NewPipe (.zip)",
@ -32,38 +32,38 @@
"Delete account?": "Eliminar conta?",
"History": "Histórico",
"An alternative front-end to YouTube": "Uma interface alternativa ao YouTube",
"JavaScript license information": "Informação de licença do JavaScript",
"source": "código-fonte",
"JavaScript license information": "Informação da licença JavaScript",
"source": "fonte",
"Log in": "Iniciar sessão",
"Log in/register": "Iniciar sessão/registar",
"User ID": "Utilizador",
"Password": "Palavra-chave",
"Password": "Palavra-passe",
"Time (h:mm:ss):": "Tempo (h:mm:ss):",
"Text CAPTCHA": "Texto CAPTCHA",
"Image CAPTCHA": "Imagem CAPTCHA",
"Sign In": "Iniciar sessão",
"Sign In": "Entrar",
"Register": "Registar",
"E-mail": "E-mail",
"Preferences": "Preferências",
"preferences_category_player": "Preferências do reprodutor",
"preferences_video_loop_label": "Repetir sempre: ",
"preferences_autoplay_label": "Reprodução automática: ",
"preferences_continue_label": "Reproduzir sempre o próximo: ",
"preferences_continue_label": "Reproduzir sempre o seguinte: ",
"preferences_continue_autoplay_label": "Reproduzir próximo vídeo automaticamente: ",
"preferences_listen_label": "Apenas áudio: ",
"preferences_local_label": "Usar proxy nos vídeos: ",
"preferences_speed_label": "Velocidade preferida: ",
"preferences_quality_label": "Qualidade de vídeo preferida: ",
"preferences_volume_label": "Volume da reprodução: ",
"preferences_comments_label": "Preferência dos comentários: ",
"preferences_volume_label": "Volume de reprodução: ",
"preferences_comments_label": "Comentários padrão: ",
"youtube": "YouTube",
"reddit": "Reddit",
"preferences_captions_label": "Legendas predefinidas: ",
"preferences_captions_label": "Legendas padrão: ",
"Fallback captions: ": "Legendas alternativas: ",
"preferences_related_videos_label": "Mostrar vídeos relacionados: ",
"preferences_annotations_label": "Mostrar anotações sempre: ",
"preferences_extend_desc_label": "Estender automaticamente a descrição do vídeo: ",
"preferences_vr_mode_label": "Vídeos interativos de 360 graus (necessita de WebGL): ",
"preferences_extend_desc_label": "Expandir automaticamente a descrição do vídeo: ",
"preferences_vr_mode_label": "Vídeos interativos de 360 graus (requer WebGL): ",
"preferences_category_visual": "Preferências visuais",
"preferences_player_style_label": "Estilo do reprodutor: ",
"Dark mode: ": "Modo escuro: ",
@ -74,9 +74,9 @@
"preferences_category_misc": "Preferências diversas",
"preferences_automatic_instance_redirect_label": "Redirecionamento de instância automática (solução de último recurso para redirect.invidious.io): ",
"preferences_category_subscription": "Preferências de subscrições",
"preferences_annotations_subscribed_label": "Mostrar sempre anotações aos canais subscritos: ",
"preferences_annotations_subscribed_label": "Mostrar sempre anotações nos canais subscritos: ",
"Redirect homepage to feed: ": "Redirecionar página inicial para subscrições: ",
"preferences_max_results_label": "Quantidade de vídeos nas subscrições: ",
"preferences_max_results_label": "Número de vídeos nas subscrições: ",
"preferences_sort_label": "Ordenar vídeos por: ",
"published": "publicado",
"published - reverse": "publicado - inverso",
@ -88,19 +88,19 @@
"Only show latest unwatched video from channel: ": "Mostrar apenas vídeos mais recentes não visualizados do canal: ",
"preferences_unseen_only_label": "Mostrar apenas vídeos não visualizados: ",
"preferences_notifications_only_label": "Mostrar apenas notificações (se existirem): ",
"Enable web notifications": "Ativar notificações pela web",
"`x` uploaded a video": "`x` publicou um novo vídeo",
"Enable web notifications": "Ativar notificações web",
"`x` uploaded a video": "`x` publicou um vídeo",
"`x` is live": "`x` está em direto",
"preferences_category_data": "Preferências de dados",
"Clear watch history": "Limpar histórico de reprodução",
"Import/export data": "Importar / exportar dados",
"Change password": "Alterar palavra-chave",
"Manage subscriptions": "Gerir as subscrições",
"Import/export data": "Importar/exportar dados",
"Change password": "Alterar palavra-passe",
"Manage subscriptions": "Gerir subscrições",
"Manage tokens": "Gerir tokens",
"Watch history": "Histórico de reprodução",
"Delete account": "Eliminar conta",
"preferences_category_admin": "Preferências de administrador",
"preferences_default_home_label": "Página inicial predefinida: ",
"preferences_default_home_label": "Página inicial padrão: ",
"preferences_feed_menu_label": "Menu de subscrições: ",
"preferences_show_nick_label": "Mostrar nome de utilizador em cima: ",
"Top enabled: ": "Destaques ativados: ",
@ -109,28 +109,29 @@
"Registration enabled: ": "Registar ativado: ",
"Report statistics: ": "Relatório de estatísticas: ",
"Save preferences": "Guardar preferências",
"Subscription manager": "Gerir subscrições",
"Token manager": "Gerir tokens",
"Subscription manager": "Gestor de subscrições",
"Token manager": "Gestor de tokens",
"Token": "Token",
"tokens_count": "{{count}} token",
"tokens_count_plural": "{{count}} tokens",
"Import/export": "Importar / exportar",
"tokens_count_0": "{{count}} token",
"tokens_count_1": "{{count}} tokens",
"tokens_count_2": "{{count}} tokens",
"Import/export": "Importar/exportar",
"unsubscribe": "anular subscrição",
"revoke": "revogar",
"Subscriptions": "Subscrições",
"search": "pesquisar",
"Log out": "Terminar sessão",
"Released under the AGPLv3 on Github.": "Lançado sob a AGPLv3 no GitHub.",
"Released under the AGPLv3 on Github.": "Disponibilizada sob a AGPLv3 no GitHub.",
"Source available here.": "Código-fonte disponível aqui.",
"View JavaScript license information.": "Ver informações da licença do JavaScript.",
"View privacy policy.": "Ver a política de privacidade.",
"View JavaScript license information.": "Ver informações da licença JavaScript.",
"View privacy policy.": "Ver política de privacidade.",
"Trending": "Tendências",
"Public": "Público",
"Unlisted": "Não listado",
"Private": "Privado",
"View all playlists": "Ver todas as listas de reprodução",
"Updated `x` ago": "Atualizado `x` atrás",
"Delete playlist `x`?": "Eliminar a lista de reprodução `x`?",
"Updated `x` ago": "Atualizado `x`",
"Delete playlist `x`?": "Eliminar lista de reprodução `x`?",
"Delete playlist": "Eliminar lista de reprodução",
"Create playlist": "Criar lista de reprodução",
"Title": "Título",
@ -139,7 +140,7 @@
"Show more": "Mostrar mais",
"Show less": "Mostrar menos",
"Watch on YouTube": "Ver no YouTube",
"Switch Invidious Instance": "Mudar a instância do Invidious",
"Switch Invidious Instance": "Alterar instância Invidious",
"Hide annotations": "Ocultar anotações",
"Show annotations": "Mostrar anotações",
"Genre: ": "Género: ",
@ -150,27 +151,27 @@
"Whitelisted regions: ": "Regiões permitidas: ",
"Blacklisted regions: ": "Regiões bloqueadas: ",
"Shared `x`": "Partilhado `x`",
"Premieres in `x`": "Estreias em `x`",
"Premieres `x`": "Estreias `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.": "Olá! Parece que o JavaScript está desativado. Clique aqui para ver os comentários, entretanto eles podem levar mais tempo para carregar.",
"Premieres in `x`": "Estreia a `x`",
"Premieres `x`": "Estreia `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.": "Olá! Parece que o JavaScript está desativado. Clique aqui para ver os comentários, mas tenha e conta que podem levar mais tempo para carregar.",
"View YouTube comments": "Ver comentários do YouTube",
"View more comments on Reddit": "Ver mais comentários no Reddit",
"View `x` comments": {
"([^.,0-9]|^)1([^.,0-9]|$)": "Ver `x` comentários",
"([^.,0-9]|^)1([^.,0-9]|$)": "Ver `x` comentário",
"": "Ver `x` comentários"
},
"View Reddit comments": "Ver comentários do Reddit",
"Hide replies": "Ocultar respostas",
"Show replies": "Mostrar respostas",
"Incorrect password": "Palavra-chave incorreta",
"Incorrect password": "Palavra-passe incorreta",
"Wrong answer": "Resposta errada",
"Erroneous CAPTCHA": "CAPTCHA inválido",
"CAPTCHA is a required field": "CAPTCHA é um campo obrigatório",
"User ID is a required field": "O nome de utilizador é um campo obrigatório",
"Password is a required field": "Palavra-chave é um campo obrigatório",
"Wrong username or password": "Nome de utilizador ou palavra-chave incorreto",
"Password cannot be empty": "A palavra-chave não pode estar vazia",
"Password cannot be longer than 55 characters": "A palavra-chave não pode ser superior a 55 caracteres",
"Password is a required field": "Palavra-passe é um campo obrigatório",
"Wrong username or password": "Nome de utilizador ou palavra-passe incorreta",
"Password cannot be empty": "A palavra-passe não pode estar vazia",
"Password cannot be longer than 55 characters": "A palavra-passe não pode ter mais do que 55 caracteres",
"Please log in": "Por favor, inicie sessão",
"Invidious Private Feed for `x`": "Feed Privado do Invidious para `x`",
"channel:`x`": "canal:`x`",
@ -180,20 +181,20 @@
"Could not fetch comments": "Não foi possível obter os comentários",
"`x` ago": "`x` atrás",
"Load more": "Carregar mais",
"Could not create mix.": "Não foi possível criar a mistura.",
"Could not create mix.": "Não foi possível criar o mix.",
"Empty playlist": "Lista de reprodução vazia",
"Not a playlist.": "Não é uma lista de reprodução.",
"Playlist does not exist.": "A lista de reprodução não existe.",
"Could not pull trending pages.": "Não foi possível obter as páginas de tendências.",
"Could not pull trending pages.": "Não foi possível obter a página de tendências.",
"Hidden field \"challenge\" is a required field": "O campo oculto \"desafio\" é obrigatório",
"Hidden field \"token\" is a required field": "O campo oculto \"token\" é um campo obrigatório",
"Erroneous challenge": "Desafio inválido",
"Erroneous token": "Token inválido",
"No such user": "Utilizador inválido",
"Token is expired, please try again": "Token expirou, tente novamente",
"Token is expired, please try again": "Token caducado, tente novamente",
"English": "Inglês",
"English (auto-generated)": "Inglês (auto-gerado)",
"Afrikaans": "Africano",
"Afrikaans": "Africânder",
"Albanian": "Albanês",
"Amharic": "Amárico",
"Arabic": "Árabe",
@ -209,7 +210,7 @@
"Cebuano": "Cebuano",
"Chinese (Simplified)": "Chinês (simplificado)",
"Chinese (Traditional)": "Chinês (tradicional)",
"Corsican": "Corso",
"Corsican": "Córsego",
"Croatian": "Croata",
"Czech": "Checo",
"Danish": "Dinamarquês",
@ -252,7 +253,7 @@
"Macedonian": "Macedónio",
"Malagasy": "Malgaxe",
"Malay": "Malaio",
"Malayalam": "Malaiala",
"Malayalam": "Malaialaio",
"Maltese": "Maltês",
"Maori": "Maori",
"Marathi": "Marathi",
@ -297,30 +298,37 @@
"Yiddish": "Iídiche",
"Yoruba": "Ioruba",
"Zulu": "Zulu",
"generic_count_years": "{{count}} ano",
"generic_count_years_plural": "{{count}} anos",
"generic_count_months": "{{count}} s",
"generic_count_months_plural": "{{count}} meses",
"generic_count_weeks": "{{count}} seman",
"generic_count_weeks_plural": "{{count}} semanas",
"generic_count_days": "{{count}} dia",
"generic_count_days_plural": "{{count}} dias",
"generic_count_hours": "{{count}} hora",
"generic_count_hours_plural": "{{count}} horas",
"generic_count_minutes": "{{count}} minuto",
"generic_count_minutes_plural": "{{count}} minutos",
"generic_count_seconds": "{{count}} segundo",
"generic_count_seconds_plural": "{{count}} segundos",
"Fallback comments: ": "Comentários alternativos: ",
"generic_count_years_0": "{{count}} ano",
"generic_count_years_1": "{{count}} anos",
"generic_count_years_2": "{{count}} anos",
"generic_count_months_0": "{{count}} mês",
"generic_count_months_1": "{{count}} meses",
"generic_count_months_2": "{{count}} meses",
"generic_count_weeks_0": "{{count}} semana",
"generic_count_weeks_1": "{{count}} semanas",
"generic_count_weeks_2": "{{count}} semanas",
"generic_count_days_0": "{{count}} dia",
"generic_count_days_1": "{{count}} dias",
"generic_count_days_2": "{{count}} dias",
"generic_count_hours_0": "{{count}} hora",
"generic_count_hours_1": "{{count}} horas",
"generic_count_hours_2": "{{count}} horas",
"generic_count_minutes_0": "{{count}} minuto",
"generic_count_minutes_1": "{{count}} minutos",
"generic_count_minutes_2": "{{count}} minutos",
"generic_count_seconds_0": "{{count}} segundo",
"generic_count_seconds_1": "{{count}} segundos",
"generic_count_seconds_2": "{{count}} segundos",
"Fallback comments: ": "Alternativa para comentários: ",
"Popular": "Popular",
"Search": "Pesquisar",
"Top": "Destaques",
"About": "Sobre",
"About": "Acerca",
"Rating: ": "Avaliação: ",
"preferences_locale_label": "Idioma: ",
"View as playlist": "Ver como lista de reprodução",
"Default": "Predefinido",
"Music": "Música",
"Default": "Padrão",
"Music": "Músicas",
"Gaming": "Jogos",
"News": "Notícias",
"Movies": "Filmes",
@ -328,9 +336,9 @@
"Download as: ": "Descarregar como: ",
"%A %B %-d, %Y": "%A %B %-d, %Y",
"(edited)": "(editado)",
"YouTube comment permalink": "Hiperligação permanente do comentário no YouTube",
"permalink": "hiperligação permanente",
"`x` marked it with a ❤": "`x` foi marcado como ❤",
"YouTube comment permalink": "Ligação permanente do comentário no YouTube",
"permalink": "ligação permanente",
"`x` marked it with a ❤": "`x` foi marcado com um ❤",
"Audio mode": "Modo de áudio",
"Video mode": "Modo de vídeo",
"channel_tab_videos_label": "Vídeos",
@ -338,7 +346,7 @@
"channel_tab_community_label": "Comunidade",
"search_filters_sort_option_relevance": "Relevância",
"search_filters_sort_option_rating": "Avaliação",
"search_filters_sort_option_date": "Data de envio",
"search_filters_sort_option_date": "Data de carregamento",
"search_filters_sort_option_views": "Visualizações",
"search_filters_type_label": "Tipo",
"search_filters_duration_label": "Duração",
@ -353,38 +361,44 @@
"search_filters_type_option_channel": "Canal",
"search_filters_type_option_playlist": "Lista de reprodução",
"search_filters_type_option_movie": "Filme",
"search_filters_type_option_show": "Espetáculo",
"search_filters_type_option_show": "Séries",
"search_filters_features_option_hd": "HD",
"search_filters_features_option_subtitles": "Legendas",
"search_filters_features_option_c_commons": "Creative Commons",
"search_filters_features_option_three_d": "3D",
"search_filters_features_option_live": "Em direto",
"search_filters_features_option_live": "Direto",
"search_filters_features_option_four_k": "4K",
"search_filters_features_option_location": "Localização",
"search_filters_features_option_hdr": "HDR",
"Current version: ": "Versão atual: ",
"next_steps_error_message": "Pode tentar as seguintes opções: ",
"next_steps_error_message_refresh": "Atualizar",
"next_steps_error_message_go_to_youtube": "Ir ao YouTube",
"next_steps_error_message_refresh": "Recarregar",
"next_steps_error_message_go_to_youtube": "Ir para o YouTube",
"search_filters_title": "Filtro",
"generic_videos_count": "{{count}} vídeo",
"generic_videos_count_plural": "{{count}} vídeos",
"generic_playlists_count": "{{count}} lista de reprodução",
"generic_playlists_count_plural": "{{count}} listas de reprodução",
"generic_subscriptions_count": "{{count}} inscrição",
"generic_subscriptions_count_plural": "{{count}} inscrições",
"generic_views_count": "{{count}} visualização",
"generic_views_count_plural": "{{count}} visualizações",
"generic_subscribers_count": "{{count}} inscrito",
"generic_subscribers_count_plural": "{{count}} inscritos",
"generic_videos_count_0": "{{count}} vídeo",
"generic_videos_count_1": "{{count}} vídeos",
"generic_videos_count_2": "{{count}} vídeos",
"generic_playlists_count_0": "{{count}} lista de reprodução",
"generic_playlists_count_1": "{{count}} listas de reprodução",
"generic_playlists_count_2": "{{count}} listas de reprodução",
"generic_subscriptions_count_0": "{{count}} subscrição",
"generic_subscriptions_count_1": "{{count}} subscrições",
"generic_subscriptions_count_2": "{{count}} subscrições",
"generic_views_count_0": "{{count}} visualização",
"generic_views_count_1": "{{count}} visualizações",
"generic_views_count_2": "{{count}} visualizações",
"generic_subscribers_count_0": "{{count}} subscritor",
"generic_subscribers_count_1": "{{count}} subscritores",
"generic_subscribers_count_2": "{{count}} subscritores",
"preferences_quality_dash_option_4320p": "4320p",
"preferences_quality_dash_label": "Qualidade de vídeo DASH preferida: ",
"preferences_quality_dash_option_2160p": "2160p",
"subscriptions_unseen_notifs_count": "{{count}} notificação não vista",
"subscriptions_unseen_notifs_count_plural": "{{count}} notificações não vistas",
"subscriptions_unseen_notifs_count_0": "{{count}} notificação não vista",
"subscriptions_unseen_notifs_count_1": "{{count}} notificações não vistas",
"subscriptions_unseen_notifs_count_2": "{{count}} notificações não vistas",
"Popular enabled: ": "Página \"popular\" ativada: ",
"search_message_no_results": "Nenhum resultado encontrado.",
"preferences_quality_dash_option_auto": "Automático",
"preferences_quality_dash_option_auto": "Automática",
"preferences_region_label": "País do conteúdo: ",
"preferences_quality_dash_option_1440p": "1440p",
"preferences_quality_dash_option_720p": "720p",
@ -403,10 +417,12 @@
"preferences_quality_dash_option_240p": "240p",
"Video unavailable": "Vídeo não disponível",
"Russian (auto-generated)": "Russo (gerado automaticamente)",
"comments_view_x_replies": "Ver {{count}} resposta",
"comments_view_x_replies_plural": "Ver {{count}} respostas",
"comments_points_count": "{{count}} ponto",
"comments_points_count_plural": "{{count}} pontos",
"comments_view_x_replies_0": "Ver {{count}} resposta",
"comments_view_x_replies_1": "Ver {{count}} respostas",
"comments_view_x_replies_2": "Ver {{count}} respostas",
"comments_points_count_0": "{{count}} ponto",
"comments_points_count_1": "{{count}} pontos",
"comments_points_count_2": "{{count}} pontos",
"English (United Kingdom)": "Inglês (Reino Unido)",
"Chinese (Hong Kong)": "Chinês (Hong Kong)",
"Chinese (Taiwan)": "Chinês (Taiwan)",
@ -432,13 +448,13 @@
"videoinfo_watch_on_youTube": "Ver no YouTube",
"videoinfo_youTube_embed_link": "Incorporar",
"adminprefs_modified_source_code_url_label": "URL do repositório do código-fonte alterado",
"videoinfo_invidious_embed_link": "Incorporar hiperligação",
"videoinfo_invidious_embed_link": "Incorporar ligação",
"none": "nenhum",
"videoinfo_started_streaming_x_ago": "Iniciou a transmissão há `x`",
"download_subtitles": "Legendas - `x` (.vtt)",
"user_created_playlists": "`x` listas de reprodução criadas",
"user_saved_playlists": "`x` listas de reprodução guardadas",
"preferences_save_player_pos_label": "Guardar a posição de reprodução atual do vídeo: ",
"preferences_save_player_pos_label": "Guardar posição de reprodução: ",
"Turkish (auto-generated)": "Turco (gerado automaticamente)",
"Cantonese (Hong Kong)": "Cantonês (Hong Kong)",
"Chinese (China)": "Chinês (China)",
@ -455,21 +471,52 @@
"search_filters_date_option_none": "Qualquer data",
"search_filters_features_option_three_sixty": "360°",
"search_filters_features_option_vr180": "VR180",
"search_message_use_another_instance": " Também pode <a href=\"`x`\">pesquisar noutra instância</a>.",
"search_message_use_another_instance": "Também pode <a href=\"`x`\">pesquisar noutra instância</a>.",
"crash_page_you_found_a_bug": "Parece que encontrou um erro no Invidious!",
"crash_page_before_reporting": "Antes de reportar um erro, verifique se:",
"crash_page_read_the_faq": "leia as <a href=\"`x`\">Perguntas frequentes (FAQ)</a>",
"crash_page_read_the_faq": "leu as <a href=\"`x`\">Perguntas frequentes (FAQ)</a>",
"crash_page_search_issue": "procurou se <a href=\"`x`\">o erro já foi reportado no GitHub</a>",
"crash_page_report_issue": "Se nenhuma opção acima ajudou, por favor <a href=\"`x`\">abra um novo problema no Github</a> (preferencialmente em inglês) e inclua o seguinte texto tal qual (NÃO o traduza):",
"crash_page_report_issue": "Se nenhuma opção acima ajudou, por favor <a href=\"`x`\">abra um novo problema no Github</a> (preferencialmente em inglês) e inclua o seguinte texto (NÃO o traduza):",
"search_message_change_filters_or_query": "Tente alargar os termos genéricos da pesquisa e/ou alterar os filtros.",
"crash_page_refresh": "tentou <a href=\"`x`\">recarregar a página</a>",
"crash_page_switch_instance": "tentou <a href=\"`x`\">usar outra instância</a>",
"error_video_not_in_playlist": "O vídeo pedido não existe nesta lista de reprodução. <a href=\"`x`\">Clique aqui para a página inicial da lista de reprodução.</a>",
"error_video_not_in_playlist": "O vídeo pedido não existe nesta lista de reprodução. <a href=\"`x`\">Clique aqui para voltar à página inicial da lista de reprodução.</a>",
"Artist: ": "Artista: ",
"Album: ": "Álbum: ",
"channel_tab_streams_label": "Diretos",
"channel_tab_streams_label": "Emissões em direto",
"channel_tab_playlists_label": "Listas de reprodução",
"channel_tab_channels_label": "Canais",
"Music in this video": "Música neste vídeo",
"channel_tab_shorts_label": "Curtos"
"channel_tab_shorts_label": "Curtos",
"generic_button_delete": "Eliminar",
"generic_button_edit": "Editar",
"generic_button_save": "Guardar",
"generic_button_cancel": "Cancelar",
"Import YouTube playlist (.csv)": "Importar lista de reprodução do YouTube (.csv)",
"Song: ": "Canção: ",
"Answer": "Responder",
"The Popular feed has been disabled by the administrator.": "O feed Popular foi desativado por um administrador.",
"Channel Sponsor": "Patrocinador do canal",
"Download is disabled": "A descarga está desativada",
"Add to playlist": "Adicionar à lista de reprodução",
"Add to playlist: ": "Adicionar à lista de reprodução: ",
"Search for videos": "Procurar vídeos",
"generic_channels_count_0": "{{count}} canal",
"generic_channels_count_1": "{{count}} canais",
"generic_channels_count_2": "{{count}} canais",
"generic_button_rss": "RSS",
"Import YouTube watch history (.json)": "Importar histórico de reprodução do YouTube (.json)",
"preferences_preload_label": "Pré-carregamento dos dados: ",
"playlist_button_add_items": "Adicionar vídeos",
"channel_tab_podcasts_label": "Podcasts",
"channel_tab_releases_label": "Lançamentos",
"carousel_slide": "Diapositivo {{current}} de{{total}}",
"carousel_skip": "Ignorar carrossel",
"carousel_go_to": "Ir para o diapositivo`x`",
"First page": "Primeira página",
"Standard YouTube license": "Licença padrão do YouTube",
"Filipino (auto-generated)": "Filipino (gerado automaticamente)",
"channel_tab_courses_label": "Cursos",
"channel_tab_posts_label": "Publicações",
"toggle_theme": "Trocar tema"
}

View File

@ -515,5 +515,8 @@
"carousel_go_to": "Ir para o diapositivo`x`",
"The Popular feed has been disabled by the administrator.": "O feed Popular foi desativado por um administrador.",
"preferences_preload_label": "Pré-carregamento dos dados: ",
"Filipino (auto-generated)": "Filipino (gerado automaticamente)"
"Filipino (auto-generated)": "Filipino (gerado automaticamente)",
"First page": "Primeira página",
"channel_tab_courses_label": "Cursos",
"channel_tab_posts_label": "Publicações"
}

View File

@ -475,7 +475,7 @@
"search_filters_date_option_none": "Любая дата",
"search_filters_date_label": "Дата загрузки",
"search_message_no_results": "Ничего не найдено.",
"search_message_use_another_instance": " Дополнительно вы можете <a href=\"`x`\">поискать на других зеркалах</a>.",
"search_message_use_another_instance": "Дополнительно вы можете <a href=\"`x`\">поискать на других зеркалах</a>.",
"search_filters_features_option_vr180": "VR180",
"search_message_change_filters_or_query": "Попробуйте расширить поисковый запрос и/или изменить фильтры.",
"search_filters_duration_option_medium": "Средние (4 - 20 минут)",
@ -515,5 +515,7 @@
"carousel_slide": "Пролистано {{current}} из {{total}}",
"carousel_skip": "Пропустить всё",
"carousel_go_to": "Перейти к странице `x`",
"preferences_preload_label": "Предзагрузка видеоданных: "
"preferences_preload_label": "Предзагрузка видеоданных: ",
"channel_tab_courses_label": "Курсы",
"channel_tab_posts_label": "Записи"
}

View File

@ -494,5 +494,9 @@
"carousel_slide": "Diapozitiv {{current}} nga {{total}}",
"carousel_go_to": "Kalo te diapozitivi `x`",
"Filipino (auto-generated)": "Filipineze (të prodhuara automatikisht)",
"preferences_preload_label": "Parangarko të dhëna videoje: "
"preferences_preload_label": "Parangarko të dhëna videoje: ",
"toggle_theme": "Ndërroni Temë",
"channel_tab_courses_label": "Kurse",
"channel_tab_posts_label": "Postime",
"First page": "Faqja e parë"
}

View File

@ -513,7 +513,10 @@
"Answer": "Odgovor",
"Search for videos": "Pretražite video snimke",
"carousel_skip": "Preskoči karusel",
"toggle_theme": "Подеси тему",
"toggle_theme": "Podesi temu",
"preferences_preload_label": "Unapred učitaj podatke o video snimku: ",
"Filipino (auto-generated)": "Filipinski (automatski generisano)"
"Filipino (auto-generated)": "Filipinski (automatski generisano)",
"channel_tab_posts_label": "Objave",
"First page": "Prva stranica",
"channel_tab_courses_label": "Kursevi"
}

View File

@ -515,5 +515,8 @@
"The Popular feed has been disabled by the administrator.": "Администратор је онемогућио фид „Популарно“.",
"carousel_slide": "Слајд {{current}} од {{total}}",
"preferences_preload_label": "Унапред учитај податке о видео снимку: ",
"Filipino (auto-generated)": "Филипински (аутоматски генерисано)"
"Filipino (auto-generated)": "Филипински (аутоматски генерисано)",
"channel_tab_courses_label": "Курсеви",
"First page": "Прва страница",
"channel_tab_posts_label": "Објаве"
}

View File

@ -498,5 +498,8 @@
"carousel_skip": "Hoppa över karusellen",
"carousel_go_to": "Gå till bildspel `x`",
"preferences_preload_label": "Förladda video data: ",
"Filipino (auto-generated)": "Filippinska (auto-genererad)"
"Filipino (auto-generated)": "Filippinska (auto-genererad)",
"First page": "Första sidan",
"channel_tab_courses_label": "Kurser",
"channel_tab_posts_label": "Inlägg"
}

View File

@ -497,5 +497,9 @@
"carousel_skip": "Kayar menüyü atla",
"carousel_go_to": "`x` sunumuna git",
"The Popular feed has been disabled by the administrator.": "Popüler akışı yönetici tarafından devre dışı bırakıldı.",
"preferences_preload_label": "Video verilerini önceden yükle: "
"preferences_preload_label": "Video verilerini önceden yükle: ",
"First page": "İlk sayfa",
"Filipino (auto-generated)": "Filipince (oto-oluşturuldu)",
"channel_tab_courses_label": "Kurslar",
"channel_tab_posts_label": "Yazılar"
}

View File

@ -515,5 +515,8 @@
"carousel_skip": "Пропустити карусель",
"carousel_go_to": "Перейти до слайда `x`",
"preferences_preload_label": "Попереднє завантаження відеоданих: ",
"Filipino (auto-generated)": "Філіппінська (згенеровано автоматично)"
"Filipino (auto-generated)": "Філіппінська (згенеровано автоматично)",
"First page": "Перша сторінка",
"channel_tab_courses_label": "Курси",
"channel_tab_posts_label": "Дописи"
}

View File

@ -314,11 +314,11 @@
"search_filters_duration_label": "Thời lượng",
"search_filters_features_label": "Đặc điểm",
"search_filters_sort_label": "Sắp xếp theo",
"search_filters_date_option_hour": "Một giờ qua",
"search_filters_date_option_hour": "Một giờ trước",
"search_filters_date_option_today": "Hôm nay",
"search_filters_date_option_week": "Tuần này",
"search_filters_date_option_month": "Tháng này",
"search_filters_date_option_year": "Năm này",
"search_filters_date_option_year": "Năm nay",
"search_filters_type_option_video": "video",
"search_filters_type_option_channel": "Kênh",
"search_filters_type_option_playlist": "Danh sách phát",
@ -479,5 +479,8 @@
"carousel_skip": "Bỏ qua Carousel",
"carousel_go_to": "Đi tới trang `x`",
"Search for videos": "Tìm kiếm video",
"The Popular feed has been disabled by the administrator.": "Bảng tin phổ biến đã bị tắt bởi ban quản lý."
"The Popular feed has been disabled by the administrator.": "Bảng tin phổ biến đã bị tắt bởi ban quản lý.",
"preferences_preload_label": "Tải trước dữ liệu video: ",
"Filipino (auto-generated)": "Tiếng Philippines (tự động tạo)",
"First page": "Trang đầu"
}

View File

@ -420,7 +420,7 @@
"Chinese": "中文",
"Chinese (China)": "中文 (中国)",
"Chinese (Hong Kong)": "中文 (中国香港)",
"Chinese (Taiwan)": "中文 (中国台湾)",
"Chinese (Taiwan)": "中文 (台湾)",
"German (auto-generated)": "德语 (自动生成)",
"Indonesian (auto-generated)": "印尼语 (自动生成)",
"Interlingue": "国际语",
@ -481,5 +481,8 @@
"carousel_skip": "跳过图集",
"carousel_go_to": "转到图 `x`",
"preferences_preload_label": "预加载视频数据: ",
"Filipino (auto-generated)": "菲律宾语 (自动生成)"
"Filipino (auto-generated)": "菲律宾语 (自动生成)",
"channel_tab_posts_label": "帖子",
"First page": "第一页",
"channel_tab_courses_label": "课程"
}

View File

@ -481,5 +481,8 @@
"carousel_go_to": "跳到投影片 `x`",
"The Popular feed has been disabled by the administrator.": "熱門 feed 已被管理員停用。",
"preferences_preload_label": "預先載入影片資訊 ",
"Filipino (auto-generated)": "菲律賓語(自動產生)"
"Filipino (auto-generated)": "菲律賓語(自動產生)",
"channel_tab_courses_label": "課程",
"First page": "第一頁",
"channel_tab_posts_label": "貼文"
}

View File

@ -0,0 +1,56 @@
# This file automatically generates Crystal strings of rows within an HTML Javascript licenses table
#
# These strings will then be placed within a `<%= %>` statement in licenses.ecr at compile time which
# will be interpolated at run-time. This interpolation is only for the translation of the "source" string
# so maybe we can just switch to a non-translated string to simplify the logic here.
#
# The Javascript Web Labels table defined at https://www.gnu.org/software/librejs/free-your-javascript.html#step3
# for example just reiterates the name of the source file rather than use a "source" string.
all_javascript_files = Dir.glob("assets/**/*.js")
videojs_js = [] of String
invidious_js = [] of String
all_javascript_files.each do |js_path|
if js_path.starts_with?("assets/videojs/")
videojs_js << js_path[7..]
else
invidious_js << js_path[7..]
end
end
def create_licence_tr(path, file_name, licence_name, licence_link, source_location)
tr = <<-HTML
"<tr>
<td><a href=\\"/#{path}\\">#{file_name}</a></td>
<td><a href=\\"#{licence_link}\\">#{licence_name}</a></td>
<td><a href=\\"#{source_location}\\">\#{translate(locale, "source")}</a></td>
</tr>"
HTML
# New lines are removed as to allow for using String.join and StringLiteral.split
# to get a clean list of each table row.
tr.gsub('\n', "")
end
# TODO Use videojs-dependencies.yml to generate license info for videojs javascript
jslicence_table_rows = [] of String
invidious_js.each do |path|
file_name = path.split('/')[-1]
# A couple non Invidious JS files are also shipped alongside Invidious due to various reasons
next if {
"sse.js", "silvermine-videojs-quality-selector.min.js", "videojs-youtube-annotations.min.js",
}.includes?(file_name)
jslicence_table_rows << create_licence_tr(
path: path,
file_name: file_name,
licence_name: "AGPL-3.0",
licence_link: "https://www.gnu.org/licenses/agpl-3.0.html",
source_location: path
)
end
puts jslicence_table_rows.join("\n")

View File

@ -18,7 +18,7 @@ shards:
exception_page:
git: https://github.com/crystal-loot/exception_page.git
version: 0.2.2
version: 0.4.1
http_proxy:
git: https://github.com/mamantoha/http_proxy.git
@ -26,11 +26,7 @@ shards:
kemal:
git: https://github.com/kemalcr/kemal.git
version: 1.1.2
kilt:
git: https://github.com/jeromegn/kilt.git
version: 0.6.1
version: 1.6.0
pg:
git: https://github.com/will/crystal-pg.git

View File

@ -1,5 +1,5 @@
name: invidious
version: 2.20250314.0-dev
version: 2.20250517.0-dev
authors:
- Invidious team <contact@invidious.io>
@ -17,10 +17,7 @@ dependencies:
version: ~> 0.21.0
kemal:
github: kemalcr/kemal
version: ~> 1.1.2
kilt:
github: jeromegn/kilt
version: ~> 0.6.1
version: ~> 1.6.0
protodec:
github: iv-org/protodec
version: ~> 0.1.5

View File

@ -1,16 +0,0 @@
# Overrides for Kemal's `content_for` macro in order to keep using
# kilt as it was before Kemal v1.1.1 (Kemal PR #618).
require "kemal"
require "kilt"
macro content_for(key, file = __FILE__)
%proc = ->() {
__kilt_io__ = IO::Memory.new
{{ yield }}
__kilt_io__.to_s
}
CONTENT_FOR_BLOCKS[{{key}}] = Tuple.new {{file}}, %proc
nil
end

View File

@ -71,7 +71,7 @@ def send_file(env : HTTP::Server::Context, file_path : String, data : Slice(UInt
filesize = data.bytesize
attachment(env, filename, disposition)
Kemal.config.static_headers.try(&.call(env.response, file_path, filestat))
Kemal.config.static_headers.try(&.call(env, file_path, filestat))
file = IO::Memory.new(data)
if env.request.method == "GET" && env.request.headers.has_key?("Range")

View File

@ -17,10 +17,8 @@
require "digest/md5"
require "file_utils"
# Require kemal, kilt, then our own overrides
# Require kemal, then our own overrides
require "kemal"
require "kilt"
require "./ext/kemal_content_for.cr"
require "./ext/kemal_static_file_handler.cr"
require "http_proxy"
@ -49,7 +47,8 @@ require "./invidious/channels/*"
require "./invidious/user/*"
require "./invidious/search/*"
require "./invidious/routes/**"
require "./invidious/jobs/**"
require "./invidious/jobs/base_job"
require "./invidious/jobs/*"
# Declare the base namespace for invidious
module Invidious
@ -226,8 +225,8 @@ error 500 do |env, ex|
error_template(500, ex)
end
static_headers do |response|
response.headers.add("Cache-Control", "max-age=2629800")
static_headers do |env|
env.response.headers.add("Cache-Control", "max-age=2629800")
end
# Init Kemal

View File

@ -91,7 +91,7 @@ module Invidious::Database::Playlists
end
# -------------------
# Salect
# Select
# -------------------
def select(*, id : String) : InvidiousPlaylist?
@ -113,7 +113,7 @@ module Invidious::Database::Playlists
end
# -------------------
# Salect (filtered)
# Select (filtered)
# -------------------
def select_like_iv(email : String) : Array(InvidiousPlaylist)
@ -213,7 +213,7 @@ module Invidious::Database::PlaylistVideos
end
# -------------------
# Salect
# Select
# -------------------
def select(plid : String, index : VideoIndex, offset, limit = 100) : Array(PlaylistVideo)

View File

@ -18,16 +18,7 @@ def github_details(summary : String, content : String)
return HTML.escape(details)
end
def error_template_helper(env : HTTP::Server::Context, status_code : Int32, exception : Exception)
if exception.is_a?(InfoException)
return error_template_helper(env, status_code, exception.message || "")
end
locale = env.get("preferences").as(Preferences).locale
env.response.content_type = "text/html"
env.response.status_code = status_code
def get_issue_template(env : HTTP::Server::Context, exception : Exception) : Tuple(String, String)
issue_title = "#{exception.message} (#{exception.class})"
issue_template = <<-TEXT
@ -40,6 +31,24 @@ def error_template_helper(env : HTTP::Server::Context, status_code : Int32, exce
issue_template += github_details("Backtrace", exception.inspect_with_backtrace)
return issue_title, issue_template
end
def error_template_helper(env : HTTP::Server::Context, status_code : Int32, exception : Exception)
if exception.is_a?(InfoException)
return error_template_helper(env, status_code, exception.message || "")
end
locale = env.get("preferences").as(Preferences).locale
env.response.content_type = "text/html"
env.response.status_code = status_code
# Unpacking into issue_title, issue_template directly causes a compiler error
# I have no idea why.
issue_template_components = get_issue_template(env, exception)
issue_title, issue_template = issue_template_components
# URLs for the error message below
url_faq = "https://github.com/iv-org/documentation/blob/master/docs/faq.md"
url_search_issues = "https://github.com/iv-org/invidious/issues"
@ -69,7 +78,7 @@ def error_template_helper(env : HTTP::Server::Context, status_code : Int32, exce
<p>#{translate(locale, "crash_page_report_issue", url_new_issue)}</p>
<!-- TODO: Add a "copy to clipboard" button -->
<pre style="padding: 20px; background: rgba(0, 0, 0, 0.12345);">#{issue_template}</pre>
<pre class="error-issue-template">#{issue_template}</pre>
</div>
END_HTML

View File

@ -55,12 +55,11 @@ macro templated(_filename, template = "template", navbar_search = true)
{{ layout = "src/invidious/views/" + template + ".ecr" }}
__content_filename__ = {{filename}}
content = Kilt.render({{filename}})
Kilt.render({{layout}})
render {{filename}}, {{layout}}
end
macro rendered(filename)
Kilt.render("src/invidious/views/#{{{filename}}}.ecr")
render("src/invidious/views/#{{{filename}}}.ecr")
end
# Similar to Kemals halt method but works in a

View File

@ -291,6 +291,55 @@ struct SearchHashtag
end
end
# A `ProblematicTimelineItem` is a `SearchItem` created by Invidious that
# represents an item that caused an exception during parsing.
#
# This is not a parsed object from YouTube but rather an Invidious-only type
# created to gracefully communicate parse errors without throwing away
# the rest of the (hopefully) successfully parsed item on a page.
struct ProblematicTimelineItem
property parse_exception : Exception
property id : String
def initialize(@parse_exception)
@id = Random.new.hex(8)
end
def to_json(locale : String?, json : JSON::Builder)
json.object do
json.field "type", "parse-error"
json.field "errorMessage", @parse_exception.message
json.field "errorBacktrace", @parse_exception.inspect_with_backtrace
end
end
# Provides compatibility with PlaylistVideo
def to_json(json : JSON::Builder, *args, **kwargs)
return to_json("", json)
end
def to_xml(env, locale, xml : XML::Builder)
xml.element("entry") do
xml.element("id") { xml.text "iv-err-#{@id}" }
xml.element("title") { xml.text "Parse Error: This item has failed to parse" }
xml.element("updated") { xml.text Time.utc.to_rfc3339 }
xml.element("content", type: "xhtml") do
xml.element("div", xmlns: "http://www.w3.org/1999/xhtml") do
xml.element("div") do
xml.element("h4") { translate(locale, "timeline_parse_error_placeholder_heading") }
xml.element("p") { translate(locale, "timeline_parse_error_placeholder_message") }
end
xml.element("pre") do
get_issue_template(env, @parse_exception)
end
end
end
end
end
end
class Category
include DB::Serializable
@ -333,4 +382,4 @@ struct Continuation
end
end
alias SearchItem = SearchVideo | SearchChannel | SearchPlaylist | SearchHashtag | Category
alias SearchItem = SearchVideo | SearchChannel | SearchPlaylist | SearchHashtag | Category | ProblematicTimelineItem

View File

@ -262,7 +262,7 @@ def get_referer(env, fallback = "/", unroll = true)
end
referer = referer.request_target
referer = "/" + referer.gsub(/[^\/?@&%=\-_.:,*0-9a-zA-Z]/, "").lstrip("/\\")
referer = "/" + referer.gsub(/[^\/?@&%=\-_.:,*0-9a-zA-Z+]/, "").lstrip("/\\")
if referer == env.request.path
referer = fallback

View File

@ -432,7 +432,7 @@ def get_playlist_videos(playlist : InvidiousPlaylist | Playlist, offset : Int32,
offset = initial_data.dig?("contents", "twoColumnWatchNextResults", "playlist", "playlist", "currentIndex").try &.as_i || offset
end
videos = [] of PlaylistVideo
videos = [] of PlaylistVideo | ProblematicTimelineItem
until videos.size >= 200 || videos.size == playlist.video_count || offset >= playlist.video_count
# 100 videos per request
@ -448,7 +448,7 @@ def get_playlist_videos(playlist : InvidiousPlaylist | Playlist, offset : Int32,
end
def extract_playlist_videos(initial_data : Hash(String, JSON::Any))
videos = [] of PlaylistVideo
videos = [] of PlaylistVideo | ProblematicTimelineItem
if initial_data["contents"]?
tabs = initial_data["contents"]["twoColumnBrowseResultsRenderer"]["tabs"]
@ -500,6 +500,8 @@ def extract_playlist_videos(initial_data : Hash(String, JSON::Any))
index: index,
})
end
rescue ex
videos << ProblematicTimelineItem.new(parse_exception: ex)
end
return videos

View File

@ -20,14 +20,6 @@ module Invidious::Routes::BeforeAll
env.response.headers["X-XSS-Protection"] = "1; mode=block"
env.response.headers["X-Content-Type-Options"] = "nosniff"
# 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 https://*.youtube.com:443"
else
extra_media_csp = ""
end
# Only allow the pages at /embed/* to be embedded
if env.request.resource.starts_with?("/embed")
frame_ancestors = "'self' file: http: https:"
@ -45,7 +37,7 @@ module Invidious::Routes::BeforeAll
"font-src 'self' data:",
"connect-src 'self'",
"manifest-src 'self'",
"media-src 'self' blob:" + extra_media_csp,
"media-src 'self' blob:",
"child-src 'self' blob:",
"frame-src 'self'",
"frame-ancestors " + frame_ancestors,
@ -110,6 +102,21 @@ module Invidious::Routes::BeforeAll
preferences.locale = locale
env.set "preferences", preferences
# Allow media resources to be loaded from google servers
# TODO: check if *.youtube.com can be removed
#
# `!preferences.local` has to be checked after setting and
# reading `preferences` from the "PREFS" cookie and
# saved user preferences from the database, otherwise
# `https://*.googlevideo.com:443 https://*.youtube.com:443`
# will not be set in the CSP header if
# `default_user_preferences.local` is set to true on the
# configuration file, causing preference “Proxy Videos”
# not to work while having it disabled and using medium quality.
if CONFIG.disabled?("local") || !preferences.local
env.response.headers["Content-Security-Policy"] = env.response.headers["Content-Security-Policy"].gsub("media-src", "media-src https://*.googlevideo.com:443 https://*.youtube.com:443")
end
current_page = env.request.path
if env.request.query
query = HTTP::Params.parse(env.request.query.not_nil!)

View File

@ -12,13 +12,15 @@ module Invidious::Routes::Embed
url = "/playlist?list=#{plid}"
raise NotFoundException.new(translate(locale, "error_video_not_in_playlist", url))
end
first_playlist_video = videos[0].as(PlaylistVideo)
rescue ex : NotFoundException
return error_template(404, ex)
rescue ex
return error_template(500, ex)
end
url = "/embed/#{videos[0].id}?#{env.params.query}"
url = "/embed/#{first_playlist_video}?#{env.params.query}"
if env.params.query.size > 0
url += "?#{env.params.query}"
@ -72,13 +74,15 @@ module Invidious::Routes::Embed
url = "/playlist?list=#{plid}"
raise NotFoundException.new(translate(locale, "error_video_not_in_playlist", url))
end
first_playlist_video = videos[0].as(PlaylistVideo)
rescue ex : NotFoundException
return error_template(404, ex)
rescue ex
return error_template(500, ex)
end
url = "/embed/#{videos[0].id}"
url = "/embed/#{first_playlist_video.id}"
elsif video_series
url = "/embed/#{video_series.shift}"
env.params.query["playlist"] = video_series.join(",")

View File

@ -202,7 +202,7 @@ module Invidious::Routes::Feeds
xml.element("link", rel: "self", href: "#{HOST_URL}#{env.request.resource}")
xml.element("id") { xml.text "yt:channel:#{ucid}" }
xml.element("yt:channelId") { xml.text ucid }
xml.element("title") { author }
xml.element("title") { xml.text author }
xml.element("link", rel: "alternate", href: "#{HOST_URL}/channel/#{ucid}")
xml.element("author") do
@ -296,7 +296,13 @@ module Invidious::Routes::Feeds
xml.element("name") { xml.text playlist.author }
end
videos.each &.to_xml(xml)
videos.each do |video|
if video.is_a? PlaylistVideo
video.to_xml(xml)
else
video.to_xml(env, locale, xml)
end
end
end
end
else

View File

@ -21,9 +21,6 @@ module Invidious::Routes::Login
account_type = env.params.query["type"]?
account_type ||= "invidious"
captcha_type = env.params.query["captcha"]?
captcha_type ||= "image"
templated "user/login"
end
@ -88,34 +85,14 @@ module Invidious::Routes::Login
password = password.byte_slice(0, 55)
if CONFIG.captcha_enabled
captcha_type = env.params.body["captcha_type"]?
answer = env.params.body["answer"]?
change_type = env.params.body["change_type"]?
if !captcha_type || change_type
if change_type
captcha_type = change_type
end
captcha_type ||= "image"
account_type = "invidious"
if captcha_type == "image"
captcha = Invidious::User::Captcha.generate_image(HMAC_KEY)
else
captcha = Invidious::User::Captcha.generate_text(HMAC_KEY)
end
return templated "user/login"
end
account_type = "invidious"
captcha = Invidious::User::Captcha.generate_image(HMAC_KEY)
tokens = env.params.body.select { |k, _| k.match(/^token\[\d+\]$/) }.map { |_, v| v }
answer ||= ""
captcha_type ||= "image"
case captcha_type
when "image"
if answer
answer = answer.lstrip('0')
answer = OpenSSL::HMAC.hexdigest(:sha256, HMAC_KEY, answer)
@ -124,27 +101,8 @@ module Invidious::Routes::Login
rescue ex
return error_template(400, ex)
end
else # "text"
answer = Digest::MD5.hexdigest(answer.downcase.strip)
if tokens.empty?
return error_template(500, "Erroneous CAPTCHA")
end
found_valid_captcha = false
error_exception = Exception.new
tokens.each do |tok|
begin
validate_request(tok, answer, env.request, HMAC_KEY, locale)
found_valid_captcha = true
rescue ex
error_exception = ex
end
end
if !found_valid_captcha
return error_template(500, error_exception)
end
else
return templated "user/login"
end
end

View File

@ -58,7 +58,11 @@ module Invidious::Routes::Search
end
begin
items = query.process
if user
items = query.process(user.as(User))
else
items = query.process
end
rescue ex : ChannelSearchException
return error_template(404, "Unable to find channel with id of '#{HTML.escape(ex.channel)}'. Are you sure that's an actual channel id? It should look like 'UC4QobU6STFB0P71PMvOGN5A'.")
rescue ex

View File

@ -31,12 +31,12 @@ def fetch_trending(trending_type, region, locale)
# See: https://github.com/iv-org/invidious/issues/2989
next if (itm.contents.size < 24 && deduplicate)
extracted.concat extract_category(itm)
extracted.concat itm.contents.select(SearchItem)
else
extracted << itm
end
end
# Deduplicate items before returning results
return extracted.select(SearchVideo).uniq!(&.id), plid
return extracted.select(SearchVideo | ProblematicTimelineItem).uniq!(&.id), plid
end

View File

@ -4,8 +4,6 @@ struct Invidious::User
module Captcha
extend self
private TEXTCAPTCHA_URL = URI.parse("https://textcaptcha.com")
def generate_image(key)
second = Random::Secure.rand(12)
second_angle = second * 30
@ -60,19 +58,5 @@ struct Invidious::User
tokens: {generate_response(answer, {":login"}, key, use_nonce: true)},
}
end
def generate_text(key)
response = make_client(TEXTCAPTCHA_URL, &.get("/github.com/iv.org/invidious.json").body)
response = JSON.parse(response)
tokens = response["a"].as_a.map do |answer|
generate_response(answer.as_s, {":login"}, key, use_nonce: true)
end
return {
question: response["q"].as_s,
tokens: tokens,
}
end
end
end

View File

@ -82,7 +82,7 @@ def extract_video_info(video_id : String)
"reason" => JSON::Any.new(reason),
}
end
elsif video_id != player_response.dig("videoDetails", "videoId")
elsif video_id != player_response.dig?("videoDetails", "videoId")
# YouTube may return a different video player response than expected.
# See: https://github.com/TeamNewPipe/NewPipe/issues/8713
# Line to be reverted if one day we solve the video not available issue.
@ -109,21 +109,33 @@ def extract_video_info(video_id : String)
params["reason"] = JSON::Any.new(reason) if reason
if !CONFIG.invidious_companion.present?
if player_response["streamingData"]? && player_response.dig?("streamingData", "adaptiveFormats", 0, "url").nil?
if player_response.dig?("streamingData", "adaptiveFormats", 0, "url").nil?
LOGGER.warn("Missing URLs for adaptive formats, falling back to other YT clients.")
players_fallback = [YoutubeAPI::ClientType::TvHtml5, YoutubeAPI::ClientType::WebMobile]
players_fallback = {YoutubeAPI::ClientType::TvHtml5, YoutubeAPI::ClientType::WebMobile}
players_fallback.each do |player_fallback|
client_config.client_type = player_fallback
player_fallback_response = try_fetch_streaming_data(video_id, client_config)
if player_fallback_response && player_fallback_response["streamingData"]? &&
player_fallback_response.dig?("streamingData", "adaptiveFormats", 0, "url")
next if !(player_fallback_response = try_fetch_streaming_data(video_id, client_config))
if player_fallback_response.dig?("streamingData", "adaptiveFormats", 0, "url")
streaming_data = player_response["streamingData"].as_h
streaming_data["adaptiveFormats"] = player_fallback_response["streamingData"]["adaptiveFormats"]
player_response["streamingData"] = JSON::Any.new(streaming_data)
break
end
rescue InfoException
next LOGGER.warn("Failed to fetch streams with #{player_fallback}")
end
end
# Seems like video page can still render even without playable streams.
# its better than nothing.
#
# # Were we able to find playable video streams?
# if player_response.dig?("streamingData", "adaptiveFormats", 0, "url").nil?
# # No :(
# end
end
{"captions", "playabilityStatus", "playerConfig", "storyboards"}.each do |f|
@ -154,7 +166,7 @@ def try_fetch_streaming_data(id : String, client_config : YoutubeAPI::ClientConf
playability_status = response["playabilityStatus"]["status"]
LOGGER.debug("try_fetch_streaming_data: [#{id}] Got playabilityStatus == #{playability_status}.")
if id != response.dig("videoDetails", "videoId")
if id != response.dig?("videoDetails", "videoId")
# YouTube may return a different video player response than expected.
# See: https://github.com/TeamNewPipe/NewPipe/issues/8713
raise InfoException.new(

View File

@ -1,6 +1,6 @@
<%-
thin_mode = env.get("preferences").as(Preferences).thin_mode
item_watched = !item.is_a?(SearchChannel | SearchHashtag | SearchPlaylist | InvidiousPlaylist | Category) && env.get?("user").try &.as(User).watched.index(item.id) != nil
item_watched = !item.is_a?(SearchChannel | SearchHashtag | SearchPlaylist | InvidiousPlaylist | Category | ProblematicTimelineItem) && env.get?("user").try &.as(User).watched.index(item.id) != nil
author_verified = item.responds_to?(:author_verified) && item.author_verified
-%>
@ -97,6 +97,18 @@
</div>
</div>
<% when Category %>
<% when ProblematicTimelineItem %>
<div class="error-card">
<div class="explanation">
<i class="icon ion-ios-alert"></i>
<h4><%=translate(locale, "timeline_parse_error_placeholder_heading")%></h4>
<p><%=translate(locale, "timeline_parse_error_placeholder_message")%></p>
</div>
<details>
<summary class="pure-button pure-button-secondary"><%=translate(locale, "timeline_parse_error_show_technical_details")%></summary>
<pre class="error-issue-template"><%=get_issue_template(env, item.parse_exception)[1]%></pre>
</details>
</div>
<% else %>
<%-
# `endpoint_params` is used for the "video-context-buttons" component

View File

@ -9,90 +9,6 @@
<body>
<h1><%= translate(locale, "JavaScript license information") %></h1>
<table id="jslicense-labels1">
<tr>
<td>
<a href="/js/_helpers.js?v=<%= ASSET_COMMIT %>">_helpers.js</a>
</td>
<td>
<a href="https://www.gnu.org/licenses/agpl-3.0.html">AGPL-3.0</a>
</td>
<td>
<a href="/js/_helpers.js?v=<%= ASSET_COMMIT %>"><%= translate(locale, "source") %></a>
</td>
</tr>
<tr>
<td>
<a href="/js/handlers.js?v=<%= ASSET_COMMIT %>">handlers.js</a>
</td>
<td>
<a href="https://www.gnu.org/licenses/agpl-3.0.html">AGPL-3.0</a>
</td>
<td>
<a href="/js/handlers.js?v=<%= ASSET_COMMIT %>"><%= translate(locale, "source") %></a>
</td>
</tr>
<tr>
<td>
<a href="/js/community.js?v=<%= ASSET_COMMIT %>">community.js</a>
</td>
<td>
<a href="https://www.gnu.org/licenses/agpl-3.0.html">AGPL-3.0</a>
</td>
<td>
<a href="/js/community.js?v=<%= ASSET_COMMIT %>"><%= translate(locale, "source") %></a>
</td>
</tr>
<tr>
<td>
<a href="/js/embed.js?v=<%= ASSET_COMMIT %>">embed.js</a>
</td>
<td>
<a href="https://www.gnu.org/licenses/agpl-3.0.html">AGPL-3.0</a>
</td>
<td>
<a href="/js/embed.js?v=<%= ASSET_COMMIT %>"><%= translate(locale, "source") %></a>
</td>
</tr>
<tr>
<td>
<a href="/js/notifications.js?v=<%= ASSET_COMMIT %>">notifications.js</a>
</td>
<td>
<a href="https://www.gnu.org/licenses/agpl-3.0.html">AGPL-3.0</a>
</td>
<td>
<a href="/js/notifications.js?v=<%= ASSET_COMMIT %>"><%= translate(locale, "source") %></a>
</td>
</tr>
<tr>
<td>
<a href="/js/player.js?v=<%= ASSET_COMMIT %>">player.js</a>
</td>
<td>
<a href="https://www.gnu.org/licenses/agpl-3.0.html">AGPL-3.0</a>
</td>
<td>
<a href="/js/player.js?v=<%= ASSET_COMMIT %>"><%= translate(locale, "source") %></a>
</td>
</tr>
<tr>
<td>
<a href="/js/silvermine-videojs-quality-selector.min.js?v=<%= ASSET_COMMIT %>">silvermine-videojs-quality-selector.min.js</a>
@ -121,34 +37,6 @@
</td>
</tr>
<tr>
<td>
<a href="/js/subscribe_widget.js?v=<%= ASSET_COMMIT %>">subscribe_widget.js</a>
</td>
<td>
<a href="https://www.gnu.org/licenses/agpl-3.0.html">AGPL-3.0</a>
</td>
<td>
<a href="/js/subscribe_widget.js?v=<%= ASSET_COMMIT %>"><%= translate(locale, "source") %></a>
</td>
</tr>
<tr>
<td>
<a href="/js/themes.js?v=<%= ASSET_COMMIT %>">themes.js</a>
</td>
<td>
<a href="https://www.gnu.org/licenses/agpl-3.0.html">AGPL-3.0</a>
</td>
<td>
<a href="/js/themes.js?v=<%= ASSET_COMMIT %>"><%= translate(locale, "source") %></a>
</td>
</tr>
<tr>
<td>
<a href="/videojs/videojs-contrib-quality-levels/videojs-contrib-quality-levels.js?v=<%= ASSET_COMMIT %>">videojs-contrib-quality-levels.js</a>
@ -289,19 +177,9 @@
</td>
</tr>
<tr>
<td>
<a href="/js/watch.js?v=<%= ASSET_COMMIT %>">watch.js</a>
</td>
<td>
<a href="https://www.gnu.org/licenses/agpl-3.0.html">AGPL-3.0</a>
</td>
<td>
<a href="/js/watch.js?v=<%= ASSET_COMMIT %>"><%= translate(locale, "source") %></a>
</td>
</tr>
<%- {% for row in run("../../../scripts/generate_js_licenses.cr").stringify.split('\n') %} %>
<%-= {{row.id}} -%>
<% {% end %} -%>
</table>
</body>
</html>

View File

@ -25,44 +25,17 @@
<% end %>
<% if captcha %>
<% case captcha_type when %>
<% when "image" %>
<% captcha = captcha.not_nil! %>
<img style="width:50%" src='<%= captcha[:question] %>'/>
<% captcha[:tokens].each_with_index do |token, i| %>
<input type="hidden" name="token[<%= i %>]" value="<%= HTML.escape(token) %>">
<% end %>
<input type="hidden" name="captcha_type" value="image">
<label for="answer"><%= translate(locale, "Time (h:mm:ss):") %></label>
<input type="text" name="answer" type="text" placeholder="h:mm:ss">
<% else # "text" %>
<% captcha = captcha.not_nil! %>
<% captcha[:tokens].each_with_index do |token, i| %>
<input type="hidden" name="token[<%= i %>]" value="<%= HTML.escape(token) %>">
<% end %>
<input type="hidden" name="captcha_type" value="text">
<label for="answer"><%= captcha[:question] %></label>
<input type="text" name="answer" type="text" placeholder="<%= translate(locale, "Answer") %>">
<% captcha = captcha.not_nil! %>
<img style="width:50%" src='<%= captcha[:question] %>'/>
<% captcha[:tokens].each_with_index do |token, i| %>
<input type="hidden" name="token[<%= i %>]" value="<%= HTML.escape(token) %>">
<% end %>
<label for="answer"><%= translate(locale, "Time (h:mm:ss):") %></label>
<input type="text" name="answer" type="text" placeholder="h:mm:ss">
<button type="submit" name="action" value="signin" class="pure-button pure-button-primary">
<%= translate(locale, "Register") %>
</button>
<% case captcha_type when %>
<% when "image" %>
<label>
<button type="submit" name="change_type" class="pure-button pure-button-primary" value="text">
<%= translate(locale, "Text CAPTCHA") %>
</button>
</label>
<% else # "text" %>
<label>
<button type="submit" name="change_type" class="pure-button pure-button-primary" value="image">
<%= translate(locale, "Image CAPTCHA") %>
</button>
</label>
<% end %>
<% else %>
<button type="submit" name="action" value="signin" class="pure-button pure-button-primary">
<%= translate(locale, "Sign In") %>/<%= translate(locale, "Register") %>

View File

@ -35,6 +35,20 @@ record AuthorFallback, name : String, id : String
# data is passed to the private `#parse()` method which returns a datastruct of the given
# type. Otherwise, nil is returned.
private module Parsers
module BaseParser
def parse(*args)
begin
return parse_internal(*args)
rescue ex
LOGGER.debug("#{{{@type.name}}}: Failed to render item.")
LOGGER.debug("#{{{@type.name}}}: Got exception: #{ex.message}")
ProblematicTimelineItem.new(
parse_exception: ex
)
end
end
end
# Parses a InnerTube videoRenderer into a SearchVideo. Returns nil when the given object isn't a videoRenderer
#
# A videoRenderer renders a video to click on within the YouTube and Invidious UI. It is **not**
@ -45,13 +59,16 @@ private module Parsers
# `videoRenderer`s can be found almost everywhere on YouTube. In categories, search results, channels, etc.
#
module VideoRendererParser
def self.process(item : JSON::Any, author_fallback : AuthorFallback)
extend self
include BaseParser
def process(item : JSON::Any, author_fallback : AuthorFallback)
if item_contents = (item["videoRenderer"]? || item["gridVideoRenderer"]?)
return self.parse(item_contents, author_fallback)
end
end
private def self.parse(item_contents, author_fallback)
private def parse_internal(item_contents, author_fallback)
video_id = item_contents["videoId"].as_s
title = extract_text(item_contents["title"]?) || ""
@ -115,7 +132,7 @@ private module Parsers
badges = VideoBadges::None
item_contents["badges"]?.try &.as_a.each do |badge|
b = badge["metadataBadgeRenderer"]
case b["label"].as_s
case b["label"]?.try &.as_s
when "LIVE"
badges |= VideoBadges::LiveNow
when "New"
@ -170,13 +187,16 @@ private module Parsers
# `channelRenderer`s can be found almost everywhere on YouTube. In categories, search results, channels, etc.
#
module ChannelRendererParser
def self.process(item : JSON::Any, author_fallback : AuthorFallback)
extend self
include BaseParser
def process(item : JSON::Any, author_fallback : AuthorFallback)
if item_contents = (item["channelRenderer"]? || item["gridChannelRenderer"]?)
return self.parse(item_contents, author_fallback)
end
end
private def self.parse(item_contents, author_fallback)
private def parse_internal(item_contents, author_fallback)
author = extract_text(item_contents["title"]) || author_fallback.name
author_id = item_contents["channelId"]?.try &.as_s || author_fallback.id
author_verified = has_verified_badge?(item_contents["ownerBadges"]?)
@ -230,13 +250,16 @@ private module Parsers
# A `hashtagTileRenderer` is a kind of search result.
# It can be found when searching for any hashtag (e.g "#hi" or "#shorts")
module HashtagRendererParser
def self.process(item : JSON::Any, author_fallback : AuthorFallback)
extend self
include BaseParser
def process(item : JSON::Any, author_fallback : AuthorFallback)
if item_contents = item["hashtagTileRenderer"]?
return self.parse(item_contents)
end
end
private def self.parse(item_contents)
private def parse_internal(item_contents)
title = extract_text(item_contents["hashtag"]).not_nil! # E.g "#hi"
# E.g "/hashtag/hi"
@ -263,10 +286,6 @@ private module Parsers
video_count: short_text_to_number(video_count_txt || ""),
channel_count: short_text_to_number(channel_count_txt || ""),
})
rescue ex
LOGGER.debug("HashtagRendererParser: Failed to extract renderer.")
LOGGER.debug("HashtagRendererParser: Got exception: #{ex.message}")
return nil
end
def self.parser_name
@ -284,13 +303,16 @@ private module Parsers
# `gridPlaylistRenderer`s can be found on the playlist-tabs of channels and expanded categories.
#
module GridPlaylistRendererParser
def self.process(item : JSON::Any, author_fallback : AuthorFallback)
extend self
include BaseParser
def process(item : JSON::Any, author_fallback : AuthorFallback)
if item_contents = item["gridPlaylistRenderer"]?
return self.parse(item_contents, author_fallback)
end
end
private def self.parse(item_contents, author_fallback)
private def parse_internal(item_contents, author_fallback)
title = extract_text(item_contents["title"]) || ""
plid = item_contents["playlistId"]?.try &.as_s || ""
@ -325,13 +347,16 @@ private module Parsers
# `playlistRenderer`s can be found almost everywhere on YouTube. In categories, search results, recommended, etc.
#
module PlaylistRendererParser
def self.process(item : JSON::Any, author_fallback : AuthorFallback)
extend self
include BaseParser
def process(item : JSON::Any, author_fallback : AuthorFallback)
if item_contents = item["playlistRenderer"]?
return self.parse(item_contents, author_fallback)
end
end
private def self.parse(item_contents, author_fallback)
private def parse_internal(item_contents, author_fallback)
title = extract_text(item_contents["title"]) || ""
plid = item_contents["playlistId"]?.try &.as_s || ""
@ -385,13 +410,16 @@ private module Parsers
# `shelfRenderer`s can be found almost everywhere on YouTube. In categories, search results, channels, etc.
#
module CategoryRendererParser
def self.process(item : JSON::Any, author_fallback : AuthorFallback)
extend self
include BaseParser
def process(item : JSON::Any, author_fallback : AuthorFallback)
if item_contents = item["shelfRenderer"]?
return self.parse(item_contents, author_fallback)
end
end
private def self.parse(item_contents, author_fallback)
private def parse_internal(item_contents, author_fallback)
title = extract_text(item_contents["title"]?) || ""
url = item_contents.dig?("endpoint", "commandMetadata", "webCommandMetadata", "url")
.try &.as_s
@ -450,13 +478,16 @@ private module Parsers
# container.It is very similar to RichItemRendererParser
#
module ItemSectionRendererParser
def self.process(item : JSON::Any, author_fallback : AuthorFallback)
extend self
include BaseParser
def process(item : JSON::Any, author_fallback : AuthorFallback)
if item_contents = item.dig?("itemSectionRenderer", "contents", 0)
return self.parse(item_contents, author_fallback)
end
end
private def self.parse(item_contents, author_fallback)
private def parse_internal(item_contents, author_fallback)
child = VideoRendererParser.process(item_contents, author_fallback)
child ||= PlaylistRendererParser.process(item_contents, author_fallback)
@ -476,13 +507,16 @@ private module Parsers
# itself inside a richGridRenderer container.
#
module RichItemRendererParser
def self.process(item : JSON::Any, author_fallback : AuthorFallback)
extend self
include BaseParser
def process(item : JSON::Any, author_fallback : AuthorFallback)
if item_contents = item.dig?("richItemRenderer", "content")
return self.parse(item_contents, author_fallback)
end
end
private def self.parse(item_contents, author_fallback)
private def parse_internal(item_contents, author_fallback)
child = VideoRendererParser.process(item_contents, author_fallback)
child ||= ReelItemRendererParser.process(item_contents, author_fallback)
child ||= PlaylistRendererParser.process(item_contents, author_fallback)
@ -506,13 +540,16 @@ private module Parsers
# TODO: Confirm that hypothesis
#
module ReelItemRendererParser
def self.process(item : JSON::Any, author_fallback : AuthorFallback)
extend self
include BaseParser
def process(item : JSON::Any, author_fallback : AuthorFallback)
if item_contents = item["reelItemRenderer"]?
return self.parse(item_contents, author_fallback)
end
end
private def self.parse(item_contents, author_fallback)
private def parse_internal(item_contents, author_fallback)
video_id = item_contents["videoId"].as_s
reel_player_overlay = item_contents.dig(
@ -600,13 +637,16 @@ private module Parsers
# a richItemRenderer or a richGridRenderer.
#
module LockupViewModelParser
def self.process(item : JSON::Any, author_fallback : AuthorFallback)
extend self
include BaseParser
def process(item : JSON::Any, author_fallback : AuthorFallback)
if item_contents = item["lockupViewModel"]?
return self.parse(item_contents, author_fallback)
end
end
private def self.parse(item_contents, author_fallback)
private def parse_internal(item_contents, author_fallback)
playlist_id = item_contents["contentId"].as_s
thumbnail_view_model = item_contents.dig(
@ -675,13 +715,16 @@ private module Parsers
# usually (always?) encapsulated in a richItemRenderer.
#
module ShortsLockupViewModelParser
def self.process(item : JSON::Any, author_fallback : AuthorFallback)
extend self
include BaseParser
def process(item : JSON::Any, author_fallback : AuthorFallback)
if item_contents = item["shortsLockupViewModel"]?
return self.parse(item_contents, author_fallback)
end
end
private def self.parse(item_contents, author_fallback)
private def parse_internal(item_contents, author_fallback)
# TODO: Maybe add support for "oardefault.jpg" thumbnails?
# thumbnail = item_contents.dig("thumbnail", "sources", 0, "url").as_s
# Gives: https://i.ytimg.com/vi/{video_id}/oardefault.jpg?...