Compare commits

...

210 commits

Author SHA1 Message Date
25c3153f4c
fixup! fix(hls): add missing scheme to m3u8 list
All checks were successful
Invidious CI / build (push) Successful in 4m38s
2025-01-01 02:31:20 -03:00
4a8fffac26
fix(hls): revert changes made on fb3ecdad9a
All checks were successful
Invidious CI / build (push) Successful in 4m49s
2025-01-01 02:17:33 -03:00
ebd89acf79
fix(hls): add missing scheme to m3u8 list 2025-01-01 02:16:40 -03:00
3bb7924181
replace the old useless HOST_URL by env.request.headers["Host"]
All checks were successful
Invidious CI / build (push) Successful in 4m51s
2025-01-01 01:00:41 -03:00
30c0b5059d
refactor CSP and fix non proxied videoplayback due to CSP
All checks were successful
Invidious CI / build (push) Successful in 4m41s
Probably fixes #65
2024-12-30 19:57:22 -03:00
391659780d
companion: move invidious-companion url CSP 2024-12-30 18:41:22 -03:00
f248024b65
Merge squash 'unixfox:invidious-companion':
commit a5acddefa92c454fced4a9176df10dc85efdb516
Author: Emilien <4016501+unixfox@users.noreply.github.com>
Date:   Mon Dec 30 22:25:13 2024 +0100

    missing ,

commit 84b87bedadbd4d35190b1f4d6b3e4fc1abf2440a
Author: Emilien <4016501+unixfox@users.noreply.github.com>
Date:   Mon Dec 30 22:19:45 2024 +0100

    fixing format

commit bfaf72b3038c3c8cad6d5e68f9f2ad3a49c2a9fc
Author: Emilien <4016501+unixfox@users.noreply.github.com>
Date:   Mon Dec 30 21:52:34 2024 +0100

    skip proxy for invidious companion

commit f550359ae941d84cdaee0a966ed332354ef18f42
Author: Emilien <4016501+unixfox@users.noreply.github.com>
Date:   Mon Dec 30 21:52:07 2024 +0100

    !empty? to present?

commit e9c354d5a34df636306b1819dd17fff9e01b1a1e
Author: Émilien (perso) <4016501+unixfox@users.noreply.github.com>
Date:   Tue Dec 24 17:43:54 2024 +0000

    Better doc for invidious_companion_key

commit 0dba7675a2c1d51988b3f2911a9fb3a1f91bae52
Author: Émilien (perso) <4016501+unixfox@users.noreply.github.com>
Date:   Tue Dec 24 16:18:58 2024 +0000

    Better document private_url and public_url

commit 1de20546182421e1280ec2b68c6d347abead7c54
Author: Emilien <4016501+unixfox@users.noreply.github.com>
Date:   Fri Dec 13 20:08:57 2024 +0100

    add ability for invidious companion to check request from invidious

commit ab72bbad7afb7d143883a7d0610145f68c06bac8
Author: Emilien <4016501+unixfox@users.noreply.github.com>
Date:   Sun Dec 8 22:24:57 2024 +0100

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

commit a571eeaa381523f5efb29dea0f5fe097f4f1252c
Author: Emilien <4016501+unixfox@users.noreply.github.com>
Date:   Sun Dec 8 22:22:08 2024 +0100

    format watch.cr

commit f710dd37bf4327748b43067d75025cc915b5639c
Author: Emilien <4016501+unixfox@users.noreply.github.com>
Date:   Sun Dec 8 22:21:10 2024 +0100

    apply all the suggestions + rework invidious_companion parameter

commit 7a070fa710b7807cdda061d413ca9369a0962353
Author: Emilien <4016501+unixfox@users.noreply.github.com>
Date:   Mon Nov 18 12:30:37 2024 +0100

    invidious companion always used so always add CSP and redirect latest_version

commit 1f51edd0b915ca64df7f195aa271f74c7ef093cb
Author: Emilien <4016501+unixfox@users.noreply.github.com>
Date:   Mon Nov 18 12:22:23 2024 +0100

    fix linting

commit 734e72503f88f9741279ab385e86f5d2b340c71b
Author: Emilien <4016501+unixfox@users.noreply.github.com>
Date:   Sun Nov 17 19:18:29 2024 +0100

    fix download function when invidious companion used

commit bb2e3b2a3e5f53610b9dd602f8507303ec641450
Author: Emilien <4016501+unixfox@users.noreply.github.com>
Date:   Sun Nov 17 12:26:35 2024 +0100

    crystal handle decompression already by itself

commit b51770dbdbdcca04d04849d37e5f11ce20948c73
Author: Emilien <4016501+unixfox@users.noreply.github.com>
Date:   Sat Nov 16 23:00:48 2024 +0100

    fix linting + use .empty?

commit 9f846127aea9b4f392acb062d662fff2cc58d1d0
Author: Emilien <4016501+unixfox@users.noreply.github.com>
Date:   Sat Nov 16 22:38:00 2024 +0100

    fixing "end" misplacement

commit 1aa154b9787eddcdee960d06aed4c1c91f17c1c3
Author: Emilien <4016501+unixfox@users.noreply.github.com>
Date:   Sat Nov 16 22:33:28 2024 +0100

    separate invidious_companion logic + better config.yaml config

commit ff3305d52175c517b035d79b3c0c6a84809cbd0f
Author: Emilien <4016501+unixfox@users.noreply.github.com>
Date:   Fri Nov 8 21:05:17 2024 +0100

    move config checks for invidious companion

commit 409df4cff3cc69c5565a12feb307441eed36f937
Author: Émilien (perso) <4016501+unixfox@users.noreply.github.com>
Date:   Tue Nov 5 15:50:59 2024 +0100

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

commit 27b24f51abcccd1c68f4dc1c29c0c62ca26e604c
Author: Émilien (perso) <4016501+unixfox@users.noreply.github.com>
Date:   Tue Nov 5 15:31:45 2024 +0100

    Remove debug puts functions

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

commit 1c9f5b0a2b38ad94fb8972764ffae98df1e41dc9
Author: Émilien (perso) <4016501+unixfox@users.noreply.github.com>
Date:   Tue Nov 5 15:31:21 2024 +0100

    Use sample instead of Random.rand

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

commit 2cc204a0457665f8e334970d7e54b1843a667ab6
Author: Emilien <4016501+unixfox@users.noreply.github.com>
Date:   Fri Nov 1 21:30:58 2024 +0100

    throw error if inv_sig_helper and invidious_companion used same time

commit c612423a4d64f0adbef135074fc55dcc1c362f84
Author: Emilien Devos <4016501+unixfox@users.noreply.github.com>
Date:   Mon Oct 21 01:20:16 2024 +0200

    fixing condition for Content-Security-Policy

commit 195446337159d2cb92b48510af7311fe0cc0f5bb
Author: Emilien Devos <4016501+unixfox@users.noreply.github.com>
Date:   Sun Oct 20 23:53:08 2024 +0200

    fix Shadowing outer local variable `response`

commit 73c84baf9fa6eaf9c5d4981bc199f81306ebe5a2
Author: Emilien Devos <4016501+unixfox@users.noreply.github.com>
Date:   Sun Oct 20 23:51:00 2024 +0200

    redirect latest_version and dash manifest to invidious companion

commit 3dff7a76cf9f64ec70aac0a057a3b0bfa1edfc82
Author: Emilien Devos <4016501+unixfox@users.noreply.github.com>
Date:   Sun Oct 20 02:10:55 2024 +0200

    add support for invidious companion
2024-12-30 18:31:52 -03:00
df94f1c0b8
support pubsub notifications without making a request to innertube
All checks were successful
Invidious CI / build (push) Successful in 5m12s
Closes #56
2024-12-18 21:57:42 -03:00
83256b2af1
support for numbered backends
All checks were successful
Invidious CI / build (push) Successful in 4m55s
2024-12-14 19:12:00 -03:00
31219ce196
external proxies: Add more information about the job
All checks were successful
Invidious CI / build (push) Successful in 4m54s
2024-12-14 17:18:23 -03:00
a01c8c63d3
tokens: rename Tokens to SessionTokens 2024-12-14 17:17:27 -03:00
58c4d8c951
tokens: use http instead of redis to get the tokens
It should be compatible with github.com/iv-org/youtube-trusted-session-generator
2024-12-14 17:15:17 -03:00
91bcec72c8
use docker registry mirror to prevent rate limits
All checks were successful
Invidious CI / build (push) Successful in 5m16s
2024-12-14 17:04:56 -03:00
a63300e284
remove unused config properties
Some checks failed
Invidious CI / build (push) Failing after 19s
2024-12-14 15:06:34 -03:00
79859100a8
dockerfile: use x86-64-v2 instead to support more CPUs
All checks were successful
Invidious CI / build (push) Successful in 5m30s
I used x86-64-v3 because my CPU supports it, but I doubt this is going
to make Invidious any faster of most of the operations.
2024-11-26 19:31:48 -03:00
47ef5dfe4c
Overwirte CONFIG.po_token and CONFIG.visitor_data by the tokens stored
Some checks failed
Invidious CI / build (push) Has been cancelled
on redis

