Compare commits

...

89 commits
test ... master

Author SHA1 Message Date
8d7ca9a4e2
External Proxies: Proxyfi HLS Playlists 2024-10-14 17:57:52 -03:00
9a66a7bd51
Videos: Completly disable annotations due to archive.org being down
Closes #15
2024-10-13 23:47:57 -03:00
66b481713d
Tokens: Refresh po_token and visitor_data every 5 seconds
Closes #11
2024-10-13 15:57:51 -03:00
6587528ed9
External Proxies: Proxyfi HD720 2024-10-13 15:19:49 -03:00
917cede8b7
Videos: Increase video cache to 4 hours 2024-10-12 02:59:36 -03:00
fc0a3ab307
Feat: Experimental support for potoken inside redis
Using https://git.nadeko.net/Fijxu/youtube-po-token-generator
2024-10-12 02:04:14 -03:00
62d64ca814
fixup! Feat: User supplied po_token and visitor_data 2024-10-11 23:25:09 -03:00
e78f7e5430
External Proxies: Use list of external videoplayback proxies 2024-10-11 13:50:42 -03:00
Samantaz Fox
b551fcf96a
Videos: Fix missing host parameter on playback URLs when local=true 2024-10-11 13:14:53 -03:00
7689251158
CI: Experimental branches for testing builds 2024-10-10 18:19:29 -03:00
b3a8866022
Feat: User supplied po_token and visitor_data 2024-10-10 17:43:18 -03:00
eac85f111c
fixup! Feeds: Get rid of feed_needs_update() since it appears to be unused 2024-10-10 15:07:50 -03:00
5dd37bfee7
Small try. 2024-10-10 15:07:33 -03:00
ab32c38719
Feeds: Get rid of feed_needs_update() since it appears to be unused 2024-10-09 18:09:23 -03:00
cf6c3a7b5b
Revert "use WEB_CREATOR when po_token with WEB_EMBED as a fallback (#4928)"
This reverts commit d9df90b5e3.
2024-10-08 19:53:35 -03:00
2f5a555ea7
Merge remote-tracking branch 'upstream/master' 2024-10-08 19:22:53 -03:00
472dd8663d
VideoJS: Increase buffer 2024-10-08 18:59:01 -03:00
dc2aba106c
Backends: Use backend switcher to indicate the current backend in use. 2024-10-08 18:59:01 -03:00
eff8673efc
Feat: Experimental support for external videoplayback proxies 2024-10-08 18:59:01 -03:00
b1f25a69ad
Logger: Add color support for different log levels 2024-10-08 18:59:01 -03:00
d5b8b0b19c
SigHelper: Reconnect to signature helper 2024-10-08 18:59:00 -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
É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 de918b9234.
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
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
4f066e880c
CI: Fix docker container tags 2024-08-26 21:55:43 +02:00
Samantaz Fox
3e17d04875
Release v2.20240825.1 2024-08-25 22:30:46 +02:00
syeopite
cec905e95e
Allow manual trigger of release-container build (#4877) 2024-08-25 19:55:52 +00:00
Samantaz Fox
80958aa0d8
Release v2.20240825 2024-08-25 21:25:48 +02:00
Samantaz Fox
b2133c6b2c
Videos: Convert URL before putting result into cache 2024-08-24 18:01:56 +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
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
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
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
70 changed files with 854 additions and 323 deletions

View file

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

View file

@ -8,6 +8,8 @@ on:
push: push:
branches: branches:
- "master" - "master"
- "experimental"
- "experimental2"
jobs: jobs:
build: build:

2
.github/CODEOWNERS vendored
View file

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

View file

@ -1,6 +1,7 @@
name: Build and release container name: Build and release container
on: on:
workflow_dispatch:
push: push:
tags: tags:
- "v*" - "v*"
@ -46,9 +47,11 @@ jobs:
uses: docker/metadata-action@v5 uses: docker/metadata-action@v5
with: with:
images: quay.io/invidious/invidious images: quay.io/invidious/invidious
flavor: |
latest=false
tags: | tags: |
type=semver,pattern={{version}} type=semver,pattern={{version}}
type=raw,value=latest,enable=${{ github.ref == format('refs/heads/{0}', 'master') }} type=raw,value=latest
labels: | labels: |
quay.expires-after=12w quay.expires-after=12w
@ -70,10 +73,11 @@ jobs:
with: with:
images: quay.io/invidious/invidious images: quay.io/invidious/invidious
flavor: | flavor: |
latest=false
suffix=-arm64 suffix=-arm64
tags: | tags: |
type=semver,pattern={{version}} type=semver,pattern={{version}}
type=raw,value=latest,enable=${{ github.ref == format('refs/heads/{0}', 'master') }} type=raw,value=latest
labels: | labels: |
quay.expires-after=12w quay.expires-after=12w

View file

@ -1,6 +1,223 @@
# CHANGELOG # CHANGELOG
## 2024-04-26 ## vX.Y.0 (future)
### Full list of pull requests merged since the last release (newest first)
* 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
[#4652]: https://github.com/iv-org/invidious/pull/4652
[#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
[#4942]: https://github.com/iv-org/invidious/pull/4942
## 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.
[semver]: https://semver.org/
### Full list of pull requests merged since the last release (newest first)
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)
### New features & important changes
#### For users
* The search bar now has a button that you can click!
* Youtube URLs can be pasted directly in the search bar. Prepend search query with a
backslash (`\`) to disable that feature (useful if you need to search for a video whose
title contains some youtube URL).
* On the channel page the "streams" tab can be sorted by either: "newest", "oldest" or "popular"
* Lots of translations have been updated (thanks to our contributors on Weblate!)
* Videos embedded in local HTML files (e.g: a webpage saved from a blog) can now be played
#### For instance owners
* Invidious now has the ability to provide a `po_token` and `visitordata` to Youtube in order to
circumvent current Youtube restrictions.
* Invidious can use an (optional) external signature server like [inv_sig_helper]. Please note that
some videos can't be played without that signature server.
* The Helm charts were moved to a separate repo: https://github.com/iv-org/invidious-helm-chart
* We have changed how containers are released: the `latest` tag now tracks tagged releases, whereas
the `master` tag tracks the most recent commits of the `master` branch ("nightly" builds).
[inv_sig_helper]: https://github.com/iv-org/inv_sig_helper
#### For developpers
* The versions of Crystal that we test in CI/CD are now: `1.9.2`, `1.10.1`, `1.11.2`, `1.12.1`.
Please note that due to a bug in the `libxml` bindings (See [#4256]), versions prior to `1.10.0`
are not recommended to use.
* Thanks to @syeopite, the code is now [ameba] compliant.
* Ameba is part of our CI/CD pipeline, and its rules will be enforced in future PRs.
* The transcript code has been rewritten to permit transcripts as a feature rather than being
only a workaround for captions. Trancripts feature is coming soon!
* Various fixes regarding the logic interacting with Youtube
* The `sort_by` parameter can be used on the `/api/v1/channels/{id}/streams` endpoint. Accepted
values are: "newest", "oldest" and "popular"
[ameba]: https://github.com/crystal-ameba/ameba
[#4256]: https://github.com/iv-org/invidious/issues/4256
### Bugs fixed
#### User-side
* Channels: fixed broken "subscribers" and "views" counters
* Watch page: playback position is reset at the end of a video, so that the next time this video
is watched, it will start from the beginning rather than 15 seconds before the end
* Watch page: the items in the "add to playlist" drop down are now sorted alphabetically
* Videos: the "genre" URL is now always pointing to a valid webpage
* Playlists: Fixed `Could not parse N episodes` error on podcast playlists
* All external links should now have the [`rel`] attibute set to `noreferrer noopener` for
increased privacy.
* Preferences: Fixed the admin-only "modified source code" input being ignored
* Watch/channel pages: use the full image URL in `og:image` and `twitter:image` meta tags
[`rel`]: https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/rel
#### API
* fixed the `local` parameter not applying to `formatStreams` on `/api/v1/videos/{id}`
* fixed an `Index out of bounds` error hapenning when a playlist had no videos
* fixed duplicated query parameters in proxied video URLs
* Return actual video height/width/fps rather than hard coded values
* Fixed the `/api/v1/popular` endpoint not returning a proper error code/message when the
popular page/endpoint are disabled.
### Full list of pull requests merged since the last release (newest first)
* HTML: Sort playlists alphabetically in watch page drop down ([#4853], by @SamantazFox)
* Videos: Fix XSS vulnerability in description/comments ([#4852], thanks _anonymous_)
* YtAPI: Bump client versions ([#4849], by @SamantazFox)
* SigHelper: Fix inverted time comparison in 'check_update' ([#4845], by @SamantazFox)
* Storyboards: Various fixes and code cleaning ([#4153], by SamantazFox)
* Fix lint errors introduced in #4146 and #4295 ([#4876], thanks @syeopite)
* Search: Add support for Youtube URLs ([#4146], by @SamantazFox)
* Channel: Render age restricted channels ([#4295], thanks @ChunkyProgrammer)
* Ameba: Miscellaneous fixes ([#4807], thanks @syeopite)
* API: Proxy formatStreams URLs too ([#4859], thanks @colinleroy)
* UI: Add search button to search bar ([#4706], thanks @thansk)
* Add ability to set po_token and visitordata ID ([#4789], thanks @unixfox)
* Add support for an external signature server ([#4772], by @SamantazFox)
* Ameba: Fix Naming/VariableNames ([#4790], thanks @syeopite)
* Translations update from Hosted Weblate ([#4659])
* Ameba: Fix Lint/UselessAssign ([#4795], thanks @syeopite)
* HTML: Add rel="noreferrer noopener" to external links ([#4667], thanks @ulmemxpoc)
* Remove unused methods in Invidious::LogHandler ([#4812], thanks @syeopite)
* Ameba: Fix Lint/NotNilAfterNoBang ([#4796], thanks @syeopite)
* Ameba: Fix unused argument Lint warnings ([#4805], thanks @syeopite)
* Ameba: i18next.cr fixes ([#4806], thanks @syeopite)
* Ameba: Disable rules ([#4792], thanks @syeopite)
* Channel: parse subscriber count and channel banner ([#4785], thanks @ChunkyProgrammer)
* Player: Fix playback position of already watched videos ([#4731], thanks @Fijxu)
* Videos: Fix genre url being unusable ([#4717], thanks @meatball133)
* API: Fix out of bound error on empty playlists ([#4696], thanks @Fijxu)
* Handle playlists cataloged as Podcast ([#4695], thanks @Fijxu)
* API: Fix duplicated query parameters in proxied video URLs ([#4587], thanks @absidue)
* API: Return actual stream height, width and fps ([#4586], thanks @absidue)
* Preferences: Fix handling of modified source code URL ([#4437], thanks @nooptek)
* API: Fix URL for vtt subtitles ([#4221], thanks @karelrooted)
* Channels: Add sort options to streams ([#4224], thanks @src-tinkerer)
* API: Fix error code for disabled popular endpoint ([#4296], thanks @iBicha)
* Allow embedding videos in local HTML files ([#4450], thanks @tomasz1986)
* CI: Bump Crystal version matrix ([#4654], by @SamantazFox)
* YtAPI: Remove API keys like official clients ([#4655], by @SamantazFox)
* HTML: Use full URL in the og:image property ([#4675], thanks @Fijxu)
* Rewrite transcript logic to be more generic ([#4747], thanks @syeopite)
* CI: Run Ameba ([#4753], thanks @syeopite)
* CI: Add release based containers ([#4763], thanks @syeopite)
* move helm chart to a dedicated github repository ([#4711], thanks @unixfox)
[#4146]: https://github.com/iv-org/invidious/pull/4146
[#4153]: https://github.com/iv-org/invidious/pull/4153
[#4221]: https://github.com/iv-org/invidious/pull/4221
[#4224]: https://github.com/iv-org/invidious/pull/4224
[#4295]: https://github.com/iv-org/invidious/pull/4295
[#4296]: https://github.com/iv-org/invidious/pull/4296
[#4437]: https://github.com/iv-org/invidious/pull/4437
[#4450]: https://github.com/iv-org/invidious/pull/4450
[#4586]: https://github.com/iv-org/invidious/pull/4586
[#4587]: https://github.com/iv-org/invidious/pull/4587
[#4654]: https://github.com/iv-org/invidious/pull/4654
[#4655]: https://github.com/iv-org/invidious/pull/4655
[#4659]: https://github.com/iv-org/invidious/pull/4659
[#4667]: https://github.com/iv-org/invidious/pull/4667
[#4675]: https://github.com/iv-org/invidious/pull/4675
[#4695]: https://github.com/iv-org/invidious/pull/4695
[#4696]: https://github.com/iv-org/invidious/pull/4696
[#4706]: https://github.com/iv-org/invidious/pull/4706
[#4711]: https://github.com/iv-org/invidious/pull/4711
[#4717]: https://github.com/iv-org/invidious/pull/4717
[#4731]: https://github.com/iv-org/invidious/pull/4731
[#4747]: https://github.com/iv-org/invidious/pull/4747
[#4753]: https://github.com/iv-org/invidious/pull/4753
[#4763]: https://github.com/iv-org/invidious/pull/4763
[#4772]: https://github.com/iv-org/invidious/pull/4772
[#4785]: https://github.com/iv-org/invidious/pull/4785
[#4789]: https://github.com/iv-org/invidious/pull/4789
[#4790]: https://github.com/iv-org/invidious/pull/4790
[#4792]: https://github.com/iv-org/invidious/pull/4792
[#4795]: https://github.com/iv-org/invidious/pull/4795
[#4796]: https://github.com/iv-org/invidious/pull/4796
[#4805]: https://github.com/iv-org/invidious/pull/4805
[#4806]: https://github.com/iv-org/invidious/pull/4806
[#4807]: https://github.com/iv-org/invidious/pull/4807
[#4812]: https://github.com/iv-org/invidious/pull/4812
[#4845]: https://github.com/iv-org/invidious/pull/4845
[#4849]: https://github.com/iv-org/invidious/pull/4849
[#4852]: https://github.com/iv-org/invidious/pull/4852
[#4853]: https://github.com/iv-org/invidious/pull/4853
[#4859]: https://github.com/iv-org/invidious/pull/4859
[#4876]: https://github.com/iv-org/invidious/pull/4876
## v2.20240427 (2024-04-27)
Major bug fixes: Major bug fixes:
* Videos: Use android test suite client (#4650, thanks @SamantazFox) * Videos: Use android test suite client (#4650, thanks @SamantazFox)

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 video_data = JSON.parse(document.getElementById('video_data').textContent);
var options = { var options = {
preload: 'auto',
liveui: true, liveui: true,
playbackRates: [0.25, 0.5, 0.75, 1.0, 1.25, 1.5, 1.75, 2.0], playbackRates: [0.25, 0.5, 0.75, 1.0, 1.25, 1.5, 1.75, 2.0],
controlBar: { controlBar: {
@ -51,8 +50,8 @@ videojs.Vhs.xhr.beforeRequest = function(options) {
return options; return options;
}; };
videojs.Vhs.GOAL_BUFFER_LENGTH = 20; videojs.Vhs.GOAL_BUFFER_LENGTH = 40;
videojs.Vhs.MAX_GOAL_BUFFER_LENGTH = 30; videojs.Vhs.MAX_GOAL_BUFFER_LENGTH = 80;
var player = videojs('player', options); var player = videojs('player', options);

View file

@ -222,6 +222,15 @@ https_only: false
## ##
#log_level: Info #log_level: Info
##
## Enables colors in logs. Useful for debugging purposes
## This is overridden if "-k" or "--colorize"
## are passed on the command line.
##
## Accepted values: true, false
## Default: false
##
#colorize_logs: false
# ----------------------------- # -----------------------------
# Features # Features
@ -770,6 +779,22 @@ default_user_preferences:
# Video player behavior # 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. ## Automatically play videos on page load.
## ##

View file

@ -47,6 +47,7 @@
"Preferences": "Einstellungen", "Preferences": "Einstellungen",
"preferences_category_player": "Wiedergabeeinstellungen", "preferences_category_player": "Wiedergabeeinstellungen",
"preferences_video_loop_label": "Immer wiederholen: ", "preferences_video_loop_label": "Immer wiederholen: ",
"preferences_preload_label": "Videodaten vorladen: ",
"preferences_autoplay_label": "Automatisch abspielen: ", "preferences_autoplay_label": "Automatisch abspielen: ",
"preferences_continue_label": "Immer automatisch nächstes Video abspielen: ", "preferences_continue_label": "Immer automatisch nächstes Video abspielen: ",
"preferences_continue_autoplay_label": "Nächstes Video automatisch abspielen: ", "preferences_continue_autoplay_label": "Nächstes Video automatisch abspielen: ",
@ -322,7 +323,7 @@
"channel_tab_community_label": "Gemeinschaft", "channel_tab_community_label": "Gemeinschaft",
"search_filters_sort_option_relevance": "Relevanz", "search_filters_sort_option_relevance": "Relevanz",
"search_filters_sort_option_rating": "Bewertung", "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_sort_option_views": "Aufrufe",
"search_filters_type_label": "Inhaltstyp", "search_filters_type_label": "Inhaltstyp",
"search_filters_duration_label": "Dauer", "search_filters_duration_label": "Dauer",
@ -493,5 +494,8 @@
"Add to playlist": "Einer Wiedergabeliste hinzufügen", "Add to playlist": "Einer Wiedergabeliste hinzufügen",
"Search for videos": "Nach Videos suchen", "Search for videos": "Nach Videos suchen",
"toggle_theme": "Thema wechseln", "toggle_theme": "Thema wechseln",
"Add to playlist: ": "Einer Wiedergabeliste hinzufügen: " "Add to playlist: ": "Einer Wiedergabeliste hinzufügen: ",
"carousel_go_to": "Zu Folie `x` gehen",
"carousel_slide": "Folie {{current}} von {{total}}",
"carousel_skip": "Karussell überspringen"
} }

View file

@ -489,5 +489,10 @@
"search_filters_date_label": "Ημερομηνία αναφόρτωσης", "search_filters_date_label": "Ημερομηνία αναφόρτωσης",
"Search for videos": "Αναζήτηση βίντεο", "Search for videos": "Αναζήτηση βίντεο",
"The Popular feed has been disabled by the administrator.": "Η δημοφιλής ροή έχει απενεργοποιηθεί από τον διαχειριστή.", "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": "Αλλαγή θέματος"
} }

View file

@ -71,6 +71,7 @@
"Preferences": "Preferences", "Preferences": "Preferences",
"preferences_category_player": "Player preferences", "preferences_category_player": "Player preferences",
"preferences_video_loop_label": "Always loop: ", "preferences_video_loop_label": "Always loop: ",
"preferences_preload_label": "Preload video data: ",
"preferences_autoplay_label": "Autoplay: ", "preferences_autoplay_label": "Autoplay: ",
"preferences_continue_label": "Play next by default: ", "preferences_continue_label": "Play next by default: ",
"preferences_continue_autoplay_label": "Autoplay next video: ", "preferences_continue_autoplay_label": "Autoplay next video: ",
@ -423,7 +424,7 @@
"search_filters_title": "Filters", "search_filters_title": "Filters",
"search_filters_date_label": "Upload date", "search_filters_date_label": "Upload date",
"search_filters_date_option_none": "Any 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_today": "Today",
"search_filters_date_option_week": "This week", "search_filters_date_option_week": "This week",
"search_filters_date_option_month": "This month", "search_filters_date_option_month": "This month",
@ -455,7 +456,7 @@
"search_filters_sort_label": "Sort By", "search_filters_sort_label": "Sort By",
"search_filters_sort_option_relevance": "Relevance", "search_filters_sort_option_relevance": "Relevance",
"search_filters_sort_option_rating": "Rating", "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_sort_option_views": "View count",
"search_filters_apply_button": "Apply selected filters", "search_filters_apply_button": "Apply selected filters",
"Current version: ": "Current version: ", "Current version: ": "Current version: ",

View file

@ -480,7 +480,7 @@
"tokens_count_0": "{{count}} token", "tokens_count_0": "{{count}} token",
"tokens_count_1": "{{count}} tokens", "tokens_count_1": "{{count}} tokens",
"tokens_count_2": "{{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? ", "Popular enabled: ": "¿Habilitar la sección popular? ",
"error_video_not_in_playlist": "El video que solicitaste no existe en esta lista de reproducción. <a href=\"`x`\">Haz clic aquí para acceder a la página de inicio de la lista de reproducción.</a>", "error_video_not_in_playlist": "El video que solicitaste no existe en esta lista de reproducción. <a href=\"`x`\">Haz clic aquí para acceder a la página de inicio de la lista de reproducción.</a>",
"channel_tab_streams_label": "Directos", "channel_tab_streams_label": "Directos",

View file

@ -360,7 +360,7 @@
"search_filters_duration_label": "مدت", "search_filters_duration_label": "مدت",
"search_filters_features_label": "ویژگی‌ها", "search_filters_features_label": "ویژگی‌ها",
"search_filters_sort_label": "به ترتیب", "search_filters_sort_label": "به ترتیب",
"search_filters_date_option_hour": "یک ساعت گذشته", "search_filters_date_option_hour": "ساعت گذشته",
"search_filters_date_option_today": "امروز", "search_filters_date_option_today": "امروز",
"search_filters_date_option_week": "این هفته", "search_filters_date_option_week": "این هفته",
"search_filters_date_option_month": "این ماه", "search_filters_date_option_month": "این ماه",
@ -461,7 +461,7 @@
"Song: ": "آهنگ: ", "Song: ": "آهنگ: ",
"Channel Sponsor": "اسپانسر کانال", "Channel Sponsor": "اسپانسر کانال",
"Standard YouTube license": "پروانه استاندارد YouTube", "Standard YouTube license": "پروانه استاندارد YouTube",
"search_message_use_another_instance": " شما همچنین می‌توانید <a href=\"`x`\">در نمونه دیگر هم جستجو کنید</a>.", "search_message_use_another_instance": "همچنین می‌توانید <a href=\"`x`\">در نمونه‌ای دیگر هم جست‌وجو کنید</a>.",
"Download is disabled": "دریافت غیرفعال است", "Download is disabled": "دریافت غیرفعال است",
"crash_page_before_reporting": "پیش از گزارش ایراد، مطمئنید شوید که:", "crash_page_before_reporting": "پیش از گزارش ایراد، مطمئنید شوید که:",
"playlist_button_add_items": "افزودن ویدیو", "playlist_button_add_items": "افزودن ویدیو",

View file

@ -449,24 +449,24 @@
"Cantonese (Hong Kong)": "Kantonski (Hong Kong)", "Cantonese (Hong Kong)": "Kantonski (Hong Kong)",
"Chinese": "Kineski", "Chinese": "Kineski",
"Chinese (Taiwan)": "Kineski (Tajvan)", "Chinese (Taiwan)": "Kineski (Tajvan)",
"Dutch (auto-generated)": "Nizozemski (automatski generiran)", "Dutch (auto-generated)": "Nizozemski (automatski generirano)",
"French (auto-generated)": "Francuski (automatski generiran)", "French (auto-generated)": "Francuski (automatski generirano)",
"Indonesian (auto-generated)": "Indonezijski (automatski generiran)", "Indonesian (auto-generated)": "Indonezijski (automatski generirano)",
"Interlingue": "Interlingua", "Interlingue": "Interlingua",
"Japanese (auto-generated)": "Japanski (automatski generiran)", "Japanese (auto-generated)": "Japanski (automatski generirano)",
"Russian (auto-generated)": "Ruski (automatski generiran)", "Russian (auto-generated)": "Ruski (automatski generirano)",
"Turkish (auto-generated)": "Turski (automatski generiran)", "Turkish (auto-generated)": "Turski (automatski generirano)",
"Vietnamese (auto-generated)": "Vijetnamski (automatski generiran)", "Vietnamese (auto-generated)": "Vijetnamski (automatski generirano)",
"Spanish (Spain)": "Španjolski (Španjolska)", "Spanish (Spain)": "Španjolski (Španjolska)",
"Italian (auto-generated)": "Talijanski (automatski generiran)", "Italian (auto-generated)": "Talijanski (automatski generirano)",
"Portuguese (Brazil)": "Portugalski (Brazil)", "Portuguese (Brazil)": "Portugalski (Brazil)",
"Spanish (Mexico)": "Španjolski (Meksiko)", "Spanish (Mexico)": "Španjolski (Meksiko)",
"German (auto-generated)": "Njemački (automatski generiran)", "German (auto-generated)": "Njemački (automatski generirano)",
"Chinese (China)": "Kineski (Kina)", "Chinese (China)": "Kineski (Kina)",
"Chinese (Hong Kong)": "Kineski (Hong Kong)", "Chinese (Hong Kong)": "Kineski (Hong Kong)",
"Korean (auto-generated)": "Korejski (automatski generiran)", "Korean (auto-generated)": "Korejski (automatski generirano)",
"Portuguese (auto-generated)": "Portugalski (automatski generiran)", "Portuguese (auto-generated)": "Portugalski (automatski generirano)",
"Spanish (auto-generated)": "Španjolski (automatski generiran)", "Spanish (auto-generated)": "Španjolski (automatski generirano)",
"preferences_watch_history_label": "Aktiviraj povijest gledanja: ", "preferences_watch_history_label": "Aktiviraj povijest gledanja: ",
"search_filters_title": "Filtri", "search_filters_title": "Filtri",
"search_filters_date_option_none": "Bilo koji datum", "search_filters_date_option_none": "Bilo koji datum",

View file

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

View file

@ -363,7 +363,7 @@
"search_filters_features_option_location": "場所", "search_filters_features_option_location": "場所",
"search_filters_features_option_hdr": "HDR", "search_filters_features_option_hdr": "HDR",
"Current version: ": "現在のバージョン: ", "Current version: ": "現在のバージョン: ",
"next_steps_error_message": "以下をお試しください: ", "next_steps_error_message": "以下をお試しください: ",
"next_steps_error_message_refresh": "再読み込み", "next_steps_error_message_refresh": "再読み込み",
"next_steps_error_message_go_to_youtube": "YouTubeを開く", "next_steps_error_message_go_to_youtube": "YouTubeを開く",
"search_filters_duration_option_short": "4分未満", "search_filters_duration_option_short": "4分未満",
@ -396,7 +396,7 @@
"download_subtitles": "字幕 - `x` (.vtt)", "download_subtitles": "字幕 - `x` (.vtt)",
"search_filters_features_option_purchased": "購入済み", "search_filters_features_option_purchased": "購入済み",
"preferences_quality_option_dash": "DASH (適応的画質)", "preferences_quality_option_dash": "DASH (適応的画質)",
"preferences_quality_dash_option_worst": "最", "preferences_quality_dash_option_worst": "最",
"preferences_quality_dash_option_best": "最高", "preferences_quality_dash_option_best": "最高",
"videoinfo_started_streaming_x_ago": "`x`前に配信を開始", "videoinfo_started_streaming_x_ago": "`x`前に配信を開始",
"videoinfo_watch_on_youTube": "YouTubeで視聴", "videoinfo_watch_on_youTube": "YouTubeで視聴",

View file

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

View file

@ -322,13 +322,13 @@
"channel_tab_community_label": "Gemenskap", "channel_tab_community_label": "Gemenskap",
"search_filters_sort_option_relevance": "relevans", "search_filters_sort_option_relevance": "relevans",
"search_filters_sort_option_rating": "vurdering", "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_sort_option_views": "visninger",
"search_filters_type_label": "innholdstype", "search_filters_type_label": "innholdstype",
"search_filters_duration_label": "varighet", "search_filters_duration_label": "varighet",
"search_filters_features_label": "funksjoner", "search_filters_features_label": "funksjoner",
"search_filters_sort_label": "sorter", "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_today": "i dag",
"search_filters_date_option_week": "uke", "search_filters_date_option_week": "uke",
"search_filters_date_option_month": "måned", "search_filters_date_option_month": "måned",
@ -494,5 +494,7 @@
"carousel_slide": "Lysark {{current}} av {{total}}", "carousel_slide": "Lysark {{current}} av {{total}}",
"carousel_skip": "Hopp over karusellen", "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: " "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"
} }

View file

@ -317,13 +317,13 @@
"channel_tab_community_label": "Gemeenschap", "channel_tab_community_label": "Gemeenschap",
"search_filters_sort_option_relevance": "relevantie", "search_filters_sort_option_relevance": "relevantie",
"search_filters_sort_option_rating": "beoordeling", "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_sort_option_views": "keren bekeken",
"search_filters_type_label": "Type inhoud", "search_filters_type_label": "Type inhoud",
"search_filters_duration_label": "duur", "search_filters_duration_label": "duur",
"search_filters_features_label": "eigenschappen", "search_filters_features_label": "eigenschappen",
"search_filters_sort_label": "sorteren", "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_today": "vandaag",
"search_filters_date_option_week": "week", "search_filters_date_option_week": "week",
"search_filters_date_option_month": "maand", "search_filters_date_option_month": "maand",
@ -357,7 +357,7 @@
"footer_original_source_code": "Originele bron-code", "footer_original_source_code": "Originele bron-code",
"footer_modfied_source_code": "Gewijzigde bron-code", "footer_modfied_source_code": "Gewijzigde bron-code",
"adminprefs_modified_source_code_url_label": "URL naar gewijzigde bron-code-opslagplaats", "adminprefs_modified_source_code_url_label": "URL naar gewijzigde bron-code-opslagplaats",
"next_steps_error_message": "Daarna moet u proberen om: ", "next_steps_error_message": "Waarna u zou kunnen proberen om: ",
"footer_source_code": "Bron-code", "footer_source_code": "Bron-code",
"search_filters_duration_option_long": "Lang (> 20 minuten)", "search_filters_duration_option_long": "Lang (> 20 minuten)",
"preferences_quality_option_dash": "DASH (adaptieve kwaliteit)", "preferences_quality_option_dash": "DASH (adaptieve kwaliteit)",
@ -477,7 +477,7 @@
"Song: ": "Lied: ", "Song: ": "Lied: ",
"generic_channels_count": "{{count}} kanaal", "generic_channels_count": "{{count}} kanaal",
"generic_channels_count_plural": "{{count}} kanalen", "generic_channels_count_plural": "{{count}} kanalen",
"Popular enabled: ": "Populair geactiveerd: ", "Popular enabled: ": "Populair ingeschakeld: ",
"channel_tab_playlists_label": "Afspeellijsten", "channel_tab_playlists_label": "Afspeellijsten",
"generic_button_edit": "Bewerken", "generic_button_edit": "Bewerken",
"Music in this video": "Muziek in deze video", "Music in this video": "Muziek in deze video",

View file

@ -508,7 +508,7 @@
"toggle_theme": "Trocar tema", "toggle_theme": "Trocar tema",
"Add to playlist": "Adicionar à lista de reprodução", "Add to playlist": "Adicionar à lista de reprodução",
"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", "Search for videos": "Procurar vídeos",
"carousel_slide": "Diapositivo {{current}} de{{total}}", "carousel_slide": "Diapositivo {{current}} de{{total}}",
"carousel_skip": "Ignorar carrossel", "carousel_skip": "Ignorar carrossel",

View file

@ -509,6 +509,9 @@
"Add to playlist: ": "Добавить в плейлист: ", "Add to playlist: ": "Добавить в плейлист: ",
"Answer": "Ответить", "Answer": "Ответить",
"Search for videos": "Поиск видео", "Search for videos": "Поиск видео",
"The Popular feed has been disabled by the administrator.": "Популярная лента была отключена администратором.", "The Popular feed has been disabled by the administrator.": "Лента популярного была отключена администратором.",
"toggle_theme": "Переключатель тем" "toggle_theme": "Переключатель тем",
"carousel_slide": "Пролистано {{current}} из {{total}}",
"carousel_skip": "Пропустить всё",
"carousel_go_to": "Перейти к странице `x`"
} }

View file

@ -257,13 +257,13 @@
"Video mode": "Mënyrë video", "Video mode": "Mënyrë video",
"channel_tab_videos_label": "Video", "channel_tab_videos_label": "Video",
"search_filters_sort_option_rating": "Vlerësim", "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_sort_option_views": "Numër parjesh",
"search_filters_type_label": "Lloj", "search_filters_type_label": "Lloj",
"search_filters_duration_label": "Kohëzgjatje", "search_filters_duration_label": "Kohëzgjatje",
"search_filters_features_label": "Veçori", "search_filters_features_label": "Veçori",
"search_filters_sort_label": "Renditi Sipas", "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_date_option_today": "Sot",
"search_filters_duration_option_long": "E gjatë (> 20 minuta)", "search_filters_duration_option_long": "E gjatë (> 20 minuta)",
"search_filters_features_option_hd": "HD", "search_filters_features_option_hd": "HD",
@ -435,7 +435,7 @@
"tokens_count_plural": "{{count}} tokenë", "tokens_count_plural": "{{count}} tokenë",
"preferences_save_player_pos_label": "Mba mend pozicionin e luajtjes: ", "preferences_save_player_pos_label": "Mba mend pozicionin e luajtjes: ",
"Import Invidious data": "Importoni të dhëna JSON Invidious", "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", "Export data as JSON": "Eksportoji të dhënat Invidious si JSON",
"preferences_vr_mode_label": "Video me ndërveprim 360 gradë (lyp WebGL): ", "preferences_vr_mode_label": "Video me ndërveprim 360 gradë (lyp WebGL): ",
"Shared `x`": "Ndarë me të tjerë më `x`", "Shared `x`": "Ndarë me të tjerë më `x`",
@ -484,5 +484,13 @@
"Import YouTube watch history (.json)": "Importo historik parjesh YouTube (.json)", "Import YouTube watch history (.json)": "Importo historik parjesh YouTube (.json)",
"preferences_local_label": "Video përmes ndërmjetësi: ", "preferences_local_label": "Video përmes ndërmjetësi: ",
"Fallback captions: ": "Titra nga halli: ", "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`"
} }

View file

@ -320,13 +320,13 @@
"channel_tab_community_label": "Gemenskap", "channel_tab_community_label": "Gemenskap",
"search_filters_sort_option_relevance": "Relevans", "search_filters_sort_option_relevance": "Relevans",
"search_filters_sort_option_rating": "Rankning", "search_filters_sort_option_rating": "Rankning",
"search_filters_sort_option_date": "Uppladdnings Datum", "search_filters_sort_option_date": "Uppladdnings datum",
"search_filters_sort_option_views": "Visningar", "search_filters_sort_option_views": "Visningar",
"search_filters_type_label": "Typ", "search_filters_type_label": "Typ",
"search_filters_duration_label": "Varaktighet", "search_filters_duration_label": "Varaktighet",
"search_filters_features_label": "Funktioner", "search_filters_features_label": "Funktioner",
"search_filters_sort_label": "Sortera efter", "search_filters_sort_label": "Sortera efter",
"search_filters_date_option_hour": "Senaste Timmen", "search_filters_date_option_hour": "Senaste timmen",
"search_filters_date_option_today": "Idag", "search_filters_date_option_today": "Idag",
"search_filters_date_option_week": "Denna vecka", "search_filters_date_option_week": "Denna vecka",
"search_filters_date_option_month": "Denna månad", "search_filters_date_option_month": "Denna månad",

View file

@ -322,13 +322,13 @@
"channel_tab_community_label": "Topluluk", "channel_tab_community_label": "Topluluk",
"search_filters_sort_option_relevance": "İlgi", "search_filters_sort_option_relevance": "İlgi",
"search_filters_sort_option_rating": "Değerlendirme", "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_sort_option_views": "Görüntüleme Sayısı",
"search_filters_type_label": "Tür", "search_filters_type_label": "Tür",
"search_filters_duration_label": "Süre", "search_filters_duration_label": "Süre",
"search_filters_features_label": "Özellikler", "search_filters_features_label": "Özellikler",
"search_filters_sort_label": "Sıralama Ölçütü", "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_today": "Bugün",
"search_filters_date_option_week": "Bu Hafta", "search_filters_date_option_week": "Bu Hafta",
"search_filters_date_option_month": "Bu Ay", "search_filters_date_option_month": "Bu Ay",

View file

@ -455,7 +455,7 @@
"search_filters_date_option_week": "Цей тиждень", "search_filters_date_option_week": "Цей тиждень",
"search_filters_type_label": "Тип", "search_filters_type_label": "Тип",
"search_filters_type_option_channel": "Канал", "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_title": "Фільтри",
"search_filters_date_option_hour": "Остання година", "search_filters_date_option_hour": "Остання година",
"search_filters_date_option_month": "Цей місяць", "search_filters_date_option_month": "Цей місяць",
@ -472,7 +472,7 @@
"search_filters_features_option_three_sixty": "360°", "search_filters_features_option_three_sixty": "360°",
"search_filters_features_option_hdr": "HDR", "search_filters_features_option_hdr": "HDR",
"search_filters_sort_label": "Спершу", "search_filters_sort_label": "Спершу",
"search_filters_sort_option_date": "Нещодавні", "search_filters_sort_option_date": "Дата вивантаження",
"search_filters_apply_button": "Застосувати фільтри", "search_filters_apply_button": "Застосувати фільтри",
"search_filters_features_option_vr180": "VR180", "search_filters_features_option_vr180": "VR180",
"search_filters_features_option_purchased": "Придбано", "search_filters_features_option_purchased": "Придбано",

View file

@ -338,13 +338,13 @@
"channel_tab_community_label": "社群", "channel_tab_community_label": "社群",
"search_filters_sort_option_relevance": "關聯", "search_filters_sort_option_relevance": "關聯",
"search_filters_sort_option_rating": "評分", "search_filters_sort_option_rating": "評分",
"search_filters_sort_option_date": "日期", "search_filters_sort_option_date": "上傳日期",
"search_filters_sort_option_views": "檢視", "search_filters_sort_option_views": "檢視",
"search_filters_type_label": "內容類型", "search_filters_type_label": "內容類型",
"search_filters_duration_label": "時長", "search_filters_duration_label": "時長",
"search_filters_features_label": "特色", "search_filters_features_label": "特色",
"search_filters_sort_label": "排序", "search_filters_sort_label": "排序",
"search_filters_date_option_hour": "小時", "search_filters_date_option_hour": "最後一小時",
"search_filters_date_option_today": "今天", "search_filters_date_option_today": "今天",
"search_filters_date_option_week": "週", "search_filters_date_option_week": "週",
"search_filters_date_option_month": "月", "search_filters_date_option_month": "月",

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.length_seconds).to eq((56.minutes + 41.seconds).total_seconds.to_i32)
expect(video_11.views).to eq(40_504_893) expect(video_11.views).to eq(40_504_893)
expect(video_11.live_now).to be_false expect(video_11.badges.live_now?).to be_false
expect(video_11.premium).to be_false expect(video_11.badges.premium?).to be_false
expect(video_11.premiere_timestamp).to be_nil 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.length_seconds).to eq((3.minutes + 14.seconds).total_seconds.to_i32)
expect(video_35.views).to eq(30_790_049) expect(video_35.views).to eq(30_790_049)
expect(video_35.live_now).to be_false expect(video_35.badges.live_now?).to be_false
expect(video_35.premium).to be_false expect(video_35.badges.premium?).to be_false
expect(video_35.premiere_timestamp).to be_nil expect(video_35.premiere_timestamp).to be_nil
end 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.length_seconds).to eq((1.hour).total_seconds.to_i32)
expect(video_41.views).to eq(63_240) expect(video_41.views).to eq(63_240)
expect(video_41.live_now).to be_false expect(video_41.badges.live_now?).to be_false
expect(video_41.premium).to be_false expect(video_41.badges.premium?).to be_false
expect(video_41.premiere_timestamp).to be_nil 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.length_seconds).to eq((35.minutes + 46.seconds).total_seconds.to_i32)
expect(video_48.views).to eq(68_704) expect(video_48.views).to eq(68_704)
expect(video_48.live_now).to be_false expect(video_48.badges.live_now?).to be_false
expect(video_48.premium).to be_false expect(video_48.badges.premium?).to be_false
expect(video_48.premiere_timestamp).to be_nil expect(video_48.premiere_timestamp).to be_nil
end end
end end

