Compare commits
62 commits
backend-co
...
master
Author | SHA1 | Date | |
---|---|---|---|
deb1187ea9 | |||
4a0e61812e | |||
ad95f0e2c0 | |||
e13800e859 | |||
69e351770d | |||
3e33c9b70f | |||
0ce17d91eb | |||
c9eed028b0 | |||
ff3d008a6f | |||
49ae71a6ac | |||
fac53ce721 | |||
b4e146fb60 | |||
d7aeb1a89f | |||
7b072200f6 | |||
bbec111997 | |||
e3d60a0517 | |||
ce052103e7 | |||
c57a4f4920 | |||
426e7bfbdb | |||
3d85519ec9 | |||
a74d89b6d9 | |||
fd8c40e0da | |||
5f1944925b | |||
015c9ec5d1 | |||
be9a3794e9 | |||
642b2e8bf0 | |||
ce97a41301 | |||
b29f5b39de | |||
895745934b | |||
d47aa3dd6a | |||
ddf6802d76 | |||
626fb2d1a8 | |||
56f309d6bb | |||
|
23ff6135bb | ||
7c9f79e1f1 | |||
7aba1f7ba3 | |||
8be23eb01d | |||
a44f37563b | |||
facd01b52e | |||
|
409d12a81e | ||
db53ee21ee | |||
|
70ff463cc6 | ||
|
e23d0d13be | ||
|
5c8b4eb379 | ||
a4cb5f094c | |||
01ccd55829 | |||
8fe965419a | |||
fda823593e | |||
24e66231df | |||
1001a72297 | |||
e5c0f15398 | |||
ecacbab2a5 | |||
bceb7a61ef | |||
|
f3d982a885 | ||
27fecf3879 | |||
|
70754659e5 | ||
|
8d4c16c79c | ||
|
9db6eb058c | ||
|
e7f7f39ce8 | ||
|
8456f8d4cd | ||
|
27dd94f60d | ||
|
4d410d124f |
59 changed files with 1057 additions and 530 deletions
|
@ -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
|
||||||
|
|
95
CHANGELOG.md
95
CHANGELOG.md
|
@ -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
|
||||||
|
|
40
README.md
40
README.md
|
@ -13,25 +13,30 @@ 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`:~~
|
||||||
|
```yaml
|
||||||
|
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:
|
||||||
|
|
||||||
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
|
||||||
```
|
```
|
||||||
|
|
||||||
- [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.
|
||||||
|
@ -41,6 +46,17 @@ It can be set using this on `config.yml`:
|
||||||
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
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
There is more things that I added to this fork, but those are the most important ones. I also regularly merge unmerged pull requests from https://github.com/iv-org/invidious and random fixes as well. Is not the most stable codebase, but you can't really make something stable when youtube is trying to destroy every third party client out there.
|
There is more things that I added to this fork, but those are the most important ones. I also regularly merge unmerged pull requests from https://github.com/iv-org/invidious and random fixes as well. Is not the most stable codebase, but you can't really make something stable when youtube is trying to destroy every third party client out there.
|
1
assets/js/.gitignore
vendored
Normal file
1
assets/js/.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
minified
|
|
@ -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/
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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"
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
97
scripts/minify-js.cr
Executable 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}`
|
29
shard.lock
29
shard.lock
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
|
|
@ -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")
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
# -------------------
|
# -------------------
|
||||||
|
|
|
@ -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)
|
||||||
|
|
85
src/invidious/helpers/backend_info.cr
Normal file
85
src/invidious/helpers/backend_info.cr
Normal 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
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
13
src/invidious/jobs/backend_checker.cr
Normal file
13
src/invidious/jobs/backend_checker.cr
Normal 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
|
|
@ -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
|
|
|
@ -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
|
||||||
# -------------------
|
# -------------------
|
||||||
|
|
|
@ -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
|
||||||
|
|
16
src/invidious/routes/backend_switcher.cr
Normal file
16
src/invidious/routes/backend_switcher.cr
Normal 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
|
|
@ -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'",
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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" %>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -18,4 +18,4 @@
|
||||||
%>
|
%>
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script src="/js/watched_indicator.js"></script>
|
<script src="/<%= JS_PATH %>/watched_indicator.js"></script>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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") %>">
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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| %>
|
||||||
|
|
|
@ -40,4 +40,4 @@
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="/js/watched_indicator.js"></script>
|
<script src="/<%= JS_PATH %>/watched_indicator.js"></script>
|
||||||
|
|
|
@ -17,4 +17,4 @@
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="/js/watched_indicator.js"></script>
|
<script src="/<%= JS_PATH %>/watched_indicator.js"></script>
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -46,4 +46,4 @@
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="/js/watched_indicator.js"></script>
|
<script src="/<%= JS_PATH %>/watched_indicator.js"></script>
|
||||||
|
|
|
@ -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 %>
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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>
|
|
@ -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 %>
|
||||||
|
|
26
src/invidious/views/user/change_username.ecr
Normal file
26
src/invidious/views/user/change_username.ecr
Normal 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>
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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(
|
||||||
|
|
Loading…
Add table
Reference in a new issue