Compare commits

..

123 Commits

Author SHA1 Message Date
src-tinkerer
72fe8af850
Merge branch 'master' into stream-sort 2024-03-26 12:19:45 +00:00
Émilien (perso)
99a5e9cbc4
Merge pull request #4473 from SamantazFox/bump-api-clients
YoutubeAPI: bump client versions
2024-03-08 11:23:03 +01:00
Samantaz Fox
619aa3ff05
YoutubeAPI: bump client versions 2024-03-06 21:36:15 +01:00
Samantaz Fox
e8a36985af
API: Add APIHandler back (#4431)
This handler should no have been removed in 4276, as it adds the required CORS
header (Access-Control-Allow-Origin) for public acces to the API.

Thanks to iBicha for noticing this!
2024-02-19 00:16:17 +01:00
Samantaz Fox
962ce23cc2
WebVTT::Builder: Add logic to escape special chars (#4414)
Note: WebVTT does allow some tags in the cue payload in some circumstances
while this PR just blindly escapes everything:
https://developer.mozilla.org/en-US/docs/Web/API/WebVTT_API#cue_payload_text_tags
2024-02-19 00:16:17 +01:00
Samantaz Fox
e0ce59d3e8
Channels: Add support for multi-image community posts (#4412)
This PR adds a CSS-only image carousel for community posts with more than
one image attached.

Closes issue 3522
2024-02-19 00:16:17 +01:00
Samantaz Fox
c5a3112e49
CSS: expand #contents width on small screens (#4405)
The #contents div now takes the full width on small screens (< 1280px).
All page elements have a little more room, especially the video titles.
2024-02-19 00:16:17 +01:00
Samantaz Fox
d3703baba9
I18n: Add missing translation strings (#4424)
Closes issue 3120
2024-02-19 00:15:44 +01:00
Samantaz Fox
20203f4ec0
I18n: Fix a typo in Finnish localization (#4375) 2024-02-18 23:53:16 +01:00
Samantaz Fox
732553519e
Translations update from Hosted Weblate (#4164) 2024-02-18 23:51:53 +01:00
ChunkyProgrammer
a957b0fb7c remove trailing white spaces 2024-02-16 16:22:43 -05:00
ChunkyProgrammer
26429bee3f make it so interpolation text can be a hash
Co-Authored-By: Samantaz Fox <coding@samantaz.fr>
2024-02-15 21:45:22 -05:00
ChunkyProgrammer
ef6b766b29 Add support for multi image community posts 2024-02-15 21:45:21 -05:00
Émilien (perso)
1e6ec605e8
Remove usage of depends_on (#4383) 2024-02-15 22:59:00 +01:00
Samantaz Fox
60f6a345d9
Locales: Fix broken i18Next v3/v4 plurals
Languages impacted: es, fa, pt
2024-02-15 22:12:04 +01:00
Samantaz Fox
d1dddc1adc
Locales: Remove Cyrillic text from Serbian (Latin) 2024-02-15 21:37:17 +01:00
Hosted Weblate
00ef004029
Update Norwegian Bokmål translation
Co-authored-by: Deleted User <noreply+73135@weblate.org>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
2024-02-15 18:02:15 +01:00
Hosted Weblate
7ff11e4c44
Update Serbian (cyrillic) translation
Update Serbian (cyrillic) translation

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: NEXI <nexiphotographer@gmail.com>
2024-02-15 18:02:15 +01:00
Hosted Weblate
8db2e060d9
Update Chinese (Simplified) translation
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: 大王叫我来巡山 <hamburger2048@users.noreply.hosted.weblate.org>
2024-02-15 18:02:15 +01:00
Hosted Weblate
8b0cbd2a29
Update Chinese (Traditional) translation
Co-authored-by: Jeff Huang <s8321414@gmail.com>
2024-02-15 18:02:15 +01:00
Hosted Weblate
d2ce519559
Update Slovenian translation
Co-authored-by: Damjan Gerl <damjan@damjan.net>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
2024-02-15 18:02:15 +01:00
Hosted Weblate
219b587945
Update Korean translation
Update Korean translation

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: simmon <simmon@nplob.com>
Co-authored-by: xrfmkrh <rF3nMd7sRKezjF2vcEQo@protonmail.com>
2024-02-15 18:02:15 +01:00
Hosted Weblate
e8810509c1
Update Albanian translation
Update Albanian translation

Co-authored-by: Besnik Bleta <besnik@programeshqip.org>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
2024-02-15 18:02:15 +01:00
Hosted Weblate
9688200caf
Update Serbian translation
Update Serbian translation

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: NEXI <nexiphotographer@gmail.com>
2024-02-15 18:02:15 +01:00
Hosted Weblate
26a50eb4e8
Update Persian translation
Update Persian translation

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Kaambiz <kambizx@gmail.com>
2024-02-15 18:02:15 +01:00
Hosted Weblate
0ce945bfa8
Update Swedish translation
Update Swedish translation

Update Swedish translation

Co-authored-by: Deleted User <noreply+73135@weblate.org>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Max Bengtzén <aura.kettles.0h@icloud.com>
Co-authored-by: bittin1ddc447d824349b2 <bittin@reimu.nl>
2024-02-15 18:02:15 +01:00
Hosted Weblate
aadf848ee6
Update French translation
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Jean Mareilles <waged1266@tutanota.com>
2024-02-15 18:02:15 +01:00
Hosted Weblate
53ce2a1a9a
Update Spanish translation
Update Spanish translation

Update Spanish translation

Update Spanish translation

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Jorge Maldonado Ventura <jorgesumle@freakspot.net>
Co-authored-by: gallegonovato <fran-carro@hotmail.es>
2024-02-15 18:02:15 +01:00
Hosted Weblate
1d5100462b
Update Dutch translation
Update Dutch translation

Co-authored-by: Deleted User <noreply+73135@weblate.org>
Co-authored-by: Gert-dev <Gert-dev@users.noreply.hosted.weblate.org>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
2024-02-15 18:02:15 +01:00
Hosted Weblate
986515dc5b
Update Indonesian translation
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Reza Almanda <rezaalmanda27@gmail.com>
2024-02-15 18:02:15 +01:00
Hosted Weblate
1d906aeecc
Update Interlingua translation
Add Interlingua translation

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Software In Interlingua <softinterlingua@gmail.com>
2024-02-15 18:02:15 +01:00
Hosted Weblate
426b472a15
Update Arabic translation
Update Arabic translation

Update Arabic translation

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Rex_sa <rex.sa@pm.me>
2024-02-15 18:02:15 +01:00
Hosted Weblate
1493e6a086
Update Italian translation
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Random <random-r@users.noreply.hosted.weblate.org>
2024-02-15 18:02:14 +01:00
Hosted Weblate
3767ab2eeb
Update Polish translation
Update Polish translation

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Matthaiks <kitynska@gmail.com>
2024-02-15 18:02:14 +01:00
Hosted Weblate
fea36fc639
Update Hindi translation
Update Hindi translation

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Saurmandal <saurmandal@protonmail.com>
Co-authored-by: Snwglb <wishitwasarchived@gmail.com>
2024-02-15 18:02:14 +01:00
Hosted Weblate
a16235d3b9
Update Croatian translation
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Milo Ivir <mail@milotype.de>
2024-02-15 18:02:14 +01:00
Hosted Weblate
99a3bd4fff
Update Vietnamese translation
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Tran Viet Duc <deli50@protonmail.com>
2024-02-15 18:02:14 +01:00
Hosted Weblate
4aed0e1102
Update Portuguese translation
Update Portuguese translation

Update Portuguese translation

Update Portuguese translation

Co-authored-by: Filipe Martins <mvrtinsbeats@gmail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Jener Gomes <jenerg1@gmail.com>
Co-authored-by: SC <lalocas@protonmail.com>
Co-authored-by: jamerLamer <akHarINlMYExpSmVPDRT@proton.me>
2024-02-15 18:02:14 +01:00
Hosted Weblate
833c711cba
Update Czech translation
Update Czech translation

Co-authored-by: Fjuro <ifjuro@proton.me>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
2024-02-15 18:02:14 +01:00
Hosted Weblate
7e1deea15e
Update Catalan translation
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: victor dargallo <victordargallo@disroot.org>
2024-02-15 18:02:14 +01:00
Hosted Weblate
b9ae1a61da
Update Japanese translation
Update Japanese translation

Update Japanese translation

Update Japanese translation

Update Japanese translation

Update Japanese translation

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: maboroshin <maboroshin@users.noreply.hosted.weblate.org>
2024-02-15 18:02:14 +01:00
Hosted Weblate
f062c18b82
Update Ukrainian translation
Update Ukrainian translation

Update Ukrainian translation

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Ihor Hordiichuk <igor_ck@outlook.com>
Co-authored-by: Сергій <sergiy.goncharuk.1@gmail.com>
2024-02-15 18:02:14 +01:00
Hosted Weblate
f21a532c0d
Update Bulgarian translation
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Radoslav Lelchev <rlelchev@abv.bg>
2024-02-15 18:02:14 +01:00
Hosted Weblate
8cec7ba004
Update Russian translation
Update Russian translation

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Noise Maker <vh0d@disroot.org>
Co-authored-by: hikiko4ern <25303622+hikiko4ern@users.noreply.github.com>
2024-02-15 18:02:14 +01:00
Hosted Weblate
8169cd8977
Update Danish translation
Co-authored-by: Grooty12 <weblate@grooty.site>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
2024-02-15 18:02:14 +01:00
Hosted Weblate
8ffc569ebd
Update German translation
Update German translation

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Lenny Angst <lenny@familie-angst.ch>
Co-authored-by: Radoslav Lelchev <rlelchev@abv.bg>
2024-02-15 18:02:14 +01:00
Hosted Weblate
736f35332a
Update Portuguese (Brazil) translation
Update Portuguese (Brazil) translation

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: joaooliva <joaooliva@protonmail.com>
2024-02-15 18:02:14 +01:00
Hosted Weblate
c52c6d3c9a
Update Turkish translation
Update Turkish translation

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Oğuz Ersen <oguz@ersen.moe>
2024-02-15 18:02:14 +01:00
Samantaz Fox
7b84bdb29b
API: Add APIHandler back
This handler should no have been removed in 4276, as it adds the required CORS
header (Access-Control-Allow-Origin) for public acces to the API.

Thanks to iBicha for noticing this!
2024-02-13 21:05:26 +01:00
Samantaz Fox
5c0b6d8afa
Stats: Fix two swapped function names (#4376)
The function names `count_users_active_6m` and `count_users_active_1m` were
swapped. As the names were swapped on both sides (declaration and use), this
had no functional impact.

No related isse was tracked.
2024-02-12 22:34:13 +01:00
Samantaz Fox
c85b908613
API: Fix missing wildcards after login redirect (#4348)
This PR fixes an issue where the `scopes` parameter would see its wildmark
characters (*) removed during the login page redirection, after that a call
to `/authorize_token` was made while the user was not logged in.

Closes issue 4200
2024-02-12 22:30:48 +01:00
Samantaz Fox
f32764c840
HTML: Preserve playlist in "Watch on YouTube" link (#4342)
It seems that at some point, Youtube changed the URL parameter from `plid`
to `list` and we didn't notice. This fixes that.

Closes #3929
2024-02-12 22:23:44 +01:00
Samantaz Fox
d30dae43fe
HTML: Add title to toggle theme icon (#4320) 2024-02-12 22:20:14 +01:00
Samantaz Fox
338d3d9f86
CSS: Fix thumbnails' aspect ratio to prevent CLS (#4278)
Force the thumbnails aspect ratio to 16/9 in order to prevent Cumulative Layout
Shifting (CLS) from hapenning during lazy loading.

It also fixes the problematic, taller thumbnails that Youtube returns for
playlists.

Closes issue 4002
2024-02-12 22:19:14 +01:00
Samantaz Fox
1f51255f2f
API: Remove the fields parameter (#4276)
Multiple users have reported that the fields parameter is slowing down API
response times significantly. As most API endpoints are already optimized to
make as few requests as possible to Youtube, there is no point in limiting the
output. Furthermore, the added processing might be part of the broader memory
leak problem (See 1438).

In addition, the small increase in data output is not much of an issue compared
to the huge video proxy that lies next to this API.

No related issue tracked
2024-02-12 22:10:45 +01:00
Samantaz Fox
dcbe52c9fb
Videos: Use start time and end time for clips (#4264)
This PR parses the start and end time for clips.

It also adds a new, dedicated API endpoint (`/api/v1/clips/{id}`) for
retrieving the start and end time of a clip.

Here is a sample response from that new endpoint (`video` is a video object,
as described in https://docs.invidious.io/api/common_types/#videoobject):

GET `/api/v1/clips/UgkxxPM3BRphCAPLP88YoUGuj79KXPfpNNO_?pretty=1`

Response:
```
{
  "startTime": 8842.645,
  "endTime": 8855.856,
  "clipTitle": "✂️ Kirby is pink!",
  "video": {}
}
```

Closes issue 3921
2024-02-12 22:10:16 +01:00
Samantaz Fox
bd5df3af5f
API: Unescape search suggestions (#4218)
Previously, the suggestion were HTML encoded. This PR fixes that.
2024-02-12 22:03:33 +01:00
Samantaz Fox
9bd2072e1d
API: Add playlist and start time to resolve_url
This adds `playlistId` and `startTimeSeconds` to /api/v1/resolveurl if these
informations were returned by Youtube's endpoint.
2024-02-12 22:01:08 +01:00
Samantaz Fox
3b4358dbd4
Extractors: Don't error if AuthorId does not exist (#3869)
Some playlist author's don't have a YouTube channel, so does movies.
This caused various extractors (related videos, search) to fail.

Closes the following issues:
2530, 3349, 3766, 3812, 4133
2024-02-12 21:54:17 +01:00
Émilien (perso)
cf686202e0
Merge pull request #4423 from tleydxdy/xml-namespace
Fix pubsub feed parsing
2024-02-12 08:29:44 +01:00
shironeko
6b33820f1f Add missing translation strings
closes #3120
2024-02-08 19:01:19 -05:00
shironeko
98c421e9f5 Fix when video from pubsub is a scheduled event 2024-02-08 18:58:23 -05:00
shironeko
c864a63b6d Fix pubsub feed parsing
similar to what's done in #3793, this is causing an assert on my instance
2024-02-08 17:05:11 -05:00
syeopite
0ad2eff2a4
WebVTT::Builder: Add logic to escape special chars 2024-01-30 15:25:45 -08:00
ThetaDev
c005ada487
fix: prevent censoring of self-harm related search queries (#4403)
* fix: prevent censoring of self-harm related search queries

* fix: yt_filters_spec with new flag
2024-01-29 14:59:25 +01:00
toabr
4a339df5c4 CSS: expand #contents width on small screens 2024-01-27 00:38:47 +01:00
syeopite
1c0b4205d4
Add parameter to disable force_resolve in make_client (#4335)
* Add option to disable force_resolve in make_client

Some websites such as archive.org and textcaptcha.com
does not support IPv6 and as such requests fail when Invidious requests
with IPv6 to those services.

* Reenable force_resolve on pubsub subcribe request

* Make force_resolve false by default in make_client

* Remove missed explicit force_resolve=false
2024-01-10 23:01:00 +00:00
syeopite
b16f66ef00
Exempt issues with "exempt-stale" from staling (#4385)
The exempt-stale label was not actually set to exempt issues from staling...
2024-01-10 20:40:19 +00:00
vojkovic
7cca1285aa
Fix two swapped function names 2024-01-06 15:51:31 +08:00
pitkajuh
c059829035 Fix typo 2024-01-05 20:39:29 +01:00
ChunkyProgrammer
7da4a7f72b add null safety to clip parsing 2023-12-26 22:05:09 -05:00
nixos script
0917efd9cb fix issue where scope would be missing the * if the user was not logged in before calling the authorize endpoint
fix #4200
2023-12-21 13:52:19 +08:00
ChunkyProgrammer
090b470bfc fix potential memory leak 2023-12-19 23:07:18 -05:00
Luigi
97c4165f55
Improve depends_on docker-compose (#4249)
* Improve depends_on checking the service is up and healthy before start the service that might cause issue first boot

* Docker version Ubuntu 22.04 has a version which doesn't support restart
2023-12-18 22:18:05 +00:00
guidiasz
87a8207f37 fix: "Watch on YouTube" preserve current playlist 2023-12-18 13:23:55 -03:00
ChunkyProgrammer
fe8b1b4cc4 Add title to toggle theme icon 2023-12-07 11:43:56 -05:00
ChunkyProgrammer
f1edb1d6bf fix related video author when id is empty 2023-12-07 09:39:33 -05:00
Chunky programmer
b5f8b4542a Search: Don't error if AuthorId does not exist 2023-12-07 09:39:33 -05:00
ChunkyProgrammer
b344d98c25 Add API endpoint for Clips 2023-12-07 09:39:04 -05:00
ChunkyProgrammer
8c22e6a640 use start time and endtime for clips 2023-12-07 09:39:03 -05:00
ChunkyProgrammer
6488794218 Unescape search suggestions 2023-12-07 09:36:59 -05:00
Samantaz Fox
9e8baa3539
Move "Projects using Invidious" section to the docs (#4283) 2023-12-06 18:30:52 +01:00
Samantaz Fox
07fe648a9c
Remove anti-captcha (#4277) 2023-12-06 18:29:16 +01:00
Samantaz Fox
6da3287e9d
Misc: Fix logic for setting user agent (#4265) 2023-12-06 18:28:12 +01:00
Samantaz Fox
37c2f5caed
Misc: Use #splat method for macro expressions (#4242) 2023-12-06 18:22:50 +01:00
Samantaz Fox
07b366f06b
Chores: Update Crystal CI (#4239) 2023-12-06 18:21:57 +01:00
Samantaz Fox
e8a14446af
Videos: Append '&mpd_version=5' to DASH manifest URL (#4196) 2023-12-06 18:20:26 +01:00
Samantaz Fox
813dc6de1c
Player: Fix iOS screen timeout in loop mode (#4076) 2023-12-06 18:19:31 +01:00
unbelauscht
72478ba704
Remove short sha docker tag (#4312) 2023-12-06 14:20:51 +01:00
unbelauscht
9e970fe4bd
docker-meta generate versioned docker tags & OCI labels (#4161)
* Use docker-meta to generate tags and labels + versioning

I updated the workflow to have additional tags based on git tags.

This way an administrator can get images by Invidious version tag.

* Use current date as image tag

* Switch to YYYY.MM.DD-SHA_SHORT

* Use short sha to follow existing tagging style

* revert build-push-actions to v5

---------

Co-authored-by: panda <panda@purplehaze.com>
2023-12-06 12:19:21 +01:00
Abdul Rauf
d76fed5850
ci: update github actions (#4209)
* ci: update actions/checkout to v4

* ci: remove patch version from crystal-lang/install-crystal

latest patch will be automatically installed which is 1.8.1 https://github.com/crystal-lang/install-crystal

* ci: update docker/setup-buildx-action to v3

* ci: update docker/build-push-action to v5

* ci: update docker/setup-qemu-action to v3

* ci: update docker/login-action to v3

* ci: actions/stale to v8

https://github.com/actions/stale/blob/main/CHANGELOG.md

* Revert "ci: remove patch version from crystal-lang/install-crystal"

This reverts commit 8c39ce846f.
2023-12-01 18:11:45 +00:00
TheFrenchGhosty
6868cade05
Rewording and formating 2023-11-23 22:23:54 +01:00
Samantaz Fox
7b6930c16b
Remove the 'fields' parameter on the client side too 2023-11-23 18:30:42 +01:00
Samantaz Fox
9d5fa2bcc4
Helpers: remove JSONFilter logic 2023-11-23 18:30:42 +01:00
Samantaz Fox
9310d09f93
Kemal: remove APIHandler middleware 2023-11-23 18:30:37 +01:00
syeopite
67571b2492
Replace projects using invidious with doc link 2023-11-21 12:49:47 -08:00
maboroshin
d5df81f0f8
Update README.md
Add GTK+ Pipe Viewer, PlasmaTube
2023-11-21 12:07:40 -08:00
maboroshin
eb27e097ed
README: Improve "Projects using Invidious" section 2023-11-21 12:07:07 -08:00
Timothy Redaelli
9ce9c54399
Install rsvg-convert on docker images (#4230) (#4282)
In #4103 alpine was updated from 3.16 to 3.18, but in 3.17 librsvg
packages was splitted and rsvg-convert is on his own package.
2023-11-21 19:47:07 +00:00
Corné Dorrestijn
16c79f1ef5
Fixed aspect ratio for thumnails to prevent CLS 2023-11-21 08:14:45 +01:00
Samantaz Fox
3a5d408602
Remove leftover functions/specs used by the anti-captcha job 2023-11-20 17:40:31 +01:00
Samantaz Fox
7e363fa3c8
Config: Remove anti-captcha related configs 2023-11-20 17:39:51 +01:00
Samantaz Fox
d9416a0be5
Jobs: Remove BypassCaptchaJob 2023-11-20 17:39:13 +01:00
Brahim Hadriche
b40cf6544a Revert "Make head request to resolve short urls"
This reverts commit 7e267da5be.
2023-11-19 16:06:29 -05:00
ChunkyProgrammer
8338a73e7b add user_agent if empty or crystal 2023-11-17 08:01:56 -05:00
ChunkyProgrammer
86ee761788 Fix logic for setting user agent 2023-11-15 00:51:43 -05:00
Samantaz Fox
c5b87e3b5e
Channel: Fetch header informations from homepage (#4241) 2023-11-08 23:43:04 +01:00
syeopite
ed8b84ed15
Replace more * in macro with #splat 2023-11-08 00:49:37 -08:00
syeopite
8ce91166d6
Remove instance of the * operator in macro expr 2023-11-08 00:42:46 -08:00
syeopite
8525758583
Use #splat method for macro expressions 2023-11-08 00:37:18 -08:00
syeopite
ab4c0a1d3c
Fetch channel info from home tab instead of about 2023-11-07 23:53:14 -08:00
syeopite
c31908a011
Update params protobuf for channel about tab 2023-11-07 22:19:56 -08:00
syeopite
2562f80695
Add CI for Crystal 1.10.1 2023-11-07 23:46:20 +00:00
syeopite
fead0e14ac
Drop support for Crystal 1.6.2 2023-11-07 23:45:01 +00:00
syeopite
438467f69a
Add playback success rate to /api/v1/stats (#4085)
* Add stats-based /videoplayback blockage status

* Count when YouTube returns wrong video as failure

* Cast playback stats hash type prior to return

* Bump stats refresh timer to 10 minutes
2023-11-04 13:52:30 +00:00
Émilien (perso)
db3c57d49f
force resolve for everything (#4225) 2023-11-02 22:35:50 +00:00
Brahim Hadriche
3881038a32 format 2023-10-26 17:51:38 -04:00
Brahim Hadriche
7e267da5be Make head request to resolve short urls 2023-10-26 17:48:58 -04:00
Brahim Hadriche
d7901c1e0d type fix 2023-10-26 17:35:52 -04:00
Brahim Hadriche
85a5bbd696 Add playlist and start time to the resolve url 2023-10-26 17:24:53 -04:00
Samantaz Fox
07de1e236f
Videos: Append '&mpd_version=5' to DASH manifest URL
This makes Youtube return a MPD manifest with templates rather than
lengthy <SegmentList>. The returned  manifest is about 44 times smaller.
2023-10-22 17:56:04 +02:00
Ming Kin Choi
27d8fa112d
Fix iOS screen timeout on video playback loop mode (more elegantly) 2023-08-27 14:11:45 +08:00
Ming Kin Choi
2a092577c6
Fix iOS screen timeout on video playback loop mode 2023-08-27 12:50:36 +08:00
84 changed files with 1343 additions and 1015 deletions

View File

@ -38,16 +38,16 @@ jobs:
matrix: matrix:
stable: [true] stable: [true]
crystal: crystal:
- 1.6.2
- 1.7.3 - 1.7.3
- 1.8.2 - 1.8.2
- 1.9.2 - 1.9.2
- 1.10.1
include: include:
- crystal: nightly - crystal: nightly
stable: false stable: false
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
with: with:
submodules: true submodules: true
@ -87,7 +87,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
- name: Build Docker - name: Build Docker
run: docker-compose build --build-arg release=0 run: docker-compose build --build-arg release=0
@ -103,18 +103,18 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
- name: Set up QEMU - name: Set up QEMU
uses: docker/setup-qemu-action@v2 uses: docker/setup-qemu-action@v3
with: with:
platforms: arm64 platforms: arm64
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2 uses: docker/setup-buildx-action@v3
- name: Build Docker ARM64 image - name: Build Docker ARM64 image
uses: docker/build-push-action@v3 uses: docker/build-push-action@v5
with: with:
context: . context: .
file: docker/Dockerfile.arm64 file: docker/Dockerfile.arm64

View File

@ -11,7 +11,6 @@ on:
- invidious.service - invidious.service
- .git* - .git*
- .editorconfig - .editorconfig
- screenshots/* - screenshots/*
- .github/ISSUE_TEMPLATE/* - .github/ISSUE_TEMPLATE/*
- kubernetes/** - kubernetes/**
@ -22,7 +21,7 @@ jobs:
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v3 uses: actions/checkout@v4
- name: Install Crystal - name: Install Crystal
uses: crystal-lang/install-crystal@v1.8.0 uses: crystal-lang/install-crystal@v1.8.0
@ -38,42 +37,64 @@ jobs:
fi fi
- name: Set up QEMU - name: Set up QEMU
uses: docker/setup-qemu-action@v2 uses: docker/setup-qemu-action@v3
with: with:
platforms: arm64 platforms: arm64
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2 uses: docker/setup-buildx-action@v3
- name: Login to registry - name: Login to registry
uses: docker/login-action@v2 uses: docker/login-action@v3
with: with:
registry: quay.io registry: quay.io
username: ${{ secrets.QUAY_USERNAME }} username: ${{ secrets.QUAY_USERNAME }}
password: ${{ secrets.QUAY_PASSWORD }} password: ${{ secrets.QUAY_PASSWORD }}
- name: Docker meta
id: meta
uses: docker/metadata-action@v5
with:
images: quay.io/invidious/invidious
tags: |
type=sha,format=short,prefix={{date 'YYYY.MM.DD'}}-,enable=${{ github.ref == format('refs/heads/{0}', 'master') }}
type=raw,value=latest,enable=${{ github.ref == format('refs/heads/{0}', 'master') }}
labels: |
quay.expires-after=12w
- name: Build and push Docker AMD64 image for Push Event - name: Build and push Docker AMD64 image for Push Event
if: github.ref == 'refs/heads/master' uses: docker/build-push-action@v5
uses: docker/build-push-action@v3
with: with:
context: . context: .
file: docker/Dockerfile file: docker/Dockerfile
platforms: linux/amd64 platforms: linux/amd64
labels: quay.expires-after=12w labels: ${{ steps.meta.outputs.labels }}
push: true push: true
tags: quay.io/invidious/invidious:${{ github.sha }},quay.io/invidious/invidious:latest tags: ${{ steps.meta.outputs.tags }}
build-args: | build-args: |
"release=1" "release=1"
- name: Docker meta
id: meta-arm64
uses: docker/metadata-action@v5
with:
images: quay.io/invidious/invidious
flavor: |
suffix=-arm64
tags: |
type=sha,format=short,prefix={{date 'YYYY.MM.DD'}}-,enable=${{ github.ref == format('refs/heads/{0}', 'master') }}
type=raw,value=latest,enable=${{ github.ref == format('refs/heads/{0}', 'master') }}
labels: |
quay.expires-after=12w
- name: Build and push Docker ARM64 image for Push Event - name: Build and push Docker ARM64 image for Push Event
if: github.ref == 'refs/heads/master' uses: docker/build-push-action@v5
uses: docker/build-push-action@v3
with: with:
context: . context: .
file: docker/Dockerfile.arm64 file: docker/Dockerfile.arm64
platforms: linux/arm64/v8 platforms: linux/arm64/v8
labels: quay.expires-after=12w labels: ${{ steps.meta-arm64.outputs.labels }}
push: true push: true
tags: quay.io/invidious/invidious:${{ github.sha }}-arm64,quay.io/invidious/invidious:latest-arm64 tags: ${{ steps.meta-arm64.outputs.tags }}
build-args: | build-args: |
"release=1" "release=1"

View File

@ -10,13 +10,13 @@ jobs:
stale: stale:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/stale@v5 - uses: actions/stale@v8
with: with:
repo-token: ${{ secrets.GITHUB_TOKEN }} repo-token: ${{ secrets.GITHUB_TOKEN }}
days-before-stale: 365 days-before-stale: 365
days-before-pr-stale: 90 days-before-pr-stale: 90
days-before-close: 30 days-before-close: 30
exempt-pr-labels: blocked exempt-pr-labels: blocked,exempt-stale
stale-issue-message: 'This issue has been automatically marked as stale and will be closed in 30 days because it has not had recent activity and is much likely outdated. If you think this issue is still relevant and applicable, you just have to post a comment and it will be unmarked.' stale-issue-message: 'This issue has been automatically marked as stale and will be closed in 30 days because it has not had recent activity and is much likely outdated. If you think this issue is still relevant and applicable, you just have to post a comment and it will be unmarked.'
stale-pr-message: 'This pull request has been automatically marked as stale and will be closed in 30 days because it has not had recent activity and is much likely abandoned or outdated. If you think this pull request is still relevant and applicable, you just have to post a comment and it will be unmarked.' stale-pr-message: 'This pull request has been automatically marked as stale and will be closed in 30 days because it has not had recent activity and is much likely abandoned or outdated. If you think this pull request is still relevant and applicable, you just have to post a comment and it will be unmarked.'
stale-issue-label: "stale" stale-issue-label: "stale"

View File

@ -145,18 +145,7 @@ Weblate also allows you to log-in with major SSO providers like Github, Gitlab,
## Projects using Invidious ## Projects using Invidious
- [FreeTube](https://github.com/FreeTubeApp/FreeTube): A libre software YouTube app for privacy. A list of projects and extensions for or utilizing Invidious can be found in the documentation: https://docs.invidious.io/applications/
- [CloudTube](https://sr.ht/~cadence/tube/): A JavaScript-rich alternate YouTube player.
- [PeerTubeify](https://gitlab.com/Cha_de_L/peertubeify): On YouTube, displays a link to the same video on PeerTube, if it exists.
- [MusicPiped](https://github.com/deep-gaurav/MusicPiped): A material design music player that streams music from YouTube.
- [HoloPlay](https://github.com/stephane-r/holoplay-pwa): Progressive Web App connecting on Invidious API's with search, playlists and favorites.
- [WatchTube](https://github.com/WatchTubeTeam/WatchTube): Powerful YouTube client for Apple Watch.
- [Yattee](https://github.com/yattee/yattee): Alternative YouTube frontend for iPhone, iPad, Mac and Apple TV.
- [TubiTui](https://codeberg.org/777/TubiTui): A lightweight, libre, TUI-based YouTube client.
- [Ytfzf](https://github.com/pystardust/ytfzf): A posix script to find and watch youtube videos from the terminal. (Without API).
- [Playlet](https://github.com/iBicha/playlet): Unofficial Youtube client for Roku TV.
- [Clipious](https://github.com/lamarios/clipious): Unofficial Invidious client for Android.
## Liability ## Liability

119
assets/css/carousel.css Normal file
View File

@ -0,0 +1,119 @@
/*
Copyright (c) 2024 by Jennifer (https://codepen.io/jwjertzoch/pen/JjyGeRy)
Permission is hereby granted, free of charge, to any person
obtaining a copy of this software and associated documentation
files (the "Software"), to deal in the Software without restriction,
including without limitation the rights to use, copy, modify,
merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall
be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
*/
.carousel {
margin: 0 auto;
overflow: hidden;
text-align: center;
}
.slides {
width: 100%;
display: flex;
overflow-x: scroll;
scrollbar-width: none;
scroll-snap-type: x mandatory;
scroll-behavior: smooth;
}
.slides::-webkit-scrollbar {
display: none;
}
.slides-item {
align-items: center;
border-radius: 10px;
display: flex;
flex-shrink: 0;
font-size: 100px;
height: 600px;
justify-content: center;
margin: 0 1rem;
position: relative;
scroll-snap-align: start;
transform: scale(1);
transform-origin: center center;
transition: transform .5s;
width: 100%;
}
.carousel__nav {
padding: 1.25rem .5rem;
}
.slider-nav {
align-items: center;
background-color: #ddd;
border-radius: 50%;
color: #000;
display: inline-flex;
height: 1.5rem;
justify-content: center;
padding: .5rem;
position: relative;
text-decoration: none;
width: 1.5rem;
}
.skip-link {
height: 1px;
overflow: hidden;
position: absolute;
top: auto;
width: 1px;
}
.skip-link:focus {
align-items: center;
background-color: #000;
color: #fff;
display: flex;
font-size: 30px;
height: 30px;
justify-content: center;
opacity: .8;
text-decoration: none;
width: 50%;
z-index: 1;
}
.light-theme .slider-nav {
background-color: #ddd;
}
.dark-theme .slider-nav {
background-color: #0005;
}
@media (prefers-color-scheme: light) {
.no-theme .slider-nav {
background-color: #ddd;
}
}
@media (prefers-color-scheme: dark) {
.no-theme .slider-nav {
background-color: #0005;
}
}

View File

@ -13,6 +13,7 @@ body {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
min-height: 100vh; min-height: 100vh;
margin: auto;
} }
.h-box { .h-box {
@ -197,6 +198,7 @@ img.thumbnail {
display: block; /* See: https://stackoverflow.com/a/11635197 */ display: block; /* See: https://stackoverflow.com/a/11635197 */
width: 100%; width: 100%;
object-fit: cover; object-fit: cover;
aspect-ratio: 16 / 9;
} }
.thumbnail-placeholder { .thumbnail-placeholder {

View File

@ -10,7 +10,7 @@ var notifications, delivered;
var notifications_mock = { close: function () { } }; var notifications_mock = { close: function () { } };
function get_subscriptions() { function get_subscriptions() {
helpers.xhr('GET', '/api/v1/auth/subscriptions?fields=authorId', { helpers.xhr('GET', '/api/v1/auth/subscriptions', {
retries: 5, retries: 5,
entity_name: 'subscriptions' entity_name: 'subscriptions'
}, { }, {
@ -22,7 +22,7 @@ function create_notification_stream(subscriptions) {
// sse.js can't be replaced to EventSource in place as it lack support of payload and headers // sse.js can't be replaced to EventSource in place as it lack support of payload and headers
// see https://developer.mozilla.org/en-US/docs/Web/API/EventSource/EventSource // see https://developer.mozilla.org/en-US/docs/Web/API/EventSource/EventSource
notifications = new SSE( notifications = new SSE(
'/api/v1/auth/notifications?fields=videoId,title,author,authorId,publishedText,published,authorThumbnails,liveNow', { '/api/v1/auth/notifications', {
withCredentials: true, withCredentials: true,
payload: 'topics=' + subscriptions.map(function (subscription) { return subscription.authorId; }).join(','), payload: 'topics=' + subscriptions.map(function (subscription) { return subscription.authorId; }).join(','),
headers: { 'Content-Type': 'application/x-www-form-urlencoded' } headers: { 'Content-Type': 'application/x-www-form-urlencoded' }

View File

@ -747,6 +747,17 @@ if (navigator.vendor === 'Apple Computer, Inc.' && video_data.params.listen) {
}); });
} }
// Safari screen timeout on looped video playback fix
if (navigator.vendor === 'Apple Computer, Inc.' && !video_data.params.listen && video_data.params.video_loop) {
player.loop(false);
player.ready(function () {
player.on('ended', function () {
player.currentTime(0);
player.play();
});
});
}
// Watch on Invidious link // Watch on Invidious link
if (location.pathname.startsWith('/embed/')) { if (location.pathname.startsWith('/embed/')) {
const Button = videojs.getComponent('Button'); const Button = videojs.getComponent('Button');

View File

@ -392,27 +392,6 @@ jobs:
enable: true enable: true
# -----------------------------
# Captcha API
# -----------------------------
##
## URL of the captcha solving service.
##
## Accepted values: any URL
## Default: https://api.anti-captcha.com
##
#captcha_api_url: https://api.anti-captcha.com
##
## API key for the captcha solving service.
##
## Accepted values: a string
## Default: <none>
##
#captcha_key:
# ----------------------------- # -----------------------------
# Miscellaneous # Miscellaneous
# ----------------------------- # -----------------------------

View File

@ -36,8 +36,6 @@ services:
interval: 30s interval: 30s
timeout: 5s timeout: 5s
retries: 2 retries: 2
depends_on:
- invidious-db
invidious-db: invidious-db:
image: docker.io/library/postgres:14 image: docker.io/library/postgres:14

View File

@ -33,7 +33,7 @@ RUN if [[ "${release}" == 1 ]] ; then \
fi fi
FROM alpine:3.18 FROM alpine:3.18
RUN apk add --no-cache librsvg ttf-opensans tini RUN apk add --no-cache rsvg-convert ttf-opensans tini
WORKDIR /invidious WORKDIR /invidious
RUN addgroup -g 1000 -S invidious && \ RUN addgroup -g 1000 -S invidious && \
adduser -u 1000 -S invidious -G invidious adduser -u 1000 -S invidious -G invidious

View File

@ -33,7 +33,7 @@ RUN if [[ "${release}" == 1 ]] ; then \
fi fi
FROM alpine:3.18 FROM alpine:3.18
RUN apk add --no-cache librsvg ttf-opensans tini RUN apk add --no-cache rsvg-convert ttf-opensans tini
WORKDIR /invidious WORKDIR /invidious
RUN addgroup -g 1000 -S invidious && \ RUN addgroup -g 1000 -S invidious && \
adduser -u 1000 -S invidious -G invidious adduser -u 1000 -S invidious -G invidious

View File

@ -41,7 +41,7 @@
"Time (h:mm:ss):": "الوقت (h:mm:ss):", "Time (h:mm:ss):": "الوقت (h:mm:ss):",
"Text CAPTCHA": "نص الكابتشا", "Text CAPTCHA": "نص الكابتشا",
"Image CAPTCHA": "صورة الكابتشا", "Image CAPTCHA": "صورة الكابتشا",
"Sign In": "تسجيل الدخول", "Sign In": "إنشاء حساب",
"Register": "التسجيل", "Register": "التسجيل",
"E-mail": "البريد الإلكتروني", "E-mail": "البريد الإلكتروني",
"Preferences": "الإعدادات", "Preferences": "الإعدادات",
@ -554,5 +554,7 @@
"generic_channels_count_2": "{{count}} قناتان", "generic_channels_count_2": "{{count}} قناتان",
"generic_channels_count_3": "{{count}} قنوات", "generic_channels_count_3": "{{count}} قنوات",
"generic_channels_count_4": "{{count}} قنوات", "generic_channels_count_4": "{{count}} قنوات",
"generic_channels_count_5": "{{count}} قناة" "generic_channels_count_5": "{{count}} قناة",
"Import YouTube watch history (.json)": "استيراد سجل مشاهدة YouTube بصيغة (.json)",
"toggle_theme": "تبديل الموضوع"
} }

View File

@ -486,5 +486,6 @@
"preferences_annotations_label": "Покажи анотаций по подразбиране: ", "preferences_annotations_label": "Покажи анотаций по подразбиране: ",
"generic_views_count": "{{count}} гледане", "generic_views_count": "{{count}} гледане",
"generic_views_count_plural": "{{count}} гледания", "generic_views_count_plural": "{{count}} гледания",
"Next page": "Следваща страница" "Next page": "Следваща страница",
"Import YouTube watch history (.json)": "Импортиране на историята на гледане от YouTube (.json)"
} }

View File

@ -486,5 +486,6 @@
"generic_channels_count_plural": "{{count}} canals", "generic_channels_count_plural": "{{count}} canals",
"generic_button_edit": "Edita", "generic_button_edit": "Edita",
"generic_button_rss": "RSS", "generic_button_rss": "RSS",
"generic_button_delete": "Suprimeix" "generic_button_delete": "Suprimeix",
"Import YouTube watch history (.json)": "Importa l'historial de visualitzacions de YouTube (.json)"
} }

View File

@ -503,5 +503,7 @@
"playlist_button_add_items": "Přidat videa", "playlist_button_add_items": "Přidat videa",
"generic_channels_count_0": "{{count}} kanál", "generic_channels_count_0": "{{count}} kanál",
"generic_channels_count_1": "{{count}} kanály", "generic_channels_count_1": "{{count}} kanály",
"generic_channels_count_2": "{{count}} kanálů" "generic_channels_count_2": "{{count}} kanálů",
"Import YouTube watch history (.json)": "Importovat historii sledování z YouTube (.json)",
"toggle_theme": "Přepnout motiv"
} }

View File

@ -452,5 +452,40 @@
"crash_page_you_found_a_bug": "Det ser ud til, at du har fundet en fejl i Invidious!", "crash_page_you_found_a_bug": "Det ser ud til, at du har fundet en fejl i Invidious!",
"crash_page_read_the_faq": "læs <a href=\"`x`\">Ofte stillede spørgsmål (FAQ)</a>", "crash_page_read_the_faq": "læs <a href=\"`x`\">Ofte stillede spørgsmål (FAQ)</a>",
"crash_page_search_issue": "søgte efter <a href=\"`x`\">eksisterende problemer på GitHub</a>", "crash_page_search_issue": "søgte efter <a href=\"`x`\">eksisterende problemer på GitHub</a>",
"search_filters_title": "Filter" "search_filters_title": "Filter",
"playlist_button_add_items": "Tilføj videoer",
"search_message_no_results": "Ingen resultater fundet.",
"Import YouTube watch history (.json)": "Importer YouTube afspilningshistorik (.json)",
"search_message_change_filters_or_query": "Prøv at udvide din søgeforspørgsel og/eller ændre filtrene.",
"search_message_use_another_instance": " Du kan også <a href=\"`x`\">søge på en anden instans</a>.",
"Music in this video": "Musik i denne video",
"search_filters_date_option_none": "Enhver dato",
"search_filters_type_option_all": "Enhver type",
"search_filters_duration_option_none": "Enhver varighed",
"search_filters_duration_option_medium": "Medium (4 - 20 minutter)",
"search_filters_features_option_vr180": "VR180",
"generic_channels_count": "{{count}} kanal",
"generic_channels_count_plural": "{{count}} kanaler",
"Import YouTube playlist (.csv)": "Importer YouTube playliste (.csv)",
"Standard YouTube license": "Standard Youtube-licens",
"Album: ": "Album: ",
"Channel Sponsor": "Kanal-sponsor",
"Song: ": "Sang: ",
"channel_tab_playlists_label": "Playlister",
"channel_tab_channels_label": "Kanaler",
"Artist: ": "Kunstner: ",
"search_filters_date_label": "Uploaddato",
"generic_button_delete": "Slet",
"generic_button_edit": "Rediger",
"generic_button_save": "Gem",
"generic_button_cancel": "Afbryd",
"generic_button_rss": "RSS",
"Popular enabled: ": "Populær aktiveret: ",
"search_filters_apply_button": "Anvend udvalgte filtre",
"channel_tab_shorts_label": "Shorts",
"channel_tab_streams_label": "Livestreams",
"channel_tab_podcasts_label": "Podcasts",
"channel_tab_releases_label": "Udgivelser",
"Download is disabled": "Download er slået fra",
"error_video_not_in_playlist": "Den ønskede video findes ikke i denne playliste. <a href=\"`x`\">Klik her for playlistens startside.</a>"
} }

View File

@ -148,7 +148,7 @@
"Whitelisted regions: ": "Erlaubte Regionen: ", "Whitelisted regions: ": "Erlaubte Regionen: ",
"Blacklisted regions: ": "Unerlaubte Regionen: ", "Blacklisted regions: ": "Unerlaubte Regionen: ",
"Shared `x`": "Geteilt `x`", "Shared `x`": "Geteilt `x`",
"Premieres in `x`": "Zuerst gesehen in `x`", "Premieres in `x`": "Premiere in `x`",
"Premieres `x`": "Erster Start `x`", "Premieres `x`": "Erster Start `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.": "Hallo! Anscheinend haben Sie JavaScript deaktiviert. Klicken Sie hier um Kommentare anzuzeigen, beachten sie dass es etwas länger dauern kann um sie zu laden.", "Hi! Looks like you have JavaScript turned off. Click here to view comments, keep in mind they may take a bit longer to load.": "Hallo! Anscheinend haben Sie JavaScript deaktiviert. Klicken Sie hier um Kommentare anzuzeigen, beachten sie dass es etwas länger dauern kann um sie zu laden.",
"View YouTube comments": "YouTube Kommentare anzeigen", "View YouTube comments": "YouTube Kommentare anzeigen",
@ -486,5 +486,6 @@
"channel_tab_podcasts_label": "Podcasts", "channel_tab_podcasts_label": "Podcasts",
"channel_tab_releases_label": "Veröffentlichungen", "channel_tab_releases_label": "Veröffentlichungen",
"generic_channels_count": "{{count}} Kanal", "generic_channels_count": "{{count}} Kanal",
"generic_channels_count_plural": "{{count}} Kanäle" "generic_channels_count_plural": "{{count}} Kanäle",
"Import YouTube watch history (.json)": "YouTube Wiedergabeverlauf importieren (.json)"
} }

View File

@ -1,4 +1,9 @@
{ {
"Add to playlist": "Add to playlist",
"Add to playlist: ": "Add to playlist: ",
"Answer": "Answer",
"Search for videos": "Search for videos",
"The Popular feed has been disabled by the administrator.": "The Popular feed has been disabled by the administrator.",
"generic_channels_count": "{{count}} channel", "generic_channels_count": "{{count}} channel",
"generic_channels_count_plural": "{{count}} channels", "generic_channels_count_plural": "{{count}} channels",
"generic_views_count": "{{count}} view", "generic_views_count": "{{count}} view",
@ -487,5 +492,9 @@
"channel_tab_releases_label": "Releases", "channel_tab_releases_label": "Releases",
"channel_tab_playlists_label": "Playlists", "channel_tab_playlists_label": "Playlists",
"channel_tab_community_label": "Community", "channel_tab_community_label": "Community",
"channel_tab_channels_label": "Channels" "channel_tab_channels_label": "Channels",
"toggle_theme": "Toggle Theme",
"carousel_slide": "Slide {{current}} of {{total}}",
"carousel_skip": "Skip the Carousel",
"carousel_go_to": "Go to slide `x`"
} }

View File

@ -90,7 +90,7 @@
"preferences_notifications_only_label": "Mostrar solo notificaciones (si hay alguna): ", "preferences_notifications_only_label": "Mostrar solo notificaciones (si hay alguna): ",
"Enable web notifications": "Habilitar notificaciones web", "Enable web notifications": "Habilitar notificaciones web",
"`x` uploaded a video": "`x` subió un video", "`x` uploaded a video": "`x` subió un video",
"`x` is live": "`x` esta en vivo", "`x` is live": "`x` está en directo",
"preferences_category_data": "Preferencias de los datos", "preferences_category_data": "Preferencias de los datos",
"Clear watch history": "Borrar el historial de reproducción", "Clear watch history": "Borrar el historial de reproducción",
"Import/export data": "Importar/Exportar datos", "Import/export data": "Importar/Exportar datos",
@ -102,7 +102,7 @@
"preferences_category_admin": "Preferencias de administrador", "preferences_category_admin": "Preferencias de administrador",
"preferences_default_home_label": "Página de inicio por defecto: ", "preferences_default_home_label": "Página de inicio por defecto: ",
"preferences_feed_menu_label": "Menú de fuentes: ", "preferences_feed_menu_label": "Menú de fuentes: ",
"preferences_show_nick_label": "Mostrar nombre de usuario arriba: ", "preferences_show_nick_label": "Mostrar nombre de usuario encima: ",
"Top enabled: ": "¿Habilitar los destacados? ", "Top enabled: ": "¿Habilitar los destacados? ",
"CAPTCHA enabled: ": "¿Habilitar los CAPTCHA? ", "CAPTCHA enabled: ": "¿Habilitar los CAPTCHA? ",
"Login enabled: ": "¿Habilitar el inicio de sesión? ", "Login enabled: ": "¿Habilitar el inicio de sesión? ",
@ -144,13 +144,13 @@
"License: ": "Licencia: ", "License: ": "Licencia: ",
"Family friendly? ": "¿Filtrar contenidos? ", "Family friendly? ": "¿Filtrar contenidos? ",
"Wilson score: ": "Puntuación Wilson: ", "Wilson score: ": "Puntuación Wilson: ",
"Engagement: ": "Compromiso: ", "Engagement: ": "Retención: ",
"Whitelisted regions: ": "Regiones permitidas: ", "Whitelisted regions: ": "Regiones permitidas: ",
"Blacklisted regions: ": "Regiones bloqueadas: ", "Blacklisted regions: ": "Regiones bloqueadas: ",
"Shared `x`": "Compartido `x`", "Shared `x`": "Compartido `x`",
"Premieres in `x`": "Se estrena en `x`", "Premieres in `x`": "Se estrena en `x`",
"Premieres `x`": "Estrenos `x`", "Premieres `x`": "Estrenos `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.": "¡Hola! Parece que tienes JavaScript desactivado. Haz clic aquí para ver los comentarios, pero tengas en cuenta que pueden tardar un poco más en cargarse.", "Hi! Looks like you have JavaScript turned off. Click here to view comments, keep in mind they may take a bit longer to load.": "¡Hola! Parece que tienes JavaScript desactivado. Haz clic aquí para ver los comentarios, ten en cuenta que pueden tardar un poco más en cargar.",
"View YouTube comments": "Ver los comentarios de YouTube", "View YouTube comments": "Ver los comentarios de YouTube",
"View more comments on Reddit": "Ver más comentarios en Reddit", "View more comments on Reddit": "Ver más comentarios en Reddit",
"View `x` comments": { "View `x` comments": {
@ -312,7 +312,7 @@
"Download as: ": "Descargar como: ", "Download as: ": "Descargar como: ",
"%A %B %-d, %Y": "%A %B %-d, %Y", "%A %B %-d, %Y": "%A %B %-d, %Y",
"(edited)": "(editado)", "(edited)": "(editado)",
"YouTube comment permalink": "Enlace permanente de YouTube del comentario", "YouTube comment permalink": "Enlace permanente de comentario de YouTube",
"permalink": "enlace permanente", "permalink": "enlace permanente",
"`x` marked it with a ❤": "`x` lo ha marcado con un ❤", "`x` marked it with a ❤": "`x` lo ha marcado con un ❤",
"Audio mode": "Modo de audio", "Audio mode": "Modo de audio",
@ -324,10 +324,10 @@
"search_filters_sort_option_rating": "Valoración", "search_filters_sort_option_rating": "Valoración",
"search_filters_sort_option_date": "Fecha de subida", "search_filters_sort_option_date": "Fecha de subida",
"search_filters_sort_option_views": "Visualizaciones", "search_filters_sort_option_views": "Visualizaciones",
"search_filters_type_label": "tipo de contenido", "search_filters_type_label": "Tipo de contenido",
"search_filters_duration_label": "duración", "search_filters_duration_label": "Duración",
"search_filters_features_label": "funcionalidades", "search_filters_features_label": "Funcionalidades",
"search_filters_sort_label": "ordenar", "search_filters_sort_label": "Ordenar",
"search_filters_date_option_hour": "Última hora", "search_filters_date_option_hour": "Última hora",
"search_filters_date_option_today": "Hoy", "search_filters_date_option_today": "Hoy",
"search_filters_date_option_week": "Esta semana", "search_filters_date_option_week": "Esta semana",
@ -390,43 +390,58 @@
"search_filters_features_option_three_sixty": "360°", "search_filters_features_option_three_sixty": "360°",
"videoinfo_watch_on_youTube": "Ver en YouTube", "videoinfo_watch_on_youTube": "Ver en YouTube",
"preferences_save_player_pos_label": "Guardar posición de reproducción: ", "preferences_save_player_pos_label": "Guardar posición de reproducción: ",
"generic_views_count": "{{count}} visualización", "generic_views_count_0": "{{count}} visualización",
"generic_views_count_plural": "{{count}} visualizaciones", "generic_views_count_1": "{{count}} visualizaciones",
"generic_subscribers_count": "{{count}} suscriptor", "generic_views_count_2": "{{count}} visualizaciones",
"generic_subscribers_count_plural": "{{count}} suscriptores", "generic_subscribers_count_0": "{{count}} suscriptor",
"generic_subscriptions_count": "{{count}} suscripción", "generic_subscribers_count_1": "{{count}} suscriptores",
"generic_subscriptions_count_plural": "{{count}} suscripciones", "generic_subscribers_count_2": "{{count}} suscriptores",
"subscriptions_unseen_notifs_count": "{{count}} notificación no vista", "generic_subscriptions_count_0": "{{count}} suscripción",
"subscriptions_unseen_notifs_count_plural": "{{count}} notificaciones no vistas", "generic_subscriptions_count_1": "{{count}} suscripciones",
"generic_count_days": "{{count}} día", "generic_subscriptions_count_2": "{{count}} suscripciones",
"generic_count_days_plural": "{{count}} días", "subscriptions_unseen_notifs_count_0": "{{count}} notificación sin ver",
"comments_view_x_replies": "Ver {{count}} respuesta", "subscriptions_unseen_notifs_count_1": "{{count}} notificaciones sin ver",
"comments_view_x_replies_plural": "Ver {{count}} respuestas", "subscriptions_unseen_notifs_count_2": "{{count}} notificaciones sin ver",
"generic_count_weeks": "{{count}} semana", "generic_count_days_0": "{{count}} día",
"generic_count_weeks_plural": "{{count}} semanas", "generic_count_days_1": "{{count}} días",
"generic_playlists_count": "{{count}} lista de reproducción", "generic_count_days_2": "{{count}} días",
"generic_playlists_count_plural": "{{count}} listas de reproducciones", "comments_view_x_replies_0": "Ver {{count}} respuesta",
"generic_videos_count": "{{count}} video", "comments_view_x_replies_1": "Ver {{count}} respuestas",
"generic_videos_count_plural": "{{count}} video", "comments_view_x_replies_2": "Ver {{count}} respuestas",
"generic_count_months": "{{count}} mes", "generic_count_weeks_0": "{{count}} semana",
"generic_count_months_plural": "{{count}} meses", "generic_count_weeks_1": "{{count}} semanas",
"comments_points_count": "{{count}} punto", "generic_count_weeks_2": "{{count}} semanas",
"comments_points_count_plural": "{{count}} puntos", "generic_playlists_count_0": "{{count}} lista de reproducción",
"generic_count_years": "{{count}} año", "generic_playlists_count_1": "{{count}} listas de reproducciones",
"generic_count_years_plural": "{{count}} años", "generic_playlists_count_2": "{{count}} listas de reproducciones",
"generic_count_hours": "{{count}} hora", "generic_videos_count_0": "{{count}} video",
"generic_count_hours_plural": "{{count}} horas", "generic_videos_count_1": "{{count}} videos",
"generic_count_minutes": "{{count}} minuto", "generic_videos_count_2": "{{count}} videos",
"generic_count_minutes_plural": "{{count}} minutos", "generic_count_months_0": "{{count}} mes",
"generic_count_seconds": "{{count}} segundo", "generic_count_months_1": "{{count}} meses",
"generic_count_seconds_plural": "{{count}} segundos", "generic_count_months_2": "{{count}} meses",
"comments_points_count_0": "{{count}} punto",
"comments_points_count_1": "{{count}} puntos",
"comments_points_count_2": "{{count}} puntos",
"generic_count_years_0": "{{count}} año",
"generic_count_years_1": "{{count}} años",
"generic_count_years_2": "{{count}} años",
"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",
"crash_page_before_reporting": "Antes de notificar un error asegúrate de que has:", "crash_page_before_reporting": "Antes de notificar un error asegúrate de que has:",
"crash_page_switch_instance": "probado a <a href=\"`x`\">usar otra instancia</a>", "crash_page_switch_instance": "probado a <a href=\"`x`\">usar otra instancia</a>",
"crash_page_read_the_faq": "leído las <a href=\"`x`\">Preguntas Frecuentes</a>", "crash_page_read_the_faq": "leído las <a href=\"`x`\">Preguntas Frecuentes</a>",
"crash_page_search_issue": "buscado <a href=\"`x`\">problemas existentes en GitHub</a>", "crash_page_search_issue": "buscado <a href=\"`x`\">problemas existentes en GitHub</a>",
"crash_page_you_found_a_bug": "¡Parece que has encontrado un error en Invidious!", "crash_page_you_found_a_bug": "¡Parece que has encontrado un error en Invidious!",
"crash_page_refresh": "probado a <a href=\"`x`\">recargar la página</a>", "crash_page_refresh": "probado a <a href=\"`x`\">recargar la página</a>",
"crash_page_report_issue": "Si nada de lo anterior ha sido de ayuda, por favor, <a href=\"`x`\">abre una nueva incidencia en GitHub</a> (preferiblemente en inglés) e incluye verbatim el siguiente texto en tu mensaje:", "crash_page_report_issue": "Si nada de lo anterior ha sido de ayuda, por favor, <a href=\"`x`\">abre una nueva incidencia en GitHub</a> (preferiblemente en inglés) e incluye el siguiente texto en tu mensaje (NO traduzcas este texto):",
"English (United States)": "Inglés (Estados Unidos)", "English (United States)": "Inglés (Estados Unidos)",
"Cantonese (Hong Kong)": "Cantonés (Hong Kong)", "Cantonese (Hong Kong)": "Cantonés (Hong Kong)",
"Dutch (auto-generated)": "Neerlandés (generados automáticamente)", "Dutch (auto-generated)": "Neerlandés (generados automáticamente)",
@ -454,14 +469,15 @@
"search_message_no_results": "No se han encontrado resultados.", "search_message_no_results": "No se han encontrado resultados.",
"search_message_change_filters_or_query": "Pruebe ampliar la consulta de búsqueda y/o a cambiar los filtros.", "search_message_change_filters_or_query": "Pruebe ampliar la consulta de búsqueda y/o a cambiar los filtros.",
"search_filters_title": "Filtros", "search_filters_title": "Filtros",
"search_filters_date_label": "fecha de subida", "search_filters_date_label": "Fecha de subida",
"search_filters_date_option_none": "Cualquier fecha", "search_filters_date_option_none": "Cualquier fecha",
"search_filters_type_option_all": "Cualquier tipo", "search_filters_type_option_all": "Cualquier tipo",
"search_filters_duration_option_none": "Cualquier duración", "search_filters_duration_option_none": "Cualquier duración",
"search_filters_features_option_vr180": "VR180", "search_filters_features_option_vr180": "VR180",
"search_filters_apply_button": "Aplicar filtros", "search_filters_apply_button": "Aplicar filtros",
"tokens_count": "{{count}} token", "tokens_count_0": "{{count}} token",
"tokens_count_plural": "{{count}} tokens", "tokens_count_1": "{{count}} tokens",
"tokens_count_2": "{{count}} tokens",
"search_message_use_another_instance": " También puede <a href=\"`x`\">buscar en otra instancia</a>.", "search_message_use_another_instance": " También puede <a href=\"`x`\">buscar en otra instancia</a>.",
"Popular enabled: ": "¿Habilitar la sección popular? ", "Popular enabled: ": "¿Habilitar la sección popular? ",
"error_video_not_in_playlist": "El video que solicitaste no existe en esta lista de reproducción. <a href=\"`x`\">Haz clic aquí para acceder a la página de inicio de la lista de reproducción.</a>", "error_video_not_in_playlist": "El video que solicitaste no existe en esta lista de reproducción. <a href=\"`x`\">Haz clic aquí para acceder a la página de inicio de la lista de reproducción.</a>",
@ -485,6 +501,9 @@
"generic_button_rss": "RSS", "generic_button_rss": "RSS",
"channel_tab_podcasts_label": "Podcasts", "channel_tab_podcasts_label": "Podcasts",
"channel_tab_releases_label": "Publicaciones", "channel_tab_releases_label": "Publicaciones",
"generic_channels_count": "{{count}} canal", "generic_channels_count_0": "{{count}} canal",
"generic_channels_count_plural": "{{count}} canales" "generic_channels_count_1": "{{count}} canales",
"generic_channels_count_2": "{{count}} canales",
"Import YouTube watch history (.json)": "Importar el historial de las visualizaciones de YouTube (.json)",
"toggle_theme": "Alternar tema"
} }

View File

@ -1,9 +1,14 @@
{ {
"generic_views_count_0": "{{count}} بازدید", "generic_views_count": "{{count}} بازدید",
"generic_videos_count_0": "{{count}} ویدئو", "generic_views_count_plural": "{{count}} بازدید",
"generic_playlists_count_0": "{{count}} فهرست پخش", "generic_videos_count": "{{count}} ویدئو",
"generic_subscribers_count_0": "{{count}} دنبال کننده", "generic_videos_count_plural": "{{count}} ویدئو",
"generic_subscriptions_count_0": "{{count}} اشتراک ها", "generic_playlists_count": "{{count}} فهرست پخش",
"generic_playlists_count_plural": "{{count}} فهرست پخش",
"generic_subscribers_count": "{{count}} دنبال کننده",
"generic_subscribers_count_plural": "{{count}} دنبال کننده",
"generic_subscriptions_count": "{{count}} اشتراک",
"generic_subscriptions_count_plural": "{{count}} اشتراک",
"LIVE": "زنده", "LIVE": "زنده",
"Shared `x` ago": "`x` پیش به اشتراک گذاشته شده", "Shared `x` ago": "`x` پیش به اشتراک گذاشته شده",
"Unsubscribe": "لغو اشتراک", "Unsubscribe": "لغو اشتراک",
@ -117,13 +122,15 @@
"Subscription manager": "مدیریت اشتراک", "Subscription manager": "مدیریت اشتراک",
"Token manager": "مدیر توکن", "Token manager": "مدیر توکن",
"Token": "توکن", "Token": "توکن",
"tokens_count_0": "{{count}} توکن ها", "tokens_count": "{{count}} توکن",
"tokens_count_plural": "{{count}} توکن",
"Import/export": "وارد کردن/خارج کردن", "Import/export": "وارد کردن/خارج کردن",
"unsubscribe": "لغو اشتراک", "unsubscribe": "لغو اشتراک",
"revoke": "ابطال", "revoke": "ابطال",
"Subscriptions": "اشتراک ها", "Subscriptions": "اشتراک ها",
"subscriptions_unseen_notifs_count_0": "{{count}} اعلان نادیده", "subscriptions_unseen_notifs_count": "{{count}} اعلان نادیده",
"search": "جستجو", "subscriptions_unseen_notifs_count_plural": "{{count}} اعلان نادیده",
"search": "جست و جو",
"Log out": "خروج", "Log out": "خروج",
"Released under the AGPLv3 on Github.": "منتشر شده تحت پروانه AGPLv3 روی گیت‌هاب.", "Released under the AGPLv3 on Github.": "منتشر شده تحت پروانه AGPLv3 روی گیت‌هاب.",
"Source available here.": "منبع اینجا دردسترس است.", "Source available here.": "منبع اینجا دردسترس است.",
@ -183,10 +190,12 @@
"This channel does not exist.": "این کانال وجود ندارد.", "This channel does not exist.": "این کانال وجود ندارد.",
"Could not get channel info.": "نمیتوان اطلاعات کانال را دریافت کرد.", "Could not get channel info.": "نمیتوان اطلاعات کانال را دریافت کرد.",
"Could not fetch comments": "نمیتوان نظرات را دریافت کرد", "Could not fetch comments": "نمیتوان نظرات را دریافت کرد",
"comments_view_x_replies_0": "نمایش {{count}} پاسخ ها", "comments_view_x_replies": "نمایش {{count}} پاسخ",
"comments_view_x_replies_plural": "نمایش {{count}} پاسخ",
"`x` ago": "`x` پیش", "`x` ago": "`x` پیش",
"Load more": "بارگذاری بیشتر", "Load more": "بارگذاری بیشتر",
"comments_points_count_0": "{{count}} نقطه ها", "comments_points_count": "{{count}} نقطه",
"comments_points_count_plural": "{{count}} نقطه",
"Could not create mix.": "نمیتوان میکس ساخت.", "Could not create mix.": "نمیتوان میکس ساخت.",
"Empty playlist": "سیاههٔ پخش خالی", "Empty playlist": "سیاههٔ پخش خالی",
"Not a playlist.": "یک سیاههٔ پخش نیست.", "Not a playlist.": "یک سیاههٔ پخش نیست.",
@ -304,16 +313,23 @@
"Yiddish": "ییدیش", "Yiddish": "ییدیش",
"Yoruba": "یوروبایی", "Yoruba": "یوروبایی",
"Zulu": "زولو", "Zulu": "زولو",
"generic_count_years_0": "{{count}} سال", "generic_count_years": "{{count}} سال",
"generic_count_months_0": "{{count}} ماه", "generic_count_years_plural": "{{count}} سال",
"generic_count_weeks_0": "{{count}} هفته", "generic_count_months": "{{count}} ماه",
"generic_count_days_0": "{{count}} روز", "generic_count_months_plural": "{{count}} ماه",
"generic_count_hours_0": "{{count}} ساعت", "generic_count_weeks": "{{count}} هفته",
"generic_count_minutes_0": "{{count}} دقیقه", "generic_count_weeks_plural": "{{count}} هفته",
"generic_count_seconds_0": "{{count}} ثانیه", "generic_count_days": "{{count}} روز",
"generic_count_days_plural": "{{count}} روز",
"generic_count_hours": "{{count}} ساعت",
"generic_count_hours_plural": "{{count}} ساعت",
"generic_count_minutes": "{{count}} دقیقه",
"generic_count_minutes_plural": "{{count}} دقیقه",
"generic_count_seconds": "{{count}} ثانیه",
"generic_count_seconds_plural": "{{count}} ثانیه",
"Fallback comments: ": "نظرات عقب گرد: ", "Fallback comments: ": "نظرات عقب گرد: ",
"Popular": "محبوب", "Popular": "محبوب",
"Search": "جستجو", "Search": "جست و جو",
"Top": "بالا", "Top": "بالا",
"About": "درباره", "About": "درباره",
"Rating: ": "رتبه دهی: ", "Rating: ": "رتبه دهی: ",
@ -445,5 +461,28 @@
"Song: ": "آهنگ: ", "Song: ": "آهنگ: ",
"Channel Sponsor": "اسپانسر کانال", "Channel Sponsor": "اسپانسر کانال",
"Standard YouTube license": "پروانه استاندارد YouTube", "Standard YouTube license": "پروانه استاندارد YouTube",
"search_message_use_another_instance": " شما همچنین می‌توانید <a href=\"`x`\">در نمونه دیگر هم جستجو کنید</a>." "search_message_use_another_instance": " شما همچنین می‌توانید <a href=\"`x`\">در نمونه دیگر هم جستجو کنید</a>.",
"Download is disabled": "دریافت غیرفعال است",
"crash_page_before_reporting": "پیش از گزارش ایراد، مطمئنید شوید که:",
"playlist_button_add_items": "افزودن ویدیو",
"user_saved_playlists": "فهرست‌های پخش ذخیره شده",
"crash_page_refresh": "که صفحه را <a href=\"`x`\">بازنشانی</a> کرده‌اید",
"generic_button_save": "ذخیره",
"generic_button_cancel": "لغو",
"generic_channels_count": "{{count}} کانال",
"generic_channels_count_plural": "{{count}} کانال",
"generic_button_edit": "ویرایش",
"crash_page_switch_instance": "که تلاش کرده‌اید <a href=\"`x`\">از یک نمونهٔ دیگر</a> استفاده کنید",
"generic_button_rss": "خوراک RSS",
"crash_page_read_the_faq": "که <a href=\"`x`\">سوالات بیشتر پرسیده شده (FAQ)</a> را خوانده‌اید",
"generic_button_delete": "حذف",
"Import YouTube playlist (.csv)": "واردکردن فهرست‌پخش YouTube (.csv)",
"Import YouTube watch history (.json)": "وارد کردن فهرست پخش YouTube (.json)",
"crash_page_you_found_a_bug": "به نظر می‌رسد که ایرادی در Invidious پیدا کرده‌اید!",
"channel_tab_podcasts_label": "پادکست‌ها",
"channel_tab_streams_label": "پخش زنده‌ها",
"channel_tab_shorts_label": "Shortها",
"channel_tab_playlists_label": "فهرست‌های پخش",
"channel_tab_channels_label": "کانال‌ها",
"error_video_not_in_playlist": "ویدیوی درخواستی معلق به این فهرست پخش نیست. <a href=\"`x`\">کلیک کنید تا به صفحهٔ اصلی فهرست پخش بروید.</a>"
} }

View File

@ -14,7 +14,7 @@
"Clear watch history?": "Tyhjennä katseluhistoria?", "Clear watch history?": "Tyhjennä katseluhistoria?",
"New password": "Uusi salasana", "New password": "Uusi salasana",
"New passwords must match": "Uusien salasanojen täytyy täsmätä", "New passwords must match": "Uusien salasanojen täytyy täsmätä",
"Authorize token?": "Valuutetaanko tunnus?", "Authorize token?": "Valtuutetaanko tunnus?",
"Authorize token for `x`?": "Valtuutetaanko tunnus `x`:lle?", "Authorize token for `x`?": "Valtuutetaanko tunnus `x`:lle?",
"Yes": "Kyllä", "Yes": "Kyllä",
"No": "Ei", "No": "Ei",

View File

@ -503,5 +503,6 @@
"Download is disabled": "Le téléchargement est désactivé", "Download is disabled": "Le téléchargement est désactivé",
"Import YouTube playlist (.csv)": "Importer des listes de lecture de Youtube (.csv)", "Import YouTube playlist (.csv)": "Importer des listes de lecture de Youtube (.csv)",
"channel_tab_releases_label": "Parutions", "channel_tab_releases_label": "Parutions",
"channel_tab_podcasts_label": "Émissions audio" "channel_tab_podcasts_label": "Émissions audio",
"Import YouTube watch history (.json)": "Importer l'historique de visionnement YouTube (.json)"
} }

View File

@ -476,7 +476,7 @@
"generic_button_cancel": "रद्द करें", "generic_button_cancel": "रद्द करें",
"generic_button_rss": "आरएसएस", "generic_button_rss": "आरएसएस",
"generic_button_edit": "संपादित करें", "generic_button_edit": "संपादित करें",
"generic_button_delete": "मिटाएं", "generic_button_delete": "टाएं",
"playlist_button_add_items": "वीडियो जोड़ें", "playlist_button_add_items": "वीडियो जोड़ें",
"Song: ": "गाना: ", "Song: ": "गाना: ",
"channel_tab_podcasts_label": "पाॅडकास्ट", "channel_tab_podcasts_label": "पाॅडकास्ट",
@ -484,5 +484,8 @@
"Import YouTube playlist (.csv)": "YouTube प्लेलिस्ट (.csv) आयात करें", "Import YouTube playlist (.csv)": "YouTube प्लेलिस्ट (.csv) आयात करें",
"Standard YouTube license": "मानक यूट्यूब लाइसेंस", "Standard YouTube license": "मानक यूट्यूब लाइसेंस",
"Channel Sponsor": "चैनल प्रायोजक", "Channel Sponsor": "चैनल प्रायोजक",
"Download is disabled": "डाउनलोड करना अक्षम है" "Download is disabled": "डाउनलोड करना अक्षम है",
"generic_channels_count": "{{count}} चैनल",
"generic_channels_count_plural": "{{count}} चैनल",
"Import YouTube watch history (.json)": "YouTube पर देखने का इतिहास आयात करें (.json)"
} }

View File

@ -503,5 +503,6 @@
"channel_tab_releases_label": "Izdanja", "channel_tab_releases_label": "Izdanja",
"generic_channels_count_0": "{{count}} kanal", "generic_channels_count_0": "{{count}} kanal",
"generic_channels_count_1": "{{count}} kanala", "generic_channels_count_1": "{{count}} kanala",
"generic_channels_count_2": "{{count}} kanala" "generic_channels_count_2": "{{count}} kanala",
"Import YouTube watch history (.json)": "Uvezi YouTube povijest gledanja (.json)"
} }

41
locales/ia.json Normal file
View File

@ -0,0 +1,41 @@
{
"New password": "Nove contrasigno",
"preferences_player_style_label": "Stylo de reproductor: ",
"preferences_region_label": "Pais de contento: ",
"oldest": "plus ancian",
"published": "data de publication",
"invidious": "Invidious",
"Image CAPTCHA": "Imagine CAPTCHA",
"newest": "plus nove",
"generic_button_save": "Salvar",
"Dark mode: ": "Modo obscur: ",
"preferences_dark_mode_label": "Thema: ",
"preferences_category_subscription": "Preferentias de subscription",
"last": "ultime",
"generic_button_cancel": "Cancellar",
"popular": "popular",
"Time (h:mm:ss):": "Tempore (h:mm:ss):",
"preferences_autoplay_label": "Reproduction automatic: ",
"Sign In": "Aperir le session",
"Log in": "Initiar le session",
"preferences_speed_label": "Velocitate per predefinition: ",
"preferences_comments_label": "Commentos predefinite: ",
"light": "clar",
"No": "Non",
"youtube": "YouTube",
"LIVE": "IN DIRECTE",
"reddit": "Reddit",
"preferences_category_player": "Preferentias de reproductor",
"Preferences": "Preferentias",
"preferences_quality_dash_option_auto": "Automatic",
"dark": "obscur",
"generic_button_rss": "RSS",
"Export": "Exportar",
"History": "Chronologia",
"Password": "Contrasigno",
"User ID": "ID de usator",
"E-mail": "E-mail",
"Delete account?": "Deler conto?",
"preferences_volume_label": "Volumine del reproductor: ",
"preferences_sort_label": "Ordinar le videos per: "
}

View File

@ -469,5 +469,6 @@
"error_video_not_in_playlist": "Video yang diminta tidak ada dalam daftar putar ini. <a href=\"`x`\">Klik di sini untuk halaman beranda daftar putar.</a>", "error_video_not_in_playlist": "Video yang diminta tidak ada dalam daftar putar ini. <a href=\"`x`\">Klik di sini untuk halaman beranda daftar putar.</a>",
"generic_button_delete": "Hapus", "generic_button_delete": "Hapus",
"Import YouTube playlist (.csv)": "Impor daftar putar YouTube (.csv)", "Import YouTube playlist (.csv)": "Impor daftar putar YouTube (.csv)",
"Standard YouTube license": "Lisensi YouTube standar" "Standard YouTube license": "Lisensi YouTube standar",
"Import YouTube watch history (.json)": "Impor riwayat tontonan YouTube (.json)"
} }

View File

@ -503,5 +503,6 @@
"channel_tab_podcasts_label": "Podcast", "channel_tab_podcasts_label": "Podcast",
"generic_channels_count_0": "{{count}} canale", "generic_channels_count_0": "{{count}} canale",
"generic_channels_count_1": "{{count}} canali", "generic_channels_count_1": "{{count}} canali",
"generic_channels_count_2": "{{count}} canali" "generic_channels_count_2": "{{count}} canali",
"Import YouTube watch history (.json)": "Importa la cronologia delle visualizzazioni di YouTube (.json)"
} }

View File

@ -53,7 +53,7 @@
"preferences_category_player": "プレイヤーの設定", "preferences_category_player": "プレイヤーの設定",
"preferences_video_loop_label": "常にループ: ", "preferences_video_loop_label": "常にループ: ",
"preferences_autoplay_label": "自動再生: ", "preferences_autoplay_label": "自動再生: ",
"preferences_continue_label": "次の動画を自動再生: ", "preferences_continue_label": "次の動画に移動: ",
"preferences_continue_autoplay_label": "次の動画を自動再生: ", "preferences_continue_autoplay_label": "次の動画を自動再生: ",
"preferences_listen_label": "音声モードを使用: ", "preferences_listen_label": "音声モードを使用: ",
"preferences_local_label": "動画視聴にプロキシを経由: ", "preferences_local_label": "動画視聴にプロキシを経由: ",
@ -363,9 +363,9 @@
"search_filters_features_option_location": "場所", "search_filters_features_option_location": "場所",
"search_filters_features_option_hdr": "HDR", "search_filters_features_option_hdr": "HDR",
"Current version: ": "現在のバージョン: ", "Current version: ": "現在のバージョン: ",
"next_steps_error_message": "下記のものを試して下さい: ", "next_steps_error_message": "以下をお試してください: ",
"next_steps_error_message_refresh": "再読込", "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分未満", "search_filters_duration_option_short": "4分未満",
"footer_documentation": "説明書", "footer_documentation": "説明書",
"footer_source_code": "ソースコード", "footer_source_code": "ソースコード",
@ -459,7 +459,7 @@
"Song: ": "曲: ", "Song: ": "曲: ",
"Channel Sponsor": "チャンネルのスポンサー", "Channel Sponsor": "チャンネルのスポンサー",
"Standard YouTube license": "標準 Youtube ライセンス", "Standard YouTube license": "標準 Youtube ライセンス",
"Download is disabled": "ダウンロード: このインスタンスは未対応", "Download is disabled": "ダウンロード: このインスタンスは未対応",
"Import YouTube playlist (.csv)": "YouTube 再生リストをインポート (.csv)", "Import YouTube playlist (.csv)": "YouTube 再生リストをインポート (.csv)",
"generic_button_delete": "削除", "generic_button_delete": "削除",
"generic_button_cancel": "キャンセル", "generic_button_cancel": "キャンセル",
@ -469,5 +469,6 @@
"generic_button_save": "保存", "generic_button_save": "保存",
"generic_button_rss": "RSS", "generic_button_rss": "RSS",
"playlist_button_add_items": "動画を追加", "playlist_button_add_items": "動画を追加",
"generic_channels_count_0": "{{count}}個のチャンネル" "generic_channels_count_0": "{{count}}個のチャンネル",
"Import YouTube watch history (.json)": "YouTube 視聴履歴をインポート (.json)"
} }

View File

@ -46,7 +46,7 @@
"source": "출처", "source": "출처",
"JavaScript license information": "자바스크립트 라이선스 정보", "JavaScript license information": "자바스크립트 라이선스 정보",
"An alternative front-end to YouTube": "유튜브의 프론트엔드 대안", "An alternative front-end to YouTube": "유튜브의 프론트엔드 대안",
"History": "역사", "History": "시청 기록",
"Delete account?": "계정을 삭제 하시겠습니까?", "Delete account?": "계정을 삭제 하시겠습니까?",
"Export data as JSON": "JSON으로 데이터 내보내기", "Export data as JSON": "JSON으로 데이터 내보내기",
"Export subscriptions as OPML (for NewPipe & FreeTube)": "OPML로 구독 내보내기 (뉴파이프 및 프리튜브)", "Export subscriptions as OPML (for NewPipe & FreeTube)": "OPML로 구독 내보내기 (뉴파이프 및 프리튜브)",
@ -351,7 +351,7 @@
"News": "뉴스", "News": "뉴스",
"Gaming": "게임", "Gaming": "게임",
"Music": "음악", "Music": "음악",
"Default": "디폴트", "Default": "전체",
"Rating: ": "평점: ", "Rating: ": "평점: ",
"About": "정보", "About": "정보",
"Top": "최고", "Top": "최고",
@ -469,5 +469,6 @@
"generic_button_cancel": "취소", "generic_button_cancel": "취소",
"generic_button_rss": "RSS", "generic_button_rss": "RSS",
"channel_tab_releases_label": "출시", "channel_tab_releases_label": "출시",
"generic_channels_count_0": "{{count}} 채널" "generic_channels_count_0": "{{count}} 채널",
"Import YouTube watch history (.json)": "유튜브 시청 기록 가져오기 (.json)"
} }

View File

@ -486,5 +486,6 @@
"generic_button_rss": "RSS", "generic_button_rss": "RSS",
"playlist_button_add_items": "Legg til videoer", "playlist_button_add_items": "Legg til videoer",
"generic_channels_count": "{{count}} kanal", "generic_channels_count": "{{count}} kanal",
"generic_channels_count_plural": "{{count}} kanaler" "generic_channels_count_plural": "{{count}} kanaler",
"Import YouTube watch history (.json)": "Importere YouTube visningshistorikk (.json)"
} }

View File

@ -107,10 +107,10 @@
"Report statistics: ": "Statistieken bijhouden? ", "Report statistics: ": "Statistieken bijhouden? ",
"Save preferences": "Instellingen opslaan", "Save preferences": "Instellingen opslaan",
"Subscription manager": "Abonnementen beheren", "Subscription manager": "Abonnementen beheren",
"Token manager": "Toegangssleutels beheren", "Token manager": "Toegangssleutelbeheerder",
"Token": "Toegangssleutel", "Token": "Toegangssleutel",
"Import/export": "Importeren/Exporteren", "Import/export": "Importeren/Exporteren",
"unsubscribe": "Deabonneren", "unsubscribe": "deabonneren",
"revoke": "Intrekken", "revoke": "Intrekken",
"Subscriptions": "Abonnementen", "Subscriptions": "Abonnementen",
"search": "zoeken", "search": "zoeken",
@ -357,7 +357,7 @@
"footer_original_source_code": "Originele bron-code", "footer_original_source_code": "Originele bron-code",
"footer_modfied_source_code": "Gewijzigde bron-code", "footer_modfied_source_code": "Gewijzigde bron-code",
"adminprefs_modified_source_code_url_label": "URL naar gewijzigde bron-code-opslagplaats", "adminprefs_modified_source_code_url_label": "URL naar gewijzigde bron-code-opslagplaats",
"next_steps_error_message": "Waarna u moet proberen om: ", "next_steps_error_message": "Daarna moet u proberen om: ",
"footer_source_code": "Bron-code", "footer_source_code": "Bron-code",
"search_filters_duration_option_long": "Lang (> 20 minuten)", "search_filters_duration_option_long": "Lang (> 20 minuten)",
"preferences_quality_option_dash": "DASH (adaptieve kwaliteit)", "preferences_quality_option_dash": "DASH (adaptieve kwaliteit)",
@ -462,5 +462,30 @@
"Spanish (auto-generated)": "Spaans (automatisch gegenereerd)", "Spanish (auto-generated)": "Spaans (automatisch gegenereerd)",
"crash_page_you_found_a_bug": "Je lijkt een bug in Invidious tegengekomen te zijn!", "crash_page_you_found_a_bug": "Je lijkt een bug in Invidious tegengekomen te zijn!",
"search_filters_duration_option_medium": "Gemiddeld (4 - 20 minuten)", "search_filters_duration_option_medium": "Gemiddeld (4 - 20 minuten)",
"crash_page_report_issue": "Indien het bovenstaande niet hielp, gelieve dan <a href=\"`x`\">een nieuw ticket op GitHub</a> te openen (liefst in het Engels) en neem de volgende tekst op in je bericht (gelieve deze NIET te vertalen):" "crash_page_report_issue": "Indien het bovenstaande niet hielp, gelieve dan <a href=\"`x`\">een nieuw ticket op GitHub</a> te openen (liefst in het Engels) en neem de volgende tekst op in je bericht (gelieve deze NIET te vertalen):",
"channel_tab_podcasts_label": "Podcasts",
"Download is disabled": "Downloaden is uitgeschakeld",
"Channel Sponsor": "Kanaalsponsor",
"channel_tab_streams_label": "Livestreams",
"playlist_button_add_items": "Video's toevoegen",
"Artist: ": "Artiest: ",
"generic_button_save": "Opslaan",
"generic_button_cancel": "Annuleren",
"Album: ": "Album: ",
"channel_tab_shorts_label": "Shorts",
"channel_tab_releases_label": "Uitgaves",
"Song: ": "Lied: ",
"generic_channels_count": "{{count}} kanaal",
"generic_channels_count_plural": "{{count}} kanalen",
"Popular enabled: ": "Populair geactiveerd: ",
"channel_tab_playlists_label": "Afspeellijsten",
"generic_button_edit": "Bewerken",
"Music in this video": "Muziek in deze video",
"generic_button_rss": "RSS",
"channel_tab_channels_label": "Kanalen",
"error_video_not_in_playlist": "De gevraagde video bestaat niet in deze afspeellijst. <a href=\"`x`\">Klik hier voor de startpagina van de afspeellijst.</a>",
"generic_button_delete": "Verwijderen",
"Import YouTube playlist (.csv)": "YouTube-afspeellijst importeren (.csv)",
"Standard YouTube license": "Standaard YouTube-licentie",
"Import YouTube watch history (.json)": "YouTube-kijkgeschiedenis importeren (.json)"
} }

View File

@ -492,7 +492,7 @@
"Song: ": "Piosenka: ", "Song: ": "Piosenka: ",
"Channel Sponsor": "Sponsor kanału", "Channel Sponsor": "Sponsor kanału",
"Standard YouTube license": "Standardowa licencja YouTube", "Standard YouTube license": "Standardowa licencja YouTube",
"Import YouTube playlist (.csv)": "Importuj playlistę YouTube (.csv)", "Import YouTube playlist (.csv)": "Importuj playlistę z YouTube (.csv)",
"generic_button_edit": "Edytuj", "generic_button_edit": "Edytuj",
"generic_button_cancel": "Anuluj", "generic_button_cancel": "Anuluj",
"generic_button_rss": "RSS", "generic_button_rss": "RSS",
@ -503,5 +503,7 @@
"playlist_button_add_items": "Dodaj filmy", "playlist_button_add_items": "Dodaj filmy",
"generic_channels_count_0": "{{count}} kanał", "generic_channels_count_0": "{{count}} kanał",
"generic_channels_count_1": "{{count}} kanały", "generic_channels_count_1": "{{count}} kanały",
"generic_channels_count_2": "{{count}} kanałów" "generic_channels_count_2": "{{count}} kanałów",
"Import YouTube watch history (.json)": "Importuj historię oglądania z YouTube (.json)",
"toggle_theme": "Przełącz motyw"
} }

View File

@ -503,5 +503,7 @@
"generic_button_rss": "RSS", "generic_button_rss": "RSS",
"generic_channels_count_0": "{{count}} canal", "generic_channels_count_0": "{{count}} canal",
"generic_channels_count_1": "{{count}} canais", "generic_channels_count_1": "{{count}} canais",
"generic_channels_count_2": "{{count}} canais" "generic_channels_count_2": "{{count}} canais",
"Import YouTube watch history (.json)": "Importar histórico de reprodução do YouTube (.json)",
"toggle_theme": "Alternar Tema"
} }

View File

@ -1,7 +1,7 @@
{ {
"search_filters_type_option_show": "Espetáculo", "search_filters_type_option_show": "Série",
"search_filters_sort_option_views": "Visualizações", "search_filters_sort_option_views": "Visualizações",
"search_filters_sort_option_date": "Data de envio", "search_filters_sort_option_date": "Data de carregamento",
"search_filters_sort_option_rating": "Avaliação", "search_filters_sort_option_rating": "Avaliação",
"search_filters_sort_option_relevance": "Relevância", "search_filters_sort_option_relevance": "Relevância",
"Switch Invidious Instance": "Mudar a instância do Invidious", "Switch Invidious Instance": "Mudar a instância do Invidious",
@ -13,7 +13,7 @@
"preferences_category_misc": "Preferências diversas", "preferences_category_misc": "Preferências diversas",
"preferences_vr_mode_label": "Vídeos interativos de 360 graus (necessita de WebGL): ", "preferences_vr_mode_label": "Vídeos interativos de 360 graus (necessita de WebGL): ",
"preferences_extend_desc_label": "Estender automaticamente a descrição do vídeo: ", "preferences_extend_desc_label": "Estender automaticamente a descrição do vídeo: ",
"next_steps_error_message_go_to_youtube": "Ir ao YouTube", "next_steps_error_message_go_to_youtube": "Ir para o YouTube",
"next_steps_error_message": "Pode tentar as seguintes opções: ", "next_steps_error_message": "Pode tentar as seguintes opções: ",
"next_steps_error_message_refresh": "Atualizar", "next_steps_error_message_refresh": "Atualizar",
"search_filters_features_option_hdr": "HDR", "search_filters_features_option_hdr": "HDR",
@ -44,20 +44,27 @@
"Default": "Predefinido", "Default": "Predefinido",
"Top": "Destaques", "Top": "Destaques",
"Search": "Pesquisar", "Search": "Pesquisar",
"generic_count_years": "{{count}} segundo", "generic_count_years_0": "{{count}} ano",
"generic_count_years_plural": "{{count}} segundos", "generic_count_years_1": "{{count}} anos",
"generic_count_months": "{{count}} minuto", "generic_count_years_2": "{{count}} anos",
"generic_count_months_plural": "{{count}} minutos", "generic_count_months_0": "{{count}} mês",
"generic_count_weeks": "{{count}} hora", "generic_count_months_1": "{{count}} meses",
"generic_count_weeks_plural": "{{count}} horas", "generic_count_months_2": "{{count}} meses",
"generic_count_days": "{{count}} dia", "generic_count_weeks_0": "{{count}} semana",
"generic_count_days_plural": "{{count}} dias", "generic_count_weeks_1": "{{count}} semanas",
"generic_count_hours": "{{count}} seman", "generic_count_weeks_2": "{{count}} semanas",
"generic_count_hours_plural": "{{count}} semanas", "generic_count_days_0": "{{count}} dia",
"generic_count_minutes": "{{count}} mês", "generic_count_days_1": "{{count}} dias",
"generic_count_minutes_plural": "{{count}} meses", "generic_count_days_2": "{{count}} dias",
"generic_count_seconds": "{{count}} ano", "generic_count_hours_0": "{{count}} hora",
"generic_count_seconds_plural": "{{count}} anos", "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",
"Chinese (Traditional)": "Chinês (tradicional)", "Chinese (Traditional)": "Chinês (tradicional)",
"Chinese (Simplified)": "Chinês (simplificado)", "Chinese (Simplified)": "Chinês (simplificado)",
"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 as páginas de tendências.",
@ -75,7 +82,7 @@
"Import/export data": "Importar / exportar dados", "Import/export data": "Importar / exportar dados",
"preferences_annotations_label": "Mostrar anotações sempre: ", "preferences_annotations_label": "Mostrar anotações sempre: ",
"preferences_continue_label": "Reproduzir sempre o próximo: ", "preferences_continue_label": "Reproduzir sempre o próximo: ",
"Sign In": "Iniciar sessão", "Sign In": "Entrar",
"Log in/register": "Iniciar sessão/registar", "Log in/register": "Iniciar sessão/registar",
"Delete account?": "Eliminar conta?", "Delete account?": "Eliminar conta?",
"Import and Export Data": "Importar e exportar dados", "Import and Export Data": "Importar e exportar dados",
@ -167,8 +174,9 @@
"Log out": "Terminar sessão", "Log out": "Terminar sessão",
"Subscriptions": "Subscrições", "Subscriptions": "Subscrições",
"revoke": "revogar", "revoke": "revogar",
"tokens_count": "{{count}} token", "tokens_count_0": "{{count}} Token",
"tokens_count_plural": "{{count}} tokens", "tokens_count_1": "{{count}} Tokens",
"tokens_count_2": "{{count}} Tokens",
"Token": "Token", "Token": "Token",
"Token manager": "Gerir tokens", "Token manager": "Gerir tokens",
"Subscription manager": "Gerir subscrições", "Subscription manager": "Gerir subscrições",
@ -402,31 +410,39 @@
"videoinfo_youTube_embed_link": "Incorporar", "videoinfo_youTube_embed_link": "Incorporar",
"preferences_save_player_pos_label": "Guardar a posição de reprodução atual do vídeo: ", "preferences_save_player_pos_label": "Guardar a posição de reprodução atual do vídeo: ",
"download_subtitles": "Legendas - `x` (.vtt)", "download_subtitles": "Legendas - `x` (.vtt)",
"generic_views_count": "{{count}} visualização", "generic_views_count_0": "{{count}} visualização",
"generic_views_count_plural": "{{count}} visualizações", "generic_views_count_1": "{{count}} visualizações",
"generic_views_count_2": "{{count}} visualizações",
"videoinfo_started_streaming_x_ago": "Iniciou a transmissão há `x`", "videoinfo_started_streaming_x_ago": "Iniciou a transmissão há `x`",
"user_saved_playlists": "`x` listas de reprodução guardadas", "user_saved_playlists": "`x` listas de reprodução guardadas",
"generic_videos_count": "{{count}} vídeo", "generic_videos_count_0": "{{count}} vídeo",
"generic_videos_count_plural": "{{count}} vídeos", "generic_videos_count_1": "{{count}} vídeos",
"generic_playlists_count": "{{count}} lista de reprodução", "generic_videos_count_2": "{{count}} vídeos",
"generic_playlists_count_plural": "{{count}} listas de reprodução", "generic_playlists_count_0": "{{count}} lista de reprodução",
"subscriptions_unseen_notifs_count": "{{count}} notificação não vista", "generic_playlists_count_1": "{{count}} listas de reprodução",
"subscriptions_unseen_notifs_count_plural": "{{count}} notificações não vistas", "generic_playlists_count_2": "{{count}} listas de reprodução",
"comments_view_x_replies": "Ver {{count}} resposta", "subscriptions_unseen_notifs_count_0": "{{count}} notificação não vista",
"comments_view_x_replies_plural": "Ver {{count}} respostas", "subscriptions_unseen_notifs_count_1": "{{count}} notificações não vistas",
"generic_subscribers_count": "{{count}} inscrito", "subscriptions_unseen_notifs_count_2": "{{count}} notificações não vistas",
"generic_subscribers_count_plural": "{{count}} inscritos", "comments_view_x_replies_0": "Ver {{count}} resposta",
"generic_subscriptions_count": "{{count}} inscrição", "comments_view_x_replies_1": "Ver {{count}} respostas",
"generic_subscriptions_count_plural": "{{count}} inscrições", "comments_view_x_replies_2": "Ver {{count}} respostas",
"comments_points_count": "{{count}} ponto", "generic_subscribers_count_0": "{{count}} inscrito",
"comments_points_count_plural": "{{count}} pontos", "generic_subscribers_count_1": "{{count}} inscritos",
"generic_subscribers_count_2": "{{count}} inscritos",
"generic_subscriptions_count_0": "{{count}} inscrição",
"generic_subscriptions_count_1": "{{count}} inscrições",
"generic_subscriptions_count_2": "{{count}} inscrições",
"comments_points_count_0": "{{count}} ponto",
"comments_points_count_1": "{{count}} pontos",
"comments_points_count_2": "{{count}} pontos",
"crash_page_you_found_a_bug": "Parece que encontrou um erro no Invidious!", "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_before_reporting": "Antes de reportar um erro, verifique se:",
"crash_page_refresh": "tentou <a href=\"`x`\">recarregar a página</a>", "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>", "crash_page_switch_instance": "tentou <a href=\"`x`\">usar outra instância</a>",
"crash_page_read_the_faq": "leia as <a href=\"`x`\">Perguntas frequentes (FAQ)</a>", "crash_page_read_the_faq": "leia 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_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):",
"user_created_playlists": "`x` listas de reprodução criadas", "user_created_playlists": "`x` listas de reprodução criadas",
"search_filters_title": "Filtro", "search_filters_title": "Filtro",
"Chinese (Taiwan)": "Chinês (Taiwan)", "Chinese (Taiwan)": "Chinês (Taiwan)",
@ -464,7 +480,7 @@
"search_filters_type_option_all": "Qualquer tipo", "search_filters_type_option_all": "Qualquer tipo",
"search_filters_duration_option_none": "Qualquer duração", "search_filters_duration_option_none": "Qualquer duração",
"Popular enabled: ": "Página \"popular\" ativada: ", "Popular enabled: ": "Página \"popular\" ativada: ",
"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>",
"channel_tab_playlists_label": "Listas de reprodução", "channel_tab_playlists_label": "Listas de reprodução",
"channel_tab_channels_label": "Canais", "channel_tab_channels_label": "Canais",
"channel_tab_shorts_label": "Curtos", "channel_tab_shorts_label": "Curtos",
@ -484,5 +500,10 @@
"channel_tab_releases_label": "Lançamentos", "channel_tab_releases_label": "Lançamentos",
"generic_button_save": "Salvar", "generic_button_save": "Salvar",
"generic_button_cancel": "Cancelar", "generic_button_cancel": "Cancelar",
"playlist_button_add_items": "Adicionar vídeos" "playlist_button_add_items": "Adicionar vídeos",
"generic_channels_count_0": "{{count}} canal",
"generic_channels_count_1": "{{count}} canais",
"generic_channels_count_2": "{{count}} canais",
"Import YouTube watch history (.json)": "Importar histórico de reprodução do YouTube (.json)",
"toggle_theme": "Trocar tema"
} }

View File

@ -8,14 +8,14 @@
"newest": "сначала новые", "newest": "сначала новые",
"oldest": "сначала старые", "oldest": "сначала старые",
"popular": "популярные", "popular": "популярные",
"last": "недавние", "last": "последние",
"Next page": "Следующая страница", "Next page": "Следующая страница",
"Previous page": "Предыдущая страница", "Previous page": "Предыдущая страница",
"Clear watch history?": "Очистить историю просмотров?", "Clear watch history?": "Очистить историю просмотров?",
"New password": "Новый пароль", "New password": "Новый пароль",
"New passwords must match": "Новые пароли не совпадают", "New passwords must match": "Новые пароли не совпадают",
"Authorize token?": "Авторизовать токен?", "Authorize token?": "Авторизовать токен?",
"Authorize token for `x`?": "Авторизовать токен для `x`?", "Authorize token for `x`?": "Токен авторизации для `x`?",
"Yes": "Да", "Yes": "Да",
"No": "Нет", "No": "Нет",
"Import and Export Data": "Импорт и экспорт данных", "Import and Export Data": "Импорт и экспорт данных",
@ -29,7 +29,7 @@
"Export subscriptions as OPML": "Экспортировать подписки в формате OPML", "Export subscriptions as OPML": "Экспортировать подписки в формате OPML",
"Export subscriptions as OPML (for NewPipe & FreeTube)": "Экспортировать подписки в формате OPML (для NewPipe и FreeTube)", "Export subscriptions as OPML (for NewPipe & FreeTube)": "Экспортировать подписки в формате OPML (для NewPipe и FreeTube)",
"Export data as JSON": "Экспортировать данные Invidious в формате JSON", "Export data as JSON": "Экспортировать данные Invidious в формате JSON",
"Delete account?": "Удалить учётку?", "Delete account?": "Удалить учётную запись?",
"History": "История", "History": "История",
"An alternative front-end to YouTube": "Альтернативный фронтенд для YouTube", "An alternative front-end to YouTube": "Альтернативный фронтенд для YouTube",
"JavaScript license information": "Информация о лицензиях JavaScript", "JavaScript license information": "Информация о лицензиях JavaScript",
@ -42,7 +42,7 @@
"Text CAPTCHA": "Текстовая капча (англ.)", "Text CAPTCHA": "Текстовая капча (англ.)",
"Image CAPTCHA": "Капча-картинка", "Image CAPTCHA": "Капча-картинка",
"Sign In": "Войти", "Sign In": "Войти",
"Register": "Зарегистрироваться", "Register": "Регистрация",
"E-mail": "Эл. почта", "E-mail": "Эл. почта",
"Preferences": "Настройки", "Preferences": "Настройки",
"preferences_category_player": "Настройки проигрывателя", "preferences_category_player": "Настройки проигрывателя",
@ -61,7 +61,7 @@
"preferences_captions_label": "Основной язык субтитров: ", "preferences_captions_label": "Основной язык субтитров: ",
"Fallback captions: ": "Дополнительный язык субтитров: ", "Fallback captions: ": "Дополнительный язык субтитров: ",
"preferences_related_videos_label": "Показывать похожие видео? ", "preferences_related_videos_label": "Показывать похожие видео? ",
"preferences_annotations_label": "Всегда показывать аннотации? ", "preferences_annotations_label": "Показывать аннотации по умолчанию: ",
"preferences_extend_desc_label": "Автоматически раскрывать описание видео: ", "preferences_extend_desc_label": "Автоматически раскрывать описание видео: ",
"preferences_vr_mode_label": "Интерактивные 360-градусные видео (необходим WebGL): ", "preferences_vr_mode_label": "Интерактивные 360-градусные видео (необходим WebGL): ",
"preferences_category_visual": "Настройки сайта", "preferences_category_visual": "Настройки сайта",
@ -77,13 +77,13 @@
"preferences_annotations_subscribed_label": "Всегда показывать аннотации на каналах из ваших подписок? ", "preferences_annotations_subscribed_label": "Всегда показывать аннотации на каналах из ваших подписок? ",
"Redirect homepage to feed: ": "Показывать подписки на главной странице: ", "Redirect homepage to feed: ": "Показывать подписки на главной странице: ",
"preferences_max_results_label": "Число видео в ленте: ", "preferences_max_results_label": "Число видео в ленте: ",
"preferences_sort_label": "Сортировать видео: ", "preferences_sort_label": "Сортировать видео по: ",
"published": "по дате публикации", "published": "дате публикации",
"published - reverse": "по дате публикации в обратном порядке", "published - reverse": "дате публикации в обратном порядке",
"alphabetically": "по алфавиту", "alphabetically": "алфавиту",
"alphabetically - reverse": "по алфавиту в обратном порядке", "alphabetically - reverse": "алфавиту в обратном порядке",
"channel name": "по названию канала", "channel name": "названию канала",
"channel name - reverse": "по названию канала в обратном порядке", "channel name - reverse": "названию канала в обратном порядке",
"Only show latest video from channel: ": "Показывать только последние видео с каналов: ", "Only show latest video from channel: ": "Показывать только последние видео с каналов: ",
"Only show latest unwatched video from channel: ": "Показывать только последние непросмотренные видео с канала: ", "Only show latest unwatched video from channel: ": "Показывать только последние непросмотренные видео с канала: ",
"preferences_unseen_only_label": "Показывать только непросмотренные видео: ", "preferences_unseen_only_label": "Показывать только непросмотренные видео: ",
@ -134,8 +134,8 @@
"Title": "Заголовок", "Title": "Заголовок",
"Playlist privacy": "Видимость плейлиста", "Playlist privacy": "Видимость плейлиста",
"Editing playlist `x`": "Редактирование плейлиста `x`", "Editing playlist `x`": "Редактирование плейлиста `x`",
"Show more": "Развернуть", "Show more": "Показать больше",
"Show less": "Свернуть", "Show less": "Показать меньше",
"Watch on YouTube": "Смотреть на YouTube", "Watch on YouTube": "Смотреть на YouTube",
"Switch Invidious Instance": "Сменить зеркало Invidious", "Switch Invidious Instance": "Сменить зеркало Invidious",
"Hide annotations": "Скрыть аннотации", "Hide annotations": "Скрыть аннотации",
@ -414,7 +414,7 @@
"generic_count_days_0": "{{count}} день", "generic_count_days_0": "{{count}} день",
"generic_count_days_1": "{{count}} дня", "generic_count_days_1": "{{count}} дня",
"generic_count_days_2": "{{count}} дней", "generic_count_days_2": "{{count}} дней",
"preferences_quality_dash_option_auto": "Автоматическое", "preferences_quality_dash_option_auto": "Авто",
"preferences_quality_dash_option_1080p": "1080p", "preferences_quality_dash_option_1080p": "1080p",
"preferences_quality_dash_option_720p": "720p", "preferences_quality_dash_option_720p": "720p",
"generic_subscriptions_count_0": "{{count}} подписка", "generic_subscriptions_count_0": "{{count}} подписка",
@ -466,7 +466,7 @@
"search_filters_features_option_three_sixty": "360°", "search_filters_features_option_three_sixty": "360°",
"Video unavailable": "Видео недоступно", "Video unavailable": "Видео недоступно",
"preferences_save_player_pos_label": "Запоминать позицию: ", "preferences_save_player_pos_label": "Запоминать позицию: ",
"preferences_region_label": "Страна: ", "preferences_region_label": "Страна источник ",
"preferences_watch_history_label": "Включить историю просмотров: ", "preferences_watch_history_label": "Включить историю просмотров: ",
"search_filters_title": "Фильтр", "search_filters_title": "Фильтр",
"search_filters_duration_option_none": "Любой длины", "search_filters_duration_option_none": "Любой длины",
@ -476,7 +476,7 @@
"search_message_no_results": "Ничего не найдено.", "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_filters_features_option_vr180": "VR180",
"search_message_change_filters_or_query": "Попробуйте расширить поисковый запрос или изменить фильтры.", "search_message_change_filters_or_query": "Попробуйте расширить поисковый запрос или изменить фильтры.",
"search_filters_duration_option_medium": "Средние (4 - 20 минут)", "search_filters_duration_option_medium": "Средние (4 - 20 минут)",
"search_filters_apply_button": "Применить фильтры", "search_filters_apply_button": "Применить фильтры",
"Popular enabled: ": "Популярное включено: ", "Popular enabled: ": "Популярное включено: ",
@ -503,5 +503,6 @@
"channel_tab_podcasts_label": "Подкасты", "channel_tab_podcasts_label": "Подкасты",
"generic_channels_count_0": "{{count}} канал", "generic_channels_count_0": "{{count}} канал",
"generic_channels_count_1": "{{count}} канала", "generic_channels_count_1": "{{count}} канала",
"generic_channels_count_2": "{{count}} каналов" "generic_channels_count_2": "{{count}} каналов",
"Import YouTube watch history (.json)": "Импортировать историю просмотра из YouTube (.json)"
} }

View File

@ -520,5 +520,6 @@
"generic_channels_count_0": "{{count}} kanal", "generic_channels_count_0": "{{count}} kanal",
"generic_channels_count_1": "{{count}} kanala", "generic_channels_count_1": "{{count}} kanala",
"generic_channels_count_2": "{{count}} kanali", "generic_channels_count_2": "{{count}} kanali",
"generic_channels_count_3": "{{count}} kanalov" "generic_channels_count_3": "{{count}} kanalov",
"Import YouTube watch history (.json)": "Uvozi zgodovino gledanja YouTube (.json)"
} }

View File

@ -79,7 +79,7 @@
"invidious": "Invidious", "invidious": "Invidious",
"preferences_captions_label": "Titra parazgjedhje: ", "preferences_captions_label": "Titra parazgjedhje: ",
"preferences_extend_desc_label": "Zgjero automatikisht përshkrimin e videos: ", "preferences_extend_desc_label": "Zgjero automatikisht përshkrimin e videos: ",
"preferences_player_style_label": "Silt lojtësi: ", "preferences_player_style_label": "Stil lojtësi: ",
"Dark mode: ": "Mënyra e errët: ", "Dark mode: ": "Mënyra e errët: ",
"preferences_dark_mode_label": "Temë: ", "preferences_dark_mode_label": "Temë: ",
"dark": "e errët", "dark": "e errët",
@ -477,5 +477,12 @@
"channel_tab_releases_label": "Hedhje në qarkullim", "channel_tab_releases_label": "Hedhje në qarkullim",
"Song: ": "Pjesë: ", "Song: ": "Pjesë: ",
"Import YouTube playlist (.csv)": "Importoni luajlistë YouTube (.csv)", "Import YouTube playlist (.csv)": "Importoni luajlistë YouTube (.csv)",
"Standard YouTube license": "Licencë YouTube standarde" "Standard YouTube license": "Licencë YouTube standarde",
"published - reverse": "publikuar më - së prapthi",
"channel_tab_podcasts_label": "Podcast-e",
"channel name - reverse": "emër kanali - së prapthi",
"Import YouTube watch history (.json)": "Importo historik parjesh YouTube (.json)",
"preferences_local_label": "Video përmes ndërmjetësi: ",
"Fallback captions: ": "Titra nga halli: ",
"Erroneous challenge": "Zgjidhje e gabuar"
} }

View File

@ -503,5 +503,6 @@
"crash_page_you_found_a_bug": "Izgleda da ste pronašli grešku u Invidious-u!", "crash_page_you_found_a_bug": "Izgleda da ste pronašli grešku u Invidious-u!",
"generic_views_count_0": "{{count}} pregled", "generic_views_count_0": "{{count}} pregled",
"generic_views_count_1": "{{count}} pregleda", "generic_views_count_1": "{{count}} pregleda",
"generic_views_count_2": "{{count}} pregleda" "generic_views_count_2": "{{count}} pregleda",
"Import YouTube watch history (.json)": "Uvezi YouTube istoriju gledanja (.json)"
} }

