Compare commits

..

322 Commits

Author SHA1 Message Date
Émilien (perso)
0c07e9d27a
chore: set dash by default (#5216) 2025-04-04 14:00:29 +02:00
Émilien (perso)
23ff6135bb
chore: enforce 16 characters for invidious_companion_key (#5220) 2025-03-26 15:27:59 +01:00
syeopite
409d12a81e
Prepare for next release (#5206) 2025-03-16 01:03:01 +00:00
Émilien (perso)
70ff463cc6
Add invidious companion support (#4985)
* add support for invidious companion

* redirect latest_version and dash manifest to invidious companion

* fix Shadowing outer local variable `response`

* fixing condition for Content-Security-Policy

* throw error if inv_sig_helper and invidious_companion used same time

* Use sample instead of Random.rand

Co-authored-by: syeopite <70992037+syeopite@users.noreply.github.com>

* Remove debug puts functions

Co-authored-by: syeopite <70992037+syeopite@users.noreply.github.com>

* modify the description for config.example.yaml about invidious companion

* move config checks for invidious companion

* separate invidious_companion logic + better config.yaml config

* fixing "end" misplacement

* fix linting + use .empty?

* crystal handle decompression already by itself

* fix download function when invidious companion used

* fix linting

* invidious companion always used so always add CSP and redirect latest_version

* apply all the suggestions + rework invidious_companion parameter

* format watch.cr

* fix ameba Redundant use of `Object#to_s` in interpolation

* add ability for invidious companion to check request from invidious

* Better document private_url and public_url

* Better doc for invidious_companion_key

* !empty? to present?

* skip proxy for invidious companion

* fixing format

* missing ,

* add companion pooling http

* fix: don't use http proxy when sending requests to companion

* fix: logic where we want to have the invidious logic if companion is not used

* chore: remove baseurl usage from invidious companion

* chore: change from inv-sig-helper to companion for required playback

* fix: use puts + add warning for inv-sig-helper deprecated

---------

Co-authored-by: syeopite <70992037+syeopite@users.noreply.github.com>
2025-03-13 16:44:00 +01:00
syeopite
e23d0d13be
Add changelog for v2.20250314.0 (#5197)
* Release v2.20250314.0

* Update CHANGELOG.md
2025-03-12 03:31:15 -07:00
syeopite
5c8b4eb379
Warn when po_token, visitor_data and/or inv-sig-helper is not configured (#5202)
* Warn when required configs for playback is missing

* Add link to documentation in warnings

* Direct users to /installation instead
2025-03-12 10:11:17 +01:00
syeopite
adcdb8cb92
Fix lint and formatting 2025-02-26 14:18:50 -08:00
syeopite
fe4fa0480a
Fix HLS being used for non-livestream videos (#5189)
Invidious does not currently support non-livestream hls playback

Originally, the HLS manifest check was essentially a boolean:
if the HLS manifest field was present, it was assumed to be a
livestream. Some videos include the HLS Manifest but aren't
livestreams.

In the case where they are livestreams, the video contains a videoType
field with the value "Livestream". In the case that they're normal
videos, the videoType is "Video". This is exposed via the
`video.live_now` property.

This commit just checks that `video.live_now` is true before treating
it as a livestream
2025-02-26 14:14:29 -08:00
syeopite
dbbcacc955
Images: fix typo in thumbnail logic 2025-02-26 14:13:58 -08:00
syeopite
58ad848d56
Channels: Support YouTube's change to from /community to /posts (#5183) 2025-02-26 14:13:22 -08:00
syeopite
f9b9e85ee4
Docker: Use Crystal compiler cache in docker builds (#5163)
Adding the compiler cache reduces the build times on repeated
builds significantly
2025-02-26 14:11:12 -08:00
syeopite
6ac74f4362
Videos: Fix empty response when rv published field is nonexistent (#5162)
Fixes #5161 by checking recommended videos published field for presence
before attempting to parse it in api
2025-02-26 14:09:28 -08:00
syeopite
9fbe3944b0
Channels: Add Courses to channel page and channel API (#5158)
Closes #5144
2025-02-26 14:08:44 -08:00
syeopite
c5e9447f41
Pick a different instance upon redirect (#5154)
The automatic instance redirection has the potential to pick
the same instance the user is currently on. This is especially
prevalent when the instance list is limited in number like how it is
today.

This PR checks the domain of the instance and ensures that it is not
the same as the current instane before redirecting the user to it.
Otherwise, it just sends the user to rediret.invidious.io
2025-02-26 14:05:21 -08:00
syeopite
3e329410d1
Add the ability to listen on UNIX sockets (#5112) 2025-02-26 14:04:29 -08:00
syeopite
74dfda150e
i18n: Enable Tamil 2025-02-26 14:02:57 -08:00
syeopite
e60f53154e
Translations update from Hosted Weblate (#4989) 2025-02-26 13:57:04 -08:00
syeopite
3d77635a5c
Add API endpoint for fetching transcripts from YouTube (#4788) 2025-02-26 13:56:39 -08:00
syeopite
d0433c8386
JS: Update timeupdate event defensive to prevent errors (#4782) 2025-02-26 13:56:13 -08:00
syeopite
4ea4878d1a
User: Batch notifications together 2025-02-26 13:55:25 -08:00
syeopite
1f0a89fb5f
RSS: Channel + Playlist improvements (#4298) 2025-02-26 13:55:01 -08:00
syeopite
f95f87e448
Frontend: Add a first page and previous page buttons for channel navigation (#4123) 2025-02-26 13:54:25 -08:00
Alex Maras
49afbf2a14 Fix an issue with the HLS manifest check for livestream videos
Originally, the HLS manifest check was essentially a boolean: if the HLS
manifest field was present, it was assumed to be a livestream. Some
videos include the HLS Manifest but aren't livestreams.

In the case where they are livestreams, the video contains a videoType
field with the value "Livestream". In the case that they're normal
videos, the videoType is "Video". This is exposed via the video.live_now
method.

This commit just checks that video.live_now is true before treating it
as a livestream
2025-02-21 16:30:39 +08:00
syeopite
d853b9f6dc
Typo
Co-authored-by: Samantaz Fox <coding@samantaz.fr>
2025-02-18 14:46:18 -08:00
Fijxu
d70681538a
Channels: Fix community tab 2025-02-18 19:20:55 -03:00
syeopite
e2df12b7d6
Use Crystal compiler cache in docker builds 2025-01-28 23:31:01 -08:00
Drikanis
29219c46a1 fix 5161 by checking recommended videos published field for presence instead of just not nil 2025-01-28 19:40:15 -07:00
epicsam123
a77f083a0a
remove ! on reject 2025-01-26 16:42:59 -05:00
ChunkyProgrammer
eaf47385c5 Add Courses to channel page and channel API 2025-01-25 14:43:39 -05:00
Hosted Weblate
1fb8d3f583
Add Toki Pona translation
Co-authored-by: Dave Brunker <dbrunker@flashmail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
2025-01-25 14:02:51 +01:00
Hosted Weblate
26b15d6e35
Update Norwegian Bokmål translation
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Petter Reinholdtsen <pere-weblate@hungry.com>
2025-01-25 14:02:51 +01:00
Hosted Weblate
786e3e0550
Update Serbian (Cyrillic script) translation
Update Serbian (Cyrillic script) translation

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: NEXI <nexiphotographer@gmail.com>
2025-01-25 14:02:51 +01:00
Hosted Weblate
104553fdc4
Update Chinese (Simplified Han script) translation
Update Chinese (Simplified Han script) translation

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: 大王叫我来巡山 <hamburger2048@users.noreply.hosted.weblate.org>
2025-01-25 14:02:50 +01:00
Hosted Weblate
ae670d5b2d
Update Chinese (Traditional Han script) translation
Update Chinese (Traditional Han script) translation

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Jeff Huang <s8321414@gmail.com>
2025-01-25 14:02:50 +01:00
Hosted Weblate
b2c14f1a2a
Update Slovenian translation
Co-authored-by: Damjan Gerl <damjan@damjan.net>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
2025-01-25 14:02:49 +01:00
Hosted Weblate
b899bc959e
Update Korean translation
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: xrfmkrh <rF3nMd7sRKezjF2vcEQo@protonmail.com>
2025-01-25 14:02:49 +01:00
Hosted Weblate
74dc6795cd
Update Albanian translation
Co-authored-by: Besnik Bleta <besnik@programeshqip.org>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
2025-01-25 14:02:48 +01:00
Hosted Weblate
5404b67bef
Update Serbian translation
Update Serbian translation

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: NEXI <nexiphotographer@gmail.com>
2025-01-25 14:02:48 +01:00
Hosted Weblate
7b59ccf645
Update Finnish translation
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Juli <julimiro@posteo.net>
2025-01-25 14:02:48 +01:00
Hosted Weblate
cc6c39d0e6
Update Persian translation
Co-authored-by: Danial Behzadi <dani.behzi@ubuntu.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
2025-01-25 14:02:47 +01:00
Hosted Weblate
37f3c285d7
Update Swedish translation
Update Swedish translation

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: bittin1ddc447d824349b2 <bittin@reimu.nl>
2025-01-25 14:02:47 +01:00
Hosted Weblate
106086c766
Update French translation
Co-authored-by: ABCraft19 <lesenfantsbergaoui@gmail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
2025-01-25 14:02:46 +01:00
Hosted Weblate
0980867d42
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>
2025-01-25 14:02:46 +01:00
Hosted Weblate
3abc377d56
Update Dutch translation
Update Dutch translation

Co-authored-by: Dick Groskamp <dikgro@yahoo.co.uk>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
2025-01-25 14:02:45 +01:00
Hosted Weblate
4a0a6f7ed5
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>
2025-01-25 14:02:45 +01:00
Hosted Weblate
3056e1767e
Update Italian translation
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Random <random-r@users.noreply.hosted.weblate.org>
2025-01-25 14:02:44 +01:00
Hosted Weblate
0846faa6f6
Update Polish translation
Update Polish translation

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Matthaiks <kitynska@gmail.com>
2025-01-25 14:02:44 +01:00
Hosted Weblate
943c42e47b
Update Croatian translation
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Milo Ivir <mail@milotype.de>
2025-01-25 14:02:43 +01:00
Hosted Weblate
fc7b5120db
Update Icelandic translation
Update Icelandic translation

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Sveinn í Felli <sv1@fellsnet.is>
2025-01-25 14:02:43 +01:00
Hosted Weblate
d4d6a4b172
Update Portuguese translation
Update Portuguese translation

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Sergio Marques <so.boston.android@gmail.com>
2025-01-25 14:02:42 +01:00
Hosted Weblate
e0cb54f7e0
Update Czech translation
Update Czech translation

Co-authored-by: Fjuro <fjuro@alius.cz>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
2025-01-25 14:02:42 +01:00
Hosted Weblate
844e1bdf43
Update Japanese translation
Update Japanese translation

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: maboroshin <maboroshin@users.noreply.hosted.weblate.org>
2025-01-25 14:02:41 +01:00
Hosted Weblate
aacfbb09da
Update Ukrainian translation
Update Ukrainian translation

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Ihor Hordiichuk <igor_ck@outlook.com>
2025-01-25 14:02:41 +01:00
Hosted Weblate
f57b4b5e4f
Update Russian translation
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: sergio <sergio+it@outerface.net>
2025-01-25 14:02:41 +01:00
Hosted Weblate
b1422b7434
Update Greek translation
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: hompre <46e989cc@opayq.com>
2025-01-25 14:02:40 +01:00
Hosted Weblate
f56e4012fe
Update German translation
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Sanny Cue <sanny.cue@gmail.com>
2025-01-25 14:02:39 +01:00
Hosted Weblate
7d5b2ec7b6
Update Portuguese (Brazil) translation
Update Portuguese (Brazil) translation

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: joaooliva <joaooliva@protonmail.com>
2025-01-25 14:02:39 +01:00
Hosted Weblate
cad64e420c
Update Tamil translation
Add Tamil translation

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: தமிழ்நேரம் <anishprabu.t@gmail.com>
2025-01-25 14:02:38 +01:00
Hosted Weblate
f181ae3cb0
Update Turkish translation
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Oğuz Ersen <oguz@ersen.moe>
2025-01-25 14:02:38 +01:00
epicsam123
0fd480bae2
lint edits, refactor 2025-01-25 03:24:38 -05:00
epicsam123
afb0aad7d3
moved comments 2025-01-24 21:54:10 -05:00
epicsam123
6816ded0fa
add missing end statement 2025-01-23 22:17:46 -05:00
epicsam123
0546a73bfa
Pick a different instance upon redirect 2025-01-22 17:33:54 -05:00
syeopite
164d764d55
API: Add a 'published' video parameter for related videos (#4149) 2025-01-22 11:38:12 -08:00
syeopite
4a31da4000
User: Ensure IO is properly closed when importing NewPipe subscriptions (#4346) 2025-01-22 11:36:58 -08:00
syeopite
831017f403
Frontend: Carry over audio-only mode in playlist links (#4784) 2025-01-22 11:35:33 -08:00
syeopite
52daafe047
Videos: Fix missing host parameter on playback URLs when local=true (#4992) 2025-01-22 11:34:46 -08:00
syeopite
dca130ca6f
Routes: Clean ajax actions handlers (#5036) 2025-01-22 11:33:51 -08:00
syeopite
086c6209ab
Remove stdlib override for proxy initialization (#5065) 2025-01-22 11:33:20 -08:00
syeopite
0d398c9d1a
API: Add support for author thumbnails in search api for videos (#5072) 2025-01-22 11:32:21 -08:00
syeopite
dc38bcdf17
Kemal: Skip route if response was closed by handlers (#5073) 2025-01-22 11:30:45 -08:00
syeopite
d5442d45bc
API: Fix video thumbnails in mixes (#5116) 2025-01-22 11:29:12 -08:00
syeopite
d4f0560e80
CI: Drop support for versions prior to 1.12 and add 1.15.0 (#5148) 2025-01-22 11:28:38 -08:00
syeopite
eae3c42dab
Videos: Set language for dash audio streams and sort (#5149) 2025-01-22 11:25:39 -08:00
syeopite
c0131d8646
Warn when any top-level config is "CHANGE_ME!!" (#5150) 2025-01-22 11:16:24 -08:00
syeopite
21fd717701
Comment out http_proxy in example config (#5151)
The http_proxy section was not commented out in the example config
causing Invidious to error out unless an HTTP proxy was configured.

This problem affects new manual installs in which the example config
is copied to create the actual config Invidious uses
2025-01-22 11:11:42 -08:00
syeopite
8ee73aa0c1
Remove formatter check on container workflows (#5153) 2025-01-22 19:07:24 +00:00
Giuliano Macedo
6e3ec10d76
feat(manifset): improved adaptationset label 2025-01-22 11:01:37 -08:00
GTechAlpha
d95ae7e6a5
Add audio track info to dash manifest, if present
- language id
  - language display name
  - main/default track
Sort audio formats so that main/default is first (for clients not using dash)

* Note: this should be a non-breaking change; if audio track info is not availablle, the behavior does not change from current
2025-01-22 11:01:37 -08:00
syeopite
d36f372bd1
CI: Add support for 1.15.0 2025-01-22 10:34:24 -08:00
syeopite
58c65e921f
CI: Drop support for versions prior to 1.12.0 2025-01-22 10:34:24 -08:00
syeopite
5d9ed95ffd
Warn when any top-level config is "CHANGE_ME!!" 2025-01-22 10:34:04 -08:00
syeopite
033e42a981
Comment out http_proxy in example config 2025-01-22 10:33:34 -08:00
syeopite
bfa6da2474
Make Invidious compliant to Crystal 1.15 formatting rules (#5014) 2025-01-22 18:32:35 +00:00
syeopite
097b4f0433
CI: Use separate shards cache for lint step
Ameba could be built with an older version of Crystal that follows
a different set of formatting rules than the latest version causing
the Lint/Formatting rule to fail when in actuality the code is actually
compliant with the formatting rules in the latest version of Crystal
2025-01-20 16:39:33 -08:00
syeopite
e1378702af
Apply upcoming formatting rules from Crystal 1.15 2025-01-20 16:15:13 -08:00
Émilien (perso)
b13f77b5af
Update bug report issue message 2025-01-09 14:21:28 +01:00
Caian Benedicto
b4a6193642
Improve syntax
Co-authored-by: syeopite <70992037+syeopite@users.noreply.github.com>
2025-01-05 09:56:00 +00:00
Caian Benedicto
525dea1e2a Add checks for socket path and permissions 2024-12-27 20:58:44 -03:00
Caian Benedicto
f9885cca8e Revert changes made to other parameters 2024-12-27 15:19:13 -03:00
Brahim Hadriche
047ead8080 Fix video thumbnails in mixes 2024-12-16 16:54:04 -05:00
Caian Benedicto
275318dae2 Change socket_binding to a nested configuration in YAML 2024-12-14 15:18:25 -03:00
Caian Benedicto
48d2250024 Unify socket_binding and socket_permissions 2024-12-14 06:53:30 -03:00
Caian Benedicto
5f8130fd03 Leave socket_binding disabled by default in the configuration example 2024-12-14 05:39:03 -03:00
Caian Benedicto
b4e930f3bc Change bind_unix to socket_binding, add socket_permissions and config example 2024-12-13 21:50:02 -03:00
Caian Benedicto
d7f5cdc2f9 Merge branch 'master' into unix-sockets 2024-12-13 20:26:52 -03:00
ChunkyProgrammer
04b0742293 remove icon element from channel rss feed 2024-11-17 13:14:39 -05:00
ChunkyProgrammer
1838ac4c99 do a sanity check on the provided ucid
Co-Authored-By: absidue <48293849+absidue@users.noreply.github.com>
Co-Authored-By: Samantaz Fox <coding@samantaz.fr>
2024-11-17 13:14:39 -05:00
ChunkyProgrammer
8729f01075 Channel RSS: deprecate author thumbnail, make less requests to youtube 2024-11-17 13:14:39 -05:00
ChunkyProgrammer
6dd89bd401 RSS: return 404 if youtube playlist doesnt exist 2024-11-17 13:14:39 -05:00
ChunkyProgrammer
bba1769f4b Use a find instead of an each loop 2024-11-17 13:12:56 -05:00
ChunkyProgrammer
6b0e4e6817 Put temp.delete inside ensure block 2024-11-17 13:12:56 -05:00
ChunkyProgrammer
6abee5de99 Ensure IO is properly closed when importing NewPipe subscriptions 2024-11-17 13:12:56 -05:00
Samantaz Fox
9892604758
Prepare for next release 2024-11-10 21:40:32 +01:00
Samantaz Fox
5d2dd40bc3
Release v2.20241110.0 2024-11-10 21:35:03 +01:00
Samantaz Fox
699d53ad41
Update shard.yml metadata (#5066)
Changes are mostly based off of the Crystal compiler's own shard.yml

Remember to bump the version attribute when creating a release!!!
2024-11-10 21:03:13 +01:00
Samantaz Fox
3ac8978e96
VideoProxy: Handle 302 redirects in chunked section 2024-11-10 18:15:24 +01:00
Samantaz Fox
e7a93fcc18
API: Replace any URL in HLS manifests 2024-11-10 18:13:30 +01:00
Samantaz Fox
aa33d9b7ec
Videos: Fix missing host parameter on playback URLs when local=true 2024-11-10 18:13:30 +01:00
Samantaz Fox
2150264d84
Update CHANGELOG.md 2024-11-10 18:00:26 +01:00
Samantaz Fox
d42561d74a
API: Add "sort_by" parameter to channels/shorts endpoint (#5071)
Small follow up to PR 5059

No related issue
2024-11-10 17:50:00 +01:00
Samantaz Fox
7092bb8855
Docker: Install tzdata in Dockerfile (#5070)
Should close 5067
2024-11-10 17:48:18 +01:00
Samantaz Fox
d7c35e6e3d
Videos: Stop using TVHTML5_SIMPLY_EMBEDDED_PLAYER (#5063)
The age restriction bypass does not work anymore with this client.
See: https://github.com/iv-org/invidious/issues/2189#issuecomment-2437740627

Related to 2189
2024-11-10 17:45:58 +01:00
Samantaz Fox
bc86fb8a82
Routing: Deprecate old channel API routes (#5045)
Deprecate the following routes:

* /api/v1/channels/videos/:ucid
* /api/v1/channels/latest/:ucid
* /api/v1/channels/playlists/:ucid
* /api/v1/channels/community/:ucid
* /api/v1/channels/search/:ucid

in favor of:

* /api/v1/channels/:ucid/videos
* /api/v1/channels/:ucid/latest
* /api/v1/channels/:ucid/playlists
* /api/v1/channels/:ucid/community
* /api/v1/channels/:ucid/search

No related issue
2024-11-10 17:44:45 +01:00
Samantaz Fox
ec82c2f539
Videos: use WEB client instead of WEB CREATOR (#4984)
Use the WEB client when a potoken is configured, otherwise try with Android
test suite if there is no potoken configured.

This PR reverts some of the changes made in 4928

Related to 4734
2024-11-10 17:41:54 +01:00
Samantaz Fox
4b363e32fa
Parsers: Fix parsing live_now and premiere_timestamp (#4934)
This pull request fixes the parsing for the 'live_now' and 'premiere_timestamp'
variables so that they work without the 'microformat' data being present.

Related to 4929
2024-11-10 17:36:49 +01:00
syeopite
7a15318fbc
Skip route if resp got closed by before handlers 2024-11-10 05:45:06 +00:00
ChunkyProgrammer
5fa87cc27c Add support for author thumbnails in search api for videos 2024-11-09 22:31:41 -05:00
Brahim Hadriche
d2123b4682 Sort channel shorts API 2024-11-09 17:49:06 -05:00
Émilien (perso)
0f8f32bca8 remove explicit usage of WEB 2024-11-09 22:21:09 +01:00
Emilien
f3e93ca83d revert back to www.youtube.com when client_config.screen embed 2024-11-09 22:21:09 +01:00
Emilien
82b1506ccc remove usage of WebEmbeddedPlayer 2024-11-09 22:21:09 +01:00
Emilien
b9ad9bd723 use WEB when po_token + android test suite when no po_token 2024-11-09 22:21:09 +01:00
syeopite
8bf7e02978
Change authors section to reflect current state 2024-11-09 13:04:10 -08:00
Samantaz Fox
1a49e798c8
Docker: Install tzdata in Dockerfile 2024-11-09 21:52:06 +01:00
syeopite
9d54cf903e
Update shard.yml metadata 2024-11-08 15:54:37 -08:00
syeopite
1333fed26c
Remove stdlib override for proxy initialization
HTTP Proxy is now initialized in the make_client function
2024-11-08 15:28:12 -08:00
Samantaz Fox
b173d4acf2
Update CHANGELOG.md 2024-11-08 23:45:15 +01:00
Samantaz Fox
43d5efd9da
Stale bot updates (#5060)
* Prevents PRs from being considered staled
* Double the stale timer for issues
* Prevent discussion issues from being staled

No related issue
2024-11-08 23:42:45 +01:00
Samantaz Fox
1480e0089f
Channels: Fix "Youtube API returned error 400" (#5059)
This PR also adds sort option to the channel "shorts" tab.
Thanks to iBicha for the original fix of the "livestreams" tab.

Closes 4029, 5021 and 5029
2024-11-08 23:40:34 +01:00
Samantaz Fox
a5fb78bba5
Locales: Add Bulgarian, Welsh and Lombard to the list (#5046)
No related issue
2024-11-08 23:33:36 +01:00
Samantaz Fox
09f5485889
Shards: Update database dependencies (#5034)
No related issue
2024-11-08 23:32:25 +01:00
Samantaz Fox
a760b69cb6
Logger: Add color support for different log levels (#4931)
No related issue
2024-11-08 23:28:51 +01:00
Samantaz Fox
4f7a18a630
Fix named arg syntax when passing force_resolve (#4754)
No related issue
2024-11-08 23:27:22 +01:00
Samantaz Fox
42da2547e3
Use make_client instead of calling HTTP::Client (#4709)
No related issue
2024-11-08 23:26:32 +01:00
Émilien (perso)
09ccea1d31
remove usage of TVHTML5_SIMPLY_EMBEDDED_PLAYER 2024-11-08 22:01:23 +01:00
Samantaz Fox
2a19dbb1fe
Channels: Use the same structure as in the other ctoken functions
Change explanation, courtesy of iBicha:
The \n is basically a decimal 10, which is 1010 binary. That is a field number
1, and a wire type 2 (length-delimited). Then the $ is a decimal 36, which is
exactly the length of 00000000-0000-0000-0000-000000000000.
So both objects end up being encoded into the same data.
2024-11-08 18:28:58 +01:00
Samantaz Fox
6dd662a5b8
Channels: lockupViewModel is also used in the "playlists" tab 2024-11-08 17:44:36 +01:00
Samantaz Fox
301aeffa78
Channels: Multiple small fixes
Fix the "newest" link not being bold when 'sort_by' uses the default value
Show 60 videos per page, rather than 30
2024-11-08 14:00:35 +01:00
Samantaz Fox
d27a5e7fae
Channels: Rename ctoken generator functions as requested 2024-11-08 14:00:35 +01:00
Samantaz Fox
afc5b27d83
Extractors: Add support for shortsLockupViewModel
The 'shortsLockupViewModel' structure is used in the channel "shorts" tab
2024-11-08 14:00:30 +01:00
Samantaz Fox
1a5047aad9
Extractors: Add support for lockupViewModel
The 'lockupViewModel' structure is used in the channel "podcasts" tab
2024-11-08 14:00:16 +01:00
syeopite
ce910b5269
Prevent discussion issues from being staled 2024-11-07 20:45:23 -08:00
syeopite
78f18b257c
Double stale timer for issues
Days before staling is increased to 730 days
Days before closing is increased to 60 days
2024-11-07 20:42:19 -08:00
syeopite
3196182d4d
Prevent PRs from being considered stale 2024-11-07 20:41:04 -08:00
Samantaz Fox
82248fad02
Channels: Add sort options to shorts 2024-11-07 23:08:36 +01:00
Samantaz Fox
cbc546f032
Channels: Add function to generate the new ctoken objects 2024-11-07 23:08:31 +01:00
Samantaz Fox
792d0d5f6d
CI: Check Crystal lint only on latest version (#5042)
* CI: Check Crystal lint only on latest version

* Apply suggestion from code review

Co-authored-by: syeopite <70992037+syeopite@users.noreply.github.com>

---------

Co-authored-by: syeopite <70992037+syeopite@users.noreply.github.com>
2024-11-07 13:14:36 +00:00
Émilien (perso)
ac6e796c73
checking the status code returned by youtube (#5052)
* checking the status code returned by youtube

* add documentation link

* Update src/invidious/yt_backend/youtube_api.cr

Co-authored-by: syeopite <70992037+syeopite@users.noreply.github.com>

---------

Co-authored-by: syeopite <70992037+syeopite@users.noreply.github.com>
2024-11-07 14:04:43 +01:00
Samantaz Fox
75c5881c55
Locales: Add Bulgarian, Welsh and Lombard to the list 2024-10-31 13:34:51 +01:00
Samantaz Fox
6da18ddc41
Routing: Also remove outdated comment about notification routes 2024-10-31 11:52:09 +01:00
Samantaz Fox
cdf93b29e6
Routing: Remove deprecated /api/v1/channels/.../:ucid routes 2024-10-31 11:51:33 +01:00
RadoslavL
eed14d08a8
Update src/invidious/jsonify/api_v1/video_json.cr
Co-authored-by: Samantaz Fox <coding@samantaz.fr>
2024-10-31 09:59:06 +02:00
Brahim Hadriche
c243d08afb refactor 2024-10-30 13:38:13 -04:00
Samantaz Fox
2e3a7ad044
Update CHANGELOG.md 2024-10-30 17:13:00 +01:00
Samantaz Fox
c427c184e2
Captions: Add "Filipino (auto-generated)" to the list of languages (#4995)
I encountered a wild
[warn] i18n: Missing translation key "Filipino (auto-generated)"
while browsing videos on the test instance.

No related issue.
2024-10-30 17:07:09 +01:00
Samantaz Fox
59acf23c0c
Makefile: Add MT option to enable the 'preview_mt' flag (#4993)
This PR add an MT option to the Makefile. When make is invoked with 'MT=1',
the 'preview_mt' flag is passed to the Crystal compiler.

It doesn't mean that invidious fully supports multi-threading, but at least
it provides an easy way for trying that out.

No related issue.
2024-10-30 17:05:21 +01:00
Samantaz Fox
2eeb6a731d
SigHelper: Reconnect to signature helper (#4991)
Fijxu have been using it for more than 3 weeks on their instance and
they report that it works really well.

This only works if 'inv_sig_helper' itself crashes and restarts (via systemd
or docker restart policy) but it will not work if 'inv_sig_helper' hangs and
stops responding to invidious (but this is an issue with 'inv_sig_helper',
not Invidious).

Closes issue 4926
2024-10-30 17:02:51 +01:00
Samantaz Fox
0fb67cc090
Player: Fix a bug where menus were hard to open (#4750)
The fix basically enlarges the hoverable area in order to avoid getting the
menu closed if the mouse isn't moved fast enough.

Video of the fix:
https://www.loom.com/share/36494a3653984650aea3eaa2af276a35

Closes issue 4749
2024-10-30 16:59:34 +01:00
Samantaz Fox
9957da28dc
Proxy: Use connection pools for images (#4326)
Theoretically this should improve memory usage and performance by quite a bit
as we aren't creating a new HTTP::Client and in a turn a new connection for
every image we request from YouTube.

Closes issue 4009
2024-10-30 13:55:28 +01:00
Samantaz Fox
f326bcf8db
Add support for using Invidious through a HTTP Proxy (#4270)
Partially addresses issue 301
2024-10-30 13:46:49 +01:00
Samantaz Fox
b0c7dd9771
HTML: Replace hidden 'action' input with query parameter
The server side can only handle parameters passed as URL query
parameters and not inside the request body
2024-10-29 22:14:27 +01:00
Samantaz Fox
dbdf2ad23a
Routes: Simplify actions in watch_ajax 2024-10-29 18:27:53 +01:00
Samantaz Fox
dbd96c77e4
Routes: Simplify actions in token_ajax 2024-10-29 18:21:58 +01:00
Samantaz Fox
e453a2a682
Routes: Simplify actions in subscription_ajax 2024-10-29 18:16:52 +01:00
Samantaz Fox
7e4b3b182a
Routes: Simplify actions in playlist_ajax 2024-10-29 18:09:50 +01:00
Samantaz Fox
711d52d47f
Shards: Update database dependencies 2024-10-29 17:26:24 +01:00
Brahim Hadriche
ee72809282 [Alternative] Fix for channel live videos 2024-10-26 12:40:31 -04:00
syeopite
d8b893e9ad
Bump CI matrix (#5015) 2024-10-18 21:33:38 +02:00
Émilien (perso)
70e4eb7f5d
Merge pull request #5004 from unixfox/update-mocks
update the mocks with the latest updated data
2024-10-14 00:06:29 +02:00
Emilien Devos
0d03818700 libsqlite3-dev is now missing in the CI env 2024-10-14 00:02:41 +02:00
Emilien Devos
e6f52eaf00 update submodule 2024-10-13 23:57:29 +02:00
Emilien Devos
90544e07b6 update the mocks with the latest updated data 2024-10-13 21:18:21 +02:00
Samantaz Fox
952b3625a0
Add "Filipino (auto-generated)" to the list of caption languages 2024-10-10 20:31:22 +02:00
Samantaz Fox
f51a3b8d2b
Makefile: Add MT option to enable the 'preview_mt' flag 2024-10-09 18:37:08 +02:00
Fijxu
84e4746265
SigHelper: Reconnect to signature helper
Signed-off-by: Fijxu <fijxu@nadeko.net>
2024-10-08 19:09:14 -03:00
Fijxu
d2edd4b63f
fixup! Logger: Add color support for different log levels 2024-10-08 18:36:50 -03:00
Samantaz Fox
a88a723de3
Update CHANGELOG.md 2024-10-08 18:36:41 +02:00
Samantaz Fox
d5f5490aee
Search: Fix 'youtu.be' URLs in sanitizer (#4894)
Use the proper URL argument when transforming youtu.be URLs to their
youtube.com equivalents.

Thanks to Tuhgy on the fediverse for reporting this!

No related issue
2024-10-08 18:03:56 +02:00
Samantaz Fox
82d797b74e
Ameba: Disable Style/RedundantNext rule (#4888)
No related issue
2024-10-08 18:02:47 +02:00
Samantaz Fox
97895a491a
Playlists: Fix 'invalid byte sequence' error when subscribing (#4887)
In Crystal, handling multi-byte sequences in UTF-8 requires understanding that
slicing by bytes can lead to invalid sequences if the slicing isn't aligned
with character boundaries. In this case, attempting to slice a string by bytes
can cut through multi-byte UTF-8 sequences, leading to invalid sequences.

To avoid this, strings should be sliced based on characters rather than bytes.

Fixes issue 4886
2024-10-08 18:01:22 +02:00
Samantaz Fox
0ac9367322
Parse more metadata badges for SearchVideos (#4863)
This PR makes it possible to display badges in the search results for third
party Invidious applications (ex: FreeTube)

See also: https://github.com/FreeTubeApp/FreeTube/pull/5590

No related issue
2024-10-08 17:59:35 +02:00
Samantaz Fox
d3830f7870
Translations update from Hosted Weblate (#4862) 2024-10-08 17:56:39 +02:00
Samantaz Fox
3cfcc16403
Videos: Convert URL before putting result into cache (#4850)
Closes issue 4837
2024-10-08 17:52:34 +02:00
Samantaz Fox
171c0a0814
HTML: Add error message to "search issues on GitHub" link (#4652)
This PR adds the error message to the "search on Github" link located on
the crash page, so that the search bar is already filled and the issues
filtered when the user opens said link.

As seen with #4584 and other critical problems, duplicate issues end up
unnecessarily flooding the issue reports. While this change won't entirely
stop this behavior, this will help the user to easily identify if the error
they have received has been reported yet and discourage them from creating
a duplicate (hopefully...).

No associated issue was open
2024-10-08 17:51:36 +02:00
Samantaz Fox
82ac9a8609
Preferences: Add option to control preloading of video data (#4122)
This PR adds a configuration option to control the preloading of video data on
page load with the HTML5 'preload'[1] attribute on the `<video>` element.

The option is enabled by default, meaning that the `preload` attribute's value
will be 'auto'. If users want to prevent preloading of video data, they
can disable the option, which will set the attribute value to 'none'.

[1](https://www.w3schools.com/tags/att_video_preload.asp)

Closes issue 4110
2024-10-08 17:38:06 +02:00
Samantaz Fox
7c79ee7cc2
Performance: Improve speed of automatic instance redirection (#4193)
The automatic instance redirection implemented in #1940 fetches a new list of
instances each time someone queries the /redirect endpoint. This is extremely
inefficient...

This PR optimizes all that into a background job that only fetches a single
list every 30 minutes. This should performance quite a bit.

No related issue was opened.
2024-10-08 17:31:20 +02:00
ChunkyProgrammer
f6e09250cd
Use "LIVE" instead of "LIVE NOW" when parsing the live_now video badge
Co-authored-by: Samantaz Fox <coding@samantaz.fr>
2024-10-07 11:30:33 -04:00
Hosted Weblate
0fecde6917
Update Norwegian Bokmål translation
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Petter Reinholdtsen <pere-weblate@hungry.com>
2024-10-06 16:16:37 +02:00
Hosted Weblate
66f5b12ecd
Update Serbian (Cyrillic script) translation
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: NEXI <nexiphotographer@gmail.com>
2024-10-06 16:16:37 +02:00
Hosted Weblate
77f57714ea
Update Chinese (Simplified Han script) translation
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: 大王叫我来巡山 <hamburger2048@users.noreply.hosted.weblate.org>
2024-10-06 16:16:37 +02:00
Hosted Weblate
d9afe38504
Update Chinese (Traditional Han script) translation
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Jeff Huang <s8321414@gmail.com>
2024-10-06 16:16:36 +02:00
Hosted Weblate
3af11d800c
Update English (United States) translation
Co-authored-by: Dick Groskamp <dikgro@yahoo.co.uk>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
2024-10-06 16:16:36 +02:00
Hosted Weblate
d72531d843
Update Korean translation
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: xrfmkrh <rF3nMd7sRKezjF2vcEQo@protonmail.com>
2024-10-06 16:16:35 +02:00
Hosted Weblate
ecfcad8d1c
Update Albanian translation
Co-authored-by: Besnik Bleta <besnik@programeshqip.org>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
2024-10-06 16:16:35 +02:00
Hosted Weblate
d63b15dc1c
Update Serbian translation
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: NEXI <nexiphotographer@gmail.com>
2024-10-06 16:16:34 +02:00
Hosted Weblate
edb69d601e
Update Persian translation
Co-authored-by: Danial Behzadi <dani.behzi@ubuntu.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
2024-10-06 16:16:34 +02:00
Hosted Weblate
51562f4b24
Update Swedish translation
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: bittin1ddc447d824349b2 <bittin@reimu.nl>
2024-10-06 16:16:34 +02:00
Hosted Weblate
76f045b8d7
Update French translation
Co-authored-by: ABCraft19 <lesenfantsbergaoui@gmail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
2024-10-06 16:16:33 +02:00
Hosted Weblate
46eaa0f9b8
Update Spanish translation
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: gallegonovato <fran-carro@hotmail.es>
2024-10-06 16:16:33 +02:00
Hosted Weblate
56bccaba77
Update Dutch translation
Update Dutch translation

Update Dutch translation

Co-authored-by: Dick Groskamp <dikgro@yahoo.co.uk>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
2024-10-06 16:16:33 +02:00
Hosted Weblate
4e8d03221b
Update Interlingua translation
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Software In Interlingua <softinterlingua@gmail.com>
2024-10-06 16:16:32 +02:00
Hosted Weblate
5d46eba6f2
Update Arabic translation
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Rex_sa <rex.sa@pm.me>
2024-10-06 16:16:32 +02:00
Hosted Weblate
d3eedab545
Update Italian translation
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Random <random-r@users.noreply.hosted.weblate.org>
2024-10-06 16:16:31 +02:00
Hosted Weblate
cd43997bba
Update Polish translation
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Matthaiks <kitynska@gmail.com>
2024-10-06 16:16:31 +02:00
Hosted Weblate
fead7603e6
Update Croatian translation
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Milo Ivir <mail@milotype.de>
2024-10-06 16:16:31 +02:00
Hosted Weblate
486b5b363c
Update Icelandic translation
Co-authored-by: Sveinn í Felli <sv1@fellsnet.is>
2024-10-06 16:16:30 +02:00
Hosted Weblate
2b3619e489
Update Portuguese translation
Co-authored-by: Henrique Oliveira <ho.henrique@proton.me>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
2024-10-06 16:16:30 +02:00
Hosted Weblate
7a95cb43ef
Update Czech translation
Co-authored-by: Fjuro <fjuro@alius.cz>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
2024-10-06 16:16:29 +02:00
Hosted Weblate
e09a7de5c7
Update Japanese translation
Update Japanese translation

Co-authored-by: Himmel <Himmel@users.noreply.hosted.weblate.org>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: maboroshin <maboroshin@users.noreply.hosted.weblate.org>
2024-10-06 16:16:29 +02:00
Hosted Weblate
79d1aaff1a
Update Ukrainian translation
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Ihor Hordiichuk <igor_ck@outlook.com>
2024-10-06 16:16:29 +02:00
Hosted Weblate
d7a5ca8fff
Update Russian translation
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: lotigara <lotigara@yandex.ru>
2024-10-06 16:16:28 +02:00
Hosted Weblate
542d4fe553
Update Greek translation
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: hompre <46e989cc@opayq.com>
2024-10-06 16:16:28 +02:00
Hosted Weblate
33df8249f1
Update German translation
Update German translation

Co-authored-by: Ettore Atalan <atalanttore@googlemail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Lenny Angst <lenny@familie-angst.ch>
2024-10-06 16:16:27 +02:00
Hosted Weblate
4e7fd7ac3b
Update Portuguese (Brazil) translation
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Jose Delvani <jsdelvani@users.noreply.hosted.weblate.org>
2024-10-06 16:16:27 +02:00
Hosted Weblate
8912e2448d
Update Turkish translation
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Oğuz Ersen <oguz@ersen.moe>
2024-10-06 16:16:26 +02:00
ChunkyProgrammer
98f1e4170b Rename CCommons to ClosedCaptions 2024-09-30 22:02:57 -04:00
ChunkyProgrammer
b384133dc9 Fix tests 2024-09-30 22:02:57 -04:00
ChunkyProgrammer
1961fc3b11 switch to enum flag instead of adding lots of properties to SearchVideo 2024-09-30 22:02:57 -04:00
ChunkyProgrammer
2e649363d2 Parse more metadata badges for SearchVideos 2024-09-30 22:02:57 -04:00
TheFrenchGhosty
53e8a5d62d
Remove myself from CODEOWNERS on the config file (#4942) 2024-09-28 23:54:52 +02:00
Fijxu
17b525f2a6
Logger: colorize_logs false by default 2024-09-27 18:08:21 -03:00
absidue
b2a83991d1 Fix parsing live_now and premiere_timestamp 2024-09-20 18:46:00 +02:00
Fijxu
d77afdcf00
Logger: Make colorize_logs true by default 2024-09-20 00:32:27 -03:00
Fijxu
f8ec312328
Logger: Add color support for different log levels 2024-09-19 21:35:52 -03:00
Émilien (perso)
a021b93063
Update latest version WEB_CREATOR + fix comment web embed (#4930)
* Update to latest version WEB_CREATOR

* fix comment about using web embed as a fallback
2024-09-20 00:05:41 +00:00
Émilien (perso)
d9df90b5e3
use WEB_CREATOR when po_token with WEB_EMBED as a fallback (#4928)
* use WEB_CREATOR when po_token with WEB_EMBEDDED_PLAYER as a fallback

* remove unrelated comment

Co-authored-by: syeopite <70992037+syeopite@users.noreply.github.com>

---------

Co-authored-by: syeopite <70992037+syeopite@users.noreply.github.com>
2024-09-20 00:19:13 +02:00
Emilien Devos
cec3cfba77 Revert "use web screen embed for fixing potoken functionality (#4923)"
This reverts commit de918b9234b99f91a0a364fc675533147581eb2e.
The code doesn't work as expected. Reverting
2024-09-17 00:22:06 +02:00
Émilien (perso)
de918b9234
use web screen embed for fixing potoken functionality (#4923)
* use web screen embed for fixing potoken functionality

* use web screen embed only for getting streamingData + disable tv screen on po_token
2024-09-16 23:42:43 +02:00
Samantaz Fox
5e899d73a9
Search: Fix for youtu.be URL in sanitizer 2024-09-02 18:14:57 +02:00
Thomas Lange
f247b2f862
Update config/config.example.yml
Accept suggested change from @SamantazFox.

Co-authored-by: Samantaz Fox <coding@samantaz.fr>
2024-08-30 19:52:33 +02:00
Dmitry Sandalov
bd34659ff6
Fix 'invalid byte sequence' error when subscribing to playlists ([] accessor with range) 2024-08-29 22:47:59 +02:00
syeopite
f1baeef4bc
Ameba: Disable Style/RedundantNext rule 2024-08-28 23:49:10 -07:00
Dmitry Sandalov
157c4c3e98
Fix 'invalid byte sequence' error when subscribing to playlists 2024-08-28 23:54:31 +02:00
⛧-440729 [sophie]
3850739d7f
apply review suggestions 2024-08-27 10:48:34 +02:00
Samantaz Fox
4782a67038
Release v2.20240825.2 2024-08-26 22:52:50 +02:00
Samantaz Fox
5baaedfa39
CI: Fix docker container tags (#4883)
Closes issue 4880
2024-08-26 22:48:14 +02:00
Samantaz Fox
9d91ac3b88
Use snake case for all variables 2024-08-26 20:17:45 +00:00
Samantaz Fox
4f066e880c
CI: Fix docker container tags 2024-08-26 21:55:43 +02:00
Sophie Tauchert
5d0149844f
Batch user notifications together 2024-08-26 21:24:27 +02:00
syeopite
75b68618ab
Remove useless proc usage in images.cr 2024-08-24 19:47:42 -07:00
syeopite
003c6f81dc
Preserve connection close header of get_storyboard 2024-08-24 19:47:42 -07:00
syeopite
4bc77b81bf
Move YTIMG_POOLS to connection_pool.cr 2024-08-24 19:47:40 -07:00
syeopite
06e1a508e8
Fix headers not being added in image requests
Regression from #2364
2024-08-24 19:45:52 -07:00
syeopite
52bc9aa328
Refactor duplicate logic in image routes 2024-08-24 19:45:52 -07:00
syeopite
480e073fa9
Use HTTP pools for image requests to YouTube 2024-08-24 19:45:52 -07:00
syeopite
6e39b9b303
make_client: add YouTube headers on *.youtube.com 2024-08-24 19:41:39 -07:00
syeopite
46c58bd84c
Pool: Use force_resolve in fallback new client 2024-08-24 19:41:23 -07:00
syeopite
7521902e88
Ensure IP family is always used when force_resolve 2024-08-24 19:41:22 -07:00
syeopite
bd48af825c
Search API: Fix named arg syntax to make_client 2024-08-24 19:34:09 -07:00
syeopite
ee89db49ba
Typo
Co-authored-by: Samantaz Fox <coding@samantaz.fr>
2024-08-24 19:34:09 -07:00
syeopite
3af6681869
Fix typo in argument to make_client
Co-authored-by: ChunkyProgrammer <78101139+ChunkyProgrammer@users.noreply.github.com>
2024-08-24 19:34:09 -07:00
syeopite
1124dd645d
Use make_client instead of calling HTTP::Client
Using `make_client` to create `HTTP::Client`, allows for a simple way to
easily add logic to all `HTTP::Client` initialized within Invidious.
2024-08-24 19:34:09 -07:00
Samantaz Fox
b2133c6b2c
Videos: Convert URL before putting result into cache 2024-08-24 18:01:56 +02:00
RadoslavL
b526f48120 Changed Unix time to Rfc3339 time and removed NaN message 2024-08-16 23:57:49 +03:00
RadoslavL
e8cd631b2d Formatting 2024-08-16 14:13:05 +03:00
RadoslavL
69ff6def5f Removed useless variable 2024-08-16 14:11:28 +03:00
RadoslavL
26dc9dc99c Solution 2024-08-16 14:08:04 +03:00
RadoslavL
2d6b46c926 Fixed a really easy mistake 2024-08-16 14:05:13 +03:00
RadoslavL
cab02d4959 Corrected usage of publishedText variable throughout the code 2024-08-16 13:54:27 +03:00
Krystof Pistek
5f590dda80
Carry over audio-only mode in playlist links 2024-08-07 20:58:08 +02:00
syeopite
b2f5b1eb68
Add logic to fetch transcripts from label
Although available this method should be discouraged as it requires
an extra request to YouTube to get caption data in order to
map label -> language code and auto-generated status, which are needed
to fetch transcripts.
2024-07-11 09:37:18 -07:00
syeopite
7693f61e44
Add API endpoint to fetch YouTube transcripts 2024-07-11 09:37:17 -07:00
PMK
7214fdaff4
JS: Update timeupdate event defensive to prevent errors 2024-07-06 21:39:00 +02:00
syeopite
c24ed85110
Fix named arg syntax when passing force_resolve 2024-06-16 14:49:48 -07:00
giacomocerquone
288e1dccda Fix player menus hiding onHover 2024-06-13 01:10:35 +02:00
syeopite
9980c0e00f
Update uptime logic to handle updown.io response 2024-05-22 13:28:15 -07:00
syeopite
aa96cf3453
Fix invalid logic for instance uptime comparison 2024-05-22 13:22:00 -07:00
syeopite
41c978d350
Use HTTP::Client directly in instance list job
The HTTP::Client created via `make_client` is affected by the
force_resolve configuration option. However, api.invidious.io
does not support ipv6 and as such any request with ipv6 to
api.invidious.io will instead raise.

Directly calling the HTTP::Client will ignore the force_resolve option
allowing requests to go through ipv4 when needed.
2024-05-22 13:22:00 -07:00
syeopite
cff25a7b25
Refactor instance fetching logic into separate job 2024-05-22 13:22:00 -07:00
syeopite
6b7e730100
Validate override for crystal 1.12.1 2024-05-22 13:10:46 -07:00
syeopite
ccb2a6c58e
Bump http_proxy to v0.10.3 2024-04-28 21:34:05 -07:00
syeopite
3b471ae964
Automatically initialize proxy via stdlib override 2024-04-28 19:43:22 -07:00
syeopite
eb8fcc9e88
Add support for using HTTP proxies 2024-04-28 19:43:17 -07:00
tracedgod
5b11ca22d0 Use string interpolation instead of concatenation 2024-04-28 00:04:30 -04:00
tracedgod
6db4a46c5f update the url_search_issues variable to search for the current error on GitHub instead of showing all issues 2024-04-26 16:01:02 -04:00
RadoslavL
7b7197cde8 retrigger checks 2024-04-22 16:26:49 +03:00
RadoslavL
3c6019edd0 retrigger checks 2024-04-22 16:20:11 +03:00
RadoslavL
6861148290 Moved code around and fixed a problem 2023-11-24 11:24:56 +02:00
RadoslavL
03f9962a47 This should work 2023-11-14 10:00:18 +02:00
RadoslavL
d098e5ae9b I hope it works at this point 2023-11-14 09:58:37 +02:00
RadoslavL
4c486634e2 Another attempt at fixing the issue 2023-11-14 09:56:06 +02:00
RadoslavL
3bced4e12b Fixed another issue 2023-11-14 09:51:12 +02:00
RadoslavL
0d22af6564 Moved methods around 2023-11-14 09:47:16 +02:00
RadoslavL
2a6a32e667 Fixed an issue 2023-11-14 09:43:52 +02:00
RadoslavL
50da6cf3e7
Organize the code better
Co-authored-by: syeopite <70992037+syeopite@users.noreply.github.com>
2023-11-12 20:52:11 +02:00
RadoslavL
7388e4ca72
Add translation to the publishedText parameter
Co-authored-by: syeopite <70992037+syeopite@users.noreply.github.com>
2023-11-12 20:51:33 +02:00
RadoslavL
be216fff94 Added the text version of the published parameter 2023-11-12 08:37:13 +02:00
RadoslavL
019807256f Seperated repetitive code in a function 2023-11-09 21:56:41 +02:00
RadoslavL
a0d24190b8 Made published be an optional parameter 2023-11-08 19:09:16 +02:00
RadoslavL
2b2d67fcfa
Fixed a typo
Co-authored-by: syeopite <70992037+syeopite@users.noreply.github.com>
2023-11-08 11:48:32 +02:00
RadoslavL
76369eb599 Removed unused attribute 2023-11-08 10:18:29 +02:00
RadoslavL
6236cea33e
Changed some variable names
Co-authored-by: syeopite <70992037+syeopite@users.noreply.github.com>
2023-11-08 10:13:16 +02:00
RadoslavL
e8c2388589 Removed the purging of the query parameters 2023-10-26 11:30:12 +03:00
RadoslavL
995df2d296 Removed a space 2023-10-22 17:50:39 +03:00
RadoslavL
c0d75bc52f Removed <noscript> and the user preferences option 2023-10-22 13:54:35 +03:00
RadoslavL
e307fcc9a1 Fixed an issue 2023-10-20 09:00:23 +03:00
RadoslavL
bae8bab3ff
Remove unnecessary code 2023-10-15 00:06:37 +03:00
RadoslavL
fa59f41f7b Fixed an issue 2023-10-11 09:12:27 +03:00
RadoslavL
20ca1ebcc0 Used the decode_date function instead 2023-10-11 09:08:23 +03:00
RadoslavL
b0b4f09b3a Seperated the code in a function 2023-10-09 12:26:38 +03:00
RadoslavL
48af0af9d5 Added minutes as well 2023-10-09 12:18:50 +03:00
RadoslavL
f9460e31bc Fixed an issue 2023-10-09 12:09:03 +03:00
RadoslavL
b7a252b096 Removed need for more API calls by parsing the publishedTimeText string 2023-10-09 12:00:37 +03:00
RadoslavL
6b929da0e1 Added a 'published' video parameter 2023-10-07 16:57:47 +03:00
RadoslavL
21122db3a7 Fixed an issue 2023-09-30 19:27:06 +03:00
RadoslavL
c9a843c7fe Replaced to_json with to_pretty_json 2023-09-30 19:11:42 +03:00
RadoslavL
275501aad3 Actually add the pagination.js file (git didn't detect it the first time) 2023-09-30 19:01:48 +03:00
RadoslavL
5cdbc184c7 Added a previous_page_button preference option and made switching between the first page and previous page buttons possible 2023-09-30 18:36:43 +03:00
RadoslavL
9996d00cb1 Fixed a problem 2023-09-27 19:49:00 +03:00
RadoslavL
9a617ae087 Fixed problem 2023-09-27 19:46:47 +03:00
RadoslavL
c257882a1f Removed a tab 2023-09-27 19:35:40 +03:00
RadoslavL
58bad6180f Changed first_page type to Bool 2023-09-27 19:22:34 +03:00
RadoslavL
509bace7d1 Removed a space 2023-09-27 19:05:44 +03:00
Thomas Lange
824cc1a5aa Don't redefine the "preload" option in player.js
If the HTML5 "<video>" element defines the "preload" attribute directly,
it isn't necessary to redefine the "preload" option in the player.js.
2023-09-27 15:36:40 +02:00
RadoslavL
07c52cba3d
Fixed an issue with tabs 2023-09-27 15:05:17 +03:00
RadoslavL
04ba7b0d58
Fix more issues related to tabs 2023-09-27 14:22:51 +03:00
RadoslavL
4788a3b4a9 Removed unnecessary spaces 2023-09-27 11:45:02 +03:00
RadoslavL
7fe2af735d Included the check for RTL languages 2023-09-27 11:37:01 +03:00
RadoslavL
905582db66 Added a first page button 2023-09-27 11:28:47 +03:00
Thomas Lange
bf470704a5 Add option to control preloading of video data
Fix #4110 by adding an option to control the preloading of video data on
page load. If disabled ("false"), the browser will not preload any video
data until the user explicitly hits the "Play" button.

If enabled ("true"), the default behavior will be used, which means the
browser decides how much of the video will be preloaded.
2023-09-26 22:21:12 +02:00
Emilien Devos
78773d7326 add the ability to listen on unix sockets 2021-05-24 23:41:14 +02:00
120 changed files with 2824 additions and 939 deletions

View File

@ -38,6 +38,9 @@ Style/RedundantBegin:
Style/RedundantReturn:
Enabled: false
Style/RedundantNext:
Enabled: false
Style/ParenthesesAroundCondition:
Enabled: false

2
.github/CODEOWNERS vendored
View File

@ -6,7 +6,7 @@ docker/ @unixfox
kubernetes/ @unixfox
README.md @thefrenchghosty
config/config.example.yml @thefrenchghosty @SamantazFox @unixfox
config/config.example.yml @SamantazFox @unixfox
scripts/ @syeopite
shards.lock @syeopite

View File

@ -10,8 +10,10 @@ assignees: ''
<!--
BEFORE TRYING TO REPORT A BUG:
* Read the FAQ!
* Use the search function to check if there is already an issue open for your problem!
* Read the FAQ: https://docs.invidious.io/faq/!
* Use the search function to check if there is already an issue open for your problem: https://github.com/search?q=repo%3Aiv-org%2Finvidious+replace+me+with+your+bug&type=issues!
MAKE SURE TO FOLLOW THE TWO STEPS ABOVE BEFORE REPORTING A BUG. A BUG THAT ALREADY EXIST WILL IMMEDIATELY CLOSED.
If you want to suggest a new feature please use "Feature request" instead
If you want to suggest an enhancement to an existing feature please use "Enhancement" instead

View File

@ -23,19 +23,6 @@ jobs:
- name: Checkout
uses: actions/checkout@v4
- name: Install Crystal
uses: crystal-lang/install-crystal@v1.8.2
with:
crystal: 1.12.2
- name: Run lint
run: |
if ! crystal tool format --check; then
crystal tool format
git diff
exit 1
fi
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
with:

View File

@ -14,19 +14,6 @@ jobs:
- name: Checkout
uses: actions/checkout@v4
- name: Install Crystal
uses: crystal-lang/install-crystal@v1.8.2
with:
crystal: 1.12.2
- name: Run lint
run: |
if ! crystal tool format --check; then
crystal tool format
git diff
exit 1
fi
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
with:
@ -47,9 +34,11 @@ jobs:
uses: docker/metadata-action@v5
with:
images: quay.io/invidious/invidious
flavor: |
latest=false
tags: |
type=semver,pattern={{version}}
type=raw,value=latest,enable=${{ github.ref == format('refs/heads/{0}', 'master') }}
type=raw,value=latest
labels: |
quay.expires-after=12w
@ -71,10 +60,11 @@ jobs:
with:
images: quay.io/invidious/invidious
flavor: |
latest=false
suffix=-arm64
tags: |
type=semver,pattern={{version}}
type=raw,value=latest,enable=${{ github.ref == format('refs/heads/{0}', 'master') }}
type=raw,value=latest
labels: |
quay.expires-after=12w

View File

@ -38,10 +38,10 @@ jobs:
matrix:
stable: [true]
crystal:
- 1.9.2
- 1.10.1
- 1.11.2
- 1.12.1
- 1.13.2
- 1.14.0
- 1.15.0
include:
- crystal: nightly
stable: false
@ -51,6 +51,11 @@ jobs:
with:
submodules: true
- name: Install required APT packages
run: |
sudo apt install -y libsqlite3-dev
shell: bash
- name: Install Crystal
uses: crystal-lang/install-crystal@v1.8.0
with:
@ -59,7 +64,9 @@ jobs:
- name: Cache Shards
uses: actions/cache@v3
with:
path: ./lib
path: |
./lib
./bin
key: shards-${{ hashFiles('shard.lock') }}
- name: Install Shards
@ -71,14 +78,6 @@ jobs:
- name: Run tests
run: crystal spec
- name: Run lint
run: |
if ! crystal tool format --check; then
crystal tool format
git diff
exit 1
fi
- name: Build
run: crystal build --warnings all --error-on-warnings --error-trace src/invidious.cr
@ -124,14 +123,19 @@ jobs:
- name: Test Docker
run: while curl -Isf http://localhost:3000; do sleep 1; done
ameba_lint:
lint:
runs-on: ubuntu-latest
continue-on-error: true
steps:
- uses: actions/checkout@v4
with:
submodules: true
- name: Install Crystal
id: lint_step_install_crystal
uses: crystal-lang/install-crystal@v1.8.0
with:
crystal: latest
@ -142,10 +146,21 @@ jobs:
path: |
./lib
./bin
key: shards-${{ hashFiles('shard.lock') }}
key: shards-${{ hashFiles('shard.lock') }}-${{ steps.lint_step_install_crystal.outputs.crystal }}
- name: Install Shards
run: shards install
run: |
if ! shards check; then
shards install
fi
- name: Check Crystal formatter compliance
run: |
if ! crystal tool format --check; then
crystal tool format
git diff
exit 1
fi
- name: Run Ameba linter
run: bin/ameba

View File

@ -13,14 +13,11 @@ jobs:
- uses: actions/stale@v8
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
days-before-stale: 365
days-before-pr-stale: 90
days-before-close: 30
exempt-pr-labels: blocked,exempt-stale
days-before-stale: 730
days-before-pr-stale: -1
days-before-close: 60
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-issue-label: "stale"
stale-pr-label: "stale"
ascending: true
# Never mark feature requests/enhancements as stale
exempt-issue-labels: "feature-request,enhancement,exempt-stale"
# Exempt the following types of issues from being staled
exempt-issue-labels: "feature-request,enhancement,discussion,exempt-stale"

View File

@ -1,5 +1,257 @@
# CHANGELOG
## vX.Y.0 (future)
## v2.20250314.0
### Wrap-up
This release brings the long awaited feature of supporting multiple audio tracks in a video, some bug fixes and UX improvements, and many other things primarily oriented to self-hosting instances, and developers using the API.
The `Community` channel tab has been replaced by `Posts` in light of YouTube changes, but the URL remains the same.
Tamil is now available as an interface language
Automatic instance redirects will no longer have the chance to annoyingly redirect to the same instance you're on.
Due to their requirements for video playback, Invidious will log warning messages when either inv-sig-helper, `po_token` or `visitor_data` is not configured
Invidious is now able to listen through a UNIX socket
User notifications are now batched for each channel
**The minimum Crystal version supported by Invidious now `1.12.0`**
### New features & important changes
#### For users
* Invidious now supports videos with multiple audio tracks allowing you to select which one you want to hear with!
* Channel pages now have a proper previous page button
* RSS feeds for channels will no longer contain the channel's profile picture
* Support for channel `courses` page has been added
* `Community` tabs has been replaced with `Posts` to comply with YouTube changes
* Tamil is now an available interface language.
#### For instance owners
* Invidious is now able to listen on a UNIX socket
* User notifications are now batched by channels, significantly reducing database load.
* **`1.12.0` is now the oldest Crystal version that Invidious supports**
* The example config will no longer force an http proxy to be configured
* Invidious will now warn when any top-level config option must be set to a custom value, instead of just `HMAC_KEY`
* Due to their requirements for video playback, Invidious will log warning messages when either inv-sig-helper, `po_token` or `visitor_data` is not configured
#### For developers
* Invidious is now compliant to Crystal 1.15 formatting rules, which are incompatible with earlier versions.
* `/api/v1/transcripts/{id}` has been added to the API to allow for fetching the transcripts for a video. The arguments are the same as the captions endpoint.
* `author_thumbnail` field has been added to videos in the various paged api endpoints
* `published` field has been added to the API response for a video's related videos.
* Docker builds now uses the Crystal compiler cache, reducing build times on repeated builds significantly.
* Invidious ajax action handlers has undergone a clean up and may face compatibility issues with code that depends on these endpoints.
* The versions of Crystal that we test in CI/CD are now: `1.12.1`, `1.13.2`, `1.14.0`, `1.15.0`
### Bugs fixed
#### User-side
* Local video listen mode is now preserved when clicking on a video in the sidebar playlist widget
* Automatic instance redirects will no longer redirect to the same instance the user is on
* Fix some thumbnails responses returning 404
* Videos: Fix missing host parameter on playback URLs when `local=true`
* Fix HLS being used for non-livestream videos
* Fix timeupdate event errors when required elements are missing
* User: Ensure IO is properly closed when importing NewPipe subscriptions
#### For instance owners
* Fix http proxy configuration being forced by the standard example config
#### API
* `/api/v1/videos/{id}` will no longer return an occasional empty JSON response
### Full list of pull requests merged since the last release (newest first)
* Make Invidious compliant to Crystal 1.15 formatting rules (https://github.com/iv-org/invidious/pull/5014, by @syeopite)
* Remove formatter check on container workflows (https://github.com/iv-org/invidious/pull/5153, by @syeopite)
* Videos: Fix missing host parameter on playback URLs when `local=true` (https://github.com/iv-org/invidious/pull/4992, by @SamantazFox)
* Remove stdlib override for proxy initialization (https://github.com/iv-org/invidious/pull/5065, by @syeopite)
* Add support for author thumbnails in search api for videos (https://github.com/iv-org/invidious/pull/5072, thanks @ChunkyProgrammer)
* Skip route if resp got closed by before handlers (https://github.com/iv-org/invidious/pull/5073, by @syeopite)
* Fix video thumbnails in mixes (https://github.com/iv-org/invidious/pull/5116, thanks @iBicha)
* CI: Drop support for versions prior to 1.12 and add 1.15.0 (https://github.com/iv-org/invidious/pull/5148, by @syeopite)
* [Continuing #5094] Set language info for dash audio streams and sort (https://github.com/iv-org/invidious/pull/5149, thanks @giuliano-macedo)
* Warn when any top-level config is "CHANGE_ME!!" (https://github.com/iv-org/invidious/pull/5150, by @syeopite)
* Comment out http_proxy in example config (https://github.com/iv-org/invidious/pull/5151, by @syeopite)
* API: Add a 'published' video parameter for related videos (https://github.com/iv-org/invidious/pull/4149, thanks @RadoslavL)
* Ensure IO is properly closed when importing NewPipe subscriptions (https://github.com/iv-org/invidious/pull/4346, thanks @ChunkyProgrammer)
* Carry over audio-only mode in playlist links (https://github.com/iv-org/invidious/pull/4784, thanks @krystof1119)
* Routes: Clean ajax actions handlers (https://github.com/iv-org/invidious/pull/5036, by @SamantazFox)
* Frontend: Add a first page and previous page buttons for channel navigation (https://github.com/iv-org/invidious/pull/4123, thanks @RadoslavL)
* RSS: Channel + Playlist improvements (https://github.com/iv-org/invidious/pull/4298, thanks @ChunkyProgrammer)
* Batch user notifications together (https://github.com/iv-org/invidious/pull/4486, thanks @999eagle)
* JS: Update timeupdate event making it more defensive to prevent errors (https://github.com/iv-org/invidious/pull/4782, thanks @PMK)
* Add API endpoint for fetching transcripts from YouTube by (https://github.com/iv-org/invidious/pull/4788, by @syeopite)
* Translations update from Hosted Weblate by (https://github.com/iv-org/invidious/pull/4989, thanks to our many translators)
* Add the ability to listen on UNIX sockets (https://github.com/iv-org/invidious/pull/5112, thanks @Caian)
* Pick a different instance upon redirect (https://github.com/iv-org/invidious/pull/5154, thanks @epicsam123)
* Add Courses to channel page and channel API (https://github.com/iv-org/invidious/pull/5158, thanks @ChunkyProgrammer)
* fix /api/v1/videos/:id returns 200 with no content (https://github.com/iv-org/invidious/pull/5162, thanks @Drikanis)
* Use Crystal compiler cache in docker builds (https://github.com/iv-org/invidious/pull/5163, by @syeopite)
* Channels: Fix community tab by (https://github.com/iv-org/invidious/pull/5183, thanks @Fijxu)
* Fix typo in `src/invidious/routes/images.cr` (https://github.com/iv-org/invidious/pull/5184, by @syeopite)
* Fix an issue with the HLS manifest check for livestream videos (https://github.com/iv-org/invidious/pull/5189, thanks @alexmaras)
* Warn when `po_token`, `visitor_data` and/or `inv-sig-helper` is not configured (https://github.com/iv-org/invidious/pull/5202, by @syeopite)
## v2.20241110.0
### Wrap-up
This release is most importantly here to fix to the annoying "Youtube API returned error 400"
error that prevented all channel pages from loading.
If you're updating from the previous release, it provides no improvements on the ability to play
videos. If updating from a commit in-between release, it removes the "Please sign in" error caused
by a previous attempt at restoring video playback on large instances.
In the preferences, a new option allows for control of video preload. When enabled, this option
tells the browser to load the video as soon as the page is loaded (this used to be the default).
When disabled, the video starts loading only when the "play" button is pressed.
New interface languages available: Bulgarian, Welsh and Lombard
New dependency required: `tzdata`.
An HTTP proxy can be configured directly in Invidious, if needed. \
**NOTE:** In that case, it is recommended to comment out `force_resolve`.
### New features & important changes
#### For users
* Channels: Fix "Youtube API returned error 400" error preventing channel pages from loading
* Channels: Shorts can now be sorted by "newest", "oldest" and "popular"
* Preferences: Addition of the new "preload" option
* New interface languages available: Bulgarian, Welsh and Lombard
* Added "Filipino (auto-generated)" to the list of caption languages available
* Lots of new translations from Weblate
#### For instance owners
* Allow the configuration of an HTTP proxy to talk to Youtube
* Invidious tries to reconnect to `inv_sig_helper` if the socket is closed
* The instance list is downloaded in the background to improve redirection speed
* New `colorize_logs` option makes each log level a different color
#### For developpers
* `/api/v1/channels/{id}/shorts` now supports the `sort-by` parameter with the following values:
`newest`, `oldest` and `popular`
* Older `/api/v1/channels/xyz/{id}` (tab name before UCID) were removed
* API/Search: New video metadata available: `isNew`, `is4k`, `is8k`, `isVr180`, `isVr360`,
`is3d` and `hasCaptions`
### Bugs fixed
#### User-side
* Channels: The second page of shorts now loads as expected
* Channels: Fixed intermittent empty "playlists" tab
* Search: Fixed `youtu.be` URLs not being properly redirected to the watch page
* Fixed `DB::MappingException` error on the subscriptions feed (due to missing `tzdata` in docker)
* Switching to another instance is much faster
* Fixed an "invalid byte sequence" error when subscribing to a playlist
* Videos: Playback URLs were sometimes broken when cached and `inv_sig_helper` was used
#### For instance owners
* Fix `force_resolve` being ignored in some cases
#### API
* API/Videos: Fixed `live_now` and `premiere_timestamp` sometimes not having the right values
### Full list of pull requests merged since the last release (newest first)
* API: Add "sort_by" parameter to channels/shorts endpoint ([#5071], thanks @iBicha)
* Docker: Install tzdata in Dockerfile ([#5070], by @SamantazFox)
* Videos: Stop using TVHTML5_SIMPLY_EMBEDDED_PLAYER ([#5063], thanks @unixfox)
* Routing: Deprecate old channel API routes ([#5045], by @SamantazFox)
* Videos: use WEB client instead of WEB CREATOR ([#4984], thanks @unixfox)
* Parsers: Fix parsing live_now and premiere_timestamp ([#4934], thanks @absidue)
* Stale bot updates ([#5060], thanks @syeopite)
* Channels: Fix "Youtube API returned error 400" ([#5059], by @SamantazFox)
* Channels: Fix for live videos ([#5027], thanks @iBicha)
* Locales: Add Bulgarian, Welsh and Lombard to the list ([#5046], by @SamantazFox)
* Shards: Update database dependencies ([#5034], by @SamantazFox)
* Logger: Add color support for different log levels ([#4931], thanks @Fijxu)
* Fix named arg syntax when passing force_resolve ([#4754], thanks @syeopite)
* Use make_client instead of calling HTTP::Client ([#4709], thanks @syeopite)
* Add "Filipino (auto-generated)" to the list of caption languages ([#4995], by @SamantazFox)
* Makefile: Add MT option to enable the 'preview_mt' flag ([#4993], by @SamantazFox)
* SigHelper: Reconnect to signature helper ([#4991], thanks @Fijxu)
* Fix player menus hiding onHover ready ([#4750], thanks @giacomocerquone)
* Use connection pools when requesting images from YouTube ([#4326], thanks @syeopite)
* Add support for using Invidious through a HTTP Proxy ([#4270], thanks @syeopite)
* Search: Fix 'youtu.be' URLs in sanitizer ([#4894], by @SamantazFox)
* Ameba: Disable Style/RedundantNext rule ([#4888], thanks @syeopite)
* Playlists: Fix 'invalid byte sequence' error when subscribing ([#4887], thanks @DmitrySandalov)
* Parse more metadata badges for SearchVideos ([#4863], thanks @ChunkyProgrammer)
* Translations update from Hosted Weblate ([#4862], thanks to our many translators)
* Videos: Convert URL before putting result into cache ([#4850], by @SamantazFox)
* HTML: Add error message to "search issues on GitHub" link ([#4652], thanks @tracedgod)
* Preferences: Add option to control preloading of video data ([#4122], thanks @Nerdmind)
* Performance: Improve speed of automatic instance redirection ([#4193], thanks @syeopite)
* Remove myself from CODEOWNERS on the config file ([#4942], by @TheFrenchGhosty)
* Update latest version WEB_CREATOR + fix comment web embed ([#4930], thanks @unixfox)
* use WEB_CREATOR when po_token with WEB_EMBED as a fallback ([#4928], thanks @unixfox)
* Revert "use web screen embed for fixing potoken functionality"
* use web screen embed for fixing potoken functionality ([#4923], thanks @unixfox)
[#4122]: https://github.com/iv-org/invidious/pull/4122
[#4193]: https://github.com/iv-org/invidious/pull/4193
[#4270]: https://github.com/iv-org/invidious/pull/4270
[#4326]: https://github.com/iv-org/invidious/pull/4326
[#4652]: https://github.com/iv-org/invidious/pull/4652
[#4709]: https://github.com/iv-org/invidious/pull/4709
[#4750]: https://github.com/iv-org/invidious/pull/4750
[#4754]: https://github.com/iv-org/invidious/pull/4754
[#4850]: https://github.com/iv-org/invidious/pull/4850
[#4862]: https://github.com/iv-org/invidious/pull/4862
[#4863]: https://github.com/iv-org/invidious/pull/4863
[#4887]: https://github.com/iv-org/invidious/pull/4887
[#4888]: https://github.com/iv-org/invidious/pull/4888
[#4894]: https://github.com/iv-org/invidious/pull/4894
[#4923]: https://github.com/iv-org/invidious/pull/4923
[#4928]: https://github.com/iv-org/invidious/pull/4928
[#4930]: https://github.com/iv-org/invidious/pull/4930
[#4931]: https://github.com/iv-org/invidious/pull/4931
[#4934]: https://github.com/iv-org/invidious/pull/4934
[#4942]: https://github.com/iv-org/invidious/pull/4942
[#4984]: https://github.com/iv-org/invidious/pull/4984
[#4991]: https://github.com/iv-org/invidious/pull/4991
[#4993]: https://github.com/iv-org/invidious/pull/4993
[#4995]: https://github.com/iv-org/invidious/pull/4995
[#5027]: https://github.com/iv-org/invidious/pull/5027
[#5034]: https://github.com/iv-org/invidious/pull/5034
[#5045]: https://github.com/iv-org/invidious/pull/5045
[#5046]: https://github.com/iv-org/invidious/pull/5046
[#5059]: https://github.com/iv-org/invidious/pull/5059
[#5060]: https://github.com/iv-org/invidious/pull/5060
[#5063]: https://github.com/iv-org/invidious/pull/5063
[#5070]: https://github.com/iv-org/invidious/pull/5070
[#5071]: https://github.com/iv-org/invidious/pull/5071
## v2.20240825.2 (2024-08-26)
This releases fixes the container tags pushed on quay.io.
Previously, the ARM64 build was released under the `latest` tag, instead of `latest-arm64`.
### Full list of pull requests merged since the last release (newest first)
CI: Fix docker container tags ([#4883], by @SamantazFox)
[#4877]: https://github.com/iv-org/invidious/pull/4877
## v2.20240825.1 (2024-08-25)
Add patch component to be [semver] compliant and make github actions happy.
@ -8,8 +260,9 @@ Add patch component to be [semver] compliant and make github actions happy.
### Full list of pull requests merged since the last release (newest first)
Allow manual trigger of release-container build (#4877, thanks @syeopite)
Allow manual trigger of release-container build ([#4877], thanks @syeopite)
[#4877]: https://github.com/iv-org/invidious/pull/4877
## v2.20240825.0 (2024-08-25)

View File

@ -7,6 +7,11 @@ STATIC := 0
NO_DBG_SYMBOLS := 0
# Enable multi-threading.
# Warning: Experimental feature!!
# invidious is not stable when MT is enabled.
MT := 0
FLAGS ?=
@ -19,6 +24,10 @@ ifeq ($(STATIC), 1)
FLAGS += --static
endif
ifeq ($(MT), 1)
FLAGS += -Dpreview_mt
endif
ifeq ($(NO_DBG_SYMBOLS), 1)
FLAGS += --no-debug

View File

@ -68,6 +68,7 @@
.video-js.player-style-youtube .vjs-menu-button-popup .vjs-menu {
margin-bottom: 2em;
padding-top: 2em
}
.video-js.player-style-youtube .vjs-progress-control .vjs-progress-holder, .video-js.player-style-youtube .vjs-progress-control {height: 5px;

View File

@ -91,7 +91,7 @@
var count = document.getElementById('count');
count.textContent--;
var url = '/token_ajax?action_revoke_token=1&redirect=false' +
var url = '/token_ajax?action=revoke_token&redirect=false' +
'&referer=' + encodeURIComponent(location.href) +
'&session=' + target.getAttribute('data-session');
@ -111,7 +111,7 @@
var count = document.getElementById('count');
count.textContent--;
var url = '/subscription_ajax?action_remove_subscriptions=1&redirect=false' +
var url = '/subscription_ajax?action=remove_subscriptions&redirect=false' +
'&referer=' + encodeURIComponent(location.href) +
'&c=' + target.getAttribute('data-ucid');

93
assets/js/pagination.js Normal file
View File

@ -0,0 +1,93 @@
'use strict';
const CURRENT_CONTINUATION = (new URL(document.location)).searchParams.get("continuation");
const CONT_CACHE_KEY = `continuation_cache_${encodeURIComponent(window.location.pathname)}`;
function get_data(){
return JSON.parse(sessionStorage.getItem(CONT_CACHE_KEY)) || [];
}
function save_data(){
const prev_data = get_data();
prev_data.push(CURRENT_CONTINUATION);
sessionStorage.setItem(CONT_CACHE_KEY, JSON.stringify(prev_data));
}
function button_press(){
let prev_data = get_data();
if (!prev_data.length) return null;
// Sanity check. Nowhere should the current continuation token exist in the cache
// but it can happen when using the browser's back feature. As such we'd need to travel
// back to the point where the current continuation token first appears in order to
// account for the rewind.
const conflict_at = prev_data.indexOf(CURRENT_CONTINUATION);
if (conflict_at != -1) {
prev_data.length = conflict_at;
}
const prev_ctoken = prev_data.pop();
// On the first page, the stored continuation token is null.
if (prev_ctoken === null) {
sessionStorage.removeItem(CONT_CACHE_KEY);
let url = set_continuation();
window.location.href = url;
return;
}
sessionStorage.setItem(CONT_CACHE_KEY, JSON.stringify(prev_data));
let url = set_continuation(prev_ctoken);
window.location.href = url;
};
// Method to set the current page's continuation token
// Removes the continuation parameter when a continuation token is not given
function set_continuation(prev_ctoken = null){
let url = window.location.href.split('?')[0];
let params = window.location.href.split('?')[1];
let url_params = new URLSearchParams(params);
if (prev_ctoken) {
url_params.set("continuation", prev_ctoken);
} else {
url_params.delete('continuation');
};
if(Array.from(url_params).length > 0){
return `${url}?${url_params.toString()}`;
} else {
return url;
}
}
addEventListener('DOMContentLoaded', function(){
const pagination_data = JSON.parse(document.getElementById('pagination-data').textContent);
const next_page_containers = document.getElementsByClassName("page-next-container");
for (let container of next_page_containers){
const next_page_button = container.getElementsByClassName("pure-button")
// exists?
if (next_page_button.length > 0){
next_page_button[0].addEventListener("click", save_data);
}
}
// Only add previous page buttons when not on the first page
if (CURRENT_CONTINUATION) {
const prev_page_containers = document.getElementsByClassName("page-prev-container")
for (let container of prev_page_containers) {
if (pagination_data.is_rtl) {
container.innerHTML = `<button class="pure-button pure-button-secondary">${pagination_data.prev_page}&nbsp;&nbsp;<i class="icon ion-ios-arrow-forward"></i></button>`
} else {
container.innerHTML = `<button class="pure-button pure-button-secondary"><i class="icon ion-ios-arrow-back"></i>&nbsp;&nbsp;${pagination_data.prev_page}</button>`
}
container.getElementsByClassName("pure-button")[0].addEventListener("click", button_press);
}
}
});

View File

@ -3,7 +3,6 @@ var player_data = JSON.parse(document.getElementById('player_data').textContent)
var video_data = JSON.parse(document.getElementById('video_data').textContent);
var options = {
preload: 'auto',
liveui: true,
playbackRates: [0.25, 0.5, 0.75, 1.0, 1.25, 1.5, 1.75, 2.0],
controlBar: {
@ -135,26 +134,32 @@ player.on('timeupdate', function () {
// YouTube links
let elem_yt_watch = document.getElementById('link-yt-watch');
if (elem_yt_watch) {
let base_url_yt_watch = elem_yt_watch.getAttribute('data-base-url');
elem_yt_watch.href = addCurrentTimeToURL(base_url_yt_watch);
}
let elem_yt_embed = document.getElementById('link-yt-embed');
let base_url_yt_watch = elem_yt_watch.getAttribute('data-base-url');
let base_url_yt_embed = elem_yt_embed.getAttribute('data-base-url');
elem_yt_watch.href = addCurrentTimeToURL(base_url_yt_watch);
elem_yt_embed.href = addCurrentTimeToURL(base_url_yt_embed);
if (elem_yt_embed) {
let base_url_yt_embed = elem_yt_embed.getAttribute('data-base-url');
elem_yt_embed.href = addCurrentTimeToURL(base_url_yt_embed);
}
// Invidious links
let domain = window.location.origin;
let elem_iv_embed = document.getElementById('link-iv-embed');
if (elem_iv_embed) {
let base_url_iv_embed = elem_iv_embed.getAttribute('data-base-url');
elem_iv_embed.href = addCurrentTimeToURL(base_url_iv_embed, domain);
}
let elem_iv_other = document.getElementById('link-iv-other');
let base_url_iv_embed = elem_iv_embed.getAttribute('data-base-url');
let base_url_iv_other = elem_iv_other.getAttribute('data-base-url');
elem_iv_embed.href = addCurrentTimeToURL(base_url_iv_embed, domain);
elem_iv_other.href = addCurrentTimeToURL(base_url_iv_other, domain);
if (elem_iv_other) {
let base_url_iv_other = elem_iv_other.getAttribute('data-base-url');
elem_iv_other.href = addCurrentTimeToURL(base_url_iv_other, domain);
}
});

View File

@ -6,7 +6,7 @@ function add_playlist_video(target) {
var select = target.parentNode.children[0].children[1];
var option = select.children[select.selectedIndex];
var url = '/playlist_ajax?action_add_video=1&redirect=false' +
var url = '/playlist_ajax?action=add_video&redirect=false' +
'&video_id=' + target.getAttribute('data-id') +
'&playlist_id=' + option.getAttribute('data-plid');
@ -21,7 +21,7 @@ function add_playlist_item(target) {
var tile = target.parentNode.parentNode.parentNode.parentNode.parentNode;
tile.style.display = 'none';
var url = '/playlist_ajax?action_add_video=1&redirect=false' +
var url = '/playlist_ajax?action=add_video&redirect=false' +
'&video_id=' + target.getAttribute('data-id') +
'&playlist_id=' + target.getAttribute('data-plid');
@ -36,7 +36,7 @@ function remove_playlist_item(target) {
var tile = target.parentNode.parentNode.parentNode.parentNode.parentNode;
tile.style.display = 'none';
var url = '/playlist_ajax?action_remove_video=1&redirect=false' +
var url = '/playlist_ajax?action=remove_video&redirect=false' +
'&set_video_id=' + target.getAttribute('data-index') +
'&playlist_id=' + target.getAttribute('data-plid');

View File

@ -16,7 +16,7 @@ function subscribe() {
subscribe_button.onclick = unsubscribe;
subscribe_button.innerHTML = '<b>' + subscribe_data.unsubscribe_text + ' | ' + subscribe_data.sub_count_text + '</b>';
var url = '/subscription_ajax?action_create_subscription_to_channel=1&redirect=false' +
var url = '/subscription_ajax?action=create_subscription_to_channel&redirect=false' +
'&c=' + subscribe_data.ucid;
helpers.xhr('POST', url, {payload: payload, retries: 5, entity_name: 'subscribe request'}, {
@ -32,7 +32,7 @@ function unsubscribe() {
subscribe_button.onclick = subscribe;
subscribe_button.innerHTML = '<b>' + subscribe_data.subscribe_text + ' | ' + subscribe_data.sub_count_text + '</b>';
var url = '/subscription_ajax?action_remove_subscriptions=1&redirect=false' +
var url = '/subscription_ajax?action=remove_subscriptions&redirect=false' +
'&c=' + subscribe_data.ucid;
helpers.xhr('POST', url, {payload: payload, retries: 5, entity_name: 'unsubscribe request'}, {

View File

@ -67,6 +67,10 @@ function get_playlist(plid) {
'&format=html&hl=' + video_data.preferences.locale;
}
if (video_data.params.listen) {
plid_url += '&listen=1'
}
helpers.xhr('GET', plid_url, {retries: 5, entity_name: 'playlist'}, {
on200: function (response) {
playlist.innerHTML = response.playlistHtml;

View File

@ -6,7 +6,7 @@ function mark_watched(target) {
var tile = target.parentNode.parentNode.parentNode.parentNode.parentNode;
tile.style.display = 'none';
var url = '/watch_ajax?action_mark_watched=1&redirect=false' +
var url = '/watch_ajax?action=mark_watched&redirect=false' +
'&id=' + target.getAttribute('data-id');
helpers.xhr('POST', url, {payload: payload}, {
@ -22,7 +22,7 @@ function mark_unwatched(target) {
var count = document.getElementById('count');
count.textContent--;
var url = '/watch_ajax?action_mark_unwatched=1&redirect=false' +
var url = '/watch_ajax?action=mark_unwatched&redirect=false' +
'&id=' + target.getAttribute('data-id');
helpers.xhr('POST', url, {payload: payload}, {

View File

@ -54,6 +54,53 @@ db:
##
#signature_server:
##
## Invidious companion is an external program
## for loading the video streams from YouTube servers.
##
## When this setting is commented out, Invidious companion is not used.
## Otherwise, Invidious will proxy the requests to Invidious companion.
##
## Note: multiple URL can be configured. In this case, invidious will
## randomly pick one every time video data needs to be retrieved. This
## URL is then kept in the video metadata cache to allow video playback
## to work. Once said cache has expired, requesting that video's data
## again will cause a new companion URL to be picked.
##
## The parameter private_url needs to be configured for the internal
## communication between the companion and Invidious.
## And public_url is the public URL from which companion is listening
## to the requests from the user(s).
##
## If you are using a reverse proxy then you will probably need to
## configure the public_url to be the same as the domain used for Invidious.
## Also apply when used from an external IP address (without a domain).
## Examples: https://MYINVIDIOUSDOMAIN or http://192.168.1.100:8282
##
## Both parameter can have identical URL when Invidious is hosted in
## an internal network or at home or locally (localhost).
##
## Accepted values: "http(s)://<IP-HOSTNAME>:<Port>"
## Default: <none>
##
#invidious_companion:
# - private_url: "http://localhost:8282"
# public_url: "http://localhost:8282"
##
## API key for Invidious companion, used for securing the communication
## between Invidious and Invidious companion.
## The size of the key needs to be more or equal to 16.
##
## Note: This parameter is mandatory when Invidious companion is enabled
## and should be a random string.
## Such random string can be generated on linux with the following
## command: `pwgen 16 1`
##
## Accepted values: a string
## Default: <none>
##
#invidious_companion_key: "CHANGE_ME!!"
#########################################
#
@ -130,6 +177,20 @@ https_only: false
##
#hsts: true
##
## Path and permissions of a UNIX socket to listen on for incoming connections.
##
## Note: Enabling socket will make invidious stop listening on the address
## specified by 'host_binding' and 'port'.
##
## Accepted values: Any path to a new file (that doesn't exist yet) and its
## permissions following the UNIX octal convention.
## Default: <none>
##
#socket_binding:
# path: /tmp/invidious.sock
# permissions: 777
# -----------------------------
# Network (outbound)
@ -173,6 +234,17 @@ https_only: false
##
#force_resolve:
##
## Configuration for using a HTTP proxy
##
## If unset, then no HTTP proxy will be used.
##
#http_proxy:
# user:
# password:
# host:
# port:
##
## Use Innertube's transcripts API instead of timedtext for closed captions
@ -222,6 +294,17 @@ https_only: false
##
#log_level: Info
##
## Enables colors in logs. Useful for debugging purposes
## This is overridden if "-k" or "--colorize"
## are passed on the command line.
## Colors are also disabled if the environment variable
## NO_COLOR is present and has any value
##
## Accepted values: true, false
## Default: true
##
#colorize_logs: false
# -----------------------------
# Features
@ -707,6 +790,22 @@ default_user_preferences:
# Video player behavior
# -----------------------------
##
## This option controls the value of the HTML5 <video> element's
## "preload" attribute.
##
## If set to 'false', no video data will be loaded until the user
## explicitly starts the video by clicking the "Play" button.
## If set to 'true', the web browser will buffer some video data
## while the page is loading.
##
## See: https://www.w3schools.com/tags/att_video_preload.asp
##
## Accepted values: true, false
## Default: true
##
#preload: true
##
## Automatically play videos on page load.
##
@ -759,9 +858,9 @@ default_user_preferences:
## Default video quality.
##
## Accepted values: dash, hd720, medium, small
## Default: hd720
## Default: dash
##
#quality: hd720
#quality: dash
##
## Default dash video quality.

View File

@ -1,4 +1,4 @@
FROM crystallang/crystal:1.12.1-alpine AS builder
FROM crystallang/crystal:1.12.2-alpine AS builder
RUN apk add --no-cache sqlite-static yaml-static
@ -21,7 +21,7 @@ COPY ./videojs-dependencies.yml ./videojs-dependencies.yml
RUN crystal spec --warnings all \
--link-flags "-lxml2 -llzma"
RUN if [[ "${release}" == 1 ]] ; then \
RUN --mount=type=cache,target=/root/.cache/crystal if [[ "${release}" == 1 ]] ; then \
crystal build ./src/invidious.cr \
--release \
--static --warnings all \
@ -32,8 +32,8 @@ RUN if [[ "${release}" == 1 ]] ; then \
--link-flags "-lxml2 -llzma"; \
fi
FROM alpine:3.18
RUN apk add --no-cache rsvg-convert ttf-opensans tini
FROM alpine:3.20
RUN apk add --no-cache rsvg-convert ttf-opensans tini tzdata
WORKDIR /invidious
RUN addgroup -g 1000 -S invidious && \
adduser -u 1000 -S invidious -G invidious

View File

@ -1,5 +1,6 @@
FROM alpine:3.19 AS builder
RUN apk add --no-cache 'crystal=1.10.1-r0' shards sqlite-static yaml-static yaml-dev libxml2-static zlib-static openssl-libs-static openssl-dev musl-dev xz-static
FROM alpine:3.20 AS builder
RUN apk add --no-cache 'crystal=1.12.2-r0' shards sqlite-static yaml-static yaml-dev libxml2-static \
zlib-static openssl-libs-static openssl-dev musl-dev xz-static
ARG release
@ -21,7 +22,7 @@ COPY ./videojs-dependencies.yml ./videojs-dependencies.yml
RUN crystal spec --warnings all \
--link-flags "-lxml2 -llzma"
RUN if [[ "${release}" == 1 ]] ; then \
RUN --mount=type=cache,target=/root/.cache/crystal if [[ "${release}" == 1 ]] ; then \
crystal build ./src/invidious.cr \
--release \
--static --warnings all \
@ -32,8 +33,8 @@ RUN if [[ "${release}" == 1 ]] ; then \
--link-flags "-lxml2 -llzma"; \
fi
FROM alpine:3.18
RUN apk add --no-cache rsvg-convert ttf-opensans tini
FROM alpine:3.20
RUN apk add --no-cache rsvg-convert ttf-opensans tini tzdata
WORKDIR /invidious
RUN addgroup -g 1000 -S invidious && \
adduser -u 1000 -S invidious -G invidious

View File

@ -483,7 +483,7 @@
"comments_view_x_replies_3": "عرض رد {{count}}",
"comments_view_x_replies_4": "عرض الردود {{count}}",
"comments_view_x_replies_5": "عرض رد {{count}}",
"search_message_use_another_instance": " يمكنك أيضًا البحث عن <a href=\"`x`\"> في مثيل آخر </a>.",
"search_message_use_another_instance": "يمكنك أيضًا البحث عن <a href=\"`x`\"> في مثيل آخر </a>.",
"comments_points_count_0": "{{count}} نقطة",
"comments_points_count_1": "نقطة واحدة",
"comments_points_count_2": "نقطتان",
@ -559,10 +559,12 @@
"toggle_theme": "تبديل الموضوع",
"Add to playlist": "أضف إلى قائمة التشغيل",
"Add to playlist: ": "أضف إلى قائمة التشغيل: ",
"Answer": "الرد",
"Answer": "اجابة",
"Search for videos": "ابحث عن مقاطع الفيديو",
"The Popular feed has been disabled by the administrator.": "تم تعطيل الخلاصة الشائعة من قبل المسؤول.",
"carousel_slide": "الشريحة {{current}} من {{total}}",
"carousel_skip": "تخطي الكاروسيل",
"carousel_go_to": "انتقل إلى الشريحة `x`"
"carousel_go_to": "انتقل إلى الشريحة `x`",
"preferences_preload_label": "التحميل المسبق لبيانات الفيديو: ",
"Filipino (auto-generated)": "الفلبينية (المولدة تلقائيًا)"
}

View File

@ -137,7 +137,7 @@
"Family friendly? ": "Vhodné pro rodiny? ",
"Engagement: ": "Zapojení: ",
"English": "Angličtina",
"English (auto-generated)": "Angličtina (automaticky generováno)",
"English (auto-generated)": "Angličtina (vytvořeno automaticky)",
"Afrikaans": "Afrikánština",
"Albanian": "Albánština",
"Amharic": "Amharština",
@ -294,8 +294,8 @@
"Chinese (China)": "Čínština (Čína)",
"Chinese (Hong Kong)": "Čínština (Hong Kong)",
"Chinese (Taiwan)": "Čínština (Taiwan)",
"Portuguese (auto-generated)": "Portugalština (automaticky generováno)",
"Spanish (auto-generated)": "Španělština (automaticky generováno)",
"Portuguese (auto-generated)": "Portugalština (vytvořeno automaticky)",
"Spanish (auto-generated)": "Španělština (vytvořeno automaticky)",
"Spanish (Mexico)": "Španělština (Mexiko)",
"Spanish (Spain)": "Španělština (Španělsko)",
"generic_count_years_0": "{{count}} rokem",
@ -352,13 +352,13 @@
"comments_points_count_0": "{{count}} bod",
"comments_points_count_1": "{{count}} body",
"comments_points_count_2": "{{count}} bodů",
"German (auto-generated)": "Němčina (automaticky generováno)",
"Indonesian (auto-generated)": "Indonéština (automaticky generováno)",
"German (auto-generated)": "Němčina (vytvořeno automaticky)",
"Indonesian (auto-generated)": "Indonéština (vytvořeno automaticky)",
"Interlingue": "Interlingue",
"Italian (auto-generated)": "Italština (automaticky generováno)",
"Japanese (auto-generated)": "Japonština (automaticky generováno)",
"Korean (auto-generated)": "Korejština (automaticky generováno)",
"Russian (auto-generated)": "Ruština (automaticky generováno)",
"Italian (auto-generated)": "Italština (vytvořeno automaticky)",
"Japanese (auto-generated)": "Japonština (vytvořeno automaticky)",
"Korean (auto-generated)": "Korejština (vytvořeno automaticky)",
"Russian (auto-generated)": "Ruština (vytvořeno automaticky)",
"generic_count_months_0": "{{count}} měsícem",
"generic_count_months_1": "{{count}} měsíci",
"generic_count_months_2": "{{count}} měsíci",
@ -371,7 +371,7 @@
"footer_documentation": "Dokumentace",
"next_steps_error_message_refresh": "Obnovit stránku",
"Chinese": "Čínština",
"Dutch (auto-generated)": "Nizozemština (automaticky generováno)",
"Dutch (auto-generated)": "Nizozemština (vytvořeno automaticky)",
"Erroneous token": "Chybný token",
"tokens_count_0": "{{count}} token",
"tokens_count_1": "{{count}} tokeny",
@ -380,9 +380,9 @@
"Token is expired, please try again": "Token vypršel, zkuste to prosím znovu",
"English (United States)": "Angličtina (Spojené státy)",
"Cantonese (Hong Kong)": "Kantonština (Hong Kong)",
"French (auto-generated)": "Francouzština (automaticky generováno)",
"Turkish (auto-generated)": "Turečtina (automaticky generováno)",
"Vietnamese (auto-generated)": "Vietnamština (automaticky generováno)",
"French (auto-generated)": "Francouzština (vytvořeno automaticky)",
"Turkish (auto-generated)": "Turečtina (vytvořeno automaticky)",
"Vietnamese (auto-generated)": "Vietnamština (vytvořeno automaticky)",
"Current version: ": "Aktuální verze: ",
"next_steps_error_message": "Měli byste zkusit: ",
"footer_donate_page": "Přispět",
@ -471,7 +471,7 @@
"search_filters_title": "Filtry",
"search_filters_duration_option_medium": "Střední (4 - 20 minut)",
"search_filters_duration_option_long": "Dlouhá (> 20 minut)",
"search_message_use_another_instance": " Můžete také <a href=\"`x`\">hledat na jiné instanci</a>.",
"search_message_use_another_instance": "Můžete také <a href=\"`x`\">hledat na jiné instanci</a>.",
"search_filters_features_label": "Vlastnosti",
"search_filters_features_option_three_sixty": "360°",
"search_filters_features_option_vr180": "VR180",
@ -513,5 +513,7 @@
"The Popular feed has been disabled by the administrator.": "Kategorie Populární byla zakázána administrátorem.",
"carousel_slide": "Snímek {{current}} z {{total}}",
"carousel_skip": "Přeskočit galerii",
"carousel_go_to": "Přejít na snímek `x`"
"carousel_go_to": "Přejít na snímek `x`",
"preferences_preload_label": "Předem načíst data videa: ",
"Filipino (auto-generated)": "Filipínština (vytvořeno automaticky)"
}

View File

@ -11,6 +11,7 @@
"last": "neueste",
"Next page": "Nächste Seite",
"Previous page": "Vorherige Seite",
"First page": "Erste Seite",
"Clear watch history?": "Verlauf löschen?",
"New password": "Neues Passwort",
"New passwords must match": "Neue Passwörter müssen übereinstimmen",
@ -47,6 +48,7 @@
"Preferences": "Einstellungen",
"preferences_category_player": "Wiedergabeeinstellungen",
"preferences_video_loop_label": "Immer wiederholen: ",
"preferences_preload_label": "Videodaten vorladen: ",
"preferences_autoplay_label": "Automatisch abspielen: ",
"preferences_continue_label": "Immer automatisch nächstes Video abspielen: ",
"preferences_continue_autoplay_label": "Nächstes Video automatisch abspielen: ",
@ -322,7 +324,7 @@
"channel_tab_community_label": "Gemeinschaft",
"search_filters_sort_option_relevance": "Relevanz",
"search_filters_sort_option_rating": "Bewertung",
"search_filters_sort_option_date": "Datum",
"search_filters_sort_option_date": "Hochladedatum",
"search_filters_sort_option_views": "Aufrufe",
"search_filters_type_label": "Inhaltstyp",
"search_filters_duration_label": "Dauer",
@ -454,7 +456,7 @@
"Portuguese (auto-generated)": "Portugiesisch (automatisch generiert)",
"search_filters_title": "Filtern",
"search_message_change_filters_or_query": "Versuchen Sie, Ihre Suchanfrage zu erweitern und/oder die Filter zu ändern.",
"search_message_use_another_instance": " Sie können auch <a href=\"`x`\">auf einer anderen Instanz suchen</a>.",
"search_message_use_another_instance": "Sie können auch <a href=\"`x`\">auf einer anderen Instanz suchen</a>.",
"Popular enabled: ": "„Beliebt“-Seite aktiviert: ",
"search_message_no_results": "Keine Ergebnisse gefunden.",
"search_filters_duration_option_medium": "Mittel (4 - 20 Minuten)",
@ -489,9 +491,13 @@
"generic_channels_count_plural": "{{count}} Kanäle",
"Import YouTube watch history (.json)": "YouTube Wiedergabeverlauf importieren (.json)",
"Answer": "Antwort",
"The Popular feed has been disabled by the administrator.": "Der Angesagt-Feed wurde vom Administrator deaktiviert.",
"The Popular feed has been disabled by the administrator.": "Der Feed für beliebte Inhalte wurde vom Administrator deaktiviert.",
"Add to playlist": "Einer Wiedergabeliste hinzufügen",
"Search for videos": "Nach Videos suchen",
"toggle_theme": "Thema wechseln",
"Add to playlist: ": "Einer Wiedergabeliste hinzufügen: "
"Add to playlist: ": "Einer Wiedergabeliste hinzufügen: ",
"carousel_go_to": "Zu Element `x` springen",
"carousel_slide": "Seite {{current}} von {{total}}",
"carousel_skip": "Galerie überspringen",
"Filipino (auto-generated)": "Philippinisch (automatisch generiert)"
}

View File

@ -21,7 +21,7 @@
"Import and Export Data": "Εισαγωγή και Εξαγωγή Δεδομένων",
"Import": "Εισαγωγή",
"Import Invidious data": "Εsαγωγή δεδομένων Invidious JSON",
"Import YouTube subscriptions": "Εισαγωγή συνδρομών YouTube/OPML",
"Import YouTube subscriptions": "Εισαγωγή συνδρομών YouTube απο CVS/OPML",
"Import FreeTube subscriptions (.db)": "Εισαγωγή συνδρομών FreeTube (.db)",
"Import NewPipe subscriptions (.json)": "Εισαγωγή συνδρομών NewPipe (.json)",
"Import NewPipe data (.zip)": "Εισαγωγή δεδομένων NewPipe (.zip)",
@ -455,7 +455,7 @@
"channel_tab_streams_label": "Ζωντανή μετάδοση",
"playlist_button_add_items": "Προσθήκη βίντεο",
"Artist: ": "Καλλιτέχνης: ",
"search_message_use_another_instance": " Μπορείτε επίσης <a href=\"`x`\">να αναζητήσετε σε άλλο instance</a>.",
"search_message_use_another_instance": "Μπορείτε επίσης <a href=\"`x`\">να αναζητήσετε σε άλλο instance</a>.",
"generic_button_save": "Αποθήκευση",
"generic_button_cancel": "Ακύρωση",
"subscriptions_unseen_notifs_count": "{{count}} μη αναγνωσμένη ειδοποίηση",
@ -489,5 +489,14 @@
"search_filters_date_label": "Ημερομηνία αναφόρτωσης",
"Search for videos": "Αναζήτηση βίντεο",
"The Popular feed has been disabled by the administrator.": "Η δημοφιλής ροή έχει απενεργοποιηθεί από τον διαχειριστή.",
"Answer": "Απάντηση"
"Answer": "Απάντηση",
"Add to playlist": "Προσθήκη στην λίιστα αναπαραγωγής",
"Add to playlist: ": "Προσθήκη στην λίστα αναπαραγωγής : ",
"carousel_slide": "Εικόνα {{current}}απο {{total}}",
"carousel_go_to": "Πήγαινε στην εικόνα`x`",
"toggle_theme": "Αλλαγή θέματος",
"Import YouTube watch history (.json)": "Εισαγωγή ιστορικού προβολής YouTube (.json)",
"Filipino (auto-generated)": "Φιλιππινέζικα (αυτόματη παραγωγή)",
"preferences_preload_label": "Προφόρτιση δεδομένων βίντεο: ",
"carousel_skip": "Αποφυγή εμφάνισης εικόνων"
}

View File

@ -33,6 +33,7 @@
"last": "last",
"Next page": "Next page",
"Previous page": "Previous page",
"First page": "First page",
"Clear watch history?": "Clear watch history?",
"New password": "New password",
"New passwords must match": "New passwords must match",
@ -71,6 +72,7 @@
"Preferences": "Preferences",
"preferences_category_player": "Player preferences",
"preferences_video_loop_label": "Always loop: ",
"preferences_preload_label": "Preload video data: ",
"preferences_autoplay_label": "Autoplay: ",
"preferences_continue_label": "Play next by default: ",
"preferences_continue_autoplay_label": "Autoplay next video: ",
@ -190,7 +192,7 @@
"Switch Invidious Instance": "Switch Invidious Instance",
"search_message_no_results": "No results found.",
"search_message_change_filters_or_query": "Try widening your search query and/or changing the filters.",
"search_message_use_another_instance": " You can also <a href=\"`x`\">search on another instance</a>.",
"search_message_use_another_instance": "You can also <a href=\"`x`\">search on another instance</a>.",
"Hide annotations": "Hide annotations",
"Show annotations": "Show annotations",
"Genre: ": "Genre: ",
@ -285,6 +287,7 @@
"Esperanto": "Esperanto",
"Estonian": "Estonian",
"Filipino": "Filipino",
"Filipino (auto-generated)": "Filipino (auto-generated)",
"Finnish": "Finnish",
"French": "French",
"French (auto-generated)": "French (auto-generated)",
@ -422,7 +425,7 @@
"search_filters_title": "Filters",
"search_filters_date_label": "Upload date",
"search_filters_date_option_none": "Any date",
"search_filters_date_option_hour": "Last Hour",
"search_filters_date_option_hour": "Last hour",
"search_filters_date_option_today": "Today",
"search_filters_date_option_week": "This week",
"search_filters_date_option_month": "This month",
@ -454,7 +457,7 @@
"search_filters_sort_label": "Sort By",
"search_filters_sort_option_relevance": "Relevance",
"search_filters_sort_option_rating": "Rating",
"search_filters_sort_option_date": "Upload Date",
"search_filters_sort_option_date": "Upload date",
"search_filters_sort_option_views": "View count",
"search_filters_apply_button": "Apply selected filters",
"Current version: ": "Current version: ",
@ -490,8 +493,10 @@
"channel_tab_streams_label": "Livestreams",
"channel_tab_podcasts_label": "Podcasts",
"channel_tab_releases_label": "Releases",
"channel_tab_courses_label": "Courses",
"channel_tab_playlists_label": "Playlists",
"channel_tab_community_label": "Community",
"channel_tab_posts_label": "Posts",
"channel_tab_channels_label": "Channels",
"toggle_theme": "Toggle Theme",
"carousel_slide": "Slide {{current}} of {{total}}",

View File

@ -478,7 +478,7 @@
"tokens_count_0": "{{count}} token",
"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 puedes <a href=\"`x`\">buscar en otra instancia</a>.",
"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>",
"channel_tab_streams_label": "Directos",
@ -513,5 +513,7 @@
"The Popular feed has been disabled by the administrator.": "El feed Popular ha sido desactivado por el administrador.",
"carousel_slide": "Diapositiva {{current}} de {{total}}",
"carousel_skip": "Saltar el carrusel",
"carousel_go_to": "Ir a la diapositiva `x`"
"carousel_go_to": "Ir a la diapositiva `x`",
"preferences_preload_label": "Precargar datos del vídeo: ",
"Filipino (auto-generated)": "Filipino (generado automáticamente)"
}

View File

@ -360,7 +360,7 @@
"search_filters_duration_label": "مدت",
"search_filters_features_label": "ویژگی‌ها",
"search_filters_sort_label": "به ترتیب",
"search_filters_date_option_hour": "یک ساعت گذشته",
"search_filters_date_option_hour": "ساعت گذشته",
"search_filters_date_option_today": "امروز",
"search_filters_date_option_week": "این هفته",
"search_filters_date_option_month": "این ماه",
@ -461,7 +461,7 @@
"Song: ": "آهنگ: ",
"Channel Sponsor": "اسپانسر کانال",
"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": "افزودن ویدیو",
@ -496,5 +496,6 @@
"crash_page_search_issue": "دنبال <a href=\"`x`\"> گشتیم بین مشکلات در گیت هاب </a>",
"crash_page_report_issue": "اگر هیچ یک از روش های بالا کمکی نکردند لطفا <a href=\"`x`\"> (ترجیحا به انگلیسی) یک سوال جدید در گیت هاب بپرسید و </a> طوری که سوالتون شامل متن زیر باشه:",
"channel_tab_releases_label": "آثار",
"toggle_theme": "تغییر وضعیت تم"
"toggle_theme": "تغییر وضعیت تم",
"preferences_preload_label": "پیش بار کردن داده‌های ویدیو: "
}

View File

@ -460,7 +460,7 @@
"search_filters_apply_button": "Ota valitut suodattimet käyttöön",
"search_filters_date_label": "Latausaika",
"search_filters_duration_option_medium": "Keskipituinen (4 - 20 minuuttia)",
"search_message_use_another_instance": " Voit myös <a href=\"`x`\">hakea toisella instanssilla</a>.",
"search_message_use_another_instance": "Voit myös <a href=\"`x`\">hakea toisella instanssilla</a>.",
"search_filters_date_option_none": "Milloin tahansa",
"search_filters_type_option_all": "Mikä tahansa tyyppi",
"Popular enabled: ": "Suosittu käytössä: ",
@ -496,5 +496,6 @@
"generic_channels_count_plural": "{{count}} kanavaa",
"The Popular feed has been disabled by the administrator.": "Järjestelmänvalvoja on poistanut Suositut-syötteen.",
"Import YouTube watch history (.json)": "Tuo Youtube-katseluhistoria (.json)",
"toggle_theme": "Vaihda teemaa"
"toggle_theme": "Vaihda teemaa",
"preferences_preload_label": "Esilataa video data. "
}

View File

@ -484,7 +484,7 @@
"search_filters_duration_option_medium": "Moyenne (de 4 à 20 minutes)",
"search_filters_apply_button": "Appliquer les filtres",
"search_message_no_results": "Aucun résultat.",
"search_message_use_another_instance": " Vous pouvez également <a href=\"`x`\">effectuer votre recherche sur une autre instance</a>.",
"search_message_use_another_instance": "Vous pouvez également <a href=\"`x`\">effectuer votre recherche sur une autre instance</a>.",
"search_filters_type_option_all": "Tous les types",
"search_filters_date_label": "Date d'ajout",
"search_filters_features_option_vr180": "VR180",
@ -505,7 +505,7 @@
"channel_tab_releases_label": "Parutions",
"channel_tab_podcasts_label": "Émissions audio",
"Import YouTube watch history (.json)": "Importer l'historique de visionnement YouTube (.json)",
"Add to playlist: ": "Ajouter à la playlist: ",
"Add to playlist: ": "Ajouter à la playlist : ",
"Add to playlist": "Ajouter à la playlist",
"Answer": "Répondre",
"Search for videos": "Rechercher des vidéos",
@ -513,5 +513,7 @@
"carousel_skip": "Passez le carrousel",
"carousel_slide": "Diapositive {{current}} sur {{total}}",
"carousel_go_to": "Aller à la diapositive `x`",
"toggle_theme": "Changer le Thème"
"toggle_theme": "Changer le Thème",
"Filipino (auto-generated)": "Philippines (automatiquement générer)",
"preferences_preload_label": "Précharger les données de la vidéo : "
}

View File

@ -449,30 +449,30 @@
"Cantonese (Hong Kong)": "Kantonski (Hong Kong)",
"Chinese": "Kineski",
"Chinese (Taiwan)": "Kineski (Tajvan)",
"Dutch (auto-generated)": "Nizozemski (automatski generiran)",
"French (auto-generated)": "Francuski (automatski generiran)",
"Indonesian (auto-generated)": "Indonezijski (automatski generiran)",
"Dutch (auto-generated)": "Nizozemski (automatski generirano)",
"French (auto-generated)": "Francuski (automatski generirano)",
"Indonesian (auto-generated)": "Indonezijski (automatski generirano)",
"Interlingue": "Interlingua",
"Japanese (auto-generated)": "Japanski (automatski generiran)",
"Russian (auto-generated)": "Ruski (automatski generiran)",
"Turkish (auto-generated)": "Turski (automatski generiran)",
"Vietnamese (auto-generated)": "Vijetnamski (automatski generiran)",
"Japanese (auto-generated)": "Japanski (automatski generirano)",
"Russian (auto-generated)": "Ruski (automatski generirano)",
"Turkish (auto-generated)": "Turski (automatski generirano)",
"Vietnamese (auto-generated)": "Vijetnamski (automatski generirano)",
"Spanish (Spain)": "Španjolski (Španjolska)",
"Italian (auto-generated)": "Talijanski (automatski generiran)",
"Italian (auto-generated)": "Talijanski (automatski generirano)",
"Portuguese (Brazil)": "Portugalski (Brazil)",
"Spanish (Mexico)": "Španjolski (Meksiko)",
"German (auto-generated)": "Njemački (automatski generiran)",
"German (auto-generated)": "Njemački (automatski generirano)",
"Chinese (China)": "Kineski (Kina)",
"Chinese (Hong Kong)": "Kineski (Hong Kong)",
"Korean (auto-generated)": "Korejski (automatski generiran)",
"Portuguese (auto-generated)": "Portugalski (automatski generiran)",
"Spanish (auto-generated)": "Španjolski (automatski generiran)",
"Korean (auto-generated)": "Korejski (automatski generirano)",
"Portuguese (auto-generated)": "Portugalski (automatski generirano)",
"Spanish (auto-generated)": "Španjolski (automatski generirano)",
"preferences_watch_history_label": "Aktiviraj povijest gledanja: ",
"search_filters_title": "Filtri",
"search_filters_date_option_none": "Bilo koji datum",
"search_filters_date_label": "Datum prijenosa",
"search_message_no_results": "Nema rezultata.",
"search_message_use_another_instance": " Također možeš <a href=\"`x`\">tražiti na jednoj drugoj instanci</a>.",
"search_message_use_another_instance": "Također možeš <a href=\"`x`\">tražiti na jednoj drugoj instanci</a>.",
"search_message_change_filters_or_query": "Pokušaj proširiti upit za pretragu i/ili promijeni filtre.",
"search_filters_features_option_vr180": "VR180",
"search_filters_duration_option_none": "Bilo koje duljine",
@ -513,5 +513,7 @@
"toggle_theme": "Uklj./Isklj. temu",
"carousel_slide": "Kadar {{current}} od {{total}}",
"carousel_go_to": "Idi na kadar `x`",
"carousel_skip": "Preskoči vrtuljak"
"carousel_skip": "Preskoči vrtuljak",
"Filipino (auto-generated)": "Filipinski (automatski generirano)",
"preferences_preload_label": "Unaprijed učitaj podatke videa: "
}

View File

@ -7,7 +7,7 @@
"invidious": "Invidious",
"Image CAPTCHA": "Imagine CAPTCHA",
"newest": "plus nove",
"generic_button_save": "Salvar",
"generic_button_save": "Salveguardar",
"Dark mode: ": "Modo obscur: ",
"preferences_dark_mode_label": "Thema: ",
"preferences_category_subscription": "Preferentias de subscription",
@ -23,7 +23,7 @@
"light": "clar",
"No": "Non",
"youtube": "YouTube",
"LIVE": "IN DIRECTE",
"LIVE": "IN DIRECTO",
"reddit": "Reddit",
"preferences_category_player": "Preferentias de reproductor",
"Preferences": "Preferentias",

View File

@ -396,7 +396,7 @@
"toggle_theme": "Víxla þema",
"carousel_skip": "Sleppa hringekjunni",
"preferences_quality_option_medium": "Miðlungs",
"search_message_use_another_instance": " Þú getur líka <a href=\"`x`\">leitað á öðrum netþjóni</a>.",
"search_message_use_another_instance": "Þú getur líka <a href=\"`x`\">leitað á öðrum netþjóni</a>.",
"footer_source_code": "Grunnkóði",
"English (United Kingdom)": "Enska (Bretland)",
"English (United States)": "Enska (Bandarísk)",
@ -496,5 +496,7 @@
"footer_documentation": "Leiðbeiningar",
"channel_tab_channels_label": "Rásir",
"Import YouTube playlist (.csv)": "Flytja inn YouTube spilunarlista (.csv)",
"preferences_quality_option_dash": "DASH (aðlaganleg gæði)"
"preferences_quality_option_dash": "DASH (aðlaganleg gæði)",
"preferences_preload_label": "Forhlaða gögnum myndskeiðs: ",
"Filipino (auto-generated)": "Filippínska (sjálfvirkt útbúin)"
}

View File

@ -449,7 +449,7 @@
"Portuguese (Brazil)": "Portoghese (Brasile)",
"preferences_watch_history_label": "Attiva cronologia di riproduzione: ",
"French (auto-generated)": "Francese (generati automaticamente)",
"search_message_use_another_instance": " Puoi anche <a href=\"`x`\">cercare in un'altra istanza</a>.",
"search_message_use_another_instance": "Puoi anche <a href=\"`x`\">cercare in un'altra istanza</a>.",
"search_message_no_results": "Nessun risultato trovato.",
"search_message_change_filters_or_query": "Prova ad ampliare la ricerca e/o modificare i filtri.",
"English (United States)": "Inglese (Stati Uniti)",
@ -469,8 +469,8 @@
"Spanish (auto-generated)": "Spagnolo (generati automaticamente)",
"Spanish (Mexico)": "Spagnolo (Messico)",
"Spanish (Spain)": "Spagnolo (Spagna)",
"Turkish (auto-generated)": "Turco (auto-generato)",
"Vietnamese (auto-generated)": "Vietnamita (auto-generato)",
"Turkish (auto-generated)": "Turco (generati automaticamente)",
"Vietnamese (auto-generated)": "Vietnamita (generati automaticamente)",
"search_filters_date_label": "Data caricamento",
"search_filters_date_option_none": "Qualunque data",
"search_filters_type_option_all": "Qualunque tipo",
@ -513,5 +513,7 @@
"The Popular feed has been disabled by the administrator.": "La sezione dei contenuti popolari è stata disabilitata dall'amministratore.",
"carousel_slide": "Fotogramma {{current}} di {{total}}",
"carousel_skip": "Salta la galleria",
"carousel_go_to": "Vai al fotogramma `x`"
"carousel_go_to": "Vai al fotogramma `x`",
"preferences_preload_label": "Precarica dati video: ",
"Filipino (auto-generated)": "Filippino (generati automaticamente)"
}

View File

@ -363,7 +363,7 @@
"search_filters_features_option_location": "場所",
"search_filters_features_option_hdr": "HDR",
"Current version: ": "現在のバージョン: ",
"next_steps_error_message": "以下をお試しください: ",
"next_steps_error_message": "以下をお試しください: ",
"next_steps_error_message_refresh": "再読み込み",
"next_steps_error_message_go_to_youtube": "YouTubeを開く",
"search_filters_duration_option_short": "4分未満",
@ -396,7 +396,7 @@
"download_subtitles": "字幕 - `x` (.vtt)",
"search_filters_features_option_purchased": "購入済み",
"preferences_quality_option_dash": "DASH (適応的画質)",
"preferences_quality_dash_option_worst": "最",
"preferences_quality_dash_option_worst": "最",
"preferences_quality_dash_option_best": "最高",
"videoinfo_started_streaming_x_ago": "`x`前に配信を開始",
"videoinfo_watch_on_youTube": "YouTubeで視聴",
@ -434,7 +434,7 @@
"crash_page_switch_instance": "<a href=\"`x`\">別のインスタンスを使用</a>を試す",
"crash_page_read_the_faq": "<a href=\"`x`\">よくある質問 (FAQ)</a> を読む",
"Popular enabled: ": "人気動画を有効化 ",
"search_message_use_another_instance": " <a href=\"`x`\">別のインスタンス上での検索</a>も可能です。",
"search_message_use_another_instance": "<a href=\"`x`\">別のインスタンス上での検索</a>も可能です。",
"search_filters_apply_button": "選択したフィルターを適用",
"user_saved_playlists": "`x`個の保存済みの再生リスト",
"crash_page_you_found_a_bug": "Invidious のバグのようです!",
@ -479,5 +479,7 @@
"carousel_go_to": "スライド`x`を表示",
"carousel_slide": "スライド{{current}} / 全{{total}}個中",
"carousel_skip": "画像のスライド表示をスキップ",
"toggle_theme": "テーマの切り替え"
"toggle_theme": "テーマの切り替え",
"preferences_preload_label": "動画データを事前に読み込む: ",
"Filipino (auto-generated)": "フィリピノ語 (自動生成)"
}

View File

@ -18,8 +18,8 @@
"preferences_related_videos_label": "관련 동영상 보기: ",
"Fallback captions: ": "대체 자막: ",
"preferences_captions_label": "기본 자막: ",
"reddit": "Reddit",
"youtube": "YouTube",
"reddit": "레딧",
"youtube": "유튜브",
"preferences_comments_label": "기본 댓글: ",
"preferences_volume_label": "플레이어 볼륨: ",
"preferences_quality_label": "선호하는 비디오 품질: ",
@ -48,7 +48,7 @@
"An alternative front-end to YouTube": "유튜브의 프론트엔드 대안",
"History": "시청 기록",
"Delete account?": "계정을 삭제 하시겠습니까?",
"Export data as JSON": "JSON으로 데이터 내보내기",
"Export data as JSON": "인비디어스 데이터 내보내기 (.json)",
"Export subscriptions as OPML (for NewPipe & FreeTube)": "OPML로 구독 내보내기 (뉴파이프 및 프리튜브)",
"Export subscriptions as OPML": "OPML로 구독 내보내기",
"Export": "내보내기",
@ -70,7 +70,7 @@
"Next page": "다음 페이지",
"last": "마지막",
"Shared `x` ago": "`x` 전",
"popular": "인기",
"popular": "인기",
"oldest": "과거순",
"newest": "최신순",
"View playlist on YouTube": "유튜브에서 재생목록 보기",
@ -78,10 +78,10 @@
"Subscribe": "구독",
"Unsubscribe": "구독 취소",
"LIVE": "실시간",
"generic_views_count_0": "조회수 {{count}}회",
"generic_videos_count_0": "동영상 {{count}}개",
"generic_playlists_count_0": "재생목록 {{count}}개",
"generic_subscribers_count_0": "구독자 {{count}}명",
"generic_views_count_0": "{{count}}",
"generic_videos_count_0": "{{count}} 동영상",
"generic_playlists_count_0": "{{count}} 재생목록",
"generic_subscribers_count_0": "{{count}} 구독자",
"generic_subscriptions_count_0": "{{count}} 구독",
"search_filters_type_option_playlist": "재생목록",
"Korean": "한국어",
@ -109,14 +109,14 @@
"This channel does not exist.": "이 채널은 존재하지 않습니다.",
"Deleted or invalid channel": "삭제되었거나 더 이상 존재하지 않는 채널",
"channel:`x`": "채널:`x`",
"Show replies": "댓글 보기",
"Show replies": "댓글 보기",
"Hide replies": "댓글 숨기기",
"Incorrect password": "잘못된 비밀번호",
"License: ": "라이선스: ",
"Genre: ": "장르: ",
"Editing playlist `x`": "재생목록 `x` 수정하기",
"Playlist privacy": "재생목록 공개 범위",
"Watch on YouTube": "YouTube에서 보기",
"Watch on YouTube": "유튜브에서 보기",
"Show less": "간략히",
"Show more": "더보기",
"Title": "제목",
@ -125,7 +125,7 @@
"Delete playlist": "재생목록 삭제",
"Delete playlist `x`?": "재생목록 `x` 를 삭제하시겠습니까?",
"Updated `x` ago": "`x` 전에 업데이트됨",
"Released under the AGPLv3 on Github.": "GitHub에 AGPLv3 으로 배포됩니다.",
"Released under the AGPLv3 on Github.": "깃허브에 AGPLv3 으로 배포됩니다.",
"View all playlists": "모든 재생목록 보기",
"Private": "비공개",
"Unlisted": "목록에 없음",
@ -135,12 +135,12 @@
"Source available here.": "소스는 여기에서 사용할 수 있습니다.",
"Log out": "로그아웃",
"search": "검색",
"subscriptions_unseen_notifs_count_0": "읽지 않은 알림 {{count}}개",
"subscriptions_unseen_notifs_count_0": "{{count}} 읽지 않은 알림",
"Subscriptions": "구독",
"revoke": "철회",
"unsubscribe": "구독 취소",
"Import/export": "가져오기/내보내기",
"tokens_count_0": "토큰 {{count}}개",
"tokens_count_0": "{{count}} 토큰",
"Token": "토큰",
"Token manager": "토큰 관리자",
"Subscription manager": "구독 관리자",
@ -163,7 +163,7 @@
"Clear watch history": "시청 기록 지우기",
"preferences_category_data": "데이터 설정",
"`x` is live": "`x` 이(가) 라이브 중입니다",
"`x` uploaded a video": "`x` 이(가) 동영상을 게시했습니다",
"`x` uploaded a video": "`x` 동영상 게시됨",
"Enable web notifications": "웹 알림 활성화",
"preferences_notifications_only_label": "알림만 표시 (있는 경우): ",
"preferences_unseen_only_label": "시청하지 않은 것만 표시: ",
@ -241,7 +241,7 @@
"Could not create mix.": "믹스를 생성할 수 없습니다.",
"`x` ago": "`x` 전",
"comments_view_x_replies_0": "답글 {{count}}개 보기",
"View Reddit comments": "Reddit 댓글 보기",
"View Reddit comments": "레딧 댓글 보기",
"Engagement: ": "약속: ",
"Wilson score: ": "Wilson Score: ",
"Family friendly? ": "전연령 영상입니까? ",
@ -267,8 +267,8 @@
"Bulgarian": "불가리아어",
"Bosnian": "보스니아어",
"Belarusian": "벨라루스어",
"View more comments on Reddit": "Reddit에서 댓글 더 보기",
"View YouTube comments": "YouTube 댓글 보기",
"View more comments on Reddit": "레딧에서 댓글 더 보기",
"View YouTube comments": "유튜브 댓글 보기",
"Hi! Looks like you have JavaScript turned off. Click here to view comments, keep in mind they may take a bit longer to load.": "자바스크립트가 꺼져 있는 것 같습니다! 댓글을 보려면 여기를 클릭하세요. 댓글을 로드하는 데 시간이 조금 더 걸릴 수 있습니다.",
"Shared `x`": "`x` 업로드",
"Whitelisted regions: ": "차단되지 않은 지역: ",
@ -289,7 +289,7 @@
"Empty playlist": "재생목록 비어 있음",
"Show annotations": "주석 보이기",
"Hide annotations": "주석 숨기기",
"Switch Invidious Instance": "Invidious 인스턴스 변경",
"Switch Invidious Instance": "인비디어스 인스턴스 변경",
"Spanish": "스페인어",
"Southern Sotho": "소토어",
"Somali": "소말리어",
@ -329,7 +329,7 @@
"Swedish": "스웨덴어",
"Spanish (Latin America)": "스페인어 (라틴 아메리카)",
"comments_points_count_0": "{{count}} 포인트",
"Invidious Private Feed for `x`": "`x` 에 대한 Invidious 비공개 피드",
"Invidious Private Feed for `x`": "`x` 에 대한 인비디어스 비공개 피드",
"Premieres `x`": "최초 공개 `x`",
"Premieres in `x`": "`x` 후 최초 공개",
"next_steps_error_message": "다음 방법을 시도해 보세요: ",
@ -408,7 +408,7 @@
"preferences_quality_dash_option_1080p": "1080p",
"preferences_quality_dash_option_worst": "최저",
"preferences_watch_history_label": "시청 기록 저장: ",
"invidious": "Invidious",
"invidious": "인비디어스",
"preferences_quality_option_small": "낮음",
"preferences_quality_dash_option_auto": "자동",
"preferences_quality_dash_option_480p": "480p",
@ -453,7 +453,7 @@
"channel_tab_streams_label": "실시간 스트리밍",
"channel_tab_channels_label": "채널",
"channel_tab_playlists_label": "재생목록",
"Standard YouTube license": "표준 YouTube 라이선스",
"Standard YouTube license": "표준 유튜브 라이선스",
"Song: ": "제목: ",
"Channel Sponsor": "채널 스폰서",
"Album: ": "앨범: ",
@ -479,5 +479,6 @@
"carousel_go_to": "`x` 슬라이드로 이동",
"Search for videos": "비디오 검색",
"toggle_theme": "테마 전환",
"carousel_slide": "{{total}}의 슬라이드 {{current}}"
"carousel_slide": "{{total}}의 슬라이드 {{current}}",
"preferences_preload_label": "비디오 데이터 사전 로드: "
}

View File

@ -322,13 +322,13 @@
"channel_tab_community_label": "Gemenskap",
"search_filters_sort_option_relevance": "relevans",
"search_filters_sort_option_rating": "vurdering",
"search_filters_sort_option_date": "dato",
"search_filters_sort_option_date": "Opplastingsdato",
"search_filters_sort_option_views": "visninger",
"search_filters_type_label": "innholdstype",
"search_filters_duration_label": "varighet",
"search_filters_features_label": "funksjoner",
"search_filters_sort_label": "sorter",
"search_filters_date_option_hour": "time",
"search_filters_date_option_hour": "Siste time",
"search_filters_date_option_today": "i dag",
"search_filters_date_option_week": "uke",
"search_filters_date_option_month": "måned",
@ -459,7 +459,7 @@
"search_message_no_results": "Resultatløst.",
"search_filters_type_option_all": "Alle typer",
"search_filters_duration_option_none": "Enhver varighet",
"search_message_use_another_instance": " Du kan også <a href=\"`x`\">søke på en annen instans</a>.",
"search_message_use_another_instance": "Du kan også <a href=\"`x`\">søke på en annen instans</a>.",
"search_filters_date_label": "Opplastningsdato",
"search_filters_apply_button": "Bruk valgte filtre",
"search_filters_date_option_none": "Siden begynnelsen",
@ -494,5 +494,8 @@
"carousel_slide": "Lysark {{current}} av {{total}}",
"carousel_skip": "Hopp over karusellen",
"Add to playlist": "Legg til i spilleliste",
"Add to playlist: ": "Legg til i spilleliste: "
"Add to playlist: ": "Legg til i spilleliste: ",
"The Popular feed has been disabled by the administrator.": "Populært-kilden er koblet ut av administratoren.",
"toggle_theme": "Endre utseende",
"preferences_preload_label": "Last videodata på forhånd: "
}

View File

@ -317,13 +317,13 @@
"channel_tab_community_label": "Gemeenschap",
"search_filters_sort_option_relevance": "relevantie",
"search_filters_sort_option_rating": "beoordeling",
"search_filters_sort_option_date": "datum",
"search_filters_sort_option_date": "Upload datum",
"search_filters_sort_option_views": "keren bekeken",
"search_filters_type_label": "Type inhoud",
"search_filters_duration_label": "duur",
"search_filters_features_label": "eigenschappen",
"search_filters_sort_label": "sorteren",
"search_filters_date_option_hour": "uur",
"search_filters_date_option_hour": "Laatste uur",
"search_filters_date_option_today": "vandaag",
"search_filters_date_option_week": "week",
"search_filters_date_option_month": "maand",
@ -357,7 +357,7 @@
"footer_original_source_code": "Originele bron-code",
"footer_modfied_source_code": "Gewijzigde bron-code",
"adminprefs_modified_source_code_url_label": "URL naar gewijzigde bron-code-opslagplaats",
"next_steps_error_message": "Daarna moet u proberen om: ",
"next_steps_error_message": "Waarna u zou kunnen proberen om: ",
"footer_source_code": "Bron-code",
"search_filters_duration_option_long": "Lang (> 20 minuten)",
"preferences_quality_option_dash": "DASH (adaptieve kwaliteit)",
@ -450,7 +450,7 @@
"Chinese (Hong Kong)": "Chinees (Hongkong)",
"Korean (auto-generated)": "Koreaans (automatisch gegenereerd)",
"search_filters_apply_button": "Geselecteerde filters toepassen",
"search_message_use_another_instance": " Je kan ook <a href=\"`x`\">zoeken op een andere instantie</a>.",
"search_message_use_another_instance": "Je kan ook <a href=\"`x`\">zoeken op een andere instantie</a>.",
"Cantonese (Hong Kong)": "Kantonees (Hongkong)",
"Chinese (China)": "Chinees (China)",
"crash_page_read_the_faq": "de <a href=\"`x`\">veelgestelde vragen (FAQ)</a> gelezen hebt",
@ -477,7 +477,7 @@
"Song: ": "Lied: ",
"generic_channels_count": "{{count}} kanaal",
"generic_channels_count_plural": "{{count}} kanalen",
"Popular enabled: ": "Populair geactiveerd: ",
"Popular enabled: ": "Populair ingeschakeld: ",
"channel_tab_playlists_label": "Afspeellijsten",
"generic_button_edit": "Bewerken",
"Music in this video": "Muziek in deze video",
@ -496,5 +496,7 @@
"Answer": "Antwoorden",
"Search for videos": "Naar video's zoeken",
"carousel_skip": "Carousel overslaan",
"toggle_theme": "Thema omschakelen"
"toggle_theme": "Thema omschakelen",
"preferences_preload_label": "Videogegevens vooraf laden: ",
"Filipino (auto-generated)": "Filipijns (automatisch gegenereerd)"
}

View File

@ -478,7 +478,7 @@
"search_filters_date_label": "Data przesłania",
"search_filters_features_option_vr180": "VR180",
"search_filters_date_option_none": "Dowolna data",
"search_message_use_another_instance": " Możesz także <a href=\"`x`\">wyszukać w innej instancji</a>.",
"search_message_use_another_instance": "Możesz także <a href=\"`x`\">wyszukać w innej instancji</a>.",
"search_filters_type_option_all": "Dowolny typ",
"search_filters_duration_option_none": "Dowolna długość",
"search_filters_duration_option_medium": "Średnia (4-20 minut)",
@ -513,5 +513,7 @@
"Add to playlist: ": "Dodaj do playlisty: ",
"carousel_slide": "Slajd {{current}} z {{total}}",
"carousel_skip": "Pomiń karuzelę",
"carousel_go_to": "Przejdź do slajdu `x`"
"carousel_go_to": "Przejdź do slajdu `x`",
"preferences_preload_label": "Wstępne ładowanie danych wideo: ",
"Filipino (auto-generated)": "filipiński (wygenerowany automatycznie)"
}

View File

@ -474,7 +474,7 @@
"Spanish (auto-generated)": "Espanhol (gerado automaticamente)",
"Spanish (Mexico)": "Espanhol (México)",
"search_filters_duration_option_none": "Qualquer duração",
"search_message_use_another_instance": " Você também pode <a href=\"`x`\">pesquisar em outra instância</a>.",
"search_message_use_another_instance": "Você também pode <a href=\"`x`\">pesquisar em outra instância</a>.",
"Spanish (Spain)": "Espanhol (Espanha)",
"Turkish (auto-generated)": "Turco (gerado automaticamente)",
"search_filters_duration_option_medium": "Médio (4 - 20 minutos)",
@ -513,5 +513,7 @@
"Answer": "Resposta",
"carousel_slide": "Slide {{current}} de {{total}}",
"carousel_skip": "Ignorar carrossel",
"carousel_go_to": "Ir ao slide `x`"
"carousel_go_to": "Ir ao slide `x`",
"preferences_preload_label": "Pré-carregar dados do vídeo: ",
"Filipino (auto-generated)": "Filipino (gerado automaticamente)"
}

View File

@ -448,7 +448,7 @@
"Chinese (Taiwan)": "Chinês (Taiwan)",
"search_message_no_results": "Nenhum resultado encontrado.",
"search_message_change_filters_or_query": "Tente alargar os termos genéricos da pesquisa e/ou alterar os filtros.",
"search_message_use_another_instance": " Também pode <a href=\"`x`\">pesquisar noutra instância</a>.",
"search_message_use_another_instance": "Também pode <a href=\"`x`\">pesquisar noutra instância</a>.",
"English (United Kingdom)": "Inglês (Reino Unido)",
"English (United States)": "Inglês (Estados Unidos)",
"Cantonese (Hong Kong)": "Cantonês (Hong Kong)",
@ -508,10 +508,12 @@
"toggle_theme": "Trocar tema",
"Add to playlist": "Adicionar à lista de reprodução",
"Add to playlist: ": "Adicionar à lista de reprodução: ",
"Answer": "Resposta",
"Answer": "Responder",
"Search for videos": "Procurar vídeos",
"carousel_slide": "Diapositivo {{current}} de{{total}}",
"carousel_skip": "Ignorar carrossel",
"carousel_go_to": "Ir para o diapositivo`x`",
"The Popular feed has been disabled by the administrator.": "O feed Popular foi desativado por um administrador."
"The Popular feed has been disabled by the administrator.": "O feed Popular foi desativado por um administrador.",
"preferences_preload_label": "Pré-carregamento dos dados: ",
"Filipino (auto-generated)": "Filipino (gerado automaticamente)"
}

View File

@ -11,6 +11,7 @@
"last": "последние",
"Next page": "Следующая страница",
"Previous page": "Предыдущая страница",
"First page": "Первая страница",
"Clear watch history?": "Очистить историю просмотров?",
"New password": "Новый пароль",
"New passwords must match": "Новые пароли не совпадают",
@ -48,8 +49,8 @@
"preferences_category_player": "Настройки проигрывателя",
"preferences_video_loop_label": "Всегда повторять: ",
"preferences_autoplay_label": "Автовоспроизведение: ",
"preferences_continue_label": "Переходить к следующему видео? ",
"preferences_continue_autoplay_label": "Автопроигрывание следующего видео: ",
"preferences_continue_label": "Воспроизводить следующее видео: ",
"preferences_continue_autoplay_label": "Автовоспроизведение следующего видео: ",
"preferences_listen_label": "Режим «только аудио» по умолчанию: ",
"preferences_local_label": "Проигрывать видео через прокси? ",
"preferences_speed_label": "Скорость видео по умолчанию: ",
@ -509,6 +510,10 @@
"Add to playlist: ": "Добавить в плейлист: ",
"Answer": "Ответить",
"Search for videos": "Поиск видео",
"The Popular feed has been disabled by the administrator.": "Популярная лента была отключена администратором.",
"toggle_theme": "Переключатель тем"
"The Popular feed has been disabled by the administrator.": "Лента популярного была отключена администратором.",
"toggle_theme": "Переключатель тем",
"carousel_slide": "Пролистано {{current}} из {{total}}",
"carousel_skip": "Пропустить всё",
"carousel_go_to": "Перейти к странице `x`",
"preferences_preload_label": "Предзагрузка видеоданных: "
}

View File

@ -13,7 +13,7 @@
"Import and Export Data": "Uvoz in izvoz podatkov",
"Import": "Uvozi",
"Import Invidious data": "Uvozi Invidious JSON podatke",
"Import YouTube subscriptions": "Uvozi YouTube/OPML naročnine",
"Import YouTube subscriptions": "Uvozi YouTube CSV ali OPML naročnine",
"Import FreeTube subscriptions (.db)": "Uvozi FreeTube (.db) naročnine",
"Import NewPipe data (.zip)": "Uvozi NewPipe (.zip) podatke",
"Export": "Izvozi",
@ -105,7 +105,7 @@
"Show more": "Pokaži več",
"Switch Invidious Instance": "Preklopi Invidious instanco",
"search_message_change_filters_or_query": "Poskusi razširiti iskalno poizvedbo in/ali spremeniti filtre.",
"search_message_use_another_instance": " Lahko tudi <a href=\"`x`\">iščeš v drugi istanci</a>.",
"search_message_use_another_instance": "Lahko tudi <a href=\"`x`\">iščeš v drugi istanci</a>.",
"Wilson score: ": "Wilsonov rezultat: ",
"Engagement: ": "Sodelovanje: ",
"Blacklisted regions: ": "Regije na seznamu nedovoljenih: ",
@ -462,7 +462,7 @@
"search_filters_features_option_four_k": "4K",
"search_filters_features_option_hdr": "HDR",
"next_steps_error_message_refresh": "Osveži",
"search_filters_date_option_hour": "Zadnja ura",
"search_filters_date_option_hour": "V zadnji uri",
"search_filters_features_option_purchased": "Kupljeno",
"search_filters_sort_label": "Razvrsti po",
"search_filters_sort_option_views": "številu ogledov",
@ -521,5 +521,16 @@
"generic_channels_count_1": "{{count}} kanala",
"generic_channels_count_2": "{{count}} kanali",
"generic_channels_count_3": "{{count}} kanalov",
"Import YouTube watch history (.json)": "Uvozi zgodovino gledanja YouTube (.json)"
"Import YouTube watch history (.json)": "Uvozi zgodovino gledanja YouTube (.json)",
"Add to playlist": "Dodaj na seznam predvajanja",
"Add to playlist: ": "Dodaj na seznam predvajanja: ",
"Search for videos": "Iskanje videoposnetkov",
"The Popular feed has been disabled by the administrator.": "Administrator je onemogočil priljubljeni vir.",
"Answer": "Odgovor",
"Filipino (auto-generated)": "filipinščina (samodejno ustvarjeno)",
"toggle_theme": "Preklopi temo",
"carousel_slide": "Diapozitiv {{current}} od {{total}}",
"carousel_skip": "Preskoči galerijo",
"carousel_go_to": "Pojdi na diapozitiv `x`",
"preferences_preload_label": "Predhodno naloži video podatke: "
}

View File

@ -257,13 +257,13 @@
"Video mode": "Mënyrë video",
"channel_tab_videos_label": "Video",
"search_filters_sort_option_rating": "Vlerësim",
"search_filters_sort_option_date": "Datë Ngarkimi",
"search_filters_sort_option_date": "Datë ngarkimi",
"search_filters_sort_option_views": "Numër parjesh",
"search_filters_type_label": "Lloj",
"search_filters_duration_label": "Kohëzgjatje",
"search_filters_features_label": "Veçori",
"search_filters_sort_label": "Renditi Sipas",
"search_filters_date_option_hour": "Orën e Fundit",
"search_filters_date_option_hour": "Orën e fundit",
"search_filters_date_option_today": "Sot",
"search_filters_duration_option_long": "E gjatë (> 20 minuta)",
"search_filters_features_option_hd": "HD",
@ -435,14 +435,14 @@
"tokens_count_plural": "{{count}} tokenë",
"preferences_save_player_pos_label": "Mba mend pozicionin e luajtjes: ",
"Import Invidious data": "Importoni të dhëna JSON Invidious",
"Import YouTube subscriptions": "Importoni pajtime YouTube/OPML",
"Import YouTube subscriptions": "Importoni pajtime YouTube CSV ose OPML",
"Export data as JSON": "Eksportoji të dhënat Invidious si JSON",
"preferences_vr_mode_label": "Video me ndërveprim 360 gradë (lyp WebGL): ",
"Shared `x`": "Ndarë me të tjerë më `x`",
"search_filters_title": "Filtra",
"Popular enabled: ": "Me populloret të aktivizuara: ",
"error_video_not_in_playlist": "Videoja e kërkuar sekziston në këtë luajlistë. <a href=\"`x`\">Klikoni këtu për faqen hyrëse të luajlistës.</a>",
"search_message_use_another_instance": " Mundeni edhe të <a href=\"`x`\">kërkoni në një instancë tjetër</a>.",
"search_message_use_another_instance": "Mundeni edhe të <a href=\"`x`\">kërkoni në një instancë tjetër</a>.",
"search_filters_date_label": "Datë ngarkimi",
"preferences_watch_history_label": "Aktivizo historik parjesh: ",
"Top enabled: ": "Me kryesueset të aktivizuara: ",
@ -484,5 +484,15 @@
"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"
"Erroneous challenge": "Zgjidhje e gabuar",
"Add to playlist: ": "Shtoje te luajlistë: ",
"Add to playlist": "Shtoje te luajlistë",
"Answer": "Përgjigje",
"Search for videos": "Kërko për video",
"The Popular feed has been disabled by the administrator.": "Prurja Popullore është çaktivizuar nga përgjegjësi.",
"carousel_skip": "Anashkaloje Rrotullamen",
"carousel_slide": "Diapozitiv {{current}} nga {{total}}",
"carousel_go_to": "Kalo te diapozitivi `x`",
"Filipino (auto-generated)": "Filipineze (të prodhuara automatikisht)",
"preferences_preload_label": "Parangarko të dhëna videoje: "
}

View File

@ -404,7 +404,7 @@
"generic_count_months_0": "{{count}} mesec",
"generic_count_months_1": "{{count}} meseca",
"generic_count_months_2": "{{count}} meseci",
"search_message_use_another_instance": " Takođe, možete <a href=\"`x`\">pretraživati na drugoj instanci</a>.",
"search_message_use_another_instance": "Takođe, možete <a href=\"`x`\">pretraživati na drugoj instanci</a>.",
"generic_subscribers_count_0": "{{count}} pratilac",
"generic_subscribers_count_1": "{{count}} pratioca",
"generic_subscribers_count_2": "{{count}} pratilaca",
@ -513,5 +513,7 @@
"Answer": "Odgovor",
"Search for videos": "Pretražite video snimke",
"carousel_skip": "Preskoči karusel",
"toggle_theme": "Подеси тему"
"toggle_theme": "Подеси тему",
"preferences_preload_label": "Unapred učitaj podatke o video snimku: ",
"Filipino (auto-generated)": "Filipinski (automatski generisano)"
}

View File

@ -404,7 +404,7 @@
"generic_count_months_0": "{{count}} месец",
"generic_count_months_1": "{{count}} месеца",
"generic_count_months_2": "{{count}} месеци",
"search_message_use_another_instance": " Такође, можете <a href=\"`x`\">претраживати на другој инстанци</a>.",
"search_message_use_another_instance": "Такође, можете <a href=\"`x`\">претраживати на другој инстанци</a>.",
"generic_subscribers_count_0": "{{count}} пратилац",
"generic_subscribers_count_1": "{{count}} пратиоца",
"generic_subscribers_count_2": "{{count}} пратилаца",
@ -513,5 +513,7 @@
"Add to playlist: ": "Додајте на плејлисту: ",
"carousel_skip": "Прескочи карусел",
"The Popular feed has been disabled by the administrator.": "Администратор је онемогућио фид „Популарно“.",
"carousel_slide": "Слајд {{current}} од {{total}}"
"carousel_slide": "Слајд {{current}} од {{total}}",
"preferences_preload_label": "Унапред учитај податке о видео снимку: ",
"Filipino (auto-generated)": "Филипински (аутоматски генерисано)"
}

View File

@ -320,13 +320,13 @@
"channel_tab_community_label": "Gemenskap",
"search_filters_sort_option_relevance": "Relevans",
"search_filters_sort_option_rating": "Rankning",
"search_filters_sort_option_date": "Uppladdnings Datum",
"search_filters_sort_option_date": "Uppladdnings datum",
"search_filters_sort_option_views": "Visningar",
"search_filters_type_label": "Typ",
"search_filters_duration_label": "Varaktighet",
"search_filters_features_label": "Funktioner",
"search_filters_sort_label": "Sortera efter",
"search_filters_date_option_hour": "Senaste Timmen",
"search_filters_date_option_hour": "Senaste timmen",
"search_filters_date_option_today": "Idag",
"search_filters_date_option_week": "Denna vecka",
"search_filters_date_option_month": "Denna månad",
@ -393,7 +393,7 @@
"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>.",
"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)",
@ -496,5 +496,7 @@
"The Popular feed has been disabled by the administrator.": "Det populära flödet har inaktiverats av administratören.",
"carousel_slide": "Bildspel {{current}} av {{total}}",
"carousel_skip": "Hoppa över karusellen",
"carousel_go_to": "Gå till bildspel `x`"
"carousel_go_to": "Gå till bildspel `x`",
"preferences_preload_label": "Förladda video data: ",
"Filipino (auto-generated)": "Filippinska (auto-genererad)"
}

502
locales/ta.json Normal file
View File

@ -0,0 +1,502 @@
{
"Add to playlist": "பிளேலிச்ட்டில் சேர்க்கவும்",
"generic_channels_count": "{{count}} சேனல்",
"generic_channels_count_plural": "{{count}} சேனல்கள்",
"generic_views_count": "{{count}} பார்வை",
"generic_views_count_plural": "{{count}} காட்சிகள்",
"generic_videos_count": "{{count}} வீடியோ",
"generic_videos_count_plural": "{{count}} வீடியோக்கள்",
"generic_playlists_count": "{{count}} பிளேலிச்ட்",
"generic_playlists_count_plural": "{{count}} பிளேலிச்ட்கள்",
"generic_subscribers_count": "{{count}} சந்தாதாரர்",
"generic_subscribers_count_plural": "{{count}} சந்தாதாரர்கள்",
"generic_button_delete": "நீக்கு",
"generic_button_rss": "ஆர்.எச்.எச்",
"LIVE": "வாழ",
"Shared `x` ago": "`X` முன்பு பகிரப்பட்டது",
"Unsubscribe": "குழுவிலகவும்",
"View playlist on YouTube": "யூடியூப்பில் பிளேலிச்ட்டைக் காண்க",
"newest": "புதியது",
"oldest": "பழமையானது",
"popular": "மக்கள்",
"last": "கடைசி",
"Next page": "அடுத்த பக்கம்",
"Previous page": "முந்தைய பக்கம்",
"Clear watch history?": "தெளிவான கண்காணிப்பு வரலாறு?",
"New password": "புதிய கடவுச்சொல்",
"New passwords must match": "புதிய கடவுச்சொற்கள் பொருந்த வேண்டும்",
"Authorize token?": "கிள்ளாக்கை அங்கீகரிக்கவா?",
"Yes": "ஆம்",
"Import YouTube playlist (.csv)": "யூடியூப் பிளேலிச்ட்டை இறக்குமதி செய்க (.csv)",
"Import YouTube watch history (.json)": "YouTube வாட்ச் வரலாற்றை இறக்குமதி செய்க (.json)",
"Import Invidious data": "வன்கவர்வு சாதொபொகு தரவை இறக்குமதி செய்க",
"Import YouTube subscriptions": "YouTube காபிம அல்லது OPML சந்தாக்களை இறக்குமதி செய்க",
"Import FreeTube subscriptions (.db)": "ஃப்ரீட்யூப் சந்தாக்களை இறக்குமதி செய்க (.db)",
"Import NewPipe data (.zip)": "நியூபைப் தரவை இறக்குமதி செய்க (.zip)",
"Export subscriptions as OPML (for NewPipe & FreeTube)": "OPML ஆக சந்தாக்களை ஏற்றுமதி செய்யுங்கள் (நியூபைப் & ஃப்ரீட்யூப்பிற்கு)",
"Export subscriptions as OPML": "OPML ஆக சந்தாக்களை ஏற்றுமதி செய்யுங்கள்",
"Export data as JSON": "சாதொபொகு ஆக வன்கவர்வு தரவை ஏற்றுமதி செய்யுங்கள்",
"Delete account?": "கணக்கை நீக்கவா?",
"History": "வரலாறு",
"JavaScript license information": "சாவாச்கிரிப்ட் உரிம செய்தி",
"source": "மூலம்",
"An alternative front-end to YouTube": "YouTube க்கு ஒரு மாற்று முன் இறுதியில்",
"Log in": "புகுபதிகை",
"Log in/register": "உள்நுழைக/பதிவு செய்யுங்கள்",
"User ID": "பயனர் ஐடி",
"Password": "கடவுச்சொல்",
"Time (h:mm:ss):": "நேரம் (h: மிமீ: எச்எச்):",
"Sign In": "விடுபதிகை",
"Register": "பதிவு செய்யுங்கள்",
"E-mail": "மின்னஞ்சல்",
"Preferences": "விருப்பத்தேர்வுகள்",
"preferences_preload_label": "வீடியோ தரவை முன்பே ஏற்றவும்: ",
"preferences_autoplay_label": "தன்னியக்க: ",
"preferences_continue_label": "இயல்பாக அடுத்து விளையாடுங்கள்: ",
"preferences_local_label": "பதிலாள் வீடியோக்கள்: ",
"preferences_watch_history_label": "கண்காணிப்பு வரலாற்றை இயக்கு: ",
"preferences_speed_label": "இயல்புநிலை வேகம்: ",
"preferences_quality_label": "விருப்பமான வீடியோ தரம்: ",
"preferences_quality_dash_label": "விருப்பமான கோடு வீடியோ தரம்: ",
"preferences_quality_dash_option_auto": "தானி",
"preferences_quality_dash_option_best": "சிறந்த",
"preferences_quality_dash_option_worst": "மோசமான",
"preferences_quality_dash_option_4320p": "4320 ப",
"preferences_quality_dash_option_1080p": "1080 ப",
"preferences_quality_dash_option_720p": "720 ஆ",
"preferences_quality_dash_option_480p": "480 ப",
"preferences_quality_dash_option_360p": "360 ப",
"preferences_quality_dash_option_144p": "144 ப",
"preferences_volume_label": "பிளேயர் தொகுதி: ",
"preferences_comments_label": "இயல்புநிலை கருத்துகள்: ",
"Fallback captions: ": "குறைவடையும் தலைப்புகள்: ",
"preferences_captions_label": "இயல்புநிலை தலைப்புகள்: ",
"preferences_related_videos_label": "தொடர்புடைய வீடியோக்களைக் காட்டு: ",
"preferences_annotations_label": "முன்னிருப்பாக சிறுகுறிப்புகளைக் காட்டு: ",
"preferences_vr_mode_label": "ஊடாடும் 360 டிகிரி வீடியோக்கள் (வெப்சிஎல் தேவை): ",
"preferences_category_visual": "காட்சி விருப்பத்தேர்வுகள்",
"light": "ஒளி",
"preferences_thin_mode_label": "மெல்லிய பயன்முறை: ",
"preferences_category_misc": "இதர விருப்பத்தேர்வுகள்",
"preferences_category_subscription": "சந்தா விருப்பத்தேர்வுகள்",
"preferences_annotations_subscribed_label": "சந்தா சேனல்களுக்கு முன்னிருப்பாக சிறுகுறிப்புகளைக் காட்டவா? ",
"Redirect homepage to feed: ": "உணவளிக்க முகப்புப்பக்கத்தை திருப்பி விடுங்கள்: ",
"preferences_sort_label": "வீடியோக்களை வரிசைப்படுத்துங்கள்: ",
"published": "வெளியிடப்பட்டது",
"published - reverse": "வெளியிடப்பட்டது - தலைகீழ்",
"alphabetically": "அகரவரிசை",
"preferences_unseen_only_label": "கவனக்குறைவாக மட்டுமே காட்டுங்கள்: ",
"preferences_notifications_only_label": "அறிவிப்புகளைக் காட்டுங்கள் (ஏதேனும் இருந்தால்): ",
"Enable web notifications": "வலை அறிவிப்புகளை இயக்கவும்",
"`x` is live": "`x` நேரலையில்",
"preferences_category_data": "தரவு விருப்பத்தேர்வுகள்",
"Manage subscriptions": "சந்தாக்களை நிர்வகிக்கவும்",
"Watch history": "வரலாற்றைப் பாருங்கள்",
"Delete account": "கணக்கை நீக்கு",
"preferences_category_admin": "நிர்வாகி விருப்பத்தேர்வுகள்",
"preferences_default_home_label": "இயல்புநிலை முகப்புப்பக்கம்: ",
"preferences_feed_menu_label": "ஊட்ட மெனு: ",
"preferences_show_nick_label": "மேலே புனைப்பெயரைக் காட்டு: ",
"Top enabled: ": "மேலே இயக்கப்பட்டது: ",
"CAPTCHA enabled: ": "கேப்ட்சா இயக்கப்பட்டது: ",
"Login enabled: ": "உள்நுழைவு இயக்கப்பட்டது: ",
"Registration enabled: ": "பதிவு இயக்கப்பட்டது: ",
"Report statistics: ": "அறிக்கை புள்ளிவிவரங்கள்: ",
"Save preferences": "விருப்பங்களை சேமிக்கவும்",
"Subscription manager": "சந்தா மேலாளர்",
"Token manager": "கிள்ளாக்கு மேலாளர்",
"Token": "கிள்ளாக்கு",
"search": "தேடல்",
"Released under the AGPLv3 on Github.": "கிட்அப்பில் AgPlv3 இன் கீழ் வெளியிடப்பட்டது.",
"View JavaScript license information.": "சாவாச்கிரிப்ட் உரிமத் தகவலைக் காண்க.",
"View privacy policy.": "தனியுரிமைக் கொள்கையைக் காண்க.",
"Trending": "டிரெண்டிங்",
"Public": "பொது",
"Unlisted": "பட்டியலிடப்படாதது",
"Private": "தனிப்பட்ட",
"View all playlists": "அனைத்து பிளேலிச்ட்களையும் காண்க",
"Updated `x` ago": "`X` முன்பு புதுப்பிக்கப்பட்டது",
"Delete playlist `x`?": "பிளேலிச்ட்டை நீக்கவா?",
"Playlist privacy": "பிளேலிச்ட் தனியுரிமை",
"Watch on YouTube": "YouTube இல் பாருங்கள்",
"Hide annotations": "சிறுகுறிப்புகளை மறைக்கவும்",
"Show replies": "பதில்களைக் காட்டு",
"Incorrect password": "தவறான கடவுச்சொல்",
"Wrong answer": "தவறான பதில்",
"Erroneous CAPTCHA": "தவறான கேப்ட்சா",
"CAPTCHA is a required field": "கேப்ட்சா ஒரு தேவையான புலம்",
"User ID is a required field": "பயனர் ஐடி தேவையான புலம்",
"Password is a required field": "கடவுச்சொல் தேவையான புலம்",
"Password cannot be empty": "கடவுச்சொல் காலியாக இருக்க முடியாது",
"Please log in": "தயவுசெய்து உள்நுழைக",
"This channel does not exist.": "இந்த சேனல் இல்லை.",
"Could not get channel info.": "சேனல் தகவலைப் பெற முடியவில்லை.",
"Could not fetch comments": "கருத்துகளைப் பெற முடியவில்லை",
"comments_points_count": "{{count}} புள்ளி",
"comments_points_count_plural": "{{count}} புள்ளிகள்",
"Could not create mix.": "கலவையை உருவாக்க முடியவில்லை.",
"Empty playlist": "வெற்று பிளேலிச்ட்",
"Not a playlist.": "ஒரு பிளேலிச்ட் அல்ல.",
"Playlist does not exist.": "பிளேலிச்ட் இல்லை.",
"Could not pull trending pages.": "பிரபலமான பக்கங்களை இழுக்க முடியவில்லை.",
"Erroneous challenge": "தவறான அறைகூவல்",
"Erroneous token": "தவறான கிள்ளாக்கு",
"No such user": "அத்தகைய பயனர் இல்லை",
"Token is expired, please try again": "கிள்ளாக்கு காலாவதியானது, தயவுசெய்து மீண்டும் முயற்சிக்கவும்",
"English": "ஆங்கிலம்",
"English (United States)": "ஆங்கிலம் (ஐக்கிய அமெரிக்க)",
"English (United Kingdom)": "ஆங்கிலம் (ஐக்கிய முடியரசு)",
"English (auto-generated)": "ஆங்கிலம் (தானாக உருவாக்கப்பட்ட)",
"Afrikaans": "ஆப்பிரிக்கா",
"Albanian": "அல்பேனிய",
"Amharic": "அம்ஆரிக்",
"Arabic": "அரபு",
"Armenian": "ஆர்மீனியன்",
"Azerbaijani": "அசர்பைசானி",
"Bangla": "பாங்லா",
"Basque": "பாச்க்",
"Belarusian": "பெலாருசியன்",
"Bosnian": "போச்னிய",
"Bulgarian": "பல்கேரியன்",
"Burmese": "பர்மீச்",
"Cantonese (Hong Kong)": "கான்டோனீச் (ஆங்காங்)",
"Catalan": "கற்றலான்",
"Cebuano": "செபுவானோ",
"Chinese": "சீன",
"Chinese (China)": "சீன (சீனா)",
"Chinese (Hong Kong)": "சீன (ஆங்காங்)",
"Chinese (Simplified)": "சீன (எளிமைப்படுத்தப்பட்ட)",
"Chinese (Taiwan)": "சீன (தைவான்)",
"Chinese (Traditional)": "சீன (பாரம்பரிய)",
"Dutch": "டச்சு",
"Finnish": "பின்னிச்",
"French": "பிரஞ்சு",
"German (auto-generated)": "செர்மன் (தானாக உருவாக்கப்பட்ட)",
"Greek": "கிரேக்கம்",
"Gujarati": "குசராத்தி",
"Haitian Creole": "ஐட்டிய கிரியோல்",
"Hungarian": "அங்கேரியன்",
"Icelandic": "ஐச்லாந்திய",
"Igbo": "இக்போ",
"Korean (auto-generated)": "கொரிய (தானாக உருவாக்கப்பட்ட)",
"Macedonian": "மாசிடோனியன்",
"Malagasy": "மலகாசி",
"Maltese": "மால்டிச்",
"Maori": "மௌரி",
"Malayalam": "மலையாளம்",
"Marathi": "மராத்தி",
"Mongolian": "மங்கோலியன்",
"Nepali": "நேபாளி",
"Norwegian Bokmål": "நார்வேசியன் பொக்மால்",
"Nyanja": "நயன்சா",
"Russian": "ரச்ய",
"Russian (auto-generated)": "ரச்ய (தானாக உருவாக்கப்பட்ட)",
"Samoan": "சமோவான்",
"Scottish Gaelic": "ச்கோட்டிச் கயாலிக்",
"Serbian": "செர்பிய",
"Shona": "சோனா",
"Sindhi": "சிந்தி",
"Somali": "சோமாலி",
"Southern Sotho": "தெற்கத்திய சோதோ",
"Spanish": "ச்பானிச்",
"Spanish (auto-generated)": "ச்பானிச் (தானாக உருவாக்கப்பட்ட)",
"Sundanese": "சுந்தானியர்கள்",
"Swahili": "ச்வாஇலி",
"Swedish": "ச்வீடிச்",
"Tajik": "தசிக்",
"Tamil": "தமிழ்",
"Thai": "தாய்",
"Turkish": "துருக்கிய",
"Vietnamese": "வியட்நாமிய",
"Welsh": "வேல்ச்",
"Xhosa": "ஓசா",
"Yiddish": "யெட்டிச்",
"Yoruba": "யோருபா",
"Top": "மேலே",
"About": "பற்றி",
"View as playlist": "பிளேலிச்ட்டாக காண்க",
"Gaming": "கேமிங்",
"News": "செய்தி",
"Movies": "திரைப்படங்கள்",
"Download as: ": "என பதிவிறக்கவும்: ",
"Download is disabled": "பதிவிறக்கம் முடக்கப்பட்டுள்ளது",
"(edited)": "(திருத்தப்பட்டது)",
"YouTube comment permalink": "YouTube கருத்து பெர்மாலின்க்",
"`x` marked it with a ❤": "`x` அதை a உடன் குறித்தது",
"Video mode": "வீடியோ பயன்முறை",
"Playlists": "பிளேலிச்ட்கள்",
"search_filters_date_option_today": "இன்று",
"search_filters_date_option_week": "இந்த வாரம்",
"search_filters_date_option_month": "இந்த மாதம்",
"search_filters_type_option_channel": "வாய்க்கால்",
"search_filters_type_option_playlist": "பிளேலிச்ட்",
"search_filters_duration_label": "காலம்",
"search_filters_duration_option_none": "எந்த காலமும்",
"search_filters_duration_option_medium": "நடுத்தர (4 - 20 நிமிடங்கள்)",
"search_filters_duration_option_long": "நீண்ட (> 20 நிமிடங்கள்)",
"search_filters_features_label": "நற்பொருத்தங்கள்",
"search_filters_features_option_four_k": "எச்.சி.",
"search_filters_features_option_live": "நேரடி",
"search_filters_features_option_hd": "எச்டி",
"search_filters_features_option_subtitles": "வசன வரிகள்/சிசி",
"search_filters_features_option_c_commons": "கிரியேட்டிவ் காமன்ச்",
"search_filters_features_option_three_sixty": "360 °",
"search_filters_features_option_three_d": "ZD",
"search_filters_features_option_hdr": "எச்.டி.ஆர்",
"search_filters_features_option_location": "இடம்",
"search_filters_sort_option_relevance": "பொருத்தமானது",
"search_filters_sort_option_rating": "செயல்வரம்பு",
"Current version: ": "தற்போதைய பதிப்பு: ",
"next_steps_error_message": "அதன் பிறகு நீங்கள் முயற்சி செய்ய வேண்டும்: ",
"next_steps_error_message_refresh": "புதுப்பிப்பு",
"next_steps_error_message_go_to_youtube": "YouTube க்குச் செல்லுங்கள்",
"footer_donate_page": "நன்கொடை",
"footer_modfied_source_code": "மாற்றியமைக்கப்பட்ட மூலக் குறியீடு",
"adminprefs_modified_source_code_url_label": "மாற்றியமைக்கப்பட்ட மூலக் குறியீடு களஞ்சியத்திற்கு முகவரி",
"videoinfo_started_streaming_x_ago": "`X` முன்பு ச்ட்ரீமிங் செய்யத் தொடங்கியது",
"videoinfo_watch_on_youTube": "YouTube இல் பாருங்கள்",
"download_subtitles": "வசன வரிகள் - `x` (.vtt)",
"user_created_playlists": "`x` உருவாக்கியது பிளேலிச்ட்கள்",
"user_saved_playlists": "`x` சேமித்த பிளேலிச்ட்கள்",
"crash_page_before_reporting": "ஒரு பிழையைப் புகாரளிப்பதற்கு முன், உங்களிடம் இருப்பதை உறுதிப்படுத்திக் கொள்ளுங்கள்:",
"crash_page_switch_instance": "<a href = \"` x` \"> மற்றொரு நிகழ்வைப் பயன்படுத்த முயற்சித்தேன் </a>",
"crash_page_search_issue": "அறிவிலிமையத்தில் உள்ள <a href=\"`x`\"> தற்போதைய சிக்கல்களைத் தேடியது</a>",
"channel_tab_shorts_label": "குறுக்குகள்",
"channel_tab_streams_label": "லைவ்ச்ட்ரீம்கள்",
"carousel_go_to": "`X` ச்லைடு செல்லவும்",
"Popular": "புகழ்பெற்ற",
"Subscribe": "குழுசேர்",
"View channel on YouTube": "YouTube இல் சேனலைக் காண்க",
"Authorize token for `x`?": "`X` க்கு கிள்ளாக்கை அங்கீகரிக்கவா?",
"No": "இல்லை",
"Add to playlist: ": "பிளேலிச்ட்டில் சேர்க்கவும்: ",
"Answer": "பதில்",
"Search for videos": "வீடியோக்களைத் தேடுங்கள்",
"The Popular feed has been disabled by the administrator.": "பிரபலமான ஊட்டத்தை நிர்வாகியால் முடக்கப்பட்டுள்ளது.",
"generic_subscriptions_count": "{{count}} சந்தா",
"generic_subscriptions_count_plural": "{{count}} சந்தாக்கள்",
"generic_button_edit": "தொகு",
"generic_button_save": "சேமி",
"generic_button_cancel": "ரத்துசெய்",
"Import and Export Data": "தரவை இறக்குமதி செய்து ஏற்றுமதி செய்யுங்கள்",
"Import": "இறக்குமதி",
"Import NewPipe subscriptions (.json)": "நியூபிப்பிப் சந்தாக்களை இறக்குமதி செய்யுங்கள் (.json)",
"Export": "ஏற்றுமதி",
"Text CAPTCHA": "உரை கேப்ட்சா",
"Image CAPTCHA": "பட கேப்ட்சா",
"preferences_category_player": "பிளேயர் விருப்பத்தேர்வுகள்",
"preferences_video_loop_label": "எப்போதும் லூப்: ",
"preferences_continue_autoplay_label": "தன்னியக்க அடுத்த வீடியோ: ",
"preferences_listen_label": "இயல்பாக கேளுங்கள்: ",
"preferences_quality_option_dash": "கோடு (தகவமைப்பு தரம்)",
"preferences_quality_option_hd720": "HD720",
"preferences_quality_option_medium": "சராசரி",
"preferences_quality_option_small": "சிறிய",
"preferences_quality_dash_option_2160p": "2160 ப",
"preferences_quality_dash_option_1440p": "1440 ப",
"preferences_quality_dash_option_240p": "240 ப",
"youtube": "YouTube",
"reddit": "ரெடிட்",
"invidious": "வெகுவாக",
"preferences_extend_desc_label": "வீடியோ விளக்கத்தை தானாக நீட்டிக்கவும்: ",
"preferences_region_label": "உள்ளடக்க நாடு: ",
"preferences_player_style_label": "பிளேயர் ச்டைல்: ",
"Dark mode: ": "இருண்ட முறை: ",
"preferences_dark_mode_label": "தீம்: ",
"dark": "இருண்ட",
"preferences_automatic_instance_redirect_label": "தானியங்கி நிகழ்வு திசைதிருப்பல் (redirect.invidious.io க்கு குறைவடையும்): ",
"preferences_max_results_label": "ஊட்டத்தில் காட்டப்பட்டுள்ள வீடியோக்களின் எண்ணிக்கை: ",
"alphabetically - reverse": "அகரவரிசை - தலைகீழ்",
"channel name": "சேனல் பெயர்",
"channel name - reverse": "சேனல் பெயர் - தலைகீழ்",
"Only show latest video from channel: ": "சேனலில் இருந்து அண்மைக் கால வீடியோவைக் காட்டுங்கள்: ",
"Only show latest unwatched video from channel: ": "சேனலில் இருந்து அண்மைக் கால கவனிக்கப்படாத வீடியோவைக் காட்டுங்கள்: ",
"`x` uploaded a video": "`x` ஒரு வீடியோவைப் பதிவேற்றியது",
"Clear watch history": "தெளிவான கண்காணிப்பு வரலாறு",
"Log out": "விடுபதிகை",
"Source available here.": "சான்று இங்கே கிடைக்கிறது.",
"Delete playlist": "பிளேலிச்ட்டை நீக்கு",
"Create playlist": "பிளேலிச்ட்டை உருவாக்கவும்",
"Title": "தலைப்பு",
"Import/export data": "தரவு இறக்குமதி/ஏற்றுமதி",
"Change password": "கடவுச்சொல்லை மாற்றவும்",
"Manage tokens": "டோக்கன்களை நிர்வகிக்கவும்",
"Popular enabled: ": "பிரபலமான இயக்கப்பட்டது: ",
"tokens_count": "{{count}} கிள்ளாக்கு",
"tokens_count_plural": "{{count}} டோக்கன்கள்",
"Import/export": "இறக்குமதி/ஏற்றுமதி",
"unsubscribe": "குழுவிலகவும்",
"revoke": "ரத்து செய்யுங்கள்",
"Subscriptions": "சந்தாக்கள்",
"subscriptions_unseen_notifs_count": "{{count}} காணப்படாத அறிவிப்பு",
"subscriptions_unseen_notifs_count_plural": "{{count}} காணப்படாத அறிவிப்புகள்",
"Editing playlist `x`": "பிளேலிச்ட்டைத் திருத்துதல் `x`",
"playlist_button_add_items": "வீடியோக்களைச் சேர்க்கவும்",
"Show more": "மேலும் காட்டு",
"Show less": "குறைவாகக் காட்டு",
"Switch Invidious Instance": "அக்யோர்ட் உதாரணத்தை மாற்றவும்",
"search_message_no_results": "முடிவுகள் எதுவும் கிடைக்கவில்லை.",
"search_message_change_filters_or_query": "உங்கள் தேடல் வினவலை அகலப்படுத்த முயற்சிக்கவும்/அல்லது வடிப்பான்களை மாற்றவும்.",
"search_message_use_another_instance": "நீங்கள் <a href = \"` x` \"> மற்றொரு நிகழ்வில் தேடலாம் </a>.",
"Show annotations": "சிறுகுறிப்புகளைக் காட்டு",
"Genre: ": "வகை: ",
"License: ": "உரிமம்: ",
"Standard YouTube license": "நிலையான YouTube உரிமம்",
"Family friendly? ": "குடும்ப நட்பு? ",
"Wilson score: ": "வில்சன் மதிப்பெண்: ",
"Engagement: ": "நிச்சயதார்த்தம்: ",
"Whitelisted regions: ": "அனுமதிப்பட்டிய பகுதிகள்: ",
"Blacklisted regions: ": "தடுப்புப்பட்டியாக்கப்பட்ட பகுதிகள்: ",
"Music in this video": "இந்த வீடியோவில் இசை",
"Artist: ": "கலைஞர்: ",
"Song: ": "பாடல்: ",
"Album: ": "ஆல்பம்: ",
"Shared `x`": "பகிரப்பட்டது `x`",
"Premieres in `x`": "`X` இல் பிரீமியர்ச்",
"Premieres `x`": "பிரீமியர்ச் `x`",
"Hi! Looks like you have JavaScript turned off. Click here to view comments, keep in mind they may take a bit longer to load.": "ஆய்! நீங்கள் சாவாச்கிரிப்ட் முடக்கப்பட்டிருப்பது போல் தெரிகிறது. கருத்துகளைக் காண இங்கே சொடுக்கு செய்க, அவர்கள் ஏற்றுவதற்கு சிறிது நேரம் ஆகலாம் என்பதை நினைவில் கொள்ளுங்கள்.",
"View YouTube comments": "YouTube கருத்துகளைக் காண்க",
"View more comments on Reddit": "ரெடிட் குறித்த கூடுதல் கருத்துகளைக் காண்க",
"View `x` comments": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`X` கருத்தைக் காண்க",
"": "`X` கருத்துகளைக் காண்க"
},
"View Reddit comments": "ரெடிட் கருத்துகளைக் காண்க",
"Hide replies": "பதில்களை மறைக்கவும்",
"Wrong username or password": "தவறான பயனர்பெயர் அல்லது கடவுச்சொல்",
"Password cannot be longer than 55 characters": "கடவுச்சொல் 55 எழுத்துகளை விட நீளமாக இருக்க முடியாது",
"Invidious Private Feed for `x`": "`X` க்கான மோசமான தனியார் ஊட்டம்",
"channel:`x`": "சேனல்: `x`",
"Deleted or invalid channel": "நீக்கப்பட்ட அல்லது தவறான சேனல்",
"comments_view_x_replies": "{{count}} பதிலைக் காண்க",
"comments_view_x_replies_plural": "{{count}} பதில்களைக் காண்க",
"`x` ago": "`x` முன்பு",
"Load more": "மேலும் ஏற்றவும்",
"Hidden field \"challenge\" is a required field": "மறைக்கப்பட்ட புலம் \"அறைகூவல்\" என்பது தேவையான புலம்",
"Hidden field \"token\" is a required field": "மறைக்கப்பட்ட புலம் \"கிள்ளாக்கு\" என்பது தேவையான புலம்",
"Corsican": "கார்சிகன்",
"Croatian": "குரோசியன்",
"Czech": "செக்",
"Danish": "டேனிச்",
"Dutch (auto-generated)": "டச்சு (தானாக உருவாக்கப்பட்ட)",
"Esperanto": "எச்பெராண்டோ",
"Estonian": "எச்டோனிய",
"Filipino": "ஃபிலிபினோ",
"Filipino (auto-generated)": "பிலிப்பைன்ச் (தானாக உருவாக்கிய)",
"French (auto-generated)": "பிரஞ்சு (தானாக உருவாக்கப்பட்ட)",
"Galician": "காலிசியன்",
"Georgian": "சார்சியன்",
"German": "செர்மன்",
"Hausa": "ஔசா",
"Lao": "லாவோ",
"Latin": "லத்தீன்",
"Latvian": "லாட்வியன்",
"Hawaiian": "அவாயியன்",
"Hebrew": "எபிரேய",
"Lithuanian": "லிதுவேனியன்",
"Hindi": "இந்தி",
"Hmong": "அமோங்",
"Indonesian": "இந்தோனேசிய",
"Indonesian (auto-generated)": "இந்தோனேசிய (தானாக உருவாக்கப்பட்ட)",
"Interlingue": "இன்டர்லின்குய்",
"Irish": "ஐரிச்",
"Italian": "இத்தாலிய",
"Italian (auto-generated)": "இத்தாலியன் (தானாக உருவாக்கப்பட்ட)",
"Japanese": "சப்பானியர்கள்",
"Japanese (auto-generated)": "சப்பானிய (தானாக உருவாக்கப்பட்ட)",
"Javanese": "சாவானீச்",
"Kannada": "கன்னடா",
"Kazakh": "கசாக்",
"Khmer": "கெமர்",
"Korean": "கொரிய",
"Kurdish": "குர்திச்",
"Kyrgyz": "கிர்கிச்",
"Luxembourgish": "லக்சம்போர்கிச்",
"Malay": "மலாய்",
"Pashto": "பச்தோ",
"Persian": "பெர்சியன்",
"Polish": "போலீச்",
"Portuguese": "போர்த்துகீசியம்",
"Portuguese (auto-generated)": "போர்த்துகீசியம் (தானாக உருவாக்கிய)",
"generic_count_minutes": "{{count}} மணித்துளி",
"generic_count_minutes_plural": "{{count}} நிமிடங்கள்",
"generic_count_seconds": "{{count}} இரண்டாவது",
"generic_count_seconds_plural": "{{count}} வினாடிகள்",
"Fallback comments: ": "குறைவடையும் கருத்துரைகள்: ",
"Portuguese (Brazil)": "போர்த்துகீசியம் (பிரேசில்)",
"Punjabi": "பஞ்சாபி",
"Romanian": "ருமேனிய",
"Sinhala": "சிங்களம்",
"Slovak": "ச்லோவாக்",
"Slovenian": "ச்லோவேனியன்",
"Spanish (Latin America)": "ச்பானிச் (லத்தீன் அமெரிக்கா)",
"Spanish (Mexico)": "ச்பானிச் (மெக்சிகோ)",
"Spanish (Spain)": "ச்பானிச் (ச்பெயின்)",
"Telugu": "தெலுங்கு",
"Turkish (auto-generated)": "துருக்கிய (தானாக உருவாக்கிய)",
"Ukrainian": "உக்ரேனிய",
"Urdu": "உருது",
"Uzbek": "உச்பெக்",
"Vietnamese (auto-generated)": "வியட்நாமிய (தானாக உருவாக்கப்பட்ட)",
"Western Frisian": "மேற்கு ஃபிரிசியன்",
"Zulu": "சுலு",
"generic_count_years": "{{count}}} ஆண்டு",
"generic_count_years_plural": "{{count}} ஆண்டுகள்",
"generic_count_months": "{{count}} மாதம்",
"generic_count_months_plural": "{{count}} மாதங்கள்",
"generic_count_weeks": "{{count}}} வாரம்",
"generic_count_weeks_plural": "{{count}} வாரங்கள்",
"generic_count_days": "{{count}}} நாள்",
"generic_count_days_plural": "{{count}} நாட்கள்",
"generic_count_hours": "{{count}} மணிநேரம்",
"generic_count_hours_plural": "{{count}} மணிநேரம்",
"Search": "தேடல்",
"Rating: ": "மதிப்பீடு: ",
"preferences_locale_label": "மொழி: ",
"Default": "இயல்புநிலை",
"Music": "இசை",
"Download": "பதிவிறக்கம்",
"%A %B %-d, %Y": "%A %b %-d, %y",
"permalink": "பெர்மாலின்க்",
"Channel Sponsor": "சேனல் ஒப்புரவாளர்",
"Audio mode": "ஆடியோ பயன்முறை",
"search_filters_duration_option_short": "குறுகிய (<4 நிமிடங்கள்)",
"search_filters_title": "வடிப்பான்கள்",
"search_filters_date_label": "தேதி பதிவேற்றும் தேதி",
"search_filters_date_option_none": "எந்த தேதி",
"search_filters_date_option_hour": "கடைசி மணி",
"search_filters_date_option_year": "இந்த ஆண்டு",
"search_filters_type_label": "வகை",
"search_filters_type_option_all": "எந்த வகை",
"search_filters_type_option_video": "ஒளிதோற்றம்",
"search_filters_type_option_movie": "படம்",
"search_filters_type_option_show": "காட்டு",
"search_filters_features_option_vr180": "VR180",
"search_filters_features_option_purchased": "வாங்கப்பட்டது",
"search_filters_sort_label": "வரிசைப்படுத்தவும்",
"search_filters_sort_option_date": "பதிவேற்ற தேதி",
"search_filters_sort_option_views": "எண்ணிக்கை காண்க",
"search_filters_apply_button": "தேர்ந்தெடுக்கப்பட்ட வடிப்பான்களைப் பயன்படுத்துங்கள்",
"footer_documentation": "ஆவணப்படுத்துதல்",
"footer_source_code": "மூலக் குறியீடு",
"footer_original_source_code": "அசல் மூலக் குறியீடு",
"none": "எதுவுமில்லை",
"videoinfo_youTube_embed_link": "உட்பொதிக்கப்பட்டது",
"videoinfo_invidious_embed_link": "உட்பொதிப்பு இணைப்பு",
"Video unavailable": "வீடியோ கிடைக்கவில்லை",
"preferences_save_player_pos_label": "பிளேபேக் நிலையை சேமிக்கவும்: ",
"crash_page_you_found_a_bug": "நீங்கள் ஒரு பிழையை கண்டுபிடித்ததாகத் தெரிகிறது!",
"crash_page_refresh": "<a href = \"` x` \"> பக்கத்தை புதுப்பிக்க முயற்சித்தேன் </a>",
"crash_page_read_the_faq": "<a href = \"` x` \"> அடிக்கடி கேட்கப்படும் கேள்விகள் (கேள்விகள்) </a> ஐப் படியுங்கள்",
"crash_page_report_issue": "மேலே எதுவும் உதவவில்லை என்றால், தயவுசெய்து <a href = \"` x` \"> அறிவிலிமையம் </a> (முன்னுரிமை ஆங்கிலத்தில்) ஒரு புதிய சிக்கலைத் திறந்து உங்கள் செய்தியில் பின்வரும் உரையைச் சேர்க்கவும் (அந்த உரையை மொழிபெயர்க்க வேண்டாம்):",
"error_video_not_in_playlist": "கோரப்பட்ட வீடியோ இந்த பிளேலிச்ட்டில் இல்லை. <a href = \"` x` \"> பிளேலிச்ட் முகப்பு பக்கத்திற்கு இங்கே சொடுக்கு செய்க. </a>",
"channel_tab_videos_label": "வீடியோக்கள்",
"channel_tab_podcasts_label": "பாட்காச்ட்கள்",
"channel_tab_releases_label": "வெளியீடுகள்",
"channel_tab_playlists_label": "பிளேலிச்ட்கள்",
"channel_tab_community_label": "சமூகம்",
"channel_tab_channels_label": "சேனல்கள்",
"toggle_theme": "கருப்பொருளை மாற்றவும்",
"carousel_slide": "{{total}} இன் ச்லைடு {{current}}",
"carousel_skip": "கொணர்வி தவிர்க்கவும்"
}

1
locales/tok.json Normal file
View File

@ -0,0 +1 @@
{}

View File

@ -322,13 +322,13 @@
"channel_tab_community_label": "Topluluk",
"search_filters_sort_option_relevance": "İlgi",
"search_filters_sort_option_rating": "Değerlendirme",
"search_filters_sort_option_date": "Yükleme Tarihi",
"search_filters_sort_option_date": "Yükleme tarihi",
"search_filters_sort_option_views": "Görüntüleme Sayısı",
"search_filters_type_label": "Tür",
"search_filters_duration_label": "Süre",
"search_filters_features_label": "Özellikler",
"search_filters_sort_label": "Sıralama Ölçütü",
"search_filters_date_option_hour": "Son Saat",
"search_filters_date_option_hour": "Son saat",
"search_filters_date_option_today": "Bugün",
"search_filters_date_option_week": "Bu Hafta",
"search_filters_date_option_month": "Bu Ay",
@ -452,7 +452,7 @@
"Spanish (Spain)": "İspanyolca (İspanya)",
"Vietnamese (auto-generated)": "Vietnamca (Otomatik Oluşturuldu)",
"preferences_watch_history_label": "İzleme Geçmişini Etkinleştir: ",
"search_message_use_another_instance": " Ayrıca <a href=\"`x`\">başka bir örnekte arayabilirsiniz</a>.",
"search_message_use_another_instance": "Ayrıca <a href=\"`x`\">başka bir örnekte arayabilirsiniz</a>.",
"search_filters_type_option_all": "Herhangi Bir Tür",
"search_filters_duration_option_none": "Herhangi Bir Süre",
"search_message_no_results": "Sonuç bulunamadı.",
@ -496,5 +496,6 @@
"carousel_slide": "Sunum {{current}} / {{total}}",
"carousel_skip": "Kayar menüyü atla",
"carousel_go_to": "`x` sunumuna git",
"The Popular feed has been disabled by the administrator.": "Popüler akışı yönetici tarafından devre dışı bırakıldı."
"The Popular feed has been disabled by the administrator.": "Popüler akışı yönetici tarafından devre dışı bırakıldı.",
"preferences_preload_label": "Video verilerini önceden yükle: "
}

View File

@ -455,7 +455,7 @@
"search_filters_date_option_week": "Цей тиждень",
"search_filters_type_label": "Тип",
"search_filters_type_option_channel": "Канал",
"search_message_use_another_instance": " Можете також <a href=\"`x`\">пошукати іншим сервером</a>.",
"search_message_use_another_instance": "Можете також <a href=\"`x`\">пошукати на іншому сервері</a>.",
"search_filters_title": "Фільтри",
"search_filters_date_option_hour": "Остання година",
"search_filters_date_option_month": "Цей місяць",
@ -472,7 +472,7 @@
"search_filters_features_option_three_sixty": "360°",
"search_filters_features_option_hdr": "HDR",
"search_filters_sort_label": "Спершу",
"search_filters_sort_option_date": "Нещодавні",
"search_filters_sort_option_date": "Дата вивантаження",
"search_filters_apply_button": "Застосувати фільтри",
"search_filters_features_option_vr180": "VR180",
"search_filters_features_option_purchased": "Придбано",
@ -513,5 +513,7 @@
"The Popular feed has been disabled by the administrator.": "Стрічка Популярні вимкнена адміністратором.",
"carousel_slide": "Слайд {{current}} з {{total}}",
"carousel_skip": "Пропустити карусель",
"carousel_go_to": "Перейти до слайда `x`"
"carousel_go_to": "Перейти до слайда `x`",
"preferences_preload_label": "Попереднє завантаження відеоданих: ",
"Filipino (auto-generated)": "Філіппінська (згенеровано автоматично)"
}

View File

@ -436,7 +436,7 @@
"Turkish (auto-generated)": "土耳其语 (自动生成)",
"Spanish (Spain)": "西班牙语 (西班牙)",
"preferences_watch_history_label": "启用观看历史: ",
"search_message_use_another_instance": " 你也可以 <a href=\"`x`\">在另一实例上搜索</a>。",
"search_message_use_another_instance": "你也可以 <a href=\"`x`\">在另一实例上搜索</a>。",
"search_filters_title": "过滤器",
"search_filters_date_label": "上传日期",
"search_filters_apply_button": "应用所选过滤器",
@ -479,5 +479,7 @@
"The Popular feed has been disabled by the administrator.": "“流行”源已被管理员禁用。",
"carousel_slide": "当前为第 {{current}} 张图,共 {{total}} 张图",
"carousel_skip": "跳过图集",
"carousel_go_to": "转到图 `x`"
"carousel_go_to": "转到图 `x`",
"preferences_preload_label": "预加载视频数据: ",
"Filipino (auto-generated)": "菲律宾语 (自动生成)"
}

View File

@ -338,13 +338,13 @@
"channel_tab_community_label": "社群",
"search_filters_sort_option_relevance": "關聯",
"search_filters_sort_option_rating": "評分",
"search_filters_sort_option_date": "日期",
"search_filters_sort_option_date": "上傳日期",
"search_filters_sort_option_views": "檢視",
"search_filters_type_label": "內容類型",
"search_filters_duration_label": "時長",
"search_filters_features_label": "特色",
"search_filters_sort_label": "排序",
"search_filters_date_option_hour": "小時",
"search_filters_date_option_hour": "最後一小時",
"search_filters_date_option_today": "今天",
"search_filters_date_option_week": "週",
"search_filters_date_option_month": "月",
@ -442,7 +442,7 @@
"search_filters_duration_option_none": "任何時長",
"search_filters_duration_option_medium": "中等4到20分鐘",
"search_filters_features_option_vr180": "VR180",
"search_message_use_another_instance": " 您也可以<a href=\"`x`\">在其他站台上搜尋</a>。",
"search_message_use_another_instance": "您也可以<a href=\"`x`\">在其他站台上搜尋</a>。",
"search_filters_title": "過濾條件",
"search_filters_date_label": "上傳日期",
"search_filters_type_option_all": "任何類型",
@ -479,5 +479,7 @@
"carousel_slide": "第 {{current}} 張投影片,共 {{total}} 張",
"carousel_skip": "略過輪播",
"carousel_go_to": "跳到投影片 `x`",
"The Popular feed has been disabled by the administrator.": "熱門 feed 已被管理員停用。"
"The Popular feed has been disabled by the administrator.": "熱門 feed 已被管理員停用。",
"preferences_preload_label": "預先載入影片資訊 ",
"Filipino (auto-generated)": "菲律賓語(自動產生)"
}

2
mocks

@ -1 +1 @@
Subproject commit 11ec372f72747c09d48ffef04843f72be67d5b54
Subproject commit b55d58dea94f7144ff0205857dfa70ec14eaa872

View File

@ -10,16 +10,20 @@ shards:
backtracer:
git: https://github.com/sija/backtracer.cr.git
version: 1.2.1
version: 1.2.2
db:
git: https://github.com/crystal-lang/crystal-db.git
version: 0.10.1
version: 0.13.1
exception_page:
git: https://github.com/crystal-loot/exception_page.git
version: 0.2.2
http_proxy:
git: https://github.com/mamantoha/http_proxy.git
version: 0.10.3
kemal:
git: https://github.com/kemalcr/kemal.git
version: 1.1.2
@ -30,7 +34,7 @@ shards:
pg:
git: https://github.com/will/crystal-pg.git
version: 0.24.0
version: 0.28.0
protodec:
git: https://github.com/iv-org/protodec.git
@ -42,9 +46,9 @@ shards:
spectator:
git: https://github.com/icy-arctic-fox/spectator.git
version: 0.10.4
version: 0.10.6
sqlite3:
git: https://github.com/crystal-lang/crystal-sqlite3.git
version: 0.18.0
version: 0.21.0

View File

@ -1,21 +1,20 @@
name: invidious
version: 0.20.1
version: 2.20250314.0-dev
authors:
- Omar Roth <omarroth@protonmail.com>
- Invidious team
- Invidious team <contact@invidious.io>
- Contributors!
targets:
invidious:
main: src/invidious.cr
description: |
Invidious is an alternative front-end to YouTube
dependencies:
pg:
github: will/crystal-pg
version: ~> 0.24.0
version: ~> 0.28.0
sqlite3:
github: crystal-lang/crystal-sqlite3
version: ~> 0.18.0
version: ~> 0.21.0
kemal:
github: kemalcr/kemal
version: ~> 1.1.2
@ -28,6 +27,9 @@ dependencies:
athena-negotiation:
github: athena-framework/negotiation
version: ~> 0.1.1
http_proxy:
github: mamantoha/http_proxy
version: ~> 0.10.3
development_dependencies:
spectator:
@ -37,6 +39,10 @@ development_dependencies:
github: crystal-ameba/ameba
version: ~> 1.6.1
crystal: ">= 1.0.0, < 2.0.0"
crystal: ">= 1.10.0, < 2.0.0"
license: AGPLv3
repository: https://github.com/iv-org/invidious
homepage: https://invidious.io
documentation: https://docs.invidious.io

View File

@ -27,8 +27,8 @@ Spectator.describe Invidious::Hashtag do
expect(video_11.length_seconds).to eq((56.minutes + 41.seconds).total_seconds.to_i32)
expect(video_11.views).to eq(40_504_893)
expect(video_11.live_now).to be_false
expect(video_11.premium).to be_false
expect(video_11.badges.live_now?).to be_false
expect(video_11.badges.premium?).to be_false
expect(video_11.premiere_timestamp).to be_nil
#
@ -49,8 +49,8 @@ Spectator.describe Invidious::Hashtag do
expect(video_35.length_seconds).to eq((3.minutes + 14.seconds).total_seconds.to_i32)
expect(video_35.views).to eq(30_790_049)
expect(video_35.live_now).to be_false
expect(video_35.premium).to be_false
expect(video_35.badges.live_now?).to be_false
expect(video_35.badges.premium?).to be_false
expect(video_35.premiere_timestamp).to be_nil
end
@ -80,8 +80,8 @@ Spectator.describe Invidious::Hashtag do
expect(video_41.length_seconds).to eq((1.hour).total_seconds.to_i32)
expect(video_41.views).to eq(63_240)
expect(video_41.live_now).to be_false
expect(video_41.premium).to be_false
expect(video_41.badges.live_now?).to be_false
expect(video_41.badges.premium?).to be_false
expect(video_41.premiere_timestamp).to be_nil
#
@ -102,8 +102,8 @@ Spectator.describe Invidious::Hashtag do
expect(video_48.length_seconds).to eq((35.minutes + 46.seconds).total_seconds.to_i32)
expect(video_48.views).to eq(68_704)
expect(video_48.live_now).to be_false
expect(video_48.premium).to be_false
expect(video_48.badges.live_now?).to be_false
expect(video_48.badges.premium?).to be_false
expect(video_48.premiere_timestamp).to be_nil
end
end

View File

@ -17,8 +17,8 @@ Spectator.describe "parse_video_info" do
# Basic video infos
expect(info["title"].as_s).to eq("I Gave My 100,000,000th Subscriber An Island")
expect(info["views"].as_i).to eq(126_573_823)
expect(info["likes"].as_i).to eq(5_157_654)
expect(info["views"].as_i).to eq(220_226_287)
expect(info["likes"].as_i).to eq(6_870_691)
# For some reason the video length from VideoDetails and the
# one from microformat differs by 1s...
@ -48,12 +48,12 @@ Spectator.describe "parse_video_info" do
expect(info["relatedVideos"].as_a.size).to eq(20)
expect(info["relatedVideos"][0]["id"]).to eq("Hwybp38GnZw")
expect(info["relatedVideos"][0]["title"]).to eq("I Built Willy Wonka's Chocolate Factory!")
expect(info["relatedVideos"][0]["id"]).to eq("krsBRQbOPQ4")
expect(info["relatedVideos"][0]["title"]).to eq("$1 vs $250,000,000 Private Island!")
expect(info["relatedVideos"][0]["author"]).to eq("MrBeast")
expect(info["relatedVideos"][0]["ucid"]).to eq("UCX6OQ3DkcsbYNE6H8uQQuVA")
expect(info["relatedVideos"][0]["view_count"]).to eq("179877630")
expect(info["relatedVideos"][0]["short_view_count"]).to eq("179M")
expect(info["relatedVideos"][0]["view_count"]).to eq("230617484")
expect(info["relatedVideos"][0]["short_view_count"]).to eq("230M")
expect(info["relatedVideos"][0]["author_verified"]).to eq("true")
# Description
@ -76,11 +76,11 @@ Spectator.describe "parse_video_info" do
expect(info["ucid"].as_s).to eq("UCX6OQ3DkcsbYNE6H8uQQuVA")
expect(info["authorThumbnail"].as_s).to eq(
"https://yt3.ggpht.com/ytc/AL5GRJVuqw82ERvHzsmBxL7avr1dpBtsVIXcEzBPZaloFg=s48-c-k-c0x00ffffff-no-rj"
"https://yt3.ggpht.com/fxGKYucJAVme-Yz4fsdCroCFCrANWqw0ql4GYuvx8Uq4l_euNJHgE-w9MTkLQA805vWCi-kE0g=s48-c-k-c0x00ffffff-no-rj"
)
expect(info["authorVerified"].as_bool).to be_true
expect(info["subCountText"].as_s).to eq("143M")
expect(info["subCountText"].as_s).to eq("320M")
end
it "parses a regular video with no descrition/comments" do
@ -99,8 +99,8 @@ Spectator.describe "parse_video_info" do
# Basic video infos
expect(info["title"].as_s).to eq("Chris Rea - Auberge")
expect(info["views"].as_i).to eq(10_943_126)
expect(info["likes"].as_i).to eq(0)
expect(info["views"].as_i).to eq(14_324_584)
expect(info["likes"].as_i).to eq(35_870)
expect(info["lengthSeconds"].as_i).to eq(283_i64)
expect(info["published"].as_s).to eq("2012-05-21T00:00:00Z")
@ -132,14 +132,14 @@ Spectator.describe "parse_video_info" do
# Related videos
expect(info["relatedVideos"].as_a.size).to eq(19)
expect(info["relatedVideos"].as_a.size).to eq(20)
expect(info["relatedVideos"][0]["id"]).to eq("Ww3KeZ2_Yv4")
expect(info["relatedVideos"][0]["title"]).to eq("Chris Rea")
expect(info["relatedVideos"][0]["author"]).to eq("PanMusic")
expect(info["relatedVideos"][0]["ucid"]).to eq("UCsKAPSuh1iNbLWUga_igPyA")
expect(info["relatedVideos"][0]["view_count"]).to eq("31581")
expect(info["relatedVideos"][0]["short_view_count"]).to eq("31K")
expect(info["relatedVideos"][0]["id"]).to eq("gUUdQfnshJ4")
expect(info["relatedVideos"][0]["title"]).to eq("Chris Rea - The Road To Hell 1989 Full Version")
expect(info["relatedVideos"][0]["author"]).to eq("NEA ZIXNH")
expect(info["relatedVideos"][0]["ucid"]).to eq("UCYMEOGcvav3gCgImK2J07CQ")
expect(info["relatedVideos"][0]["view_count"]).to eq("53298661")
expect(info["relatedVideos"][0]["short_view_count"]).to eq("53M")
expect(info["relatedVideos"][0]["author_verified"]).to eq("false")
# Description
@ -156,11 +156,13 @@ Spectator.describe "parse_video_info" do
# Author infos
expect(info["author"].as_s).to eq("ChrisReaOfficial")
expect(info["author"].as_s).to eq("ChrisReaVideos")
expect(info["ucid"].as_s).to eq("UC_5q6nWPbD30-y6oiWF_oNA")
expect(info["authorThumbnail"].as_s).to be_empty
expect(info["authorThumbnail"].as_s).to eq(
"https://yt3.ggpht.com/ytc/AIdro_n71nsegpKfjeRKwn1JJmK5IVMh_7j5m_h3_1KnUUg=s48-c-k-c0x00ffffff-no-rj"
)
expect(info["authorVerified"].as_bool).to be_false
expect(info["subCountText"].as_s).to eq("-")
expect(info["subCountText"].as_s).to eq("3.11K")
end
end

View File

@ -23,6 +23,7 @@ require "kilt"
require "./ext/kemal_content_for.cr"
require "./ext/kemal_static_file_handler.cr"
require "http_proxy"
require "athena-negotiation"
require "openssl/hmac"
require "option_parser"
@ -92,6 +93,14 @@ SOFTWARE = {
YT_POOL = YoutubeConnectionPool.new(YT_URL, capacity: CONFIG.pool_size)
# Image request pool
GGPHT_POOL = YoutubeConnectionPool.new(URI.parse("https://yt3.ggpht.com"), capacity: CONFIG.pool_size)
COMPANION_POOL = CompanionConnectionPool.new(
capacity: CONFIG.pool_size
)
# CLI
Kemal.config.extra_options do |parser|
parser.banner = "Usage: invidious [arguments]"
@ -117,6 +126,9 @@ Kemal.config.extra_options do |parser|
parser.on("-l LEVEL", "--log-level=LEVEL", "Log level, one of #{LogLevel.values} (default: #{CONFIG.log_level})") do |log_level|
CONFIG.log_level = LogLevel.parse(log_level)
end
parser.on("-k", "--colorize", "Colorize logs") do
CONFIG.colorize_logs = true
end
parser.on("-v", "--version", "Print version") do
puts SOFTWARE.to_pretty_json
exit
@ -133,7 +145,7 @@ if CONFIG.output.upcase != "STDOUT"
FileUtils.mkdir_p(File.dirname(CONFIG.output))
end
OUTPUT = CONFIG.output.upcase == "STDOUT" ? STDOUT : File.open(CONFIG.output, mode: "a")
LOGGER = Invidious::LogHandler.new(OUTPUT, CONFIG.log_level)
LOGGER = Invidious::LogHandler.new(OUTPUT, CONFIG.log_level, CONFIG.colorize_logs)
# Check table integrity
Invidious::Database.check_integrity(CONFIG)
@ -184,11 +196,14 @@ if CONFIG.popular_enabled
Invidious::Jobs.register Invidious::Jobs::PullPopularVideosJob.new(PG_DB)
end
CONNECTION_CHANNEL = ::Channel({Bool, ::Channel(PQ::Notification)}).new(32)
Invidious::Jobs.register Invidious::Jobs::NotificationJob.new(CONNECTION_CHANNEL, CONFIG.database_url)
NOTIFICATION_CHANNEL = ::Channel(VideoNotification).new(32)
CONNECTION_CHANNEL = ::Channel({Bool, ::Channel(PQ::Notification)}).new(32)
Invidious::Jobs.register Invidious::Jobs::NotificationJob.new(NOTIFICATION_CHANNEL, CONNECTION_CHANNEL, CONFIG.database_url)
Invidious::Jobs.register Invidious::Jobs::ClearExpiredItemsJob.new
Invidious::Jobs.register Invidious::Jobs::InstanceListRefreshJob.new
Invidious::Jobs.start_all
def popular_videos
@ -229,8 +244,6 @@ add_context_storage_type(Preferences)
add_context_storage_type(Invidious::User)
Kemal.config.logger = LOGGER
Kemal.config.host_binding = Kemal.config.host_binding != "0.0.0.0" ? Kemal.config.host_binding : CONFIG.host_binding
Kemal.config.port = Kemal.config.port != 3000 ? Kemal.config.port : CONFIG.port
Kemal.config.app_name = "Invidious"
# Use in kemal's production mode.
@ -239,4 +252,16 @@ Kemal.config.app_name = "Invidious"
Kemal.config.env = "production" if !ENV.has_key?("KEMAL_ENV")
{% end %}
Kemal.run
Kemal.run do |config|
if socket_binding = CONFIG.socket_binding
File.delete?(socket_binding.path)
# Create a socket and set its desired permissions
server = UNIXServer.new(socket_binding.path)
perms = socket_binding.permissions.to_i(base: 8)
File.chmod(socket_binding.path, perms)
config.server.not_nil!.bind server
else
Kemal.config.host_binding = Kemal.config.host_binding != "0.0.0.0" ? Kemal.config.host_binding : CONFIG.host_binding
Kemal.config.port = Kemal.config.port != 3000 ? Kemal.config.port : CONFIG.port
end
end

View File

@ -223,7 +223,7 @@ def fetch_channel(ucid, pull_all_videos : Bool)
length_seconds = channel_video.try &.length_seconds
length_seconds ||= 0
live_now = channel_video.try &.live_now
live_now = channel_video.try &.badges.live_now?
live_now ||= false
premiere_timestamp = channel_video.try &.premiere_timestamp
@ -249,11 +249,7 @@ def fetch_channel(ucid, pull_all_videos : Bool)
if was_insert
LOGGER.trace("fetch_channel: #{ucid} : video #{video_id} : Inserted, updating subscriptions")
if CONFIG.enable_user_notifications
Invidious::Database::Users.add_notification(video)
else
Invidious::Database::Users.feed_needs_update(video)
end
NOTIFICATION_CHANNEL.send(VideoNotification.from_video(video))
else
LOGGER.trace("fetch_channel: #{ucid} : video #{video_id} : Updated")
end
@ -275,7 +271,7 @@ def fetch_channel(ucid, pull_all_videos : Bool)
ucid: video.ucid,
author: video.author,
length_seconds: video.length_seconds,
live_now: video.live_now,
live_now: video.badges.live_now?,
premiere_timestamp: video.premiere_timestamp,
views: video.views,
})
@ -285,11 +281,7 @@ def fetch_channel(ucid, pull_all_videos : Bool)
if Time.utc - video.published > 1.minute
was_insert = Invidious::Database::ChannelVideos.insert(video)
if was_insert
if CONFIG.enable_user_notifications
Invidious::Database::Users.add_notification(video)
else
Invidious::Database::Users.feed_needs_update(video)
end
NOTIFICATION_CHANNEL.send(VideoNotification.from_video(video))
end
end
end

View File

@ -44,3 +44,12 @@ def fetch_channel_releases(ucid, author, continuation)
end
return extract_items(initial_data, author, ucid)
end
def fetch_channel_courses(ucid, author, continuation)
if continuation
initial_data = YoutubeAPI.browse(continuation)
else
initial_data = YoutubeAPI.browse(ucid, params: "Egdjb3Vyc2Vz8gYFCgPCAQA%3D")
end
return extract_items(initial_data, author, ucid)
end

View File

@ -1,78 +1,3 @@
def produce_channel_content_continuation(ucid, content_type, page = 1, auto_generated = nil, sort_by = "newest", v2 = false)
object_inner_2 = {
"2:0:embedded" => {
"1:0:varint" => 0_i64,
},
"5:varint" => 50_i64,
"6:varint" => 1_i64,
"7:varint" => (page * 30).to_i64,
"9:varint" => 1_i64,
"10:varint" => 0_i64,
}
object_inner_2_encoded = object_inner_2
.try { |i| Protodec::Any.cast_json(i) }
.try { |i| Protodec::Any.from_json(i) }
.try { |i| Base64.urlsafe_encode(i) }
.try { |i| URI.encode_www_form(i) }
content_type_numerical =
case content_type
when "videos" then 15
when "livestreams" then 14
else 15 # Fallback to "videos"
end
sort_by_numerical =
case sort_by
when "newest" then 1_i64
when "popular" then 2_i64
when "oldest" then 4_i64
else 1_i64 # Fallback to "newest"
end
object_inner_1 = {
"110:embedded" => {
"3:embedded" => {
"#{content_type_numerical}:embedded" => {
"1:embedded" => {
"1:string" => object_inner_2_encoded,
},
"2:embedded" => {
"1:string" => "00000000-0000-0000-0000-000000000000",
},
"3:varint" => sort_by_numerical,
},
},
},
}
object_inner_1_encoded = object_inner_1
.try { |i| Protodec::Any.cast_json(i) }
.try { |i| Protodec::Any.from_json(i) }
.try { |i| Base64.urlsafe_encode(i) }
.try { |i| URI.encode_www_form(i) }
object = {
"80226972:embedded" => {
"2:string" => ucid,
"3:string" => object_inner_1_encoded,
"35:string" => "browse-feed#{ucid}videos102",
},
}
continuation = object.try { |i| Protodec::Any.cast_json(i) }
.try { |i| Protodec::Any.from_json(i) }
.try { |i| Base64.urlsafe_encode(i) }
.try { |i| URI.encode_www_form(i) }
return continuation
end
def make_initial_content_ctoken(ucid, content_type, sort_by) : String
return produce_channel_content_continuation(ucid, content_type, sort_by: sort_by)
end
module Invidious::Channel::Tabs
extend self
@ -101,7 +26,7 @@ module Invidious::Channel::Tabs
end
def get_videos(author : String, ucid : String, *, continuation : String? = nil, sort_by = "newest")
continuation ||= make_initial_content_ctoken(ucid, "videos", sort_by)
continuation ||= make_initial_videos_ctoken(ucid, sort_by)
initial_data = YoutubeAPI.browse(continuation: continuation)
return extract_items(initial_data, author, ucid)
@ -130,14 +55,10 @@ module Invidious::Channel::Tabs
# Shorts
# -------------------
def get_shorts(channel : AboutChannel, continuation : String? = nil)
if continuation.nil?
# EgZzaG9ydHPyBgUKA5oBAA%3D%3D is the protobuf object to load "shorts"
# TODO: try to extract the continuation tokens that allows other sorting options
initial_data = YoutubeAPI.browse(channel.ucid, params: "EgZzaG9ydHPyBgUKA5oBAA%3D%3D")
else
initial_data = YoutubeAPI.browse(continuation: continuation)
end
def get_shorts(channel : AboutChannel, *, continuation : String? = nil, sort_by = "newest")
continuation ||= make_initial_shorts_ctoken(channel.ucid, sort_by)
initial_data = YoutubeAPI.browse(continuation: continuation)
return extract_items(initial_data, channel.author, channel.ucid)
end
@ -145,9 +66,8 @@ module Invidious::Channel::Tabs
# Livestreams
# -------------------
def get_livestreams(channel : AboutChannel, continuation : String? = nil, sort_by = "newest")
continuation ||= make_initial_content_ctoken(channel.ucid, "livestreams", sort_by)
def get_livestreams(channel : AboutChannel, *, continuation : String? = nil, sort_by = "newest")
continuation ||= make_initial_livestreams_ctoken(channel.ucid, sort_by)
initial_data = YoutubeAPI.browse(continuation: continuation)
return extract_items(initial_data, channel.author, channel.ucid)
@ -171,4 +91,102 @@ module Invidious::Channel::Tabs
return items, next_continuation
end
# -------------------
# C-tokens
# -------------------
private def sort_options_videos_short(sort_by : String)
case sort_by
when "newest" then return 4_i64
when "popular" then return 2_i64
when "oldest" then return 5_i64
else return 4_i64 # Fallback to "newest"
end
end
# Generate the initial "continuation token" to get the first page of the
# "videos" tab. The following page requires the ctoken provided in that
# first page, and so on.
private def make_initial_videos_ctoken(ucid : String, sort_by = "newest")
object = {
"15:embedded" => {
"2:embedded" => {
"1:string" => "00000000-0000-0000-0000-000000000000",
},
"4:varint" => sort_options_videos_short(sort_by),
},
}
return channel_ctoken_wrap(ucid, object)
end
# Generate the initial "continuation token" to get the first page of the
# "shorts" tab. The following page requires the ctoken provided in that
# first page, and so on.
private def make_initial_shorts_ctoken(ucid : String, sort_by = "newest")
object = {
"10:embedded" => {
"2:embedded" => {
"1:string" => "00000000-0000-0000-0000-000000000000",
},
"4:varint" => sort_options_videos_short(sort_by),
},
}
return channel_ctoken_wrap(ucid, object)
end
# Generate the initial "continuation token" to get the first page of the
# "livestreams" tab. The following page requires the ctoken provided in that
# first page, and so on.
private def make_initial_livestreams_ctoken(ucid : String, sort_by = "newest")
sort_by_numerical =
case sort_by
when "newest" then 12_i64
when "popular" then 14_i64
when "oldest" then 13_i64
else 12_i64 # Fallback to "newest"
end
object = {
"14:embedded" => {
"2:embedded" => {
"1:string" => "00000000-0000-0000-0000-000000000000",
},
"5:varint" => sort_by_numerical,
},
}
return channel_ctoken_wrap(ucid, object)
end
# The protobuf structure common between videos/shorts/livestreams
private def channel_ctoken_wrap(ucid : String, object)
object_inner = {
"110:embedded" => {
"3:embedded" => object,
},
}
object_inner_encoded = object_inner
.try { |i| Protodec::Any.cast_json(i) }
.try { |i| Protodec::Any.from_json(i) }
.try { |i| Base64.urlsafe_encode(i) }
.try { |i| URI.encode_www_form(i) }
object = {
"80226972:embedded" => {
"2:string" => ucid,
"3:string" => object_inner_encoded,
},
}
continuation = object.try { |i| Protodec::Any.cast_json(i) }
.try { |i| Protodec::Any.from_json(i) }
.try { |i| Base64.urlsafe_encode(i) }
.try { |i| URI.encode_www_form(i) }
return continuation
end
end

View File

@ -8,11 +8,19 @@ struct DBConfig
property dbname : String
end
struct SocketBindingConfig
include YAML::Serializable
property path : String
property permissions : String
end
struct ConfigPreferences
include YAML::Serializable
property annotations : Bool = false
property annotations_subscribed : Bool = false
property preload : Bool = true
property autoplay : Bool = false
property captions : Array(String) = ["", "", ""]
property comments : Array(String) = ["youtube", ""]
@ -27,7 +35,7 @@ struct ConfigPreferences
property max_results : Int32 = 40
property notifications_only : Bool = false
property player_style : String = "invidious"
property quality : String = "hd720"
property quality : String = "dash"
property quality_dash : String = "auto"
property default_home : String? = "Popular"
property feed_menu : Array(String) = ["Popular", "Trending", "Subscriptions", "Playlists"]
@ -54,9 +62,28 @@ struct ConfigPreferences
end
end
struct HTTPProxyConfig
include YAML::Serializable
property user : String
property password : String
property host : String
property port : Int32
end
class Config
include YAML::Serializable
class CompanionConfig
include YAML::Serializable
@[YAML::Field(converter: Preferences::URIConverter)]
property private_url : URI = URI.parse("")
@[YAML::Field(converter: Preferences::URIConverter)]
property public_url : URI = URI.parse("")
end
# Number of threads to use for crawling videos from channels (for updating subscriptions)
property channel_threads : Int32 = 1
# Time interval between two executions of the job that crawls channel videos (subscriptions update).
@ -68,6 +95,8 @@ class Config
property output : String = "STDOUT"
# Default log level, valid YAML values are ints and strings, see src/invidious/helpers/logger.cr
property log_level : LogLevel = LogLevel::Info
# Enables colors in logs. Useful for debugging purposes
property colorize_logs : Bool = false
# Database configuration with separate parameters (username, hostname, etc)
property db : DBConfig? = nil
@ -126,8 +155,12 @@ class Config
property port : Int32 = 3000
# Host to bind (overridden by command line argument)
property host_binding : String = "0.0.0.0"
# Path and permissions to make Invidious listen on a UNIX socket instead of a TCP port
property socket_binding : SocketBindingConfig? = nil
# Pool size for HTTP requests to youtube.com and ytimg.com (each domain has a separate pool of `pool_size`)
property pool_size : Int32 = 100
# HTTP Proxy configuration
property http_proxy : HTTPProxyConfig? = nil
# Use Innertube's transcripts API instead of timedtext for closed captions
property use_innertube_for_captions : Bool = false
@ -137,6 +170,12 @@ class Config
# poToken for passing bot attestation
property po_token : String? = nil
# Invidious companion
property invidious_companion : Array(CompanionConfig) = [] of CompanionConfig
# Invidious companion API key
property invidious_companion_key : String = ""
# Saved cookies in "name1=value1; name2=value2..." format
@[YAML::Field(converter: Preferences::StringToCookies)]
property cookies : HTTP::Cookies = HTTP::Cookies.new
@ -170,6 +209,9 @@ class Config
config = Config.from_yaml(config_yaml)
# Update config from env vars (upcased and prefixed with "INVIDIOUS_")
#
# Also checks if any top-level config options are set to "CHANGE_ME!!"
# TODO: Support non-top-level config options such as the ones in DBConfig
{% for ivar in Config.instance_vars %}
{% env_id = "INVIDIOUS_#{ivar.id.upcase}" %}
@ -206,16 +248,40 @@ class Config
exit(1)
end
end
# Warn when any config attribute is set to "CHANGE_ME!!"
if config.{{ivar.id}} == "CHANGE_ME!!"
puts "Config: The value of '#{ {{ivar.stringify}} }' needs to be changed!!"
exit(1)
end
{% end %}
if config.invidious_companion.present?
# invidious_companion and signature_server can't work together
if config.signature_server
puts "Config: You can not run inv_sig_helper and invidious_companion at the same time."
exit(1)
elsif config.invidious_companion_key.empty?
puts "Config: Please configure a key if you are using invidious companion."
exit(1)
elsif config.invidious_companion_key == "CHANGE_ME!!"
puts "Config: The value of 'invidious_companion_key' needs to be changed!!"
exit(1)
elsif config.invidious_companion_key.size != 16
puts "Config: The value of 'invidious_companion_key' needs to be a size of 16 characters."
exit(1)
end
elsif config.signature_server
puts("WARNING: inv-sig-helper is deprecated. Please switch to Invidious companion: https://docs.invidious.io/companion-installation/")
else
puts("WARNING: Invidious companion is required to view and playback videos. For more information see https://docs.invidious.io/companion-installation/")
end
# HMAC_key is mandatory
# See: https://github.com/iv-org/invidious/issues/3854
if config.hmac_key.empty?
puts "Config: 'hmac_key' is required/can't be empty"
exit(1)
elsif config.hmac_key == "CHANGE_ME!!"
puts "Config: The value of 'hmac_key' needs to be changed!!"
exit(1)
end
# Build database_url from db.* if it's not set directly
@ -235,6 +301,24 @@ class Config
end
end
# Check if the socket configuration is valid
if sb = config.socket_binding
if sb.path.ends_with?("/") || File.directory?(sb.path)
puts "Config: The socket path " + sb.path + " must not be a directory!"
exit(1)
end
d = File.dirname(sb.path)
if !File.directory?(d)
puts "Config: Socket directory " + sb.path + " does not exist or is not a directory!"
exit(1)
end
p = sb.permissions.to_i?(base: 8)
if !p || p < 0 || p > 0o777
puts "Config: Socket permissions must be an octal between 0 and 777!"
exit(1)
end
end
return config
end
end

View File

@ -119,15 +119,15 @@ module Invidious::Database::Users
# Update (notifs)
# -------------------
def add_notification(video : ChannelVideo)
def add_multiple_notifications(channel_id : String, video_ids : Array(String))
request = <<-SQL
UPDATE users
SET notifications = array_append(notifications, $1),
SET notifications = array_cat(notifications, $1),
feed_needs_update = true
WHERE $2 = ANY(subscriptions)
SQL
PG_DB.exec(request, video.id, video.ucid)
PG_DB.exec(request, video_ids, channel_id)
end
def remove_notification(user : User, vid : String)
@ -154,14 +154,14 @@ module Invidious::Database::Users
# Update (misc)
# -------------------
def feed_needs_update(video : ChannelVideo)
def feed_needs_update(channel_id : String)
request = <<-SQL
UPDATE users
SET feed_needs_update = true
WHERE $1 = ANY(subscriptions)
SQL
PG_DB.exec(request, video.ucid)
PG_DB.exec(request, channel_id)
end
def update_preferences(user : User)

View File

@ -7,8 +7,9 @@ module Invidious::Frontend::ChannelPage
Streams
Podcasts
Releases
Courses
Playlists
Community
Posts
Channels
end

View File

@ -3,6 +3,24 @@ require "uri"
module Invidious::Frontend::Pagination
extend self
private def first_page(str : String::Builder, locale : String?, url : String)
str << %(<a href=") << url << %(" class="pure-button pure-button-secondary">)
if locale_is_rtl?(locale)
# Inverted arrow ("first" points to the right)
str << translate(locale, "First page")
str << "&nbsp;&nbsp;"
str << %(<i class="icon ion-ios-arrow-forward"></i>)
else
# Regular arrow ("first" points to the left)
str << %(<i class="icon ion-ios-arrow-back"></i>)
str << "&nbsp;&nbsp;"
str << translate(locale, "First page")
end
str << "</a>"
end
private def previous_page(str : String::Builder, locale : String?, url : String)
# Link
str << %(<a href=") << url << %(" class="pure-button pure-button-secondary">)
@ -72,18 +90,24 @@ module Invidious::Frontend::Pagination
end
end
def nav_ctoken(locale : String?, *, base_url : String | URI, ctoken : String?)
def nav_ctoken(locale : String?, *, base_url : String | URI, ctoken : String?, first_page : Bool, params : URI::Params)
return String.build do |str|
str << %(<div class="h-box">\n)
str << %(<div class="page-nav-container flexible">\n)
str << %(<div class="page-prev-container flex-left"></div>\n)
str << %(<div class="page-prev-container flex-left">)
if !first_page
self.first_page(str, locale, base_url.to_s)
end
str << %(</div>\n)
str << %(<div class="page-next-container flex-right">)
if !ctoken.nil?
params_next = URI::Params{"continuation" => ctoken}
url_next = HttpServer::Utils.add_params_to_url(base_url, params_next)
params["continuation"] = ctoken
url_next = HttpServer::Utils.add_params_to_url(base_url, params)
self.next_page(str, locale, url_next.to_s)
end

View File

@ -13,7 +13,7 @@ module Invidious::Frontend::WatchPage
@full_videos,
@video_streams,
@audio_streams,
@captions
@captions,
)
end
end

View File

@ -43,6 +43,8 @@ def error_template_helper(env : HTTP::Server::Context, status_code : Int32, exce
# URLs for the error message below
url_faq = "https://github.com/iv-org/documentation/blob/master/docs/faq.md"
url_search_issues = "https://github.com/iv-org/invidious/issues"
url_search_issues += "?q=is:issue+is:open+"
url_search_issues += URI.encode_www_form("[Bug] #{issue_title}")
url_switch = "https://redirect.invidious.io" + env.request.resource
@ -128,7 +130,7 @@ def error_json_helper(
env : HTTP::Server::Context,
status_code : Int32,
exception : Exception,
additional_fields : Hash(String, Object) | Nil = nil
additional_fields : Hash(String, Object) | Nil = nil,
)
if exception.is_a?(InfoException)
return error_json_helper(env, status_code, exception.message || "", additional_fields)
@ -150,7 +152,7 @@ def error_json_helper(
env : HTTP::Server::Context,
status_code : Int32,
message : String,
additional_fields : Hash(String, Object) | Nil = nil
additional_fields : Hash(String, Object) | Nil = nil,
)
env.response.content_type = "application/json"
env.response.status_code = status_code

View File

@ -27,6 +27,7 @@ class Kemal::RouteHandler
# Processes the route if it's a match. Otherwise renders 404.
private def process_request(context)
raise Kemal::Exceptions::RouteNotFound.new(context) unless context.route_found?
return if context.response.closed?
content = context.route.handler.call(context)
if !Kemal.config.error_handlers.empty? && Kemal.config.error_handlers.has_key?(context.response.status_code) && exclude_match?(context)

View File

@ -1,8 +1,22 @@
# Languages requiring a better level of translation (at least 20%)
# to be added to the list below:
#
# "af" => "", # Afrikaans
# "az" => "", # Azerbaijani
# "be" => "", # Belarusian
# "bn_BD" => "", # Bengali (Bangladesh)
# "ia" => "", # Interlingua
# "or" => "", # Odia
# "tk" => "", # Turkmen
# "tok => "", # Toki Pona
#
LOCALES_LIST = {
"ar" => "العربية", # Arabic
"bg" => "български", # Bulgarian
"bn" => "বাংলা", # Bengali
"ca" => "Català", # Catalan
"cs" => "Čeština", # Czech
"cy" => "Cymraeg", # Welsh
"da" => "Dansk", # Danish
"de" => "Deutsch", # German
"el" => "Ελληνικά", # Greek
@ -23,6 +37,7 @@ LOCALES_LIST = {
"it" => "Italiano", # Italian
"ja" => "日本語", # Japanese
"ko" => "한국어", # Korean
"lmo" => "Lombard", # Lombard
"lt" => "Lietuvių", # Lithuanian
"nb-NO" => "Norsk bokmål", # Norwegian Bokmål
"nl" => "Nederlands", # Dutch
@ -39,6 +54,7 @@ LOCALES_LIST = {
"sr" => "Srpski (latinica)", # Serbian (Latin)
"sr_Cyrl" => "Српски (ћирилица)", # Serbian (Cyrillic)
"sv-SE" => "Svenska", # Swedish
"ta" => "தமிழ்", # Tamil
"tr" => "Türkçe", # Turkish
"uk" => "Українська", # Ukrainian
"vi" => "Tiếng Việt", # Vietnamese

View File

@ -1,3 +1,5 @@
require "colorize"
enum LogLevel
All = 0
Trace = 1
@ -10,7 +12,9 @@ enum LogLevel
end
class Invidious::LogHandler < Kemal::BaseLogHandler
def initialize(@io : IO = STDOUT, @level = LogLevel::Debug)
def initialize(@io : IO = STDOUT, @level = LogLevel::Debug, use_color : Bool = true)
Colorize.enabled = use_color
Colorize.on_tty_only!
end
def call(context : HTTP::Server::Context)
@ -39,10 +43,22 @@ class Invidious::LogHandler < Kemal::BaseLogHandler
@io.flush
end
def color(level)
case level
when LogLevel::Trace then :cyan
when LogLevel::Debug then :green
when LogLevel::Info then :white
when LogLevel::Warn then :yellow
when LogLevel::Error then :red
when LogLevel::Fatal then :magenta
else :default
end
end
{% for level in %w(trace debug info warn error fatal) %}
def {{level.id}}(message : String)
if LogLevel::{{level.id.capitalize}} >= @level
puts("#{Time.utc} [{{level.id}}] #{message}")
puts("#{Time.utc} [{{level.id}}] #{message}".colorize(color(LogLevel::{{level.id.capitalize}})))
end
end
{% end %}

View File

@ -1,3 +1,16 @@
@[Flags]
enum VideoBadges
LiveNow
Premium
ThreeD
FourK
New
EightK
VR180
VR360
ClosedCaptions
end
struct SearchVideo
include DB::Serializable
@ -9,10 +22,10 @@ struct SearchVideo
property views : Int64
property description_html : String
property length_seconds : Int32
property live_now : Bool
property premium : Bool
property premiere_timestamp : Time?
property author_verified : Bool
property author_thumbnail : String?
property badges : VideoBadges
def to_xml(auto_generated, query_params, xml : XML::Builder)
query_params["v"] = self.id
@ -76,6 +89,24 @@ struct SearchVideo
json.field "authorUrl", "/channel/#{self.ucid}"
json.field "authorVerified", self.author_verified
author_thumbnail = self.author_thumbnail
if author_thumbnail
json.field "authorThumbnails" do
json.array do
qualities = {32, 48, 76, 100, 176, 512}
qualities.each do |quality|
json.object do
json.field "url", author_thumbnail.gsub(/=s\d+/, "=s#{quality}")
json.field "width", quality
json.field "height", quality
end
end
end
end
end
json.field "videoThumbnails" do
Invidious::JSONify::APIv1.thumbnails(json, self.id)
end
@ -88,13 +119,20 @@ struct SearchVideo
json.field "published", self.published.to_unix
json.field "publishedText", translate(locale, "`x` ago", recode_date(self.published, locale))
json.field "lengthSeconds", self.length_seconds
json.field "liveNow", self.live_now
json.field "premium", self.premium
json.field "liveNow", self.badges.live_now?
json.field "premium", self.badges.premium?
json.field "isUpcoming", self.upcoming?
if self.premiere_timestamp
json.field "premiereTimestamp", self.premiere_timestamp.try &.to_unix
end
json.field "isNew", self.badges.new?
json.field "is4k", self.badges.four_k?
json.field "is8k", self.badges.eight_k?
json.field "isVr180", self.badges.vr180?
json.field "isVr360", self.badges.vr360?
json.field "is3d", self.badges.three_d?
json.field "hasCaptions", self.badges.closed_captions?
end
end
@ -204,7 +242,7 @@ struct SearchChannel
qualities.each do |quality|
json.object do
json.field "url", self.author_thumbnail.gsub(/=\d+/, "=s#{quality}")
json.field "url", self.author_thumbnail.gsub(/=s\d+/, "=s#{quality}")
json.field "width", quality
json.field "height", quality
end

View File

@ -175,8 +175,9 @@ module Invidious::SigHelper
@queue = {} of TransactionID => Transaction
@conn : Connection
@uri_or_path : String
def initialize(uri_or_path)
def initialize(@uri_or_path)
@conn = Connection.new(uri_or_path)
listen
end
@ -186,10 +187,26 @@ module Invidious::SigHelper
LOGGER.debug("SigHelper: Multiplexor listening")
# TODO: reopen socket if unexpectedly closed
spawn do
loop do
receive_data
begin
receive_data
rescue ex
LOGGER.info("SigHelper: Connection to helper died with '#{ex.message}' trying to reconnect...")
# We close the socket because for some reason is not closed.
@conn.close
loop do
begin
@conn = Connection.new(@uri_or_path)
LOGGER.info("SigHelper: Reconnected to SigHelper!")
rescue ex
LOGGER.debug("SigHelper: Reconnection to helper unsuccessful with error '#{ex.message}'. Retrying")
sleep 500.milliseconds
next
end
break if !@conn.closed?
end
end
Fiber.yield
end
end

View File

@ -323,68 +323,6 @@ def parse_range(range)
return 0_i64, nil
end
def fetch_random_instance
begin
instance_api_client = make_client(URI.parse("https://api.invidious.io"))
# Timeouts
instance_api_client.connect_timeout = 10.seconds
instance_api_client.dns_timeout = 10.seconds
instance_list = JSON.parse(instance_api_client.get("/instances.json").body).as_a
instance_api_client.close
rescue Socket::ConnectError | IO::TimeoutError | JSON::ParseException
instance_list = [] of JSON::Any
end
filtered_instance_list = [] of String
instance_list.each do |data|
# TODO Check if current URL is onion instance and use .onion types if so.
if data[1]["type"] == "https"
# Instances can have statistics disabled, which is an requirement of version validation.
# as_nil? doesn't exist. Thus we'll have to handle the error raised if as_nil fails.
begin
data[1]["stats"].as_nil
next
rescue TypeCastError
end
# stats endpoint could also lack the software dict.
next if data[1]["stats"]["software"]?.nil?
# Makes sure the instance isn't too outdated.
if remote_version = data[1]["stats"]?.try &.["software"]?.try &.["version"]
remote_commit_date = remote_version.as_s.match(/\d{4}\.\d{2}\.\d{2}/)
next if !remote_commit_date
remote_commit_date = Time.parse(remote_commit_date[0], "%Y.%m.%d", Time::Location::UTC)
local_commit_date = Time.parse(CURRENT_VERSION, "%Y.%m.%d", Time::Location::UTC)
next if (remote_commit_date - local_commit_date).abs.days > 30
begin
data[1]["monitor"].as_nil
health = data[1]["monitor"].as_h["dailyRatios"][0].as_h["ratio"]
filtered_instance_list << data[0].as_s if health.to_s.to_f > 90
rescue TypeCastError
# We can't check the health if the monitoring is broken. Thus we'll just add it to the list
# and move on. Ideally we'll ignore any instance that has broken health monitoring but due to the fact that
# it's an error that often occurs with all the instances at the same time, we have to just skip the check.
filtered_instance_list << data[0].as_s
end
end
end
end
# If for some reason no instances managed to get fetched successfully then we'll just redirect to redirect.invidious.io
if filtered_instance_list.size == 0
return "redirect.invidious.io"
end
return filtered_instance_list.sample(1)[0]
end
def reduce_uri(uri : URI | String, max_length : Int32 = 50, suffix : String = "") : String
str = uri.to_s.sub(/^https?:\/\//, "")
if str.size > max_length
@ -445,3 +383,22 @@ def parse_link_endpoint(endpoint : JSON::Any, text : String, video_id : String)
end
return text
end
def encrypt_ecb_without_salt(data, key)
cipher = OpenSSL::Cipher.new("aes-128-ecb")
cipher.encrypt
cipher.key = key
io = IO::Memory.new
io.write(cipher.update(data))
io.write(cipher.final)
io.rewind
return io
end
def invidious_companion_encrypt(data)
timestamp = Time.utc.to_unix
encrypted_data = encrypt_ecb_without_salt("#{timestamp}|#{data}", CONFIG.invidious_companion_key)
return Base64.urlsafe_encode(encrypted_data)
end

View File

@ -0,0 +1,97 @@
class Invidious::Jobs::InstanceListRefreshJob < Invidious::Jobs::BaseJob
# We update the internals of a constant as so it can be accessed from anywhere
# within the codebase
#
# "INSTANCES" => Array(Tuple(String, String)) # region, instance
INSTANCES = {"INSTANCES" => [] of Tuple(String, String)}
def initialize
end
def begin
loop do
refresh_instances
LOGGER.info("InstanceListRefreshJob: Done, sleeping for 30 minutes")
sleep 30.minute
Fiber.yield
end
end
# Refreshes the list of instances used for redirects.
#
# Does the following three checks for each instance
# - Is it a clear-net instance?
# - Is it an instance with a good uptime?
# - Is it an updated instance?
private def refresh_instances
raw_instance_list = self.fetch_instances
filtered_instance_list = [] of Tuple(String, String)
raw_instance_list.each do |instance_data|
# TODO allow Tor hidden service instances when the current instance
# is also a hidden service. Same for i2p and any other non-clearnet instances.
begin
domain = instance_data[0]
info = instance_data[1]
stats = info["stats"]
next unless info["type"] == "https"
next if bad_uptime?(info["monitor"])
next if outdated?(stats["software"]["version"])
filtered_instance_list << {info["region"].as_s, domain.as_s}
rescue ex
if domain
LOGGER.info("InstanceListRefreshJob: failed to parse information from '#{domain}' because \"#{ex}\"\n\"#{ex.backtrace.join('\n')}\" ")
else
LOGGER.info("InstanceListRefreshJob: failed to parse information from an instance because \"#{ex}\"\n\"#{ex.backtrace.join('\n')}\" ")
end
end
end
if !filtered_instance_list.empty?
INSTANCES["INSTANCES"] = filtered_instance_list
end
end
# Fetches information regarding instances from api.invidious.io or an otherwise configured URL
private def fetch_instances : Array(JSON::Any)
begin
# We directly call the stdlib HTTP::Client here as it allows us to negate the effects
# of the force_resolve config option. This is needed as api.invidious.io does not support ipv6
# and as such the following request raises if we were to use force_resolve with the ipv6 value.
instance_api_client = HTTP::Client.new(URI.parse("https://api.invidious.io"))
# Timeouts
instance_api_client.connect_timeout = 10.seconds
instance_api_client.dns_timeout = 10.seconds
raw_instance_list = JSON.parse(instance_api_client.get("/instances.json").body).as_a
instance_api_client.close
rescue ex : Socket::ConnectError | IO::TimeoutError | JSON::ParseException
raw_instance_list = [] of JSON::Any
end
return raw_instance_list
end
# Checks if the given target instance is outdated
private def outdated?(target_instance_version) : Bool
remote_commit_date = target_instance_version.as_s.match(/\d{4}\.\d{2}\.\d{2}/)
return false if !remote_commit_date
remote_commit_date = Time.parse(remote_commit_date[0], "%Y.%m.%d", Time::Location::UTC)
local_commit_date = Time.parse(CURRENT_VERSION, "%Y.%m.%d", Time::Location::UTC)
return (remote_commit_date - local_commit_date).abs.days > 30
end
# Checks if the uptime of the target instance is greater than 90% over a 30 day period
private def bad_uptime?(target_instance_health_monitor) : Bool
return true if !target_instance_health_monitor["down"].as_bool == false
return true if target_instance_health_monitor["uptime"].as_f < 90
return false
end
end

View File

@ -1,8 +1,32 @@
struct VideoNotification
getter video_id : String
getter channel_id : String
getter published : Time
def_hash @channel_id, @video_id
def ==(other)
video_id == other.video_id
end
def self.from_video(video : ChannelVideo) : self
VideoNotification.new(video.id, video.ucid, video.published)
end
def initialize(@video_id, @channel_id, @published)
end
def clone : VideoNotification
VideoNotification.new(video_id.clone, channel_id.clone, published.clone)
end
end
class Invidious::Jobs::NotificationJob < Invidious::Jobs::BaseJob
private getter notification_channel : ::Channel(VideoNotification)
private getter connection_channel : ::Channel({Bool, ::Channel(PQ::Notification)})
private getter pg_url : URI
def initialize(@connection_channel, @pg_url)
def initialize(@notification_channel, @connection_channel, @pg_url)
end
def begin
@ -10,6 +34,70 @@ class Invidious::Jobs::NotificationJob < Invidious::Jobs::BaseJob
PG.connect_listen(pg_url, "notifications") { |event| connections.each(&.send(event)) }
# hash of channels to their videos (id+published) that need notifying
to_notify = Hash(String, Set(VideoNotification)).new(
->(hash : Hash(String, Set(VideoNotification)), key : String) {
hash[key] = Set(VideoNotification).new
}
)
notify_mutex = Mutex.new
# fiber to locally cache all incoming notifications (from pubsub webhooks and refresh channels job)
spawn do
begin
loop do
notification = notification_channel.receive
notify_mutex.synchronize do
to_notify[notification.channel_id] << notification
end
end
end
end
# fiber to regularly persist all cached notifications
spawn do
loop do
begin
LOGGER.debug("NotificationJob: waking up")
cloned = {} of String => Set(VideoNotification)
notify_mutex.synchronize do
cloned = to_notify.clone
to_notify.clear
end
cloned.each do |channel_id, notifications|
if notifications.empty?
next
end
LOGGER.info("NotificationJob: updating channel #{channel_id} with #{notifications.size} notifications")
if CONFIG.enable_user_notifications
video_ids = notifications.map(&.video_id)
Invidious::Database::Users.add_multiple_notifications(channel_id, video_ids)
PG_DB.using_connection do |conn|
notifications.each do |n|
# Deliver notifications to `/api/v1/auth/notifications`
payload = {
"topic" => n.channel_id,
"videoId" => n.video_id,
"published" => n.published.to_unix,
}.to_json
conn.exec("NOTIFY notifications, E'#{payload}'")
end
end
else
Invidious::Database::Users.feed_needs_update(channel_id)
end
end
LOGGER.trace("NotificationJob: Done, sleeping")
rescue ex
LOGGER.error("NotificationJob: #{ex.message}")
end
sleep 1.minute
Fiber.yield
end
end
loop do
action, connection = connection_channel.receive

View File

@ -267,6 +267,12 @@ module Invidious::JSONify::APIv1
json.field "lengthSeconds", rv["length_seconds"]?.try &.to_i
json.field "viewCountText", rv["short_view_count"]?
json.field "viewCount", rv["view_count"]?.try &.empty? ? nil : rv["view_count"].to_i64
json.field "published", rv["published"]?
if rv["published"]?.try &.presence
json.field "publishedText", translate(locale, "`x` ago", recode_date(Time.parse_rfc3339(rv["published"].to_s), locale))
else
json.field "publishedText", ""
end
end
end
end

View File

@ -81,7 +81,7 @@ def fetch_mix(rdid, video_id, cookies = nil, locale = nil)
})
end
def template_mix(mix)
def template_mix(mix, listen)
html = <<-END_HTML
<h3>
<a href="/mix?list=#{mix["mixId"]}">
@ -95,7 +95,7 @@ def template_mix(mix)
mix["videos"].as_a.each do |video|
html += <<-END_HTML
<li class="pure-menu-item">
<a href="/watch?v=#{video["videoId"]}&list=#{mix["mixId"]}">
<a href="/watch?v=#{video["videoId"]}&list=#{mix["mixId"]}#{listen ? "&listen=1" : ""}">
<div class="thumbnail">
<img loading="lazy" class="thumbnail" src="/vi/#{video["videoId"]}/mqdefault.jpg" alt="" />
<p class="length">#{recode_length_seconds(video["lengthSeconds"].as_i)}</p>

View File

@ -270,7 +270,7 @@ end
def subscribe_playlist(user, playlist)
playlist = InvidiousPlaylist.new({
title: playlist.title.byte_slice(0, 150),
title: playlist.title[..150],
id: playlist.id,
author: user.email,
description: "", # Max 5000 characters
@ -505,7 +505,7 @@ def extract_playlist_videos(initial_data : Hash(String, JSON::Any))
return videos
end
def template_playlist(playlist)
def template_playlist(playlist, listen)
html = <<-END_HTML
<h3>
<a href="/playlist?list=#{playlist["playlistId"]}">
@ -519,7 +519,7 @@ def template_playlist(playlist)
playlist["videos"].as_a.each do |video|
html += <<-END_HTML
<li class="pure-menu-item" id="#{video["videoId"]}">
<a href="/watch?v=#{video["videoId"]}&list=#{playlist["playlistId"]}&index=#{video["index"]}">
<a href="/watch?v=#{video["videoId"]}&list=#{playlist["playlistId"]}&index=#{video["index"]}#{listen ? "&listen=1" : ""}">
<div class="thumbnail">
<img loading="lazy" class="thumbnail" src="/vi/#{video["videoId"]}/mqdefault.jpg" alt="" />
<p class="length">#{recode_length_seconds(video["lengthSeconds"].as_i)}</p>

View File

@ -328,17 +328,9 @@ module Invidious::Routes::Account
end
end
if env.params.query["action_revoke_token"]?
action = "action_revoke_token"
else
return env.redirect referer
end
session = env.params.query["session"]?
session ||= ""
case action
when .starts_with? "action_revoke_token"
case action = env.params.query["action"]?
when "revoke_token"
session = env.params.query["session"]
Invidious::Database::SessionIDs.delete(sid: session, email: user.email)
else
return error_json(400, "Unsupported action #{action}")

View File

@ -8,6 +8,11 @@ module Invidious::Routes::API::Manifest
id = env.params.url["id"]
region = env.params.query["region"]?
if CONFIG.invidious_companion.present?
invidious_companion = CONFIG.invidious_companion.sample
return env.redirect "#{invidious_companion.public_url}/api/manifest/dash/id/#{id}?#{env.params.query}"
end
# Since some implementations create playlists based on resolution regardless of different codecs,
# we can opt to only add a source to a representation if it has a unique height within that representation
unique_res = env.params.query["unique_res"]?.try { |q| (q == "true" || q == "1").to_unsafe }
@ -27,28 +32,21 @@ module Invidious::Routes::API::Manifest
haltf env, status_code: response.status_code
end
manifest = response.body
manifest = manifest.gsub(/<BaseURL>[^<]+<\/BaseURL>/) do |baseurl|
url = baseurl.lchop("<BaseURL>")
url = url.rchop("</BaseURL>")
if local
uri = URI.parse(url)
url = "#{HOST_URL}#{uri.request_target}host/#{uri.host}/"
end
# Proxy URLs for video playback on invidious.
# Other API clients can get the original URLs by omiting `local=true`.
manifest = response.body.gsub(/<BaseURL>[^<]+<\/BaseURL>/) do |baseurl|
url = baseurl.lchop("<BaseURL>").rchop("</BaseURL>")
url = HttpServer::Utils.proxy_video_url(url, absolute: true) if local
"<BaseURL>#{url}</BaseURL>"
end
return manifest
end
adaptive_fmts = video.adaptive_fmts
# Ditto, only proxify URLs if `local=true` is used
if local
adaptive_fmts.each do |fmt|
fmt["url"] = JSON::Any.new("#{HOST_URL}#{URI.parse(fmt["url"].as_s).request_target}")
video.adaptive_fmts.each do |fmt|
fmt["url"] = JSON::Any.new(HttpServer::Utils.proxy_video_url(fmt["url"].as_s, absolute: true))
end
end
@ -70,17 +68,23 @@ module Invidious::Routes::API::Manifest
# OTF streams aren't supported yet (See https://github.com/TeamNewPipe/NewPipe/issues/2415)
next if !(fmt.has_key?("indexRange") && fmt.has_key?("initRange"))
audio_track = fmt["audioTrack"]?.try &.as_h? || {} of String => JSON::Any
lang = audio_track["id"]?.try &.as_s.split('.')[0] || "und"
is_default = audio_track.has_key?("audioIsDefault") ? audio_track["audioIsDefault"].as_bool : i == 0
displayname = audio_track["displayName"]?.try &.as_s || "Unknown"
bitrate = fmt["bitrate"]
# Different representations of the same audio should be groupped into one AdaptationSet.
# However, most players don't support auto quality switching, so we have to trick them
# into providing a quality selector.
# See https://github.com/iv-org/invidious/issues/3074 for more details.
xml.element("AdaptationSet", id: i, mimeType: mime_type, startWithSAP: 1, subsegmentAlignment: true, label: fmt["bitrate"].to_s + "k") do
xml.element("AdaptationSet", id: i, mimeType: mime_type, startWithSAP: 1, subsegmentAlignment: true, label: "#{displayname} [#{bitrate}k]", lang: lang) do
codecs = fmt["mimeType"].as_s.split("codecs=")[1].strip('"')
bandwidth = fmt["bitrate"].as_i
itag = fmt["itag"].as_i
url = fmt["url"].as_s
xml.element("Role", schemeIdUri: "urn:mpeg:dash:role:2011", value: i == 0 ? "main" : "alternate")
xml.element("Role", schemeIdUri: "urn:mpeg:dash:role:2011", value: is_default ? "main" : "alternate")
xml.element("Representation", id: fmt["itag"], codecs: codecs, bandwidth: bandwidth) do
xml.element("AudioChannelConfiguration", schemeIdUri: "urn:mpeg:dash:23003:3:audio_channel_configuration:2011",
@ -177,8 +181,9 @@ module Invidious::Routes::API::Manifest
manifest = response.body
if local
manifest = manifest.gsub(/^https:\/\/\w+---.{11}\.c\.youtube\.com[^\n]*/m) do |match|
path = URI.parse(match).path
manifest = manifest.gsub(/https:\/\/[^\n"]*/m) do |match|
uri = URI.parse(match)
path = uri.path
path = path.lchop("/videoplayback/")
path = path.rchop("/")
@ -207,7 +212,7 @@ module Invidious::Routes::API::Manifest
raw_params["fvip"] = fvip["fvip"]
end
raw_params["local"] = "true"
raw_params["host"] = uri.host.not_nil!
"#{HOST_URL}/videoplayback?#{raw_params}"
end

View File

@ -197,6 +197,7 @@ module Invidious::Routes::API::V1::Channels
get_channel()
# Retrieve continuation from URL parameters
sort_by = env.params.query["sort_by"]?.try &.downcase || "newest"
continuation = env.params.query["continuation"]?
if channel.is_age_gated
@ -211,7 +212,7 @@ module Invidious::Routes::API::V1::Channels
else
begin
videos, next_continuation = Channel::Tabs.get_shorts(
channel, continuation: continuation
channel, continuation: continuation, sort_by: sort_by
)
rescue ex
return error_json(500, ex)
@ -367,6 +368,35 @@ module Invidious::Routes::API::V1::Channels
end
end
def self.courses(env)
locale = env.get("preferences").as(Preferences).locale
env.response.content_type = "application/json"
ucid = env.params.url["ucid"]
continuation = env.params.query["continuation"]?
# Use the macro defined above
channel = nil # Make the compiler happy
get_channel()
items, next_continuation = fetch_channel_courses(channel.ucid, channel.author, continuation)
JSON.build do |json|
json.object do
json.field "playlists" do
json.array do
items.each do |item|
item.to_json(locale, json) if item.is_a?(SearchPlaylist)
end
end
end
json.field "continuation", next_continuation if next_continuation
end
end
end
def self.community(env)
locale = env.get("preferences").as(Preferences).locale

View File

@ -42,6 +42,9 @@ module Invidious::Routes::API::V1::Misc
format = env.params.query["format"]?
format ||= "json"
listen_param = env.params.query["listen"]?
listen = (listen_param == "true" || listen_param == "1")
if plid.starts_with? "RD"
return env.redirect "/api/v1/mixes/#{plid}"
end
@ -85,7 +88,7 @@ module Invidious::Routes::API::V1::Misc
end
if format == "html"
playlist_html = template_playlist(json_response)
playlist_html = template_playlist(json_response, listen)
index, next_video = json_response["videos"].as_a.skip(1 + lookback).select { |video| !video["author"].as_s.empty? }[0]?.try { |v| {v["index"], v["videoId"]} } || {nil, nil}
response = {
@ -111,6 +114,9 @@ module Invidious::Routes::API::V1::Misc
format = env.params.query["format"]?
format ||= "json"
listen_param = env.params.query["listen"]?
listen = (listen_param == "true" || listen_param == "1")
begin
mix = fetch_mix(rdid, continuation, locale: locale)
@ -141,9 +147,7 @@ module Invidious::Routes::API::V1::Misc
json.field "authorUrl", "/channel/#{video.ucid}"
json.field "videoThumbnails" do
json.array do
Invidious::JSONify::APIv1.thumbnails(json, video.id)
end
Invidious::JSONify::APIv1.thumbnails(json, video.id)
end
json.field "index", video.index
@ -157,7 +161,7 @@ module Invidious::Routes::API::V1::Misc
if format == "html"
response = JSON.parse(response)
playlist_html = template_mix(response)
playlist_html = template_mix(response, listen)
next_video = response["videos"].as_a.select { |video| !video["author"].as_s.empty? }[0]?.try &.["videoId"]
response = {

View File

@ -31,9 +31,7 @@ module Invidious::Routes::API::V1::Search
query = env.params.query["q"]? || ""
begin
client = HTTP::Client.new("suggestqueries-clients6.youtube.com")
client.before_request { |r| add_yt_headers(r) }
client = make_client(URI.parse("https://suggestqueries-clients6.youtube.com"), force_youtube_headers: true)
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

View File

@ -429,4 +429,90 @@ module Invidious::Routes::API::V1::Videos
end
end
end
# Fetches transcripts from YouTube
#
# Use the `lang` and `autogen` query parameter to select which transcript to fetch
# Request without any URL parameters to see all the available transcripts.
def self.transcripts(env)
env.response.content_type = "application/json"
id = env.params.url["id"]
lang = env.params.query["lang"]?
label = env.params.query["label"]?
auto_generated = env.params.query["autogen"]? ? true : false
# Return all available transcript options when none is given
if !label && !lang
begin
video = get_video(id)
rescue ex : NotFoundException
return error_json(404, ex)
rescue ex
return error_json(500, ex)
end
response = JSON.build do |json|
# The amount of transcripts available to fetch is the
# same as the amount of captions available.
available_transcripts = video.captions
json.object do
json.field "transcripts" do
json.array do
available_transcripts.each do |transcript|
json.object do
json.field "label", transcript.name
json.field "languageCode", transcript.language_code
json.field "autoGenerated", transcript.auto_generated
if transcript.auto_generated
json.field "url", "/api/v1/transcripts/#{id}?lang=#{URI.encode_www_form(transcript.language_code)}&autogen"
else
json.field "url", "/api/v1/transcripts/#{id}?lang=#{URI.encode_www_form(transcript.language_code)}"
end
end
end
end
end
end
end
return response
end
# If lang is not given then we attempt to fetch
# the transcript through the given label
if lang.nil?
begin
video = get_video(id)
rescue ex : NotFoundException
return error_json(404, ex)
rescue ex
return error_json(500, ex)
end
target_transcript = video.captions.select(&.name.== label)
if target_transcript.empty?
return error_json(404, NotFoundException.new("Requested transcript does not exist"))
else
target_transcript = target_transcript[0]
lang, auto_generated = target_transcript.language_code, target_transcript.auto_generated
end
end
params = Invidious::Videos::Transcript.generate_param(id, lang, auto_generated)
begin
transcript = Invidious::Videos::Transcript.from_raw(
YoutubeAPI.get_transcript(params), lang, auto_generated
)
rescue ex : NotFoundException
return error_json(404, ex)
rescue ex
return error_json(500, ex)
end
return transcript.to_json
end
end

View File

@ -20,10 +20,11 @@ module Invidious::Routes::Channels
sort_by = env.params.query["sort_by"]?.try &.downcase
if channel.auto_generated
sort_by ||= "last"
sort_options = {"last", "oldest", "newest"}
items, next_continuation = fetch_channel_playlists(
channel.ucid, channel.author, continuation, (sort_by || "last")
channel.ucid, channel.author, continuation, sort_by
)
items.uniq! do |item|
@ -49,9 +50,11 @@ module Invidious::Routes::Channels
end
next_continuation = nil
else
sort_by ||= "newest"
sort_options = {"newest", "oldest", "popular"}
items, next_continuation = Channel::Tabs.get_videos(
channel, continuation: continuation, sort_by: (sort_by || "newest")
items, next_continuation = Channel::Tabs.get_60_videos(
channel, continuation: continuation, sort_by: sort_by
)
end
end
@ -82,13 +85,12 @@ module Invidious::Routes::Channels
end
next_continuation = nil
else
# TODO: support sort option for shorts
sort_by = ""
sort_options = [] of String
sort_by = env.params.query["sort_by"]?.try &.downcase || "newest"
sort_options = {"newest", "oldest", "popular"}
# Fetch items and continuation token
items, next_continuation = Channel::Tabs.get_shorts(
channel, continuation: continuation
channel, continuation: continuation, sort_by: sort_by
)
end
@ -195,7 +197,29 @@ module Invidious::Routes::Channels
templated "channel"
end
def self.courses(env)
data = self.fetch_basic_information(env)
return data if !data.is_a?(Tuple)
locale, user, subscriptions, continuation, ucid, channel = data
sort_by = ""
sort_options = [] of String
items, next_continuation = fetch_channel_courses(
channel.ucid, channel.author, continuation
)
items = items.select(SearchPlaylist)
items.each(&.author = "")
selected_tab = Frontend::ChannelPage::TabsAvailable::Courses
templated "channel"
end
def self.community(env)
return env.redirect env.request.path.sub("posts", "community") if env.request.path.split("/").last == "posts"
data = self.fetch_basic_information(env)
if !data.is_a?(Tuple)
return data
@ -212,7 +236,7 @@ module Invidious::Routes::Channels
continuation = env.params.query["continuation"]?
if !channel.tabs.includes? "community"
if !channel.tabs.includes? "community" && "posts"
return env.redirect "/channel/#{channel.ucid}"
end
@ -305,7 +329,8 @@ module Invidious::Routes::Channels
private KNOWN_TABS = {
"home", "videos", "shorts", "streams", "podcasts",
"releases", "playlists", "community", "channels", "about",
"releases", "courses", "playlists", "community", "channels", "about",
"posts",
}
# Redirects brand url channels to a normal /channel/:ucid route

View File

@ -157,10 +157,12 @@ module Invidious::Routes::Embed
adaptive_fmts = video.adaptive_fmts
if params.local
fmt_stream.each { |fmt| fmt["url"] = JSON::Any.new(URI.parse(fmt["url"].as_s).request_target) }
adaptive_fmts.each { |fmt| fmt["url"] = JSON::Any.new(URI.parse(fmt["url"].as_s).request_target) }
fmt_stream.each { |fmt| fmt["url"] = JSON::Any.new(HttpServer::Utils.proxy_video_url(fmt["url"].as_s)) }
end
# Always proxy DASH streams, otherwise youtube CORS headers will prevent playback
adaptive_fmts.each { |fmt| fmt["url"] = JSON::Any.new(HttpServer::Utils.proxy_video_url(fmt["url"].as_s)) }
video_streams = video.video_streams
audio_streams = video.audio_streams
@ -201,6 +203,14 @@ module Invidious::Routes::Embed
return env.redirect url
end
if CONFIG.invidious_companion.present?
invidious_companion = CONFIG.invidious_companion.sample
env.response.headers["Content-Security-Policy"] =
env.response.headers["Content-Security-Policy"]
.gsub("media-src", "media-src #{invidious_companion.public_url}")
.gsub("connect-src", "connect-src #{invidious_companion.public_url}")
end
rendered "embed"
end
end

View File

@ -143,32 +143,25 @@ module Invidious::Routes::Feeds
# RSS feeds
def self.rss_channel(env)
locale = env.get("preferences").as(Preferences).locale
env.response.headers["Content-Type"] = "application/atom+xml"
env.response.content_type = "application/atom+xml"
ucid = env.params.url["ucid"]
if env.params.url["ucid"].matches?(/^[\w-]+$/)
ucid = env.params.url["ucid"]
else
return error_atom(400, InfoException.new("Invalid channel ucid provided."))
end
params = HTTP::Params.parse(env.params.query["params"]? || "")
begin
channel = get_about_info(ucid, locale)
rescue ex : ChannelRedirect
return env.redirect env.request.resource.gsub(ucid, ex.channel_id)
rescue ex : NotFoundException
return error_atom(404, ex)
rescue ex
return error_atom(500, ex)
end
namespaces = {
"yt" => "http://www.youtube.com/xml/schemas/2015",
"media" => "http://search.yahoo.com/mrss/",
"default" => "http://www.w3.org/2005/Atom",
}
response = YT_POOL.client &.get("/feeds/videos.xml?channel_id=#{channel.ucid}")
response = YT_POOL.client &.get("/feeds/videos.xml?channel_id=#{ucid}")
return error_atom(404, NotFoundException.new("Channel does not exist.")) if response.status_code == 404
rss = XML.parse(response.body)
videos = rss.xpath_nodes("//default:feed/default:entry", namespaces).map do |entry|
@ -179,7 +172,7 @@ module Invidious::Routes::Feeds
updated = Time.parse_rfc3339(entry.xpath_node("default:updated", namespaces).not_nil!.content)
author = entry.xpath_node("default:author/default:name", namespaces).not_nil!.content
ucid = entry.xpath_node("yt:channelId", namespaces).not_nil!.content
video_ucid = entry.xpath_node("yt:channelId", namespaces).not_nil!.content
description_html = entry.xpath_node("media:group/media:description", namespaces).not_nil!.to_s
views = entry.xpath_node("media:group/media:community/media:statistics", namespaces).not_nil!.["views"].to_i64
@ -187,43 +180,44 @@ module Invidious::Routes::Feeds
title: title,
id: video_id,
author: author,
ucid: ucid,
ucid: video_ucid,
published: published,
views: views,
description_html: description_html,
length_seconds: 0,
live_now: false,
paid: false,
premium: false,
premiere_timestamp: nil,
author_verified: false,
author_thumbnail: nil,
badges: VideoBadges::None,
})
end
author = ""
author = videos[0].author if videos.size > 0
XML.build(indent: " ", encoding: "UTF-8") do |xml|
xml.element("feed", "xmlns:yt": "http://www.youtube.com/xml/schemas/2015",
"xmlns:media": "http://search.yahoo.com/mrss/", xmlns: "http://www.w3.org/2005/Atom",
"xml:lang": "en-US") do
xml.element("link", rel: "self", href: "#{HOST_URL}#{env.request.resource}")
xml.element("id") { xml.text "yt:channel:#{channel.ucid}" }
xml.element("yt:channelId") { xml.text channel.ucid }
xml.element("icon") { xml.text channel.author_thumbnail }
xml.element("title") { xml.text channel.author }
xml.element("link", rel: "alternate", href: "#{HOST_URL}/channel/#{channel.ucid}")
xml.element("id") { xml.text "yt:channel:#{ucid}" }
xml.element("yt:channelId") { xml.text ucid }
xml.element("title") { author }
xml.element("link", rel: "alternate", href: "#{HOST_URL}/channel/#{ucid}")
xml.element("author") do
xml.element("name") { xml.text channel.author }
xml.element("uri") { xml.text "#{HOST_URL}/channel/#{channel.ucid}" }
xml.element("name") { xml.text author }
xml.element("uri") { xml.text "#{HOST_URL}/channel/#{ucid}" }
end
xml.element("image") do
xml.element("url") { xml.text channel.author_thumbnail }
xml.element("title") { xml.text channel.author }
xml.element("url") { xml.text "" }
xml.element("title") { xml.text author }
xml.element("link", rel: "self", href: "#{HOST_URL}#{env.request.resource}")
end
videos.each do |video|
video.to_xml(channel.auto_generated, params, xml)
video.to_xml(false, params, xml)
end
end
end
@ -311,8 +305,9 @@ module Invidious::Routes::Feeds
end
response = YT_POOL.client &.get("/feeds/videos.xml?playlist_id=#{plid}")
document = XML.parse(response.body)
return error_atom(404, NotFoundException.new("Playlist does not exist.")) if response.status_code == 404
document = XML.parse(response.body)
document.xpath_nodes(%q(//*[@href]|//*[@url])).each do |node|
node.attributes.each do |attribute|
case attribute.name
@ -425,16 +420,6 @@ module Invidious::Routes::Feeds
next # skip this video since it raised an exception (e.g. it is a scheduled live event)
end
if CONFIG.enable_user_notifications
# Deliver notifications to `/api/v1/auth/notifications`
payload = {
"topic" => video.ucid,
"videoId" => video.id,
"published" => published.to_unix,
}.to_json
PG_DB.exec("NOTIFY notifications, E'#{payload}'")
end
video = ChannelVideo.new({
id: id,
title: video.title,
@ -450,11 +435,7 @@ module Invidious::Routes::Feeds
was_insert = Invidious::Database::ChannelVideos.insert(video, with_premiere_timestamp: true)
if was_insert
if CONFIG.enable_user_notifications
Invidious::Database::Users.add_notification(video)
else
Invidious::Database::Users.feed_needs_update(video)
end
NOTIFICATION_CHANNEL.send(VideoNotification.from_video(video))
end
end
end

View File

@ -11,29 +11,9 @@ module Invidious::Routes::Images
end
end
# We're encapsulating this into a proc in order to easily reuse this
# portion of the code for each request block below.
request_proc = ->(response : HTTP::Client::Response) {
env.response.status_code = response.status_code
response.headers.each do |key, value|
if !RESPONSE_HEADERS_BLACKLIST.includes?(key.downcase)
env.response.headers[key] = value
end
end
env.response.headers["Access-Control-Allow-Origin"] = "*"
if response.status_code >= 300
env.response.headers.delete("Transfer-Encoding")
return
end
proxy_file(response, env)
}
begin
HTTP::Client.get("https://yt3.ggpht.com#{url}") do |resp|
return request_proc.call(resp)
GGPHT_POOL.client &.get(url, headers) do |resp|
return self.proxy_image(env, resp)
end
rescue ex
end
@ -61,27 +41,10 @@ module Invidious::Routes::Images
end
end
request_proc = ->(response : HTTP::Client::Response) {
env.response.status_code = response.status_code
response.headers.each do |key, value|
if !RESPONSE_HEADERS_BLACKLIST.includes?(key.downcase)
env.response.headers[key] = value
end
end
env.response.headers["Connection"] = "close"
env.response.headers["Access-Control-Allow-Origin"] = "*"
if response.status_code >= 300
return env.response.headers.delete("Transfer-Encoding")
end
proxy_file(response, env)
}
begin
HTTP::Client.get("https://#{authority}.ytimg.com#{url}") do |resp|
return request_proc.call(resp)
get_ytimg_pool(authority).client &.get(url, headers) do |resp|
env.response.headers["Connection"] = "close"
return self.proxy_image(env, resp)
end
rescue ex
end
@ -101,26 +64,9 @@ module Invidious::Routes::Images
end
end
request_proc = ->(response : HTTP::Client::Response) {
env.response.status_code = response.status_code
response.headers.each do |key, value|
if !RESPONSE_HEADERS_BLACKLIST.includes?(key.downcase)
env.response.headers[key] = value
end
end
env.response.headers["Access-Control-Allow-Origin"] = "*"
if response.status_code >= 300 && response.status_code != 404
return env.response.headers.delete("Transfer-Encoding")
end
proxy_file(response, env)
}
begin
HTTP::Client.get("https://i9.ytimg.com#{url}") do |resp|
return request_proc.call(resp)
get_ytimg_pool("i9").client &.get(url, headers) do |resp|
return self.proxy_image(env, resp)
end
rescue ex
end
@ -165,8 +111,7 @@ module Invidious::Routes::Images
if name == "maxres.jpg"
build_thumbnails(id).each do |thumb|
thumbnail_resource_path = "/vi/#{id}/#{thumb[:url]}.jpg"
# This can likely be optimized into a (small) pool sometime in the future.
if HTTP::Client.head("https://i.ytimg.com#{thumbnail_resource_path}").status_code == 200
if get_ytimg_pool("i").client &.head(thumbnail_resource_path, headers).status_code == 200
name = thumb[:url] + ".jpg"
break
end
@ -181,29 +126,28 @@ module Invidious::Routes::Images
end
end
request_proc = ->(response : HTTP::Client::Response) {
env.response.status_code = response.status_code
response.headers.each do |key, value|
if !RESPONSE_HEADERS_BLACKLIST.includes?(key.downcase)
env.response.headers[key] = value
end
end
env.response.headers["Access-Control-Allow-Origin"] = "*"
if response.status_code >= 300 && response.status_code != 404
return env.response.headers.delete("Transfer-Encoding")
end
proxy_file(response, env)
}
begin
# This can likely be optimized into a (small) pool sometime in the future.
HTTP::Client.get("https://i.ytimg.com#{url}") do |resp|
return request_proc.call(resp)
get_ytimg_pool("i").client &.get(url, headers) do |resp|
return self.proxy_image(env, resp)
end
rescue ex
end
end
private def self.proxy_image(env, response)
env.response.status_code = response.status_code
response.headers.each do |key, value|
if !RESPONSE_HEADERS_BLACKLIST.includes?(key.downcase)
env.response.headers[key] = value
end
end
env.response.headers["Access-Control-Allow-Origin"] = "*"
if response.status_code >= 300
return env.response.headers.delete("Transfer-Encoding")
end
return proxy_file(response, env)
end
end

View File

@ -40,7 +40,21 @@ module Invidious::Routes::Misc
def self.cross_instance_redirect(env)
referer = get_referer(env)
instance_url = fetch_random_instance
instance_list = Invidious::Jobs::InstanceListRefreshJob::INSTANCES["INSTANCES"]
# Filter out the current instance
other_available_instances = instance_list.reject { |_, domain| domain == CONFIG.domain }
if other_available_instances.empty?
# If the current instance is the only one, use the redirect URL as fallback
instance_url = "redirect.invidious.io"
else
# Select other random instance
# Sample returns an array
# Instances are packaged as {region, domain} in the instance list
instance_url = other_available_instances.sample(1)[0][1]
end
env.redirect "https://#{instance_url}#{referer}"
end
end

View File

@ -304,23 +304,6 @@ module Invidious::Routes::Playlists
end
end
if env.params.query["action_create_playlist"]?
action = "action_create_playlist"
elsif env.params.query["action_delete_playlist"]?
action = "action_delete_playlist"
elsif env.params.query["action_edit_playlist"]?
action = "action_edit_playlist"
elsif env.params.query["action_add_video"]?
action = "action_add_video"
video_id = env.params.query["video_id"]
elsif env.params.query["action_remove_video"]?
action = "action_remove_video"
elsif env.params.query["action_move_video_before"]?
action = "action_move_video_before"
else
return env.redirect referer
end
begin
playlist_id = env.params.query["playlist_id"]
playlist = get_playlist(playlist_id).as(InvidiousPlaylist)
@ -335,12 +318,8 @@ module Invidious::Routes::Playlists
end
end
email = user.email
case action
when "action_edit_playlist"
# TODO: Playlist stub
when "action_add_video"
case action = env.params.query["action"]?
when "add_video"
if playlist.index.size >= CONFIG.playlist_length_limit
if redirect
return error_template(400, "Playlist cannot have more than #{CONFIG.playlist_length_limit} videos")
@ -377,12 +356,14 @@ module Invidious::Routes::Playlists
Invidious::Database::PlaylistVideos.insert(playlist_video)
Invidious::Database::Playlists.update_video_added(playlist_id, playlist_video.index)
when "action_remove_video"
when "remove_video"
index = env.params.query["set_video_id"]
Invidious::Database::PlaylistVideos.delete(index)
Invidious::Database::Playlists.update_video_removed(playlist_id, index)
when "action_move_video_before"
when "move_video_before"
# TODO: Playlist stub
when nil
return error_json(400, "Missing action")
else
return error_json(400, "Unsupported action #{action}")
end

View File

@ -27,6 +27,10 @@ module Invidious::Routes::PreferencesRoute
annotations_subscribed ||= "off"
annotations_subscribed = annotations_subscribed == "on"
preload = env.params.body["preload"]?.try &.as(String)
preload ||= "off"
preload = preload == "on"
autoplay = env.params.body["autoplay"]?.try &.as(String)
autoplay ||= "off"
autoplay = autoplay == "on"
@ -144,6 +148,7 @@ module Invidious::Routes::PreferencesRoute
preferences = Preferences.from_json({
annotations: annotations,
annotations_subscribed: annotations_subscribed,
preload: preload,
autoplay: autoplay,
captions: captions,
comments: comments,

View File

@ -32,24 +32,16 @@ module Invidious::Routes::Subscriptions
end
end
if env.params.query["action_create_subscription_to_channel"]?.try &.to_i?.try &.== 1
action = "action_create_subscription_to_channel"
elsif env.params.query["action_remove_subscriptions"]?.try &.to_i?.try &.== 1
action = "action_remove_subscriptions"
else
return env.redirect referer
end
channel_id = env.params.query["c"]?
channel_id ||= ""
case action
when "action_create_subscription_to_channel"
case action = env.params.query["action"]?
when "create_subscription_to_channel"
if !user.subscriptions.includes? channel_id
get_channel(channel_id)
Invidious::Database::Users.subscribe_channel(user, channel_id)
end
when "action_remove_subscriptions"
when "remove_subscriptions"
Invidious::Database::Users.unsubscribe_channel(user, channel_id)
else
return error_json(400, "Unsupported action #{action}")

View File

@ -42,7 +42,7 @@ module Invidious::Routes::VideoPlayback
headers["Range"] = "bytes=#{range_for_head}"
end
client = make_client(URI.parse(host), region, force_resolve = true)
client = make_client(URI.parse(host), region, force_resolve: true)
response = HTTP::Client::Response.new(500)
error = ""
5.times do
@ -57,7 +57,7 @@ module Invidious::Routes::VideoPlayback
if new_host != host
host = new_host
client.close
client = make_client(URI.parse(new_host), region, force_resolve = true)
client = make_client(URI.parse(new_host), region, force_resolve: true)
end
url = "#{location.request_target}&host=#{location.host}#{region ? "&region=#{region}" : ""}"
@ -71,7 +71,7 @@ module Invidious::Routes::VideoPlayback
fvip = "3"
host = "https://r#{fvip}---#{mn}.googlevideo.com"
client = make_client(URI.parse(host), region, force_resolve = true)
client = make_client(URI.parse(host), region, force_resolve: true)
rescue ex
error = ex.message
end
@ -164,10 +164,13 @@ module Invidious::Routes::VideoPlayback
env.response.headers["Access-Control-Allow-Origin"] = "*"
if location = resp.headers["Location"]?
location = URI.parse(location)
location = "#{location.request_target}&host=#{location.host}#{region ? "&region=#{region}" : ""}"
url = Invidious::HttpServer::Utils.proxy_video_url(location, region: region)
env.redirect location
if title = query_params["title"]?
url = "#{url}&title=#{URI.encode_www_form(title)}"
end
env.redirect url
break
end
@ -196,7 +199,7 @@ module Invidious::Routes::VideoPlayback
break
else
client.close
client = make_client(URI.parse(host), region, force_resolve = true)
client = make_client(URI.parse(host), region, force_resolve: true)
end
end
@ -253,6 +256,11 @@ module Invidious::Routes::VideoPlayback
# YouTube /videoplayback links expire after 6 hours,
# so we have a mechanism here to redirect to the latest version
def self.latest_version(env)
if CONFIG.invidious_companion.present?
invidious_companion = CONFIG.invidious_companion.sample
return env.redirect "#{invidious_companion.public_url}/latest_version?#{env.params.query}"
end
id = env.params.query["id"]?
itag = env.params.query["itag"]?.try &.to_i?

View File

@ -121,10 +121,12 @@ module Invidious::Routes::Watch
adaptive_fmts = video.adaptive_fmts
if params.local
fmt_stream.each { |fmt| fmt["url"] = JSON::Any.new(URI.parse(fmt["url"].as_s).request_target) }
adaptive_fmts.each { |fmt| fmt["url"] = JSON::Any.new(URI.parse(fmt["url"].as_s).request_target) }
fmt_stream.each { |fmt| fmt["url"] = JSON::Any.new(HttpServer::Utils.proxy_video_url(fmt["url"].as_s)) }
end
# Always proxy DASH streams, otherwise youtube CORS headers will prevent playback
adaptive_fmts.each { |fmt| fmt["url"] = JSON::Any.new(HttpServer::Utils.proxy_video_url(fmt["url"].as_s)) }
video_streams = video.video_streams
audio_streams = video.audio_streams
@ -190,6 +192,14 @@ module Invidious::Routes::Watch
captions: video.captions
)
if CONFIG.invidious_companion.present?
invidious_companion = CONFIG.invidious_companion.sample
env.response.headers["Content-Security-Policy"] =
env.response.headers["Content-Security-Policy"]
.gsub("media-src", "media-src #{invidious_companion.public_url}")
.gsub("connect-src", "connect-src #{invidious_companion.public_url}")
end
templated "watch"
end
@ -241,18 +251,10 @@ module Invidious::Routes::Watch
end
end
if env.params.query["action_mark_watched"]?
action = "action_mark_watched"
elsif env.params.query["action_mark_unwatched"]?
action = "action_mark_unwatched"
else
return env.redirect referer
end
case action
when "action_mark_watched"
case action = env.params.query["action"]?
when "mark_watched"
Invidious::Database::Users.mark_watched(user, id)
when "action_mark_unwatched"
when "mark_unwatched"
Invidious::Database::Users.mark_unwatched(user, id)
else
return error_json(400, "Unsupported action #{action}")
@ -320,14 +322,19 @@ module Invidious::Routes::Watch
env.params.query["label"] = URI.decode_www_form(label.as_s)
return Invidious::Routes::API::V1::Videos.captions(env)
elsif itag = download_widget["itag"]?.try &.as_i
elsif itag = download_widget["itag"]?.try &.as_i.to_s
# URL params specific to /latest_version
env.params.query["id"] = video_id
env.params.query["itag"] = itag.to_s
env.params.query["title"] = filename
env.params.query["local"] = "true"
return Invidious::Routes::VideoPlayback.latest_version(env)
if (CONFIG.invidious_companion.present?)
video = get_video(video_id)
invidious_companion = CONFIG.invidious_companion.sample
return env.redirect "#{invidious_companion.public_url}/latest_version?#{env.params.query}"
else
return Invidious::Routes::VideoPlayback.latest_version(env)
end
else
return error_template(400, "Invalid label or itag")
end

View File

@ -120,8 +120,10 @@ module Invidious::Routing
get "/channel/:ucid/streams", Routes::Channels, :streams
get "/channel/:ucid/podcasts", Routes::Channels, :podcasts
get "/channel/:ucid/releases", Routes::Channels, :releases
get "/channel/:ucid/courses", Routes::Channels, :courses
get "/channel/:ucid/playlists", Routes::Channels, :playlists
get "/channel/:ucid/community", Routes::Channels, :community
get "/channel/:ucid/posts", Routes::Channels, :community
get "/channel/:ucid/channels", Routes::Channels, :channels
get "/channel/:ucid/about", Routes::Channels, :about
@ -236,6 +238,7 @@ module Invidious::Routing
get "/api/v1/annotations/:id", {{namespace}}::Videos, :annotations
get "/api/v1/comments/:id", {{namespace}}::Videos, :comments
get "/api/v1/clips/:id", {{namespace}}::Videos, :clips
get "/api/v1/transcripts/:id", {{namespace}}::Videos, :transcripts
# Feeds
get "/api/v1/trending", {{namespace}}::Feeds, :trending
@ -243,17 +246,18 @@ module Invidious::Routing
# Channels
get "/api/v1/channels/:ucid", {{namespace}}::Channels, :home
get "/api/v1/channels/:ucid/latest", {{namespace}}::Channels, :latest
get "/api/v1/channels/:ucid/videos", {{namespace}}::Channels, :videos
get "/api/v1/channels/:ucid/shorts", {{namespace}}::Channels, :shorts
get "/api/v1/channels/:ucid/streams", {{namespace}}::Channels, :streams
get "/api/v1/channels/:ucid/podcasts", {{namespace}}::Channels, :podcasts
get "/api/v1/channels/:ucid/releases", {{namespace}}::Channels, :releases
get "/api/v1/channels/:ucid/courses", {{namespace}}::Channels, :courses
get "/api/v1/channels/:ucid/playlists", {{namespace}}::Channels, :playlists
get "/api/v1/channels/:ucid/community", {{namespace}}::Channels, :community
get "/api/v1/channels/:ucid/posts", {{namespace}}::Channels, :community
get "/api/v1/channels/:ucid/channels", {{namespace}}::Channels, :channels
{% for route in {"videos", "latest", "playlists", "community", "search"} %}
get "/api/v1/channels/#{{{route}}}/:ucid", {{namespace}}::Channels, :{{route}}
get "/api/v1/channels/:ucid/#{{{route}}}", {{namespace}}::Channels, :{{route}}
{% end %}
get "/api/v1/channels/:ucid/search", {{namespace}}::Channels, :search
# Posts
get "/api/v1/post/:id", {{namespace}}::Channels, :post
@ -271,11 +275,6 @@ module Invidious::Routing
# Authenticated
# The notification APIs cannot be extracted yet! They require the *local* notifications constant defined in invidious.cr
#
# Invidious::Routing.get "/api/v1/auth/notifications", {{namespace}}::Authenticated, :notifications
# Invidious::Routing.post "/api/v1/auth/notifications", {{namespace}}::Authenticated, :notifications
get "/api/v1/auth/preferences", {{namespace}}::Authenticated, :get_preferences
post "/api/v1/auth/preferences", {{namespace}}::Authenticated, :set_preferences

View File

@ -75,7 +75,7 @@ module Invidious::Search
@type : Type = Type::All,
@duration : Duration = Duration::None,
@features : Features = Features::None,
@sort : Sort = Sort::Relevance
@sort : Sort = Sort::Relevance,
)
end

View File

@ -47,7 +47,7 @@ module Invidious::Search
def initialize(
params : HTTP::Params,
@type : Type = Type::Regular,
@region : String? = nil
@region : String? = nil,
)
# Get the raw search query string (common to all search types). In
# Regular search mode, also look for the `search_query` URL parameter

View File

@ -290,42 +290,39 @@ struct Invidious::User
end
def from_newpipe(user : User, body : String) : Bool
io = IO::Memory.new(body)
Compress::Zip::File.open(IO::Memory.new(body), true) do |file|
entry = file.entries.find { |file_entry| file_entry.filename == "newpipe.db" }
return false if entry.nil?
entry.open do |file_io|
# Ensure max size of 4MB
io_sized = IO::Sized.new(file_io, 0x400000)
Compress::Zip::File.open(io) do |file|
file.entries.each do |entry|
entry.open do |file_io|
# Ensure max size of 4MB
io_sized = IO::Sized.new(file_io, 0x400000)
begin
temp = File.tempfile(".db") do |tempfile|
begin
File.write(tempfile.path, io_sized.gets_to_end)
rescue
return false
end
next if entry.filename != "newpipe.db"
DB.open("sqlite3://" + tempfile.path) do |db|
user.watched += db.query_all("SELECT url FROM streams", as: String)
.map(&.lchop("https://www.youtube.com/watch?v="))
tempfile = File.tempfile(".db")
user.watched.uniq!
Invidious::Database::Users.update_watch_history(user)
begin
File.write(tempfile.path, io_sized.gets_to_end)
rescue
return false
user.subscriptions += db.query_all("SELECT url FROM subscriptions", as: String)
.map(&.lchop("https://www.youtube.com/channel/"))
user.subscriptions.uniq!
user.subscriptions = get_batch_channels(user.subscriptions)
Invidious::Database::Users.update_subscriptions(user)
end
end
db = DB.open("sqlite3://" + tempfile.path)
user.watched += db.query_all("SELECT url FROM streams", as: String)
.map(&.lchop("https://www.youtube.com/watch?v="))
user.watched.uniq!
Invidious::Database::Users.update_watch_history(user)
user.subscriptions += db.query_all("SELECT url FROM subscriptions", as: String)
.map(&.lchop("https://www.youtube.com/channel/"))
user.subscriptions.uniq!
user.subscriptions = get_batch_channels(user.subscriptions)
Invidious::Database::Users.update_subscriptions(user)
db.close
tempfile.delete
ensure
temp.delete if !temp.nil?
end
end
end

View File

@ -4,6 +4,7 @@ struct Preferences
property annotations : Bool = CONFIG.default_user_preferences.annotations
property annotations_subscribed : Bool = CONFIG.default_user_preferences.annotations_subscribed
property preload : Bool = CONFIG.default_user_preferences.preload
property autoplay : Bool = CONFIG.default_user_preferences.autoplay
property automatic_instance_redirect : Bool = CONFIG.default_user_preferences.automatic_instance_redirect

View File

@ -15,7 +15,7 @@ struct Video
# NOTE: don't forget to bump this number if any change is made to
# the `params` structure in videos/parser.cr!!!
#
SCHEMA_VERSION = 2
SCHEMA_VERSION = 3
property id : String
@ -26,12 +26,6 @@ struct Video
@[DB::Field(ignore: true)]
@captions = [] of Invidious::Videos::Captions::Metadata
@[DB::Field(ignore: true)]
property adaptive_fmts : Array(Hash(String, JSON::Any))?
@[DB::Field(ignore: true)]
property fmt_stream : Array(Hash(String, JSON::Any))?
@[DB::Field(ignore: true)]
property description : String?
@ -98,72 +92,24 @@ struct Video
# Methods for parsing streaming data
def convert_url(fmt)
if cfr = fmt["signatureCipher"]?.try { |json| HTTP::Params.parse(json.as_s) }
sp = cfr["sp"]
url = URI.parse(cfr["url"])
params = url.query_params
LOGGER.debug("Videos: Decoding '#{cfr}'")
unsig = DECRYPT_FUNCTION.try &.decrypt_signature(cfr["s"])
params[sp] = unsig if unsig
def fmt_stream : Array(Hash(String, JSON::Any))
if formats = info.dig?("streamingData", "formats")
return formats
.as_a.map(&.as_h)
.sort_by! { |f| f["width"]?.try &.as_i || 0 }
else
url = URI.parse(fmt["url"].as_s)
params = url.query_params
return [] of Hash(String, JSON::Any)
end
n = DECRYPT_FUNCTION.try &.decrypt_nsig(params["n"])
params["n"] = n if n
if token = CONFIG.po_token
params["pot"] = token
end
params["host"] = url.host.not_nil!
if region = self.info["region"]?.try &.as_s
params["region"] = region
end
url.query_params = params
LOGGER.trace("Videos: new url is '#{url}'")
return url.to_s
rescue ex
LOGGER.debug("Videos: Error when parsing video URL")
LOGGER.trace(ex.inspect_with_backtrace)
return ""
end
def fmt_stream
return @fmt_stream.as(Array(Hash(String, JSON::Any))) if @fmt_stream
fmt_stream = info.dig?("streamingData", "formats")
.try &.as_a.map &.as_h || [] of Hash(String, JSON::Any)
fmt_stream.each do |fmt|
fmt["url"] = JSON::Any.new(self.convert_url(fmt))
def adaptive_fmts : Array(Hash(String, JSON::Any))
if formats = info.dig?("streamingData", "adaptiveFormats")
return formats
.as_a.map(&.as_h)
.sort_by! { |f| f["width"]?.try &.as_i || f["audioTrack"]?.try { |a| a["audioIsDefault"]?.try { |v| v.as_bool ? -1 : 0 } } || 0 }
else
return [] of Hash(String, JSON::Any)
end
fmt_stream.sort_by! { |f| f["width"]?.try &.as_i || 0 }
@fmt_stream = fmt_stream
return @fmt_stream.as(Array(Hash(String, JSON::Any)))
end
def adaptive_fmts
return @adaptive_fmts.as(Array(Hash(String, JSON::Any))) if @adaptive_fmts
fmt_stream = info.dig("streamingData", "adaptiveFormats")
.try &.as_a.map &.as_h || [] of Hash(String, JSON::Any)
fmt_stream.each do |fmt|
fmt["url"] = JSON::Any.new(self.convert_url(fmt))
end
fmt_stream.sort_by! { |f| f["width"]?.try &.as_i || 0 }
@adaptive_fmts = fmt_stream
return @adaptive_fmts.as(Array(Hash(String, JSON::Any)))
end
def video_streams

Some files were not shown because too many files have changed in this diff Show More