View file

@ -72,6 +72,7 @@ PUBSUB_URL = URI.parse("https://pubsubhubbub.appspot.com")
REDDIT_URL = URI.parse("https://www.reddit.com") REDDIT_URL = URI.parse("https://www.reddit.com")
YT_URL = URI.parse("https://www.youtube.com") YT_URL = URI.parse("https://www.youtube.com")
HOST_URL = make_host_url(Kemal.config) HOST_URL = make_host_url(Kemal.config)
EXT_VIDEOP_LIST = gen_videoplayback_proxy_list()
CHARS_SAFE = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_" CHARS_SAFE = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"
TEST_IDS = {"AgbeGFYluEA", "BaW_jenozKc", "a9LDPn-MO4I", "ddFvjfvPnqk", "iqKdEhx-dD4"} TEST_IDS = {"AgbeGFYluEA", "BaW_jenozKc", "a9LDPn-MO4I", "ddFvjfvPnqk", "iqKdEhx-dD4"}
@ -115,6 +116,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| 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) CONFIG.log_level = LogLevel.parse(log_level)
end end
parser.on("-k", "--colorize", "Colorize logs") do
CONFIG.colorize_logs = true
end
parser.on("-v", "--version", "Print version") do parser.on("-v", "--version", "Print version") do
puts SOFTWARE.to_pretty_json puts SOFTWARE.to_pretty_json
exit exit
@ -131,7 +135,7 @@ if CONFIG.output.upcase != "STDOUT"
FileUtils.mkdir_p(File.dirname(CONFIG.output)) FileUtils.mkdir_p(File.dirname(CONFIG.output))
end end
OUTPUT = CONFIG.output.upcase == "STDOUT" ? STDOUT : File.open(CONFIG.output, mode: "a") 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 # Check table integrity
Invidious::Database.check_integrity(CONFIG) Invidious::Database.check_integrity(CONFIG)
@ -185,6 +189,14 @@ Invidious::Jobs.register Invidious::Jobs::ClearExpiredItemsJob.new
Invidious::Jobs.register Invidious::Jobs::InstanceListRefreshJob.new Invidious::Jobs.register Invidious::Jobs::InstanceListRefreshJob.new
if !CONFIG.external_videoplayback_proxy.empty?
Invidious::Jobs.register Invidious::Jobs::CheckExternalProxy.new
end
if CONFIG.refresh_tokens
Invidious::Jobs.register Invidious::Jobs::RefreshTokens.new
end
Invidious::Jobs.start_all Invidious::Jobs.start_all
def popular_videos def popular_videos