View File

@ -503,5 +503,7 @@
"crash_page_you_found_a_bug": "Изгледа да сте пронашли грешку у Invidious-у!", "crash_page_you_found_a_bug": "Изгледа да сте пронашли грешку у Invidious-у!",
"generic_views_count_0": "{{count}} преглед", "generic_views_count_0": "{{count}} преглед",
"generic_views_count_1": "{{count}} прегледа", "generic_views_count_1": "{{count}} прегледа",
"generic_views_count_2": "{{count}} прегледа" "generic_views_count_2": "{{count}} прегледа",
"Import YouTube watch history (.json)": "Увези YouTube историју гледањa (.json)",
"toggle_theme": "Укључи тему"
} }

View File

@ -20,15 +20,15 @@
"No": "Nej", "No": "Nej",
"Import and Export Data": "Importera och exportera data", "Import and Export Data": "Importera och exportera data",
"Import": "Importera", "Import": "Importera",
"Import Invidious data": "Importera Invidious-data", "Import Invidious data": "Importera Invidious JSON data",
"Import YouTube subscriptions": "Importera YouTube-prenumerationer", "Import YouTube subscriptions": "Importera YouTube/OPML prenumerationer",
"Import FreeTube subscriptions (.db)": "Importera FreeTube-prenumerationer (.db)", "Import FreeTube subscriptions (.db)": "Importera FreeTube-prenumerationer (.db)",
"Import NewPipe subscriptions (.json)": "Importera NewPipe-prenumerationer (.json)", "Import NewPipe subscriptions (.json)": "Importera NewPipe-prenumerationer (.json)",
"Import NewPipe data (.zip)": "Importera NewPipe-data (.zip)", "Import NewPipe data (.zip)": "Importera NewPipe-data (.zip)",
"Export": "Exportera", "Export": "Exportera",
"Export subscriptions as OPML": "Exportera prenumerationer som OPML", "Export subscriptions as OPML": "Exportera prenumerationer som OPML",
"Export subscriptions as OPML (for NewPipe & FreeTube)": "Exportera prenumerationer som OPML (för NewPipe och FreeTube)", "Export subscriptions as OPML (for NewPipe & FreeTube)": "Exportera prenumerationer som OPML (för NewPipe och FreeTube)",
"Export data as JSON": "Exportera data som JSON", "Export data as JSON": "Exportera Invidious data som JSON",
"Delete account?": "Radera konto?", "Delete account?": "Radera konto?",
"History": "Historik", "History": "Historik",
"An alternative front-end to YouTube": "Ett alternativt gränssnitt till YouTube", "An alternative front-end to YouTube": "Ett alternativt gränssnitt till YouTube",
@ -63,7 +63,7 @@
"preferences_related_videos_label": "Visa relaterade videor? ", "preferences_related_videos_label": "Visa relaterade videor? ",
"preferences_annotations_label": "Visa länkar-i-videon som förval? ", "preferences_annotations_label": "Visa länkar-i-videon som förval? ",
"preferences_extend_desc_label": "Förläng videobeskrivning automatiskt: ", "preferences_extend_desc_label": "Förläng videobeskrivning automatiskt: ",
"preferences_vr_mode_label": "Interaktiva 360-gradervideos: ", "preferences_vr_mode_label": "Interaktiva 360-gradervideos (kräver WebGL): ",
"preferences_category_visual": "Visuella inställningar", "preferences_category_visual": "Visuella inställningar",
"preferences_player_style_label": "Spelarstil: ", "preferences_player_style_label": "Spelarstil: ",
"Dark mode: ": "Mörkt läge: ", "Dark mode: ": "Mörkt läge: ",
@ -152,7 +152,7 @@
"View YouTube comments": "Visa YouTube-kommentarer", "View YouTube comments": "Visa YouTube-kommentarer",
"View more comments on Reddit": "Visa flera kommentarer på Reddit", "View more comments on Reddit": "Visa flera kommentarer på Reddit",
"View `x` comments": { "View `x` comments": {
"([^.,0-9]|^)1([^.,0-9]|$)": "Visa `x` kommentarer", "([^.,0-9]|^)1([^.,0-9]|$)": "Visa `x` kommentar",
"": "Visa `x` kommentarer" "": "Visa `x` kommentarer"
}, },
"View Reddit comments": "Visa Reddit-kommentarer", "View Reddit comments": "Visa Reddit-kommentarer",
@ -167,7 +167,7 @@
"Wrong username or password": "Ogiltigt användarnamn eller lösenord", "Wrong username or password": "Ogiltigt användarnamn eller lösenord",
"Password cannot be empty": "Lösenordet kan inte vara tomt", "Password cannot be empty": "Lösenordet kan inte vara tomt",
"Password cannot be longer than 55 characters": "Lösenordet kan inte vara längre än 55 tecken", "Password cannot be longer than 55 characters": "Lösenordet kan inte vara längre än 55 tecken",
"Please log in": "Logga in", "Please log in": "Snälla logga in",
"Invidious Private Feed for `x`": "Ogiltig privat flöde för `x`", "Invidious Private Feed for `x`": "Ogiltig privat flöde för `x`",
"channel:`x`": "kanal `x`", "channel:`x`": "kanal `x`",
"Deleted or invalid channel": "Raderad eller ogiltig kanal", "Deleted or invalid channel": "Raderad eller ogiltig kanal",
@ -311,8 +311,8 @@
"%A %B %-d, %Y": "%A %B %-d, %Y", "%A %B %-d, %Y": "%A %B %-d, %Y",
"(edited)": "(redigerad)", "(edited)": "(redigerad)",
"YouTube comment permalink": "Permanent YouTube-länk till innehållet", "YouTube comment permalink": "Permanent YouTube-länk till innehållet",
"permalink": "permalänk", "permalink": "permanent länk",
"`x` marked it with a ❤": "`x` lämnade ett ❤", "`x` marked it with a ❤": "`x` markerade det med ett ❤",
"Audio mode": "Ljudläge", "Audio mode": "Ljudläge",
"Video mode": "Videoläge", "Video mode": "Videoläge",
"channel_tab_videos_label": "Videor", "channel_tab_videos_label": "Videor",
@ -320,30 +320,30 @@
"channel_tab_community_label": "Gemenskap", "channel_tab_community_label": "Gemenskap",
"search_filters_sort_option_relevance": "Relevans", "search_filters_sort_option_relevance": "Relevans",
"search_filters_sort_option_rating": "Rankning", "search_filters_sort_option_rating": "Rankning",
"search_filters_sort_option_date": "Datum", "search_filters_sort_option_date": "Uppladdnings Datum",
"search_filters_sort_option_views": "Visningar", "search_filters_sort_option_views": "Visningar",
"search_filters_type_label": "Typ", "search_filters_type_label": "Typ",
"search_filters_duration_label": "Varaktighet", "search_filters_duration_label": "Varaktighet",
"search_filters_features_label": "Funktioner", "search_filters_features_label": "Funktioner",
"search_filters_sort_label": "Sortera efter", "search_filters_sort_label": "Sortera efter",
"search_filters_date_option_hour": "timme", "search_filters_date_option_hour": "Senaste Timmen",
"search_filters_date_option_today": "idag", "search_filters_date_option_today": "Idag",
"search_filters_date_option_week": "vecka", "search_filters_date_option_week": "Denna vecka",
"search_filters_date_option_month": "månad", "search_filters_date_option_month": "Denna månad",
"search_filters_date_option_year": "år", "search_filters_date_option_year": "Detta år",
"search_filters_type_option_video": "video", "search_filters_type_option_video": "Video",
"search_filters_type_option_channel": "kanal", "search_filters_type_option_channel": "Kanal",
"search_filters_type_option_playlist": "spellista", "search_filters_type_option_playlist": "Spellista",
"search_filters_type_option_movie": "film", "search_filters_type_option_movie": "Film",
"search_filters_type_option_show": "tv-serie", "search_filters_type_option_show": "Serie",
"search_filters_features_option_hd": "hd", "search_filters_features_option_hd": "HD",
"search_filters_features_option_subtitles": "undertexter", "search_filters_features_option_subtitles": "Undertexter/CC",
"search_filters_features_option_c_commons": "creative_commons", "search_filters_features_option_c_commons": "Creative Commons",
"search_filters_features_option_three_d": "3d", "search_filters_features_option_three_d": "3D",
"search_filters_features_option_live": "live", "search_filters_features_option_live": "Live",
"search_filters_features_option_four_k": "4k", "search_filters_features_option_four_k": "4K",
"search_filters_features_option_location": "plats", "search_filters_features_option_location": "Plats",
"search_filters_features_option_hdr": "hdr", "search_filters_features_option_hdr": "HDR",
"Current version: ": "Nuvarande version: ", "Current version: ": "Nuvarande version: ",
"next_steps_error_message_refresh": "Uppdatera", "next_steps_error_message_refresh": "Uppdatera",
"next_steps_error_message_go_to_youtube": "Gå till Youtube", "next_steps_error_message_go_to_youtube": "Gå till Youtube",
@ -352,5 +352,141 @@
"search_filters_duration_option_long": "Lång (> 20 minuter)", "search_filters_duration_option_long": "Lång (> 20 minuter)",
"footer_documentation": "Dokumentation", "footer_documentation": "Dokumentation",
"search_filters_duration_option_short": "Kort (< 4 minuter)", "search_filters_duration_option_short": "Kort (< 4 minuter)",
"search_filters_title": "Filter" "search_filters_title": "Filter",
"Korean (auto-generated)": "Koreanska (auto-genererad)",
"search_filters_features_option_three_sixty": "360°",
"preferences_quality_dash_option_worst": "Sämst",
"channel_tab_podcasts_label": "Podcaster",
"preferences_save_player_pos_label": "Spara uppspelningsposition: ",
"Spanish (Mexico)": "Spanska (Mexiko)",
"preferences_region_label": "Innehållsland: ",
"generic_subscriptions_count": "{{count}} prenumeration",
"generic_subscriptions_count_plural": "{{count}} prenumerationer",
"search_filters_apply_button": "Använd valda filter",
"Download is disabled": "Nedladdning är inaktiverad",
"comments_points_count": "{{count}} poäng",
"comments_points_count_plural": "{{count}} poäng",
"preferences_quality_dash_option_2160p": "2160p",
"German (auto-generated)": "Tyska (auto-genererad)",
"Japanese (auto-generated)": "Japanska (auto-genererad)",
"preferences_quality_option_medium": "Medium",
"footer_donate_page": "Donera",
"search_message_change_filters_or_query": "Prova att bredda din sökfråga och/eller ändra filtren.",
"crash_page_before_reporting": "Innan du rapporterar en bugg, se till att du har:",
"preferences_quality_dash_option_best": "Bäst",
"Channel Sponsor": "Kanal Sponsor",
"generic_videos_count": "{{count}} video",
"generic_videos_count_plural": "{{count}} videor",
"videoinfo_started_streaming_x_ago": "Började sända `x` sedan",
"videoinfo_youTube_embed_link": "Bädda in",
"channel_tab_streams_label": "Livesändningar",
"playlist_button_add_items": "Lägg till videor",
"generic_count_minutes": "{{count}}minut",
"generic_count_minutes_plural": "{{count}}minuter",
"preferences_quality_dash_option_720p": "720p",
"preferences_watch_history_label": "Aktivera visningshistorik: ",
"user_saved_playlists": "`x` sparade spellistor",
"Spanish (Spain)": "Spanska (Spanien)",
"invidious": "Invidious",
"crash_page_refresh": "försökte <a href=\"`x`\">uppdatera sidan</a>",
"Chinese (Hong Kong)": "Kinesiska (Hong Kong)",
"Artist: ": "Artist: ",
"generic_count_months": "{{count}}månad",
"generic_count_months_plural": "{{count}}månader",
"search_message_use_another_instance": " Du kan också <a href=\"`x`\">söka på en annan instans</a>.",
"generic_subscribers_count": "{{count}} prenumerant",
"generic_subscribers_count_plural": "{{count}} prenumeranter",
"download_subtitles": "Undertexter - `x` (.vtt)",
"generic_button_save": "Spara",
"crash_page_search_issue": "sökte efter <a href=\"`x`\">befintliga problem på GitHub</a>",
"generic_button_cancel": "Avbryt",
"none": "ingen",
"English (United States)": "English (Förenta staterna)",
"subscriptions_unseen_notifs_count": "{{count}}osedd notifikation",
"subscriptions_unseen_notifs_count_plural": "{{count}}osedda notifikationer",
"Album: ": "Album: ",
"preferences_quality_option_dash": "DASH (adaptiv kvalitet)",
"preferences_quality_dash_option_1080p": "1080p",
"Video unavailable": "Video inte tillgänglig",
"tokens_count": "{{count}}nyckel",
"tokens_count_plural": "{{count}}nycklar",
"Chinese (China)": "Kinesiska (Kina)",
"Italian (auto-generated)": "Italienska (auto-genererad)",
"channel_tab_shorts_label": "Shorts",
"preferences_quality_dash_option_1440p": "1440p",
"preferences_quality_dash_option_360p": "360p",
"search_message_no_results": "Inga resultat hittades.",
"channel_tab_releases_label": "Releaser",
"preferences_quality_dash_option_144p": "144p",
"Interlingue": "Interlingue (auto-genererad)",
"Song: ": "Låt: ",
"generic_channels_count": "{{count}} kanal",
"generic_channels_count_plural": "{{count}} kanaler",
"Chinese (Taiwan)": "Kinesiska (Taiwan)",
"preferences_quality_dash_label": "Önskad DASH-videokvalitet: ",
"adminprefs_modified_source_code_url_label": "URL till modifierad källkodslager",
"Turkish (auto-generated)": "Turkiska (auto-genererad)",
"Indonesian (auto-generated)": "Indonesiska (auto-genererad)",
"Portuguese (auto-generated)": "Portugisiska (auto-genererad)",
"generic_count_years": "{{count}}år",
"generic_count_years_plural": "{{count}}år",
"videoinfo_invidious_embed_link": "Bädda in länk",
"Popular enabled: ": "Populär aktiverad: ",
"Spanish (auto-generated)": "Spanska (auto-genererad)",
"preferences_quality_option_small": "Liten",
"English (United Kingdom)": "Engelska (Storbritannien)",
"channel_tab_playlists_label": "Spellistor",
"generic_button_edit": "Redigera",
"generic_playlists_count": "{{count}} spellista",
"generic_playlists_count_plural": "{{count}} spellistor",
"preferences_quality_option_hd720": "HD720p",
"search_filters_features_option_purchased": "Köpt",
"search_filters_date_option_none": "Vilket datum som helst",
"preferences_quality_dash_option_auto": "Auto",
"Cantonese (Hong Kong)": "Katonesiska (Hong Kong)",
"crash_page_report_issue": "Om inget av ovanstående hjälpte, vänligen <a href=\"`x`\">öppna ett nytt nummer på GitHub</a> (helst på engelska) och inkludera följande text i ditt meddelande (översätt INTE den texten):",
"crash_page_switch_instance": "försökte <a href=\"`x`\">använda en annan instans</a>",
"generic_count_weeks": "{{count}}vecka",
"generic_count_weeks_plural": "{{count}}veckor",
"videoinfo_watch_on_youTube": "Titta på YouTube",
"Music in this video": "Musik i denna video",
"footer_modfied_source_code": "Modifierad källkod",
"generic_button_rss": "RSS",
"preferences_quality_dash_option_4320p": "4320p",
"generic_count_hours": "{{count}}timme",
"generic_count_hours_plural": "{{count}}timmar",
"French (auto-generated)": "Franska (auto-genererad)",
"crash_page_read_the_faq": "läs <a href=\"`x`\">Vanliga frågor (FAQ)</a>",
"user_created_playlists": "`x` skapade spellistor",
"channel_tab_channels_label": "Kanaler",
"search_filters_type_option_all": "Vilken typ som helst",
"Russian (auto-generated)": "Ryska (auto-genererad)",
"preferences_quality_dash_option_480p": "480p",
"comments_view_x_replies": "Se {{count}} svar",
"comments_view_x_replies_plural": "Se {{count}} svar",
"footer_original_source_code": "Ursprunglig källkod",
"Portuguese (Brazil)": "Portugisiska (Brasilien)",
"search_filters_features_option_vr180": "VR180",
"error_video_not_in_playlist": "Den begärda videon finns inte i den här spellistan. <a href=\"`x`\">Klicka här för startsidan för spellistan.</a>",
"Dutch (auto-generated)": "Nederländska (auto-genererad)",
"generic_count_days": "{{count}}dag",
"generic_count_days_plural": "{{count}}dagar",
"Vietnamese (auto-generated)": "Vietnamesiska (auto-genererad)",
"search_filters_duration_option_none": "Vilken varaktighet som helst",
"preferences_quality_dash_option_240p": "240p",
"Chinese": "Kinesiska",
"preferences_automatic_instance_redirect_label": "Automatisk instansomdirigering (återgång till redirect.invidious.io): ",
"generic_button_delete": "Radera",
"Import YouTube playlist (.csv)": "Importera YouTube spellista (.csv)",
"next_steps_error_message": "Därefter bör du försöka: ",
"Standard YouTube license": "Standard YouTube licens",
"Import YouTube watch history (.json)": "Importera YouTube visningshistorik (.json)",
"search_filters_duration_option_medium": "Medium (4 - 20 minuter)",
"generic_count_seconds": "{{count}}sekund",
"generic_count_seconds_plural": "{{count}}sekunder",
"search_filters_date_label": "Uppladdningsdatum",
"crash_page_you_found_a_bug": "Det verkar som att du har hittat en bugg i Invidious!",
"generic_views_count": "{{count}} visning",
"generic_views_count_plural": "{{count}} visningar",
"toggle_theme": "Växla tema"
} }

