Compare commits

...
Sign in to create a new pull request.

62 commits

Author SHA1 Message Date
deb1187ea9
feat: option to increase the number of videos shown on the popular page
All checks were successful
Invidious CI / build (push) Successful in 6m50s
2025-04-19 20:56:14 -04:00
4a0e61812e
chore(locales): update spanish translations
All checks were successful
Invidious CI / build (push) Successful in 6m23s
2025-04-14 16:56:04 -04:00
ad95f0e2c0
feat(backends): redirect to another backend if one is unavailable
Some checks failed
Invidious CI / build (push) Has been cancelled
Only works with cookies for now. Support for numbered backends will be
added later since it requires some black magic.
2025-04-14 16:53:09 -04:00
e13800e859
fix: support for I2P backends! (again!)
All checks were successful
Invidious CI / build (push) Successful in 5m59s
2025-04-13 18:35:00 -04:00
69e351770d
chore(backends): change logic used to detect if the user accessed via a numbered backend or main domain
All checks were successful
Invidious CI / build (push) Successful in 6m4s
Numbered backends will only work if the domain has a number at the end, for example:

inv1.nadeko.net will redirect to backend 1
inv2.nadeko.net will redirect to backend 2
2inv.nadeko.net will use cookies
nadeko1.net will redirect to backend 1

The number of the backend in the domain should be always on the end of it to be able to use numbered backends.
2025-04-13 18:18:06 -04:00
3e33c9b70f
redis: update library and use the recently added #ping method
All checks were successful
Invidious CI / build (push) Successful in 6m45s
2025-04-13 15:53:54 -04:00
0ce17d91eb
chore(config): change server_id cookie name 2025-04-13 15:53:54 -04:00
c9eed028b0
ci: update to crystal 1.16.0 2025-04-13 15:53:42 -04:00
ff3d008a6f
ci: remove unused crystal spec from Dockerfile 2025-04-13 15:52:31 -04:00
49ae71a6ac
ci: only build production docker images 2025-04-13 15:52:08 -04:00
fac53ce721
ci: enable docker cache 2025-04-13 15:50:18 -04:00
b4e146fb60
redis: replace lib by jgaskins/redis
https://github.com/jgaskins/redis

It's faster and in active development, old one gave me this using a TCP
connection:

test   5.94k (168.34µs) (±56.73%)  144B/op  fastest
test   5.11k (195.76µs) (±58.89%)  144B/op  fastest
test   5.48k (182.33µs) (±73.39%)  144B/op  fastest
test   3.42k (292.56µs) (±66.19%)  144B/op  fastest

meanwhile, the jgaskins/redis one gives:

test   6.96k (143.66µs) (±58.73%)  96.0B/op  fastest
test   6.36k (157.16µs) (±55.95%)  96.0B/op  fastest
test   7.06k (141.65µs) (±57.03%)  96.0B/op  fastest
2025-04-13 02:30:37 -04:00
d7aeb1a89f
minify-js: add missing subscribe_widget.js script 2025-04-13 00:39:07 -04:00
7b072200f6
add support for i2p backends and onion numbered backends
All checks were successful
Invidious CI / build (push) Successful in 5m30s
2025-04-06 01:33:08 -04:00
bbec111997
feat(experimental): minify js files using esbuild
All checks were successful
Invidious CI / build (push) Successful in 5m37s
2025-04-05 03:03:04 -03:00
e3d60a0517
fix missing scheme on meta elements
move scheme logic
2025-04-05 03:03:00 -03:00
ce052103e7
deprecate support for external video playback proxy 2025-04-03 03:12:36 -03:00
c57a4f4920
add CSP based on backend selected by the user
xd
2025-04-03 03:12:36 -03:00
426e7bfbdb
use Host header on img-src 'self' data: CSP 2025-04-03 03:12:36 -03:00
3d85519ec9
Only show embed link on error pages if v query param is present 2025-04-03 03:12:36 -03:00
a74d89b6d9
Safely handle missing current_companion just in case 2025-04-03 03:12:36 -03:00
fd8c40e0da
fix: fix wrong invidious companion logic on backends
do not change to another companion if request fails
2025-04-03 03:12:36 -03:00
5f1944925b
remove unused Content-Security-Policy generated on every request to watch end embed
All checks were successful
Invidious CI / build (push) Successful in 4m48s
2025-04-01 18:51:13 -03:00
015c9ec5d1
support for numbered backends
All checks were successful
Invidious CI / build (push) Successful in 6m3s
2025-03-31 19:26:33 -03:00
be9a3794e9
cookies: remove port number from domain if it exists
All checks were successful
Invidious CI / build (push) Successful in 5m4s
2025-03-31 01:42:25 -03:00
642b2e8bf0
cookies: replace alternative domains and backend domains by Host header instead 2025-03-31 01:42:25 -03:00
ce97a41301
views/template.ecr: remove trailing | character on backend switcher 2025-03-31 01:42:25 -03:00
b29f5b39de
add note to backend 2025-03-31 00:04:52 -03:00
895745934b
generate CSP each time the backend checker runs instead of each request made to invidious 2025-03-31 00:03:40 -03:00
d47aa3dd6a
feat: do all the backend balancing on the invidious side
All checks were successful
Invidious CI / build (push) Successful in 5m11s
This will make invidious easier to maintain and escalate without the need of an overcomplicated reverse proxy configuration and multiple invidious instances with each one with a different configuration (in this case, invidious companion)
2025-03-30 20:08:15 -03:00
ddf6802d76
chore: add message if checkbackend job is disabled
Some checks failed
Invidious CI / build (push) Failing after 17s
2025-03-30 19:19:47 -03:00
626fb2d1a8
add option to disable livestreams since they don't work right now
All checks were successful
Invidious CI / build (push) Successful in 5m9s
2025-03-29 02:50:19 -03:00
56f309d6bb
Merge remote-tracking branch 'upstream/master' 2025-03-28 21:46:32 -03:00
Émilien (perso)
23ff6135bb
chore: enforce 16 characters for invidious_companion_key (#5220) 2025-03-26 15:27:59 +01:00
7c9f79e1f1 feat: add option to force proxying of videos
All checks were successful
Invidious CI / build (push) Successful in 5m20s
2025-03-24 21:15:38 -03:00
7aba1f7ba3
Revert "Merge branch 'add-prometheus-metrics-endpoint'"
All checks were successful
Invidious CI / build (push) Successful in 5m11s
This reverts commit e5c0f15398 due to
https://github.com/iv-org/invidious/pull/3576#issuecomment-2727898574
, reversing changes made to ecacbab2a5.
2025-03-16 23:51:16 -03:00
8be23eb01d
fix: fix checking logic on the backend checker 2025-03-16 23:30:27 -03:00
a44f37563b
chore: add check backends interval configuration option 2025-03-16 23:10:19 -03:00
facd01b52e
feat: add support for encrypted query parameters
Some checks are pending
Invidious CI / build (push) Waiting to run
Related:

- 6bd0f28d77
- 7eae31613e
2025-03-16 19:54:40 -03:00
syeopite
409d12a81e
Prepare for next release (#5206) 2025-03-16 01:03:01 +00:00
db53ee21ee
Merge remote-tracking branch 'upstream/master'
All checks were successful
Invidious CI / build (push) Successful in 5m17s
2025-03-14 12:26:53 -03:00
Émilien (perso)
70ff463cc6
Add invidious companion support (#4985)
* add support for invidious companion

* redirect latest_version and dash manifest to invidious companion

* fix Shadowing outer local variable `response`

* fixing condition for Content-Security-Policy

* throw error if inv_sig_helper and invidious_companion used same time

* Use sample instead of Random.rand

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

* Remove debug puts functions

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

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

* move config checks for invidious companion

* separate invidious_companion logic + better config.yaml config

* fixing "end" misplacement

* fix linting + use .empty?

* crystal handle decompression already by itself

* fix download function when invidious companion used

* fix linting

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

* apply all the suggestions + rework invidious_companion parameter

* format watch.cr

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

* add ability for invidious companion to check request from invidious

* Better document private_url and public_url

* Better doc for invidious_companion_key

* !empty? to present?

* skip proxy for invidious companion

* fixing format

* missing ,

* add companion pooling http

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

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

* chore: remove baseurl usage from invidious companion

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

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

---------

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

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

* Add link to documentation in warnings

* Direct users to /installation instead
2025-03-12 10:11:17 +01:00
a4cb5f094c
fix: add missing check for c.youtube.com host
All checks were successful
Invidious CI / build (push) Successful in 6m4s
2025-03-10 15:40:59 -03:00
01ccd55829
feat: add option to change username
All checks were successful
Invidious CI / build (push) Successful in 5m5s
fix: rename subscriptions materialized view of the user too

remove materialized views from username change

fix: downcase username and limit username lenght (from routes/login.cr)

Users that changed their username to something like `User`, were unable
to login because the username is downcased on routes/login.cr
2025-03-08 02:47:42 -03:00
8fe965419a
fix: use short_description as description if microformat is not available 2025-03-08 02:47:24 -03:00
fda823593e
fix: handle microformat as nil if is not present on innertube response.
All checks were successful
Invidious CI / build (push) Successful in 4m57s
Videos that return CONTENT_CHECK_REQUIRED do not include the
microformat JSON object literal on them. I think it's better to handle
microformat as nil instead of raising if microformat is not present.
2025-03-07 00:23:48 -03:00
24e66231df
chore: remove extra function call in check_backends
All checks were successful
Invidious CI / build (push) Successful in 5m9s
2025-03-06 20:24:07 -03:00
1001a72297
feat: show status of the instance with a colored dot
All checks were successful
Invidious CI / build (push) Successful in 5m9s
2025-03-02 16:35:44 -03:00
e5c0f15398
Merge branch 'add-prometheus-metrics-endpoint'
From https://github.com/iv-org/invidious/pull/3576
2025-03-01 03:39:21 -03:00
ecacbab2a5
update readme 2025-03-01 03:11:11 -03:00
bceb7a61ef
feat: Detect videoplayback proxy from invidious-companion and add it to the CSP header
All checks were successful
Invidious CI / build (push) Successful in 4m58s
2025-02-28 20:06:09 -03:00
syeopite
f3d982a885
Update Kemal to 1.6.0 and remove Kilt
Kilt is unmaintained and the ECR templating logic has been
natively integrated into Kemal with the issues previously seen
having been resolved.

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

See https://github.com/kemalcr/kemal/pull/688
2025-02-28 20:04:42 -03:00
27fecf3879
require base_job before the other jobs
The crystal compiler seems to evaluate `require` in an alphabetical way,
so if anyone in the future, wants to add another job and that job is
above `base_job.cr` in alphabetical order, the compiler is going to fail
with `Error: undefined constant: Invidious::Jobs::BaseJob`.

This doesn't fix anything, but it will prevent a future headache.
2025-02-28 20:04:28 -03:00
Mateusz Bączek
70754659e5 Move if CONFIG.statistics_enabled into the handler for the /metrics route 2024-01-23 21:30:31 +01:00
wint3rmute
8d4c16c79c Moved from misused constants to class variables in MetricsCollector, MetricsCollector is no longer initialized as a singleton 2024-01-23 21:30:31 +01:00
wint3rmute
9db6eb058c Fix formatting 2024-01-23 21:30:31 +01:00
wint3rmute
e7f7f39ce8 Return empty response on /api/v1/metrics endpoint if metrics are not enabled 2024-01-23 21:30:31 +01:00
wint3rmute
8456f8d4cd Move metrics handler registration together with the rest of handlers, into invidious.cr 2024-01-23 21:30:31 +01:00
wint3rmute
27dd94f60d Collecting num of requests and handling time from each Kemal route 2024-01-23 21:30:31 +01:00
wint3rmute
4d410d124f Add prometheus metrics at /api/v1/metrics 2024-01-23 21:28:46 +01:00
59 changed files with 1057 additions and 530 deletions

View file

@ -52,5 +52,5 @@ jobs:
tags: ${{ steps.meta.outputs.tags }} tags: ${{ steps.meta.outputs.tags }}
platforms: linux/amd64 platforms: linux/amd64
push: true push: true
build-args: | cache-from: type=gha
"release=1" cache-to: type=gha,mode=max

View file

@ -2,7 +2,102 @@
## vX.Y.0 (future) ## vX.Y.0 (future)
## v2.20250314.0
### Wrap-up
This release brings the long awaited feature of supporting multiple audio tracks in a video, some bug fixes and UX improvements, and many other things primarily oriented to self-hosting instances, and developers using the API.
The `Community` channel tab has been replaced by `Posts` in light of YouTube changes, but the URL remains the same.
Tamil is now available as an interface language
Automatic instance redirects will no longer have the chance to annoyingly redirect to the same instance you're on.
Due to their requirements for video playback, Invidious will log warning messages when either inv-sig-helper, `po_token` or `visitor_data` is not configured
Invidious is now able to listen through a UNIX socket
User notifications are now batched for each channel
**The minimum Crystal version supported by Invidious now `1.12.0`**
### New features & important changes
#### For users
* Invidious now supports videos with multiple audio tracks allowing you to select which one you want to hear with!
* Channel pages now have a proper previous page button
* RSS feeds for channels will no longer contain the channel's profile picture
* Support for channel `courses` page has been added
* `Community` tabs has been replaced with `Posts` to comply with YouTube changes
* Tamil is now an available interface language.
#### For instance owners
* Invidious is now able to listen on a UNIX socket
* User notifications are now batched by channels, significantly reducing database load.
* **`1.12.0` is now the oldest Crystal version that Invidious supports**
* The example config will no longer force an http proxy to be configured
* Invidious will now warn when any top-level config option must be set to a custom value, instead of just `HMAC_KEY`
* Due to their requirements for video playback, Invidious will log warning messages when either inv-sig-helper, `po_token` or `visitor_data` is not configured
#### For developers
* Invidious is now compliant to Crystal 1.15 formatting rules, which are incompatible with earlier versions.
* `/api/v1/transcripts/{id}` has been added to the API to allow for fetching the transcripts for a video. The arguments are the same as the captions endpoint.
* `author_thumbnail` field has been added to videos in the various paged api endpoints
* `published` field has been added to the API response for a video's related videos.
* Docker builds now uses the Crystal compiler cache, reducing build times on repeated builds significantly.
* Invidious ajax action handlers has undergone a clean up and may face compatibility issues with code that depends on these endpoints.
* The versions of Crystal that we test in CI/CD are now: `1.12.1`, `1.13.2`, `1.14.0`, `1.15.0`
### Bugs fixed
#### User-side
* Local video listen mode is now preserved when clicking on a video in the sidebar playlist widget
* Automatic instance redirects will no longer redirect to the same instance the user is on
* Fix some thumbnails responses returning 404
* Videos: Fix missing host parameter on playback URLs when `local=true`
* Fix HLS being used for non-livestream videos
* Fix timeupdate event errors when required elements are missing
* User: Ensure IO is properly closed when importing NewPipe subscriptions
#### For instance owners
* Fix http proxy configuration being forced by the standard example config
#### API
* `/api/v1/videos/{id}` will no longer return an occasional empty JSON response
### Full list of pull requests merged since the last release (newest first)
* Make Invidious compliant to Crystal 1.15 formatting rules (https://github.com/iv-org/invidious/pull/5014, by @syeopite)
* Remove formatter check on container workflows (https://github.com/iv-org/invidious/pull/5153, by @syeopite)
* Videos: Fix missing host parameter on playback URLs when `local=true` (https://github.com/iv-org/invidious/pull/4992, by @SamantazFox)
* Remove stdlib override for proxy initialization (https://github.com/iv-org/invidious/pull/5065, by @syeopite)
* Add support for author thumbnails in search api for videos (https://github.com/iv-org/invidious/pull/5072, thanks @ChunkyProgrammer)
* Skip route if resp got closed by before handlers (https://github.com/iv-org/invidious/pull/5073, by @syeopite)
* Fix video thumbnails in mixes (https://github.com/iv-org/invidious/pull/5116, thanks @iBicha)
* CI: Drop support for versions prior to 1.12 and add 1.15.0 (https://github.com/iv-org/invidious/pull/5148, by @syeopite)
* [Continuing #5094] Set language info for dash audio streams and sort (https://github.com/iv-org/invidious/pull/5149, thanks @giuliano-macedo)
* Warn when any top-level config is "CHANGE_ME!!" (https://github.com/iv-org/invidious/pull/5150, by @syeopite)
* Comment out http_proxy in example config (https://github.com/iv-org/invidious/pull/5151, by @syeopite)
* API: Add a 'published' video parameter for related videos (https://github.com/iv-org/invidious/pull/4149, thanks @RadoslavL)
* Ensure IO is properly closed when importing NewPipe subscriptions (https://github.com/iv-org/invidious/pull/4346, thanks @ChunkyProgrammer)
* Carry over audio-only mode in playlist links (https://github.com/iv-org/invidious/pull/4784, thanks @krystof1119)
* Routes: Clean ajax actions handlers (https://github.com/iv-org/invidious/pull/5036, by @SamantazFox)
* Frontend: Add a first page and previous page buttons for channel navigation (https://github.com/iv-org/invidious/pull/4123, thanks @RadoslavL)
* RSS: Channel + Playlist improvements (https://github.com/iv-org/invidious/pull/4298, thanks @ChunkyProgrammer)
* Batch user notifications together (https://github.com/iv-org/invidious/pull/4486, thanks @999eagle)
* JS: Update timeupdate event making it more defensive to prevent errors (https://github.com/iv-org/invidious/pull/4782, thanks @PMK)
* Add API endpoint for fetching transcripts from YouTube by (https://github.com/iv-org/invidious/pull/4788, by @syeopite)
* Translations update from Hosted Weblate by (https://github.com/iv-org/invidious/pull/4989, thanks to our many translators)
* Add the ability to listen on UNIX sockets (https://github.com/iv-org/invidious/pull/5112, thanks @Caian)
* Pick a different instance upon redirect (https://github.com/iv-org/invidious/pull/5154, thanks @epicsam123)
* Add Courses to channel page and channel API (https://github.com/iv-org/invidious/pull/5158, thanks @ChunkyProgrammer)
* fix /api/v1/videos/:id returns 200 with no content (https://github.com/iv-org/invidious/pull/5162, thanks @Drikanis)
* Use Crystal compiler cache in docker builds (https://github.com/iv-org/invidious/pull/5163, by @syeopite)
* Channels: Fix community tab by (https://github.com/iv-org/invidious/pull/5183, thanks @Fijxu)
* Fix typo in `src/invidious/routes/images.cr` (https://github.com/iv-org/invidious/pull/5184, by @syeopite)
* Fix an issue with the HLS manifest check for livestream videos (https://github.com/iv-org/invidious/pull/5189, thanks @alexmaras)
* Warn when `po_token`, `visitor_data` and/or `inv-sig-helper` is not configured (https://github.com/iv-org/invidious/pull/5202, by @syeopite)
## v2.20241110.0 ## v2.20241110.0
### Wrap-up ### Wrap-up

View file

@ -13,33 +13,49 @@ https://git.nadeko.net/Fijxu/-/packages/container/invidious/latest
## Features and changes of this fork: ## Features and changes of this fork:
- [Use a Redis compatible DB for video cache instead of just PostgreSQL](https://git.nadeko.net/Fijxu/invidious/commit/bbc5913b8dacaed4d466bcc466a0782d5e3f5edc): Invidious by default caches the video information for some hours in PostgreSQL. Since the data is accessed a lot, it is better off using an in memory database instead, it's faster and it will not wear out your SSD (due to constant writes to the database). - ~~[Use a Redis compatible DB for video cache instead of just PostgreSQL](https://git.nadeko.net/Fijxu/invidious/commit/bbc5913b8dacaed4d466bcc466a0782d5e3f5edc): Invidious by default caches the video information for some hours in PostgreSQL. Since the data is accessed a lot, it is better off using an in memory database instead, it's faster and it will not wear out your SSD (due to constant writes to the database).~~
It can be set using this on `config.yml`: ~~It can be set using this on `config.yml`:~~
```yaml ```yaml
redis_url: tcp://127.0.0.1:6379 redis_url: tcp://127.0.0.1:6379
``` ```
- [Ability to use different video caching backends](https://git.nadeko.net/Fijxu/invidious/commit/e76867aaba022d64ebab73648a37a0c63b788e0f): If you want, you can the PostgreSQL video cache the Redis one or the built-in in memory one that uses the LRU algorithm. Redis and LRU are recommended for public instances, but since Invidious has memory leaks, the LRU cache is lost if Invidious crashes or it's restarted, so because of this, redis is the default option.
```yaml
video_cache:
enabled: true
backend: 1 # 0 is PSQL, 1 Redis, 2 Built-in LRU
lru_max_size: 18000 # ~500MB (ignored if backend is 0 or 1)
```
If you choose to use Redis, make sure to set the `redis_url` config property:
```yaml
redis_url: tcp://127.0.0.1:6379
```
- [Removal of materialized views on PostgreSQL](github.com/iv-org/invidious/pull/2469): If you don't have this on your Invidious public instance, your SSD will suffer and it will catch on fire https://github.com/iv-org/invidious/pull/2469#issuecomment-2012623454 - [Removal of materialized views on PostgreSQL](github.com/iv-org/invidious/pull/2469): If you don't have this on your Invidious public instance, your SSD will suffer and it will catch on fire https://github.com/iv-org/invidious/pull/2469#issuecomment-2012623454
- External video playback proxy: Let's you use an external video playback proxy like https://git.nadeko.net/Fijxu/http3-ytproxy or https://github.com/TeamPiped/piped-proxy instead of the one that is bundled with Invidious. It's useful if you are proxying video and your throughput is not low. I did this to distribute the traffic across different servers. If you are selfhosting only for a few amount of people, this is not really useful for you.
It can be set using this on `config.yml`:
```yaml
external_videoplayback_proxy: "https://inv-proxy.example.com"
```
> [!NOTE]
> If you setup this, Invidious will check if the proxy is alive doing a request to `https://inv-proxy.example.com/health`, and if it doesn't get a response code of 200, Invidious will fallback to the local videoplayback proxy! This is only currently supported by https://git.nadeko.net/Fijxu/http3-ytproxy
- Limit the DASH resolution sent to the clients: It can be set using `max_dash_resolution` on the config. Example: `max_dash_resolution: 1080` - Limit the DASH resolution sent to the clients: It can be set using `max_dash_resolution` on the config. Example: `max_dash_resolution: 1080`
- [Limit requests made to Youtube API when pulling subscriptions (feeds)](https://git.nadeko.net/Fijxu/invidious/commit/df94f1c0b82d95846574487231ea251530838ef0): Due to the recent changes of Youtube ("This helps protect out community", "Sign in to confirm you are not a bot"), subscriptions now have limited information, this is because Invidious by default, makes a video request to youtube to be able to get more information about the video, like `length_seconds`, `live_now`, `premiere_timestamp`, and `views`. If you have a lot of users with a ton of subscriptions, Invidious will basically spam youtube API all the time, resulting in a block from youtube. - [Limit requests made to Youtube API when pulling subscriptions (feeds)](https://git.nadeko.net/Fijxu/invidious/commit/df94f1c0b82d95846574487231ea251530838ef0): Due to the recent changes of Youtube ("This helps protect out community", "Sign in to confirm you are not a bot"), subscriptions now have limited information, this is because Invidious by default, makes a video request to youtube to be able to get more information about the video, like `length_seconds`, `live_now`, `premiere_timestamp`, and `views`. If you have a lot of users with a ton of subscriptions, Invidious will basically spam youtube API all the time, resulting in a block from youtube.
It can be set using this on `config.yml`: It can be set using this on `config.yml`:
```yaml ```yaml
use_innertube_for_feeds: false use_innertube_for_feeds: false
``` ```
- Autoreload configuration: If you are hosting Invidious on Linux without docker, this may be useful for you if you want to change the banner without restarting Invidious.
```yaml
reload_config_automatically: true
```
## Development features
- Option to disable CSP: Useful for local development, set `csp: false` on the config and done
--- ---

1
assets/js/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
minified

View file

@ -1072,8 +1072,7 @@ default_user_preferences:
## ##
#extend_desc: false #extend_desc: false
# redis_url: 127.0.0.1:6379 # redis_url: redis://127.0.0.1:6379/0?initial_pool_size=1&max_pool_size=10&checkout_timeout=10&retry_attempts=2&retry_delay=0.5&max_idle_pool_size=50
# redis_socket: /var/run/valkey/valkey.sock
# donation_url: "https://example.com/donate" # donation_url: "https://example.com/donate"
# contact_url: "https://example.com/contact" # contact_url: "https://example.com/contact"
# home_domain: "https://example.com/ # home_domain: "https://example.com/

View file

@ -1,4 +1,4 @@
FROM mirror.gcr.io/crystallang/crystal:1.15.1-alpine AS builder FROM mirror.gcr.io/crystallang/crystal:1.16.0-alpine AS builder
RUN apk add --no-cache sqlite-static yaml-static RUN apk add --no-cache sqlite-static yaml-static
@ -7,6 +7,7 @@ ARG release
WORKDIR /invidious WORKDIR /invidious
COPY ./shard.yml ./shard.yml COPY ./shard.yml ./shard.yml
COPY ./shard.lock ./shard.lock COPY ./shard.lock ./shard.lock
RUN shards install --production RUN shards install --production
COPY ./src/ ./src/ COPY ./src/ ./src/
@ -19,18 +20,11 @@ COPY ./scripts/ ./scripts/
COPY ./assets/ ./assets/ COPY ./assets/ ./assets/
COPY ./videojs-dependencies.yml ./videojs-dependencies.yml COPY ./videojs-dependencies.yml ./videojs-dependencies.yml
RUN crystal spec --warnings all \ RUN --mount=type=cache,target=/root/.cache/crystal \
--link-flags "-lxml2 -llzma"
RUN --mount=type=cache,target=/root/.cache/crystal if [[ "${release}" == 1 ]] ; then \
crystal build ./src/invidious.cr \ crystal build ./src/invidious.cr \
--release --mcpu=x86-64-v2 \ --release --mcpu=x86-64-v2 \
--static --warnings all \ --static --warnings all \
--link-flags "-lxml2 -llzma"; \ --link-flags "-lxml2 -llzma";
else \
crystal build ./src/invidious.cr \
--static --warnings all \
--link-flags "-lxml2 -llzma"; \
fi
FROM mirror.gcr.io/alpine:3.20 FROM mirror.gcr.io/alpine:3.20
RUN apk add --no-cache rsvg-convert ttf-opensans tini tzdata RUN apk add --no-cache rsvg-convert ttf-opensans tini tzdata

View file

@ -522,6 +522,12 @@
"carousel_slide": "Slide {{current}} of {{total}}", "carousel_slide": "Slide {{current}} of {{total}}",
"carousel_skip": "Skip the Carousel", "carousel_skip": "Skip the Carousel",
"carousel_go_to": "Go to slide `x`", "carousel_go_to": "Go to slide `x`",
"footer_contact_url": "Contact the Administrator" "footer_contact_url": "Contact the Administrator",
"new_username": "New username",
"change_username": "Change username",
"username_required_field": "Username is a required field",
"username_empty": "Username cannot be empty",
"username_is_the_same": "This is your username, use another one",
"username_taken": "Username is already taken, use another one",
"backend_unavailable": "The backend you selected is unavailable. You have been redirected to the next one"
} }

View file

@ -518,5 +518,12 @@
"carousel_go_to": "Ir a la diapositiva `x`", "carousel_go_to": "Ir a la diapositiva `x`",
"footer_contact_url": "Contactar al Administrador", "footer_contact_url": "Contactar al Administrador",
"preferences_preload_label": "Precargar datos del vídeo: ", "preferences_preload_label": "Precargar datos del vídeo: ",
"Filipino (auto-generated)": "Filipino (generado automáticamente)" "Filipino (auto-generated)": "Filipino (generado automáticamente)",
"new_username": "Nuevo nombre de usuario",
"change_username": "Cambiar nombre de usuario",
"username_required_field": "El nombre de usuario es un campo obligatorio",
"username_empty": "El nombre de usuario no puede estar vacío",
"username_is_the_same": "Este es tu nombre de usuario, usa otro",
"username_taken": "El nombre de usuario ya está en uso, usa otro",
"backend_unavailable": "El backend seleccionado no está disponible. Has sido redireccionado al siguiente"
} }

97
scripts/minify-js.cr Executable file
View file

@ -0,0 +1,97 @@
require "http"
require "colorize"
ESBUILD_VERSION = "0.25.0"
esbuild_os = ""
esbuild_arch = ""
# https://esbuild.github.io/getting-started/#other-ways-to-install
{% if flag?(:linux) %}
esbuild_os = "linux"
{% elsif flag?(:windows) %}
esbuild_os = "win32"
{% elsif flag?(:darwin) %}
esbuild_os = "darwin"
{% elsif flag?(:freebsd) %}
esbuild_os = "freebsd"
{% elsif flag?(:openbsd) %}
esbuild_os = "openbsd"
{% elsif flag?(:netbsd) %}
esbuild_os = "netbsd"
{% elsif flag?(:solaris) %}
esbuild_os = "sunos"
{% else %}
esbuild_os = "linux"
{% end %}
{% if flag?(:x86_64) %}
esbuild_arch = "x64"
{% elsif flag?(:arm64) %}
esbuild_arch = "arm64"
{% else %}
esbuild_arch = "x64"
{% end %}
tmp_dir_path = "#{Dir.tempdir}/invidious-esbuild-binary"
esbuild_tar_location = "#{tmp_dir_path}/esbuild-#{esbuild_os}-#{esbuild_os}-#{ESBUILD_VERSION}.tgz"
esbuild_binary_location = "#{tmp_dir_path}/package/bin/esbuild"
Dir.mkdir(tmp_dir_path) if !Dir.exists? tmp_dir_path
esbuild_url = "https://registry.npmjs.org/@esbuild/#{esbuild_os}-#{esbuild_arch}/-/#{esbuild_os}-#{esbuild_arch}-#{ESBUILD_VERSION}.tgz"
# Download esbuild binary
HTTP::Client.get(esbuild_url) do |response|
puts "Downloading esbuild from #{esbuild_url}"
data = response.body_io.gets_to_end
File.write(esbuild_tar_location, data)
`tar -vzxf '#{esbuild_tar_location}' -C '#{tmp_dir_path}'`
raise "Extraction for #{esbuild_tar_location} failed" if !$?.success?
puts "esbuild downloaded successfully"
end
files_to_minify = [
"_helpers.js",
"comments.js",
"embed.js",
"handlers.js",
"notifications.js",
"pagination.js",
"playlist_widget.js",
"post.js",
"sse.js",
"subscribe_widget.js",
"themes.js",
"watch.js",
"player.js",
"watched_indicator.js",
"watched_widget.js",
]
files_to_minify.each do |file|
file_path = "assets/js/#{file}"
outdir = "assets/js/minified"
process_output = IO::Memory.new
process = Process.run("#{esbuild_binary_location}", error: process_output, args: [
file_path,
"--color=false",
"--sourcemap",
"--minify",
"--outdir=#{outdir}",
]
)
if process.success?
puts "Minified #{file}".colorize(:green)
elsif !process.success?
puts "Failed to minify #{file}, esbuild exited with exit code #{process.exit_code}: #{process_output.to_s.split("\n").first}".colorize(:red)
raise Exception.new("All files have to be minified sucessfully in order to compile!")
end
end
puts "Minify done!"
# Cleanup
`rm -rf #{tmp_dir_path}`

View file

@ -2,15 +2,15 @@ version: 2.0
shards: shards:
ameba: ameba:
git: https://github.com/crystal-ameba/ameba.git git: https://github.com/crystal-ameba/ameba.git
version: 1.6.1 version: 1.6.4
athena-negotiation: athena-negotiation:
git: https://github.com/athena-framework/negotiation.git git: https://github.com/athena-framework/negotiation.git
version: 0.1.1 version: 0.1.5
backtracer: backtracer:
git: https://github.com/sija/backtracer.cr.git git: https://github.com/sija/backtracer.cr.git
version: 1.2.2 version: 1.2.4
db: db:
git: https://github.com/crystal-lang/crystal-db.git git: https://github.com/crystal-lang/crystal-db.git
@ -18,31 +18,24 @@ shards:
exception_page: exception_page:
git: https://github.com/crystal-loot/exception_page.git git: https://github.com/crystal-loot/exception_page.git
version: 0.2.2 version: 0.4.1
inotify:
git: https://github.com/petoem/inotify.cr.git
version: 1.0.3
http_proxy: http_proxy:
git: https://github.com/mamantoha/http_proxy.git git: https://github.com/mamantoha/http_proxy.git
version: 0.10.3 version: 0.10.3
inotify:
git: https://github.com/petoem/inotify.cr.git
version: 1.0.3
kemal: kemal:
git: https://github.com/kemalcr/kemal.git git: https://github.com/kemalcr/kemal.git
version: 1.1.2 version: 1.6.0
kilt:
git: https://github.com/jeromegn/kilt.git
version: 0.6.1
pg: pg:
git: https://github.com/will/crystal-pg.git git: https://github.com/will/crystal-pg.git
version: 0.28.0 version: 0.28.0
pool:
git: https://github.com/ysbaddaden/pool.git
version: 0.2.4
protodec: protodec:
git: https://github.com/iv-org/protodec.git git: https://github.com/iv-org/protodec.git
version: 0.1.5 version: 0.1.5
@ -52,8 +45,8 @@ shards:
version: 0.4.1 version: 0.4.1
redis: redis:
git: https://github.com/stefanwille/crystal-redis.git git: https://github.com/jgaskins/redis.git
version: 2.9.1 version: 0.12.0
spectator: spectator:
git: https://github.com/icy-arctic-fox/spectator.git git: https://github.com/icy-arctic-fox/spectator.git

View file

@ -1,5 +1,5 @@
name: invidious name: invidious
version: 2.20241110.0-dev version: 2.20250314.0-dev
authors: authors:
- Invidious team <contact@invidious.io> - Invidious team <contact@invidious.io>
@ -17,10 +17,7 @@ dependencies:
version: ~> 0.21.0 version: ~> 0.21.0
kemal: kemal:
github: kemalcr/kemal github: kemalcr/kemal
version: ~> 1.1.2 version: ~> 1.6.0
kilt:
github: jeromegn/kilt
version: ~> 0.6.1
protodec: protodec:
github: iv-org/protodec github: iv-org/protodec
version: ~> 0.1.5 version: ~> 0.1.5
@ -28,7 +25,7 @@ dependencies:
github: athena-framework/negotiation github: athena-framework/negotiation
version: ~> 0.1.1 version: ~> 0.1.1
redis: redis:
github: stefanwille/crystal-redis github: jgaskins/redis
inotify: inotify:
github: petoem/inotify.cr github: petoem/inotify.cr
version: 1.0.3 version: 1.0.3

View file

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

View file

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

View file

@ -17,10 +17,8 @@
require "digest/md5" require "digest/md5"
require "file_utils" require "file_utils"
# Require kemal, kilt, then our own overrides # Require kemal, then our own overrides
require "kemal" require "kemal"
require "kilt"
require "./ext/kemal_content_for.cr"
require "./ext/kemal_static_file_handler.cr" require "./ext/kemal_static_file_handler.cr"
require "http_proxy" require "http_proxy"
@ -51,7 +49,8 @@ require "./invidious/channels/*"
require "./invidious/user/*" require "./invidious/user/*"
require "./invidious/search/*" require "./invidious/search/*"
require "./invidious/routes/**" require "./invidious/routes/**"
require "./invidious/jobs/**" require "./invidious/jobs/base_job"
require "./invidious/jobs/*"
# Declare the base namespace for invidious # Declare the base namespace for invidious
module Invidious module Invidious
@ -114,9 +113,11 @@ YT_POOL = YoutubeConnectionPool.new(YT_URL, capacity: CONFIG.pool_size)
GGPHT_POOL = YoutubeConnectionPool.new(URI.parse("https://yt3.ggpht.com"), capacity: CONFIG.pool_size) GGPHT_POOL = YoutubeConnectionPool.new(URI.parse("https://yt3.ggpht.com"), capacity: CONFIG.pool_size)
COMPANION_POOL = CompanionConnectionPool.new( COMPANION_POOL = [] of CompanionConnectionPool
capacity: CONFIG.pool_size
) CONFIG.invidious_companion.each do |companion|
COMPANION_POOL << CompanionConnectionPool.new(companion, capacity: CONFIG.pool_size)
end
# CLI # CLI
Kemal.config.extra_options do |parser| Kemal.config.extra_options do |parser|
@ -159,6 +160,15 @@ LOGGER = Invidious::LogHandler.new(OUTPUT, CONFIG.log_level, CONFIG.colorize_log
# Check table integrity # Check table integrity
Invidious::Database.check_integrity(CONFIG) Invidious::Database.check_integrity(CONFIG)
# Minifies Invidious Javascript
{% if flag?(:minify_debug) || (flag?(:release) || flag?(:production)) && !flag?(:skip_minified_js) %}
{% puts "\nMinifying Invidious JavaScript\n" %}
{% puts run("../scripts/minify-js.cr").stringify %}
JS_PATH="js/minified"
{% else %}
JS_PATH="js"
{% end %}
{% if !flag?(:skip_videojs_download) %} {% if !flag?(:skip_videojs_download) %}
# Resolve player dependencies. This is done at compile time. # Resolve player dependencies. This is done at compile time.
# #
@ -209,20 +219,18 @@ Invidious::Jobs.register Invidious::Jobs::ClearExpiredItemsJob.new
Invidious::Jobs.register Invidious::Jobs::InstanceListRefreshJob.new Invidious::Jobs.register Invidious::Jobs::InstanceListRefreshJob.new
if !CONFIG.external_videoplayback_proxy.empty?
Invidious::Jobs.register Invidious::Jobs::CheckExternalProxy.new
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? if !CONFIG.tokens_server.empty?
Invidious::Jobs.register Invidious::Jobs::RefreshSessionTokens.new Invidious::Jobs.register Invidious::Jobs::RefreshSessionTokens.new
else else
LOGGER.info("jobs: Disabling RefreshSessionTokens job. Invidious will use the tokens that are on the configuration file") LOGGER.info("jobs: Disabling RefreshSessionTokens job. Invidious will use the tokens that are on the configuration file")
end end
if CONFIG.invidious_companion.present?
Invidious::Jobs.register Invidious::Jobs::CheckBackend.new
else
LOGGER.info("jobs: Disabling CheckBackend job. invidious-companion and their respective external video playback proxies (if set on invidious-companion) will not be checked")
end
Invidious::Jobs.start_all Invidious::Jobs.start_all
def popular_videos def popular_videos
@ -245,8 +253,8 @@ error 500 do |env, ex|
error_template(500, ex) error_template(500, ex)
end end
static_headers do |response| static_headers do |env|
response.headers.add("Cache-Control", "max-age=2629800") env.response.headers.add("Cache-Control", "max-age=2629800")
end end
# Init Kemal # Init Kemal

View file

@ -52,6 +52,7 @@ struct ConfigPreferences
property vr_mode : Bool = true property vr_mode : Bool = true
property show_nick : Bool = true property show_nick : Bool = true
property save_player_pos : Bool = false property save_player_pos : Bool = false
property enable_dearrow : Bool = false
def to_tuple def to_tuple
{% begin %} {% begin %}
@ -82,6 +83,12 @@ class Config
@[YAML::Field(converter: Preferences::URIConverter)] @[YAML::Field(converter: Preferences::URIConverter)]
property public_url : URI = URI.parse("") property public_url : URI = URI.parse("")
@[YAML::Field(converter: Preferences::URIConverter)]
property i2p_public_url : URI = URI.parse("")
property note : String = ""
property domain : Array(String) = [] of String
end end
# Number of threads to use for crawling videos from channels (for updating subscriptions) # Number of threads to use for crawling videos from channels (for updating subscriptions)
@ -101,8 +108,8 @@ class Config
# Database configuration using 12-Factor "Database URL" syntax # Database configuration using 12-Factor "Database URL" syntax
@[YAML::Field(converter: Preferences::URIConverter)] @[YAML::Field(converter: Preferences::URIConverter)]
property database_url : URI = URI.parse("") property database_url : URI = URI.parse("")
property redis_url : String? @[YAML::Field(converter: Preferences::URIConverter)]
property redis_socket : String? property redis_url : URI = URI.parse("")
# Use polling to keep decryption function up to date # Use polling to keep decryption function up to date
property decrypt_polling : Bool = false property decrypt_polling : Bool = false
# Used for crawling channels: threads should check all videos uploaded by a channel # Used for crawling channels: threads should check all videos uploaded by a channel
@ -121,10 +128,6 @@ class Config
property domain : String? property domain : String?
# Materialious redirects # Materialious redirects
property materialious_domain : String? property materialious_domain : String?
# Alternative domains. You can add other domains, like TOR and I2P addresses
property alternative_domains : Array(String) = [] of String
# Backend domains. Domains for numbered backends
property backend_domains : Array(String) = [] of String
# Subscribe to channels using PubSubHubbub (requires domain, hmac_key) # Subscribe to channels using PubSubHubbub (requires domain, hmac_key)
property use_pubsub_feeds : Bool | Int32 = false property use_pubsub_feeds : Bool | Int32 = false
@ -216,13 +219,9 @@ class Config
# of the backend # of the backend
property backends_delimiter : String = "|" 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 pubsub_domain : String = ""
property server_id_cookie_name : String = "INVIDIOUS_SERVER_ID" property server_id_cookie_name : String = "COMPANION_ID"
property tokens_server : String = "" property tokens_server : String = ""
@ -237,6 +236,14 @@ class Config
property lru_max_size : Int32 = 18432 # ~512MB property lru_max_size : Int32 = 18432 # ~512MB
end end
property check_backends_interval : Int32 = 30
property force_local : Bool = true
property disable_livestreams : Bool = true
property max_popuplar_results : Int32 = 40
{% if flag?(:linux) %} {% if flag?(:linux) %}
property reload_config_automatically : Bool = true property reload_config_automatically : Bool = true
{% end %} {% end %}
@ -383,10 +390,14 @@ class Config
elsif config.invidious_companion_key == "CHANGE_ME!!" elsif config.invidious_companion_key == "CHANGE_ME!!"
puts "Config: The value of 'invidious_companion_key' needs to be changed!!" puts "Config: The value of 'invidious_companion_key' needs to be changed!!"
exit(1) exit(1)
elsif config.invidious_companion_key.size < 16 elsif config.invidious_companion_key.size != 16
puts "Config: The value of 'invidious_companion_key' needs to be a size of 16 or more." puts "Config: The value of 'invidious_companion_key' needs to be a size of 16 characters."
exit(1) exit(1)
end end
elsif config.signature_server
puts("WARNING: inv-sig-helper is deprecated. Please switch to Invidious companion: https://docs.invidious.io/companion-installation/")
else
puts("WARNING: Invidious companion is required to view and playback videos. For more information see https://docs.invidious.io/companion-installation/")
end end
# HMAC_key is mandatory # HMAC_key is mandatory

View file

@ -149,7 +149,7 @@ module Invidious::Database::ChannelVideos
SELECT DISTINCT ON (ucid) * SELECT DISTINCT ON (ucid) *
FROM channel_videos FROM channel_videos
WHERE ucid IN (SELECT channel FROM (SELECT UNNEST(subscriptions) AS channel FROM users) AS d WHERE ucid IN (SELECT channel FROM (SELECT UNNEST(subscriptions) AS channel FROM users) AS d
GROUP BY channel ORDER BY COUNT(channel) DESC LIMIT 40) GROUP BY channel ORDER BY COUNT(channel) DESC LIMIT #{CONFIG.max_popuplar_results})
ORDER BY ucid, published DESC ORDER BY ucid, published DESC
SQL SQL

View file

@ -184,6 +184,36 @@ module Invidious::Database::Users
PG_DB.exec(request, pass, user.email) PG_DB.exec(request, pass, user.email)
end end
def update_username(user : User, username : String)
request = <<-SQL
UPDATE users
SET email = $1
WHERE email = $2
SQL
PG_DB.exec(request, username, user.email)
end
def update_user_session_id(user : User, username : String)
request = <<-SQL
UPDATE session_ids
SET email = $1
WHERE email = $2
SQL
PG_DB.exec(request, username, user.email)
end
def update_user_playlists_author(user : User, username : String)
request = <<-SQL
UPDATE playlists
SET author = $1
WHERE author = $2
SQL
PG_DB.exec(request, username, user.email)
end
# ------------------- # -------------------
# Select # Select
# ------------------- # -------------------

View file

@ -97,21 +97,20 @@ module Invidious::Database::Videos
end end
class Redis_ class Redis_
@redis : Redis::PooledClient @redis : Redis::Client
def initialize def initialize
@redis = Redis::PooledClient.new(unixsocket: CONFIG.redis_socket || nil, url: CONFIG.redis_url || nil) @redis = Redis::Client.new(CONFIG.redis_url)
LOGGER.info "Video Cache: Using Redis compatible DB to store video cache" LOGGER.info "Video Cache: Using Redis compatible DB to store video cache"
LOGGER.info "Connecting to Redis compatible DB" LOGGER.info "Connecting to Redis compatible DB"
if @redis.ping if @redis.ping
LOGGER.info "Connected to Redis compatible DB via unix domain socket at '#{CONFIG.redis_socket}'" if CONFIG.redis_socket LOGGER.info "Connected to Redis compatible DB at '#{CONFIG.redis_url}'" if CONFIG.redis_url
LOGGER.info "Connected to Redis compatible DB via TCP socket at '#{CONFIG.redis_url}'" if CONFIG.redis_url
end end
end end
def set(video : Video, expire_time) def set(video : Video, expire_time)
@redis.set(video.id, video.info.to_json, expire_time) @redis.set(video.id, video.info.to_json, ex: expire_time)
@redis.set(video.id + ":time", video.updated, expire_time) @redis.set(video.id + ":time", video.updated.to_s, ex: expire_time)
end end
def del(id : String) def del(id : String)

View file

@ -0,0 +1,85 @@
module BackendInfo
extend self
@@exvpp_url : Array(String) = Array.new(CONFIG.invidious_companion.size, "")
@@status : Array(Int32) = Array.new(CONFIG.invidious_companion.size, 0)
@@csp : Array(String) = Array.new(CONFIG.invidious_companion.size, "")
@@mutex : Mutex = Mutex.new
def check_backends
check_companion()
end
private def check_companion
CONFIG.invidious_companion.each_with_index do |companion, index|
spawn do
begin
response = HTTP::Client.get "#{companion.private_url}/healthz"
if response.status_code == 200
check_videoplayback_proxy(companion, index)
generate_csp([companion.public_url.to_s, companion.i2p_public_url.to_s], @@exvpp_url[index], index)
else
@@status[index] = 0
end
rescue
@@status[index] = 0
end
end
end
end
private def check_videoplayback_proxy(companion : Config::CompanionConfig, index : Int32)
begin
info = HTTP::Client.get "#{companion.private_url}/info"
exvpp_url = JSON.parse(info.body)["external_videoplayback_proxy"]?.try &.to_s
rescue JSON::ParseException
@@status[index] = 2
return
end
exvpp_url = "" if exvpp_url.nil?
@@exvpp_url[index] = exvpp_url
if exvpp_url.empty?
@@status[index] = 2
return
else
begin
exvpp_health = HTTP::Client.get "#{exvpp_url}/health"
if exvpp_health.status_code == 200
@@status[index] = 2
return exvpp_url
else
@@status[index] = 1
end
rescue
@@status[index] = 1
end
end
end
private def generate_csp(companion_url : Array(String), exvpp_url : String? = nil, index : Int32? = nil)
@@mutex.synchronize do
@@csp[index] = ""
companion_url.each do |url|
@@csp[index] += " #{url}"
end
@@csp[index] += " #{exvpp_url}"
end
end
def get_status
return @@status
end
def get_exvpp
return @@exvpp_url
end
def get_csp(index : Int32)
# A little mutex to prevent sending a partial CSP header
# Not sure if this is necessary. But if the @@csp[index] is being assigned
# at the same time when it's being accessed, a data race will appear
@@mutex.synchronize do
return @@csp[index], @@csp[index]
end
end
end

View file

@ -183,6 +183,8 @@ def error_redirect_helper(env : HTTP::Server::Context)
go_to_youtube_embed = translate(locale, "videoinfo_youTube_embed_link") go_to_youtube_embed = translate(locale, "videoinfo_youTube_embed_link")
switch_instance = translate(locale, "Switch Invidious Instance") switch_instance = translate(locale, "Switch Invidious Instance")
show_embed_link = "(<a rel=\"noreferrer noopener\" href=\"https://youtube.com/embed/#{env.params.query["v"]}\">#{go_to_youtube_embed}</a>)" if env.params.query["v"]?
return <<-END_HTML return <<-END_HTML
<p style="margin-bottom: 4px;">#{next_steps_text}</p> <p style="margin-bottom: 4px;">#{next_steps_text}</p>
<ul> <ul>
@ -194,7 +196,7 @@ def error_redirect_helper(env : HTTP::Server::Context)
</li> </li>
<li> <li>
<a rel="noreferrer noopener" href="https://youtube.com#{env.request.resource}">#{go_to_youtube}</a> <a rel="noreferrer noopener" href="https://youtube.com#{env.request.resource}">#{go_to_youtube}</a>
(<a rel="noreferrer noopener" href="https://youtube.com/embed/#{env.params.query["v"]}">#{go_to_youtube_embed}</a>) #{show_embed_link}
</li> </li>
</ul> </ul>
END_HTML END_HTML

View file

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

View file

@ -384,6 +384,29 @@ def parse_link_endpoint(endpoint : JSON::Any, text : String, video_id : String)
return text return text
end end
def decrypt_ecb_without_salt(data, key)
cipher = OpenSSL::Cipher.new("aes-128-ecb")
cipher.decrypt
cipher.key = key
cipher.padding = false
io = IO::Memory.new
io.write(cipher.update(data))
io.write(cipher.final)
io.rewind
data_ = io.to_s
padding = data_[-1].ord
return data_[0...(data_.bytesize - padding)]
end
def video_playback_decrypt(data)
data = Base64.decode(data)
decrypted_query = decrypt_ecb_without_salt(data, CONFIG.invidious_companion_key)
return decrypted_query
end
def encrypt_ecb_without_salt(data, key) def encrypt_ecb_without_salt(data, key)
cipher = OpenSSL::Cipher.new("aes-128-ecb") cipher = OpenSSL::Cipher.new("aes-128-ecb")
cipher.encrypt cipher.encrypt

View file

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

View file

@ -0,0 +1,13 @@
class Invidious::Jobs::CheckBackend < Invidious::Jobs::BaseJob
def initialize
end
def begin
loop do
BackendInfo.check_backends
LOGGER.info("Backend Checker: Done, sleeping for #{CONFIG.check_backends_interval} seconds")
sleep CONFIG.check_backends_interval.seconds
Fiber.yield
end
end
end

View file

@ -1,13 +0,0 @@
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

@ -78,6 +78,75 @@ module Invidious::Routes::Account
env.redirect referer env.redirect referer
end end
# -------------------
# Username update
# -------------------
# Show the username change interface (GET request)
def get_change_username(env)
locale = env.get("preferences").as(Preferences).locale
user = env.get? "user"
sid = env.get? "sid"
referer = get_referer(env)
if !user
return env.redirect referer
end
user = user.as(User)
sid = sid.as(String)
csrf_token = generate_response(sid, {":change_username"}, HMAC_KEY)
templated "user/change_username"
end
# Handle the username change (POST request)
def post_change_username(env)
locale = env.get("preferences").as(Preferences).locale
user = env.get? "user"
sid = env.get? "sid"
referer = get_referer(env)
if !user
return env.redirect referer
end
user = user.as(User)
sid = sid.as(String)
token = env.params.body["csrf_token"]?
begin
validate_request(token, sid, env.request, HMAC_KEY, locale)
rescue ex
return error_template(400, ex)
end
new_username = env.params.body["new_username"]?.try &.downcase.byte_slice(0, 254)
if new_username.nil?
return error_template(401, "username_required_field")
end
if new_username.empty?
return error_template(401, "username_empty")
end
if new_username == user.email
return error_template(401, "username_is_the_same")
end
if Invidious::Database::Users.select(email: new_username)
return error_template(401, "username_taken")
end
Invidious::Database::Users.update_username(user, new_username.to_s)
Invidious::Database::Users.update_user_session_id(user, new_username.to_s)
Invidious::Database::Users.update_user_playlists_author(user, new_username.to_s)
env.redirect referer
end
# ------------------- # -------------------
# Account deletion # Account deletion
# ------------------- # -------------------

View file

@ -9,8 +9,8 @@ module Invidious::Routes::API::Manifest
region = env.params.query["region"]? region = env.params.query["region"]?
if CONFIG.invidious_companion.present? if CONFIG.invidious_companion.present?
invidious_companion = CONFIG.invidious_companion.sample companion_public_url = env.get("companion_public_url").as(String)
return env.redirect "#{invidious_companion.public_url}/api/manifest/dash/id/#{id}?#{env.params.query}" return env.redirect "#{companion_public_url}/api/manifest/dash/id/#{id}?#{env.params.query}"
end end
# Since some implementations create playlists based on resolution regardless of different codecs, # Since some implementations create playlists based on resolution regardless of different codecs,
@ -221,21 +221,15 @@ module Invidious::Routes::API::Manifest
raw_params["host"] = uri.host.not_nil! raw_params["host"] = uri.host.not_nil!
proxy = Invidious::HttpServer::Utils.get_external_proxy
if CONFIG.https_only if CONFIG.https_only
scheme = "https://" scheme = "https://"
else else
scheme = "http://" scheme = "http://"
end end
if !proxy.empty?
"#{proxy}/videoplayback?#{raw_params}"
else
"#{scheme}#{env.request.headers["Host"]}/videoplayback?#{raw_params}" "#{scheme}#{env.request.headers["Host"]}/videoplayback?#{raw_params}"
end end
end end
end
manifest manifest
end end

View file

@ -0,0 +1,16 @@
{% skip_file if flag?(:api_only) %}
module Invidious::Routes::BackendSwitcher
def self.switch(env)
referer = get_referer(env)
backend_id = env.params.query["backend_id"]?.try &.to_i
if backend_id.nil?
return error_template(400, "Backend ID is required")
end
env.response.cookies[CONFIG.server_id_cookie_name] = Invidious::User::Cookies.server_id(env.request.headers["Host"], backend_id)
env.redirect referer
end
end

View file

@ -1,6 +1,7 @@
module Invidious::Routes::BeforeAll module Invidious::Routes::BeforeAll
def self.handle(env) def self.handle(env)
preferences = Preferences.from_json("{}") preferences = Preferences.from_json("{}")
host = env.request.headers["Host"]
begin begin
if prefs_cookie = env.request.cookies["PREFS"]? if prefs_cookie = env.request.cookies["PREFS"]?
@ -24,15 +25,50 @@ module Invidious::Routes::BeforeAll
extra_connect_csp = "" extra_connect_csp = ""
if CONFIG.invidious_companion.present? if CONFIG.invidious_companion.present?
extra_media_csp = " #{CONFIG.invidious_companion.sample.public_url}" current_companion_d = host.split(".")[0].scan(/(\d+)$/).last?.try &.[0].to_i
extra_connect_csp = " #{CONFIG.invidious_companion.sample.public_url}"
if current_companion_d
current_companion_d = current_companion_d - 1
env.set "using_domain", true
env.set "current_companion", current_companion_d
env.set "companion_public_url", CONFIG.invidious_companion[current_companion_d].public_url.to_s
else
if !env.request.cookies[CONFIG.server_id_cookie_name]?
env.response.cookies[CONFIG.server_id_cookie_name] = Invidious::User::Cookies.server_id(host)
end end
if !CONFIG.external_videoplayback_proxy.empty? begin
CONFIG.external_videoplayback_proxy.each do |proxy| current_companion = env.request.cookies[CONFIG.server_id_cookie_name].value.try &.to_i
extra_media_csp += " #{proxy}" rescue
extra_connect_csp += " #{proxy}" current_companion = rand(CONFIG.invidious_companion.size)
end end
if current_companion > CONFIG.invidious_companion.size
current_companion = current_companion % CONFIG.invidious_companion.size
env.response.cookies[CONFIG.server_id_cookie_name] = Invidious::User::Cookies.server_id(host, current_companion)
end
companion_status = BackendInfo.get_status
if companion_status[current_companion] != 2
alive_companion = companion_status.index(2)
if alive_companion
env.set "companion_switched", true
current_companion = alive_companion
env.response.cookies[CONFIG.server_id_cookie_name] = Invidious::User::Cookies.server_id(host, current_companion)
end
end
env.set "current_companion", current_companion
if host.split(".").last == "i2p"
env.set "companion_public_url", CONFIG.invidious_companion[current_companion].i2p_public_url.to_s
else
env.set "companion_public_url", CONFIG.invidious_companion[current_companion].public_url.to_s
end
end
extra_media_csp, extra_connect_csp = BackendInfo.get_csp(env.get("current_companion").as(Int32))
end end
# Allow media resources to be loaded from google servers # Allow media resources to be loaded from google servers
@ -48,13 +84,16 @@ module Invidious::Routes::BeforeAll
frame_ancestors = "'none'" frame_ancestors = "'none'"
end end
scheme = env.request.headers["X-Forwarded-Proto"]? || ("https" if CONFIG.https_only) || "http"
env.set "scheme", scheme
# TODO: Remove style-src's 'unsafe-inline', requires to remove all # TODO: Remove style-src's 'unsafe-inline', requires to remove all
# inline styles (<style> [..] </style>, style=" [..] ") # inline styles (<style> [..] </style>, style=" [..] ")
env.response.headers["Content-Security-Policy"] = { env.response.headers["Content-Security-Policy"] = {
"default-src 'none'", "default-src 'none'",
"script-src 'self'", "script-src 'self'",
"style-src 'self' 'unsafe-inline'", "style-src 'self' 'unsafe-inline'",
"img-src 'self' data: " + HOST_URL, "img-src 'self' data: " + "#{scheme}://#{env.request.headers["Host"]?}",
"font-src 'self' data:", "font-src 'self' data:",
"connect-src 'self'" + extra_connect_csp, "connect-src 'self'" + extra_connect_csp,
"manifest-src 'self'", "manifest-src 'self'",

View file

@ -203,6 +203,11 @@ module Invidious::Routes::Embed
return env.redirect url return env.redirect url
end end
if CONFIG.invidious_companion.present?
current_companion = env.get("current_companion").as(Int32)
invidious_companion = CONFIG.invidious_companion[current_companion]
end
rendered "embed" rendered "embed"
end end
end end

View file

@ -60,15 +60,7 @@ module Invidious::Routes::Login
sid = Base64.urlsafe_encode(Random::Secure.random_bytes(32)) sid = Base64.urlsafe_encode(Random::Secure.random_bytes(32))
Invidious::Database::SessionIDs.insert(sid, email) Invidious::Database::SessionIDs.insert(sid, email)
# Checks if there is any alternative domain, like a second domain name, env.response.cookies["SID"] = Invidious::User::Cookies.sid(env.request.headers["Host"], sid)
# 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
else else
return error_template(401, "Wrong username or password") return error_template(401, "Wrong username or password")
end end
@ -168,15 +160,7 @@ module Invidious::Routes::Login
Invidious::Database::Users.insert(user) Invidious::Database::Users.insert(user)
Invidious::Database::SessionIDs.insert(sid, email) Invidious::Database::SessionIDs.insert(sid, email)
# Checks if there is any alternative domain, like a second domain name, env.response.cookies["SID"] = Invidious::User::Cookies.sid(env.request.headers["Host"], sid)
# 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
if env.request.cookies["PREFS"]? if env.request.cookies["PREFS"]?
user.preferences = env.get("preferences").as(Preferences) user.preferences = env.get("preferences").as(Preferences)

View file

@ -224,15 +224,7 @@ module Invidious::Routes::PreferencesRoute
File.write("config/config.yml", CONFIG.to_yaml) File.write("config/config.yml", CONFIG.to_yaml)
end end
else else
# Checks if there is any alternative domain, like a second domain name, env.response.cookies["PREFS"] = Invidious::User::Cookies.prefs(env.request.headers["Host"], preferences)
# 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
end end
env.redirect referer env.redirect referer
@ -267,15 +259,7 @@ module Invidious::Routes::PreferencesRoute
preferences.dark_mode = "dark" preferences.dark_mode = "dark"
end end
# Checks if there is any alternative domain, like a second domain name, env.response.cookies["PREFS"] = Invidious::User::Cookies.prefs(env.request.headers["Host"], preferences)
# 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
end end
if redirect if redirect

View file

@ -3,6 +3,11 @@ module Invidious::Routes::VideoPlayback
def self.get_video_playback(env) def self.get_video_playback(env)
locale = env.get("preferences").as(Preferences).locale locale = env.get("preferences").as(Preferences).locale
query_params = env.params.query query_params = env.params.query
if query_params["enc"]? == "yes"
query_params = URI::Params.parse(video_playback_decrypt(query_params["data"]))
end
array = UInt8[0x78, 0] array = UInt8[0x78, 0]
protobuf = Bytes.new(array.size) protobuf = Bytes.new(array.size)
array.each_with_index do |byte, index| array.each_with_index do |byte, index|
@ -26,7 +31,7 @@ module Invidious::Routes::VideoPlayback
end end
# Sanity check, to avoid being used as an open proxy # Sanity check, to avoid being used as an open proxy
if !host.matches?(/[\w-]+.googlevideo.com/) if !host.matches?(/[\w-]+.googlevideo.com/) && !host.matches?(/[\w-]+.c.youtube.com/)
return error_template(400, "Invalid \"host\" parameter.") return error_template(400, "Invalid \"host\" parameter.")
end end
@ -262,8 +267,8 @@ module Invidious::Routes::VideoPlayback
# so we have a mechanism here to redirect to the latest version # so we have a mechanism here to redirect to the latest version
def self.latest_version(env) def self.latest_version(env)
if CONFIG.invidious_companion.present? if CONFIG.invidious_companion.present?
invidious_companion = CONFIG.invidious_companion.sample companion_public_url = env.get("companion_public_url").as(String)
return env.redirect "#{invidious_companion.public_url}/latest_version?#{env.params.query}" return env.redirect "#{companion_public_url}/latest_version?#{env.params.query}"
end end
id = env.params.query["id"]? id = env.params.query["id"]?
@ -307,16 +312,7 @@ module Invidious::Routes::VideoPlayback
end end
if local if local
external_proxy = Invidious::HttpServer::Utils.get_external_proxy
if !external_proxy.empty?
url = URI.parse(url)
external_proxy = URI.parse(external_proxy)
url.host = external_proxy.host
url.port = external_proxy.port
url = url.to_s
else
url = URI.parse(url).request_target.not_nil! url = URI.parse(url).request_target.not_nil!
end
url += "&title=#{URI.encode_www_form(title, space_to_plus: false)}" if title url += "&title=#{URI.encode_www_form(title, space_to_plus: false)}" if title
end end

View file

@ -52,7 +52,7 @@ module Invidious::Routes::Watch
env.params.query.delete_all("listen") env.params.query.delete_all("listen")
begin begin
video = get_video(id, region: params.region) video = get_video(id, region: params.region, env: env)
rescue ex : NotFoundException rescue ex : NotFoundException
LOGGER.error("get_video not found: #{id} : #{ex.message}") LOGGER.error("get_video not found: #{id} : #{ex.message}")
return error_template(404, ex) return error_template(404, ex)
@ -61,6 +61,10 @@ module Invidious::Routes::Watch
return error_template(500, ex) return error_template(500, ex)
end end
if video.live_now && CONFIG.disable_livestreams
return error_template(403, "Livestreams are disabled as they are not working with invidious-companion right now. Please wait until an update comes out!")
end
if preferences.annotations_subscribed && if preferences.annotations_subscribed &&
subscriptions.includes?(video.ucid) && subscriptions.includes?(video.ucid) &&
(env.params.query["iv_load_policy"]? || "1") == "1" (env.params.query["iv_load_policy"]? || "1") == "1"
@ -209,6 +213,11 @@ module Invidious::Routes::Watch
video_url = nil video_url = nil
end end
if CONFIG.invidious_companion.present?
current_companion = env.get("current_companion").as(Int32)
invidious_companion = CONFIG.invidious_companion[current_companion]
end
templated "watch" templated "watch"
end end
@ -338,8 +347,9 @@ module Invidious::Routes::Watch
env.params.query["local"] = "true" env.params.query["local"] = "true"
if (CONFIG.invidious_companion.present?) if (CONFIG.invidious_companion.present?)
video = get_video(video_id) video = get_video(video_id, env: env)
return env.redirect "#{video.invidious_companion["baseUrl"].as_s}/latest_version?#{env.params.query}" companion_public_url = env.get("companion_public_url").as(String)
return env.redirect "#{companion_public_url}/latest_version?#{env.params.query}"
else else
return Invidious::Routes::VideoPlayback.latest_version(env) return Invidious::Routes::VideoPlayback.latest_version(env)
end end

View file

@ -21,6 +21,7 @@ module Invidious::Routing
get "/privacy", Routes::Misc, :privacy get "/privacy", Routes::Misc, :privacy
get "/licenses", Routes::Misc, :licenses get "/licenses", Routes::Misc, :licenses
get "/redirect", Routes::Misc, :cross_instance_redirect get "/redirect", Routes::Misc, :cross_instance_redirect
get "/switchbackend", Routes::BackendSwitcher, :switch
self.register_channel_routes self.register_channel_routes
self.register_watch_routes self.register_watch_routes
@ -68,6 +69,8 @@ module Invidious::Routing
# User account management # User account management
get "/change_password", Routes::Account, :get_change_password get "/change_password", Routes::Account, :get_change_password
post "/change_password", Routes::Account, :post_change_password post "/change_password", Routes::Account, :post_change_password
get "/change_username", Routes::Account, :get_change_username
post "/change_username", Routes::Account, :post_change_username
get "/delete_account", Routes::Account, :get_delete get "/delete_account", Routes::Account, :get_delete
post "/delete_account", Routes::Account, :post_delete post "/delete_account", Routes::Account, :post_delete
get "/clear_watch_history", Routes::Account, :get_clear_history get "/clear_watch_history", Routes::Account, :get_clear_history

View file

@ -11,6 +11,10 @@ struct Invidious::User
# Session ID (SID) cookie # Session ID (SID) cookie
# Parameter "domain" comes from the global config # Parameter "domain" comes from the global config
def sid(domain : String?, sid) : HTTP::Cookie def sid(domain : String?, sid) : HTTP::Cookie
# Strip the port from the domain if it's being accessed from another port
# Browsers will reject the cookie if it contains the port number. This is
# because `example.com:3000` is not the same as `example.com` on a cookie.
domain = domain.split(":")[0]
# Not secure if it's being accessed from I2P # Not secure if it's being accessed from I2P
# Browsers expect the domain to include https. On I2P there is no HTTPS # Browsers expect the domain to include https. On I2P there is no HTTPS
if domain.not_nil!.split(".").last == "i2p" if domain.not_nil!.split(".").last == "i2p"
@ -30,6 +34,10 @@ struct Invidious::User
# Preferences (PREFS) cookie # Preferences (PREFS) cookie
# Parameter "domain" comes from the global config # Parameter "domain" comes from the global config
def prefs(domain : String?, preferences : Preferences) : HTTP::Cookie def prefs(domain : String?, preferences : Preferences) : HTTP::Cookie
# Strip the port from the domain if it's being accessed from another port
# Browsers will reject the cookie if it contains the port number. This is
# because `example.com:3000` is not the same as `example.com` on a cookie.
domain = domain.split(":")[0]
# Not secure if it's being accessed from I2P # Not secure if it's being accessed from I2P
# Browsers expect the domain to include https. On I2P there is no HTTPS # Browsers expect the domain to include https. On I2P there is no HTTPS
if domain.not_nil!.split(".").last == "i2p" if domain.not_nil!.split(".").last == "i2p"
@ -45,5 +53,31 @@ struct Invidious::User
samesite: HTTP::Cookie::SameSite::Lax samesite: HTTP::Cookie::SameSite::Lax
) )
end end
# Backend (CONFIG.server_id_cookie_name) cookie
# Parameter "domain" comes from the global config
def server_id(domain : String?, server_id : Int32? = nil) : HTTP::Cookie
if server_id.nil?
server_id = rand(CONFIG.invidious_companion.size)
end
# Strip the port from the domain if it's being accessed from another port
# Browsers will reject the cookie if it contains the port number. This is
# because `example.com:3000` is not the same as `example.com` on a cookie.
domain = domain.split(":")[0]
# Not secure if it's being accessed from I2P
# Browsers expect the domain to include https. On I2P there is no HTTPS
if domain.not_nil!.split(".").last == "i2p"
@@secure = false
end
return HTTP::Cookie.new(
name: CONFIG.server_id_cookie_name,
domain: domain,
path: "/",
value: server_id.to_s,
secure: @@secure,
http_only: true,
samesite: HTTP::Cookie::SameSite::Lax
)
end
end end
end end

View file

@ -298,7 +298,7 @@ struct Video
predicate_bool upcoming, isUpcoming predicate_bool upcoming, isUpcoming
end end
def get_video(id, refresh = true, region = nil, force_refresh = false) def get_video(id, refresh = true, region = nil, force_refresh = false, env : HTTP::Server::Context | Nil = nil)
if (video = Invidious::Database::Videos.select(id)) && !region if (video = Invidious::Database::Videos.select(id)) && !region
# If record was last updated over 10 minutes ago, or video has since premiered, # If record was last updated over 10 minutes ago, or video has since premiered,
# refresh (expire param in response lasts for 6 hours) # refresh (expire param in response lasts for 6 hours)
@ -308,7 +308,7 @@ def get_video(id, refresh = true, region = nil, force_refresh = false)
force_refresh || force_refresh ||
video.schema_version != Video::SCHEMA_VERSION # cache control video.schema_version != Video::SCHEMA_VERSION # cache control
begin begin
video = fetch_video(id, region) video = fetch_video(id, region, env)
Invidious::Database::Videos.insert(video) Invidious::Database::Videos.insert(video)
rescue ex rescue ex
Invidious::Database::Videos.delete(id) Invidious::Database::Videos.delete(id)
@ -316,7 +316,7 @@ def get_video(id, refresh = true, region = nil, force_refresh = false)
end end
end end
else else
video = fetch_video(id, region) video = fetch_video(id, region, env)
Invidious::Database::Videos.insert(video) if !region Invidious::Database::Videos.insert(video) if !region
end end
@ -324,11 +324,11 @@ def get_video(id, refresh = true, region = nil, force_refresh = false)
rescue DB::Error rescue DB::Error
# Avoid common `DB::PoolRetryAttemptsExceeded` error and friends # Avoid common `DB::PoolRetryAttemptsExceeded` error and friends
# Note: All DB errors inherit from `DB::Error` # Note: All DB errors inherit from `DB::Error`
return fetch_video(id, region) return fetch_video(id, region, env)
end end
def fetch_video(id, region) def fetch_video(id, region, env)
info = extract_video_info(video_id: id) info = extract_video_info(video_id: id, env: env)
if reason = info["reason"]? if reason = info["reason"]?
if reason == "Video unavailable" if reason == "Video unavailable"

View file

@ -58,12 +58,12 @@ def parse_related_video(related : JSON::Any) : Hash(String, JSON::Any)?
} }
end end
def extract_video_info(video_id : String) def extract_video_info(video_id : String, env : HTTP::Server::Context | Nil = nil)
# Init client config for the API # Init client config for the API
client_config = YoutubeAPI::ClientConfig.new client_config = YoutubeAPI::ClientConfig.new
# Fetch data from the player endpoint # Fetch data from the player endpoint
player_response = YoutubeAPI.player(video_id: video_id, params: "2AMB", client_config: client_config) player_response = YoutubeAPI.player(env: env, video_id: video_id, params: "2AMB", client_config: client_config)
playability_status = player_response.dig?("playabilityStatus", "status").try &.as_s playability_status = player_response.dig?("playabilityStatus", "status").try &.as_s
@ -108,7 +108,7 @@ def extract_video_info(video_id : String)
params = parse_video_info(video_id, player_response) params = parse_video_info(video_id, player_response)
params["reason"] = JSON::Any.new(reason) if reason params["reason"] = JSON::Any.new(reason) if reason
if CONFIG.invidious_companion.present? if !CONFIG.invidious_companion.present?
new_player_response = nil new_player_response = nil
# Don't use Android test suite client if po_token is passed because po_token doesn't # Don't use Android test suite client if po_token is passed because po_token doesn't
@ -119,7 +119,7 @@ def extract_video_info(video_id : String)
# following issue for an explanation about decrypted URLs: # following issue for an explanation about decrypted URLs:
# https://github.com/TeamNewPipe/NewPipeExtractor/issues/562 # https://github.com/TeamNewPipe/NewPipeExtractor/issues/562
client_config.client_type = YoutubeAPI::ClientType::AndroidTestSuite client_config.client_type = YoutubeAPI::ClientType::AndroidTestSuite
new_player_response = try_fetch_streaming_data(video_id, client_config) new_player_response = try_fetch_streaming_data(video_id, client_config, env)
end end
# Replace player response and reset reason # Replace player response and reset reason
@ -133,7 +133,7 @@ def extract_video_info(video_id : String)
end end
end end
{"captions", "playabilityStatus", "playerConfig", "storyboards", "invidiousCompanion"}.each do |f| {"captions", "playabilityStatus", "playerConfig", "storyboards"}.each do |f|
params[f] = player_response[f] if player_response[f]? params[f] = player_response[f] if player_response[f]?
end end
@ -154,9 +154,9 @@ def extract_video_info(video_id : String)
return params return params
end end
def try_fetch_streaming_data(id : String, client_config : YoutubeAPI::ClientConfig) : Hash(String, JSON::Any)? def try_fetch_streaming_data(id : String, client_config : YoutubeAPI::ClientConfig, env : HTTP::Server::Context | Nil = nil) : Hash(String, JSON::Any)?
LOGGER.debug("try_fetch_streaming_data: [#{id}] Using #{client_config.client_type} client.") LOGGER.debug("try_fetch_streaming_data: [#{id}] Using #{client_config.client_type} client.")
response = YoutubeAPI.player(video_id: id, params: "2AMB", client_config: client_config) response = YoutubeAPI.player(video_id: id, params: "2AMB", client_config: client_config, env: env)
playability_status = response["playabilityStatus"]["status"] playability_status = response["playabilityStatus"]["status"]
LOGGER.debug("try_fetch_streaming_data: [#{id}] Got playabilityStatus == #{playability_status}.") LOGGER.debug("try_fetch_streaming_data: [#{id}] Got playabilityStatus == #{playability_status}.")
@ -200,7 +200,6 @@ def parse_video_info(video_id : String, player_response : Hash(String, JSON::Any
microformat = player_response.dig?("microformat", "playerMicroformatRenderer") microformat = player_response.dig?("microformat", "playerMicroformatRenderer")
raise BrokenTubeException.new("videoDetails") if !video_details raise BrokenTubeException.new("videoDetails") if !video_details
raise BrokenTubeException.new("microformat") if !microformat
# Basic video infos # Basic video infos
@ -216,13 +215,13 @@ def parse_video_info(video_id : String, player_response : Hash(String, JSON::Any
views_txt ||= video_details["viewCount"]?.try &.as_s || "" views_txt ||= video_details["viewCount"]?.try &.as_s || ""
views = views_txt.gsub(/\D/, "").to_i64? views = views_txt.gsub(/\D/, "").to_i64?
length_txt = (microformat["lengthSeconds"]? || video_details["lengthSeconds"]) length_txt = (microformat.try &.["lengthSeconds"]? || video_details["lengthSeconds"])
.try &.as_s.to_i64 .try &.as_s.to_i64
published = microformat["publishDate"]? published = microformat.try &.["publishDate"]?
.try { |t| Time.parse(t.as_s, "%Y-%m-%d", Time::Location::UTC) } || Time.utc .try { |t| Time.parse(t.as_s, "%Y-%m-%d", Time::Location::UTC) } || Time.utc
premiere_timestamp = microformat.dig?("liveBroadcastDetails", "startTimestamp") premiere_timestamp = microformat.try &.dig?("liveBroadcastDetails", "startTimestamp")
.try { |t| Time.parse_rfc3339(t.as_s) } .try { |t| Time.parse_rfc3339(t.as_s) }
premiere_timestamp ||= player_response.dig?( premiere_timestamp ||= player_response.dig?(
@ -233,7 +232,7 @@ def parse_video_info(video_id : String, player_response : Hash(String, JSON::Any
.try &.as_s.to_i64 .try &.as_s.to_i64
.try { |t| Time.unix(t) } .try { |t| Time.unix(t) }
live_now = microformat.dig?("liveBroadcastDetails", "isLiveNow") live_now = microformat.try &.dig?("liveBroadcastDetails", "isLiveNow")
.try &.as_bool .try &.as_bool
live_now ||= video_details.dig?("isLive").try &.as_bool || false live_now ||= video_details.dig?("isLive").try &.as_bool || false
@ -242,11 +241,11 @@ def parse_video_info(video_id : String, player_response : Hash(String, JSON::Any
# Extra video infos # Extra video infos
allowed_regions = microformat["availableCountries"]? allowed_regions = microformat.try &.["availableCountries"]?
.try &.as_a.map &.as_s || [] of String .try &.as_a.map &.as_s || [] of String
allow_ratings = video_details["allowRatings"]?.try &.as_bool allow_ratings = video_details["allowRatings"]?.try &.as_bool
family_friendly = microformat["isFamilySafe"].try &.as_bool family_friendly = microformat.try &.["isFamilySafe"].try &.as_bool
is_listed = video_details["isCrawlable"]?.try &.as_bool is_listed = video_details["isCrawlable"]?.try &.as_bool
is_upcoming = video_details["isUpcoming"]?.try &.as_bool is_upcoming = video_details["isUpcoming"]?.try &.as_bool
@ -329,8 +328,8 @@ def parse_video_info(video_id : String, player_response : Hash(String, JSON::Any
# Description # Description
description = microformat.dig?("description", "simpleText").try &.as_s || ""
short_description = player_response.dig?("videoDetails", "shortDescription") short_description = player_response.dig?("videoDetails", "shortDescription")
description = microformat.try &.dig?("description", "simpleText").try &.as_s || short_description.try &.as_s || ""
# description_html = video_secondary_renderer.try &.dig?("description", "runs") # description_html = video_secondary_renderer.try &.dig?("description", "runs")
# .try &.as_a.try { |t| content_to_comment_html(t, video_id) } # .try &.as_a.try { |t| content_to_comment_html(t, video_id) }
@ -343,7 +342,7 @@ def parse_video_info(video_id : String, player_response : Hash(String, JSON::Any
.try &.dig?("metadataRowContainer", "metadataRowContainerRenderer", "rows") .try &.dig?("metadataRowContainer", "metadataRowContainerRenderer", "rows")
.try &.as_a .try &.as_a
genre = microformat["category"]? genre = microformat.try &.["category"]?
genre_ucid = nil genre_ucid = nil
license = nil license = nil

View file

@ -109,7 +109,9 @@ def process_video_params(query, preferences)
quality = "high" quality = "high"
end end
if CONFIG.disabled?("local") && local if CONFIG.force_local
local = true
elsif CONFIG.disabled?("local") && local
local = false local = false
end end

View file

@ -29,7 +29,7 @@
}.to_pretty_json }.to_pretty_json
%> %>
</script> </script>
<script src="/js/playlist_widget.js?v=<%= ASSET_COMMIT %>"></script> <script src="/<%= JS_PATH %>/playlist_widget.js?v=<%= ASSET_COMMIT %>"></script>
<%= rendered "components/items_paginated" %> <%= rendered "components/items_paginated" %>

View file

@ -3,6 +3,7 @@
author = HTML.escape(channel.author) author = HTML.escape(channel.author)
channel_profile_pic = URI.parse(channel.author_thumbnail).request_target channel_profile_pic = URI.parse(channel.author_thumbnail).request_target
host = env.request.headers["Host"] host = env.request.headers["Host"]
scheme = env.get("scheme")
relative_url = relative_url =
case selected_tab case selected_tab
@ -32,19 +33,19 @@
<%- if selected_tab.videos? -%> <%- if selected_tab.videos? -%>
<meta name="description" content="<%= channel.description %>"> <meta name="description" content="<%= channel.description %>">
<meta property="og:site_name" content="Invidious"> <meta property="og:site_name" content="Invidious">
<meta property="og:url" content="<%= host %>/channel/<%= ucid %>"> <meta property="og:url" content="<%= scheme %>://<%= host %>/channel/<%= ucid %>">
<meta property="og:title" content="<%= author %>"> <meta property="og:title" content="<%= author %>">
<meta property="og:image" content="<%= host %>/ggpht<%= channel_profile_pic %>"> <meta property="og:image" content="<%= scheme %>://<%= host %>/ggpht<%= channel_profile_pic %>">
<meta property="og:description" content="<%= channel.description %>"> <meta property="og:description" content="<%= channel.description %>">
<meta name="twitter:card" content="summary"> <meta name="twitter:card" content="summary">
<meta name="twitter:url" content="<%= host %>/channel/<%= ucid %>"> <meta name="twitter:url" content="<%= scheme %>://<%= host %>/channel/<%= ucid %>">
<meta name="twitter:title" content="<%= author %>"> <meta name="twitter:title" content="<%= author %>">
<meta name="twitter:description" content="<%= channel.description %>"> <meta name="twitter:description" content="<%= channel.description %>">
<meta name="twitter:image" content="<%= host %>/ggpht<%= channel_profile_pic %>"> <meta name="twitter:image" content="<%= scheme %>://<%= host %>/ggpht<%= channel_profile_pic %>">
<link rel="alternate" type="application/rss+xml" title="RSS" href="/feed/channel/<%= ucid %>" /> <link rel="alternate" type="application/rss+xml" title="RSS" href="/feed/channel/<%= ucid %>" />
<%- end -%> <%- end -%>
<script src="/js/pagination.js?v=<%= ASSET_COMMIT %>"></script> <script src="/<%= JS_PATH %>/pagination.js?v=<%= ASSET_COMMIT %>"></script>
<link rel="alternate" href="<%= youtube_url %>"> <link rel="alternate" href="<%= youtube_url %>">
<title><%= author %> - Invidious</title> <title><%= author %> - Invidious</title>

View file

@ -43,4 +43,4 @@
}.to_pretty_json }.to_pretty_json
%> %>
</script> </script>
<script src="/js/community.js?v=<%= ASSET_COMMIT %>"></script> <script src="/<%= JS_PATH %>/community.js?v=<%= ASSET_COMMIT %>"></script>

View file

@ -18,4 +18,4 @@
%> %>
</script> </script>
<script src="/js/watched_indicator.js"></script> <script src="/<%= JS_PATH %>/watched_indicator.js"></script>

View file

@ -22,8 +22,9 @@
audio_streams.each_with_index do |fmt, i| audio_streams.each_with_index do |fmt, i|
src_url = "/latest_version?id=#{video.id}&itag=#{fmt["itag"]}" src_url = "/latest_version?id=#{video.id}&itag=#{fmt["itag"]}"
src_url += "&local=true" if params.local src_url += "&local=true" if params.local
src_url = video.invidious_companion["baseUrl"].as_s + src_url + companion_public_url = env.get("companion_public_url").as(String)
"&check=#{invidious_companion_encrypt(video.id)}" if (CONFIG.invidious_companion.present?) src_url = companion_public_url + src_url +
"&check=#{invidious_companion_encrypt(video.id)}" if (invidious_companion)
bitrate = fmt["bitrate"] bitrate = fmt["bitrate"]
mimetype = HTML.escape(fmt["mimeType"].as_s) mimetype = HTML.escape(fmt["mimeType"].as_s)
@ -38,8 +39,9 @@
<% else %> <% else %>
<% if params.quality == "dash" <% if params.quality == "dash"
src_url = "/api/manifest/dash/id/" + video.id + "?local=true&unique_res=1" src_url = "/api/manifest/dash/id/" + video.id + "?local=true&unique_res=1"
src_url = video.invidious_companion["baseUrl"].as_s + src_url + companion_public_url = env.get("companion_public_url").as(String)
"&check=#{invidious_companion_encrypt(video.id)}" if (CONFIG.invidious_companion.present?) src_url = companion_public_url + src_url +
"&check=#{invidious_companion_encrypt(video.id)}" if (invidious_companion)
%> %>
<source src="<%= src_url %>" type='application/dash+xml' label="dash"> <source src="<%= src_url %>" type='application/dash+xml' label="dash">
<% end %> <% end %>
@ -50,8 +52,9 @@
fmt_stream.each_with_index do |fmt, i| fmt_stream.each_with_index do |fmt, i|
src_url = "/latest_version?id=#{video.id}&itag=#{fmt["itag"]}" src_url = "/latest_version?id=#{video.id}&itag=#{fmt["itag"]}"
src_url += "&local=true" if params.local src_url += "&local=true" if params.local
src_url = video.invidious_companion["baseUrl"].as_s + src_url + companion_public_url = env.get("companion_public_url").as(String)
"&check=#{invidious_companion_encrypt(video.id)}" if (CONFIG.invidious_companion.present?) src_url = companion_public_url + src_url +
"&check=#{invidious_companion_encrypt(video.id)}" if (invidious_companion)
quality = fmt["quality"] quality = fmt["quality"]
mimetype = HTML.escape(fmt["mimeType"].as_s) mimetype = HTML.escape(fmt["mimeType"].as_s)
@ -86,4 +89,4 @@
}.to_pretty_json }.to_pretty_json
%> %>
</script> </script>
<script src="/js/player.js?v=<%= ASSET_COMMIT %>"></script> <script src="/<%= JS_PATH %>/player.js?v=<%= ASSET_COMMIT %>"></script>

View file

@ -27,7 +27,7 @@
}.to_pretty_json }.to_pretty_json
%> %>
</script> </script>
<script src="/js/subscribe_widget.js?v=<%= ASSET_COMMIT %>"></script> <script src="/<%= JS_PATH %>/subscribe_widget.js?v=<%= ASSET_COMMIT %>"></script>
<% else %> <% else %>
<a id="subscribe" class="pure-button pure-button-primary" <a id="subscribe" class="pure-button pure-button-primary"
href="/login?referer=<%= env.get("current_page") %>"> href="/login?referer=<%= env.get("current_page") %>">

View file

@ -11,7 +11,7 @@
<link rel="stylesheet" href="/css/default.css?v=<%= ASSET_COMMIT %>"> <link rel="stylesheet" href="/css/default.css?v=<%= ASSET_COMMIT %>">
<link rel="stylesheet" href="/css/embed.css?v=<%= ASSET_COMMIT %>"> <link rel="stylesheet" href="/css/embed.css?v=<%= ASSET_COMMIT %>">
<title><%= HTML.escape(video.title) %> - Invidious</title> <title><%= HTML.escape(video.title) %> - Invidious</title>
<script src="/js/_helpers.js?v=<%= ASSET_COMMIT %>"></script> <script src="/<%= JS_PATH %>/_helpers.js?v=<%= ASSET_COMMIT %>"></script>
</head> </head>
<body class="dark-theme"> <body class="dark-theme">
@ -32,6 +32,6 @@
</script> </script>
<%= rendered "components/player" %> <%= rendered "components/player" %>
<script src="/js/embed.js?v=<%= ASSET_COMMIT %>"></script> <script src="/<%= JS_PATH %>/embed.js?v=<%= ASSET_COMMIT %>"></script>
</body> </body>
</html> </html>

View file

@ -25,7 +25,7 @@
}.to_pretty_json }.to_pretty_json
%> %>
</script> </script>
<script src="/js/watched_widget.js"></script> <script src="/<%= JS_PATH %>/watched_widget.js"></script>
<div class="pure-g"> <div class="pure-g">
<% watched.each do |item| %> <% watched.each do |item| %>

View file

@ -40,4 +40,4 @@
<% end %> <% end %>
</div> </div>
<script src="/js/watched_indicator.js"></script> <script src="/<%= JS_PATH %>/watched_indicator.js"></script>

View file

@ -17,4 +17,4 @@
<% end %> <% end %>
</div> </div>
<script src="/js/watched_indicator.js"></script> <script src="/<%= JS_PATH %>/watched_indicator.js"></script>

View file

@ -53,7 +53,7 @@
}.to_pretty_json }.to_pretty_json
%> %>
</script> </script>
<script src="/js/watched_widget.js"></script> <script src="/<%= JS_PATH %>/watched_widget.js"></script>
<div class="pure-g"> <div class="pure-g">
@ -62,7 +62,7 @@
<% end %> <% end %>
</div> </div>
<script src="/js/watched_indicator.js"></script> <script src="/<%= JS_PATH %>/watched_indicator.js"></script>
<%= <%=
IV::Frontend::Pagination.nav_numeric(locale, IV::Frontend::Pagination.nav_numeric(locale,

View file

@ -46,4 +46,4 @@
<% end %> <% end %>
</div> </div>
<script src="/js/watched_indicator.js"></script> <script src="/<%= JS_PATH %>/watched_indicator.js"></script>

View file

@ -118,7 +118,7 @@
}.to_pretty_json }.to_pretty_json
%> %>
</script> </script>
<script src="/js/playlist_widget.js?v=<%= ASSET_COMMIT %>"></script> <script src="/<%= JS_PATH %>/playlist_widget.js?v=<%= ASSET_COMMIT %>"></script>
<% end %> <% end %>

View file

@ -44,5 +44,5 @@
}.to_pretty_json }.to_pretty_json
%> %>
</script> </script>
<script src="/js/comments.js?v=<%= ASSET_COMMIT %>"></script> <script src="/<%= JS_PATH %>/comments.js?v=<%= ASSET_COMMIT %>"></script>
<script src="/js/post.js?v=<%= ASSET_COMMIT %>"></script> <script src="/<%= JS_PATH %>/post.js?v=<%= ASSET_COMMIT %>"></script>

View file

@ -1,8 +1,6 @@
<% <%
locale = env.get("preferences").as(Preferences).locale locale = env.get("preferences").as(Preferences).locale
dark_mode = env.get("preferences").as(Preferences).dark_mode dark_mode = env.get("preferences").as(Preferences).dark_mode
current_backend = env.request.cookies[CONFIG.server_id_cookie_name]?.try &.value || env.request.headers["Host"]
current_external_videoplayback_proxy = Invidious::HttpServer::Utils.get_external_proxy()
%> %>
<!DOCTYPE html> <!DOCTYPE html>
<html lang="<%= locale %>"> <html lang="<%= locale %>">
@ -24,7 +22,7 @@
<link rel="stylesheet" href="/css/ionicons.min.css?v=<%= ASSET_COMMIT %>"> <link rel="stylesheet" href="/css/ionicons.min.css?v=<%= ASSET_COMMIT %>">
<link rel="stylesheet" href="/css/default.css?v=<%= ASSET_COMMIT %>"> <link rel="stylesheet" href="/css/default.css?v=<%= ASSET_COMMIT %>">
<link rel="stylesheet" href="/css/carousel.css?v=<%= ASSET_COMMIT %>"> <link rel="stylesheet" href="/css/carousel.css?v=<%= ASSET_COMMIT %>">
<script src="/js/_helpers.js?v=<%= ASSET_COMMIT %>"></script> <script src="/<%= JS_PATH %>/_helpers.js?v=<%= ASSET_COMMIT %>"></script>
</head> </head>
<body class="<%= dark_mode.blank? ? "no" : dark_mode %>-theme"> <body class="<%= dark_mode.blank? ? "no" : dark_mode %>-theme">
@ -34,7 +32,9 @@
<div class="pure-g navbar h-box"> <div class="pure-g navbar h-box">
<% if navbar_search %> <% if navbar_search %>
<div class="pure-u-1 pure-u-md-4-24"> <div class="pure-u-1 pure-u-md-4-24">
<a href="/" class="index-link pure-menu-heading">Invidious</a> <a href="/" class="index-link pure-menu-heading">
Invidious
</a>
</div> </div>
<div class="pure-u-1 pure-u-md-12-24 searchbar"> <div class="pure-u-1 pure-u-md-12-24 searchbar">
<% autofocus = false %><%= rendered "components/search_box" %> <% autofocus = false %><%= rendered "components/search_box" %>
@ -106,30 +106,54 @@
</div> </div>
</div> </div>
<% if !CONFIG.backends.empty? %> <%
<% if !CONFIG.backend_domains.includes?(env.request.headers["Host"]) %> if CONFIG.invidious_companion.present?
current_backend = env.get?("current_companion").try &.as(Int32)
domain = env.get?("using_domain")
scheme = env.get("scheme")
status = BackendInfo.get_status
companion_switched = env.get?("companion_switched")
%>
<div class="h-box" style="margin-bottom: 10px;"> <div class="h-box" style="margin-bottom: 10px;">
<b>Switch Backend:</b> <b>Switch Backend:</b>
<% CONFIG.backends.each do | backend | %> <% if domain %>
<% backend = backend.split(CONFIG.backends_delimiter) %> <% CONFIG.invidious_companion.each_with_index do | companion, index | %>
<% if current_backend == backend[0] %> <% host_backend = env.request.headers["Host"].sub(/([^.]+)(\d+)/, "\\1#{index+1}") %>
<a href="/switchbackend?backend_id=<%= backend[0] %>" style="text-decoration-line: underline; display: inline-block;"> <% is_current_backend_host = host_backend == env.request.headers["Host"] %>
Backend<%= HTML.escape(backend[0]) %> <a href="<%= scheme %>://<%= host_backend %><%= env.request.resource %>" style="<%= is_current_backend_host ? "text-decoration-line: underline;" : "" %> display: inline-block;">
<% if backend.size == 2 %> Backend<%= HTML.escape((index + 1).to_s) %> <%= HTML.escape(companion.note) %>
<%= HTML.escape(backend[1]) %> <span style="color:
<% if status[index] == 0 %> #fd4848; <% end %>
<% if status[index] == 1 %> #d06925; <% end %>
<% if status[index] == 2 %> #42ae3c; <% end %>
">•</span>
</a>
<% if !(index == CONFIG.invidious_companion.size-1) %>
<span> | </span>
<% end %>
<% end %> <% end %>
</a> <span> | </span>
<% else %> <% else %>
<a href="/switchbackend?backend_id=<%= backend[0] %>" style="display: inline-block;"> <% CONFIG.invidious_companion.each_with_index do | companion, index | %>
Backend<%= HTML.escape(backend[0]) %> <a href="/switchbackend?backend_id=<%= index.to_s %>" style="<%= current_backend == index ? "text-decoration-line: underline;" : "" %> display: inline-block;">
<% if backend.size == 2 %> Backend<%= HTML.escape((index + 1).to_s) %> <%= HTML.escape(companion.note) %>
<%= HTML.escape(backend[1]) %> <span style="color:
<% if status[index] == 0 %> #fd4848; <% end %>
<% if status[index] == 1 %> #d06925; <% end %>
<% if status[index] == 2 %> #42ae3c; <% end %>
">•</span>
</a>
<% if !(index == CONFIG.invidious_companion.size-1) %>
<span> | </span>
<% end %> <% end %>
</a> <span> | </span>
<% end %> <% end %>
<% end %> <% end %>
</div> </div>
<% end %> <% end %>
<% if companion_switched %>
<div class="h-box">
<p><%= translate(locale, "backend_unavailable") %></p>
</div>
<% end %> <% end %>
<% if CONFIG.banner %> <% if CONFIG.banner %>
@ -146,10 +170,10 @@
</div> </div>
</div> </div>
<script src="/js/handlers.js?v=<%= ASSET_COMMIT %>"></script> <script src="/<%= JS_PATH %>/handlers.js?v=<%= ASSET_COMMIT %>"></script>
<script src="/js/themes.js?v=<%= ASSET_COMMIT %>"></script> <script src="/<%= JS_PATH %>/themes.js?v=<%= ASSET_COMMIT %>"></script>
<% if env.get? "user" %> <% if env.get? "user" %>
<script src="/js/sse.js?v=<%= ASSET_COMMIT %>"></script> <script src="/<%= JS_PATH %>/sse.js?v=<%= ASSET_COMMIT %>"></script>
<script id="notification_data" type="application/json"> <script id="notification_data" type="application/json">
<%= <%=
{ {
@ -159,7 +183,7 @@
%> %>
</script> </script>
<% if CONFIG.enable_user_notifications %> <% if CONFIG.enable_user_notifications %>
<script src="/js/notifications.js?v=<%= ASSET_COMMIT %>"></script> <script src="/<%= JS_PATH %>/notifications.js?v=<%= ASSET_COMMIT %>"></script>
<% end %> <% end %>
<% end %> <% end %>
@ -313,9 +337,11 @@
</div> </div>
<hr/> <hr/>
<div class="footer-footer"> <div class="footer-footer">
<div class="box">You are currently using Backend: <%= current_backend %></div> <%
<% if !current_external_videoplayback_proxy.empty? %> if CONFIG.invidious_companion.present?
<div class="box">External Videoplayback Proxy: <%= current_external_videoplayback_proxy %></div> companion_public_url = env.get("companion_public_url").as(String)
%>
<div class="box">You are currently using Backend: <%= current_backend ? companion_public_url : "Unable to get backend, this is bug, please report it!" %></div>
<% end %> <% end %>
<span class="left"> <span class="left">
<% if CONFIG.modified_source_code_url %> <% if CONFIG.modified_source_code_url %>

View file

@ -0,0 +1,26 @@
<% content_for "header" do %>
<title><%= translate(locale, "change_username") %> - Invidious</title>
<% end %>
<div class="pure-g">
<div class="pure-u-1 pure-u-lg-1-5"></div>
<div class="pure-u-1 pure-u-lg-3-5">
<div class="h-box">
<form class="pure-form pure-form-aligned" action="/change_username?referer=<%= URI.encode_www_form(referer) %>" method="post">
<legend><%= translate(locale, "") %></legend>
<fieldset>
<label for="new_username"><%= translate(locale, "new_username") %> :</label>
<input required class="pure-input-1" name="new_username" type="text" placeholder="<%= translate(locale, "new_username") %>">
<button type="submit" name="action" value="change_username" class="pure-button pure-button-primary">
<%= translate(locale, "change_username") %>
</button>
<input type="hidden" name="csrf_token" value="<%= HTML.escape(csrf_token) %>">
</fieldset>
</form>
</div>
</div>
<div class="pure-u-1 pure-u-lg-1-5"></div>
</div>

View file

@ -34,7 +34,10 @@
<div class="pure-control-group"> <div class="pure-control-group">
<label for="local"><%= translate(locale, "preferences_local_label") %></label> <label for="local"><%= translate(locale, "preferences_local_label") %></label>
<input name="local" id="local" type="checkbox" <% if preferences.local && !CONFIG.disabled?("local") %>checked<% end %> <% if CONFIG.disabled?("local") %>disabled<% end %>> <input name="local" id="local" type="checkbox" <% if CONFIG.force_local %>disabled="" <% end %><% if preferences.local && !CONFIG.disabled?("local") %>checked<% end %> <% if CONFIG.disabled?("local") %>disabled<% end %>>
<% if CONFIG.force_local %>
<label for="local">(All videos are proxied by default)</label>
<% end %>
</div> </div>
<div class="pure-control-group"> <div class="pure-control-group">
@ -330,6 +333,10 @@
<a href="/change_password?referer=<%= URI.encode_www_form(referer) %>"><%= translate(locale, "Change password") %></a> <a href="/change_password?referer=<%= URI.encode_www_form(referer) %>"><%= translate(locale, "Change password") %></a>
</div> </div>
<div class="pure-control-group">
<a href="/change_username?referer=<%= URI.encode_www_form(referer) %>"><%= translate(locale, "change_username") %></a>
</div>
<div class="pure-control-group"> <div class="pure-control-group">
<a href="/data_control?referer=<%= URI.encode_www_form(referer) %>"><%= translate(locale, "Import/export data") %></a> <a href="/data_control?referer=<%= URI.encode_www_form(referer) %>"><%= translate(locale, "Import/export data") %></a>
</div> </div>

View file

@ -2,16 +2,17 @@
<% title = HTML.escape(video.title) %> <% title = HTML.escape(video.title) %>
<% author = HTML.escape(video.author) %> <% author = HTML.escape(video.author) %>
<% host = env.request.headers["Host"] %> <% host = env.request.headers["Host"] %>
<% scheme = env.get("scheme") %>
<% content_for "header" do %> <% content_for "header" do %>
<meta name="thumbnail" content="<%= thumbnail %>"> <meta name="thumbnail" content="<%= scheme %>://<%= host %><%= thumbnail %>">
<meta name="description" content="<%= HTML.escape(video.short_description) %>"> <meta name="description" content="<%= HTML.escape(video.short_description) %>">
<meta name="keywords" content="<%= video.keywords.join(",") %>"> <meta name="keywords" content="<%= video.keywords.join(",") %>">
<meta property="og:site_name" content="<%= author %> | Invidious"> <meta property="og:site_name" content="<%= author %> | Invidious">
<meta property="og:url" content="<%= host %>/watch?v=<%= video.id %>"> <meta property="og:url" content="<%= scheme %>://<%= host %>/watch?v=<%= video.id %>">
<meta property="og:title" content="<%= title %>"> <meta property="og:title" content="<%= title %>">
<meta property="og:image" content="<%= host %>/vi/<%= video.id %>/maxres.jpg"> <meta property="og:image" content="<%= scheme %>://<%= host %>/vi/<%= video.id %>/maxres.jpg">
<meta property="og:description" content="<%= HTML.escape(video.short_description) %>"> <meta property="og:description" content="<%= HTML.escape(video.short_description) %>">
<meta property="og:type" content="video.other"> <meta property="og:type" content="video.other">
<!-- This shouldn't be empty, ever. --> <!-- This shouldn't be empty, ever. -->
@ -22,11 +23,11 @@
<meta property="og:video:width" content="640"> <meta property="og:video:width" content="640">
<meta property="og:video:height" content="360"> <meta property="og:video:height" content="360">
<meta name="twitter:card" content="player"> <meta name="twitter:card" content="player">
<meta name="twitter:url" content="<%= host %>/watch?v=<%= video.id %>"> <meta name="twitter:url" content="<%= scheme %>://<%= host %>/watch?v=<%= video.id %>">
<meta name="twitter:title" content="<%= title %>"> <meta name="twitter:title" content="<%= title %>">
<meta name="twitter:description" content="<%= HTML.escape(video.short_description) %>"> <meta name="twitter:description" content="<%= HTML.escape(video.short_description) %>">
<meta name="twitter:image" content="<%= host %>/vi/<%= video.id %>/maxres.jpg"> <meta name="twitter:image" content="<%= scheme %>://<%= host %>/vi/<%= video.id %>/maxres.jpg">
<meta name="twitter:player" content="<%= host %>/embed/<%= video.id %>"> <meta name="twitter:player" content="<%= scheme %>://<%= host %>/embed/<%= video.id %>">
<meta name="twitter:player:width" content="1280"> <meta name="twitter:player:width" content="1280">
<meta name="twitter:player:height" content="720"> <meta name="twitter:player:height" content="720">
<link rel="alternate" href="https://www.youtube.com/watch?v=<%= video.id %>"> <link rel="alternate" href="https://www.youtube.com/watch?v=<%= video.id %>">
@ -198,7 +199,7 @@ we're going to need to do it here in order to allow for translations.
}.to_pretty_json }.to_pretty_json
%> %>
</script> </script>
<script src="/js/playlist_widget.js?v=<%= Time.utc.to_unix_ms %>"></script> <script src="/<%= JS_PATH %>/playlist_widget.js?v=<%= Time.utc.to_unix_ms %>"></script>
<% end %> <% end %>
<% end %> <% end %>
@ -207,6 +208,7 @@ we're going to need to do it here in order to allow for translations.
<p id="views"><i class="icon ion-ios-eye"></i> <%= number_with_separator(video.views) %></p> <p id="views"><i class="icon ion-ios-eye"></i> <%= number_with_separator(video.views) %></p>
<p id="likes"><i class="icon ion-ios-thumbs-up"></i> <%= number_with_separator(video.likes) %></p> <p id="likes"><i class="icon ion-ios-thumbs-up"></i> <%= number_with_separator(video.likes) %></p>
<p id="dislikes" style="display: none; visibility: hidden;"></p> <p id="dislikes" style="display: none; visibility: hidden;"></p>
<% if !video.genre.nil? && !video.genre.empty? %>
<p id="genre"><%= translate(locale, "Genre: ") %> <p id="genre"><%= translate(locale, "Genre: ") %>
<% if !video.genre_url %> <% if !video.genre_url %>
<%= video.genre %> <%= video.genre %>
@ -214,6 +216,7 @@ we're going to need to do it here in order to allow for translations.
<a href="<%= video.genre_url %>"><%= video.genre %></a> <a href="<%= video.genre_url %>"><%= video.genre %></a>
<% end %> <% end %>
</p> </p>
<% end %>
<% if video.license %> <% if video.license %>
<% if video.license.empty? %> <% if video.license.empty? %>
<p id="license"><%= translate(locale, "License: ") %><%= translate(locale, "Standard YouTube license") %></p> <p id="license"><%= translate(locale, "License: ") %><%= translate(locale, "Standard YouTube license") %></p>
@ -225,6 +228,7 @@ we're going to need to do it here in order to allow for translations.
<p id="wilson" style="display: none; visibility: hidden;"></p> <p id="wilson" style="display: none; visibility: hidden;"></p>
<p id="rating" style="display: none; visibility: hidden;"></p> <p id="rating" style="display: none; visibility: hidden;"></p>
<p id="engagement" style="display: none; visibility: hidden;"></p> <p id="engagement" style="display: none; visibility: hidden;"></p>
<% if !video.allowed_regions.nil? && !video.allowed_regions.empty? %>
<% if video.allowed_regions.size != REGIONS.size %> <% if video.allowed_regions.size != REGIONS.size %>
<p id="allowed_regions"> <p id="allowed_regions">
<% if video.allowed_regions.size < REGIONS.size // 2 %> <% if video.allowed_regions.size < REGIONS.size // 2 %>
@ -234,6 +238,7 @@ we're going to need to do it here in order to allow for translations.
<% end %> <% end %>
</p> </p>
<% end %> <% end %>
<% end %>
</div> </div>
</div> </div>
@ -386,5 +391,5 @@ we're going to need to do it here in order to allow for translations.
</div> </div>
<% end %> <% end %>
</div> </div>
<script src="/js/comments.js?v=<%= ASSET_COMMIT %>"></script> <script src="/<%= JS_PATH %>/comments.js?v=<%= ASSET_COMMIT %>"></script>
<script src="/js/watch.js?v=<%= ASSET_COMMIT %>"></script> <script src="/<%= JS_PATH %>/watch.js?v=<%= ASSET_COMMIT %>"></script>

View file

@ -49,7 +49,7 @@ end
struct CompanionConnectionPool struct CompanionConnectionPool
property pool : DB::Pool(HTTP::Client) property pool : DB::Pool(HTTP::Client)
def initialize(capacity = 5, timeout = 5.0) def initialize(companion, capacity = 5, timeout = 5.0)
options = DB::Pool::Options.new( options = DB::Pool::Options.new(
initial_pool_size: 0, initial_pool_size: 0,
max_pool_size: capacity, max_pool_size: capacity,
@ -58,23 +58,22 @@ struct CompanionConnectionPool
) )
@pool = DB::Pool(HTTP::Client).new(options) do @pool = DB::Pool(HTTP::Client).new(options) do
companion = CONFIG.invidious_companion.sample next make_client(companion.private_url, use_http_proxy: false)
next make_client(companion.private_url, force_resolve: true)
end end
end end
def client(&) def client(&)
conn = pool.checkout 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 begin
response = yield conn response = yield conn
rescue ex rescue ex
conn.close conn.close
companion = CONFIG.invidious_companion.sample scheme = "https" if conn.tls || "http"
conn = make_client(companion.private_url, force_resolve: true) same_companion = "#{scheme}://#{conn.host}:#{conn.port}"
conn = make_client(URI.parse(same_companion), use_http_proxy: false)
response = yield conn response = yield conn
ensure ensure

View file

@ -456,6 +456,7 @@ module YoutubeAPI
*, # Force the following parameters to be passed by name *, # Force the following parameters to be passed by name
params : String, params : String,
client_config : ClientConfig | Nil = nil, client_config : ClientConfig | Nil = nil,
env : HTTP::Server::Context | Nil = nil,
) )
# Playback context, separate because it can be different between clients # Playback context, separate because it can be different between clients
playback_ctx = { playback_ctx = {
@ -492,7 +493,7 @@ module YoutubeAPI
end end
if CONFIG.invidious_companion.present? if CONFIG.invidious_companion.present?
return self._post_invidious_companion("/youtubei/v1/player", data) return self._post_invidious_companion("/youtubei/v1/player", data, env)
else else
return self._post_json("/youtubei/v1/player", data, client_config) return self._post_json("/youtubei/v1/player", data, client_config)
end end
@ -672,7 +673,8 @@ module YoutubeAPI
# #
def _post_invidious_companion( def _post_invidious_companion(
endpoint : String, endpoint : String,
data : Hash data : Hash,
env : HTTP::Server::Context | Nil,
) : Hash(String, JSON::Any) ) : Hash(String, JSON::Any)
headers = HTTP::Headers{ headers = HTTP::Headers{
"Content-Type" => "application/json; charset=UTF-8", "Content-Type" => "application/json; charset=UTF-8",
@ -686,7 +688,12 @@ module YoutubeAPI
# Send the POST request # Send the POST request
begin begin
response = COMPANION_POOL.client &.post(endpoint, headers: headers, body: data.to_json) if env.nil?
current_companion = rand(CONFIG.invidious_companion.size)
else
current_companion = env.get("current_companion").as(Int32)
end
response = COMPANION_POOL[current_companion].client &.post(endpoint, headers: headers, body: data.to_json)
body = response.body body = response.body
if (response.status_code != 200) if (response.status_code != 200)
raise Exception.new( raise Exception.new(