View file

@ -223,7 +223,7 @@ def fetch_channel(ucid, pull_all_videos : Bool)
length_seconds = channel_video.try &.length_seconds length_seconds = channel_video.try &.length_seconds
length_seconds ||= 0 length_seconds ||= 0
live_now = channel_video.try &.live_now live_now = channel_video.try &.badges.live_now?
live_now ||= false live_now ||= false
premiere_timestamp = channel_video.try &.premiere_timestamp premiere_timestamp = channel_video.try &.premiere_timestamp
@ -251,8 +251,8 @@ def fetch_channel(ucid, pull_all_videos : Bool)
LOGGER.trace("fetch_channel: #{ucid} : video #{video_id} : Inserted, updating subscriptions") LOGGER.trace("fetch_channel: #{ucid} : video #{video_id} : Inserted, updating subscriptions")
if CONFIG.enable_user_notifications if CONFIG.enable_user_notifications
Invidious::Database::Users.add_notification(video) Invidious::Database::Users.add_notification(video)
else # else
Invidious::Database::Users.feed_needs_update(video) # Invidious::Database::Users.feed_needs_update(video)
end end
else else
LOGGER.trace("fetch_channel: #{ucid} : video #{video_id} : Updated") LOGGER.trace("fetch_channel: #{ucid} : video #{video_id} : Updated")
@ -275,7 +275,7 @@ def fetch_channel(ucid, pull_all_videos : Bool)
ucid: video.ucid, ucid: video.ucid,
author: video.author, author: video.author,
length_seconds: video.length_seconds, length_seconds: video.length_seconds,
live_now: video.live_now, live_now: video.badges.live_now?,
premiere_timestamp: video.premiere_timestamp, premiere_timestamp: video.premiere_timestamp,
views: video.views, views: video.views,
}) })
@ -287,8 +287,8 @@ def fetch_channel(ucid, pull_all_videos : Bool)
if was_insert if was_insert
if CONFIG.enable_user_notifications if CONFIG.enable_user_notifications
Invidious::Database::Users.add_notification(video) Invidious::Database::Users.add_notification(video)
else # else
Invidious::Database::Users.feed_needs_update(video) # Invidious::Database::Users.feed_needs_update(video)
end end
end end
end end