View File

@ -486,5 +486,7 @@
"playlist_button_add_items": "Video ekle", "playlist_button_add_items": "Video ekle",
"channel_tab_podcasts_label": "Podcast'ler", "channel_tab_podcasts_label": "Podcast'ler",
"generic_channels_count": "{{count}} kanal", "generic_channels_count": "{{count}} kanal",
"generic_channels_count_plural": "{{count}} kanal" "generic_channels_count_plural": "{{count}} kanal",
"Import YouTube watch history (.json)": "YouTube İzleme Geçmişini İçe Aktar (.json)",
"toggle_theme": "Temayı Değiştir"
} }

View File

@ -503,5 +503,7 @@
"generic_button_save": "Зберегти", "generic_button_save": "Зберегти",
"generic_channels_count_0": "{{count}} канал", "generic_channels_count_0": "{{count}} канал",
"generic_channels_count_1": "{{count}} канали", "generic_channels_count_1": "{{count}} канали",
"generic_channels_count_2": "{{count}} каналів" "generic_channels_count_2": "{{count}} каналів",
"Import YouTube watch history (.json)": "Імпортувати історію переглядів YouTube (.json)",
"toggle_theme": "Перемкнути тему"
} }

View File

@ -1,16 +1,16 @@
{ {
"generic_videos_count_0": "{{count}} video", "generic_videos_count_0": "{{count}} video",
"generic_subscribers_count_0": "{{count}} người theo dõi", "generic_subscribers_count_0": "{{count}} người đăng ký",
"LIVE": "TRỰC TIẾP", "LIVE": "TRỰC TIẾP",
"Shared `x` ago": "Đã chia sẻ `x` trước", "Shared `x` ago": "Đã chia sẻ `x` trước",
"Unsubscribe": "Hủy theo dõi", "Unsubscribe": "Hủy đăng ký",
"Subscribe": "Theo dõi", "Subscribe": "Đăng ký",
"View channel on YouTube": "Xem kênh trên YouTube", "View channel on YouTube": "Xem kênh trên YouTube",
"View playlist on YouTube": "Xem danh sách phát trên YouTube", "View playlist on YouTube": "Xem danh sách phát trên YouTube",
"newest": "mới nhất", "newest": "Mới nhất",
"oldest": "lâu đời nhất", "oldest": " nhất",
"popular": "phổ biến", "popular": "Phổ biến",
"last": "Cuối cùng", "last": "cuối cùng",
"Next page": "Trang tiếp theo", "Next page": "Trang tiếp theo",
"Previous page": "Trang trước", "Previous page": "Trang trước",
"Clear watch history?": "Xóa lịch sử xem?", "Clear watch history?": "Xóa lịch sử xem?",
@ -18,45 +18,45 @@
"New passwords must match": "Mật khẩu mới phải khớp", "New passwords must match": "Mật khẩu mới phải khớp",
"Authorize token?": "Cấp phép mã thông báo?", "Authorize token?": "Cấp phép mã thông báo?",
"Authorize token for `x`?": "Cấp phép mã thông báo cho `x`?", "Authorize token for `x`?": "Cấp phép mã thông báo cho `x`?",
"Yes": "Đúng", "Yes": "",
"No": "Không", "No": "Không",
"Import and Export Data": "Nhập và xuất dữ liệu", "Import and Export Data": "Nhập và xuất dữ liệu",
"Import": "Nhập", "Import": "Nhập",
"Import Invidious data": "Nhập dữ liệu Invidious JSON", "Import Invidious data": "Nhập dữ liệu Invidious dưới dạng JSON",
"Import YouTube subscriptions": "Nhập dữ liệu thuê bao YouTube/OPML", "Import YouTube subscriptions": "Nhập các kênh đã đăng ký từ YouTube/OPML",
"Import FreeTube subscriptions (.db)": "Nhập đăng ký FreeTube (.db)", "Import FreeTube subscriptions (.db)": "Nhập các kênh đã đăng ký từ FreeTube (.db)",
"Import NewPipe subscriptions (.json)": "Nhập đăng ký NewPipe (.json)", "Import NewPipe subscriptions (.json)": "Nhập các kênh đã đăng ký từ NewPipe (.json)",
"Import NewPipe data (.zip)": "Nhập dữ liệu NewPipe (.zip)", "Import NewPipe data (.zip)": "Nhập dữ liệu từ NewPipe (.zip)",
"Export": "Xuất", "Export": "Xuất",
"Export subscriptions as OPML": "Xuất đăng ký dưới dạng OPML", "Export subscriptions as OPML": "Xuất các kênh đã đăng ký dưới dạng OPML",
"Export subscriptions as OPML (for NewPipe & FreeTube)": "Xuất đăng ký dưới dạng OPML (cho NewPipe & FreeTube)", "Export subscriptions as OPML (for NewPipe & FreeTube)": "Xuất các kênh đã đăng ký dưới dạng OPML (cho NewPipe & FreeTube)",
"Export data as JSON": "Xuất dữ liệu Invidious dưới dạng JSON", "Export data as JSON": "Xuất dữ liệu Invidious dưới dạng JSON",
"Delete account?": "Xóa tài khoản?", "Delete account?": "Xóa tài khoản?",
"History": "Lịch sử", "History": "Lịch sử",
"An alternative front-end to YouTube": "Giao diện người dùng thay thế cho YouTube", "An alternative front-end to YouTube": "Một front-end thay thế cho YouTube",
"JavaScript license information": "Thông tin giấy phép JavaScript", "JavaScript license information": "Thông tin giấy phép JavaScript",
"source": "nguồn", "source": "nguồn",
"Log in": "Đăng nhập", "Log in": "Đăng nhập",
"Log in/register": "Đăng nhập / đăng ký", "Log in/register": "Đăng nhập / đăng ký",
"User ID": "Tên người dùng", "User ID": "ID người dùng",
"Password": "Mật khẩu", "Password": "Mật khẩu",
"Time (h:mm:ss):": "Thời gian (h:mm:ss):", "Time (h:mm:ss):": "Thời gian (h:mm:ss):",
"Text CAPTCHA": "Nhắn tin tới CAPTCHA", "Text CAPTCHA": "CAPTCHA dạng chữ",
"Image CAPTCHA": "Hình ảnh CAPTCHA", "Image CAPTCHA": "CAPTCHA dạng ảnh",
"Sign In": "Đăng nhập", "Sign In": "Đăng nhập",
"Register": "Đăng ký", "Register": "Đăng ký",
"E-mail": "E-mail", "E-mail": "E-mail",
"Preferences": "Sở thích", "Preferences": "Sở thích",
"preferences_category_player": "Tùy chọn trình phát video", "preferences_category_player": "Tùy chọn trình phát video",
"preferences_video_loop_label": "Luôn lặp lại: ", "preferences_video_loop_label": "Luôn lặp lại: ",
"preferences_autoplay_label": "Tự chạy: ", "preferences_autoplay_label": "Tự động phát: ",
"preferences_continue_label": "Phát kế tiếp theo mặc định: ", "preferences_continue_label": "Phát kế tiếp theo mặc định: ",
"preferences_continue_autoplay_label": "Tự động phát video tiếp theo: ", "preferences_continue_autoplay_label": "Tự động phát video tiếp theo: ",
"preferences_listen_label": "Nghe theo mặc định: ", "preferences_listen_label": "Nghe theo mặc định: ",
"preferences_local_label": "Video proxy: ", "preferences_local_label": "Video proxy: ",
"preferences_speed_label": "Tốc độ mặc định: ", "preferences_speed_label": "Tốc độ mặc định: ",
"preferences_quality_label": "Chất lượng video ưa thích: ", "preferences_quality_label": "Chất lượng video ưa thích: ",
"preferences_volume_label": "Âm lượng trình phát video: ", "preferences_volume_label": "Âm lượng video: ",
"preferences_comments_label": "Nhận xét mặc định: ", "preferences_comments_label": "Nhận xét mặc định: ",
"youtube": "YouTube", "youtube": "YouTube",
"reddit": "Reddit", "reddit": "Reddit",
@ -64,7 +64,7 @@
"Fallback captions: ": "Phụ đề dự phòng: ", "Fallback captions: ": "Phụ đề dự phòng: ",
"preferences_related_videos_label": "Hiển thị các video có liên quan: ", "preferences_related_videos_label": "Hiển thị các video có liên quan: ",
"preferences_annotations_label": "Hiển thị chú thích theo mặc định: ", "preferences_annotations_label": "Hiển thị chú thích theo mặc định: ",
"preferences_extend_desc_label": "Tự động mở rộng mô tả video: ", "preferences_extend_desc_label": "Tự động mở rộng phần mô tả của video: ",
"preferences_vr_mode_label": "Video 360 độ tương tác (yêu cầu WebGL): ", "preferences_vr_mode_label": "Video 360 độ tương tác (yêu cầu WebGL): ",
"preferences_category_visual": "Tùy chọn hình ảnh", "preferences_category_visual": "Tùy chọn hình ảnh",
"preferences_player_style_label": "Phong cách trình phát: ", "preferences_player_style_label": "Phong cách trình phát: ",
@ -82,13 +82,13 @@
"preferences_sort_label": "Sắp xếp video theo: ", "preferences_sort_label": "Sắp xếp video theo: ",
"published": "được phát hành", "published": "được phát hành",
"published - reverse": "đã xuất bản - đảo ngược", "published - reverse": "đã xuất bản - đảo ngược",
"alphabetically": "theo thứ tự bảng chữ cái", "alphabetically": "Thứ tự (A - Z)",
"alphabetically - reverse": "theo thứ tự bảng chữ cái - đảo ngược", "alphabetically - reverse": "Thứ tự (Z - A)",
"channel name": "Tên kênh", "channel name": "Tên kênh (A - Z)",
"channel name - reverse": "tên kênh - đảo ngược", "channel name - reverse": "Tên kênh (Z - A)",
"Only show latest video from channel: ": "Chỉ hiển thị video mới nhất từ kênh: ", "Only show latest video from channel: ": "Chỉ hiển thị video mới nhất từ kênh: ",
"Only show latest unwatched video from channel: ": "Chỉ hiển thị video chưa xem mới nhất từ kênh: ", "Only show latest unwatched video from channel: ": "Chỉ hiển thị video chưa xem mới nhất từ kênh: ",
"preferences_unseen_only_label": "Chỉ hiển thị chưa xem: ", "preferences_unseen_only_label": "Chỉ hiển thị các video chưa từng xem: ",
"preferences_notifications_only_label": "Chỉ hiển thị thông báo (nếu có): ", "preferences_notifications_only_label": "Chỉ hiển thị thông báo (nếu có): ",
"Enable web notifications": "Bật thông báo web", "Enable web notifications": "Bật thông báo web",
"`x` uploaded a video": "`x` đã tải lên một video", "`x` uploaded a video": "`x` đã tải lên một video",
@ -99,7 +99,7 @@
"Change password": "Đổi mật khẩu", "Change password": "Đổi mật khẩu",
"Manage subscriptions": "Quản lý các mục đăng kí", "Manage subscriptions": "Quản lý các mục đăng kí",
"Manage tokens": "Quản lý mã thông báo", "Manage tokens": "Quản lý mã thông báo",
"Watch history": "Lịch sử xem", "Watch history": "Xem lịch sử",
"Delete account": "Xóa tài khoản", "Delete account": "Xóa tài khoản",
"preferences_category_admin": "Tùy chọn quản trị viên", "preferences_category_admin": "Tùy chọn quản trị viên",
"preferences_default_home_label": "Trang chủ mặc định: ", "preferences_default_home_label": "Trang chủ mặc định: ",
@ -121,7 +121,7 @@
"View privacy policy.": "Xem chính sách bảo mật.", "View privacy policy.": "Xem chính sách bảo mật.",
"Trending": "Xu hướng", "Trending": "Xu hướng",
"Public": "Công khai", "Public": "Công khai",
"Unlisted": "Không hiển thị", "Unlisted": "Không công khai",
"Private": "Riêng tư", "Private": "Riêng tư",
"View all playlists": "Xem tất cả danh sách phát", "View all playlists": "Xem tất cả danh sách phát",
"Updated `x` ago": "Đã cập nhật` x` trước", "Updated `x` ago": "Đã cập nhật` x` trước",
@ -131,24 +131,24 @@
"Title": "Tiêu đề", "Title": "Tiêu đề",
"Playlist privacy": "Bảo mật danh sách phát", "Playlist privacy": "Bảo mật danh sách phát",
"Editing playlist `x`": "Chỉnh sửa danh sách phát` x`", "Editing playlist `x`": "Chỉnh sửa danh sách phát` x`",
"Show more": "Cho xem nhiều hơn", "Show more": "Hiển thị thêm",
"Show less": "Hiện ít hơn", "Show less": "Hiển thị ít hơn",
"Watch on YouTube": "Xem trên YouTube", "Watch on YouTube": "Xem trên YouTube",
"Switch Invidious Instance": "Chuyển phiên bản Invidious", "Switch Invidious Instance": "Chuyển phiên bản Invidious",
"Hide annotations": "Ẩn chú thích", "Hide annotations": "Ẩn chú thích",
"Show annotations": "Hiển thị chú thích", "Show annotations": "Hiển thị chú thích",
"Genre: ": "Thể loại: ", "Genre: ": "Thể loại: ",
"License: ": "Giấy phép: ", "License: ": "Giấy phép: ",
"Family friendly? ": "Gia đình thân thiện? ", "Family friendly? ": "Thân thiện với gia đình? ",
"Wilson score: ": "Điểm số Wilson: ", "Wilson score: ": "Điểm số Wilson: ",
"Engagement: ": "Hôn ước: ", "Engagement: ": "Hôn ước: ",
"Whitelisted regions: ": "Các vùng nằm trong danh sách trắng: ", "Whitelisted regions: ": "Các vùng nằm trong danh sách trắng: ",
"Blacklisted regions: ": "Khu vực nằm trong danh sách đen: ", "Blacklisted regions: ": "Các vùng nằm trong danh sách đen: ",
"Shared `x`": "Chia sẻ` x`", "Shared `x`": "Chia sẻ` x`",
"View Reddit comments": "Xem nhận xét trên Reddit", "View Reddit comments": "Xem nh luận trên Reddit",
"Hide replies": "Ẩn câu trả lời", "Hide replies": "Ẩn phản hồi",
"Show replies": "Hiển thị câu trả lời", "Show replies": "Hiển thị phản hồi",
"Incorrect password": "Mật khẩu không đúng", "Incorrect password": "Mật khẩu không chính xác",
"Wrong answer": "Câu trả lời sai", "Wrong answer": "Câu trả lời sai",
"Erroneous CAPTCHA": "CAPTCHA bị lỗi", "Erroneous CAPTCHA": "CAPTCHA bị lỗi",
"CAPTCHA is a required field": "CAPTCHA là trường bắt buộc", "CAPTCHA is a required field": "CAPTCHA là trường bắt buộc",
@ -190,35 +190,35 @@
"Bulgarian": "Tiếng Bungari", "Bulgarian": "Tiếng Bungari",
"Burmese": "Tiếng Miến Điện", "Burmese": "Tiếng Miến Điện",
"Catalan": "Tiếng Catalan", "Catalan": "Tiếng Catalan",
"Cebuano": "Cebuano", "Cebuano": "Tiếng Cebu",
"Chinese (Simplified)": "Tiếng Trung (Giản thể)", "Chinese (Simplified)": "Tiếng Trung (Giản thể)",
"Chinese (Traditional)": "Tiếng Trung (Phồn thể)", "Chinese (Traditional)": "Tiếng Trung (Phồn thể)",
"Corsican": "Corsican", "Corsican": "Tiếng Corse",
"Croatian": "Tiếng Croatia", "Croatian": "Tiếng Croatia",
"Czech": "Tiếng Séc", "Czech": "Tiếng Séc",
"Danish": "Người Đan Mạch", "Danish": "Tiếng Đan Mạch",
"Dutch": "Tiếng Hà Lan", "Dutch": "Tiếng Hà Lan",
"Esperanto": "Quốc tế ngữ", "Esperanto": "Quốc tế ngữ",
"Estonian": "Tiếng Estonia", "Estonian": "Tiếng Estonia",
"Filipino": "Filipino", "Filipino": "Tiếng Philippines",
"Finnish": "Tiếng Phần Lan", "Finnish": "Tiếng Phần Lan",
"French": "Người Pháp", "French": "Tiếng Pháp",
"Galician": "Tiếng Galicia", "Galician": "Tiếng Galicia",
"Georgian": "Tiếng Georgia", "Georgian": "Tiếng Georgia",
"German": "Tiếng Đức", "German": "Tiếng Đức",
"Greek": "Người Hy Lạp", "Greek": "Tiếng Hy Lạp",
"Gujarati": "Gujarati", "Gujarati": "Tiếng Gujarat",
"Haitian Creole": "Tiếng Creole của Haiti", "Haitian Creole": "Tiếng Creole (Haiti)",
"Hausa": "Hausa", "Hausa": "Tiếng Hausa",
"Hawaiian": "Tiếng Hawaii", "Hawaiian": "Tiếng Hawaii",
"Hebrew": "Tiếng Do Thái", "Hebrew": "Tiếng Do Thái",
"Hindi": "Tiếng Hindi", "Hindi": "Tiếng Hindi",
"Hmong": "Hmong", "Hmong": "Tiếng Hmong",
"Hungarian": "Người Hungary", "Hungarian": "Tiếng Hungary",
"Icelandic": "Tiếng Iceland", "Icelandic": "Tiếng Iceland",
"Igbo": "Igbo", "Igbo": "Tiếng Igbo",
"Indonesian": "Tiếng Indonesia", "Indonesian": "Tiếng Indonesia",
"Irish": "Tiếng Ailen", "Irish": "Tiếng Ireland",
"Italian": "Tiếng Ý", "Italian": "Tiếng Ý",
"Japanese": "Tiếng Nhật", "Japanese": "Tiếng Nhật",
"Javanese": "Tiếng Java", "Javanese": "Tiếng Java",
@ -237,37 +237,37 @@
"Malagasy": "Tiếng Malagasy", "Malagasy": "Tiếng Malagasy",
"Malay": "Tiếng Mã Lai", "Malay": "Tiếng Mã Lai",
"Malayalam": "Tiếng Malayalam", "Malayalam": "Tiếng Malayalam",
"Maltese": "Cây nho", "Maltese": "Tiếng Malta",
"Maori": "Tiếng Maori", "Maori": "Tiếng Maori",
"Marathi": "Marathi", "Marathi": "Tiếng Marathi",
"Mongolian": "Tiếng Mông Cổ", "Mongolian": "Tiếng Mông Cổ",
"Nepali": "Tiếng Nepal", "Nepali": "Tiếng Nepal",
"Norwegian Bokmål": "Tiếng Na Uy Bokmål", "Norwegian Bokmål": "Tiếng Na Uy (Bokmål)",
"Nyanja": "Nyanja", "Nyanja": "Tiếng Chewa / Nyanja",
"Pashto": "Pashto", "Pashto": "Tiếng Pashtun",
"Persian": "Tiếng Ba Tư", "Persian": "Tiếng Ba Tư",
"Polish": "Đánh bóng", "Polish": "Tiếng Ba Lan",
"Portuguese": "Tiếng Bồ Đào Nha", "Portuguese": "Tiếng Bồ Đào Nha",
"Punjabi": "Punjabi", "Punjabi": "Tiếng Punjab",
"Romanian": "Tiếng Rumani", "Romanian": "Tiếng Rumani",
"Russian": "Tiếng Nga", "Russian": "Tiếng Nga",
"Samoan": "Samoan", "Samoan": "Tiếng Samoa",
"Scottish Gaelic": "Tiếng Gaelic Scotland", "Scottish Gaelic": "Tiếng Gaelic (Scotland)",
"Serbian": "Tiếng Serbia", "Serbian": "Tiếng Serbia",
"Shona": "Shona", "Shona": "Tiếng Shona",
"Sindhi": "Sindhi", "Sindhi": "Tiếng Sindh",
"Sinhala": "Sinhala", "Sinhala": "Tiếng Sinhala",
"Slovak": "Tiếng Slovak", "Slovak": "Tiếng Slovak",
"Slovenian": "Tiếng Slovenia", "Slovenian": "Tiếng Slovenia",
"Somali": "Tiếng Somali", "Somali": "Tiếng Somali",
"Southern Sotho": "Southern Sotho", "Southern Sotho": "Southern Sotho",
"Spanish": "Người Tây Ban Nha", "Spanish": "Tiếng Tây Ban Nha",
"Spanish (Latin America)": "Tiếng Tây Ban Nha (Mỹ Latinh)", "Spanish (Latin America)": "Tiếng Tây Ban Nha (Mỹ Latinh)",
"Sundanese": "Tiếng Sundan", "Sundanese": "Tiếng Sundan",
"Swahili": "Tiếng Swahili", "Swahili": "Tiếng Swahili",
"Swedish": "Tiếng Thụy Điển", "Swedish": "Tiếng Thụy Điển",
"Tajik": "Tajik", "Tajik": "Tiếng Tajik",
"Tamil": "Tamil", "Tamil": "Tiếng Tamil",
"Telugu": "Tiếng Telugu", "Telugu": "Tiếng Telugu",
"Thai": "Tiếng Thái", "Thai": "Tiếng Thái",
"Turkish": "Tiếng Thổ Nhĩ Kỳ", "Turkish": "Tiếng Thổ Nhĩ Kỳ",
@ -275,17 +275,17 @@
"Urdu": "Tiếng Urdu", "Urdu": "Tiếng Urdu",
"Uzbek": "Tiếng Uzbek", "Uzbek": "Tiếng Uzbek",
"Vietnamese": "Tiếng Việt", "Vietnamese": "Tiếng Việt",
"Welsh": "Người xứ Wales", "Welsh": "Tiếng Wales",
"Western Frisian": "Western Frisian", "Western Frisian": "Tiếng Tây Frisia",
"Xhosa": "Xhosa", "Xhosa": "Tiếng Nam Phi",
"Yiddish": "Yiddish", "Yiddish": "Tiếng Yiddish",
"Yoruba": "Yoruba", "Yoruba": "Tiếng Yoruba",
"Zulu": "Tiếng Zulu", "Zulu": "Tiếng Zulu",
"Fallback comments: ": "Nhận xét dự phòng: ", "Fallback comments: ": "Nhận xét dự phòng: ",
"Popular": "Phổ biến", "Popular": "Phổ biến",
"Search": "Tìm kiếm", "Search": "Tìm kiếm",
"Top": "Hàng đầu", "Top": "Hàng đầu",
"About": "Trong khoảng", "About": "Giới thiệu",
"Rating: ": "Xếp hạng: ", "Rating: ": "Xếp hạng: ",
"preferences_locale_label": "Ngôn ngữ: ", "preferences_locale_label": "Ngôn ngữ: ",
"View as playlist": "Xem dưới dạng danh sách phát", "View as playlist": "Xem dưới dạng danh sách phát",
@ -295,45 +295,45 @@
"News": "Tin tức", "News": "Tin tức",
"Movies": "Phim", "Movies": "Phim",
"Download": "Tải xuống", "Download": "Tải xuống",
"Download as: ": "Tải tệp dưới dạng: ", "Download as: ": "Tải xuống dưới dạng: ",
"%A %B %-d, %Y": "% A% B% -d,% Y", "%A %B %-d, %Y": "% A% B% -d,% Y",
"(edited)": "(đã chỉnh sửa)", "(edited)": "(đã chỉnh sửa)",
"YouTube comment permalink": "Liên kết cố định nhận xét trên YouTube", "YouTube comment permalink": "Liên kết cố định nhận xét trên YouTube",
"permalink": "liên kết cố định", "permalink": "liên kết cố định",
"`x` marked it with a ❤": "` x` đã đánh dấu nó bằng một ❤", "`x` marked it with a ❤": "` x` đã đánh dấu nó bằng một ❤",
"Audio mode": "Chế độ âm thanh", "Audio mode": "Chế độ audio",
"Video mode": "Chế độ quay", "Video mode": "Chế độ video",
"channel_tab_videos_label": "Video", "channel_tab_videos_label": "Video",
"Playlists": "Danh sách phát", "Playlists": "Danh sách phát",
"channel_tab_community_label": "Cộng đồng", "channel_tab_community_label": "Cộng đồng",
"search_filters_sort_option_relevance": "liên quan", "search_filters_sort_option_relevance": "Liên quan",
"search_filters_sort_option_rating": "Xếp hạng", "search_filters_sort_option_rating": "Xếp hạng",
"search_filters_sort_option_date": "ngày", "search_filters_sort_option_date": "Ngày tải lên",
"search_filters_sort_option_views": "lượt xem", "search_filters_sort_option_views": "Lượt xem",
"search_filters_type_label": "content_type", "search_filters_type_label": "Thể loại",
"search_filters_duration_label": "thời lượng", "search_filters_duration_label": "Thời lượng",
"search_filters_features_label": "đặc trưng", "search_filters_features_label": "Đặc điểm",
"search_filters_sort_label": "sắp xếp", "search_filters_sort_label": "Sắp xếp theo",
"search_filters_date_option_hour": "giờ", "search_filters_date_option_hour": "Một giờ qua",
"search_filters_date_option_today": "hôm nay", "search_filters_date_option_today": "Hôm nay",
"search_filters_date_option_week": "tuần", "search_filters_date_option_week": "Tuần này",
"search_filters_date_option_month": "tháng", "search_filters_date_option_month": "Tháng này",
"search_filters_date_option_year": "năm", "search_filters_date_option_year": "Năm này",
"search_filters_type_option_video": "video", "search_filters_type_option_video": "video",
"search_filters_type_option_channel": "kênh", "search_filters_type_option_channel": "Kênh",
"search_filters_type_option_playlist": "danh sách phát", "search_filters_type_option_playlist": "Danh sách phát",
"search_filters_type_option_movie": "bộ phim", "search_filters_type_option_movie": "Phim",
"search_filters_type_option_show": "chỉ", "search_filters_type_option_show": "Hiện",
"search_filters_features_option_hd": "hd", "search_filters_features_option_hd": "HD",
"search_filters_features_option_subtitles": "phụ đề", "search_filters_features_option_subtitles": "Phụ đề",
"search_filters_features_option_c_commons": "Commons sáng tạo", "search_filters_features_option_c_commons": "Giấy phép Creative Commons",
"search_filters_features_option_three_d": "3d", "search_filters_features_option_three_d": "3D",
"search_filters_features_option_live": "trực tiếp", "search_filters_features_option_live": "Trực tiếp",
"search_filters_features_option_four_k": "4k", "search_filters_features_option_four_k": "4K",
"search_filters_features_option_location": "vị trí", "search_filters_features_option_location": "Vị trí",
"search_filters_features_option_hdr": "hdr", "search_filters_features_option_hdr": "HDR",
"Current version: ": "Phiên bản hiện tại: ", "Current version: ": "Phiên bản hiện tại: ",
"search_filters_title": "bộ lọc", "search_filters_title": "Bộ lọc",
"generic_playlists_count": "{{count}} danh sách phát", "generic_playlists_count": "{{count}} danh sách phát",
"generic_views_count": "{{count}} lượt xem", "generic_views_count": "{{count}} lượt xem",
"View `x` comments": { "View `x` comments": {
@ -350,31 +350,31 @@
"preferences_quality_dash_label": "Chất lượng video DASH ưa thích ", "preferences_quality_dash_label": "Chất lượng video DASH ưa thích ",
"preferences_quality_dash_option_auto": "Tự động", "preferences_quality_dash_option_auto": "Tự động",
"Subscriptions": "Thuê bao", "Subscriptions": "Thuê bao",
"View YouTube comments": "Hiển thị bình luận trên YouTube", "View YouTube comments": "Hiển thị bình luận t YouTube",
"View more comments on Reddit": "Hiển thị thêm bình luận từ Reddit", "View more comments on Reddit": "Hiển thị thêm bình luận từ Reddit",
"Music in this video": "Nhạc trong video này", "Music in this video": "Nhạc trong video này",
"Artist: ": "Nghệ sĩ: ", "Artist: ": "Nghệ sĩ: ",
"Premieres `x`": "Phát lần đầu `x`", "Premieres `x`": "Phát lần đầu `x`",
"preferences_region_label": "Nội dung theo quốc gia ", "preferences_region_label": "Nội dung theo quốc gia ",
"search_message_change_filters_or_query": "Thử mở rộng nội dung tìm kiếm hoặc thay đổi bộ lọc.", "search_message_change_filters_or_query": "Thử mở rộng nội dung tìm kiếm hoặc thay đổi bộ lọc.",
"preferences_quality_option_small": "Nhỏ", "preferences_quality_option_small": "Thấp",
"preferences_quality_dash_option_144p": "144p", "preferences_quality_dash_option_144p": "144p",
"invidious": "Invidious", "invidious": "Invidious",
"preferences_quality_dash_option_240p": "240p", "preferences_quality_dash_option_240p": "240p",
"Import/export": "Xuất/nhập dữ liệu", "Import/export": "Nhập/Xuất",
"preferences_quality_dash_option_4320p": "4320p", "preferences_quality_dash_option_4320p": "4320p (8K)",
"preferences_quality_option_dash": "DASH (tự tối ưu chất lượng)", "preferences_quality_option_dash": "DASH (tự tối ưu chất lượng)",
"generic_subscriptions_count_0": "{{count}} người đăng kí", "generic_subscriptions_count_0": "{{count}} người đăng kí",
"preferences_quality_dash_option_1440p": "1440p", "preferences_quality_dash_option_1440p": "1440p (2K)",
"preferences_quality_dash_option_480p": "480p", "preferences_quality_dash_option_480p": "480p",
"preferences_quality_dash_option_2160p": "2160p", "preferences_quality_dash_option_2160p": "2160p (4K)",
"search_message_no_results": "Tìm kiếm không có kết quả.", "search_message_no_results": "Tìm kiếm không có kết quả.",
"preferences_quality_dash_option_1080p": "1080p", "preferences_quality_dash_option_1080p": "1080p",
"preferences_quality_dash_option_720p": "720p", "preferences_quality_dash_option_720p": "720p",
"preferences_quality_option_medium": "Trung bình", "preferences_quality_option_medium": "Trung bình",
"Load more": "Hiển thị thêm", "Load more": "Tải thêm",
"comments_points_count_0": "{{count}} điểm", "comments_points_count_0": "{{count}} điểm",
"Import YouTube playlist (.csv)": "Nhập danh sách phát YouTube (.csv)", "Import YouTube playlist (.csv)": "Nhập các danh sách phát từ YouTube (.csv)",
"preferences_quality_dash_option_best": "Tốt nhất", "preferences_quality_dash_option_best": "Tốt nhất",
"preferences_quality_dash_option_360p": "360p", "preferences_quality_dash_option_360p": "360p",
"subscriptions_unseen_notifs_count_0": "{{count}} thông báo chưa đọc", "subscriptions_unseen_notifs_count_0": "{{count}} thông báo chưa đọc",
@ -382,10 +382,93 @@
"search_message_use_another_instance": " Bạn cũng có thể tìm kiếm <a href=\"`x`\"> ở một phiên bản khác</a>.", "search_message_use_another_instance": " Bạn cũng có thể tìm kiếm <a href=\"`x`\"> ở một phiên bản khác</a>.",
"Standard YouTube license": "Giấy phép YouTube thông thường", "Standard YouTube license": "Giấy phép YouTube thông thường",
"Album: ": "Album: ", "Album: ": "Album: ",
"preferences_save_player_pos_label": "Lưu vị trí xem cuối cùng ", "preferences_save_player_pos_label": "Lưu vị trí xem: ",
"Hi! Looks like you have JavaScript turned off. Click here to view comments, keep in mind they may take a bit longer to load.": "Xin chào! Có vẻ như bạn đã tắt JavaScript. Bấm vào đây để xem bình luận, lưu ý rằng thời gian tải có thể lâu hơn.", "Hi! Looks like you have JavaScript turned off. Click here to view comments, keep in mind they may take a bit longer to load.": "Xin chào! Có vẻ như bạn đã tắt JavaScript. Bấm vào đây để xem bình luận, lưu ý rằng thời gian tải có thể lâu hơn.",
"Chinese (China)": "Tiếng Trung (Trung Quốc)", "Chinese (China)": "Tiếng Trung (Trung Quốc)",
"generic_button_cancel": "Hủy", "generic_button_cancel": "Hủy",
"Chinese": "Tiếng Trung", "Chinese": "Tiếng Trung",
"generic_button_delete": "Xóa" "generic_button_delete": "Xóa",
"Korean (auto-generated)": "Tiếng Hàn (được tạo tự động)",
"search_filters_features_option_three_sixty": "360°",
"channel_tab_podcasts_label": "Podcast",
"Spanish (Mexico)": "Tiếng Tây Ban Nha (Mexico)",
"search_filters_apply_button": "Áp dụng các mục đã chọn",
"Download is disabled": "Tải xuống đã bị vô hiệu hóa.",
"next_steps_error_message_go_to_youtube": "Đi đến YouTube",
"German (auto-generated)": "Tiếng Đức (được tạo tự động)",
"Japanese (auto-generated)": "Tiếng Nhật (được tạo tự động)",
"footer_donate_page": "Ủng hộ",
"crash_page_before_reporting": "Trước khi báo cáo lỗi, hãy chắc chắn rằng bạn đã:",
"Channel Sponsor": "Nhà tài trợ của kênh",
"videoinfo_started_streaming_x_ago": "Đã bắt đầu phát sóng `x` trước",
"videoinfo_youTube_embed_link": "Nhúng",
"channel_tab_streams_label": "Phát trực tiếp",
"playlist_button_add_items": "Thêm video",
"generic_count_minutes_0": "{{count}} phút",
"user_saved_playlists": "`x` danh sách phát đã lưu",
"Spanish (Spain)": "Tiếng Tây Ban Nha (Tây Ban Nha)",
"crash_page_refresh": "Đã thử <a href=\"`x`\">tải lại trang</a>",
"Chinese (Hong Kong)": "Tiếng Trung (Hồng Kông)",
"generic_count_months_0": "{{count}} tháng",
"download_subtitles": "Phụ đề - `x` (.vtt)",
"generic_button_save": "Lưu",
"crash_page_search_issue": "Tìm <a href=\"`x`\">lỗi có sẵn trên GitHub</a>",
"none": "không",
"English (United States)": "Tiếng Anh (Mỹ)",
"next_steps_error_message_refresh": "Tải lại",
"Video unavailable": "Video không có sẵn",
"footer_source_code": "Mã nguồn",
"search_filters_duration_option_short": "Ngắn (< 4 phút)",
"search_filters_duration_option_long": "Dài (> 20 phút)",
"tokens_count_0": "{{count}} mã thông báo",
"Italian (auto-generated)": "Tiếng Ý (được tạo tự động)",
"channel_tab_shorts_label": "Shorts",
"channel_tab_releases_label": "Mới tải lên",
"`x` ago": "`x` trước",
"Interlingue": "Tiếng Khoa học Quốc tế",
"generic_channels_count_0": "{{count}} kênh",
"Chinese (Taiwan)": "Tiếng Trung (Đài Loan)",
"adminprefs_modified_source_code_url_label": "URL tới kho lưu trữ mã nguồn đã sửa đổi",
"Turkish (auto-generated)": "Tiếng Thổ Nhĩ Kỳ (được tạo tự động)",
"Indonesian (auto-generated)": "Tiếng Indonesia (được tạo tự động)",
"Portuguese (auto-generated)": "Tiếng Bồ Đào Nha (được tạo tự động)",
"generic_count_years_0": "{{count}} năm",
"videoinfo_invidious_embed_link": "Liên kết nhúng",
"Popular enabled: ": "Đã bật phổ biến: ",
"Spanish (auto-generated)": "Tiếng Tây Ban Nha (được tạo tự động)",
"English (United Kingdom)": "Tiếng Anh Anh",
"channel_tab_playlists_label": "Danh sách phát",
"generic_button_edit": "Sửa",
"search_filters_features_option_purchased": "Đã mua",
"search_filters_date_option_none": "Mọi thời điểm",
"Cantonese (Hong Kong)": "Tiếng Quảng Châu (Hồng Kông)",
"crash_page_report_issue": "Nếu các điều trên không giúp được, xin hãy <a href=\"`x`\">tạo vấn đề mới trên GitHub</a> (ưu tiên tiếng Anh) và đính kèm đoạn chữ sau trong nội dung (giữ nguyên KHÔNG dịch):",
"crash_page_switch_instance": "Đã thử <a href=\"`x`\">dùng một phiên bản khác</a>",
"generic_count_weeks_0": "{{count}} tuần",
"videoinfo_watch_on_youTube": "Xem trên YouTube",
"footer_modfied_source_code": "Mã nguồn đã chỉnh sửa",
"generic_button_rss": "RSS",
"generic_count_hours_0": "{{count}} giờ",
"French (auto-generated)": "Tiếng Pháp (được tạo tự động)",
"crash_page_read_the_faq": "Đọc <a href=\"`x`\">Hỏi đáp thường gặp (FAQ)</a>",
"user_created_playlists": "`x` danh sách phát đã tạo",
"channel_tab_channels_label": "Kênh",
"search_filters_type_option_all": "Mọi thể loại",
"Russian (auto-generated)": "Tiếng Nga (được tạo tự động)",
"comments_view_x_replies_0": "Xem {{count}} lượt trả lời",
"footer_original_source_code": "Mã nguồn gốc",
"Portuguese (Brazil)": "Tiếng Bồ Đào Nha (Brazil)",
"search_filters_features_option_vr180": "VR180",
"error_video_not_in_playlist": "Video không tồn tại trong danh sách phát. <a href=\"`x`\">Bấm để trở về trang chủ của danh sách phát.</a>",
"Dutch (auto-generated)": "Tiếng Hà Lan (được tạo tự động)",
"generic_count_days_0": "{{count}} ngày",
"Vietnamese (auto-generated)": "Tiếng Việt (được tạo tự động)",
"search_filters_duration_option_none": "Mọi thời lượng",
"footer_documentation": "Tài liệu",
"next_steps_error_message": "Bạn có thể thử: ",
"Import YouTube watch history (.json)": "Nhập lịch sử xem từ YouTube (.json)",
"search_filters_duration_option_medium": "Trung bình (4 - 20 phút)",
"generic_count_seconds_0": "{{count}} giây",
"search_filters_date_label": "Ngày tải lên",
"crash_page_you_found_a_bug": "Có vẻ như bạn đã tìm ra lỗi trong Indivious!"
} }

View File

@ -470,5 +470,6 @@
"generic_button_save": "保存", "generic_button_save": "保存",
"generic_button_rss": "RSS", "generic_button_rss": "RSS",
"channel_tab_releases_label": "公告", "channel_tab_releases_label": "公告",
"generic_channels_count_0": "{{count}} 个频道" "generic_channels_count_0": "{{count}} 个频道",
"toggle_theme": "切换主题"
} }