This approach is better to prevent conflicts with the upstream
repository.
2024-11-16 12:27:10 -03:00
13e00e674b
Remove server side generated tokens (448007e5ba) 2024-11-16 12:10:51 -03:00
3615bbd893
Remove user supplied po_token and visitor_data 2024-11-16 12:07:05 -03:00
9b9efc6841
Merge remote-tracking branch 'upstream/master'
All checks were successful
Invidious CI / build (push) Successful in 5m23s
2024-11-13 21:14:26 -03:00
Samantaz Fox
9892604758
Prepare for next release 2024-11-10 21:40:32 +01:00
Samantaz Fox
5d2dd40bc3
Release v2.20241110.0 2024-11-10 21:35:03 +01:00
Samantaz Fox
699d53ad41
Update shard.yml metadata (#5066)
Changes are mostly based off of the Crystal compiler's own shard.yml

Remember to bump the version attribute when creating a release!!!
2024-11-10 21:03:13 +01:00
Samantaz Fox
2150264d84
Update CHANGELOG.md 2024-11-10 18:00:26 +01:00
Samantaz Fox
d42561d74a
API: Add "sort_by" parameter to channels/shorts endpoint (#5071)
Small follow up to PR 5059

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

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

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

in favor of:

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

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

This PR reverts some of the changes made in 4928

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

Related to 4929
2024-11-10 17:36:49 +01:00
036ab6ef65
Docker: Install tzdata in Dockerfile
All checks were successful
Invidious CI / build (push) Successful in 5m6s
2024-11-10 00:02:33 -03:00
c27a703544
Merge remote-tracking branch 'upstream/master'
All checks were successful
Invidious CI / build (push) Successful in 5m23s
2024-11-09 23:42:52 -03:00
5a75ef7f94
Remove old code that is done on the Openresty side 2024-11-09 23:37:58 -03:00
91c9cd45a4
Update CI 2024-11-09 23:37:58 -03:00
b953dc1ce7
Videos: Add support for OpenGraph videos
To support OpenGraph clients like Discord and other platforms able to
pull the video from the OpenGraph metadata.
2024-11-09 23:37:58 -03:00
Brahim Hadriche
d2123b4682 Sort channel shorts API 2024-11-09 17:49:06 -05:00
Émilien (perso)
0f8f32bca8 remove explicit usage of WEB 2024-11-09 22:21:09 +01:00
Emilien
f3e93ca83d revert back to www.youtube.com when client_config.screen embed 2024-11-09 22:21:09 +01:00
Emilien
82b1506ccc remove usage of WebEmbeddedPlayer 2024-11-09 22:21:09 +01:00
Emilien
b9ad9bd723 use WEB when po_token + android test suite when no po_token 2024-11-09 22:21:09 +01:00
syeopite
8bf7e02978
Change authors section to reflect current state 2024-11-09 13:04:10 -08:00
Samantaz Fox
1a49e798c8
Docker: Install tzdata in Dockerfile 2024-11-09 21:52:06 +01:00
syeopite
9d54cf903e
Update shard.yml metadata 2024-11-08 15:54:37 -08:00
Samantaz Fox
b173d4acf2
Update CHANGELOG.md 2024-11-08 23:45:15 +01:00
Samantaz Fox
43d5efd9da
Stale bot updates (#5060)
* Prevents PRs from being considered staled
* Double the stale timer for issues
* Prevent discussion issues from being staled

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

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

* Apply suggestion from code review

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

---------

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

* add documentation link

* Update src/invidious/yt_backend/youtube_api.cr

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

---------

Co-authored-by: syeopite <70992037+syeopite@users.noreply.github.com>
2024-11-07 14:04:43 +01:00
70dc1a9f11
Tokens: Better logging
All checks were successful
Invidious CI / build (push) Successful in 5m46s
2024-10-31 21:38:59 -03:00
fc910b43ba
External Proxies: Adapt it to use a NamedTuple 2024-10-31 21:38:40 -03:00
67998d1f36
Revert "External Proxies: Rotate between proxies with balance enabled"
This reverts commit 26bee068eb.
It's broken and it doesn't work when a proxy comes back up.
2024-10-31 21:26:03 -03:00
e2276ace1b
Merge remote-tracking branch 'upstream/master' into master
All checks were successful
Invidious CI / build (push) Successful in 5m35s
2024-10-31 20:25:33 -03:00
Samantaz Fox
75c5881c55
Locales: Add Bulgarian, Welsh and Lombard to the list 2024-10-31 13:34:51 +01:00
Samantaz Fox
6da18ddc41
Routing: Also remove outdated comment about notification routes 2024-10-31 11:52:09 +01:00
Samantaz Fox
cdf93b29e6
Routing: Remove deprecated /api/v1/channels/.../:ucid routes 2024-10-31 11:51:33 +01:00
Brahim Hadriche
c243d08afb refactor 2024-10-30 13:38:13 -04:00
c61b2963ac
Videos: Fix audio tracks language.
Video will only return the default language. The rest of the audio
tracks are deleted since they will not be used.
2024-10-30 13:14:54 -03:00
Samantaz Fox
2e3a7ad044
Update CHANGELOG.md 2024-10-30 17:13:00 +01:00
Samantaz Fox
c427c184e2
Captions: Add "Filipino (auto-generated)" to the list of languages (#4995)
I encountered a wild
[warn] i18n: Missing translation key "Filipino (auto-generated)"
while browsing videos on the test instance.

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

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

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

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

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

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

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

Closes issue 4009
2024-10-30 13:55:28 +01:00
Samantaz Fox
f326bcf8db
Add support for using Invidious through a HTTP Proxy (#4270)
Partially addresses issue 301
2024-10-30 13:46:49 +01:00
26bee068eb
External Proxies: Rotate between proxies with balance enabled
Closes #17
2024-10-30 01:59:08 -03:00
486c5845cd
Config: Also reload env variables 2024-10-30 01:57:06 -03:00
6f10a7c67e
Use POST requests for /videoplayback requests 2024-10-29 19:02:05 -03:00
Samantaz Fox
711d52d47f
Shards: Update database dependencies 2024-10-29 17:26:24 +01:00
67d7b78ac9
Config: Reload configuration on modification
It detects changes on the config.yml automtically if invidious is
running on linux. If not, the configuration can be reloaded using
`kill -s HUP $(pidof invidious)` or any other tool that sends a SIGHUP
signal to the invidious process.

Closes #16
2024-10-28 13:37:06 -03:00
Brahim Hadriche
ee72809282 [Alternative] Fix for channel live videos 2024-10-26 12:40:31 -04:00
3afac4d842
Tokens: Option to disable user tokens. 2024-10-25 10:36:20 -03:00
syeopite
d8b893e9ad
Bump CI matrix (#5015) 2024-10-18 21:33:38 +02:00
448007e5ba
Tokens: Server side generated tokens.
#18
2024-10-17 23:44:30 -03:00
3cc0dbca01
PubSub: Use external domain for pubsub feeds 2024-10-17 17:02:12 -03:00
c3e8721051
External Proxies: Proxyfi HLS Playlists 2024-10-14 17:57:52 -03:00
cf5028d09a
Videos: Completly disable annotations due to archive.org being down
Closes #15
2024-10-13 23:47:57 -03:00
Émilien (perso)
70e4eb7f5d
Merge pull request #5004 from unixfox/update-mocks
update the mocks with the latest updated data
2024-10-14 00:06:29 +02:00
Emilien Devos
0d03818700 libsqlite3-dev is now missing in the CI env 2024-10-14 00:02:41 +02:00
Emilien Devos
e6f52eaf00 update submodule 2024-10-13 23:57:29 +02:00
Emilien Devos
90544e07b6 update the mocks with the latest updated data 2024-10-13 21:18:21 +02:00
eb2670fe49
Tokens: Refresh po_token and visitor_data every 5 seconds
Closes #11
2024-10-13 15:57:51 -03:00
976e1ccf5a
External Proxies: Proxyfi HD720 2024-10-13 15:19:49 -03:00
fee2acc666
Videos: Increase video cache to 4 hours 2024-10-12 02:59:36 -03:00
b5ab49e8e8
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
65f3bbcb10
External Proxies: Use list of external videoplayback proxies 2024-10-11 13:50:42 -03:00
Samantaz Fox
952b3625a0
Add "Filipino (auto-generated)" to the list of caption languages 2024-10-10 20:31:22 +02:00
Samantaz Fox
f51a3b8d2b
Makefile: Add MT option to enable the 'preview_mt' flag 2024-10-09 18:37:08 +02:00
Samantaz Fox
fb3ecdad9a
Videos: Fix missing host parameter on playback URLs when local=true 2024-10-09 16:15:50 +02:00
5357c83e00
CI: Experimental branches for testing builds 2024-10-10 18:19:29 -03:00
8dc0a67be3
Feat: User supplied po_token and visitor_data 2024-10-11 16:50:21 -03:00
d61043edea
Small try. 2024-10-10 15:07:33 -03:00
3111158a7c
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)"
All checks were successful
Invidious CI / build (push) Successful in 8m10s
This reverts commit d9df90b5e3.
2024-10-08 19:53:35 -03:00
2f5a555ea7
Merge remote-tracking branch 'upstream/master'
All checks were successful
Invidious CI / build (push) Successful in 9m50s
2024-10-08 19:22:53 -03:00
84e4746265
SigHelper: Reconnect to signature helper
Signed-off-by: Fijxu <fijxu@nadeko.net>
2024-10-08 19:09:14 -03:00
472dd8663d
VideoJS: Increase buffer
Some checks are pending
Invidious CI / build (push) Waiting to run
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
d2edd4b63f
fixup! Logger: Add color support for different log levels 2024-10-08 18:36:50 -03:00
Emilien Devos
b3e6aaddab
decrease buffer seconds for saving bandwidth 2024-10-08 16:54:19 -03:00
33ffafb9e3
Feat: backend supports with cookies 2024-10-08 16:54:18 -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
17b525f2a6
Logger: colorize_logs false by default 2024-09-27 18:08:21 -03:00
absidue
b2a83991d1 Fix parsing live_now and premiere_timestamp 2024-09-20 18:46:00 +02:00
d77afdcf00
Logger: Make colorize_logs true by default 2024-09-20 00:32:27 -03:00
f8ec312328
Logger: Add color support for different log levels 2024-09-19 21:35:52 -03:00
Émilien (perso)
a021b93063
Update latest version WEB_CREATOR + fix comment web embed (#4930)
* Update to latest version WEB_CREATOR

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

* remove unrelated comment

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

---------

Co-authored-by: syeopite <70992037+syeopite@users.noreply.github.com>
2024-09-20 00:19:13 +02:00
Emilien Devos
cec3cfba77 Revert "use web screen embed for fixing potoken functionality (#4923)"
This reverts commit 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
syeopite
75b68618ab
Remove useless proc usage in images.cr 2024-08-24 19:47:42 -07:00
syeopite
003c6f81dc
Preserve connection close header of get_storyboard 2024-08-24 19:47:42 -07:00
syeopite
4bc77b81bf
Move YTIMG_POOLS to connection_pool.cr 2024-08-24 19:47:40 -07:00
syeopite
06e1a508e8
Fix headers not being added in image requests
Regression from #2364
2024-08-24 19:45:52 -07:00
syeopite
52bc9aa328
Refactor duplicate logic in image routes 2024-08-24 19:45:52 -07:00
syeopite
480e073fa9
Use HTTP pools for image requests to YouTube 2024-08-24 19:45:52 -07:00
syeopite
6e39b9b303
make_client: add YouTube headers on *.youtube.com 2024-08-24 19:41:39 -07:00
syeopite
46c58bd84c
Pool: Use force_resolve in fallback new client 2024-08-24 19:41:23 -07:00
syeopite
7521902e88
Ensure IP family is always used when force_resolve 2024-08-24 19:41:22 -07:00
syeopite
bd48af825c
Search API: Fix named arg syntax to make_client 2024-08-24 19:34:09 -07:00
syeopite
ee89db49ba
Typo
Co-authored-by: Samantaz Fox <coding@samantaz.fr>
2024-08-24 19:34:09 -07:00
syeopite
3af6681869
Fix typo in argument to make_client
Co-authored-by: ChunkyProgrammer <78101139+ChunkyProgrammer@users.noreply.github.com>
2024-08-24 19:34:09 -07:00
syeopite
1124dd645d
Use make_client instead of calling HTTP::Client
Using `make_client` to create `HTTP::Client`, allows for a simple way to
easily add logic to all `HTTP::Client` initialized within Invidious.
2024-08-24 19:34:09 -07:00
Samantaz Fox
b2133c6b2c
Videos: Convert URL before putting result into cache 2024-08-24 18:01:56 +02:00
syeopite
c24ed85110
Fix named arg syntax when passing force_resolve 2024-06-16 14:49:48 -07:00
giacomocerquone
288e1dccda Fix player menus hiding onHover 2024-06-13 01:10:35 +02:00
syeopite
9980c0e00f
Update uptime logic to handle updown.io response 2024-05-22 13:28:15 -07:00
syeopite
aa96cf3453
Fix invalid logic for instance uptime comparison 2024-05-22 13:22:00 -07:00
syeopite
41c978d350
Use HTTP::Client directly in instance list job
The HTTP::Client created via `make_client` is affected by the
force_resolve configuration option. However, api.invidious.io
does not support ipv6 and as such any request with ipv6 to
api.invidious.io will instead raise.

Directly calling the HTTP::Client will ignore the force_resolve option
allowing requests to go through ipv4 when needed.
2024-05-22 13:22:00 -07:00
syeopite
cff25a7b25
Refactor instance fetching logic into separate job 2024-05-22 13:22:00 -07:00
syeopite
6b7e730100
Validate override for crystal 1.12.1 2024-05-22 13:10:46 -07:00
syeopite
ccb2a6c58e
Bump http_proxy to v0.10.3 2024-04-28 21:34:05 -07:00
syeopite
3b471ae964
Automatically initialize proxy via stdlib override 2024-04-28 19:43:22 -07:00
syeopite
eb8fcc9e88
Add support for using HTTP proxies 2024-04-28 19:43:17 -07:00
tracedgod
5b11ca22d0 Use string interpolation instead of concatenation 2024-04-28 00:04:30 -04:00
tracedgod
6db4a46c5f update the url_search_issues variable to search for the current error on GitHub instead of showing all issues 2024-04-26 16:01:02 -04:00
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
96 changed files with 1948 additions and 678 deletions

View file

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

View file

@ -8,6 +8,8 @@ on:
push:
branches:
- "master"
- "experimental"
- "experimental2"
jobs:
build:
@ -35,13 +37,15 @@ jobs:
type=sha,format=short,prefix={{date 'YYYY.MM.DD'}}-,enable=${{ github.ref == format('refs/heads/{0}', 'master') }}
type=raw,value=latest,enable=${{ github.ref == format('refs/heads/{0}', 'master') }}
- uses: https://code.forgejo.org/docker/build-push-action@v5
- uses: https://code.forgejo.org/docker/build-push-action@v6
name: Build images
with:
context: .
file: docker/Dockerfile
tags: ${{ steps.meta.outputs.tags }}
platforms: linux/amd64
# cache-from: type=gha
# cache-to: type=gha,mode=max
push: true
build-args: |
"release=1"

2
.github/CODEOWNERS vendored
View file

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

View file

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

View file

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

View file

@ -13,14 +13,11 @@ jobs:
- uses: actions/stale@v8
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
days-before-stale: 365
days-before-pr-stale: 90
days-before-close: 30
exempt-pr-labels: blocked,exempt-stale
days-before-stale: 730
days-before-pr-stale: -1
days-before-close: 60
stale-issue-message: 'This issue has been automatically marked as stale and will be closed in 30 days because it has not had recent activity and is much likely outdated. If you think this issue is still relevant and applicable, you just have to post a comment and it will be unmarked.'
stale-pr-message: 'This pull request has been automatically marked as stale and will be closed in 30 days because it has not had recent activity and is much likely abandoned or outdated. If you think this pull request is still relevant and applicable, you just have to post a comment and it will be unmarked.'
stale-issue-label: "stale"
stale-pr-label: "stale"
ascending: true
# Never mark feature requests/enhancements as stale
exempt-issue-labels: "feature-request,enhancement,exempt-stale"
# Exempt the following types of issues from being staled
exempt-issue-labels: "feature-request,enhancement,discussion,exempt-stale"

View file

@ -1,6 +1,333 @@
# CHANGELOG
## 2024-04-26
## vX.Y.0 (future)
## v2.20241110.0
### Wrap-up
This release is most importantly here to fix to the annoying "Youtube API returned error 400"
error that prevented all channel pages from loading.
If you're updating from the previous release, it provides no improvements on the ability to play
videos. If updating from a commit in-between release, it removes the "Please sign in" error caused
by a previous attempt at restoring video playback on large instances.
In the preferences, a new option allows for control of video preload. When enabled, this option
tells the browser to load the video as soon as the page is loaded (this used to be the default).
When disabled, the video starts loading only when the "play" button is pressed.
New interface languages available: Bulgarian, Welsh and Lombard
New dependency required: `tzdata`.
An HTTP proxy can be configured directly in Invidious, if needed. \
**NOTE:** In that case, it is recommended to comment out `force_resolve`.
### New features & important changes
#### For users
* Channels: Fix "Youtube API returned error 400" error preventing channel pages from loading
* Channels: Shorts can now be sorted by "newest", "oldest" and "popular"
* Preferences: Addition of the new "preload" option
* New interface languages available: Bulgarian, Welsh and Lombard
* Added "Filipino (auto-generated)" to the list of caption languages available
* Lots of new translations from Weblate
#### For instance owners
* Allow the configuration of an HTTP proxy to talk to Youtube
* Invidious tries to reconnect to `inv_sig_helper` if the socket is closed
* The instance list is downloaded in the background to improve redirection speed
* New `colorize_logs` option makes each log level a different color
#### For developpers
* `/api/v1/channels/{id}/shorts` now supports the `sort-by` parameter with the following values:
`newest`, `oldest` and `popular`
* Older `/api/v1/channels/xyz/{id}` (tab name before UCID) were removed
* API/Search: New video metadata available: `isNew`, `is4k`, `is8k`, `isVr180`, `isVr360`,
`is3d` and `hasCaptions`
### Bugs fixed
#### User-side
* Channels: The second page of shorts now loads as expected
* Channels: Fixed intermittent empty "playlists" tab
* Search: Fixed `youtu.be` URLs not being properly redirected to the watch page
* Fixed `DB::MappingException` error on the subscriptions feed (due to missing `tzdata` in docker)
* Switching to another instance is much faster
* Fixed an "invalid byte sequence" error when subscribing to a playlist
* Videos: Playback URLs were sometimes broken when cached and `inv_sig_helper` was used
#### For instance owners
* Fix `force_resolve` being ignored in some cases
#### API
* API/Videos: Fixed `live_now` and `premiere_timestamp` sometimes not having the right values
### Full list of pull requests merged since the last release (newest first)
* API: Add "sort_by" parameter to channels/shorts endpoint ([#5071], thanks @iBicha)
* Docker: Install tzdata in Dockerfile ([#5070], by @SamantazFox)
* Videos: Stop using TVHTML5_SIMPLY_EMBEDDED_PLAYER ([#5063], thanks @unixfox)
* Routing: Deprecate old channel API routes ([#5045], by @SamantazFox)
* Videos: use WEB client instead of WEB CREATOR ([#4984], thanks @unixfox)
* Parsers: Fix parsing live_now and premiere_timestamp ([#4934], thanks @absidue)
* Stale bot updates ([#5060], thanks @syeopite)
* Channels: Fix "Youtube API returned error 400" ([#5059], by @SamantazFox)
* Channels: Fix for live videos ([#5027], thanks @iBicha)
* Locales: Add Bulgarian, Welsh and Lombard to the list ([#5046], by @SamantazFox)
* Shards: Update database dependencies ([#5034], by @SamantazFox)
* Logger: Add color support for different log levels ([#4931], thanks @Fijxu)
* Fix named arg syntax when passing force_resolve ([#4754], thanks @syeopite)
* Use make_client instead of calling HTTP::Client ([#4709], thanks @syeopite)
* Add "Filipino (auto-generated)" to the list of caption languages ([#4995], by @SamantazFox)
* Makefile: Add MT option to enable the 'preview_mt' flag ([#4993], by @SamantazFox)
* SigHelper: Reconnect to signature helper ([#4991], thanks @Fijxu)
* Fix player menus hiding onHover ready ([#4750], thanks @giacomocerquone)
* Use connection pools when requesting images from YouTube ([#4326], thanks @syeopite)
* Add support for using Invidious through a HTTP Proxy ([#4270], thanks @syeopite)
* Search: Fix 'youtu.be' URLs in sanitizer ([#4894], by @SamantazFox)
* Ameba: Disable Style/RedundantNext rule ([#4888], thanks @syeopite)
* Playlists: Fix 'invalid byte sequence' error when subscribing ([#4887], thanks @DmitrySandalov)
* Parse more metadata badges for SearchVideos ([#4863], thanks @ChunkyProgrammer)
* Translations update from Hosted Weblate ([#4862], thanks to our many translators)
* Videos: Convert URL before putting result into cache ([#4850], by @SamantazFox)
* HTML: Add error message to "search issues on GitHub" link ([#4652], thanks @tracedgod)
* Preferences: Add option to control preloading of video data ([#4122], thanks @Nerdmind)
* Performance: Improve speed of automatic instance redirection ([#4193], thanks @syeopite)
* Remove myself from CODEOWNERS on the config file ([#4942], by @TheFrenchGhosty)
* Update latest version WEB_CREATOR + fix comment web embed ([#4930], thanks @unixfox)
* use WEB_CREATOR when po_token with WEB_EMBED as a fallback ([#4928], thanks @unixfox)
* Revert "use web screen embed for fixing potoken functionality"
* use web screen embed for fixing potoken functionality ([#4923], thanks @unixfox)
[#4122]: https://github.com/iv-org/invidious/pull/4122
[#4193]: https://github.com/iv-org/invidious/pull/4193
[#4270]: https://github.com/iv-org/invidious/pull/4270
[#4326]: https://github.com/iv-org/invidious/pull/4326
[#4652]: https://github.com/iv-org/invidious/pull/4652
[#4709]: https://github.com/iv-org/invidious/pull/4709
[#4750]: https://github.com/iv-org/invidious/pull/4750
[#4754]: https://github.com/iv-org/invidious/pull/4754
[#4850]: https://github.com/iv-org/invidious/pull/4850
[#4862]: https://github.com/iv-org/invidious/pull/4862
[#4863]: https://github.com/iv-org/invidious/pull/4863
[#4887]: https://github.com/iv-org/invidious/pull/4887
[#4888]: https://github.com/iv-org/invidious/pull/4888
[#4894]: https://github.com/iv-org/invidious/pull/4894
[#4923]: https://github.com/iv-org/invidious/pull/4923
[#4928]: https://github.com/iv-org/invidious/pull/4928
[#4930]: https://github.com/iv-org/invidious/pull/4930
[#4931]: https://github.com/iv-org/invidious/pull/4931
[#4934]: https://github.com/iv-org/invidious/pull/4934
[#4942]: https://github.com/iv-org/invidious/pull/4942
[#4984]: https://github.com/iv-org/invidious/pull/4984
[#4991]: https://github.com/iv-org/invidious/pull/4991
[#4993]: https://github.com/iv-org/invidious/pull/4993
[#4995]: https://github.com/iv-org/invidious/pull/4995
[#5027]: https://github.com/iv-org/invidious/pull/5027
[#5034]: https://github.com/iv-org/invidious/pull/5034
[#5045]: https://github.com/iv-org/invidious/pull/5045
[#5046]: https://github.com/iv-org/invidious/pull/5046
[#5059]: https://github.com/iv-org/invidious/pull/5059
[#5060]: https://github.com/iv-org/invidious/pull/5060
[#5063]: https://github.com/iv-org/invidious/pull/5063
[#5070]: https://github.com/iv-org/invidious/pull/5070
[#5071]: https://github.com/iv-org/invidious/pull/5071
## v2.20240825.2 (2024-08-26)
This releases fixes the container tags pushed on quay.io.
Previously, the ARM64 build was released under the `latest` tag, instead of `latest-arm64`.
### Full list of pull requests merged since the last release (newest first)
CI: Fix docker container tags ([#4883], by @SamantazFox)
[#4877]: https://github.com/iv-org/invidious/pull/4877
## v2.20240825.1 (2024-08-25)
Add patch component to be [semver] compliant and make github actions happy.
[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:
* Videos: Use android test suite client (#4650, thanks @SamantazFox)

View file

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

View file

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

View file

@ -3,7 +3,6 @@ var player_data = JSON.parse(document.getElementById('player_data').textContent)
var video_data = JSON.parse(document.getElementById('video_data').textContent);
var options = {
preload: 'auto',
liveui: true,
playbackRates: [0.25, 0.5, 0.75, 1.0, 1.25, 1.5, 1.75, 2.0],
controlBar: {
@ -51,6 +50,9 @@ videojs.Vhs.xhr.beforeRequest = function(options) {
return options;
};
videojs.Vhs.GOAL_BUFFER_LENGTH = 40;
videojs.Vhs.MAX_GOAL_BUFFER_LENGTH = 80;
var player = videojs('player', options);
player.on('error', function () {

View file

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

62
crystal_formatters.py Normal file
View file

@ -0,0 +1,62 @@
import lldb
class CrystalArraySyntheticProvider:
def __init__(self, valobj, internal_dict):
self.valobj = valobj
self.buffer = None
self.size = 0
def update(self):
if self.valobj.type.is_pointer:
self.valobj = self.valobj.Dereference()
self.size = int(self.valobj.child[0].value)
self.type = self.valobj.type
self.buffer = self.valobj.child[3]
def num_children(self):
size = 0 if self.size is None else self.size
return size
def get_child_index(self, name):
try:
return int(name.lstrip('[').rstrip(']'))
except:
return -1
def get_child_at_index(self,index):
if index >= self.size:
return None
try:
elementType = self.buffer.type.GetPointeeType()
offset = elementType.size * index
return self.buffer.CreateChildAtOffset('[' + str(index) + ']', offset, elementType)
except Exception as e:
print('Got exception %s' % (str(e)))
return None
def findType(name, module):
cachedTypes = module.GetTypes()
for idx in range(cachedTypes.GetSize()):
type = cachedTypes.GetTypeAtIndex(idx)
if type.name == name:
return type
return None
def CrystalString_SummaryProvider(value, dict):
error = lldb.SBError()
if value.TypeIsPointerType():
value = value.Dereference()
process = value.GetTarget().GetProcess()
byteSize = int(value.child[0].value)
len = int(value.child[1].value)
len = byteSize or len
strAddr = value.child[2].load_addr
val = process.ReadCStringFromMemory(strAddr, len + 1, error)
return '"%s"' % val
def __lldb_init_module(debugger, dict):
debugger.HandleCommand(r'type synthetic add -l crystal_formatters.CrystalArraySyntheticProvider -x "^Array\(.+\)(\s*\**)?" -w Crystal')
debugger.HandleCommand(r'type summary add -F crystal_formatters.CrystalString_SummaryProvider -x "^(String|\(String \| Nil\))(\s*\**)?$" -w Crystal')
debugger.HandleCommand(r'type category enable Crystal')

View file

@ -1,4 +1,4 @@
FROM crystallang/crystal:1.12.1-alpine AS builder
FROM mirror.gcr.io/crystallang/crystal:1.14.0-alpine AS builder
RUN apk add --no-cache sqlite-static yaml-static
@ -23,7 +23,7 @@ RUN crystal spec --warnings all \
--link-flags "-lxml2 -llzma"
RUN if [[ "${release}" == 1 ]] ; then \
crystal build ./src/invidious.cr \
--release --mcpu=x86-64-v3 \
--release --mcpu=x86-64-v2 \
--static --warnings all \
--link-flags "-lxml2 -llzma"; \
else \
@ -32,8 +32,8 @@ RUN if [[ "${release}" == 1 ]] ; then \
--link-flags "-lxml2 -llzma"; \
fi
FROM alpine:3.18
RUN apk add --no-cache rsvg-convert ttf-opensans tini
FROM mirror.gcr.io/alpine:3.20
RUN apk add --no-cache rsvg-convert ttf-opensans tini tzdata
WORKDIR /invidious
RUN addgroup -g 1000 -S invidious && \
adduser -u 1000 -S invidious -G invidious

View file

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

View file

@ -483,7 +483,7 @@
"comments_view_x_replies_3": "عرض رد {{count}}",
"comments_view_x_replies_4": "عرض الردود {{count}}",
"comments_view_x_replies_5": "عرض رد {{count}}",
"search_message_use_another_instance": " يمكنك أيضًا البحث عن <a href=\"`x`\"> في مثيل آخر </a>.",
"search_message_use_another_instance": "يمكنك أيضًا البحث عن <a href=\"`x`\"> في مثيل آخر </a>.",
"comments_points_count_0": "{{count}} نقطة",
"comments_points_count_1": "نقطة واحدة",
"comments_points_count_2": "نقطتان",

View file

@ -471,7 +471,7 @@
"search_filters_title": "Filtry",
"search_filters_duration_option_medium": "Střední (4 - 20 minut)",
"search_filters_duration_option_long": "Dlouhá (> 20 minut)",
"search_message_use_another_instance": " Můžete také <a href=\"`x`\">hledat na jiné instanci</a>.",
"search_message_use_another_instance": "Můžete také <a href=\"`x`\">hledat na jiné instanci</a>.",
"search_filters_features_label": "Vlastnosti",
"search_filters_features_option_three_sixty": "360°",
"search_filters_features_option_vr180": "VR180",

View file

@ -47,6 +47,7 @@
"Preferences": "Einstellungen",
"preferences_category_player": "Wiedergabeeinstellungen",
"preferences_video_loop_label": "Immer wiederholen: ",
"preferences_preload_label": "Videodaten vorladen: ",
"preferences_autoplay_label": "Automatisch abspielen: ",
"preferences_continue_label": "Immer automatisch nächstes Video abspielen: ",
"preferences_continue_autoplay_label": "Nächstes Video automatisch abspielen: ",
@ -322,7 +323,7 @@
"channel_tab_community_label": "Gemeinschaft",
"search_filters_sort_option_relevance": "Relevanz",
"search_filters_sort_option_rating": "Bewertung",
"search_filters_sort_option_date": "Datum",
"search_filters_sort_option_date": "Hochladedatum",
"search_filters_sort_option_views": "Aufrufe",
"search_filters_type_label": "Inhaltstyp",
"search_filters_duration_label": "Dauer",
@ -454,7 +455,7 @@
"Portuguese (auto-generated)": "Portugiesisch (automatisch generiert)",
"search_filters_title": "Filtern",
"search_message_change_filters_or_query": "Versuchen Sie, Ihre Suchanfrage zu erweitern und/oder die Filter zu ändern.",
"search_message_use_another_instance": " Sie können auch <a href=\"`x`\">auf einer anderen Instanz suchen</a>.",
"search_message_use_another_instance": "Sie können auch <a href=\"`x`\">auf einer anderen Instanz suchen</a>.",
"Popular enabled: ": "„Beliebt“-Seite aktiviert: ",
"search_message_no_results": "Keine Ergebnisse gefunden.",
"search_filters_duration_option_medium": "Mittel (4 - 20 Minuten)",
@ -493,5 +494,8 @@
"Add to playlist": "Einer Wiedergabeliste hinzufügen",
"Search for videos": "Nach Videos suchen",
"toggle_theme": "Thema wechseln",
"Add to playlist: ": "Einer Wiedergabeliste hinzufügen: "
"Add to playlist: ": "Einer Wiedergabeliste hinzufügen: ",
"carousel_go_to": "Zu Folie `x` gehen",
"carousel_slide": "Folie {{current}} von {{total}}",
"carousel_skip": "Karussell überspringen"
}

View file

@ -489,5 +489,10 @@
"search_filters_date_label": "Ημερομηνία αναφόρτωσης",
"Search for videos": "Αναζήτηση βίντεο",
"The Popular feed has been disabled by the administrator.": "Η δημοφιλής ροή έχει απενεργοποιηθεί από τον διαχειριστή.",
"Answer": "Απάντηση"
"Answer": "Απάντηση",
"Add to playlist": "Λίιστα αναπαραγωγής",
"Add to playlist: ": "Λίστα αναπαραγωγής: ",
"carousel_slide": "Εικόνα {{current}}απο {{total}}",
"carousel_go_to": "Πήγαινε στην εικόνα`x`",
"toggle_theme": "Αλλαγή θέματος"
}

View file

@ -71,6 +71,7 @@
"Preferences": "Preferences",
"preferences_category_player": "Player preferences",
"preferences_video_loop_label": "Always loop: ",
"preferences_preload_label": "Preload video data: ",
"preferences_autoplay_label": "Autoplay: ",
"preferences_continue_label": "Play next by default: ",
"preferences_continue_autoplay_label": "Autoplay next video: ",
@ -191,7 +192,7 @@
"Switch Invidious Instance": "Switch Invidious Instance",
"search_message_no_results": "No results found.",
"search_message_change_filters_or_query": "Try widening your search query and/or changing the filters.",
"search_message_use_another_instance": " You can also <a href=\"`x`\">search on another instance</a>.",
"search_message_use_another_instance": "You can also <a href=\"`x`\">search on another instance</a>.",
"Hide annotations": "Hide annotations",
"Show annotations": "Show annotations",
"Genre: ": "Genre: ",
@ -286,6 +287,7 @@
"Esperanto": "Esperanto",
"Estonian": "Estonian",
"Filipino": "Filipino",
"Filipino (auto-generated)": "Filipino (auto-generated)",
"Finnish": "Finnish",
"French": "French",
"French (auto-generated)": "French (auto-generated)",
@ -423,7 +425,7 @@
"search_filters_title": "Filters",
"search_filters_date_label": "Upload date",
"search_filters_date_option_none": "Any date",
"search_filters_date_option_hour": "Last Hour",
"search_filters_date_option_hour": "Last hour",
"search_filters_date_option_today": "Today",
"search_filters_date_option_week": "This week",
"search_filters_date_option_month": "This month",
@ -455,7 +457,7 @@
"search_filters_sort_label": "Sort By",
"search_filters_sort_option_relevance": "Relevance",
"search_filters_sort_option_rating": "Rating",
"search_filters_sort_option_date": "Upload Date",
"search_filters_sort_option_date": "Upload date",
"search_filters_sort_option_views": "View count",
"search_filters_apply_button": "Apply selected filters",
"Current version: ": "Current version: ",

View file

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

View file

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

View file

@ -484,7 +484,7 @@
"search_filters_duration_option_medium": "Moyenne (de 4 à 20 minutes)",
"search_filters_apply_button": "Appliquer les filtres",
"search_message_no_results": "Aucun résultat.",
"search_message_use_another_instance": " Vous pouvez également <a href=\"`x`\">effectuer votre recherche sur une autre instance</a>.",
"search_message_use_another_instance": "Vous pouvez également <a href=\"`x`\">effectuer votre recherche sur une autre instance</a>.",
"search_filters_type_option_all": "Tous les types",
"search_filters_date_label": "Date d'ajout",
"search_filters_features_option_vr180": "VR180",

View file

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

View file

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

View file

@ -396,7 +396,7 @@
"toggle_theme": "Víxla þema",
"carousel_skip": "Sleppa hringekjunni",
"preferences_quality_option_medium": "Miðlungs",
"search_message_use_another_instance": " Þú getur líka <a href=\"`x`\">leitað á öðrum netþjóni</a>.",
"search_message_use_another_instance": "Þú getur líka <a href=\"`x`\">leitað á öðrum netþjóni</a>.",
"footer_source_code": "Grunnkóði",
"English (United Kingdom)": "Enska (Bretland)",
"English (United States)": "Enska (Bandarísk)",

View file

@ -449,7 +449,7 @@
"Portuguese (Brazil)": "Portoghese (Brasile)",
"preferences_watch_history_label": "Attiva cronologia di riproduzione: ",
"French (auto-generated)": "Francese (generati automaticamente)",
"search_message_use_another_instance": " Puoi anche <a href=\"`x`\">cercare in un'altra istanza</a>.",
"search_message_use_another_instance": "Puoi anche <a href=\"`x`\">cercare in un'altra istanza</a>.",
"search_message_no_results": "Nessun risultato trovato.",
"search_message_change_filters_or_query": "Prova ad ampliare la ricerca e/o modificare i filtri.",
"English (United States)": "Inglese (Stati Uniti)",

View file

@ -363,7 +363,7 @@
"search_filters_features_option_location": "場所",
"search_filters_features_option_hdr": "HDR",
"Current version: ": "現在のバージョン: ",
"next_steps_error_message": "以下をお試しください: ",
"next_steps_error_message": "以下をお試しください: ",
"next_steps_error_message_refresh": "再読み込み",
"next_steps_error_message_go_to_youtube": "YouTubeを開く",
"search_filters_duration_option_short": "4分未満",
@ -396,7 +396,7 @@
"download_subtitles": "字幕 - `x` (.vtt)",
"search_filters_features_option_purchased": "購入済み",
"preferences_quality_option_dash": "DASH (適応的画質)",
"preferences_quality_dash_option_worst": "最",
"preferences_quality_dash_option_worst": "最",
"preferences_quality_dash_option_best": "最高",
"videoinfo_started_streaming_x_ago": "`x`前に配信を開始",
"videoinfo_watch_on_youTube": "YouTubeで視聴",
@ -434,7 +434,7 @@
"crash_page_switch_instance": "<a href=\"`x`\">別のインスタンスを使用</a>を試す",
"crash_page_read_the_faq": "<a href=\"`x`\">よくある質問 (FAQ)</a> を読む",
"Popular enabled: ": "人気動画を有効化 ",
"search_message_use_another_instance": " <a href=\"`x`\">別のインスタンス上での検索</a>も可能です。",
"search_message_use_another_instance": "<a href=\"`x`\">別のインスタンス上での検索</a>も可能です。",
"search_filters_apply_button": "選択したフィルターを適用",
"user_saved_playlists": "`x`個の保存済みの再生リスト",
"crash_page_you_found_a_bug": "Invidious のバグのようです!",

View file

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

View file

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

View file

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

View file

@ -478,7 +478,7 @@
"search_filters_date_label": "Data przesłania",
"search_filters_features_option_vr180": "VR180",
"search_filters_date_option_none": "Dowolna data",
"search_message_use_another_instance": " Możesz także <a href=\"`x`\">wyszukać w innej instancji</a>.",
"search_message_use_another_instance": "Możesz także <a href=\"`x`\">wyszukać w innej instancji</a>.",
"search_filters_type_option_all": "Dowolny typ",
"search_filters_duration_option_none": "Dowolna długość",
"search_filters_duration_option_medium": "Średnia (4-20 minut)",

View file

@ -474,7 +474,7 @@
"Spanish (auto-generated)": "Espanhol (gerado automaticamente)",
"Spanish (Mexico)": "Espanhol (México)",
"search_filters_duration_option_none": "Qualquer duração",
"search_message_use_another_instance": " Você também pode <a href=\"`x`\">pesquisar em outra instância</a>.",
"search_message_use_another_instance": "Você também pode <a href=\"`x`\">pesquisar em outra instância</a>.",
"Spanish (Spain)": "Espanhol (Espanha)",
"Turkish (auto-generated)": "Turco (gerado automaticamente)",
"search_filters_duration_option_medium": "Médio (4 - 20 minutos)",

View file

@ -448,7 +448,7 @@
"Chinese (Taiwan)": "Chinês (Taiwan)",
"search_message_no_results": "Nenhum resultado encontrado.",
"search_message_change_filters_or_query": "Tente alargar os termos genéricos da pesquisa e/ou alterar os filtros.",
"search_message_use_another_instance": " Também pode <a href=\"`x`\">pesquisar noutra instância</a>.",
"search_message_use_another_instance": "Também pode <a href=\"`x`\">pesquisar noutra instância</a>.",
"English (United Kingdom)": "Inglês (Reino Unido)",
"English (United States)": "Inglês (Estados Unidos)",
"Cantonese (Hong Kong)": "Cantonês (Hong Kong)",
@ -508,7 +508,7 @@
"toggle_theme": "Trocar tema",
"Add to playlist": "Adicionar à lista de reprodução",
"Add to playlist: ": "Adicionar à lista de reprodução: ",
"Answer": "Resposta",
"Answer": "Responder",
"Search for videos": "Procurar vídeos",
"carousel_slide": "Diapositivo {{current}} de{{total}}",
"carousel_skip": "Ignorar carrossel",

View file

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

View file

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

View file

@ -404,7 +404,7 @@
"generic_count_months_0": "{{count}} mesec",
"generic_count_months_1": "{{count}} meseca",
"generic_count_months_2": "{{count}} meseci",
"search_message_use_another_instance": " Takođe, možete <a href=\"`x`\">pretraživati na drugoj instanci</a>.",
"search_message_use_another_instance": "Takođe, možete <a href=\"`x`\">pretraživati na drugoj instanci</a>.",
"generic_subscribers_count_0": "{{count}} pratilac",
"generic_subscribers_count_1": "{{count}} pratioca",
"generic_subscribers_count_2": "{{count}} pratilaca",

View file

@ -404,7 +404,7 @@
"generic_count_months_0": "{{count}} месец",
"generic_count_months_1": "{{count}} месеца",
"generic_count_months_2": "{{count}} месеци",
"search_message_use_another_instance": " Такође, можете <a href=\"`x`\">претраживати на другој инстанци</a>.",
"search_message_use_another_instance": "Такође, можете <a href=\"`x`\">претраживати на другој инстанци</a>.",
"generic_subscribers_count_0": "{{count}} пратилац",
"generic_subscribers_count_1": "{{count}} пратиоца",
"generic_subscribers_count_2": "{{count}} пратилаца",

View file

@ -320,13 +320,13 @@
"channel_tab_community_label": "Gemenskap",
"search_filters_sort_option_relevance": "Relevans",
"search_filters_sort_option_rating": "Rankning",
"search_filters_sort_option_date": "Uppladdnings Datum",
"search_filters_sort_option_date": "Uppladdnings datum",
"search_filters_sort_option_views": "Visningar",
"search_filters_type_label": "Typ",
"search_filters_duration_label": "Varaktighet",
"search_filters_features_label": "Funktioner",
"search_filters_sort_label": "Sortera efter",
"search_filters_date_option_hour": "Senaste Timmen",
"search_filters_date_option_hour": "Senaste timmen",
"search_filters_date_option_today": "Idag",
"search_filters_date_option_week": "Denna vecka",
"search_filters_date_option_month": "Denna månad",
@ -393,7 +393,7 @@
"Artist: ": "Artist: ",
"generic_count_months": "{{count}}månad",
"generic_count_months_plural": "{{count}}månader",
"search_message_use_another_instance": " Du kan också <a href=\"`x`\">söka på en annan instans</a>.",
"search_message_use_another_instance": "Du kan också <a href=\"`x`\">söka på en annan instans</a>.",
"generic_subscribers_count": "{{count}} prenumerant",
"generic_subscribers_count_plural": "{{count}} prenumeranter",
"download_subtitles": "Undertexter - `x` (.vtt)",

View file

@ -322,13 +322,13 @@
"channel_tab_community_label": "Topluluk",
"search_filters_sort_option_relevance": "İlgi",
"search_filters_sort_option_rating": "Değerlendirme",
"search_filters_sort_option_date": "Yükleme Tarihi",
"search_filters_sort_option_date": "Yükleme tarihi",
"search_filters_sort_option_views": "Görüntüleme Sayısı",
"search_filters_type_label": "Tür",
"search_filters_duration_label": "Süre",
"search_filters_features_label": "Özellikler",
"search_filters_sort_label": "Sıralama Ölçütü",
"search_filters_date_option_hour": "Son Saat",
"search_filters_date_option_hour": "Son saat",
"search_filters_date_option_today": "Bugün",
"search_filters_date_option_week": "Bu Hafta",
"search_filters_date_option_month": "Bu Ay",
@ -452,7 +452,7 @@
"Spanish (Spain)": "İspanyolca (İspanya)",
"Vietnamese (auto-generated)": "Vietnamca (Otomatik Oluşturuldu)",
"preferences_watch_history_label": "İzleme Geçmişini Etkinleştir: ",
"search_message_use_another_instance": " Ayrıca <a href=\"`x`\">başka bir örnekte arayabilirsiniz</a>.",
"search_message_use_another_instance": "Ayrıca <a href=\"`x`\">başka bir örnekte arayabilirsiniz</a>.",
"search_filters_type_option_all": "Herhangi Bir Tür",
"search_filters_duration_option_none": "Herhangi Bir Süre",
"search_message_no_results": "Sonuç bulunamadı.",

View file

@ -455,7 +455,7 @@
"search_filters_date_option_week": "Цей тиждень",
"search_filters_type_label": "Тип",
"search_filters_type_option_channel": "Канал",
"search_message_use_another_instance": " Можете також <a href=\"`x`\">пошукати іншим сервером</a>.",
"search_message_use_another_instance": "Можете також <a href=\"`x`\">пошукати на іншому сервері</a>.",
"search_filters_title": "Фільтри",
"search_filters_date_option_hour": "Остання година",
"search_filters_date_option_month": "Цей місяць",
@ -472,7 +472,7 @@
"search_filters_features_option_three_sixty": "360°",
"search_filters_features_option_hdr": "HDR",
"search_filters_sort_label": "Спершу",
"search_filters_sort_option_date": "Нещодавні",
"search_filters_sort_option_date": "Дата вивантаження",
"search_filters_apply_button": "Застосувати фільтри",
"search_filters_features_option_vr180": "VR180",
"search_filters_features_option_purchased": "Придбано",

View file

@ -436,7 +436,7 @@
"Turkish (auto-generated)": "土耳其语 (自动生成)",
"Spanish (Spain)": "西班牙语 (西班牙)",
"preferences_watch_history_label": "启用观看历史: ",
"search_message_use_another_instance": " 你也可以 <a href=\"`x`\">在另一实例上搜索</a>。",
"search_message_use_another_instance": "你也可以 <a href=\"`x`\">在另一实例上搜索</a>。",
"search_filters_title": "过滤器",
"search_filters_date_label": "上传日期",
"search_filters_apply_button": "应用所选过滤器",

View file

@ -338,13 +338,13 @@
"channel_tab_community_label": "社群",
"search_filters_sort_option_relevance": "關聯",
"search_filters_sort_option_rating": "評分",
"search_filters_sort_option_date": "日期",
"search_filters_sort_option_date": "上傳日期",
"search_filters_sort_option_views": "檢視",
"search_filters_type_label": "內容類型",
"search_filters_duration_label": "時長",
"search_filters_features_label": "特色",
"search_filters_sort_label": "排序",
"search_filters_date_option_hour": "小時",
"search_filters_date_option_hour": "最後一小時",
"search_filters_date_option_today": "今天",
"search_filters_date_option_week": "週",
"search_filters_date_option_month": "月",
@ -442,7 +442,7 @@
"search_filters_duration_option_none": "任何時長",
"search_filters_duration_option_medium": "中等4到20分鐘",
"search_filters_features_option_vr180": "VR180",
"search_message_use_another_instance": " 您也可以<a href=\"`x`\">在其他站台上搜尋</a>。",
"search_message_use_another_instance": "您也可以<a href=\"`x`\">在其他站台上搜尋</a>。",
"search_filters_title": "過濾條件",
"search_filters_date_label": "上傳日期",
"search_filters_type_option_all": "任何類型",

2
mocks

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

View file

@ -10,16 +10,23 @@ shards:
backtracer:
git: https://github.com/sija/backtracer.cr.git
version: 1.2.1
version: 1.2.2
db:
git: https://github.com/crystal-lang/crystal-db.git
version: 0.10.1
version: 0.13.1
exception_page:
git: https://github.com/crystal-loot/exception_page.git
version: 0.2.2
inotify:
git: https://github.com/petoem/inotify.cr.git
version: 1.0.3
http_proxy:
git: https://github.com/mamantoha/http_proxy.git
version: 0.10.3
kemal:
git: https://github.com/kemalcr/kemal.git
version: 1.1.2
@ -30,7 +37,7 @@ shards:
pg:
git: https://github.com/will/crystal-pg.git
version: 0.24.0
version: 0.28.0
pool:
git: https://github.com/ysbaddaden/pool.git
@ -50,9 +57,9 @@ shards:
spectator:
git: https://github.com/icy-arctic-fox/spectator.git
version: 0.10.4
version: 0.10.6
sqlite3:
git: https://github.com/crystal-lang/crystal-sqlite3.git
version: 0.18.0
version: 0.21.0

View file

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

View file

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

View file

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

View file

@ -23,6 +23,7 @@ require "kilt"
require "./ext/kemal_content_for.cr"
require "./ext/kemal_static_file_handler.cr"
require "http_proxy"
require "athena-negotiation"
require "openssl/hmac"
require "option_parser"
@ -32,6 +33,7 @@ require "yaml"
require "compress/zip"
require "protodec/utils"
require "redis"
require "inotify"
require "./invidious/database/*"
require "./invidious/database/migrations/*"
@ -58,7 +60,20 @@ end
# Simple alias to make code easier to read
alias IV = Invidious
CONFIG = Config.load
CONFIG = Config.load
Signal::HUP.trap do
Config.reload
end
{% if flag?(:linux) %}
if CONFIG.reload_config_automatically
Inotify.watch("config/config.yml") do |event|
Config.reload
end
end
{% end %}
HMAC_KEY = CONFIG.hmac_key
PG_DB = DB.open CONFIG.database_url
@ -67,11 +82,12 @@ REDIS_DB = Redis::PooledClient.new(unixsocket: CONFIG.redis_socket || nil, url:
if REDIS_DB.ping
puts "Connected to redis"
end
ARCHIVE_URL = URI.parse("https://archive.org")
PUBSUB_URL = URI.parse("https://pubsubhubbub.appspot.com")
REDDIT_URL = URI.parse("https://www.reddit.com")
YT_URL = URI.parse("https://www.youtube.com")
HOST_URL = make_host_url(Kemal.config)
ARCHIVE_URL = URI.parse("https://archive.org")
PUBSUB_URL = URI.parse("https://pubsubhubbub.appspot.com")
REDDIT_URL = URI.parse("https://www.reddit.com")
YT_URL = URI.parse("https://www.youtube.com")
PUBSUB_HOST_URL = CONFIG.pubsub_domain
HOST_URL = make_host_url(Kemal.config)
CHARS_SAFE = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"
TEST_IDS = {"AgbeGFYluEA", "BaW_jenozKc", "a9LDPn-MO4I", "ddFvjfvPnqk", "iqKdEhx-dD4"}
@ -98,6 +114,10 @@ SOFTWARE = {
YT_POOL = YoutubeConnectionPool.new(YT_URL, capacity: CONFIG.pool_size)
# Image request pool
GGPHT_POOL = YoutubeConnectionPool.new(URI.parse("https://yt3.ggpht.com"), capacity: CONFIG.pool_size)
# CLI
Kemal.config.extra_options do |parser|
parser.banner = "Usage: invidious [arguments]"
@ -115,6 +135,9 @@ Kemal.config.extra_options do |parser|
parser.on("-l LEVEL", "--log-level=LEVEL", "Log level, one of #{LogLevel.values} (default: #{CONFIG.log_level})") do |log_level|
CONFIG.log_level = LogLevel.parse(log_level)
end
parser.on("-k", "--colorize", "Colorize logs") do
CONFIG.colorize_logs = true
end
parser.on("-v", "--version", "Print version") do
puts SOFTWARE.to_pretty_json
exit
@ -131,7 +154,7 @@ if CONFIG.output.upcase != "STDOUT"
FileUtils.mkdir_p(File.dirname(CONFIG.output))
end
OUTPUT = CONFIG.output.upcase == "STDOUT" ? STDOUT : File.open(CONFIG.output, mode: "a")
LOGGER = Invidious::LogHandler.new(OUTPUT, CONFIG.log_level)
LOGGER = Invidious::LogHandler.new(OUTPUT, CONFIG.log_level, CONFIG.colorize_logs)
# Check table integrity
Invidious::Database.check_integrity(CONFIG)
@ -185,6 +208,20 @@ Invidious::Jobs.register Invidious::Jobs::ClearExpiredItemsJob.new
Invidious::Jobs.register Invidious::Jobs::InstanceListRefreshJob.new
if !CONFIG.external_videoplayback_proxy.empty?
Invidious::Jobs.register Invidious::Jobs::CheckExternalProxy.new
else
# Invidious will it's own videoplayback proxy unless the admin decides to rewrite
# the /videoplayback location in the reverse proxy configuration (NGINX, Caddy, etc)
LOGGER.info("jobs: Disabling CheckExternalProxy job. Invidious will it's own videoplayback proxy")
end
if !CONFIG.tokens_server.empty?
Invidious::Jobs.register Invidious::Jobs::RefreshSessionTokens.new
else
LOGGER.info("jobs: Disabling RefreshSessionTokens job. Invidious will use the tokens that are on the configuration file")
end
Invidious::Jobs.start_all
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 ||= 0
live_now = channel_video.try &.live_now
live_now = channel_video.try &.badges.live_now?
live_now ||= false
premiere_timestamp = channel_video.try &.premiere_timestamp
@ -251,8 +251,8 @@ def fetch_channel(ucid, pull_all_videos : Bool)
LOGGER.trace("fetch_channel: #{ucid} : video #{video_id} : Inserted, updating subscriptions")
if CONFIG.enable_user_notifications
Invidious::Database::Users.add_notification(video)
else
Invidious::Database::Users.feed_needs_update(video)
# else
# Invidious::Database::Users.feed_needs_update(video)
end
else
LOGGER.trace("fetch_channel: #{ucid} : video #{video_id} : Updated")
@ -275,7 +275,7 @@ def fetch_channel(ucid, pull_all_videos : Bool)
ucid: video.ucid,
author: video.author,
length_seconds: video.length_seconds,
live_now: video.live_now,
live_now: video.badges.live_now?,
premiere_timestamp: video.premiere_timestamp,
views: video.views,
})
@ -287,8 +287,8 @@ def fetch_channel(ucid, pull_all_videos : Bool)
if was_insert
if CONFIG.enable_user_notifications
Invidious::Database::Users.add_notification(video)
else
Invidious::Database::Users.feed_needs_update(video)
# else
# Invidious::Database::Users.feed_needs_update(video)
end
end
end

View file

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

View file

@ -13,6 +13,7 @@ struct ConfigPreferences
property annotations : Bool = false
property annotations_subscribed : Bool = false
property preload : Bool = true
property autoplay : Bool = false
property captions : Array(String) = ["", "", ""]
property comments : Array(String) = ["youtube", ""]
@ -54,9 +55,28 @@ struct ConfigPreferences
end
end
struct HTTPProxyConfig
include YAML::Serializable
property user : String
property password : String
property host : String
property port : Int32
end
class Config
include YAML::Serializable
class CompanionConfig
include YAML::Serializable
@[YAML::Field(converter: Preferences::URIConverter)]
property private_url : URI = URI.parse("")
@[YAML::Field(converter: Preferences::URIConverter)]
property public_url : URI = URI.parse("")
end
# Number of threads to use for crawling videos from channels (for updating subscriptions)
property channel_threads : Int32 = 1
# Time interval between two executions of the job that crawls channel videos (subscriptions update).
@ -66,6 +86,8 @@ class Config
property output : String = "STDOUT"
# Default log level, valid YAML values are ints and strings, see src/invidious/helpers/logger.cr
property log_level : LogLevel = LogLevel::Info
# Enables colors in logs. Useful for debugging purposes
property colorize_logs : Bool = false
# Database configuration with separate parameters (username, hostname, etc)
property db : DBConfig? = nil
@ -84,18 +106,22 @@ class Config
# Used to tell Invidious it is behind a proxy, so links to resources should be https://
property https_only : Bool?
# Enable or disable CSP
property csp : Bool? = true
# HMAC signing key for CSRF tokens and verifying pubsub subscriptions
property hmac_key : String = ""
# Domain to be used for links to resources on the site where an absolute URL is required
property domain : String?
# Materialious redirects
property materialious_domain : String?
# Alternative domains. You can add other domains, like TOR and I2P addresses
property alternative_domains : Array(String) = [] of String
property donation_url : String?
property contact_url : String?
property home_domain : String?
# Backend domains. Domains for numbered backends
property backend_domains : Array(String) = [] of String
# Subscribe to channels using PubSubHubbub (requires domain, hmac_key)
property use_pubsub_feeds : Bool | Int32 = false
property use_innertube_for_feeds : Bool = true
property popular_enabled : Bool = true
property captcha_enabled : Bool = true
property login_enabled : Bool = true
@ -148,6 +174,8 @@ class Config
property host_binding : String = "0.0.0.0"
# Pool size for HTTP requests to youtube.com and ytimg.com (each domain has a separate pool of `pool_size`)
property pool_size : Int32 = 100
# HTTP Proxy configuration
property http_proxy : HTTPProxyConfig? = nil
# Use Innertube's transcripts API instead of timedtext for closed captions
property use_innertube_for_captions : Bool = false
@ -157,6 +185,12 @@ class Config
# poToken for passing bot attestation
property po_token : String? = nil
# Invidious companion
property invidious_companion : Array(CompanionConfig) = [] of CompanionConfig
# Invidious companion API key
property invidious_companion_key : String = ""
# Saved cookies in "name1=value1; name2=value2..." format
@[YAML::Field(converter: Preferences::StringToCookies)]
property cookies : HTTP::Cookies = HTTP::Cookies.new
@ -167,8 +201,27 @@ class Config
# The max resolution the Instance can offer
property max_dash_resolution : Int32?
# Materialious redirects
property materialious_domain : String?
# List of names of the backends
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
property pubsub_domain : String = ""
property ignore_user_tokens : Bool = false
property server_id_cookie_name : String = "INVIDIOUS_SERVER_ID"
property tokens_server : String = ""
{% if flag?(:linux) %}
property reload_config_automatically : Bool = true
{% end %}
def disabled?(option)
case disabled = CONFIG.disable_proxy
@ -185,6 +238,64 @@ class Config
end
end
def self.reload
LOGGER.info("Config: Reloading configuration")
# Load config from file or YAML string env var
env_config_file = "INVIDIOUS_CONFIG_FILE"
env_config_yaml = "INVIDIOUS_CONFIG"
config_file = ENV.has_key?(env_config_file) ? ENV.fetch(env_config_file) : "config/config.yml"
config_yaml = ENV.has_key?(env_config_yaml) ? ENV.fetch(env_config_yaml) : File.read(config_file)
begin
config = Config.from_yaml(config_yaml)
rescue ex
LOGGER.error("Config: Error when reloading configuration: '#{ex.message}'")
config = CONFIG
end
# TODO: Preserve old config and don't exit on fail
{% for ivar in Config.instance_vars %}
CONFIG.{{ivar}} = config.{{ivar}}
{% env_id = "INVIDIOUS_#{ivar.id.upcase}" %}
if ENV.has_key?({{env_id}})
env_value = ENV.fetch({{env_id}})
success = false
# Use YAML converter if specified
{% ann = ivar.annotation(::YAML::Field) %}
{% if ann && ann[:converter] %}
CONFIG.{{ivar.id}} = {{ann[:converter]}}.from_yaml(YAML::ParseContext.new, YAML::Nodes.parse(ENV.fetch({{env_id}})).nodes[0])
success = true
# Use regular YAML parser otherwise
{% else %}
{% ivar_types = ivar.type.union? ? ivar.type.union_types : [ivar.type] %}
# Sort types to avoid parsing nulls and numbers as strings
{% ivar_types = ivar_types.sort_by { |ivar_type| ivar_type == Nil ? 0 : ivar_type == Int32 ? 1 : 2 } %}
{{ivar_types}}.each do |ivar_type|
if !success
begin
CONFIG.{{ivar.id}} = ivar_type.from_yaml(env_value)
success = true
rescue
# nop
end
end
end
{% end %}
# Exit on fail
if !success
LOGGER.error("Config: Error when reloading environment variables for the configuration, exiting (fixme!)")
exit(1)
end
end
{% end %}
LOGGER.info("Config: Reload successfull")
end
def self.load
# Load config from file or YAML string env var
env_config_file = "INVIDIOUS_CONFIG_FILE"
@ -234,6 +345,23 @@ class Config
end
{% end %}
if config.invidious_companion.present?
# invidious_companion and signature_server can't work together
if config.signature_server
puts "Config: You can not run inv_sig_helper and invidious_companion at the same time."
exit(1)
elsif config.invidious_companion_key.empty?
puts "Config: Please configure a key if you are using invidious companion."
exit(1)
elsif config.invidious_companion_key == "CHANGE_ME!!"
puts "Config: The value of 'invidious_companion_key' needs to be changed!!"
exit(1)
elsif config.invidious_companion_key.size < 16
puts "Config: The value of 'invidious_companion_key' needs to be a size of 16 or more."
exit(1)
end
end
# HMAC_key is mandatory
# See: https://github.com/iv-org/invidious/issues/3854
if config.hmac_key.empty?

View file

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

View file

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

View file

@ -18,6 +18,40 @@ end
class HTTP::Client
property family : Socket::Family = Socket::Family::UNSPEC
# Override stdlib to automatically initialize proxy if configured
#
# Accurate as of crystal 1.12.1
def initialize(@host : String, port = nil, tls : TLSContext = nil)
check_host_only(@host)
{% if flag?(:without_openssl) %}
if tls
raise "HTTP::Client TLS is disabled because `-D without_openssl` was passed at compile time"
end
@tls = nil
{% else %}
@tls = case tls
when true
OpenSSL::SSL::Context::Client.new
when OpenSSL::SSL::Context::Client
tls
when false, nil
nil
end
{% end %}
@port = (port || (@tls ? 443 : 80)).to_i
self.proxy = make_configured_http_proxy_client() if CONFIG.http_proxy
end
def initialize(@io : IO, @host = "", @port = 80)
@reconnect = false
self.proxy = make_configured_http_proxy_client() if CONFIG.http_proxy
end
private def io
io = @io
return io if io

View file

@ -43,6 +43,8 @@ def error_template_helper(env : HTTP::Server::Context, status_code : Int32, exce
# URLs for the error message below
url_faq = "https://github.com/iv-org/documentation/blob/master/docs/faq.md"
url_search_issues = "https://github.com/iv-org/invidious/issues"
url_search_issues += "?q=is:issue+is:open+"
url_search_issues += URI.encode_www_form("[Bug] #{issue_title}")
url_switch = "https://redirect.invidious.io" + env.request.resource

View file

@ -1,8 +1,22 @@
# Languages requiring a better level of translation (at least 20%)
# to be added to the list below:
#
# "af" => "", # Afrikaans
# "az" => "", # Azerbaijani
# "be" => "", # Belarusian
# "bn_BD" => "", # Bengali (Bangladesh)
# "ia" => "", # Interlingua
# "or" => "", # Odia
# "tk" => "", # Turkmen
# "tok => "", # Toki Pona
#
LOCALES_LIST = {
"ar" => "العربية", # Arabic
"bg" => "български", # Bulgarian
"bn" => "বাংলা", # Bengali
"ca" => "Català", # Catalan
"cs" => "Čeština", # Czech
"cy" => "Cymraeg", # Welsh
"da" => "Dansk", # Danish
"de" => "Deutsch", # German
"el" => "Ελληνικά", # Greek
@ -23,6 +37,7 @@ LOCALES_LIST = {
"it" => "Italiano", # Italian
"ja" => "日本語", # Japanese
"ko" => "한국어", # Korean
"lmo" => "Lombard", # Lombard
"lt" => "Lietuvių", # Lithuanian
"nb-NO" => "Norsk bokmål", # Norwegian Bokmål
"nl" => "Nederlands", # Dutch

View file

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

View file

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

View file

@ -0,0 +1,35 @@
module SessionTokens
extend self
@@po_token : String | Nil
@@visitor_data : String | Nil
def refresh_tokens
begin
response = HTTP::Client.get "#{CONFIG.tokens_server}/generate"
if !response.status_code == 200
LOGGER.error("RefreshSessionTokens: Expected response to have status code 200 but got #{response.status_code} from #{CONFIG.tokens_server}")
end
json = JSON.parse(response.body)
@@po_token = json.try &.["potoken"].as_s || nil
@@visitor_data = json.try &.["visitorData"].as_s || nil
rescue ex
LOGGER.error("RefreshSessionTokens: Failed to fetch tokens from #{CONFIG.tokens_server}: #{ex.message}")
return
end
if !@@po_token.nil? && !@@visitor_data.nil?
set_tokens
LOGGER.debug("RefreshSessionTokens: Successfully updated po_token and visitor_data")
else
LOGGER.warn("RefreshSessionTokens: Tokens are empty!. Invidious will use the tokens that are on the configuration file")
end
LOGGER.trace("RefreshSessionTokens: Tokens are:")
LOGGER.trace("RefreshSessionTokens: po_token: #{CONFIG.po_token}")
LOGGER.trace("RefreshSessionTokens: visitor_data: #{CONFIG.visitor_data}")
end
def set_tokens
CONFIG.po_token = @@po_token
CONFIG.visitor_data = @@visitor_data
end
end

View file

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

View file

@ -294,7 +294,7 @@ def subscribe_pubsub(topic, key)
signature = "#{time}:#{nonce}"
body = {
"hub.callback" => "#{HOST_URL}/feed/webhook/v1:#{time}:#{nonce}:#{OpenSSL::HMAC.hexdigest(:sha1, key, signature)}",
"hub.callback" => "#{PUBSUB_HOST_URL}/feed/webhook/v1:#{time}:#{nonce}:#{OpenSSL::HMAC.hexdigest(:sha1, key, signature)}",
"hub.topic" => "https://www.youtube.com/xml/feeds/videos.xml?#{topic}",
"hub.verify" => "async",
"hub.mode" => "subscribe",
@ -383,3 +383,36 @@ def parse_link_endpoint(endpoint : JSON::Any, text : String, video_id : String)
end
return text
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
def encrypt_ecb_without_salt(data, key)
cipher = OpenSSL::Cipher.new("aes-128-ecb")
cipher.encrypt
cipher.key = key
io = IO::Memory.new
io.write(cipher.update(data))
io.write(cipher.final)
io.rewind
return io
end
def invidious_companion_encrypt(data)
timestamp = Time.utc.to_unix
encrypted_data = encrypt_ecb_without_salt("#{timestamp}|#{data}", CONFIG.invidious_companion_key)
return Base64.urlsafe_encode(encrypted_data)
end

View file

@ -4,6 +4,30 @@ module Invidious::HttpServer
module Utils
extend self
@@proxy_alive : String = ""
def check_external_proxy
CONFIG.external_videoplayback_proxy.each do |proxy|
begin
response = HTTP::Client.get("#{proxy}/health")
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
if @@proxy_alive.empty?
LOGGER.warn("CheckExternalProxy: No proxies alive! Using own server proxy")
end
end
def get_external_proxy
return @@proxy_alive
end
def proxy_video_url(raw_url : String, *, region : String? = nil, absolute : Bool = false)
url = URI.parse(raw_url)
@ -14,7 +38,11 @@ module Invidious::HttpServer
url.query_params = params
if absolute
return "#{HOST_URL}#{url.request_target}"
if !@@proxy_alive.empty?
return "#{@@proxy_alive}#{url.request_target}"
else
return "#{HOST_URL}#{url.request_target}"
end
else
return url.request_target
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 10 seconds")
sleep 10.seconds
Fiber.yield
end
end
end

View file

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

View file

@ -30,6 +30,8 @@ class Invidious::Jobs::SubscribeToFeedsJob < Invidious::Jobs::BaseJob
spawn do
begin
response = subscribe_pubsub(ucid, hmac_key)
LOGGER.debug("SubscribeToFeedsJob: Subscribed to #{ucid}.")
LOGGER.trace("SubscribeToFeedsJob: response.body: #{response.body}")
if response.status_code >= 400
LOGGER.error("SubscribeToFeedsJob: #{ucid} : #{response.body}")

View file

@ -8,7 +8,7 @@ module Invidious::JSONify::APIv1
build_thumbnails(id).each do |thumbnail|
json.object do
json.field "quality", thumbnail[:name]
json.field "url", "#{thumbnail[:host]}/vi/#{id}/#{thumbnail["url"]}.jpg"
json.field "url", "/vi/#{id}/#{thumbnail["url"]}.jpg"
json.field "width", thumbnail[:width]
json.field "height", thumbnail[:height]
end

View file

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

View file

@ -8,6 +8,11 @@ module Invidious::Routes::API::Manifest
id = env.params.url["id"]
region = env.params.query["region"]?
if CONFIG.invidious_companion.present?
invidious_companion = CONFIG.invidious_companion.sample
return env.redirect "#{invidious_companion.public_url}/api/manifest/dash/id/#{id}?#{env.params.query}"
end
# Since some implementations create playlists based on resolution regardless of different codecs,
# we can opt to only add a source to a representation if it has a unique height within that representation
unique_res = env.params.query["unique_res"]?.try { |q| (q == "true" || q == "1").to_unsafe }
@ -27,41 +32,37 @@ module Invidious::Routes::API::Manifest
haltf env, status_code: response.status_code
end
manifest = response.body
manifest = manifest.gsub(/<BaseURL>[^<]+<\/BaseURL>/) do |baseurl|
url = baseurl.lchop("<BaseURL>")
url = url.rchop("</BaseURL>")
if local
uri = URI.parse(url)
url = "#{HOST_URL}#{uri.request_target}host/#{uri.host}/"
end
# Proxy URLs for video playback on invidious.
# Other API clients can get the original URLs by omiting `local=true`.
manifest = response.body.gsub(/<BaseURL>[^<]+<\/BaseURL>/) do |baseurl|
url = baseurl.lchop("<BaseURL>").rchop("</BaseURL>")
url = HttpServer::Utils.proxy_video_url(url, absolute: true) if local
"<BaseURL>#{url}</BaseURL>"
end
return manifest
end
adaptive_fmts = video.adaptive_fmts
# Ditto, only proxify URLs if `local=true` is used
if local
adaptive_fmts.each do |fmt|
fmt["url"] = JSON::Any.new("#{HOST_URL}#{URI.parse(fmt["url"].as_s).request_target}")
video.adaptive_fmts.each do |fmt|
fmt["url"] = JSON::Any.new(HttpServer::Utils.proxy_video_url(fmt["url"].as_s, absolute: true))
end
end
audio_streams = video.audio_streams.sort_by { |stream| {stream["bitrate"].as_i} }.reverse!
video_streams = video.video_streams.sort_by { |stream| {stream["width"].as_i, stream["fps"].as_i} }.reverse!
# Removes all the resolutions with a height higher than CONFIG.max_dash_resolution
# Removes all the resolutions with a height higher than CONFIG.max_dash_resolution
if CONFIG.max_dash_resolution
video_streams.reject! do |z|
(z["height"].as_i > CONFIG.max_dash_resolution.not_nil!) if z["height"]?
end
end
audio_streams.reject! do |z|
z if z.dig?("audioTrack", "audioIsDefault") == false
end
manifest = XML.build(indent: " ", encoding: "UTF-8") do |xml|
xml.element("MPD", "xmlns": "urn:mpeg:dash:schema:mpd:2011",
@ -217,7 +218,19 @@ module Invidious::Routes::API::Manifest
raw_params["local"] = "true"
"#{HOST_URL}/videoplayback?#{raw_params}"
proxy = Invidious::HttpServer::Utils.get_external_proxy
if CONFIG.https_only
scheme = "https://"
else
scheme = "http://"
end
if !proxy.empty?
"#{proxy}/videoplayback?#{raw_params}"
else
"#{scheme}#{env.request.headers["Host"]}/videoplayback?#{raw_params}"
end
end
end
@ -240,7 +253,12 @@ module Invidious::Routes::API::Manifest
manifest = response.body
if local
manifest = manifest.gsub("https://www.youtube.com", HOST_URL)
if CONFIG.https_only
scheme = "https://"
else
scheme = "http://"
end
manifest = manifest.gsub("https://www.youtube.com", "#{scheme}#{env.request.headers["Host"]}")
manifest = manifest.gsub("index.m3u8", "index.m3u8?local=true")
end

View file

@ -226,7 +226,7 @@ module Invidious::Routes::API::V1::Authenticated
end
playlist = create_playlist(title, privacy, user)
env.response.headers["Location"] = "#{HOST_URL}/api/v1/auth/playlists/#{playlist.id}"
env.response.headers["Location"] = "#{env.request.headers["Host"]}/api/v1/auth/playlists/#{playlist.id}"
env.response.status_code = 201
{
"title" => title,

View file

@ -197,6 +197,7 @@ module Invidious::Routes::API::V1::Channels
get_channel()
# Retrieve continuation from URL parameters
sort_by = env.params.query["sort_by"]?.try &.downcase || "newest"
continuation = env.params.query["continuation"]?
if channel.is_age_gated
@ -211,7 +212,7 @@ module Invidious::Routes::API::V1::Channels
else
begin
videos, next_continuation = Channel::Tabs.get_shorts(
channel, continuation: continuation
channel, continuation: continuation, sort_by: sort_by
)
rescue ex
return error_json(500, ex)

View file

@ -31,9 +31,7 @@ module Invidious::Routes::API::V1::Search
query = env.params.query["q"]? || ""
begin
client = HTTP::Client.new("suggestqueries-clients6.youtube.com")
client.before_request { |r| add_yt_headers(r) }
client = make_client(URI.parse("https://suggestqueries-clients6.youtube.com"), force_youtube_headers: true)
url = "/complete/search?client=youtube&hl=en&gl=#{region}&q=#{URI.encode_www_form(query)}&gs_ri=youtube&ds=yt"
response = client.get(url).body

View file

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

View file

@ -20,12 +20,25 @@ module Invidious::Routes::BeforeAll
env.response.headers["X-XSS-Protection"] = "1; mode=block"
env.response.headers["X-Content-Type-Options"] = "nosniff"
extra_media_csp = ""
extra_connect_csp = ""
if CONFIG.invidious_companion.present?
extra_media_csp = " #{CONFIG.invidious_companion.sample.public_url}"
extra_connect_csp = " #{CONFIG.invidious_companion.sample.public_url}"
end
if !CONFIG.external_videoplayback_proxy.empty?
CONFIG.external_videoplayback_proxy.each do |proxy|
extra_media_csp += " #{proxy}"
extra_connect_csp += " #{proxy}"
end
end
# Allow media resources to be loaded from google servers
# TODO: check if *.youtube.com can be removed
if CONFIG.disabled?("local") || !preferences.local
extra_media_csp = " https://*.googlevideo.com:443 https://*.youtube.com:443"
else
extra_media_csp = ""
extra_media_csp += " https://*.googlevideo.com:443 https://*.youtube.com:443"
end
# Only allow the pages at /embed/* to be embedded
@ -43,7 +56,7 @@ module Invidious::Routes::BeforeAll
"style-src 'self' 'unsafe-inline'",
"img-src 'self' data:",
"font-src 'self' data:",
"connect-src 'self'",
"connect-src 'self'" + extra_connect_csp,
"manifest-src 'self'",
"media-src 'self' blob:" + extra_media_csp,
"child-src 'self' blob:",

View file

@ -20,10 +20,11 @@ module Invidious::Routes::Channels
sort_by = env.params.query["sort_by"]?.try &.downcase
if channel.auto_generated
sort_by ||= "last"
sort_options = {"last", "oldest", "newest"}
items, next_continuation = fetch_channel_playlists(
channel.ucid, channel.author, continuation, (sort_by || "last")
channel.ucid, channel.author, continuation, sort_by
)
items.uniq! do |item|
@ -49,9 +50,11 @@ module Invidious::Routes::Channels
end
next_continuation = nil
else
sort_by ||= "newest"
sort_options = {"newest", "oldest", "popular"}
items, next_continuation = Channel::Tabs.get_videos(
channel, continuation: continuation, sort_by: (sort_by || "newest")
items, next_continuation = Channel::Tabs.get_60_videos(
channel, continuation: continuation, sort_by: sort_by
)
end
end
@ -82,13 +85,12 @@ module Invidious::Routes::Channels
end
next_continuation = nil
else
# TODO: support sort option for shorts
sort_by = ""
sort_options = [] of String
sort_by = env.params.query["sort_by"]?.try &.downcase || "newest"
sort_options = {"newest", "oldest", "popular"}
# Fetch items and continuation token
items, next_continuation = Channel::Tabs.get_shorts(
channel, continuation: continuation
channel, continuation: continuation, sort_by: sort_by
)
end

View file

@ -157,10 +157,12 @@ module Invidious::Routes::Embed
adaptive_fmts = video.adaptive_fmts
if params.local
fmt_stream.each { |fmt| fmt["url"] = JSON::Any.new(URI.parse(fmt["url"].as_s).request_target) }
adaptive_fmts.each { |fmt| fmt["url"] = JSON::Any.new(URI.parse(fmt["url"].as_s).request_target) }
fmt_stream.each { |fmt| fmt["url"] = JSON::Any.new(HttpServer::Utils.proxy_video_url(fmt["url"].as_s)) }
end
# Always proxy DASH streams, otherwise youtube CORS headers will prevent playback
adaptive_fmts.each { |fmt| fmt["url"] = JSON::Any.new(HttpServer::Utils.proxy_video_url(fmt["url"].as_s)) }
video_streams = video.video_streams
audio_streams = video.audio_streams

View file

@ -192,11 +192,9 @@ module Invidious::Routes::Feeds
views: views,
description_html: description_html,
length_seconds: 0,
live_now: false,
paid: false,
premium: false,
premiere_timestamp: nil,
author_verified: false,
badges: VideoBadges::None,
})
end
@ -418,18 +416,22 @@ module Invidious::Routes::Feeds
author = entry.xpath_node("default:author/default:name", namespaces).not_nil!.content
published = Time.parse_rfc3339(entry.xpath_node("default:published", namespaces).not_nil!.content)
updated = Time.parse_rfc3339(entry.xpath_node("default:updated", namespaces).not_nil!.content)
ucid = entry.xpath_node("yt:channelId", namespaces).not_nil!.content
title = entry.xpath_node("default:title", namespaces).not_nil!.content
begin
video = get_video(id, force_refresh: true)
rescue
next # skip this video since it raised an exception (e.g. it is a scheduled live event)
if CONFIG.use_innertube_for_feeds
begin
video_ = get_video(id, force_refresh: true)
rescue
next # skip this video since it raised an exception (e.g. it is a scheduled live event)
end
end
if CONFIG.enable_user_notifications
# Deliver notifications to `/api/v1/auth/notifications`
payload = {
"topic" => video.ucid,
"videoId" => video.id,
"topic" => ucid,
"videoId" => id,
"published" => published.to_unix,
}.to_json
PG_DB.exec("NOTIFY notifications, E'#{payload}'")
@ -437,23 +439,23 @@ module Invidious::Routes::Feeds
video = ChannelVideo.new({
id: id,
title: video.title,
title: title,
published: published,
updated: updated,
ucid: video.ucid,
ucid: ucid,
author: author,
length_seconds: video.length_seconds,
live_now: video.live_now,
premiere_timestamp: video.premiere_timestamp,
views: video.views,
length_seconds: video_.try &.length_seconds || 0,
live_now: video_.try &.live_now || false,
premiere_timestamp: video_.try &.premiere_timestamp || nil,
views: video_.try &.views || nil,
})
was_insert = Invidious::Database::ChannelVideos.insert(video, with_premiere_timestamp: true)
if was_insert
if CONFIG.enable_user_notifications
Invidious::Database::Users.add_notification(video)
else
Invidious::Database::Users.feed_needs_update(video)
# else
# Invidious::Database::Users.feed_needs_update(video)
end
end
end

View file

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

View file

@ -64,6 +64,8 @@ module Invidious::Routes::Login
# TOR or I2P address
if alt = CONFIG.alternative_domains.index(env.request.headers["Host"])
env.response.cookies["SID"] = Invidious::User::Cookies.sid(CONFIG.alternative_domains[alt], sid)
elsif alt = CONFIG.backend_domains.index(env.request.headers["Host"])
env.response.cookies["SID"] = Invidious::User::Cookies.sid(CONFIG.backend_domains[alt], sid)
else
env.response.cookies["SID"] = Invidious::User::Cookies.sid(CONFIG.domain, sid)
end
@ -170,6 +172,8 @@ module Invidious::Routes::Login
# TOR or I2P address
if alt = CONFIG.alternative_domains.index(env.request.headers["Host"])
env.response.cookies["SID"] = Invidious::User::Cookies.sid(CONFIG.alternative_domains[alt], sid)
elsif alt = CONFIG.backend_domains.index(env.request.headers["Host"])
env.response.cookies["SID"] = Invidious::User::Cookies.sid(CONFIG.backend_domains[alt], sid)
else
env.response.cookies["SID"] = Invidious::User::Cookies.sid(CONFIG.domain, sid)
end

View file

@ -27,6 +27,10 @@ module Invidious::Routes::PreferencesRoute
annotations_subscribed ||= "off"
annotations_subscribed = annotations_subscribed == "on"
preload = env.params.body["preload"]?.try &.as(String)
preload ||= "off"
preload = preload == "on"
autoplay = env.params.body["autoplay"]?.try &.as(String)
autoplay ||= "off"
autoplay = autoplay == "on"
@ -144,6 +148,7 @@ module Invidious::Routes::PreferencesRoute
preferences = Preferences.from_json({
annotations: annotations,
annotations_subscribed: annotations_subscribed,
preload: preload,
autoplay: autoplay,
captions: captions,
comments: comments,
@ -223,6 +228,8 @@ module Invidious::Routes::PreferencesRoute
# TOR or I2P address
if alt = CONFIG.alternative_domains.index(env.request.headers["Host"])
env.response.cookies["PREFS"] = Invidious::User::Cookies.prefs(CONFIG.alternative_domains[alt], preferences)
elsif alt = CONFIG.backend_domains.index(env.request.headers["Host"])
env.response.cookies["PREFS"] = Invidious::User::Cookies.prefs(CONFIG.backend_domains[alt], preferences)
else
env.response.cookies["PREFS"] = Invidious::User::Cookies.prefs(CONFIG.domain, preferences)
end
@ -264,6 +271,8 @@ module Invidious::Routes::PreferencesRoute
# TOR or I2P address
if alt = CONFIG.alternative_domains.index(env.request.headers["Host"])
env.response.cookies["PREFS"] = Invidious::User::Cookies.prefs(CONFIG.alternative_domains[alt], preferences)
elsif alt = CONFIG.backend_domains.index(env.request.headers["Host"])
env.response.cookies["PREFS"] = Invidious::User::Cookies.prefs(CONFIG.backend_domains[alt], preferences)
else
env.response.cookies["PREFS"] = Invidious::User::Cookies.prefs(CONFIG.domain, preferences)
end

View file

@ -3,6 +3,11 @@ module Invidious::Routes::VideoPlayback
def self.get_video_playback(env)
locale = env.get("preferences").as(Preferences).locale
query_params = env.params.query
array = UInt8[0x78, 0]
protobuf = Bytes.new(array.size)
array.each_with_index do |byte, index|
protobuf[index] = byte
end
fvip = query_params["fvip"]? || "3"
mns = query_params["mn"]?.try &.split(",")
@ -42,11 +47,7 @@ module Invidious::Routes::VideoPlayback
headers["Range"] = "bytes=#{range_for_head}"
end
headers["Origin"] = "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"
client = make_client(URI.parse(host), region, force_resolve = true)
client = make_client(URI.parse(host), region, force_resolve: true)
response = HTTP::Client::Response.new(500)
error = ""
5.times do
@ -61,7 +62,7 @@ module Invidious::Routes::VideoPlayback
if new_host != host
host = new_host
client.close
client = make_client(URI.parse(new_host), region, force_resolve = true)
client = make_client(URI.parse(new_host), region, force_resolve: true)
end
url = "#{location.request_target}&host=#{location.host}#{region ? "&region=#{region}" : ""}"
@ -75,7 +76,7 @@ module Invidious::Routes::VideoPlayback
fvip = "3"
host = "https://r#{fvip}---#{mn}.googlevideo.com"
client = make_client(URI.parse(host), region, force_resolve = true)
client = make_client(URI.parse(host), region, force_resolve: true)
rescue ex
error = ex.message
end
@ -100,7 +101,7 @@ module Invidious::Routes::VideoPlayback
end
begin
client.get(url, headers) do |resp|
client.post(url, headers, protobuf) do |resp|
resp.headers.each do |key, value|
if !RESPONSE_HEADERS_BLACKLIST.includes?(key.downcase)
env.response.headers[key] = value
@ -151,7 +152,7 @@ module Invidious::Routes::VideoPlayback
headers["Range"] = "bytes=#{chunk_start}-#{chunk_end}"
begin
client.get(url, headers) do |resp|
client.post(url, headers, protobuf) do |resp|
if first_chunk
if !env.request.headers["Range"]? && resp.status_code == 206
env.response.status_code = 200
@ -200,7 +201,7 @@ module Invidious::Routes::VideoPlayback
break
else
client.close
client = make_client(URI.parse(host), region, force_resolve = true)
client = make_client(URI.parse(host), region, force_resolve: true)
end
end
@ -257,6 +258,11 @@ module Invidious::Routes::VideoPlayback
# YouTube /videoplayback links expire after 6 hours,
# so we have a mechanism here to redirect to the latest version
def self.latest_version(env)
if CONFIG.invidious_companion.present?
invidious_companion = CONFIG.invidious_companion.sample
return env.redirect "#{invidious_companion.public_url}/latest_version?#{env.params.query}"
end
id = env.params.query["id"]?
itag = env.params.query["itag"]?.try &.to_i?
@ -298,7 +304,16 @@ module Invidious::Routes::VideoPlayback
end
if local
url = URI.parse(url).request_target.not_nil!
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!
end
url += "&title=#{URI.encode_www_form(title, space_to_plus: false)}" if title
end

View file

@ -120,7 +120,7 @@ module Invidious::Routes::Watch
fmt_stream = video.fmt_stream
adaptive_fmts = video.adaptive_fmts
# Removes all the resolutions with a height higher than CONFIG.max_dash_resolution
# Removes all the resolutions with a height higher than CONFIG.max_dash_resolution
if CONFIG.max_dash_resolution
adaptive_fmts.reject! do |z|
(z["height"].as_i > CONFIG.max_dash_resolution.not_nil!) if z["height"]?
@ -128,20 +128,27 @@ module Invidious::Routes::Watch
end
if params.local
fmt_stream.each { |fmt| fmt["url"] = JSON::Any.new(URI.parse(fmt["url"].as_s).request_target) }
adaptive_fmts.each { |fmt| fmt["url"] = JSON::Any.new(URI.parse(fmt["url"].as_s).request_target) }
fmt_stream.each { |fmt| fmt["url"] = JSON::Any.new(HttpServer::Utils.proxy_video_url(fmt["url"].as_s)) }
end
# Always proxy DASH streams, otherwise youtube CORS headers will prevent playback
adaptive_fmts.each { |fmt| fmt["url"] = JSON::Any.new(HttpServer::Utils.proxy_video_url(fmt["url"].as_s)) }
video_streams = video.video_streams
audio_streams = video.audio_streams
# Removes all the resolutions with a height higher than CONFIG.max_dash_resolution
# Removes all the resolutions with a height higher than CONFIG.max_dash_resolution
if CONFIG.max_dash_resolution
video_streams.reject! do |z|
(z["height"].as_i > CONFIG.max_dash_resolution.not_nil!) if z["height"]?
end
end
# Removes non default audio tracks
audio_streams.reject! do |z|
z if z.dig?("audioTrack", "audioIsDefault") == false
end
# Older videos may not have audio sources available.
# We redirect here so they're not unplayable
if audio_streams.empty? && !video.live_now
@ -204,6 +211,12 @@ module Invidious::Routes::Watch
captions: video.captions
)
begin
video_url = fmt_stream[0]["url"].to_s
rescue
video_url = nil
end
templated "watch"
end
@ -334,14 +347,18 @@ module Invidious::Routes::Watch
env.params.query["label"] = URI.decode_www_form(label.as_s)
return Invidious::Routes::API::V1::Videos.captions(env)
elsif itag = download_widget["itag"]?.try &.as_i
elsif itag = download_widget["itag"]?.try &.as_i.to_s
# URL params specific to /latest_version
env.params.query["id"] = video_id
env.params.query["itag"] = itag.to_s
env.params.query["title"] = filename
env.params.query["local"] = "true"
return Invidious::Routes::VideoPlayback.latest_version(env)
if (CONFIG.invidious_companion.present?)
video = get_video(video_id)
return env.redirect "#{video.invidious_companion["baseUrl"].as_s}/latest_version?#{env.params.query}"
else
return Invidious::Routes::VideoPlayback.latest_version(env)
end
else
return error_template(400, "Invalid label or itag")
end

View file

@ -243,17 +243,16 @@ module Invidious::Routing
# Channels
get "/api/v1/channels/:ucid", {{namespace}}::Channels, :home
get "/api/v1/channels/:ucid/latest", {{namespace}}::Channels, :latest
get "/api/v1/channels/:ucid/videos", {{namespace}}::Channels, :videos
get "/api/v1/channels/:ucid/shorts", {{namespace}}::Channels, :shorts
get "/api/v1/channels/:ucid/streams", {{namespace}}::Channels, :streams
get "/api/v1/channels/:ucid/podcasts", {{namespace}}::Channels, :podcasts
get "/api/v1/channels/:ucid/releases", {{namespace}}::Channels, :releases
get "/api/v1/channels/:ucid/playlists", {{namespace}}::Channels, :playlists
get "/api/v1/channels/:ucid/community", {{namespace}}::Channels, :community
get "/api/v1/channels/:ucid/channels", {{namespace}}::Channels, :channels
{% for route in {"videos", "latest", "playlists", "community", "search"} %}
get "/api/v1/channels/#{{{route}}}/:ucid", {{namespace}}::Channels, :{{route}}
get "/api/v1/channels/:ucid/#{{{route}}}", {{namespace}}::Channels, :{{route}}
{% end %}
get "/api/v1/channels/:ucid/search", {{namespace}}::Channels, :search
# Posts
get "/api/v1/post/:id", {{namespace}}::Channels, :post
@ -271,11 +270,6 @@ module Invidious::Routing
# Authenticated
# The notification APIs cannot be extracted yet! They require the *local* notifications constant defined in invidious.cr
#
# Invidious::Routing.get "/api/v1/auth/notifications", {{namespace}}::Authenticated, :notifications
# Invidious::Routing.post "/api/v1/auth/notifications", {{namespace}}::Authenticated, :notifications
get "/api/v1/auth/preferences", {{namespace}}::Authenticated, :get_preferences
post "/api/v1/auth/preferences", {{namespace}}::Authenticated, :set_preferences

View file

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

View file

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

View file

@ -123,6 +123,7 @@ module Invidious::Videos
"Esperanto",
"Estonian",
"Filipino",
"Filipino (auto-generated)",
"Finnish",
"French",
"French (auto-generated)",

View file

@ -100,42 +100,46 @@ def extract_video_info(video_id : String)
params = parse_video_info(video_id, player_response)
params["reason"] = JSON::Any.new(reason) if reason
new_player_response = nil
if CONFIG.invidious_companion.present?
new_player_response = nil
# Don't use Android client if po_token is passed because po_token doesn't
# work for Android client.
if reason.nil? && CONFIG.po_token.nil?
# Fetch the video streams using an Android client in order to get the
# decrypted URLs and maybe fix throttling issues (#2194). See the
# following issue for an explanation about decrypted URLs:
# https://github.com/TeamNewPipe/NewPipeExtractor/issues/562
client_config.client_type = YoutubeAPI::ClientType::AndroidTestSuite
new_player_response = try_fetch_streaming_data(video_id, client_config)
# Don't use Android test suite client if po_token is passed because po_token doesn't
# work for Android test suite client.
if reason.nil? && CONFIG.po_token.nil?
# Fetch the video streams using an Android client in order to get the
# decrypted URLs and maybe fix throttling issues (#2194). See the
# following issue for an explanation about decrypted URLs:
# https://github.com/TeamNewPipe/NewPipeExtractor/issues/562
client_config.client_type = YoutubeAPI::ClientType::AndroidTestSuite
new_player_response = try_fetch_streaming_data(video_id, client_config)
end
# Replace player response and reset reason
if !new_player_response.nil?
# Preserve captions & storyboard data before replacement
new_player_response["storyboards"] = player_response["storyboards"] if player_response["storyboards"]?
new_player_response["captions"] = player_response["captions"] if player_response["captions"]?
player_response = new_player_response
params.delete("reason")
end
end
# Last hope
# 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
# if the IP address is not blocked.
if CONFIG.po_token && reason || CONFIG.po_token.nil? && new_player_response.nil?
client_config.client_type = YoutubeAPI::ClientType::TvHtml5ScreenEmbed
new_player_response = try_fetch_streaming_data(video_id, client_config)
end
# Replace player response and reset reason
if !new_player_response.nil?
# Preserve captions & storyboard data before replacement
new_player_response["storyboards"] = player_response["storyboards"] if player_response["storyboards"]?
new_player_response["captions"] = player_response["captions"] if player_response["captions"]?
player_response = new_player_response
params.delete("reason")
end
{"captions", "playabilityStatus", "playerConfig", "storyboards", "streamingData"}.each do |f|
{"captions", "playabilityStatus", "playerConfig", "storyboards", "invidiousCompanion"}.each do |f|
params[f] = player_response[f] if player_response[f]?
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))
end
end
params["streamingData"] = streaming_data
end
# Data structure version, for cache control
params["version"] = JSON::Any.new(Video::SCHEMA_VERSION.to_i64)
@ -213,8 +217,17 @@ def parse_video_info(video_id : String, player_response : Hash(String, JSON::Any
premiere_timestamp = microformat.dig?("liveBroadcastDetails", "startTimestamp")
.try { |t| Time.parse_rfc3339(t.as_s) }
premiere_timestamp ||= player_response.dig?(
"playabilityStatus", "liveStreamability",
"liveStreamabilityRenderer", "offlineSlate",
"liveStreamOfflineSlateRenderer", "scheduledStartTime"
)
.try &.as_s.to_i64
.try { |t| Time.unix(t) }
live_now = microformat.dig?("liveBroadcastDetails", "isLiveNow")
.try &.as_bool || false
.try &.as_bool
live_now ||= video_details.dig?("isLive").try &.as_bool || false
post_live_dvr = video_details.dig?("isPostLiveDvr")
.try &.as_bool || false
@ -443,3 +456,35 @@ def parse_video_info(video_id : String, player_response : Hash(String, JSON::Any
return params
end
private def convert_url(fmt)
if cfr = fmt["signatureCipher"]?.try { |json| HTTP::Params.parse(json.as_s) }
sp = cfr["sp"]
url = URI.parse(cfr["url"])
params = url.query_params
LOGGER.debug("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 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
property annotations : Bool
property preload : Bool
property autoplay : Bool
property comments : Array(String)
property continue : Bool
@ -28,6 +29,7 @@ end
def process_video_params(query, preferences)
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 }
comments = query["comments"]?.try &.split(",").map(&.downcase)
continue = query["continue"]?.try { |q| (q == "true" || q == "1").to_unsafe }
@ -50,6 +52,7 @@ def process_video_params(query, preferences)
if preferences
# region ||= preferences.region
annotations ||= preferences.annotations.to_unsafe
preload ||= preferences.preload.to_unsafe
autoplay ||= preferences.autoplay.to_unsafe
comments ||= preferences.comments
continue ||= preferences.continue.to_unsafe
@ -70,6 +73,7 @@ def process_video_params(query, preferences)
end
annotations ||= CONFIG.default_user_preferences.annotations.to_unsafe
preload ||= CONFIG.default_user_preferences.preload.to_unsafe
autoplay ||= CONFIG.default_user_preferences.autoplay.to_unsafe
comments ||= CONFIG.default_user_preferences.comments
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
annotations = annotations == 1
preload = preload == 1
autoplay = autoplay == 1
continue = continue == 1
continue_autoplay = continue_autoplay == 1
@ -128,6 +133,7 @@ def process_video_params(query, preferences)
params = VideoPreferences.new({
annotations: annotations,
preload: preload,
autoplay: autoplay,
comments: comments,
continue: continue,

View file

@ -2,6 +2,7 @@
ucid = channel.ucid
author = HTML.escape(channel.author)
channel_profile_pic = URI.parse(channel.author_thumbnail).request_target
host = env.request.headers["Host"]
relative_url =
case selected_tab
@ -28,15 +29,15 @@
<%- if selected_tab.videos? -%>
<meta name="description" content="<%= channel.description %>">
<meta property="og:site_name" content="Invidious">
<meta property="og:url" content="<%= HOST_URL %>/channel/<%= ucid %>">
<meta property="og:url" content="<%= host %>/channel/<%= ucid %>">
<meta property="og:title" content="<%= author %>">
<meta property="og:image" content="<%= HOST_URL %>/ggpht<%= channel_profile_pic %>">
<meta property="og:image" content="<%= host %>/ggpht<%= channel_profile_pic %>">
<meta property="og:description" content="<%= channel.description %>">
<meta name="twitter:card" content="summary">
<meta name="twitter:url" content="<%= HOST_URL %>/channel/<%= ucid %>">
<meta name="twitter:url" content="<%= host %>/channel/<%= ucid %>">
<meta name="twitter:title" content="<%= author %>">
<meta name="twitter:description" content="<%= channel.description %>">
<meta name="twitter:image" content="<%= HOST_URL %>/ggpht<%= channel_profile_pic %>">
<meta name="twitter:image" content="<%= host %>/ggpht<%= channel_profile_pic %>">
<link rel="alternate" type="application/rss+xml" title="RSS" href="/feed/channel/<%= ucid %>" />
<%- end -%>

View file

@ -1,5 +1,6 @@
<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 %>"
preload="<% if params.preload %>auto<% else %>none<% end %>"
<% if params.autoplay %>autoplay<% end %>
<% if params.video_loop %>loop<% end %>
<% if params.controls %>controls<% end %>>
@ -21,6 +22,8 @@
audio_streams.each_with_index do |fmt, i|
src_url = "/latest_version?id=#{video.id}&itag=#{fmt["itag"]}"
src_url += "&local=true" if params.local
src_url = video.invidious_companion["baseUrl"].as_s + src_url +
"&check=#{invidious_companion_encrypt(video.id)}" if (CONFIG.invidious_companion.present?)
bitrate = fmt["bitrate"]
mimetype = HTML.escape(fmt["mimeType"].as_s)
@ -33,8 +36,12 @@
<% end %>
<% end %>
<% else %>
<% if params.quality == "dash" %>
<source src="/api/manifest/dash/id/<%= video.id %>?local=true&unique_res=1" type='application/dash+xml' label="dash">
<% if params.quality == "dash"
src_url = "/api/manifest/dash/id/" + video.id + "?local=true&unique_res=1"
src_url = video.invidious_companion["baseUrl"].as_s + src_url +
"&check=#{invidious_companion_encrypt(video.id)}" if (CONFIG.invidious_companion.present?)
%>
<source src="<%= src_url %>" type='application/dash+xml' label="dash">
<% end %>
<%
@ -43,6 +50,8 @@
fmt_stream.each_with_index do |fmt, i|
src_url = "/latest_version?id=#{video.id}&itag=#{fmt["itag"]}"
src_url += "&local=true" if params.local
src_url = video.invidious_companion["baseUrl"].as_s + src_url +
"&check=#{invidious_companion_encrypt(video.id)}" if (CONFIG.invidious_companion.present?)
quality = fmt["quality"]
mimetype = HTML.escape(fmt["mimeType"].as_s)

View file

@ -1,6 +1,8 @@
<%
locale = env.get("preferences").as(Preferences).locale
dark_mode = env.get("preferences").as(Preferences).dark_mode
current_backend = env.request.cookies[CONFIG.server_id_cookie_name]?.try &.value || env.request.headers["Host"]
current_external_videoplayback_proxy = Invidious::HttpServer::Utils.get_external_proxy()
%>
<!DOCTYPE html>
<html lang="<%= locale %>">
@ -104,6 +106,32 @@
</div>
</div>
<% if !CONFIG.backends.empty? %>
<% if !CONFIG.backend_domains.includes?(env.request.headers["Host"]) %>
<div class="h-box">
<b>Switch Backend:</b>
<% CONFIG.backends.each do | backend | %>
<% backend = backend.split(CONFIG.backends_delimiter) %>
<% if current_backend == backend[0] %>
<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 %>
</div>
<% end %>
<% end %>
<% if CONFIG.banner %>
<div class="h-box">
<h3><%= CONFIG.banner %></h3>
@ -285,6 +313,10 @@
</div>
<hr/>
<div class="footer-footer">
<div class="box">You are currently using Backend: <%= current_backend %></div>
<% if !current_external_videoplayback_proxy.empty? %>
<div class="box">External Videoplayback Proxy: <%= current_external_videoplayback_proxy %></div>
<% end %>
<span class="left">
<% if CONFIG.modified_source_code_url %>
<%= 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 %>>
</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">
<label for="autoplay"><%= translate(locale, "preferences_autoplay_label") %></label>
<input name="autoplay" id="autoplay" type="checkbox" <% if preferences.autoplay %>checked<% end %>>

View file

@ -1,6 +1,7 @@
<% ucid = video.ucid %>
<% title = HTML.escape(video.title) %>
<% author = HTML.escape(video.author) %>
<% host = env.request.headers["Host"] %>
<% content_for "header" do %>
@ -8,22 +9,24 @@
<meta name="description" content="<%= HTML.escape(video.short_description) %>">
<meta name="keywords" content="<%= video.keywords.join(",") %>">
<meta property="og:site_name" content="<%= author %> | Invidious">
<meta property="og:url" content="<%= HOST_URL %>/watch?v=<%= video.id %>">
<meta property="og:url" content="<%= host %>/watch?v=<%= video.id %>">
<meta property="og:title" content="<%= title %>">
<meta property="og:image" content="<%= HOST_URL %>/vi/<%= video.id %>/maxres.jpg">
<meta property="og:image" content="<%= host %>/vi/<%= video.id %>/maxres.jpg">
<meta property="og:description" content="<%= HTML.escape(video.short_description) %>">
<meta property="og:type" content="video.other">
<meta property="og:video:url" content="<%= HOST_URL %>/embed/<%= video.id %>">
<meta property="og:video:secure_url" content="<%= HOST_URL %>/embed/<%= video.id %>">
<meta property="og:video:type" content="text/html">
<meta property="og:video:width" content="1280">
<meta property="og:video:height" content="720">
<!-- This shouldn't be empty, ever. -->
<meta property="og:video" content="<%= video_url %>">
<meta property="og:video:url" content="<%= video_url %>">
<meta property="og:video:secure_url" content="<%= video_url %>">
<meta property="og:video:type" content="video/mp4">
<meta property="og:video:width" content="640">
<meta property="og:video:height" content="360">
<meta name="twitter:card" content="player">
<meta name="twitter:url" content="<%= HOST_URL %>/watch?v=<%= video.id %>">
<meta name="twitter:url" content="<%= host %>/watch?v=<%= video.id %>">
<meta name="twitter:title" content="<%= title %>">
<meta name="twitter:description" content="<%= HTML.escape(video.short_description) %>">
<meta name="twitter:image" content="<%= HOST_URL %>/vi/<%= video.id %>/maxres.jpg">
<meta name="twitter:player" content="<%= HOST_URL %>/embed/<%= video.id %>">
<meta name="twitter:image" content="<%= host %>/vi/<%= video.id %>/maxres.jpg">
<meta name="twitter:player" content="<%= host %>/embed/<%= video.id %>">
<meta name="twitter:player:width" content="1280">
<meta name="twitter:player:height" content="720">
<link rel="alternate" href="https://www.youtube.com/watch?v=<%= video.id %>">

View file

@ -1,3 +1,51 @@
# Mapping of subdomain => YoutubeConnectionPool
# This is needed as we may need to access arbitrary subdomains of ytimg
private YTIMG_POOLS = {} of String => YoutubeConnectionPool
struct YoutubeConnectionPool
property! url : URI
property! capacity : Int32
property! timeout : Float64
property pool : DB::Pool(HTTP::Client)
def initialize(url : URI, @capacity = 5, @timeout = 5.0)
@url = url
@pool = build_pool()
end
def client(&)
conn = pool.checkout
# Proxy needs to be reinstated every time we get a client from the pool
conn.proxy = make_configured_http_proxy_client() if CONFIG.http_proxy
begin
response = yield conn
rescue ex
conn.close
conn = make_client(url, force_resolve: true)
response = yield conn
ensure
pool.release(conn)
end
response
end
private def build_pool
options = DB::Pool::Options.new(
initial_pool_size: 0,
max_pool_size: capacity,
max_idle_pool_size: capacity,
checkout_timeout: timeout
)
DB::Pool(HTTP::Client).new(options) do
next make_client(url, force_resolve: true)
end
end
end
def add_yt_headers(request)
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"
@ -13,67 +61,56 @@ def add_yt_headers(request)
end
end
struct YoutubeConnectionPool
property! url : URI
property! capacity : Int32
property! timeout : Float64
property pool : DB::Pool(HTTP::Client)
def initialize(url : URI, @capacity = 5, @timeout = 5.0)
@url = url
@pool = build_pool()
end
def client(&)
conn = pool.checkout
begin
response = yield conn
rescue ex
conn.close
conn = HTTP::Client.new(url)
conn.family = CONFIG.force_resolve
conn.family = Socket::Family::INET if conn.family == Socket::Family::UNSPEC
conn.before_request { |r| add_yt_headers(r) } if url.host == "www.youtube.com"
response = yield conn
ensure
pool.release(conn)
end
response
end
private def build_pool
DB::Pool(HTTP::Client).new(initial_pool_size: 0, max_pool_size: capacity, max_idle_pool_size: capacity, checkout_timeout: timeout) do
conn = HTTP::Client.new(url)
conn.family = CONFIG.force_resolve
conn.family = Socket::Family::INET if conn.family == Socket::Family::UNSPEC
conn.before_request { |r| add_yt_headers(r) } if url.host == "www.youtube.com"
conn
end
end
end
def make_client(url : URI, region = nil, force_resolve : Bool = false)
def make_client(url : URI, region = nil, force_resolve : Bool = false, force_youtube_headers : Bool = false, use_http_proxy : Bool = true)
client = HTTP::Client.new(url)
client.proxy = make_configured_http_proxy_client() if CONFIG.http_proxy && use_http_proxy
# Force the usage of a specific configured IP Family
if force_resolve
client.family = CONFIG.force_resolve
client.family = Socket::Family::INET if client.family == Socket::Family::UNSPEC
end
client.before_request { |r| add_yt_headers(r) } if url.host == "www.youtube.com"
client.before_request { |r| add_yt_headers(r) } if url.host.try &.ends_with?("youtube.com") || force_youtube_headers
client.read_timeout = 10.seconds
client.connect_timeout = 10.seconds
return client
end
def make_client(url : URI, region = nil, force_resolve : Bool = false, &)
client = make_client(url, region, force_resolve)
def make_client(url : URI, region = nil, force_resolve : Bool = false, use_http_proxy : Bool = true, &)
client = make_client(url, region, force_resolve: force_resolve, use_http_proxy: use_http_proxy)
begin
yield client
ensure
client.close
end
end
def make_configured_http_proxy_client
# This method is only called when configuration for an HTTP proxy are set
config_proxy = CONFIG.http_proxy.not_nil!
return HTTP::Proxy::Client.new(
config_proxy.host,
config_proxy.port,
username: config_proxy.user,
password: config_proxy.password,
)
end
# Fetches a HTTP pool for the specified subdomain of ytimg.com
#
# Creates a new one when the specified pool for the subdomain does not exist
def get_ytimg_pool(subdomain)
if pool = YTIMG_POOLS[subdomain]?
return pool
else
LOGGER.info("ytimg_pool: Creating a new HTTP pool for \"https://#{subdomain}.ytimg.com\"")
pool = YoutubeConnectionPool.new(URI.parse("https://#{subdomain}.ytimg.com"), capacity: CONFIG.pool_size)
YTIMG_POOLS[subdomain] = pool
return pool
end
end

View file

@ -21,6 +21,7 @@ private ITEM_PARSERS = {
Parsers::ItemSectionRendererParser,
Parsers::ContinuationItemRendererParser,
Parsers::HashtagRendererParser,
Parsers::LockupViewModelParser,
}
private alias InitialData = Hash(String, JSON::Any)
@ -108,21 +109,30 @@ private module Parsers
length_seconds = 0
end
live_now = false
premium = false
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|
b = badge["metadataBadgeRenderer"]
case b["label"].as_s
when "LIVE NOW"
live_now = true
when "New", "4K", "CC"
# TODO
when "LIVE"
badges |= VideoBadges::LiveNow
when "New"
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"
# TODO: Potentially available as item_contents["topStandaloneBadge"]["metadataBadgeRenderer"]
premium = true
badges |= VideoBadges::Premium
else nil # Ignore
end
end
@ -136,10 +146,9 @@ private module Parsers
views: view_count,
description_html: description_html,
length_seconds: length_seconds,
live_now: live_now,
premium: premium,
premiere_timestamp: premiere_timestamp,
author_verified: author_verified,
badges: badges,
})
end
@ -459,9 +468,9 @@ private module Parsers
# Parses an InnerTube richItemRenderer into a SearchVideo.
# Returns nil when the given object isn't a RichItemRenderer
#
# A richItemRenderer seems to be a simple wrapper for a videoRenderer, used
# by the result page for hashtags and for the podcast tab on channels.
# It is located inside a continuationItems container for hashtags.
# A richItemRenderer seems to be a simple wrapper for a various other types,
# used on the hashtags result page and the channel podcast tab. It is located
# itself inside a richGridRenderer container.
#
module RichItemRendererParser
def self.process(item : JSON::Any, author_fallback : AuthorFallback)
@ -474,6 +483,8 @@ private module Parsers
child = VideoRendererParser.process(item_contents, author_fallback)
child ||= ReelItemRendererParser.process(item_contents, author_fallback)
child ||= PlaylistRendererParser.process(item_contents, author_fallback)
child ||= LockupViewModelParser.process(item_contents, author_fallback)
child ||= ShortsLockupViewModelParser.process(item_contents, author_fallback)
return child
end
@ -488,6 +499,9 @@ private module Parsers
# reelItemRenderer items are used in the new (2022) channel layout,
# in the "shorts" tab.
#
# NOTE: As of 10/2024, it might have been fully replaced by shortsLockupViewModel
# TODO: Confirm that hypothesis
#
module ReelItemRendererParser
def self.process(item : JSON::Any, author_fallback : AuthorFallback)
if item_contents = item["reelItemRenderer"]?
@ -563,10 +577,138 @@ private module Parsers
views: view_count,
description_html: "",
length_seconds: duration,
live_now: false,
premium: false,
premiere_timestamp: Time.unix(0),
author_verified: false,
badges: VideoBadges::None,
})
end
def self.parser_name
return {{@type.name}}
end
end
# Parses an InnerTube lockupViewModel into a SearchPlaylist.
# Returns nil when the given object is not a lockupViewModel.
#
# This structure is present since November 2024 on the "podcasts" and
# "playlists" tabs of the channel page. It is usually encapsulated in either
# a richItemRenderer or a richGridRenderer.
#
module LockupViewModelParser
def self.process(item : JSON::Any, author_fallback : AuthorFallback)
if item_contents = item["lockupViewModel"]?
return self.parse(item_contents, author_fallback)
end
end
private def self.parse(item_contents, author_fallback)
playlist_id = item_contents["contentId"].as_s
thumbnail_view_model = item_contents.dig(
"contentImage", "collectionThumbnailViewModel",
"primaryThumbnail", "thumbnailViewModel"
)
thumbnail = thumbnail_view_model.dig("image", "sources", 0, "url").as_s
# This complicated sequences tries to extract the following data structure:
# "overlays": [{
# "thumbnailOverlayBadgeViewModel": {
# "thumbnailBadges": [{
# "thumbnailBadgeViewModel": {
# "text": "430 episodes",
# "badgeStyle": "THUMBNAIL_OVERLAY_BADGE_STYLE_DEFAULT"
# }
# }]
# }
# }]
#
# NOTE: this simplistic `.to_i` conversion might not work on larger
# playlists and hasn't been tested.
video_count = thumbnail_view_model.dig("overlays").as_a
.compact_map(&.dig?("thumbnailOverlayBadgeViewModel", "thumbnailBadges").try &.as_a)
.flatten
.find(nil, &.dig?("thumbnailBadgeViewModel", "text").try { |node|
{"episodes", "videos"}.any? { |str| node.as_s.ends_with?(str) }
})
.try &.dig("thumbnailBadgeViewModel", "text").as_s.to_i(strict: false)
metadata = item_contents.dig("metadata", "lockupMetadataViewModel")
title = metadata.dig("title", "content").as_s
# TODO: Retrieve "updated" info from metadata parts
# rows = metadata.dig("metadata", "contentMetadataViewModel", "metadataRows").as_a
# parts_text = rows.map(&.dig?("metadataParts", "text", "content").try &.as_s)
# One of these parts should contain a string like: "Updated 2 days ago"
# TODO: Maybe add a button to access the first video of the playlist?
# item_contents.dig("rendererContext", "commandContext", "onTap", "innertubeCommand", "watchEndpoint")
# Available fields: "videoId", "playlistId", "params"
return SearchPlaylist.new({
title: title,
id: playlist_id,
author: author_fallback.name,
ucid: author_fallback.id,
video_count: video_count || -1,
videos: [] of SearchPlaylistVideo,
thumbnail: thumbnail,
author_verified: false,
})
end
def self.parser_name
return {{@type.name}}
end
end
# Parses an InnerTube shortsLockupViewModel into a SearchVideo.
# Returns nil when the given object is not a shortsLockupViewModel.
#
# This structure is present since around October 2024 on the "shorts" tab of
# the channel page and likely replaces the reelItemRenderer structure. It is
# usually (always?) encapsulated in a richItemRenderer.
#
module ShortsLockupViewModelParser
def self.process(item : JSON::Any, author_fallback : AuthorFallback)
if item_contents = item["shortsLockupViewModel"]?
return self.parse(item_contents, author_fallback)
end
end
private def self.parse(item_contents, author_fallback)
# TODO: Maybe add support for "oardefault.jpg" thumbnails?
# thumbnail = item_contents.dig("thumbnail", "sources", 0, "url").as_s
# Gives: https://i.ytimg.com/vi/{video_id}/oardefault.jpg?...
video_id = item_contents.dig(
"onTap", "innertubeCommand", "reelWatchEndpoint", "videoId"
).as_s
title = item_contents.dig("overlayMetadata", "primaryText", "content").as_s
view_count = short_text_to_number(
item_contents.dig("overlayMetadata", "secondaryText", "content").as_s
)
# Approximate to one minute, as "shorts" generally don't exceed that.
# NOTE: The actual duration is not provided by Youtube anymore.
# TODO: Maybe use -1 as an error value and handle that on the frontend?
duration = 60_i32
SearchVideo.new({
title: title,
id: video_id,
author: author_fallback.name,
ucid: author_fallback.id,
published: Time.unix(0),
views: view_count,
description_html: "",
length_seconds: duration,
premiere_timestamp: Time.unix(0),
author_verified: false,
badges: VideoBadges::None,
})
end

View file

@ -111,7 +111,7 @@ module UrlSanitizer
new_uri.path = "/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
end

View file

@ -455,7 +455,7 @@ module YoutubeAPI
video_id : String,
*, # Force the following parameters to be passed by name
params : String,
client_config : ClientConfig | Nil = nil
client_config : ClientConfig | Nil = nil,
)
# Playback context, separate because it can be different between clients
playback_ctx = {
@ -491,7 +491,11 @@ module YoutubeAPI
data["params"] = params
end
return self._post_json("/youtubei/v1/player", data, client_config)
if CONFIG.invidious_companion.present?
return self._post_invidious_companion("/youtubei/v1/player", data)
else
return self._post_json("/youtubei/v1/player", data, client_config)
end
end
####################################################################
@ -596,7 +600,7 @@ module YoutubeAPI
def _post_json(
endpoint : String,
data : Hash,
client_config : ClientConfig | Nil
client_config : ClientConfig | Nil,
) : Hash(String, JSON::Any)
# Use the default client config if nil is passed
client_config ||= DEFAULT_CLIENT_CONFIG
@ -628,6 +632,11 @@ module YoutubeAPI
# Send the POST request
body = YT_POOL.client() do |client|
client.post(url, headers: headers, body: data.to_json) do |response|
if response.status_code != 200
raise InfoException.new("Error: non 200 status code. Youtube API returned \
status code #{response.status_code}. See <a href=\"https://docs.invidious.io/youtube-errors-explained/\"> \
https://docs.invidious.io/youtube-errors-explained/</a> for troubleshooting.")
end
self._decompress(response.body_io, response.headers["Content-Encoding"]?)
end
end
@ -652,6 +661,51 @@ module YoutubeAPI
return initial_data
end
####################################################################
# _post_invidious_companion(endpoint, data)
#
# Internal function that does the actual request to Invidious companion
# and handles errors.
#
# The requested data is an endpoint (URL without the domain part)
# and the data as a Hash object.
#
def _post_invidious_companion(
endpoint : String,
data : Hash
) : Hash(String, JSON::Any)
headers = HTTP::Headers{
"Content-Type" => "application/json; charset=UTF-8",
"Authorization" => "Bearer #{CONFIG.invidious_companion_key}",
}
# Logging
LOGGER.debug("Invidious companion: Using endpoint: \"#{endpoint}\"")
LOGGER.trace("Invidious companion: POST data: #{data}")
# Send the POST request
begin
invidious_companion = CONFIG.invidious_companion.sample
response = make_client(invidious_companion.private_url, use_http_proxy: false,
&.post(endpoint, headers: headers, body: data.to_json))
body = response.body
if (response.status_code != 200)
raise Exception.new(
"Error while communicating with Invidious companion: \
status code: #{response.status_code} and body: #{body.dump}"
)
end
rescue ex
raise InfoException.new("Error while communicating with Invidious companion: " + (ex.message || "no extra info found"))
end
# Convert result to Hash
initial_data = JSON.parse(body).as_h
return initial_data
end
####################################################################
# _decompress(body_io, headers)
#