View file

@ -13,6 +13,7 @@ struct ConfigPreferences
property annotations : Bool = false property annotations : Bool = false
property annotations_subscribed : Bool = false property annotations_subscribed : Bool = false
property preload : Bool = true
property autoplay : Bool = false property autoplay : Bool = false
property captions : Array(String) = ["", "", ""] property captions : Array(String) = ["", "", ""]
property comments : Array(String) = ["youtube", ""] property comments : Array(String) = ["youtube", ""]
@ -44,6 +45,8 @@ struct ConfigPreferences
property vr_mode : Bool = true property vr_mode : Bool = true
property show_nick : Bool = true property show_nick : Bool = true
property save_player_pos : Bool = false property save_player_pos : Bool = false
property po_token : String = ""
property visitor_data : String = ""
def to_tuple def to_tuple
{% begin %} {% begin %}
@ -66,6 +69,8 @@ class Config
property output : String = "STDOUT" property output : String = "STDOUT"
# Default log level, valid YAML values are ints and strings, see src/invidious/helpers/logger.cr # Default log level, valid YAML values are ints and strings, see src/invidious/helpers/logger.cr
property log_level : LogLevel = LogLevel::Info 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) # Database configuration with separate parameters (username, hostname, etc)
property db : DBConfig? = nil property db : DBConfig? = nil
@ -84,6 +89,8 @@ class Config
# Used to tell Invidious it is behind a proxy, so links to resources should be https:// # Used to tell Invidious it is behind a proxy, so links to resources should be https://
property https_only : Bool? property https_only : Bool?
# Enable or disable CSP
property csp : Bool? = true
# HMAC signing key for CSRF tokens and verifying pubsub subscriptions # HMAC signing key for CSRF tokens and verifying pubsub subscriptions
property hmac_key : String = "" property hmac_key : String = ""
# Domain to be used for links to resources on the site where an absolute URL is required # Domain to be used for links to resources on the site where an absolute URL is required
@ -169,6 +176,16 @@ class Config
# List of names of the backends # List of names of the backends
property backends : Array(String) = [] of String property backends : Array(String) = [] of String
# Character used to separate the backend number from the description/note
# of the backend
property backends_delimiter : String = "|"
# External videoplayback proxies list. They should include `https://`
# at the start of the URI
property external_videoplayback_proxy : Array(String) = [] of String
# Job to refresh tokens from a Redis compatible DB
property refresh_tokens : Bool = true
# Materialious redirects # Materialious redirects
property materialious_domain : String? property materialious_domain : String?

View file