View File

@ -470,5 +470,6 @@
"playlist_button_add_items": "新增影片", "playlist_button_add_items": "新增影片",
"channel_tab_podcasts_label": "Podcast", "channel_tab_podcasts_label": "Podcast",
"channel_tab_releases_label": "發布", "channel_tab_releases_label": "發布",
"generic_channels_count_0": "{{count}} 個頻道" "generic_channels_count_0": "{{count}} 個頻道",
"toggle_theme": "切換佈景主題"
} }

View File

@ -1,34 +1,27 @@
require "../../spec_helper.cr" require "../../spec_helper.cr"
MockLines = [ MockLines = ["Line 1", "Line 2"]
{ MockLinesWithEscapableCharacter = ["<Line 1>", "&Line 2>", '\u200E' + "Line\u200F 3", "\u00A0Line 4"]
"start_time": Time::Span.new(seconds: 1),
"end_time": Time::Span.new(seconds: 2),
"text": "Line 1",
},
{
"start_time": Time::Span.new(seconds: 2),
"end_time": Time::Span.new(seconds: 3),
"text": "Line 2",
},
]
Spectator.describe "WebVTT::Builder" do Spectator.describe "WebVTT::Builder" do
it "correctly builds a vtt file" do it "correctly builds a vtt file" do
result = WebVTT.build do |vtt| result = WebVTT.build do |vtt|
MockLines.each do |line| 2.times do |i|
vtt.cue(line["start_time"], line["end_time"], line["text"]) vtt.cue(
Time::Span.new(seconds: i),
Time::Span.new(seconds: i + 1),
MockLines[i]
)
end end
end end
expect(result).to eq([ expect(result).to eq([
"WEBVTT", "WEBVTT",
"", "",
"00:00:01.000 --> 00:00:02.000", "00:00:00.000 --> 00:00:01.000",
"Line 1", "Line 1",
"", "",
"00:00:02.000 --> 00:00:03.000", "00:00:01.000 --> 00:00:02.000",
"Line 2", "Line 2",
"", "",
"", "",
@ -42,8 +35,12 @@ Spectator.describe "WebVTT::Builder" do
} }
result = WebVTT.build(setting_fields) do |vtt| result = WebVTT.build(setting_fields) do |vtt|
MockLines.each do |line| 2.times do |i|
vtt.cue(line["start_time"], line["end_time"], line["text"]) vtt.cue(
Time::Span.new(seconds: i),
Time::Span.new(seconds: i + 1),
MockLines[i]
)
end end
end end
@ -52,13 +49,39 @@ Spectator.describe "WebVTT::Builder" do
"Kind: captions", "Kind: captions",
"Language: en", "Language: en",
"", "",
"00:00:01.000 --> 00:00:02.000", "00:00:00.000 --> 00:00:01.000",
"Line 1", "Line 1",
"", "",
"00:00:02.000 --> 00:00:03.000", "00:00:01.000 --> 00:00:02.000",
"Line 2", "Line 2",
"", "",
"", "",
].join('\n')) ].join('\n'))
end end
it "properly escapes characters" do
result = WebVTT.build do |vtt|
4.times do |i|
vtt.cue(Time::Span.new(seconds: i), Time::Span.new(seconds: i + 1), MockLinesWithEscapableCharacter[i])
end
end
expect(result).to eq([
"WEBVTT",
"",
"00:00:00.000 --> 00:00:01.000",
"&lt;Line 1&gt;",
"",
"00:00:01.000 --> 00:00:02.000",
"&amp;Line 2&gt;",
"",
"00:00:02.000 --> 00:00:03.000",
"&lrm;Line&rlm; 3",
"",
"00:00:03.000 --> 00:00:04.000",
"&nbsp;Line 4",
"",
"",
].join('\n'))
end
end end