@ -154,15 +154,17 @@ module Invidious::Database::Users
# Update (misc) # Update (misc)
# ------------------- # -------------------
def feed_needs_update(video : ChannelVideo) # Feeds never need update. PubSubHubBub is the one that sends videos to
request = <<-SQL # invidious.
UPDATE users # def feed_needs_update(video : ChannelVideo)
SET feed_needs_update = true # request = <<-SQL
WHERE $1 = ANY(subscriptions) # UPDATE users
SQL # SET feed_needs_update = true
# WHERE $1 = ANY(subscriptions)
# SQL
PG_DB.exec(request, video.ucid) # PG_DB.exec(request, video.ucid)
end # end
def update_preferences(user : User) def update_preferences(user : User)
request = <<-SQL request = <<-SQL

View file

@ -10,8 +10,8 @@ module Invidious::Database::Videos
ON CONFLICT (id) DO NOTHING ON CONFLICT (id) DO NOTHING
SQL SQL
REDIS_DB.set(video.id, video.info.to_json, ex: 3600) REDIS_DB.set(video.id, video.info.to_json, ex: 14400)
REDIS_DB.set(video.id + ":time", video.updated, ex: 3600) REDIS_DB.set(video.id + ":time", video.updated, ex: 14400)
end end
def delete(id) def delete(id)

View file

@ -43,6 +43,8 @@ def error_template_helper(env : HTTP::Server::Context, status_code : Int32, exce
# URLs for the error message below # URLs for the error message below
url_faq = "https://github.com/iv-org/documentation/blob/master/docs/faq.md" 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 = "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 url_switch = "https://redirect.invidious.io" + env.request.resource

View file

@ -1,3 +1,5 @@
require "colorize"
enum LogLevel enum LogLevel
All = 0 All = 0
Trace = 1 Trace = 1
@ -10,7 +12,7 @@ enum LogLevel
end end
class Invidious::LogHandler < Kemal::BaseLogHandler class Invidious::LogHandler < Kemal::BaseLogHandler
def initialize(@io : IO = STDOUT, @level = LogLevel::Debug) def initialize(@io : IO = STDOUT, @level = LogLevel::Debug, @color : Bool = true)
end end
def call(context : HTTP::Server::Context) def call(context : HTTP::Server::Context)
@ -39,10 +41,23 @@ class Invidious::LogHandler < Kemal::BaseLogHandler
@io.flush @io.flush
end 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) %} {% for level in %w(trace debug info warn error fatal) %}
def {{level.id}}(message : String) def {{level.id}}(message : String)
if LogLevel::{{level.id.capitalize}} >= @level if LogLevel::{{level.id.capitalize}} >= @level
puts("#{Time.utc} [{{level.id}}] #{message}") puts("#{Time.utc} [{{level.id}}] #{message}".colorize(color(LogLevel::{{level.id.capitalize}})).toggle(@color))
end end
end end
{% end %} {% end %}

View file

@ -0,0 +1,25 @@
module Tokens
extend self
@@po_token : String | Nil
@@visitor_data : String | Nil
def refresh_tokens
@@po_token = REDIS_DB.get("invidious:po_token")
@@visitor_data = REDIS_DB.get("invidious:visitor_data")
LOGGER.debug("RefreshTokens: Tokens are:")
LOGGER.debug("RefreshTokens: po_token: #{@@po_token}")
LOGGER.debug("RefreshTokens: visitor_data: #{@@visitor_data}")
end
def get_tokens
return {@@po_token, @@visitor_data}
end
def get_po_token
return @@po_token
end
def get_visitor_data
return @@visitor_data
end
end

View file

@ -1,3 +1,16 @@
@[Flags]
enum VideoBadges
LiveNow
Premium
ThreeD
FourK
New
EightK
VR180
VR360
ClosedCaptions
end
struct SearchVideo struct SearchVideo
include DB::Serializable include DB::Serializable
@ -9,10 +22,9 @@ struct SearchVideo
property views : Int64 property views : Int64
property description_html : String property description_html : String
property length_seconds : Int32 property length_seconds : Int32
property live_now : Bool
property premium : Bool
property premiere_timestamp : Time? property premiere_timestamp : Time?
property author_verified : Bool property author_verified : Bool
property badges : VideoBadges
def to_xml(auto_generated, query_params, xml : XML::Builder) def to_xml(auto_generated, query_params, xml : XML::Builder)
query_params["v"] = self.id query_params["v"] = self.id
@ -88,13 +100,20 @@ struct SearchVideo
json.field "published", self.published.to_unix json.field "published", self.published.to_unix
json.field "publishedText", translate(locale, "`x` ago", recode_date(self.published, locale)) json.field "publishedText", translate(locale, "`x` ago", recode_date(self.published, locale))
json.field "lengthSeconds", self.length_seconds json.field "lengthSeconds", self.length_seconds
json.field "liveNow", self.live_now json.field "liveNow", self.badges.live_now?
json.field "premium", self.premium json.field "premium", self.badges.premium?
json.field "isUpcoming", self.upcoming? json.field "isUpcoming", self.upcoming?
if self.premiere_timestamp if self.premiere_timestamp
json.field "premiereTimestamp", self.premiere_timestamp.try &.to_unix json.field "premiereTimestamp", self.premiere_timestamp.try &.to_unix
end 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
end end

View file

@ -176,7 +176,9 @@ module Invidious::SigHelper
@conn : Connection @conn : Connection
def initialize(uri_or_path) @uri_or_path : String
def initialize(@uri_or_path)
@conn = Connection.new(uri_or_path) @conn = Connection.new(uri_or_path)
listen listen
end end
@ -186,10 +188,26 @@ module Invidious::SigHelper
LOGGER.debug("SigHelper: Multiplexor listening") LOGGER.debug("SigHelper: Multiplexor listening")
# TODO: reopen socket if unexpectedly closed
spawn do spawn do
loop do loop do
begin
receive_data 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 Fiber.yield
end end
end end

View file

@ -383,3 +383,17 @@ def parse_link_endpoint(endpoint : JSON::Any, text : String, video_id : String)
end end
return text return text
end end
# Generates a list of external videoplayback proxies for
# CSP
def gen_videoplayback_proxy_list
if !CONFIG.external_videoplayback_proxy.empty?
external_videoplayback_proxy = ""
CONFIG.external_videoplayback_proxy.each do |proxy|
external_videoplayback_proxy += " #{proxy}"
end
else
external_videoplayback_proxy = ""
end
return external_videoplayback_proxy
end

View file

@ -4,6 +4,27 @@ module Invidious::HttpServer
module Utils module Utils
extend self extend self
@@proxy_alive : String = ""
def check_external_proxy
CONFIG.external_videoplayback_proxy.each do |proxy|
begin
response = HTTP::Client.get(proxy)
if response.status_code == 200
@@proxy_alive = proxy
LOGGER.debug("CheckExternalProxy: Proxy set to: '#{proxy}'")
break
end
rescue
LOGGER.debug("CheckExternalProxy: Proxy '#{proxy}' is not available")
end
end
end
def get_external_proxy
return @@proxy_alive
end
def proxy_video_url(raw_url : String, *, region : String? = nil, absolute : Bool = false) def proxy_video_url(raw_url : String, *, region : String? = nil, absolute : Bool = false)
url = URI.parse(raw_url) url = URI.parse(raw_url)
@ -14,7 +35,11 @@ module Invidious::HttpServer
url.query_params = params url.query_params = params
if absolute if absolute
if !@@proxy_alive.empty?
return "#{@@proxy_alive}#{url.request_target}"
else
return "#{HOST_URL}#{url.request_target}" return "#{HOST_URL}#{url.request_target}"
end
else else
return url.request_target return url.request_target
end end

View file

@ -0,0 +1,13 @@
class Invidious::Jobs::CheckExternalProxy < Invidious::Jobs::BaseJob
def initialize
end
def begin
loop do
HttpServer::Utils.check_external_proxy
LOGGER.info("CheckExternalProxy: Done, sleeping for 1 minute")
sleep 1.minutes
Fiber.yield
end
end
end

View file

@ -0,0 +1,13 @@
class Invidious::Jobs::RefreshTokens < Invidious::Jobs::BaseJob
def initialize
end
def begin
loop do
Tokens.refresh_tokens
LOGGER.info("RefreshTokens: Done, sleeping for 5 seconds")
sleep 5.seconds
Fiber.yield
end
end
end

View file

@ -270,7 +270,7 @@ end
def subscribe_playlist(user, playlist) def subscribe_playlist(user, playlist)
playlist = InvidiousPlaylist.new({ playlist = InvidiousPlaylist.new({
title: playlist.title.byte_slice(0, 150), title: playlist.title[..150],
id: playlist.id, id: playlist.id,
author: user.email, author: user.email,
description: "", # Max 5000 characters description: "", # Max 5000 characters

View file

@ -27,28 +27,21 @@ module Invidious::Routes::API::Manifest
haltf env, status_code: response.status_code haltf env, status_code: response.status_code
end end
manifest = response.body # Proxy URLs for video playback on invidious.
# Other API clients can get the original URLs by omiting `local=true`.
manifest = manifest.gsub(/<BaseURL>[^<]+<\/BaseURL>/) do |baseurl| manifest = response.body.gsub(/<BaseURL>[^<]+<\/BaseURL>/) do |baseurl|
url = baseurl.lchop("<BaseURL>") url = baseurl.lchop("<BaseURL>").rchop("</BaseURL>")
url = url.rchop("</BaseURL>") url = HttpServer::Utils.proxy_video_url(url, absolute: true) if local
if local
uri = URI.parse(url)
url = "#{HOST_URL}#{uri.request_target}host/#{uri.host}/"
end
"<BaseURL>#{url}</BaseURL>" "<BaseURL>#{url}</BaseURL>"
end end
return manifest return manifest
end end
adaptive_fmts = video.adaptive_fmts # Ditto, only proxify URLs if `local=true` is used
if local if local
adaptive_fmts.each do |fmt| video.adaptive_fmts.each do |fmt|
fmt["url"] = JSON::Any.new("#{HOST_URL}#{URI.parse(fmt["url"].as_s).request_target}") fmt["url"] = JSON::Any.new(HttpServer::Utils.proxy_video_url(fmt["url"].as_s, absolute: true))
end end
end end
@ -62,7 +55,6 @@ module Invidious::Routes::API::Manifest
end end
end end
manifest = XML.build(indent: " ", encoding: "UTF-8") do |xml| manifest = XML.build(indent: " ", encoding: "UTF-8") do |xml|
xml.element("MPD", "xmlns": "urn:mpeg:dash:schema:mpd:2011", xml.element("MPD", "xmlns": "urn:mpeg:dash:schema:mpd:2011",
"profiles": "urn:mpeg:dash:profile:full:2011", minBufferTime: "PT1.5S", type: "static", "profiles": "urn:mpeg:dash:profile:full:2011", minBufferTime: "PT1.5S", type: "static",
@ -185,8 +177,9 @@ module Invidious::Routes::API::Manifest
manifest = response.body manifest = response.body
if local if local
manifest = manifest.gsub(/^https:\/\/\w+---.{11}\.c\.youtube\.com[^\n]*/m) do |match| manifest = manifest.gsub(/^https:\/\/[^\n"]*/m) do |match|
path = URI.parse(match).path uri = URI.parse(match)
path = uri.path
path = path.lchop("/videoplayback/") path = path.lchop("/videoplayback/")
path = path.rchop("/") path = path.rchop("/")
@ -215,11 +208,17 @@ module Invidious::Routes::API::Manifest
raw_params["fvip"] = fvip["fvip"] raw_params["fvip"] = fvip["fvip"]
end end
raw_params["local"] = "true" raw_params["host"] = uri.host.not_nil!
proxy = Invidious::HttpServer::Utils.get_external_proxy
if !proxy.empty?
"#{proxy}/videoplayback?#{raw_params}"
else
"#{HOST_URL}/videoplayback?#{raw_params}" "#{HOST_URL}/videoplayback?#{raw_params}"
end end
end end
end
manifest manifest
end end

View file

@ -263,60 +263,61 @@ module Invidious::Routes::API::V1::Videos
annotations = "" annotations = ""
case source # case source
when "archive" # when "archive"
if CONFIG.cache_annotations && (cached_annotation = Invidious::Database::Annotations.select(id)) # if CONFIG.cache_annotations && (cached_annotation = Invidious::Database::Annotations.select(id))
annotations = cached_annotation.annotations # annotations = cached_annotation.annotations
else # else
index = CHARS_SAFE.index!(id[0]).to_s.rjust(2, '0') # index = CHARS_SAFE.index!(id[0]).to_s.rjust(2, '0')
# IA doesn't handle leading hyphens, # # IA doesn't handle leading hyphens,
# so we use https://archive.org/details/youtubeannotations_64 # # so we use https://archive.org/details/youtubeannotations_64
if index == "62" # if index == "62"
index = "64" # index = "64"
id = id.sub(/^-/, 'A') # id = id.sub(/^-/, 'A')
end # end
file = URI.encode_www_form("#{id[0, 3]}/#{id}.xml") # file = URI.encode_www_form("#{id[0, 3]}/#{id}.xml")
location = make_client(ARCHIVE_URL, &.get("/download/youtubeannotations_#{index}/#{id[0, 2]}.tar/#{file}")) # location = make_client(ARCHIVE_URL, &.get("/download/youtubeannotations_#{index}/#{id[0, 2]}.tar/#{file}"))
if !location.headers["Location"]? # if !location.headers["Location"]?
env.response.status_code = location.status_code # env.response.status_code = location.status_code
end # end
response = make_client(URI.parse(location.headers["Location"]), &.get(location.headers["Location"])) # response = make_client(URI.parse(location.headers["Location"]), &.get(location.headers["Location"]))
if response.body.empty? # if response.body.empty?
haltf env, 404 # haltf env, 404
end # end
if response.status_code != 200 # if response.status_code != 200
haltf env, response.status_code # haltf env, response.status_code
end # end
annotations = response.body # annotations = response.body
cache_annotation(id, annotations) # cache_annotation(id, annotations)
end # end
else # "youtube" # else # "youtube"
response = YT_POOL.client &.get("/annotations_invideo?video_id=#{id}") # response = YT_POOL.client &.get("/annotations_invideo?video_id=#{id}")
if response.status_code != 200 # if response.status_code != 200
haltf env, response.status_code # haltf env, response.status_code
end # end
annotations = response.body # annotations = response.body
end # end
etag = sha256(annotations)[0, 16] # etag = sha256(annotations)[0, 16]
if env.request.headers["If-None-Match"]?.try &.== etag # if env.request.headers["If-None-Match"]?.try &.== etag
haltf env, 304 # haltf env, 304
else # else
env.response.headers["ETag"] = etag # env.response.headers["ETag"] = etag
# annotations
# end
annotations annotations
end end
end
def self.comments(env) def self.comments(env)
locale = env.get("preferences").as(Preferences).locale locale = env.get("preferences").as(Preferences).locale

View file

@ -43,9 +43,9 @@ module Invidious::Routes::BeforeAll
"style-src 'self' 'unsafe-inline'", "style-src 'self' 'unsafe-inline'",
"img-src 'self' data:", "img-src 'self' data:",
"font-src 'self' data:", "font-src 'self' data:",
"connect-src 'self'", "connect-src 'self'" + EXT_VIDEOP_LIST,
"manifest-src 'self'", "manifest-src 'self'",
"media-src 'self' blob:" + extra_media_csp, "media-src 'self' blob:" + extra_media_csp + EXT_VIDEOP_LIST,
"child-src 'self' blob:", "child-src 'self' blob:",
"frame-src 'self'", "frame-src 'self'",
"frame-ancestors " + frame_ancestors, "frame-ancestors " + frame_ancestors,

View file

@ -157,10 +157,12 @@ module Invidious::Routes::Embed
adaptive_fmts = video.adaptive_fmts adaptive_fmts = video.adaptive_fmts
if params.local if params.local
fmt_stream.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)) }
adaptive_fmts.each { |fmt| fmt["url"] = JSON::Any.new(URI.parse(fmt["url"].as_s).request_target) }
end 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 video_streams = video.video_streams
audio_streams = video.audio_streams audio_streams = video.audio_streams

View file

@ -192,11 +192,9 @@ module Invidious::Routes::Feeds
views: views, views: views,
description_html: description_html, description_html: description_html,
length_seconds: 0, length_seconds: 0,
live_now: false,
paid: false,
premium: false,
premiere_timestamp: nil, premiere_timestamp: nil,
author_verified: false, author_verified: false,
badges: VideoBadges::None,
}) })
end end
@ -452,8 +450,8 @@ module Invidious::Routes::Feeds
if was_insert if was_insert
if CONFIG.enable_user_notifications if CONFIG.enable_user_notifications
Invidious::Database::Users.add_notification(video) Invidious::Database::Users.add_notification(video)
else # else
Invidious::Database::Users.feed_needs_update(video) # Invidious::Database::Users.feed_needs_update(video)
end end
end end
end end

View file

@ -27,6 +27,10 @@ module Invidious::Routes::PreferencesRoute
annotations_subscribed ||= "off" annotations_subscribed ||= "off"
annotations_subscribed = annotations_subscribed == "on" 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 = env.params.body["autoplay"]?.try &.as(String)
autoplay ||= "off" autoplay ||= "off"
autoplay = autoplay == "on" autoplay = autoplay == "on"
@ -82,6 +86,12 @@ module Invidious::Routes::PreferencesRoute
show_nick ||= "off" show_nick ||= "off"
show_nick = show_nick == "on" show_nick = show_nick == "on"
po_token = env.params.body["po_token"]?.try &.as(String)
po_token ||= CONFIG.default_user_preferences.po_token
visitor_data = env.params.body["visitor_data"]?.try &.as(String)
visitor_data ||= CONFIG.default_user_preferences.visitor_data
comments = [] of String comments = [] of String
2.times do |i| 2.times do |i|
comments << (env.params.body["comments[#{i}]"]?.try &.as(String) || CONFIG.default_user_preferences.comments[i]) comments << (env.params.body["comments[#{i}]"]?.try &.as(String) || CONFIG.default_user_preferences.comments[i])
@ -144,6 +154,7 @@ module Invidious::Routes::PreferencesRoute
preferences = Preferences.from_json({ preferences = Preferences.from_json({
annotations: annotations, annotations: annotations,
annotations_subscribed: annotations_subscribed, annotations_subscribed: annotations_subscribed,
preload: preload,
autoplay: autoplay, autoplay: autoplay,
captions: captions, captions: captions,
comments: comments, comments: comments,
@ -175,6 +186,8 @@ module Invidious::Routes::PreferencesRoute
vr_mode: vr_mode, vr_mode: vr_mode,
show_nick: show_nick, show_nick: show_nick,
save_player_pos: save_player_pos, save_player_pos: save_player_pos,
po_token: po_token,
visitor_data: visitor_data,
}.to_json) }.to_json)
if user = env.get? "user" if user = env.get? "user"

View file

@ -44,7 +44,7 @@ module Invidious::Routes::VideoPlayback
headers["Origin"] = "https://www.youtube.com" headers["Origin"] = "https://www.youtube.com"
headers["Referer"] = "https://www.youtube.com/" headers["Referer"] = "https://www.youtube.com/"
headers["User-Agent"] = "Mozilla/5.0 (X11; Linux x86_64; rv:122.0) Gecko/20100101 Firefox/122.0" headers["User-Agent"] = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36"
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) response = HTTP::Client::Response.new(500)
@ -298,7 +298,16 @@ module Invidious::Routes::VideoPlayback
end end
if local if local
external_proxy = Invidious::HttpServer::Utils.get_external_proxy
if !external_proxy.empty?
url = URI.parse(url)
external_proxy = URI.parse(external_proxy)
url.host = external_proxy.host
url.port = external_proxy.port
url = url.to_s
else
url = URI.parse(url).request_target.not_nil! url = URI.parse(url).request_target.not_nil!
end
url += "&title=#{URI.encode_www_form(title, space_to_plus: false)}" if title url += "&title=#{URI.encode_www_form(title, space_to_plus: false)}" if title
end end

View file

@ -3,6 +3,9 @@
module Invidious::Routes::Watch module Invidious::Routes::Watch
def self.handle(env) def self.handle(env)
locale = env.get("preferences").as(Preferences).locale locale = env.get("preferences").as(Preferences).locale
user_po_token = env.get("preferences").as(Preferences).po_token
user_visitor_data = env.get("preferences").as(Preferences).visitor_data
region = env.params.query["region"]? region = env.params.query["region"]?
if env.params.query.to_s.includes?("%20") || env.params.query.to_s.includes?("+") if env.params.query.to_s.includes?("%20") || env.params.query.to_s.includes?("+")
@ -52,7 +55,7 @@ module Invidious::Routes::Watch
env.params.query.delete_all("listen") env.params.query.delete_all("listen")
begin begin
video = get_video(id, region: params.region) video = get_video(id, region: params.region, po_token: user_po_token, visitor_data: user_visitor_data)
rescue ex : NotFoundException rescue ex : NotFoundException
LOGGER.error("get_video not found: #{id} : #{ex.message}") LOGGER.error("get_video not found: #{id} : #{ex.message}")
return error_template(404, ex) return error_template(404, ex)
@ -128,10 +131,12 @@ module Invidious::Routes::Watch
end end
if params.local if params.local
fmt_stream.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)) }
adaptive_fmts.each { |fmt| fmt["url"] = JSON::Any.new(URI.parse(fmt["url"].as_s).request_target) }
end 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 video_streams = video.video_streams
audio_streams = video.audio_streams audio_streams = video.audio_streams

View file

@ -4,6 +4,7 @@ struct Preferences
property annotations : Bool = CONFIG.default_user_preferences.annotations property annotations : Bool = CONFIG.default_user_preferences.annotations
property annotations_subscribed : Bool = CONFIG.default_user_preferences.annotations_subscribed 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 autoplay : Bool = CONFIG.default_user_preferences.autoplay
property automatic_instance_redirect : Bool = CONFIG.default_user_preferences.automatic_instance_redirect property automatic_instance_redirect : Bool = CONFIG.default_user_preferences.automatic_instance_redirect
@ -56,6 +57,10 @@ struct Preferences
property volume : Int32 = CONFIG.default_user_preferences.volume property volume : Int32 = CONFIG.default_user_preferences.volume
property save_player_pos : Bool = CONFIG.default_user_preferences.save_player_pos property save_player_pos : Bool = CONFIG.default_user_preferences.save_player_pos
@[YAML::Field(converter: Preferences::ProcessString)]
property po_token : String = ""
property visitor_data : String = ""
module BoolToString module BoolToString
def self.to_json(value : String, json : JSON::Builder) def self.to_json(value : String, json : JSON::Builder)
json.string value json.string value

View file