View File

@ -17,7 +17,7 @@ FORM_TESTS = {
"cy" => I18next::Plurals::PluralForms::Special_Welsh, "cy" => I18next::Plurals::PluralForms::Special_Welsh,
"fr" => I18next::Plurals::PluralForms::Special_French_Portuguese, "fr" => I18next::Plurals::PluralForms::Special_French_Portuguese,
"en" => I18next::Plurals::PluralForms::Single_not_one, "en" => I18next::Plurals::PluralForms::Single_not_one,
"es" => I18next::Plurals::PluralForms::Single_not_one, "es" => I18next::Plurals::PluralForms::Special_Spanish_Italian,
"ga" => I18next::Plurals::PluralForms::Special_Irish, "ga" => I18next::Plurals::PluralForms::Special_Irish,
"gd" => I18next::Plurals::PluralForms::Special_Scottish_Gaelic, "gd" => I18next::Plurals::PluralForms::Special_Scottish_Gaelic,
"he" => I18next::Plurals::PluralForms::Special_Hebrew, "he" => I18next::Plurals::PluralForms::Special_Hebrew,
@ -33,7 +33,8 @@ FORM_TESTS = {
"mt" => I18next::Plurals::PluralForms::Special_Maltese, "mt" => I18next::Plurals::PluralForms::Special_Maltese,
"or" => I18next::Plurals::PluralForms::Special_Odia, "or" => I18next::Plurals::PluralForms::Special_Odia,
"pl" => I18next::Plurals::PluralForms::Special_Polish_Kashubian, "pl" => I18next::Plurals::PluralForms::Special_Polish_Kashubian,
"pt" => I18next::Plurals::PluralForms::Single_gt_one, "pt" => I18next::Plurals::PluralForms::Special_French_Portuguese,
"pt-PT" => I18next::Plurals::PluralForms::Single_gt_one,
"pt-BR" => I18next::Plurals::PluralForms::Special_French_Portuguese, "pt-BR" => I18next::Plurals::PluralForms::Special_French_Portuguese,
"ro" => I18next::Plurals::PluralForms::Special_Romanian, "ro" => I18next::Plurals::PluralForms::Special_Romanian,
"sk" => I18next::Plurals::PluralForms::Special_Czech_Slovak, "sk" => I18next::Plurals::PluralForms::Special_Czech_Slovak,
@ -77,10 +78,10 @@ SUFFIX_TESTS = {
{num: 10, suffix: "_plural"}, {num: 10, suffix: "_plural"},
], ],
"es" => [ "es" => [
{num: 0, suffix: "_plural"}, {num: 0, suffix: "_2"},
{num: 1, suffix: ""}, {num: 1, suffix: "_0"},
{num: 10, suffix: "_plural"}, {num: 10, suffix: "_2"},
{num: 6_000_000, suffix: "_plural"}, {num: 6_000_000, suffix: "_1"},
], ],
"fr" => [ "fr" => [
{num: 0, suffix: "_0"}, {num: 0, suffix: "_0"},

View File

@ -3,18 +3,6 @@ require "../spec_helper"
CONFIG = Config.from_yaml(File.open("config/config.example.yml")) CONFIG = Config.from_yaml(File.open("config/config.example.yml"))
Spectator.describe "Helper" do Spectator.describe "Helper" do
describe "#produce_channel_videos_url" do
it "correctly produces url for requesting page `x` of a channel's videos" do
# expect(produce_channel_videos_url(ucid: "UCXuqSBlHAE6Xw-yeJA0Tunw")).to eq("/browse_ajax?continuation=4qmFsgI8EhhVQ1h1cVNCbEhBRTZYdy15ZUpBMFR1bncaIEVnWjJhV1JsYjNNd0FqZ0JZQUZxQUxnQkFDQUFlZ0V4&gl=US&hl=en")
#
# expect(produce_channel_videos_url(ucid: "UCXuqSBlHAE6Xw-yeJA0Tunw", sort_by: "popular")).to eq("/browse_ajax?continuation=4qmFsgJAEhhVQ1h1cVNCbEhBRTZYdy15ZUpBMFR1bncaJEVnWjJhV1JsYjNNd0FqZ0JZQUZxQUxnQkFDQUFlZ0V4R0FFPQ%3D%3D&gl=US&hl=en")
# expect(produce_channel_videos_url(ucid: "UCXuqSBlHAE6Xw-yeJA0Tunw", page: 20)).to eq("/browse_ajax?continuation=4qmFsgJAEhhVQ1h1cVNCbEhBRTZYdy15ZUpBMFR1bncaJEVnWjJhV1JsYjNNd0FqZ0JZQUZxQUxnQkFDQUFlZ0l5TUE9PQ%3D%3D&gl=US&hl=en")
# expect(produce_channel_videos_url(ucid: "UC-9-kyTW8ZkZNDHQJ6FgpwQ", page: 20, sort_by: "popular")).to eq("/browse_ajax?continuation=4qmFsgJAEhhVQy05LWt5VFc4WmtaTkRIUUo2Rmdwd1EaJEVnWjJhV1JsYjNNd0FqZ0JZQUZxQUxnQkFDQUFlZ0l5TUJnQg%3D%3D&gl=US&hl=en")
end
end
describe "#produce_channel_search_continuation" do describe "#produce_channel_search_continuation" do
it "correctly produces token for searching a specific channel" do it "correctly produces token for searching a specific channel" do
expect(produce_channel_search_continuation("UCXuqSBlHAE6Xw-yeJA0Tunw", "", 100)).to eq("4qmFsgJqEhhVQ1h1cVNCbEhBRTZYdy15ZUpBMFR1bncaIEVnWnpaV0Z5WTJnd0FUZ0JZQUY2QkVkS2IxaTRBUUE9WgCaAilicm93c2UtZmVlZFVDWHVxU0JsSEFFNlh3LXllSkEwVHVud3NlYXJjaA%3D%3D") expect(produce_channel_search_continuation("UCXuqSBlHAE6Xw-yeJA0Tunw", "", 100)).to eq("4qmFsgJqEhhVQ1h1cVNCbEhBRTZYdy15ZUpBMFR1bncaIEVnWnpaV0Z5WTJnd0FUZ0JZQUY2QkVkS2IxaTRBUUE9WgCaAilicm93c2UtZmVlZFVDWHVxU0JsSEFFNlh3LXllSkEwVHVud3NlYXJjaA%3D%3D")

View File

@ -12,45 +12,45 @@ end
# page of Youtube with any browser devtools HTML inspector. # page of Youtube with any browser devtools HTML inspector.
DATE_FILTERS = { DATE_FILTERS = {
Invidious::Search::Filters::Date::Hour => "EgIIAQ%3D%3D", Invidious::Search::Filters::Date::Hour => "EgIIAfABAQ%3D%3D",
Invidious::Search::Filters::Date::Today => "EgIIAg%3D%3D", Invidious::Search::Filters::Date::Today => "EgIIAvABAQ%3D%3D",
Invidious::Search::Filters::Date::Week => "EgIIAw%3D%3D", Invidious::Search::Filters::Date::Week => "EgIIA_ABAQ%3D%3D",
Invidious::Search::Filters::Date::Month => "EgIIBA%3D%3D", Invidious::Search::Filters::Date::Month => "EgIIBPABAQ%3D%3D",
Invidious::Search::Filters::Date::Year => "EgIIBQ%3D%3D", Invidious::Search::Filters::Date::Year => "EgIIBfABAQ%3D%3D",
} }
TYPE_FILTERS = { TYPE_FILTERS = {
Invidious::Search::Filters::Type::Video => "EgIQAQ%3D%3D", Invidious::Search::Filters::Type::Video => "EgIQAfABAQ%3D%3D",
Invidious::Search::Filters::Type::Channel => "EgIQAg%3D%3D", Invidious::Search::Filters::Type::Channel => "EgIQAvABAQ%3D%3D",
Invidious::Search::Filters::Type::Playlist => "EgIQAw%3D%3D", Invidious::Search::Filters::Type::Playlist => "EgIQA_ABAQ%3D%3D",
Invidious::Search::Filters::Type::Movie => "EgIQBA%3D%3D", Invidious::Search::Filters::Type::Movie => "EgIQBPABAQ%3D%3D",
} }
DURATION_FILTERS = { DURATION_FILTERS = {
Invidious::Search::Filters::Duration::Short => "EgIYAQ%3D%3D", Invidious::Search::Filters::Duration::Short => "EgIYAfABAQ%3D%3D",
Invidious::Search::Filters::Duration::Medium => "EgIYAw%3D%3D", Invidious::Search::Filters::Duration::Medium => "EgIYA_ABAQ%3D%3D",
Invidious::Search::Filters::Duration::Long => "EgIYAg%3D%3D", Invidious::Search::Filters::Duration::Long => "EgIYAvABAQ%3D%3D",
} }
FEATURE_FILTERS = { FEATURE_FILTERS = {
Invidious::Search::Filters::Features::Live => "EgJAAQ%3D%3D", Invidious::Search::Filters::Features::Live => "EgJAAfABAQ%3D%3D",
Invidious::Search::Filters::Features::FourK => "EgJwAQ%3D%3D", Invidious::Search::Filters::Features::FourK => "EgJwAfABAQ%3D%3D",
Invidious::Search::Filters::Features::HD => "EgIgAQ%3D%3D", Invidious::Search::Filters::Features::HD => "EgIgAfABAQ%3D%3D",
Invidious::Search::Filters::Features::Subtitles => "EgIoAQ%3D%3D", Invidious::Search::Filters::Features::Subtitles => "EgIoAfABAQ%3D%3D",
Invidious::Search::Filters::Features::CCommons => "EgIwAQ%3D%3D", Invidious::Search::Filters::Features::CCommons => "EgIwAfABAQ%3D%3D",
Invidious::Search::Filters::Features::ThreeSixty => "EgJ4AQ%3D%3D", Invidious::Search::Filters::Features::ThreeSixty => "EgJ4AfABAQ%3D%3D",
Invidious::Search::Filters::Features::VR180 => "EgPQAQE%3D", Invidious::Search::Filters::Features::VR180 => "EgPQAQHwAQE%3D",
Invidious::Search::Filters::Features::ThreeD => "EgI4AQ%3D%3D", Invidious::Search::Filters::Features::ThreeD => "EgI4AfABAQ%3D%3D",
Invidious::Search::Filters::Features::HDR => "EgPIAQE%3D", Invidious::Search::Filters::Features::HDR => "EgPIAQHwAQE%3D",
Invidious::Search::Filters::Features::Location => "EgO4AQE%3D", Invidious::Search::Filters::Features::Location => "EgO4AQHwAQE%3D",
Invidious::Search::Filters::Features::Purchased => "EgJIAQ%3D%3D", Invidious::Search::Filters::Features::Purchased => "EgJIAfABAQ%3D%3D",
} }
SORT_FILTERS = { SORT_FILTERS = {
Invidious::Search::Filters::Sort::Relevance => "", Invidious::Search::Filters::Sort::Relevance => "8AEB",
Invidious::Search::Filters::Sort::Date => "CAI%3D", Invidious::Search::Filters::Sort::Date => "CALwAQE%3D",
Invidious::Search::Filters::Sort::Views => "CAM%3D", Invidious::Search::Filters::Sort::Views => "CAPwAQE%3D",
Invidious::Search::Filters::Sort::Rating => "CAE%3D", Invidious::Search::Filters::Sort::Rating => "CAHwAQE%3D",
} }
Spectator.describe Invidious::Search::Filters do Spectator.describe Invidious::Search::Filters do

View File

@ -18,8 +18,8 @@ record AboutChannel,
def get_about_info(ucid, locale) : AboutChannel def get_about_info(ucid, locale) : AboutChannel
begin begin
# "EgVhYm91dA==" is the base64-encoded protobuf object {"2:string":"about"} # Fetch channel information from channel home page
initdata = YoutubeAPI.browse(browse_id: ucid, params: "EgVhYm91dA==") initdata = YoutubeAPI.browse(browse_id: ucid, params: "")
rescue rescue
raise InfoException.new("Could not get channel info.") raise InfoException.new("Could not get channel info.")
end end

View File

@ -93,7 +93,7 @@ struct ChannelVideo
def to_tuple def to_tuple
{% begin %} {% begin %}
{ {
{{*@type.instance_vars.map(&.name)}} {{@type.instance_vars.map(&.name).splat}}
} }
{% end %} {% end %}
end end

View File

@ -73,12 +73,6 @@ def make_initial_content_ctoken(ucid, content_type, sort_by) : String
return produce_channel_content_continuation(ucid, content_type, sort_by: sort_by) return produce_channel_content_continuation(ucid, content_type, sort_by: sort_by)
end end
# Used in bypass_captcha_job.cr
def produce_channel_videos_url(ucid, page = 1, auto_generated = nil, sort_by = "newest", v2 = false)
continuation = produce_channel_content_continuation(ucid, "videos", page, auto_generated, sort_by, v2)
return "/browse_ajax?continuation=#{continuation}&gl=US&hl=en"
end
module Invidious::Channel::Tabs module Invidious::Channel::Tabs
extend self extend self

View File

@ -48,7 +48,7 @@ struct ConfigPreferences
def to_tuple def to_tuple
{% begin %} {% begin %}
{ {
{{*@type.instance_vars.map { |var| "#{var.name}: #{var.name}".id }}} {{(@type.instance_vars.map { |var| "#{var.name}: #{var.name}".id }).splat}}
} }
{% end %} {% end %}
end end
@ -133,10 +133,6 @@ class Config
# Saved cookies in "name1=value1; name2=value2..." format # Saved cookies in "name1=value1; name2=value2..." format
@[YAML::Field(converter: Preferences::StringToCookies)] @[YAML::Field(converter: Preferences::StringToCookies)]
property cookies : HTTP::Cookies = HTTP::Cookies.new property cookies : HTTP::Cookies = HTTP::Cookies.new
# Key for Anti-Captcha
property captcha_key : String? = nil
# API URL for Anti-Captcha
property captcha_api_url : String = "https://api.anti-captcha.com"
# Playlist length limit # Playlist length limit
property playlist_length_limit : Int32 = 500 property playlist_length_limit : Int32 = 500

View File

@ -15,7 +15,7 @@ module Invidious::Database::Statistics
PG_DB.query_one(request, as: Int64) PG_DB.query_one(request, as: Int64)
end end
def count_users_active_1m : Int64 def count_users_active_6m : Int64
request = <<-SQL request = <<-SQL
SELECT count(*) FROM users SELECT count(*) FROM users
WHERE CURRENT_TIMESTAMP - updated < '6 months' WHERE CURRENT_TIMESTAMP - updated < '6 months'
@ -24,7 +24,7 @@ module Invidious::Database::Statistics
PG_DB.query_one(request, as: Int64) PG_DB.query_one(request, as: Int64)
end end
def count_users_active_6m : Int64 def count_users_active_1m : Int64
request = <<-SQL request = <<-SQL
SELECT count(*) FROM users SELECT count(*) FROM users
WHERE CURRENT_TIMESTAMP - updated < '1 month' WHERE CURRENT_TIMESTAMP - updated < '1 month'

View File

@ -33,7 +33,7 @@ module Invidious::Frontend::Comments
<a href="javascript:void(0)" data-onclick="toggle_parent">[ ]</a> <a href="javascript:void(0)" data-onclick="toggle_parent">[ ]</a>
<b><a href="https://www.reddit.com/user/#{child.author}">#{child.author}</a></b> <b><a href="https://www.reddit.com/user/#{child.author}">#{child.author}</a></b>
#{translate_count(locale, "comments_points_count", child.score, NumberFormatting::Separator)} #{translate_count(locale, "comments_points_count", child.score, NumberFormatting::Separator)}
<span title="#{child.created_utc.to_s(translate(locale, "%a %B %-d %T %Y UTC"))}">#{translate(locale, "`x` ago", recode_date(child.created_utc, locale))}</span> <span title="#{child.created_utc.to_s("%a %B %-d %T %Y UTC")}">#{translate(locale, "`x` ago", recode_date(child.created_utc, locale))}</span>
<a href="https://www.reddit.com#{child.permalink}" title="#{translate(locale, "permalink")}">#{translate(locale, "permalink")}</a> <a href="https://www.reddit.com#{child.permalink}" title="#{translate(locale, "permalink")}">#{translate(locale, "permalink")}</a>
</p> </p>
<div> <div>

View File

@ -107,6 +107,36 @@ module Invidious::Frontend::Comments
</div> </div>
END_HTML END_HTML
end end
when "multiImage"
html << <<-END_HTML
<section class="carousel">
<a class="skip-link" href="#skip-#{child["commentId"]}">#{translate(locale, "carousel_skip")}</a>
<div class="slides">
END_HTML
image_array = attachment["images"].as_a
image_array.each_index do |i|
html << <<-END_HTML
<div class="slides-item slide-#{i + 1}" id="#{child["commentId"]}-slide-#{i + 1}" aria-label="#{translate(locale, "carousel_slide", {"current" => (i + 1).to_s, "total" => image_array.size.to_s})}" tabindex="0">
<img loading="lazy" src="/ggpht#{URI.parse(image_array[i][1]["url"].as_s).request_target}" alt="" />
</div>
END_HTML
end
html << <<-END_HTML
</div>
<div class="carousel__nav">
END_HTML
attachment["images"].as_a.each_index do |i|
html << <<-END_HTML
<a class="slider-nav" href="##{child["commentId"]}-slide-#{i + 1}" aria-label="#{translate(locale, "carousel_go_to", (i + 1).to_s)}" tabindex="-1" aria-hidden="true">#{i + 1}</a>
END_HTML
end
html << <<-END_HTML
</div>
<div id="skip-#{child["commentId"]}"></div>
</section>
END_HTML
else nil # Ignore else nil # Ignore
end end
end end

View File

@ -3,7 +3,7 @@
# ------------------- # -------------------
macro error_template(*args) macro error_template(*args)
error_template_helper(env, {{*args}}) error_template_helper(env, {{args.splat}})
end end
def github_details(summary : String, content : String) def github_details(summary : String, content : String)
@ -95,7 +95,7 @@ end
# ------------------- # -------------------
macro error_atom(*args) macro error_atom(*args)
error_atom_helper(env, {{*args}}) error_atom_helper(env, {{args.splat}})
end end
def error_atom_helper(env : HTTP::Server::Context, status_code : Int32, exception : Exception) def error_atom_helper(env : HTTP::Server::Context, status_code : Int32, exception : Exception)
@ -121,7 +121,7 @@ end
# ------------------- # -------------------
macro error_json(*args) macro error_json(*args)
error_json_helper(env, {{*args}}) error_json_helper(env, {{args.splat}})
end end
def error_json_helper( def error_json_helper(

View File

@ -142,63 +142,8 @@ class APIHandler < Kemal::Handler
exclude ["/api/v1/auth/notifications"], "POST" exclude ["/api/v1/auth/notifications"], "POST"
def call(env) def call(env)
return call_next env unless only_match? env env.response.headers["Access-Control-Allow-Origin"] = "*" if only_match?(env)
env.response.headers["Access-Control-Allow-Origin"] = "*"
# Since /api/v1/notifications is an event-stream, we don't want
# to wrap the response
return call_next env if exclude_match? env
# Here we swap out the socket IO so we can modify the response as needed
output = env.response.output
env.response.output = IO::Memory.new
begin
call_next env call_next env
env.response.output.rewind
if env.response.output.as(IO::Memory).size != 0 &&
env.response.headers.includes_word?("Content-Type", "application/json")
response = JSON.parse(env.response.output)
if fields_text = env.params.query["fields"]?
begin
JSONFilter.filter(response, fields_text)
rescue ex
env.response.status_code = 400
response = {"error" => ex.message}
end
end
if env.params.query["pretty"]?.try &.== "1"
response = response.to_pretty_json
else
response = response.to_json
end
else
response = env.response.output.gets_to_end
end
rescue ex
env.response.content_type = "application/json" if env.response.headers.includes_word?("Content-Type", "text/html")
env.response.status_code = 500
if env.response.headers.includes_word?("Content-Type", "application/json")
response = {"error" => ex.message || "Unspecified error"}
if env.params.query["pretty"]?.try &.== "1"
response = response.to_pretty_json
else
response = response.to_json
end
end
ensure
env.response.output = output
env.response.print response
env.response.flush
end
end end
end end

View File

@ -78,15 +78,6 @@ def create_notification_stream(env, topics, connection_channel)
video.published = published video.published = published
response = JSON.parse(video.to_json(locale, nil)) response = JSON.parse(video.to_json(locale, nil))
if fields_text = env.params.query["fields"]?
begin
JSONFilter.filter(response, fields_text)
rescue ex
env.response.status_code = 400
response = {"error" => ex.message}
end
end
env.response.puts "id: #{id}" env.response.puts "id: #{id}"
env.response.puts "data: #{response.to_json}" env.response.puts "data: #{response.to_json}"
env.response.puts env.response.puts
@ -113,15 +104,6 @@ def create_notification_stream(env, topics, connection_channel)
Invidious::Database::ChannelVideos.select_notfications(topic, since_unix).each do |video| Invidious::Database::ChannelVideos.select_notfications(topic, since_unix).each do |video|
response = JSON.parse(video.to_json(locale)) response = JSON.parse(video.to_json(locale))
if fields_text = env.params.query["fields"]?
begin
JSONFilter.filter(response, fields_text)
rescue ex
env.response.status_code = 400
response = {"error" => ex.message}
end
end
env.response.puts "id: #{id}" env.response.puts "id: #{id}"
env.response.puts "data: #{response.to_json}" env.response.puts "data: #{response.to_json}"
env.response.puts env.response.puts
@ -155,15 +137,6 @@ def create_notification_stream(env, topics, connection_channel)
video.published = Time.unix(published) video.published = Time.unix(published)
response = JSON.parse(video.to_json(locale, nil)) response = JSON.parse(video.to_json(locale, nil))
if fields_text = env.params.query["fields"]?
begin
JSONFilter.filter(response, fields_text)
rescue ex
env.response.status_code = 400
response = {"error" => ex.message}
end
end
env.response.puts "id: #{id}" env.response.puts "id: #{id}"
env.response.puts "data: #{response.to_json}" env.response.puts "data: #{response.to_json}"
env.response.puts env.response.puts
@ -208,3 +181,20 @@ def proxy_file(response, env)
IO.copy response.body_io, env.response IO.copy response.body_io, env.response
end end
end end
# Fetch the playback requests tracker from the statistics endpoint.
#
# Creates a new tracker when unavailable.
def get_playback_statistic
if (tracker = Invidious::Jobs::StatisticsRefreshJob::STATISTICS["playback"]) && tracker.as(Hash).empty?
tracker = {
"totalRequests" => 0_i64,
"successfulRequests" => 0_i64,
"ratio" => 0_f64,
}
Invidious::Jobs::StatisticsRefreshJob::STATISTICS["playback"] = tracker
end
return tracker.as(Hash(String, Int64 | Float64))
end

View File

@ -78,7 +78,7 @@ def load_all_locales
return locales return locales
end end
def translate(locale : String?, key : String, text : String | Nil = nil) : String def translate(locale : String?, key : String, text : String | Hash(String, String) | Nil = nil) : String
# Log a warning if "key" doesn't exist in en-US locale and return # Log a warning if "key" doesn't exist in en-US locale and return
# that key as the text, so this is more or less transparent to the user. # that key as the text, so this is more or less transparent to the user.
if !LOCALES["en-US"].has_key?(key) if !LOCALES["en-US"].has_key?(key)
@ -101,6 +101,7 @@ def translate(locale : String?, key : String, text : String | Nil = nil) : Strin
match_length = 0 match_length = 0
raw_data.as_h.each do |hash_key, value| raw_data.as_h.each do |hash_key, value|
if text.is_a?(String)
if md = text.try &.match(/#{hash_key}/) if md = text.try &.match(/#{hash_key}/)
if md[0].size >= match_length if md[0].size >= match_length
translation = value.as_s translation = value.as_s
@ -108,14 +109,20 @@ def translate(locale : String?, key : String, text : String | Nil = nil) : Strin
end end
end end
end end
end
when .as_s? when .as_s?
translation = raw_data.as_s translation = raw_data.as_s
else else
raise "Invalid translation \"#{raw_data}\"" raise "Invalid translation \"#{raw_data}\""
end end
if text if text.is_a?(String)
translation = translation.gsub("`x`", text) translation = translation.gsub("`x`", text)
elsif text.is_a?(Hash(String, String))
# adds support for multi string interpolation. Based on i18next https://www.i18next.com/translation-function/interpolation#basic
text.each_key do |hash_key|
translation = translation.gsub("{{#{hash_key}}}", text[hash_key])
end
end end
return translation return translation

View File

@ -47,19 +47,19 @@ module I18next::Plurals
private PLURAL_SETS = { private PLURAL_SETS = {
PluralForms::Single_gt_one => [ PluralForms::Single_gt_one => [
"ach", "ak", "am", "arn", "br", "fil", "gun", "ln", "mfe", "mg", "ach", "ak", "am", "arn", "br", "fa", "fil", "gun", "ln", "mfe", "mg",
"mi", "oc", "pt", "tg", "tl", "ti", "tr", "uz", "wa", "mi", "oc", "pt-PT", "tg", "tl", "ti", "tr", "uz", "wa",
], ],
PluralForms::Single_not_one => [ PluralForms::Single_not_one => [
"af", "an", "ast", "az", "bg", "bn", "ca", "da", "de", "dev", "el", "en", "af", "an", "ast", "az", "bg", "bn", "ca", "da", "de", "dev", "el", "en",
"eo", "es", "et", "eu", "fi", "fo", "fur", "fy", "gl", "gu", "ha", "hi", "eo", "et", "eu", "fi", "fo", "fur", "fy", "gl", "gu", "ha", "hi",
"hu", "hy", "ia", "kk", "kn", "ku", "lb", "mai", "ml", "mn", "mr", "hu", "hy", "ia", "kk", "kn", "ku", "lb", "mai", "ml", "mn", "mr",
"nah", "nap", "nb", "ne", "nl", "nn", "no", "nso", "pa", "pap", "pms", "nah", "nap", "nb", "ne", "nl", "nn", "no", "nso", "pa", "pap", "pms",
"ps", "rm", "sco", "se", "si", "so", "son", "sq", "sv", "sw", "ps", "rm", "sco", "se", "si", "so", "son", "sq", "sv", "sw",
"ta", "te", "tk", "ur", "yo", "ta", "te", "tk", "ur", "yo",
], ],
PluralForms::None => [ PluralForms::None => [
"ay", "bo", "cgg", "fa", "ht", "id", "ja", "jbo", "ka", "km", "ko", "ky", "ay", "bo", "cgg", "ht", "id", "ja", "jbo", "ka", "km", "ko", "ky",
"lo", "ms", "sah", "su", "th", "tt", "ug", "vi", "wo", "zh", "lo", "ms", "sah", "su", "th", "tt", "ug", "vi", "wo", "zh",
], ],
PluralForms::Dual_Slavic => [ PluralForms::Dual_Slavic => [
@ -90,10 +90,12 @@ module I18next::Plurals
"sk" => PluralForms::Special_Czech_Slovak, "sk" => PluralForms::Special_Czech_Slovak,
"sl" => PluralForms::Special_Slovenian, "sl" => PluralForms::Special_Slovenian,
# Mixed v3/v4 rules # Mixed v3/v4 rules
"es" => PluralForms::Special_Spanish_Italian,
"fr" => PluralForms::Special_French_Portuguese, "fr" => PluralForms::Special_French_Portuguese,
"hr" => PluralForms::Special_Hungarian_Serbian, "hr" => PluralForms::Special_Hungarian_Serbian,
"it" => PluralForms::Special_Spanish_Italian, "it" => PluralForms::Special_Spanish_Italian,
"pt-BR" => PluralForms::Special_French_Portuguese, "pt" => PluralForms::Special_French_Portuguese,
"pt" => PluralForms::Special_French_Portuguese,
"sr" => PluralForms::Special_Hungarian_Serbian, "sr" => PluralForms::Special_Hungarian_Serbian,
} }
@ -165,7 +167,7 @@ module I18next::Plurals
def get_plural_form(locale : String) : PluralForms def get_plural_form(locale : String) : PluralForms
# Extract the ISO 639-1 or 639-2 code from an RFC 5646 language code # Extract the ISO 639-1 or 639-2 code from an RFC 5646 language code
if !locale.matches?(/^pt-BR$/) if !locale.matches?(/^pt-PT$/)
locale = locale.split('-')[0] locale = locale.split('-')[0]
end end

View File

@ -1,248 +0,0 @@
module JSONFilter
alias BracketIndex = Hash(Int64, Int64)
alias GroupedFieldsValue = String | Array(GroupedFieldsValue)
alias GroupedFieldsList = Array(GroupedFieldsValue)
class FieldsParser
class ParseError < Exception
end
# Returns the `Regex` pattern used to match nest groups
def self.nest_group_pattern : Regex
# uses a '.' character to match json keys as they are allowed
# to contain any unicode codepoint
/(?:|,)(?<groupname>[^,\n]*?)\(/
end
# Returns the `Regex` pattern used to check if there are any empty nest groups
def self.unnamed_nest_group_pattern : Regex
/^\(|\(\(|\/\(/
end
def self.parse_fields(fields_text : String, &) : Nil
if fields_text.empty?
raise FieldsParser::ParseError.new "Fields is empty"
end
opening_bracket_count = fields_text.count('(')
closing_bracket_count = fields_text.count(')')
if opening_bracket_count != closing_bracket_count
bracket_type = opening_bracket_count > closing_bracket_count ? "opening" : "closing"
raise FieldsParser::ParseError.new "There are too many #{bracket_type} brackets (#{opening_bracket_count}:#{closing_bracket_count})"
elsif match_result = unnamed_nest_group_pattern.match(fields_text)
raise FieldsParser::ParseError.new "Unnamed nest group at position #{match_result.begin}"
end
# first, handle top-level single nested properties: items/id, playlistItems/snippet, etc
parse_single_nests(fields_text) { |nest_list| yield nest_list }
# next, handle nest groups: items(id, etag, etc)
parse_nest_groups(fields_text) { |nest_list| yield nest_list }
end
def self.parse_single_nests(fields_text : String, &) : Nil
single_nests = remove_nest_groups(fields_text)
if !single_nests.empty?
property_nests = single_nests.split(',')
property_nests.each do |nest|
nest_list = nest.split('/')
if nest_list.includes? ""
raise FieldsParser::ParseError.new "Empty key in nest list: #{nest_list}"
end
yield nest_list
end
# else
# raise FieldsParser::ParseError.new "Empty key in nest list 22: #{fields_text} | #{single_nests}"
end
end
def self.parse_nest_groups(fields_text : String, &) : Nil
nest_stack = [] of NamedTuple(group_name: String, closing_bracket_index: Int64)
bracket_pairs = get_bracket_pairs(fields_text, true)
text_index = 0
regex_index = 0
while regex_result = self.nest_group_pattern.match(fields_text, regex_index)
raw_match = regex_result[0]
group_name = regex_result["groupname"]
text_index = regex_result.begin
regex_index = regex_result.end
if text_index.nil? || regex_index.nil?
raise FieldsParser::ParseError.new "Received invalid index while parsing nest groups: text_index: #{text_index} | regex_index: #{regex_index}"
end
offset = raw_match.starts_with?(',') ? 1 : 0
opening_bracket_index = (text_index + group_name.size) + offset
closing_bracket_index = bracket_pairs[opening_bracket_index]
content_start = opening_bracket_index + 1
content = fields_text[content_start...closing_bracket_index]
if content.empty?
raise FieldsParser::ParseError.new "Empty nest group at position #{content_start}"
else
content = remove_nest_groups(content)
end
while nest_stack.size > 0 && closing_bracket_index > nest_stack[nest_stack.size - 1][:closing_bracket_index]
if nest_stack.size
nest_stack.pop
end
end
group_name.split('/').each do |name|
nest_stack.push({
group_name: name,
closing_bracket_index: closing_bracket_index,
})
end
if !content.empty?
properties = content.split(',')
properties.each do |prop|
nest_list = nest_stack.map { |nest_prop| nest_prop[:group_name] }
if !prop.empty?
if prop.includes?('/')
parse_single_nests(prop) { |list| nest_list += list }
else
nest_list.push prop
end
else
raise FieldsParser::ParseError.new "Empty key in nest list: #{nest_list << prop}"
end
yield nest_list
end
end
end
end
def self.remove_nest_groups(text : String) : String
content_bracket_pairs = get_bracket_pairs(text, false)
content_bracket_pairs.each_key.to_a.reverse.each do |opening_bracket|
closing_bracket = content_bracket_pairs[opening_bracket]
last_comma = text.rindex(',', opening_bracket) || 0
text = text[0...last_comma] + text[closing_bracket + 1...text.size]
end
return text.starts_with?(',') ? text[1...text.size] : text
end
def self.get_bracket_pairs(text : String, recursive = true) : BracketIndex
istart = [] of Int64
bracket_index = BracketIndex.new
text.each_char_with_index do |char, index|
if char == '('
istart.push(index.to_i64)
end
if char == ')'
begin
opening = istart.pop
if recursive || (!recursive && istart.size == 0)
bracket_index[opening] = index.to_i64
end
rescue
raise FieldsParser::ParseError.new "No matching opening parenthesis at: #{index}"
end
end
end
if istart.size != 0
idx = istart.pop
raise FieldsParser::ParseError.new "No matching closing parenthesis at: #{idx}"
end
return bracket_index
end
end
class FieldsGrouper
alias SkeletonValue = Hash(String, SkeletonValue)
def self.create_json_skeleton(fields_text : String) : SkeletonValue
root_hash = {} of String => SkeletonValue
FieldsParser.parse_fields(fields_text) do |nest_list|
current_item = root_hash
nest_list.each do |key|
if current_item[key]?
current_item = current_item[key]
else
current_item[key] = {} of String => SkeletonValue
current_item = current_item[key]
end
end
end
root_hash
end
def self.create_grouped_fields_list(json_skeleton : SkeletonValue) : GroupedFieldsList
grouped_fields_list = GroupedFieldsList.new
json_skeleton.each do |key, value|
grouped_fields_list.push key
nested_keys = create_grouped_fields_list(value)
grouped_fields_list.push nested_keys unless nested_keys.empty?
end
return grouped_fields_list
end
end
class FilterError < Exception
end
def self.filter(item : JSON::Any, fields_text : String, in_place : Bool = true)
skeleton = FieldsGrouper.create_json_skeleton(fields_text)
grouped_fields_list = FieldsGrouper.create_grouped_fields_list(skeleton)
filter(item, grouped_fields_list, in_place)
end
def self.filter(item : JSON::Any, grouped_fields_list : GroupedFieldsList, in_place : Bool = true) : JSON::Any
item = item.clone unless in_place
if !item.as_h? && !item.as_a?
raise FilterError.new "Can't filter '#{item}' by #{grouped_fields_list}"
end
top_level_keys = Array(String).new
grouped_fields_list.each do |value|
if value.is_a? String
top_level_keys.push value
elsif value.is_a? Array
if !top_level_keys.empty?
key_to_filter = top_level_keys.last
if item.as_h?
filter(item[key_to_filter], value, in_place: true)
elsif item.as_a?
item.as_a.each { |arr_item| filter(arr_item[key_to_filter], value, in_place: true) }
end
else
raise FilterError.new "Tried to filter while top level keys list is empty"
end
end
end
if item.as_h?
item.as_h.select! top_level_keys
elsif item.as_a?
item.as_a.map { |value| filter(value, top_level_keys, in_place: true) }
end
item
end
end

View File

@ -262,7 +262,7 @@ def get_referer(env, fallback = "/", unroll = true)
end end
referer = referer.request_target 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 if referer == env.request.path
referer = fallback referer = fallback

View File

@ -4,13 +4,23 @@
module WebVTT module WebVTT
# A WebVTT builder generates WebVTT files # A WebVTT builder generates WebVTT files
private class Builder private class Builder
# See https://developer.mozilla.org/en-US/docs/Web/API/WebVTT_API#cue_payload
private ESCAPE_SUBSTITUTIONS = {
'&' => "&amp;",
'<' => "&lt;",
'>' => "&gt;",
'\u200E' => "&lrm;",
'\u200F' => "&rlm;",
'\u00A0' => "&nbsp;",
}
def initialize(@io : IO) def initialize(@io : IO)
end end
# Writes an vtt cue with the specified time stamp and contents # Writes an vtt cue with the specified time stamp and contents
def cue(start_time : Time::Span, end_time : Time::Span, text : String) def cue(start_time : Time::Span, end_time : Time::Span, text : String)
timestamp(start_time, end_time) timestamp(start_time, end_time)
@io << text @io << self.escape(text)
@io << "\n\n" @io << "\n\n"
end end
@ -29,6 +39,10 @@ module WebVTT
@io << '.' << timestamp.milliseconds.to_s.rjust(3, '0') @io << '.' << timestamp.milliseconds.to_s.rjust(3, '0')
end end
private def escape(text : String) : String
return text.gsub(ESCAPE_SUBSTITUTIONS)
end
def document(setting_fields : Hash(String, String)? = nil, &) def document(setting_fields : Hash(String, String)? = nil, &)
@io << "WEBVTT\n" @io << "WEBVTT\n"

View File

@ -1,135 +0,0 @@
class Invidious::Jobs::BypassCaptchaJob < Invidious::Jobs::BaseJob
def begin
loop do
begin
random_video = PG_DB.query_one?("select id, ucid from (select id, ucid from channel_videos limit 1000) as s ORDER BY RANDOM() LIMIT 1", as: {id: String, ucid: String})
if !random_video
random_video = {id: "zj82_v2R6ts", ucid: "UCK87Lox575O_HCHBWaBSyGA"}
end
{"/watch?v=#{random_video["id"]}&gl=US&hl=en&has_verified=1&bpctr=9999999999", produce_channel_videos_url(ucid: random_video["ucid"])}.each do |path|
response = YT_POOL.client &.get(path)
if response.body.includes?("To continue with your YouTube experience, please fill out the form below.")
html = XML.parse_html(response.body)
form = html.xpath_node(%(//form[@action="/das_captcha"])).not_nil!
site_key = form.xpath_node(%(.//div[@id="recaptcha"])).try &.["data-sitekey"]
s_value = form.xpath_node(%(.//div[@id="recaptcha"])).try &.["data-s"]
inputs = {} of String => String
form.xpath_nodes(%(.//input[@name])).map do |node|
inputs[node["name"]] = node["value"]
end
headers = response.cookies.add_request_headers(HTTP::Headers.new)
response = JSON.parse(HTTP::Client.post(CONFIG.captcha_api_url + "/createTask",
headers: HTTP::Headers{"Content-Type" => "application/json"}, body: {
"clientKey" => CONFIG.captcha_key,
"task" => {
"type" => "NoCaptchaTaskProxyless",
"websiteURL" => "https://www.youtube.com#{path}",
"websiteKey" => site_key,
"recaptchaDataSValue" => s_value,
},
}.to_json).body)
raise response["error"].as_s if response["error"]?
task_id = response["taskId"].as_i
loop do
sleep 10.seconds
response = JSON.parse(HTTP::Client.post(CONFIG.captcha_api_url + "/getTaskResult",
headers: HTTP::Headers{"Content-Type" => "application/json"}, body: {
"clientKey" => CONFIG.captcha_key,
"taskId" => task_id,
}.to_json).body)
if response["status"]?.try &.== "ready"
break
elsif response["errorId"]?.try &.as_i != 0
raise response["errorDescription"].as_s
end
end
inputs["g-recaptcha-response"] = response["solution"]["gRecaptchaResponse"].as_s
headers["Cookies"] = response["solution"]["cookies"].as_h?.try &.map { |k, v| "#{k}=#{v}" }.join("; ") || ""
response = YT_POOL.client &.post("/das_captcha", headers, form: inputs)
response.cookies
.select { |cookie| cookie.name != "PREF" }
.each { |cookie| CONFIG.cookies << cookie }
# Persist cookies between runs
File.write("config/config.yml", CONFIG.to_yaml)
elsif response.headers["Location"]?.try &.includes?("/sorry/index")
location = response.headers["Location"].try { |u| URI.parse(u) }
headers = HTTP::Headers{":authority" => location.host.not_nil!}
response = YT_POOL.client &.get(location.request_target, headers)
html = XML.parse_html(response.body)
form = html.xpath_node(%(//form[@action="index"])).not_nil!
site_key = form.xpath_node(%(.//div[@id="recaptcha"])).try &.["data-sitekey"]
s_value = form.xpath_node(%(.//div[@id="recaptcha"])).try &.["data-s"]
inputs = {} of String => String
form.xpath_nodes(%(.//input[@name])).map do |node|
inputs[node["name"]] = node["value"]
end
captcha_client = HTTPClient.new(URI.parse(CONFIG.captcha_api_url))
captcha_client.family = CONFIG.force_resolve || Socket::Family::INET
response = JSON.parse(captcha_client.post("/createTask",
headers: HTTP::Headers{"Content-Type" => "application/json"}, body: {
"clientKey" => CONFIG.captcha_key,
"task" => {
"type" => "NoCaptchaTaskProxyless",
"websiteURL" => location.to_s,
"websiteKey" => site_key,
"recaptchaDataSValue" => s_value,
},
}.to_json).body)
captcha_client.close
raise response["error"].as_s if response["error"]?
task_id = response["taskId"].as_i
loop do
sleep 10.seconds
response = JSON.parse(captcha_client.post("/getTaskResult",
headers: HTTP::Headers{"Content-Type" => "application/json"}, body: {
"clientKey" => CONFIG.captcha_key,
"taskId" => task_id,
}.to_json).body)
if response["status"]?.try &.== "ready"
break
elsif response["errorId"]?.try &.as_i != 0
raise response["errorDescription"].as_s
end
end
inputs["g-recaptcha-response"] = response["solution"]["gRecaptchaResponse"].as_s
headers["Cookies"] = response["solution"]["cookies"].as_h?.try &.map { |k, v| "#{k}=#{v}" }.join("; ") || ""
response = YT_POOL.client &.post("/sorry/index", headers: headers, form: inputs)
headers = HTTP::Headers{
"Cookie" => URI.parse(response.headers["location"]).query_params["google_abuse"].split(";")[0],
}
cookies = HTTP::Cookies.from_client_headers(headers)
cookies.each { |cookie| CONFIG.cookies << cookie }
# Persist cookies between runs
File.write("config/config.yml", CONFIG.to_yaml)
end
end
rescue ex
LOGGER.error("BypassCaptchaJob: #{ex.message}")
ensure
sleep 1.minute
Fiber.yield
end
end
end
end

View File

@ -18,6 +18,13 @@ class Invidious::Jobs::StatisticsRefreshJob < Invidious::Jobs::BaseJob
"updatedAt" => Time.utc.to_unix, "updatedAt" => Time.utc.to_unix,
"lastChannelRefreshedAt" => 0_i64, "lastChannelRefreshedAt" => 0_i64,
}, },
#
# "totalRequests" => 0_i64,
# "successfulRequests" => 0_i64
# "ratio" => 0_i64
#
"playback" => {} of String => Int64 | Float64,
} }
private getter db : DB::Database private getter db : DB::Database
@ -30,7 +37,7 @@ class Invidious::Jobs::StatisticsRefreshJob < Invidious::Jobs::BaseJob
loop do loop do
refresh_stats refresh_stats
sleep 1.minute sleep 10.minute
Fiber.yield Fiber.yield
end end
end end
@ -49,12 +56,15 @@ class Invidious::Jobs::StatisticsRefreshJob < Invidious::Jobs::BaseJob
users = STATISTICS.dig("usage", "users").as(Hash(String, Int64)) users = STATISTICS.dig("usage", "users").as(Hash(String, Int64))
users["total"] = Invidious::Database::Statistics.count_users_total users["total"] = Invidious::Database::Statistics.count_users_total
users["activeHalfyear"] = Invidious::Database::Statistics.count_users_active_1m users["activeHalfyear"] = Invidious::Database::Statistics.count_users_active_6m
users["activeMonth"] = Invidious::Database::Statistics.count_users_active_6m users["activeMonth"] = Invidious::Database::Statistics.count_users_active_1m
STATISTICS["metadata"] = { STATISTICS["metadata"] = {
"updatedAt" => Time.utc.to_unix, "updatedAt" => Time.utc.to_unix,
"lastChannelRefreshedAt" => Invidious::Database::Statistics.channel_last_update.try &.to_unix || 0_i64, "lastChannelRefreshedAt" => Invidious::Database::Statistics.channel_last_update.try &.to_unix || 0_i64,
} }
# Reset playback requests tracker
STATISTICS["playback"] = {} of String => Int64 | Float64
end end
end end

View File

@ -6,6 +6,22 @@ module Invidious::Routes::API::V1::Misc
if !CONFIG.statistics_enabled if !CONFIG.statistics_enabled
return {"software" => SOFTWARE}.to_json return {"software" => SOFTWARE}.to_json
else else
# Calculate playback success rate
if (tracker = Invidious::Jobs::StatisticsRefreshJob::STATISTICS["playback"]?)
tracker = tracker.as(Hash(String, Int64 | Float64))
if !tracker.empty?
total_requests = tracker["totalRequests"]
success_count = tracker["successfulRequests"]
if total_requests.zero?
tracker["ratio"] = 1_i64
else
tracker["ratio"] = (success_count / (total_requests)).round(2)
end
end
end
return Invidious::Jobs::StatisticsRefreshJob::STATISTICS.to_json return Invidious::Jobs::StatisticsRefreshJob::STATISTICS.to_json
end end
end end
@ -175,6 +191,8 @@ module Invidious::Routes::API::V1::Misc
json.object do json.object do
json.field "ucid", sub_endpoint["browseId"].as_s if sub_endpoint["browseId"]? json.field "ucid", sub_endpoint["browseId"].as_s if sub_endpoint["browseId"]?
json.field "videoId", sub_endpoint["videoId"].as_s if sub_endpoint["videoId"]? json.field "videoId", sub_endpoint["videoId"].as_s if sub_endpoint["videoId"]?
json.field "playlistId", sub_endpoint["playlistId"].as_s if sub_endpoint["playlistId"]?
json.field "startTimeSeconds", sub_endpoint["startTimeSeconds"].as_i if sub_endpoint["startTimeSeconds"]?
json.field "params", params.try &.as_s json.field "params", params.try &.as_s
json.field "pageType", pageType json.field "pageType", pageType
end end

View File

@ -32,11 +32,14 @@ module Invidious::Routes::API::V1::Search
begin begin
client = HTTP::Client.new("suggestqueries-clients6.youtube.com") client = HTTP::Client.new("suggestqueries-clients6.youtube.com")
url = "/complete/search?client=youtube&hl=en&gl=#{region}&q=#{URI.encode_www_form(query)}&xssi=t&gs_ri=youtube&ds=yt" client.before_request { |r| add_yt_headers(r) }
url = "/complete/search?client=youtube&hl=en&gl=#{region}&q=#{URI.encode_www_form(query)}&gs_ri=youtube&ds=yt"
response = client.get(url).body response = client.get(url).body
client.close
body = JSON.parse(response[5..-1]).as_a body = JSON.parse(response[19..-2]).as_a
suggestions = body[1].as_a[0..-2] suggestions = body[1].as_a[0..-2]
JSON.build do |json| JSON.build do |json|

View File

@ -363,4 +363,47 @@ module Invidious::Routes::API::V1::Videos
end end
end end
end end
def self.clips(env)
locale = env.get("preferences").as(Preferences).locale
env.response.content_type = "application/json"
clip_id = env.params.url["id"]
region = env.params.query["region"]?
proxy = {"1", "true"}.any? &.== env.params.query["local"]?
response = YoutubeAPI.resolve_url("https://www.youtube.com/clip/#{clip_id}")
return error_json(400, "Invalid clip ID") if response["error"]?
video_id = response.dig?("endpoint", "watchEndpoint", "videoId").try &.as_s
return error_json(400, "Invalid clip ID") if video_id.nil?
start_time = nil
end_time = nil
clip_title = nil
if params = response.dig?("endpoint", "watchEndpoint", "params").try &.as_s
start_time, end_time, clip_title = parse_clip_parameters(params)
end
begin
video = get_video(video_id, region: region)
rescue ex : NotFoundException
return error_json(404, ex)
rescue ex
return error_json(500, ex)
end
return JSON.build do |json|
json.object do
json.field "startTime", start_time
json.field "endTime", end_time
json.field "clipTitle", clip_title
json.field "video" do
Invidious::JSONify::APIv1.video(video, json, locale: locale, proxy: proxy)
end
end
end
end
end end

View File

@ -407,14 +407,23 @@ module Invidious::Routes::Feeds
end end
spawn do spawn do
rss = XML.parse_html(body) # TODO: unify this with the other almost identical looking parts in this and channels.cr somehow?
rss.xpath_nodes("//feed/entry").each do |entry| namespaces = {
id = entry.xpath_node("videoid").not_nil!.content "yt" => "http://www.youtube.com/xml/schemas/2015",
author = entry.xpath_node("author/name").not_nil!.content "default" => "http://www.w3.org/2005/Atom",
published = Time.parse_rfc3339(entry.xpath_node("published").not_nil!.content) }
updated = Time.parse_rfc3339(entry.xpath_node("updated").not_nil!.content) rss = XML.parse(body)
rss.xpath_nodes("//default:feed/default:entry", namespaces).each do |entry|
id = entry.xpath_node("yt:videoId", namespaces).not_nil!.content
author = entry.xpath_node("default:author/default:name", namespaces).not_nil!.content
published = Time.parse_rfc3339(entry.xpath_node("default:published", namespaces).not_nil!.content)
updated = Time.parse_rfc3339(entry.xpath_node("default:updated", namespaces).not_nil!.content)
begin
video = get_video(id, force_refresh: true) video = get_video(id, force_refresh: true)
rescue
next # skip this video since it raised an exception (e.g. it is a scheduled live event)
end
if CONFIG.enable_user_notifications if CONFIG.enable_user_notifications
# Deliver notifications to `/api/v1/auth/notifications` # Deliver notifications to `/api/v1/auth/notifications`

View File

@ -42,7 +42,7 @@ module Invidious::Routes::VideoPlayback
headers["Range"] = "bytes=#{range_for_head}" headers["Range"] = "bytes=#{range_for_head}"
end end
client = make_client(URI.parse(host), region) client = make_client(URI.parse(host), region, force_resolve = true)
response = HTTP::Client::Response.new(500) response = HTTP::Client::Response.new(500)
error = "" error = ""
5.times do 5.times do
@ -57,7 +57,7 @@ module Invidious::Routes::VideoPlayback
if new_host != host if new_host != host
host = new_host host = new_host
client.close client.close
client = make_client(URI.parse(new_host), region) client = make_client(URI.parse(new_host), region, force_resolve = true)
end end
url = "#{location.request_target}&host=#{location.host}#{region ? "&region=#{region}" : ""}" url = "#{location.request_target}&host=#{location.host}#{region ? "&region=#{region}" : ""}"
@ -71,7 +71,7 @@ module Invidious::Routes::VideoPlayback
fvip = "3" fvip = "3"
host = "https://r#{fvip}---#{mn}.googlevideo.com" host = "https://r#{fvip}---#{mn}.googlevideo.com"
client = make_client(URI.parse(host), region) client = make_client(URI.parse(host), region, force_resolve = true)
rescue ex rescue ex
error = ex.message error = ex.message
end end
@ -80,9 +80,14 @@ module Invidious::Routes::VideoPlayback
# Remove the Range header added previously. # Remove the Range header added previously.
headers.delete("Range") if range_header.nil? headers.delete("Range") if range_header.nil?
playback_statistics = get_playback_statistic()
playback_statistics["totalRequests"] += 1
if response.status_code >= 400 if response.status_code >= 400
env.response.content_type = "text/plain" env.response.content_type = "text/plain"
haltf env, response.status_code haltf env, response.status_code
else
playback_statistics["successfulRequests"] += 1
end end
if url.includes? "&file=seg.ts" if url.includes? "&file=seg.ts"
@ -191,7 +196,7 @@ module Invidious::Routes::VideoPlayback
break break
else else
client.close client.close
client = make_client(URI.parse(host), region) client = make_client(URI.parse(host), region, force_resolve = true)
end end
end end

View File

@ -275,6 +275,12 @@ module Invidious::Routes::Watch
return error_template(400, "Invalid clip ID") if response["error"]? return error_template(400, "Invalid clip ID") if response["error"]?
if video_id = response.dig?("endpoint", "watchEndpoint", "videoId") if video_id = response.dig?("endpoint", "watchEndpoint", "videoId")
if params = response.dig?("endpoint", "watchEndpoint", "params").try &.as_s
start_time, end_time, _ = parse_clip_parameters(params)
env.params.query["start"] = start_time.to_s if start_time != nil
env.params.query["end"] = end_time.to_s if end_time != nil
end
return env.redirect "/watch?v=#{video_id}&#{env.params.query}" return env.redirect "/watch?v=#{video_id}&#{env.params.query}"
else else
return error_template(404, "The requested clip doesn't exist") return error_template(404, "The requested clip doesn't exist")

View File

@ -235,6 +235,7 @@ module Invidious::Routing
get "/api/v1/captions/:id", {{namespace}}::Videos, :captions get "/api/v1/captions/:id", {{namespace}}::Videos, :captions
get "/api/v1/annotations/:id", {{namespace}}::Videos, :annotations get "/api/v1/annotations/:id", {{namespace}}::Videos, :annotations
get "/api/v1/comments/:id", {{namespace}}::Videos, :comments get "/api/v1/comments/:id", {{namespace}}::Videos, :comments
get "/api/v1/clips/:id", {{namespace}}::Videos, :clips
# Feeds # Feeds
get "/api/v1/trending", {{namespace}}::Feeds, :trending get "/api/v1/trending", {{namespace}}::Feeds, :trending

View File

@ -300,9 +300,9 @@ module Invidious::Search
object["9:varint"] = ((page - 1) * 20).to_i64 object["9:varint"] = ((page - 1) * 20).to_i64
end end
# If the object is empty, return an empty string, # Prevent censoring of self harm topics
# otherwise encode to protobuf then to base64 # See https://github.com/iv-org/invidious/issues/4398
return "" if object.empty? object["30:varint"] = 1.to_i64
return object return object
.try { |i| Protodec::Any.cast_json(i) } .try { |i| Protodec::Any.cast_json(i) }

View File

@ -227,8 +227,22 @@ struct Video
info.dig?("streamingData", "hlsManifestUrl").try &.as_s info.dig?("streamingData", "hlsManifestUrl").try &.as_s
end end
def dash_manifest_url def dash_manifest_url : String?
info.dig?("streamingData", "dashManifestUrl").try &.as_s raw_dash_url = info.dig?("streamingData", "dashManifestUrl").try &.as_s
return nil if raw_dash_url.nil?
# Use manifest v5 parameter to reduce file size
# See https://github.com/iv-org/invidious/issues/4186
dash_url = URI.parse(raw_dash_url)
dash_query = dash_url.query || ""
if dash_query.empty?
dash_url.path = "#{dash_url.path}/mpd_version/5"
else
dash_url.query = "#{dash_query}&mpd_version=5"
end
return dash_url.to_s
end end
def genre_url : String? def genre_url : String?

View File

@ -0,0 +1,22 @@
require "json"
# returns start_time, end_time and clip_title
def parse_clip_parameters(params) : {Float64?, Float64?, String?}
decoded_protobuf = params.try { |i| URI.decode_www_form(i) }
.try { |i| Base64.decode(i) }
.try { |i| IO::Memory.new(i) }
.try { |i| Protodec::Any.parse(i) }
start_time = decoded_protobuf
.try(&.["50:0:embedded"]["2:1:varint"].as_i64)
.try { |i| i/1000 }
end_time = decoded_protobuf
.try(&.["50:0:embedded"]["3:2:varint"].as_i64)
.try { |i| i/1000 }
clip_title = decoded_protobuf
.try(&.["50:0:embedded"]["4:3:string"].as_s)
return start_time, end_time, clip_title
end

View File

@ -78,6 +78,11 @@ def extract_video_info(video_id : String, proxy_region : String? = nil)
# YouTube may return a different video player response than expected. # YouTube may return a different video player response than expected.
# See: https://github.com/TeamNewPipe/NewPipe/issues/8713 # See: https://github.com/TeamNewPipe/NewPipe/issues/8713
# Line to be reverted if one day we solve the video not available issue. # Line to be reverted if one day we solve the video not available issue.
# Although technically not a call to /videoplayback the fact that YouTube is returning the
# wrong video means that we should count it as a failure.
get_playback_statistic()["totalRequests"] += 1
return { return {
"version" => JSON::Any.new(Video::SCHEMA_VERSION.to_i64), "version" => JSON::Any.new(Video::SCHEMA_VERSION.to_i64),
"reason" => JSON::Any.new("Can't load the video on this Invidious instance. YouTube is currently trying to block Invidious instances. <a href=\"https://github.com/iv-org/invidious/issues/3822\">Click here for more info about the issue.</a>"), "reason" => JSON::Any.new("Can't load the video on this Invidious instance. YouTube is currently trying to block Invidious instances. <a href=\"https://github.com/iv-org/invidious/issues/3822\">Click here for more info about the issue.</a>"),

View File

@ -82,11 +82,19 @@
</div> </div>
<div class="video-card-row flexible"> <div class="video-card-row flexible">
<div class="flex-left"><a href="/channel/<%= item.ucid %>"> <div class="flex-left">
<% if !item.ucid.to_s.empty? %>
<a href="/channel/<%= item.ucid %>">
<p class="channel-name" dir="auto"><%= HTML.escape(item.author) %> <p class="channel-name" dir="auto"><%= HTML.escape(item.author) %>
<%- if author_verified %>&nbsp;<i class="icon ion ion-md-checkmark-circle"></i><% end -%> <%- if author_verified %>&nbsp;<i class="icon ion ion-md-checkmark-circle"></i><% end -%>
</p> </p>
</a></div> </a>
<% else %>
<p class="channel-name" dir="auto"><%= HTML.escape(item.author) %>
<%- if author_verified %>&nbsp;<i class="icon ion ion-md-checkmark-circle"></i><% end -%>
</p>
<% end %>
</div>
</div> </div>
<% when Category %> <% when Category %>
<% else %> <% else %>
@ -160,11 +168,19 @@
</div> </div>
<div class="video-card-row flexible"> <div class="video-card-row flexible">
<div class="flex-left"><a href="/channel/<%= item.ucid %>"> <div class="flex-left">
<% if !item.ucid.to_s.empty? %>
<a href="/channel/<%= item.ucid %>">
<p class="channel-name" dir="auto"><%= HTML.escape(item.author) %> <p class="channel-name" dir="auto"><%= HTML.escape(item.author) %>
<%- if author_verified %>&nbsp;<i class="icon ion ion-md-checkmark-circle"></i><% end -%> <%- if author_verified %>&nbsp;<i class="icon ion ion-md-checkmark-circle"></i><% end -%>
</p> </p>
</a></div> </a>
<% else %>
<p class="channel-name" dir="auto"><%= HTML.escape(item.author) %>
<%- if author_verified %>&nbsp;<i class="icon ion ion-md-checkmark-circle"></i><% end -%>
</p>
<% end %>
</div>
<%= rendered "components/video-context-buttons" %> <%= rendered "components/video-context-buttons" %>
</div> </div>

View File

@ -1,5 +1,9 @@
<%
locale = env.get("preferences").as(Preferences).locale
dark_mode = env.get("preferences").as(Preferences).dark_mode
%>
<!DOCTYPE html> <!DOCTYPE html>
<html lang="<%= env.get("preferences").as(Preferences).locale %>"> <html lang="<%= locale %>">
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
@ -17,19 +21,14 @@
<link rel="stylesheet" href="/css/grids-responsive-min.css?v=<%= ASSET_COMMIT %>"> <link rel="stylesheet" href="/css/grids-responsive-min.css?v=<%= ASSET_COMMIT %>">
<link rel="stylesheet" href="/css/ionicons.min.css?v=<%= ASSET_COMMIT %>"> <link rel="stylesheet" href="/css/ionicons.min.css?v=<%= ASSET_COMMIT %>">
<link rel="stylesheet" href="/css/default.css?v=<%= ASSET_COMMIT %>"> <link rel="stylesheet" href="/css/default.css?v=<%= ASSET_COMMIT %>">
<link rel="stylesheet" href="/css/carousel.css?v=<%= ASSET_COMMIT %>">
<script src="/js/_helpers.js?v=<%= ASSET_COMMIT %>"></script> <script src="/js/_helpers.js?v=<%= ASSET_COMMIT %>"></script>
</head> </head>
<%
locale = env.get("preferences").as(Preferences).locale
dark_mode = env.get("preferences").as(Preferences).dark_mode
%>
<body class="<%= dark_mode.blank? ? "no" : dark_mode %>-theme"> <body class="<%= dark_mode.blank? ? "no" : dark_mode %>-theme">
<span style="display:none" id="dark_mode_pref"><%= env.get("preferences").as(Preferences).dark_mode %></span> <span style="display:none" id="dark_mode_pref"><%= dark_mode %></span>
<div class="pure-g"> <div class="pure-g">
<div class="pure-u-1 pure-u-md-2-24"></div> <div class="pure-u-1 pure-u-xl-20-24" id="contents">
<div class="pure-u-1 pure-u-md-20-24" id="contents">
<div class="pure-g navbar h-box"> <div class="pure-g navbar h-box">
<% if navbar_search %> <% if navbar_search %>
<div class="pure-u-1 pure-u-md-4-24"> <div class="pure-u-1 pure-u-md-4-24">
@ -43,8 +42,8 @@
<div class="pure-u-1 pure-u-md-8-24 user-field"> <div class="pure-u-1 pure-u-md-8-24 user-field">
<% if env.get? "user" %> <% if env.get? "user" %>
<div class="pure-u-1-4"> <div class="pure-u-1-4">
<a id="toggle_theme" href="/toggle_theme?referer=<%= env.get?("current_page") %>" class="pure-menu-heading"> <a id="toggle_theme" href="/toggle_theme?referer=<%= env.get?("current_page") %>" class="pure-menu-heading" title="<%= translate(locale, "toggle_theme") %>">
<% if env.get("preferences").as(Preferences).dark_mode == "dark" %> <% if dark_mode == "dark" %>
<i class="icon ion-ios-sunny"></i> <i class="icon ion-ios-sunny"></i>
<% else %> <% else %>
<i class="icon ion-ios-moon"></i> <i class="icon ion-ios-moon"></i>
@ -81,8 +80,8 @@
</div> </div>
<% else %> <% else %>
<div class="pure-u-1-3"> <div class="pure-u-1-3">
<a id="toggle_theme" href="/toggle_theme?referer=<%= env.get?("current_page") %>" class="pure-menu-heading"> <a id="toggle_theme" href="/toggle_theme?referer=<%= env.get?("current_page") %>" class="pure-menu-heading" title="<%= translate(locale, "toggle_theme") %>">
<% if env.get("preferences").as(Preferences).dark_mode == "dark" %> <% if dark_mode == "dark" %>
<i class="icon ion-ios-sunny"></i> <i class="icon ion-ios-sunny"></i>
<% else %> <% else %>
<i class="icon ion-ios-moon"></i> <i class="icon ion-ios-moon"></i>
@ -156,7 +155,6 @@
</footer> </footer>
</div> </div>
<div class="pure-u-1 pure-u-md-2-24"></div>
</div> </div>
<script src="/js/handlers.js?v=<%= ASSET_COMMIT %>"></script> <script src="/js/handlers.js?v=<%= ASSET_COMMIT %>"></script>
<script src="/js/themes.js?v=<%= ASSET_COMMIT %>"></script> <script src="/js/themes.js?v=<%= ASSET_COMMIT %>"></script>

View File

@ -118,7 +118,7 @@ we're going to need to do it here in order to allow for translations.
link_yt_embed = URI.new(scheme: "https", host: "www.youtube.com", path: "/embed/#{video.id}") link_yt_embed = URI.new(scheme: "https", host: "www.youtube.com", path: "/embed/#{video.id}")
if !plid.nil? && !continuation.nil? if !plid.nil? && !continuation.nil?
link_yt_param = URI::Params{"plid" => [plid], "index" => [continuation.to_s]} link_yt_param = URI::Params{"list" => [plid], "index" => [continuation.to_s]}
link_yt_watch = IV::HttpServer::Utils.add_params_to_url(link_yt_watch, link_yt_param) link_yt_watch = IV::HttpServer::Utils.add_params_to_url(link_yt_watch, link_yt_param)
link_yt_embed = IV::HttpServer::Utils.add_params_to_url(link_yt_embed, link_yt_param) link_yt_embed = IV::HttpServer::Utils.add_params_to_url(link_yt_embed, link_yt_param)
end end
@ -346,7 +346,7 @@ we're going to need to do it here in order to allow for translations.
<h5 class="pure-g"> <h5 class="pure-g">
<div class="pure-u-14-24"> <div class="pure-u-14-24">
<% if rv["ucid"]? %> <% if !rv["ucid"].empty? %>
<b style="width:100%"><a href="/channel/<%= rv["ucid"] %>"><%= rv["author"]? %><% if rv["author_verified"]? == "true" %>&nbsp;<i class="icon ion ion-md-checkmark-circle"></i><% end %></a></b> <b style="width:100%"><a href="/channel/<%= rv["ucid"] %>"><%= rv["author"]? %><% if rv["author_verified"]? == "true" %>&nbsp;<i class="icon ion ion-md-checkmark-circle"></i><% end %></a></b>
<% else %> <% else %>
<b style="width:100%"><%= rv["author"]? %><% if rv["author_verified"]? == "true" %>&nbsp;<i class="icon ion ion-md-checkmark-circle"></i><% end %></b> <b style="width:100%"><%= rv["author"]? %><% if rv["author_verified"]? == "true" %>&nbsp;<i class="icon ion ion-md-checkmark-circle"></i><% end %></b>

View File

@ -1,7 +1,6 @@
def add_yt_headers(request) def add_yt_headers(request)
if request.headers["User-Agent"] == "Crystal" request.headers.delete("User-Agent") if request.headers["User-Agent"] == "Crystal"
request.headers["User-Agent"] ||= "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36" request.headers["User-Agent"] ||= "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36"
end
request.headers["Accept-Charset"] ||= "ISO-8859-1,utf-8;q=0.7,*;q=0.7" request.headers["Accept-Charset"] ||= "ISO-8859-1,utf-8;q=0.7,*;q=0.7"
request.headers["Accept"] ||= "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8" request.headers["Accept"] ||= "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"
@ -27,7 +26,7 @@ struct YoutubeConnectionPool
def client(region = nil, &block) def client(region = nil, &block)
if region if region
conn = make_client(url, region) conn = make_client(url, region, force_resolve = true)
response = yield conn response = yield conn
else else
conn = pool.checkout conn = pool.checkout
@ -37,7 +36,7 @@ struct YoutubeConnectionPool
conn.close conn.close
conn = HTTP::Client.new(url) conn = HTTP::Client.new(url)
conn.family = (url.host == "www.youtube.com") ? CONFIG.force_resolve : Socket::Family::INET conn.family = CONFIG.force_resolve
conn.family = Socket::Family::INET if conn.family == Socket::Family::UNSPEC conn.family = Socket::Family::INET if conn.family == Socket::Family::UNSPEC
conn.before_request { |r| add_yt_headers(r) } if url.host == "www.youtube.com" conn.before_request { |r| add_yt_headers(r) } if url.host == "www.youtube.com"
response = yield conn response = yield conn
@ -52,7 +51,7 @@ struct YoutubeConnectionPool
private def build_pool private def build_pool
DB::Pool(HTTP::Client).new(initial_pool_size: 0, max_pool_size: capacity, max_idle_pool_size: capacity, checkout_timeout: timeout) do DB::Pool(HTTP::Client).new(initial_pool_size: 0, max_pool_size: capacity, max_idle_pool_size: capacity, checkout_timeout: timeout) do
conn = HTTP::Client.new(url) conn = HTTP::Client.new(url)
conn.family = (url.host == "www.youtube.com") ? CONFIG.force_resolve : Socket::Family::INET conn.family = CONFIG.force_resolve
conn.family = Socket::Family::INET if conn.family == Socket::Family::UNSPEC conn.family = Socket::Family::INET if conn.family == Socket::Family::UNSPEC
conn.before_request { |r| add_yt_headers(r) } if url.host == "www.youtube.com" conn.before_request { |r| add_yt_headers(r) } if url.host == "www.youtube.com"
conn conn
@ -60,9 +59,14 @@ struct YoutubeConnectionPool
end end
end end
def make_client(url : URI, region = nil) def make_client(url : URI, region = nil, force_resolve : Bool = false)
client = HTTPClient.new(url, OpenSSL::SSL::Context::Client.insecure) client = HTTPClient.new(url, OpenSSL::SSL::Context::Client.insecure)
client.family = (url.host == "www.youtube.com") ? CONFIG.force_resolve : Socket::Family::UNSPEC
# Some services do not support IPv6.
if force_resolve
client.family = CONFIG.force_resolve
end
client.before_request { |r| add_yt_headers(r) } if url.host == "www.youtube.com" client.before_request { |r| add_yt_headers(r) } if url.host == "www.youtube.com"
client.read_timeout = 10.seconds client.read_timeout = 10.seconds
client.connect_timeout = 10.seconds client.connect_timeout = 10.seconds
@ -81,8 +85,8 @@ def make_client(url : URI, region = nil)
return client return client
end end
def make_client(url : URI, region = nil, &block) def make_client(url : URI, region = nil, force_resolve : Bool = false, &block)
client = make_client(url, region) client = make_client(url, region, force_resolve)
begin begin
yield client yield client
ensure ensure

View File

@ -822,9 +822,9 @@ module HelperExtractors
end end
# Retrieves the ID required for querying the InnerTube browse endpoint. # Retrieves the ID required for querying the InnerTube browse endpoint.
# Raises when it's unable to do so # Returns an empty string when it's unable to do so
def self.get_browse_id(container) def self.get_browse_id(container)
return container.dig("navigationEndpoint", "browseEndpoint", "browseId").as_s return container.dig?("navigationEndpoint", "browseEndpoint", "browseId").try &.as_s || ""
end end
end end

View File

@ -7,17 +7,18 @@ module YoutubeAPI
private DEFAULT_API_KEY = "AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8" private DEFAULT_API_KEY = "AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8"
private ANDROID_APP_VERSION = "18.20.38" # For Android versions, see https://en.wikipedia.org/wiki/Android_version_history
# github.com/TeamNewPipe/NewPipeExtractor/blob/943b7c033bb9d07ead63ddab4441c287653e4384/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/YoutubeParsingHelper.java#L1308 private ANDROID_APP_VERSION = "19.09.36"
private ANDROID_USER_AGENT = "com.google.android.youtube/18.20.38 (Linux; U; Android 12; US) gzip" private ANDROID_USER_AGENT = "com.google.android.youtube/19.09.36 (Linux; U; Android 12; US) gzip"
private ANDROID_SDK_VERSION = 31_i64 private ANDROID_SDK_VERSION = 31_i64
private ANDROID_VERSION = "12" private ANDROID_VERSION = "12"
private IOS_APP_VERSION = "18.21.3" # For Apple device names, see https://gist.github.com/adamawolf/3048717
# github.com/TeamNewPipe/NewPipeExtractor/blob/943b7c033bb9d07ead63ddab4441c287653e4384/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/YoutubeParsingHelper.java#L1330 # For iOS versions, see https://en.wikipedia.org/wiki/IOS_version_history#Releases,
private IOS_USER_AGENT = "com.google.ios.youtube/18.21.3 (iPhone14,5; U; CPU iOS 15_6 like Mac OS X;)" # then go to the dedicated article of the major version you want.
# github.com/TeamNewPipe/NewPipeExtractor/blob/943b7c033bb9d07ead63ddab4441c287653e4384/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/YoutubeParsingHelper.java#L1224 private IOS_APP_VERSION = "19.09.3"
private IOS_VERSION = "15.6.0.19G71" private IOS_USER_AGENT = "com.google.ios.youtube/19.09.3 (iPhone14,5; U; CPU iOS 17_4 like Mac OS X;)"
private IOS_VERSION = "17.4.0.21E219" # Major.Minor.Patch.Build
private WINDOWS_VERSION = "10.0" private WINDOWS_VERSION = "10.0"
@ -45,7 +46,7 @@ module YoutubeAPI
ClientType::Web => { ClientType::Web => {
name: "WEB", name: "WEB",
name_proto: "1", name_proto: "1",
version: "2.20230602.01.00", version: "2.20240304.00.00",
api_key: DEFAULT_API_KEY, api_key: DEFAULT_API_KEY,
screen: "WATCH_FULL_SCREEN", screen: "WATCH_FULL_SCREEN",
os_name: "Windows", os_name: "Windows",
@ -55,7 +56,7 @@ module YoutubeAPI
ClientType::WebEmbeddedPlayer => { ClientType::WebEmbeddedPlayer => {
name: "WEB_EMBEDDED_PLAYER", name: "WEB_EMBEDDED_PLAYER",
name_proto: "56", name_proto: "56",
version: "1.20220803.01.00", version: "1.20240303.00.00",
api_key: DEFAULT_API_KEY, api_key: DEFAULT_API_KEY,
screen: "EMBED", screen: "EMBED",
os_name: "Windows", os_name: "Windows",
@ -65,7 +66,7 @@ module YoutubeAPI
ClientType::WebMobile => { ClientType::WebMobile => {
name: "MWEB", name: "MWEB",
name_proto: "2", name_proto: "2",
version: "2.20230531.05.00", version: "2.20240304.08.00",
api_key: DEFAULT_API_KEY, api_key: DEFAULT_API_KEY,
os_name: "Android", os_name: "Android",
os_version: ANDROID_VERSION, os_version: ANDROID_VERSION,
@ -74,7 +75,7 @@ module YoutubeAPI
ClientType::WebScreenEmbed => { ClientType::WebScreenEmbed => {
name: "WEB", name: "WEB",
name_proto: "1", name_proto: "1",
version: "2.20220804.00.00", version: "2.20240304.00.00",
api_key: DEFAULT_API_KEY, api_key: DEFAULT_API_KEY,
screen: "EMBED", screen: "EMBED",
os_name: "Windows", os_name: "Windows",
@ -99,7 +100,7 @@ module YoutubeAPI
name: "ANDROID_EMBEDDED_PLAYER", name: "ANDROID_EMBEDDED_PLAYER",
name_proto: "55", name_proto: "55",
version: ANDROID_APP_VERSION, version: ANDROID_APP_VERSION,
api_key: DEFAULT_API_KEY, api_key: "AIzaSyCjc_pVEDi4qsv5MtC2dMXzpIaDoRFLsxw",
}, },
ClientType::AndroidScreenEmbed => { ClientType::AndroidScreenEmbed => {
name: "ANDROID", name: "ANDROID",
@ -143,9 +144,9 @@ module YoutubeAPI
ClientType::IOSMusic => { ClientType::IOSMusic => {
name: "IOS_MUSIC", name: "IOS_MUSIC",
name_proto: "26", name_proto: "26",
version: "5.21", version: "6.42",
api_key: "AIzaSyBAETezhkwP0ZWA02RsqT1zu78Fpt0bC_s", api_key: "AIzaSyBAETezhkwP0ZWA02RsqT1zu78Fpt0bC_s",
user_agent: "com.google.ios.youtubemusic/5.21 (iPhone14,5; U; CPU iOS 15_6 like Mac OS X;)", user_agent: "com.google.ios.youtubemusic/6.42 (iPhone14,5; U; CPU iOS 17_4 like Mac OS X;)",
device_make: "Apple", device_make: "Apple",
device_model: "iPhone14,5", device_model: "iPhone14,5",
os_name: "iPhone", os_name: "iPhone",
@ -158,7 +159,7 @@ module YoutubeAPI
ClientType::TvHtml5 => { ClientType::TvHtml5 => {
name: "TVHTML5", name: "TVHTML5",
name_proto: "7", name_proto: "7",
version: "7.20220325", version: "7.20240304.10.00",
api_key: DEFAULT_API_KEY, api_key: DEFAULT_API_KEY,
}, },
ClientType::TvHtml5ScreenEmbed => { ClientType::TvHtml5ScreenEmbed => {