@ -26,12 +26,6 @@ struct Video
@[DB::Field(ignore: true)] @[DB::Field(ignore: true)]
@captions = [] of Invidious::Videos::Captions::Metadata @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)] @[DB::Field(ignore: true)]
property description : String? property description : String?
@ -98,72 +92,24 @@ struct Video
# Methods for parsing streaming data # Methods for parsing streaming data
def convert_url(fmt) def fmt_stream : Array(Hash(String, JSON::Any))
if cfr = fmt["signatureCipher"]?.try { |json| HTTP::Params.parse(json.as_s) } if formats = info.dig?("streamingData", "formats")
sp = cfr["sp"] return formats
url = URI.parse(cfr["url"]) .as_a.map(&.as_h)
params = url.query_params .sort_by! { |f| f["width"]?.try &.as_i || 0 }
LOGGER.debug("Videos: Decoding '#{cfr}'")
unsig = DECRYPT_FUNCTION.try &.decrypt_signature(cfr["s"])
params[sp] = unsig if unsig
else else
url = URI.parse(fmt["url"].as_s) return [] of Hash(String, JSON::Any)
params = url.query_params end
end end
n = DECRYPT_FUNCTION.try &.decrypt_nsig(params["n"]) def adaptive_fmts : Array(Hash(String, JSON::Any))
params["n"] = n if n if formats = info.dig?("streamingData", "adaptiveFormats")
return formats
if token = CONFIG.po_token .as_a.map(&.as_h)
params["pot"] = token .sort_by! { |f| f["width"]?.try &.as_i || 0 }
else
return [] of Hash(String, JSON::Any)
end 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))
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 end
def video_streams def video_streams
@ -348,7 +294,7 @@ struct Video
predicate_bool upcoming, isUpcoming predicate_bool upcoming, isUpcoming
end end
def get_video(id, refresh = true, region = nil, force_refresh = false) def get_video(id, refresh = true, region = nil, force_refresh = false, po_token = "", visitor_data = "")
if (video = Invidious::Database::Videos.select(id)) && !region if (video = Invidious::Database::Videos.select(id)) && !region
# If record was last updated over 10 minutes ago, or video has since premiered, # If record was last updated over 10 minutes ago, or video has since premiered,
# refresh (expire param in response lasts for 6 hours) # refresh (expire param in response lasts for 6 hours)
@ -358,7 +304,7 @@ def get_video(id, refresh = true, region = nil, force_refresh = false)
force_refresh || force_refresh ||
video.schema_version != Video::SCHEMA_VERSION # cache control video.schema_version != Video::SCHEMA_VERSION # cache control
begin begin
video = fetch_video(id, region) video = fetch_video(id, region, po_token, visitor_data)
Invidious::Database::Videos.insert(video) Invidious::Database::Videos.insert(video)
rescue ex rescue ex
Invidious::Database::Videos.delete(id) Invidious::Database::Videos.delete(id)
@ -366,7 +312,7 @@ def get_video(id, refresh = true, region = nil, force_refresh = false)
end end
end end
else else
video = fetch_video(id, region) video = fetch_video(id, region, po_token, visitor_data)
Invidious::Database::Videos.insert(video) if !region Invidious::Database::Videos.insert(video) if !region
end end
@ -374,11 +320,11 @@ def get_video(id, refresh = true, region = nil, force_refresh = false)
rescue DB::Error rescue DB::Error
# Avoid common `DB::PoolRetryAttemptsExceeded` error and friends # Avoid common `DB::PoolRetryAttemptsExceeded` error and friends
# Note: All DB errors inherit from `DB::Error` # Note: All DB errors inherit from `DB::Error`
return fetch_video(id, region) return fetch_video(id, region, po_token, visitor_data)
end end
def fetch_video(id, region) def fetch_video(id, region, po_token, visitor_data)
info = extract_video_info(video_id: id) info = extract_video_info(video_id: id, user_po_token: po_token, user_visitor_data: visitor_data)
if reason = info["reason"]? if reason = info["reason"]?
if reason == "Video unavailable" if reason == "Video unavailable"

View file

@ -50,12 +50,17 @@ def parse_related_video(related : JSON::Any) : Hash(String, JSON::Any)?
} }
end end
def extract_video_info(video_id : String) def extract_video_info(video_id : String, user_po_token, user_visitor_data)
# Init client config for the API # Init client config for the API
client_config = YoutubeAPI::ClientConfig.new client_config = YoutubeAPI::ClientConfig.new
redis_po_token, redis_visitor_data = Tokens.get_tokens
po_token = (user_po_token if !user_po_token.empty?) || redis_po_token || CONFIG.po_token
visitor_data = (user_visitor_data if !user_visitor_data.empty?) || redis_visitor_data || CONFIG.visitor_data
# Fetch data from the player endpoint # Fetch data from the player endpoint
player_response = YoutubeAPI.player(video_id: video_id, params: "2AMB", client_config: client_config) player_response = YoutubeAPI.player(video_id: video_id, params: "2AMB", client_config: client_config, po_token: po_token, visitor_data: visitor_data)
playability_status = player_response.dig?("playabilityStatus", "status").try &.as_s playability_status = player_response.dig?("playabilityStatus", "status").try &.as_s
@ -104,22 +109,22 @@ def extract_video_info(video_id : String)
# Don't use Android client if po_token is passed because po_token doesn't # Don't use Android client if po_token is passed because po_token doesn't
# work for Android client. # work for Android client.
if reason.nil? && CONFIG.po_token.nil? if reason.nil? && po_token.nil?
# Fetch the video streams using an Android client in order to get the # Fetch the video streams using an Android client in order to get the
# decrypted URLs and maybe fix throttling issues (#2194). See the # decrypted URLs and maybe fix throttling issues (#2194). See the
# following issue for an explanation about decrypted URLs: # following issue for an explanation about decrypted URLs:
# https://github.com/TeamNewPipe/NewPipeExtractor/issues/562 # https://github.com/TeamNewPipe/NewPipeExtractor/issues/562
client_config.client_type = YoutubeAPI::ClientType::AndroidTestSuite client_config.client_type = YoutubeAPI::ClientType::AndroidTestSuite
new_player_response = try_fetch_streaming_data(video_id, client_config) new_player_response = try_fetch_streaming_data(video_id, client_config, po_token, visitor_data)
end end
# Last hope # Last hope
# Only trigger if reason found and po_token or didn't work wth Android client. # Only trigger if reason found and po_token or didn't work wth Android client.
# TvHtml5ScreenEmbed now requires sig helper for it to work but po_token is not required # TvHtml5ScreenEmbed now requires sig helper for it to work but po_token is not required
# if the IP address is not blocked. # if the IP address is not blocked.
if CONFIG.po_token && reason || CONFIG.po_token.nil? && new_player_response.nil? if po_token.nil? && reason || po_token.nil? && new_player_response.nil?
client_config.client_type = YoutubeAPI::ClientType::TvHtml5ScreenEmbed client_config.client_type = YoutubeAPI::ClientType::TvHtml5ScreenEmbed
new_player_response = try_fetch_streaming_data(video_id, client_config) new_player_response = try_fetch_streaming_data(video_id, client_config, po_token, visitor_data)
end end
# Replace player response and reset reason # Replace player response and reset reason
@ -132,19 +137,30 @@ def extract_video_info(video_id : String)
params.delete("reason") params.delete("reason")
end end
{"captions", "playabilityStatus", "playerConfig", "storyboards", "streamingData"}.each do |f| {"captions", "playabilityStatus", "playerConfig", "storyboards"}.each do |f|
params[f] = player_response[f] if player_response[f]? params[f] = player_response[f] if player_response[f]?
end end
# Convert URLs, if those are present
if streaming_data = player_response["streamingData"]?
%w[formats adaptiveFormats].each do |key|
streaming_data.as_h[key]?.try &.as_a.each do |format|
format.as_h["url"] = JSON::Any.new(convert_url(format, po_token))
end
end
params["streamingData"] = streaming_data
end
# Data structure version, for cache control # Data structure version, for cache control
params["version"] = JSON::Any.new(Video::SCHEMA_VERSION.to_i64) params["version"] = JSON::Any.new(Video::SCHEMA_VERSION.to_i64)
return params return params
end end
def try_fetch_streaming_data(id : String, client_config : YoutubeAPI::ClientConfig) : Hash(String, JSON::Any)? def try_fetch_streaming_data(id : String, client_config : YoutubeAPI::ClientConfig, po_token, visitor_data) : Hash(String, JSON::Any)?
LOGGER.debug("try_fetch_streaming_data: [#{id}] Using #{client_config.client_type} client.") LOGGER.debug("try_fetch_streaming_data: [#{id}] Using #{client_config.client_type} client.")
response = YoutubeAPI.player(video_id: id, params: "2AMB", client_config: client_config) response = YoutubeAPI.player(video_id: id, params: "2AMB", client_config: client_config, po_token: po_token, visitor_data: visitor_data)
playability_status = response["playabilityStatus"]["status"] playability_status = response["playabilityStatus"]["status"]
LOGGER.debug("try_fetch_streaming_data: [#{id}] Got playabilityStatus == #{playability_status}.") LOGGER.debug("try_fetch_streaming_data: [#{id}] Got playabilityStatus == #{playability_status}.")
@ -443,3 +459,37 @@ def parse_video_info(video_id : String, player_response : Hash(String, JSON::Any
return params return params
end end
private def convert_url(fmt, po_token)
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("convert_url: Decoding '#{cfr}'")
unsig = DECRYPT_FUNCTION.try &.decrypt_signature(cfr["s"])
params[sp] = unsig if unsig
else
url = URI.parse(fmt["url"].as_s)
params = url.query_params
end
n = DECRYPT_FUNCTION.try &.decrypt_nsig(params["n"])
params["n"] = n if n
if !po_token.nil?
params["pot"] = po_token
elsif token = CONFIG.po_token
params["pot"] = token
end
url.query_params = params
LOGGER.trace("convert_url: new url is '#{url}'")
return url.to_s
rescue ex
LOGGER.debug("convert_url: Error when parsing video URL")
LOGGER.trace(ex.inspect_with_backtrace)
return ""
end

View file

@ -2,6 +2,7 @@ struct VideoPreferences
include JSON::Serializable include JSON::Serializable
property annotations : Bool property annotations : Bool
property preload : Bool
property autoplay : Bool property autoplay : Bool
property comments : Array(String) property comments : Array(String)
property continue : Bool property continue : Bool
@ -28,6 +29,7 @@ end
def process_video_params(query, preferences) def process_video_params(query, preferences)
annotations = query["iv_load_policy"]?.try &.to_i? annotations = query["iv_load_policy"]?.try &.to_i?
preload = query["preload"]?.try { |q| (q == "true" || q == "1").to_unsafe }
autoplay = query["autoplay"]?.try { |q| (q == "true" || q == "1").to_unsafe } autoplay = query["autoplay"]?.try { |q| (q == "true" || q == "1").to_unsafe }
comments = query["comments"]?.try &.split(",").map(&.downcase) comments = query["comments"]?.try &.split(",").map(&.downcase)
continue = query["continue"]?.try { |q| (q == "true" || q == "1").to_unsafe } continue = query["continue"]?.try { |q| (q == "true" || q == "1").to_unsafe }
@ -50,6 +52,7 @@ def process_video_params(query, preferences)
if preferences if preferences
# region ||= preferences.region # region ||= preferences.region
annotations ||= preferences.annotations.to_unsafe annotations ||= preferences.annotations.to_unsafe
preload ||= preferences.preload.to_unsafe
autoplay ||= preferences.autoplay.to_unsafe autoplay ||= preferences.autoplay.to_unsafe
comments ||= preferences.comments comments ||= preferences.comments
continue ||= preferences.continue.to_unsafe continue ||= preferences.continue.to_unsafe
@ -70,6 +73,7 @@ def process_video_params(query, preferences)
end end
annotations ||= CONFIG.default_user_preferences.annotations.to_unsafe annotations ||= CONFIG.default_user_preferences.annotations.to_unsafe
preload ||= CONFIG.default_user_preferences.preload.to_unsafe
autoplay ||= CONFIG.default_user_preferences.autoplay.to_unsafe autoplay ||= CONFIG.default_user_preferences.autoplay.to_unsafe
comments ||= CONFIG.default_user_preferences.comments comments ||= CONFIG.default_user_preferences.comments
continue ||= CONFIG.default_user_preferences.continue.to_unsafe continue ||= CONFIG.default_user_preferences.continue.to_unsafe
@ -89,6 +93,7 @@ def process_video_params(query, preferences)
save_player_pos ||= CONFIG.default_user_preferences.save_player_pos.to_unsafe save_player_pos ||= CONFIG.default_user_preferences.save_player_pos.to_unsafe
annotations = annotations == 1 annotations = annotations == 1
preload = preload == 1
autoplay = autoplay == 1 autoplay = autoplay == 1
continue = continue == 1 continue = continue == 1
continue_autoplay = continue_autoplay == 1 continue_autoplay = continue_autoplay == 1
@ -128,6 +133,7 @@ def process_video_params(query, preferences)
params = VideoPreferences.new({ params = VideoPreferences.new({
annotations: annotations, annotations: annotations,
preload: preload,
autoplay: autoplay, autoplay: autoplay,
comments: comments, comments: comments,
continue: continue, continue: continue,

View file

@ -1,5 +1,6 @@
<video style="outline:none;width:100%;background-color:#000" playsinline poster="<%= thumbnail %>" <video style="outline:none;width:100%;background-color:#000" playsinline poster="<%= thumbnail %>"
id="player" class="on-video_player video-js player-style-<%= params.player_style %>" id="player" class="on-video_player video-js player-style-<%= params.player_style %>"
preload="<% if params.preload %>auto<% else %>none<% end %>"
<% if params.autoplay %>autoplay<% end %> <% if params.autoplay %>autoplay<% end %>
<% if params.video_loop %>loop<% end %> <% if params.video_loop %>loop<% end %>
<% if params.controls %>controls<% end %>> <% if params.controls %>controls<% end %>>

View file

@ -1,6 +1,7 @@
<% <%
locale = env.get("preferences").as(Preferences).locale locale = env.get("preferences").as(Preferences).locale
dark_mode = env.get("preferences").as(Preferences).dark_mode dark_mode = env.get("preferences").as(Preferences).dark_mode
current_backend = env.request.cookies["SERVER_ID"]?.try &.value
%> %>
<!DOCTYPE html> <!DOCTYPE html>
<html lang="<%= locale %>"> <html lang="<%= locale %>">
@ -104,13 +105,26 @@
</div> </div>
</div> </div>
<% if CONFIG.backends %> <% if !CONFIG.backends.empty? %>
<div class="h-box"> <div class="h-box">
<b>Switch Backend:</b> <b>Switch Backend:</b>
<% CONFIG.backends.each do | backend | %> <% CONFIG.backends.each do | backend | %>
<a href="/switchbackend?backend_id=<%= backend %>"> <% backend = backend.split(CONFIG.backends_delimiter) %>
Backend<%= HTML.escape(backend) %> <% if current_backend == backend[0] %>
</a> | <a href="/switchbackend?backend_id=<%= backend[0] %>" style="text-decoration-line: underline; display: inline-block;">
Backend<%= HTML.escape(backend[0]) %>
<% if backend.size == 2 %>
<%= HTML.escape(backend[1]) %>
<% end %>
</a> <span> | </span>
<% else %>
<a href="/switchbackend?backend_id=<%= backend[0] %>" style="display: inline-block;">
Backend<%= HTML.escape(backend[0]) %>
<% if backend.size == 2 %>
<%= HTML.escape(backend[1]) %>
<% end %>
</a> <span> | </span>
<% end %>
<% end %> <% end %>
</div> </div>
<% end %> <% end %>
@ -296,6 +310,7 @@
</div> </div>
<hr/> <hr/>
<div class="footer-footer"> <div class="footer-footer">
<div class="box">You are currently using Backend: <%= current_backend %></p>
<span class="left"> <span class="left">
<% if CONFIG.modified_source_code_url %> <% if CONFIG.modified_source_code_url %>
<%= translate(locale, "footer_current_version_modified") %> <%= translate(locale, "footer_current_version_modified") %>

View file

@ -12,6 +12,11 @@
<input name="video_loop" id="video_loop" type="checkbox" <% if preferences.video_loop %>checked<% end %>> <input name="video_loop" id="video_loop" type="checkbox" <% if preferences.video_loop %>checked<% end %>>
</div> </div>
<div class="pure-control-group">
<label for="preload"><%= translate(locale, "preferences_preload_label") %></label>
<input name="preload" id="preload" type="checkbox" <% if preferences.preload %>checked<% end %>>
</div>
<div class="pure-control-group"> <div class="pure-control-group">
<label for="autoplay"><%= translate(locale, "preferences_autoplay_label") %></label> <label for="autoplay"><%= translate(locale, "preferences_autoplay_label") %></label>
<input name="autoplay" id="autoplay" type="checkbox" <% if preferences.autoplay %>checked<% end %>> <input name="autoplay" id="autoplay" type="checkbox" <% if preferences.autoplay %>checked<% end %>>
@ -121,6 +126,16 @@
<input name="save_player_pos" id="save_player_pos" type="checkbox" <% if preferences.save_player_pos %>checked<% end %>> <input name="save_player_pos" id="save_player_pos" type="checkbox" <% if preferences.save_player_pos %>checked<% end %>>
</div> </div>
<div class="pure-control-group">
<label for="po_token"><%= translate(locale, "preferences_po_token") %></label>
<input name="po_token" id="po_token" type="text" value="<%= preferences.po_token %>">
</div>
<div class="pure-control-group">
<label for="visitor_data"><%= translate(locale, "preferences_visitor_data") %></label>
<input name="visitor_data" id="visitor_data" type="text" value="<%= preferences.visitor_data %>">
</div>
<legend><%= translate(locale, "preferences_category_visual") %></legend> <legend><%= translate(locale, "preferences_category_visual") %></legend>
<div class="pure-control-group"> <div class="pure-control-group">

View file

@ -1,10 +1,10 @@
def add_yt_headers(request) def add_yt_headers(request)
request.headers.delete("User-Agent") if request.headers["User-Agent"] == "Crystal" request.headers.delete("User-Agent") if request.headers["User-Agent"] == "Crystal"
request.headers["User-Agent"] ||= "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36" request.headers["User-Agent"] ||= "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36"
request.headers["Accept-Charset"] ||= "ISO-8859-1,utf-8;q=0.7,*;q=0.7" request.headers["Accept-Charset"] ||= "ISO-8859-1,utf-8;q=0.7,*;q=0.7"
request.headers["Accept"] ||= "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8" request.headers["Accept"] ||= "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7"
request.headers["Accept-Language"] ||= "en-us,en;q=0.5" request.headers["Accept-Language"] ||= "en-US,en;q=0.9"
# Preserve original cookies and add new YT consent cookie for EU servers # Preserve original cookies and add new YT consent cookie for EU servers
request.headers["Cookie"] = "#{request.headers["cookie"]?}; CONSENT=PENDING+#{Random.rand(100..999)}" request.headers["Cookie"] = "#{request.headers["cookie"]?}; CONSENT=PENDING+#{Random.rand(100..999)}"

View file

@ -108,21 +108,30 @@ private module Parsers
length_seconds = 0 length_seconds = 0
end end
live_now = false
premium = false
premiere_timestamp = item_contents.dig?("upcomingEventData", "startTime").try { |t| Time.unix(t.as_s.to_i64) } premiere_timestamp = item_contents.dig?("upcomingEventData", "startTime").try { |t| Time.unix(t.as_s.to_i64) }
badges = VideoBadges::None
item_contents["badges"]?.try &.as_a.each do |badge| item_contents["badges"]?.try &.as_a.each do |badge|
b = badge["metadataBadgeRenderer"] b = badge["metadataBadgeRenderer"]
case b["label"].as_s case b["label"].as_s
when "LIVE NOW" when "LIVE"
live_now = true badges |= VideoBadges::LiveNow
when "New", "4K", "CC" when "New"
# TODO badges |= VideoBadges::New
when "4K"
badges |= VideoBadges::FourK
when "8K"
badges |= VideoBadges::EightK
when "VR180"
badges |= VideoBadges::VR180
when "360°"
badges |= VideoBadges::VR360
when "3D"
badges |= VideoBadges::ThreeD
when "CC"
badges |= VideoBadges::ClosedCaptions
when "Premium" when "Premium"
# TODO: Potentially available as item_contents["topStandaloneBadge"]["metadataBadgeRenderer"] # TODO: Potentially available as item_contents["topStandaloneBadge"]["metadataBadgeRenderer"]
premium = true badges |= VideoBadges::Premium
else nil # Ignore else nil # Ignore
end end
end end
@ -136,10 +145,9 @@ private module Parsers
views: view_count, views: view_count,
description_html: description_html, description_html: description_html,
length_seconds: length_seconds, length_seconds: length_seconds,
live_now: live_now,
premium: premium,
premiere_timestamp: premiere_timestamp, premiere_timestamp: premiere_timestamp,
author_verified: author_verified, author_verified: author_verified,
badges: badges,
}) })
end end
@ -563,10 +571,9 @@ private module Parsers
views: view_count, views: view_count,
description_html: "", description_html: "",
length_seconds: duration, length_seconds: duration,
live_now: false,
premium: false,
premiere_timestamp: Time.unix(0), premiere_timestamp: Time.unix(0),
author_verified: false, author_verified: false,
badges: VideoBadges::None,
}) })
end end

View file

@ -111,7 +111,7 @@ module UrlSanitizer
new_uri.path = "/watch" new_uri.path = "/watch"
new_params = copy_params(unsafe_uri.query_params, :watch) new_params = copy_params(unsafe_uri.query_params, :watch)
new_params["id"] = breadcrumbs[0] new_params["v"] = breadcrumbs[0]
new_uri.query_params = new_params new_uri.query_params = new_params
end end

View file

@ -3,6 +3,8 @@
# #
module YoutubeAPI module YoutubeAPI
@@visitor_data : String = ""
extend self extend self
# For Android versions, see https://en.wikipedia.org/wiki/Android_version_history # For Android versions, see https://en.wikipedia.org/wiki/Android_version_history
@ -320,7 +322,9 @@ module YoutubeAPI
client_context["client"]["platform"] = platform client_context["client"]["platform"] = platform
end end
if CONFIG.visitor_data.is_a?(String) if !@@visitor_data.empty?
client_context["client"]["visitorData"] = @@visitor_data
elsif CONFIG.visitor_data.is_a?(String)
client_context["client"]["visitorData"] = CONFIG.visitor_data.as(String) client_context["client"]["visitorData"] = CONFIG.visitor_data.as(String)
end end
@ -455,8 +459,13 @@ module YoutubeAPI
video_id : String, video_id : String,
*, # Force the following parameters to be passed by name *, # Force the following parameters to be passed by name
params : String, params : String,
client_config : ClientConfig | Nil = nil client_config : ClientConfig | Nil = nil,
po_token : String | Nil,
visitor_data : String | Nil,
) )
if visitor_data
@@visitor_data = visitor_data
end
# Playback context, separate because it can be different between clients # Playback context, separate because it can be different between clients
playback_ctx = { playback_ctx = {
"html5Preference" => "HTML5_PREF_WANTS", "html5Preference" => "HTML5_PREF_WANTS",
@ -482,7 +491,7 @@ module YoutubeAPI
"contentPlaybackContext" => playback_ctx, "contentPlaybackContext" => playback_ctx,
}, },
"serviceIntegrityDimensions" => { "serviceIntegrityDimensions" => {
"poToken" => CONFIG.po_token, "poToken" => po_token || CONFIG.po_token,
}, },
} }
@ -596,7 +605,7 @@ module YoutubeAPI
def _post_json( def _post_json(
endpoint : String, endpoint : String,
data : Hash, data : Hash,
client_config : ClientConfig | Nil client_config : ClientConfig | Nil,
) : Hash(String, JSON::Any) ) : Hash(String, JSON::Any)
# Use the default client config if nil is passed # Use the default client config if nil is passed
client_config ||= DEFAULT_CLIENT_CONFIG client_config ||= DEFAULT_CLIENT_CONFIG
@ -616,7 +625,9 @@ module YoutubeAPI
headers["User-Agent"] = user_agent headers["User-Agent"] = user_agent
end end
if CONFIG.visitor_data.is_a?(String) if !@@visitor_data.empty?
headers["X-Goog-Visitor-Id"] = @@visitor_data
elsif CONFIG.visitor_data.is_a?(String)
headers["X-Goog-Visitor-Id"] = CONFIG.visitor_data.as(String) headers["X-Goog-Visitor-Id"] = CONFIG.visitor_data.as(String)
end end