Compare commits

..

1 commit

60 changed files with 586 additions and 1027 deletions

View file

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

View file

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

View file

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

View file

@ -1 +0,0 @@
minified

View file

@ -1072,7 +1072,8 @@ default_user_preferences:
## ##
#extend_desc: false #extend_desc: false
# 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_url: 127.0.0.1:6379
# redis_socket: /var/run/valkey/valkey.sock
# donation_url: "https://example.com/donate" # donation_url: "https://example.com/donate"
# contact_url: "https://example.com/contact" # contact_url: "https://example.com/contact"
# home_domain: "https://example.com/ # home_domain: "https://example.com/

View file

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

View file

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

View file

@ -518,12 +518,5 @@
"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"
} }

View file

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

View file

@ -2,15 +2,15 @@ version: 2.0
shards: shards:
ameba: ameba:
git: https://github.com/crystal-ameba/ameba.git git: https://github.com/crystal-ameba/ameba.git
version: 1.6.4 version: 1.6.1
athena-negotiation: athena-negotiation:
git: https://github.com/athena-framework/negotiation.git git: https://github.com/athena-framework/negotiation.git
version: 0.1.5 version: 0.1.1
backtracer: backtracer:
git: https://github.com/sija/backtracer.cr.git git: https://github.com/sija/backtracer.cr.git
version: 1.2.4 version: 1.2.2
db: db:
git: https://github.com/crystal-lang/crystal-db.git git: https://github.com/crystal-lang/crystal-db.git
@ -18,24 +18,31 @@ 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.4.1 version: 0.2.2
http_proxy:
git: https://github.com/mamantoha/http_proxy.git
version: 0.10.3
inotify: inotify:
git: https://github.com/petoem/inotify.cr.git git: https://github.com/petoem/inotify.cr.git
version: 1.0.3 version: 1.0.3
http_proxy:
git: https://github.com/mamantoha/http_proxy.git
version: 0.10.3
kemal: kemal:
git: https://github.com/kemalcr/kemal.git git: https://github.com/kemalcr/kemal.git
version: 1.6.0 version: 1.1.2
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
@ -45,8 +52,8 @@ shards:
version: 0.4.1 version: 0.4.1
redis: redis:
git: https://github.com/jgaskins/redis.git git: https://github.com/stefanwille/crystal-redis.git
version: 0.12.0 version: 2.9.1
spectator: spectator:
git: https://github.com/icy-arctic-fox/spectator.git git: https://github.com/icy-arctic-fox/spectator.git

View file

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

View file

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

View file

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

View file

@ -17,8 +17,10 @@
require "digest/md5" require "digest/md5"
require "file_utils" require "file_utils"
# Require kemal, then our own overrides # Require kemal, kilt, 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"
@ -49,8 +51,7 @@ require "./invidious/channels/*"
require "./invidious/user/*" require "./invidious/user/*"
require "./invidious/search/*" require "./invidious/search/*"
require "./invidious/routes/**" require "./invidious/routes/**"
require "./invidious/jobs/base_job" require "./invidious/jobs/**"
require "./invidious/jobs/*"
# Declare the base namespace for invidious # Declare the base namespace for invidious
module Invidious module Invidious
@ -113,11 +114,9 @@ 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 = [] of CompanionConnectionPool COMPANION_POOL = CompanionConnectionPool.new(
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|
@ -160,15 +159,6 @@ 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.
# #
@ -219,6 +209,14 @@ Invidious::Jobs.register Invidious::Jobs::ClearExpiredItemsJob.new
Invidious::Jobs.register Invidious::Jobs::InstanceListRefreshJob.new Invidious::Jobs.register Invidious::Jobs::InstanceListRefreshJob.new
if !CONFIG.external_videoplayback_proxy.empty?
Invidious::Jobs.register Invidious::Jobs::CheckExternalProxy.new
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
@ -227,8 +225,6 @@ end
if CONFIG.invidious_companion.present? if CONFIG.invidious_companion.present?
Invidious::Jobs.register Invidious::Jobs::CheckBackend.new 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 end
Invidious::Jobs.start_all Invidious::Jobs.start_all
@ -253,8 +249,8 @@ error 500 do |env, ex|
error_template(500, ex) error_template(500, ex)
end end
static_headers do |env| static_headers do |response|
env.response.headers.add("Cache-Control", "max-age=2629800") response.headers.add("Cache-Control", "max-age=2629800")
end end
# Init Kemal # Init Kemal

View file

@ -52,7 +52,6 @@ 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 %}
@ -83,12 +82,6 @@ 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)
@ -108,8 +101,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("")
@[YAML::Field(converter: Preferences::URIConverter)] property redis_url : String?
property redis_url : URI = URI.parse("") property redis_socket : String?
# 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
@ -128,6 +121,10 @@ 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
@ -219,9 +216,13 @@ 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 = "COMPANION_ID" property server_id_cookie_name : String = "INVIDIOUS_SERVER_ID"
property tokens_server : String = "" property tokens_server : String = ""
@ -236,14 +237,6 @@ 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 %}
@ -390,14 +383,10 @@ 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 characters." puts "Config: The value of 'invidious_companion_key' needs to be a size of 16 or more."
exit(1) exit(1)
end end
elsif config.signature_server
puts("WARNING: inv-sig-helper is deprecated. Please switch to Invidious companion: https://docs.invidious.io/companion-installation/")
else
puts("WARNING: Invidious companion is required to view and playback videos. For more information see https://docs.invidious.io/companion-installation/")
end end
# HMAC_key is mandatory # HMAC_key is mandatory

View file

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

View file

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

View file

@ -97,20 +97,21 @@ module Invidious::Database::Videos
end end
class Redis_ class Redis_
@redis : Redis::Client @redis : Redis::PooledClient
def initialize def initialize
@redis = Redis::Client.new(CONFIG.redis_url) @redis = Redis::PooledClient.new(unixsocket: CONFIG.redis_socket || nil, url: CONFIG.redis_url || nil)
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 at '#{CONFIG.redis_url}'" if CONFIG.redis_url 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 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, ex: expire_time) @redis.set(video.id, video.info.to_json, expire_time)
@redis.set(video.id + ":time", video.updated.to_s, ex: expire_time) @redis.set(video.id + ":time", video.updated, expire_time)
end end
def del(id : String) def del(id : String)

View file

@ -1,68 +1,55 @@
module BackendInfo module BackendInfo
extend self extend self
@@exvpp_url : Array(String) = Array.new(CONFIG.invidious_companion.size, "") @@status : Int32 = 0
@@status : Array(Int32) = Array.new(CONFIG.invidious_companion.size, 0) @@exvpp_url : String = ""
@@csp : Array(String) = Array.new(CONFIG.invidious_companion.size, "")
@@mutex : Mutex = Mutex.new
def check_backends def self.check_backends
check_videoplayback_proxy()
check_companion() check_companion()
end end
private def check_companion def check_companion
CONFIG.invidious_companion.each_with_index do |companion, index| begin
spawn do response = HTTP::Client.get "#{CONFIG.invidious_companion.sample.private_url}/healthz"
if response.status_code == 200
@@status = 1
check_videoplayback_proxy()
else
@@status = 0
end
rescue
@@status = 0
end
end
def check_videoplayback_proxy
begin
info = HTTP::Client.get "#{CONFIG.invidious_companion.sample.private_url}/info"
exvpp = JSON.parse(info.body)["external_videoplayback_proxy"]?.try &.to_s
if exvpp.nil? || exvpp.empty?
@@status = 2
return
else
begin begin
response = HTTP::Client.get "#{companion.private_url}/healthz" exvpp_health = HTTP::Client.get "#{exvpp}/health"
if response.status_code == 200 if exvpp_health.status_code == 200
check_videoplayback_proxy(companion, index) @@status = 2
generate_csp([companion.public_url.to_s, companion.i2p_public_url.to_s], @@exvpp_url[index], index) return
else
@@status[index] = 0
end end
rescue rescue
@@status[index] = 0 @@status = 1
end end
end end
rescue
end end
end end
private def check_videoplayback_proxy(companion : Config::CompanionConfig, index : Int32) def get_videoplayback_proxy
begin begin
info = HTTP::Client.get "#{companion.private_url}/info" response = HTTP::Client.get "#{CONFIG.invidious_companion.sample.private_url}/info"
exvpp_url = JSON.parse(info.body)["external_videoplayback_proxy"]?.try &.to_s exvpp_url = JSON.parse(response.body)["external_videoplayback_proxy"].to_s
rescue JSON::ParseException @@exvpp_url = exvpp_url
@@status[index] = 2 rescue
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
end end
@ -73,13 +60,4 @@ module BackendInfo
def get_exvpp def get_exvpp
return @@exvpp_url return @@exvpp_url
end 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 end

View file

@ -183,8 +183,6 @@ 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>
@ -196,7 +194,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>
#{show_embed_link} (<a rel="noreferrer noopener" href="https://youtube.com/embed/#{env.params.query["v"]}">#{go_to_youtube_embed}</a>)
</li> </li>
</ul> </ul>
END_HTML END_HTML

View file

@ -56,11 +56,12 @@ 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}}
render {{filename}}, {{layout}} content = Kilt.render({{filename}})
Kilt.render({{layout}})
end end
macro rendered(filename) macro rendered(filename)
render("src/invidious/views/#{{{filename}}}.ecr") Kilt.render("src/invidious/views/#{{{filename}}}.ecr")
end end
# Similar to Kemals halt method but works in a # Similar to Kemals halt method but works in a

View file

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

View file

@ -4,6 +4,30 @@ 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)
@ -14,7 +38,11 @@ module Invidious::HttpServer
url.query_params = params url.query_params = params
if absolute if absolute
return "#{HOST_URL}#{url.request_target}" if !@@proxy_alive.empty?
return "#{@@proxy_alive}#{url.request_target}"
else
return "#{HOST_URL}#{url.request_target}"
end
else else
return url.request_target return url.request_target
end end

View file

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

View file

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

View file

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

View file

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

View file

@ -9,8 +9,8 @@ module Invidious::Routes::API::Manifest
region = env.params.query["region"]? region = env.params.query["region"]?
if CONFIG.invidious_companion.present? if CONFIG.invidious_companion.present?
companion_public_url = env.get("companion_public_url").as(String) invidious_companion = CONFIG.invidious_companion.sample
return env.redirect "#{companion_public_url}/api/manifest/dash/id/#{id}?#{env.params.query}" return env.redirect "#{invidious_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,13 +221,19 @@ 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
"#{scheme}#{env.request.headers["Host"]}/videoplayback?#{raw_params}" if !proxy.empty?
"#{proxy}/videoplayback?#{raw_params}"
else
"#{scheme}#{env.request.headers["Host"]}/videoplayback?#{raw_params}"
end
end end
end end

View file

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

View file

@ -1,7 +1,6 @@
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"]?
@ -25,50 +24,20 @@ module Invidious::Routes::BeforeAll
extra_connect_csp = "" extra_connect_csp = ""
if CONFIG.invidious_companion.present? if CONFIG.invidious_companion.present?
current_companion_d = host.split(".")[0].scan(/(\d+)$/).last?.try &.[0].to_i extra_media_csp = " #{CONFIG.invidious_companion.sample.public_url}"
extra_connect_csp = " #{CONFIG.invidious_companion.sample.public_url}"
if current_companion_d exvpp_url = BackendInfo.get_exvpp
current_companion_d = current_companion_d - 1 if !exvpp_url.empty?
env.set "using_domain", true extra_media_csp += " #{exvpp_url}"
env.set "current_companion", current_companion_d extra_connect_csp += " #{exvpp_url}"
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
begin
current_companion = env.request.cookies[CONFIG.server_id_cookie_name].value.try &.to_i
rescue
current_companion = rand(CONFIG.invidious_companion.size)
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 end
end
extra_media_csp, extra_connect_csp = BackendInfo.get_csp(env.get("current_companion").as(Int32)) if !CONFIG.external_videoplayback_proxy.empty?
CONFIG.external_videoplayback_proxy.each do |proxy|
extra_media_csp += " #{proxy}"
extra_connect_csp += " #{proxy}"
end
end end
# Allow media resources to be loaded from google servers # Allow media resources to be loaded from google servers
@ -84,16 +53,13 @@ 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: " + "#{scheme}://#{env.request.headers["Host"]?}", "img-src 'self' data: " + HOST_URL,
"font-src 'self' data:", "font-src 'self' data:",
"connect-src 'self'" + extra_connect_csp, "connect-src 'self'" + extra_connect_csp,
"manifest-src 'self'", "manifest-src 'self'",

View file

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

View file

@ -60,7 +60,15 @@ 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)
env.response.cookies["SID"] = Invidious::User::Cookies.sid(env.request.headers["Host"], sid) # Checks if there is any alternative domain, like a second domain name,
# 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
@ -160,7 +168,15 @@ 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)
env.response.cookies["SID"] = Invidious::User::Cookies.sid(env.request.headers["Host"], sid) # Checks if there is any alternative domain, like a second domain name,
# TOR or I2P address
if alt = CONFIG.alternative_domains.index(env.request.headers["Host"])
env.response.cookies["SID"] = Invidious::User::Cookies.sid(CONFIG.alternative_domains[alt], sid)
elsif alt = CONFIG.backend_domains.index(env.request.headers["Host"])
env.response.cookies["SID"] = Invidious::User::Cookies.sid(CONFIG.backend_domains[alt], sid)
else
env.response.cookies["SID"] = Invidious::User::Cookies.sid(CONFIG.domain, sid)
end
if env.request.cookies["PREFS"]? if env.request.cookies["PREFS"]?
user.preferences = env.get("preferences").as(Preferences) user.preferences = env.get("preferences").as(Preferences)

View file

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

View file

@ -3,11 +3,6 @@ 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|
@ -31,7 +26,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/) && !host.matches?(/[\w-]+.c.youtube.com/) if !host.matches?(/[\w-]+.googlevideo.com/)
return error_template(400, "Invalid \"host\" parameter.") return error_template(400, "Invalid \"host\" parameter.")
end end
@ -267,8 +262,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?
companion_public_url = env.get("companion_public_url").as(String) invidious_companion = CONFIG.invidious_companion.sample
return env.redirect "#{companion_public_url}/latest_version?#{env.params.query}" return env.redirect "#{invidious_companion.public_url}/latest_version?#{env.params.query}"
end end
id = env.params.query["id"]? id = env.params.query["id"]?
@ -312,7 +307,16 @@ module Invidious::Routes::VideoPlayback
end end
if local if local
url = URI.parse(url).request_target.not_nil! external_proxy = Invidious::HttpServer::Utils.get_external_proxy
if !external_proxy.empty?
url = URI.parse(url)
external_proxy = URI.parse(external_proxy)
url.host = external_proxy.host
url.port = external_proxy.port
url = url.to_s
else
url = URI.parse(url).request_target.not_nil!
end
url += "&title=#{URI.encode_www_form(title, space_to_plus: false)}" if title url += "&title=#{URI.encode_www_form(title, space_to_plus: false)}" if title
end end

View file

@ -52,7 +52,7 @@ module Invidious::Routes::Watch
env.params.query.delete_all("listen") env.params.query.delete_all("listen")
begin begin
video = get_video(id, region: params.region, env: env) video = get_video(id, region: params.region)
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,10 +61,6 @@ 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"
@ -213,11 +209,6 @@ 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
@ -347,9 +338,8 @@ 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, env: env) video = get_video(video_id)
companion_public_url = env.get("companion_public_url").as(String) return env.redirect "#{video.invidious_companion["baseUrl"].as_s}/latest_version?#{env.params.query}"
return env.redirect "#{companion_public_url}/latest_version?#{env.params.query}"
else else
return Invidious::Routes::VideoPlayback.latest_version(env) return Invidious::Routes::VideoPlayback.latest_version(env)
end end

View file

@ -21,7 +21,6 @@ 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
@ -69,8 +68,6 @@ module Invidious::Routing
# User account management # User account management
get "/change_password", Routes::Account, :get_change_password get "/change_password", Routes::Account, :get_change_password
post "/change_password", Routes::Account, :post_change_password post "/change_password", Routes::Account, :post_change_password
get "/change_username", Routes::Account, :get_change_username
post "/change_username", Routes::Account, :post_change_username
get "/delete_account", Routes::Account, :get_delete get "/delete_account", Routes::Account, :get_delete
post "/delete_account", Routes::Account, :post_delete post "/delete_account", Routes::Account, :post_delete
get "/clear_watch_history", Routes::Account, :get_clear_history get "/clear_watch_history", Routes::Account, :get_clear_history

View file

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

View file

@ -298,7 +298,7 @@ struct Video
predicate_bool upcoming, isUpcoming predicate_bool upcoming, isUpcoming
end end
def get_video(id, refresh = true, region = nil, force_refresh = false, env : HTTP::Server::Context | Nil = nil) def get_video(id, refresh = true, region = nil, force_refresh = false)
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, env : HTT
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, env) video = fetch_video(id, region)
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, env : HTT
end end
end end
else else
video = fetch_video(id, region, env) video = fetch_video(id, region)
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, env : HTT
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, env) return fetch_video(id, region)
end end
def fetch_video(id, region, env) def fetch_video(id, region)
info = extract_video_info(video_id: id, env: env) info = extract_video_info(video_id: id)
if reason = info["reason"]? if reason = info["reason"]?
if reason == "Video unavailable" if reason == "Video unavailable"

View file

@ -58,12 +58,12 @@ def parse_related_video(related : JSON::Any) : Hash(String, JSON::Any)?
} }
end end
def extract_video_info(video_id : String, env : HTTP::Server::Context | Nil = nil) def extract_video_info(video_id : String)
# 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(env: env, video_id: video_id, params: "2AMB", client_config: client_config) player_response = YoutubeAPI.player(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, env : HTTP::Server::Context | Nil = ni
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, env : HTTP::Server::Context | Nil = ni
# 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, env) new_player_response = try_fetch_streaming_data(video_id, client_config)
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, env : HTTP::Server::Context | Nil = ni
end end
end end
{"captions", "playabilityStatus", "playerConfig", "storyboards"}.each do |f| {"captions", "playabilityStatus", "playerConfig", "storyboards", "invidiousCompanion"}.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, env : HTTP::Server::Context | Nil = ni
return params return params
end end
def try_fetch_streaming_data(id : String, client_config : YoutubeAPI::ClientConfig, env : HTTP::Server::Context | Nil = nil) : Hash(String, JSON::Any)? def try_fetch_streaming_data(id : String, client_config : YoutubeAPI::ClientConfig) : 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, env: env) response = YoutubeAPI.player(video_id: id, params: "2AMB", client_config: client_config)
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,6 +200,7 @@ 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
@ -215,13 +216,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.try &.["lengthSeconds"]? || video_details["lengthSeconds"]) length_txt = (microformat["lengthSeconds"]? || video_details["lengthSeconds"])
.try &.as_s.to_i64 .try &.as_s.to_i64
published = microformat.try &.["publishDate"]? published = microformat["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.try &.dig?("liveBroadcastDetails", "startTimestamp") premiere_timestamp = microformat.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?(
@ -232,7 +233,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.try &.dig?("liveBroadcastDetails", "isLiveNow") live_now = microformat.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
@ -241,11 +242,11 @@ def parse_video_info(video_id : String, player_response : Hash(String, JSON::Any
# Extra video infos # Extra video infos
allowed_regions = microformat.try &.["availableCountries"]? allowed_regions = microformat["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.try &.["isFamilySafe"].try &.as_bool family_friendly = microformat["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
@ -328,8 +329,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) }
@ -342,7 +343,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.try &.["category"]? genre = microformat["category"]?
genre_ucid = nil genre_ucid = nil
license = nil license = nil

View file

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

View file

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

View file

@ -3,7 +3,6 @@
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
@ -33,19 +32,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="<%= scheme %>://<%= host %>/channel/<%= ucid %>"> <meta property="og:url" content="<%= host %>/channel/<%= ucid %>">
<meta property="og:title" content="<%= author %>"> <meta property="og:title" content="<%= author %>">
<meta property="og:image" content="<%= scheme %>://<%= host %>/ggpht<%= channel_profile_pic %>"> <meta property="og:image" content="<%= 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="<%= scheme %>://<%= host %>/channel/<%= ucid %>"> <meta name="twitter:url" content="<%= 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="<%= scheme %>://<%= host %>/ggpht<%= channel_profile_pic %>"> <meta name="twitter:image" content="<%= host %>/ggpht<%= channel_profile_pic %>">
<link rel="alternate" type="application/rss+xml" title="RSS" href="/feed/channel/<%= ucid %>" /> <link rel="alternate" type="application/rss+xml" title="RSS" href="/feed/channel/<%= ucid %>" />
<%- end -%> <%- end -%>
<script src="/<%= JS_PATH %>/pagination.js?v=<%= ASSET_COMMIT %>"></script> <script src="/js/pagination.js?v=<%= ASSET_COMMIT %>"></script>
<link rel="alternate" href="<%= youtube_url %>"> <link rel="alternate" href="<%= youtube_url %>">
<title><%= author %> - Invidious</title> <title><%= author %> - Invidious</title>

View file

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

View file

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

View file

@ -22,9 +22,8 @@
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
companion_public_url = env.get("companion_public_url").as(String) src_url = video.invidious_companion["baseUrl"].as_s + src_url +
src_url = companion_public_url + src_url + "&check=#{invidious_companion_encrypt(video.id)}" if (CONFIG.invidious_companion.present?)
"&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)
@ -39,9 +38,8 @@
<% 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"
companion_public_url = env.get("companion_public_url").as(String) src_url = video.invidious_companion["baseUrl"].as_s + src_url +
src_url = companion_public_url + src_url + "&check=#{invidious_companion_encrypt(video.id)}" if (CONFIG.invidious_companion.present?)
"&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 %>
@ -52,9 +50,8 @@
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
companion_public_url = env.get("companion_public_url").as(String) src_url = video.invidious_companion["baseUrl"].as_s + src_url +
src_url = companion_public_url + src_url + "&check=#{invidious_companion_encrypt(video.id)}" if (CONFIG.invidious_companion.present?)
"&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)
@ -89,4 +86,4 @@
}.to_pretty_json }.to_pretty_json
%> %>
</script> </script>
<script src="/<%= JS_PATH %>/player.js?v=<%= ASSET_COMMIT %>"></script> <script src="/js/player.js?v=<%= ASSET_COMMIT %>"></script>

View file

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

View file

@ -11,7 +11,7 @@
<link rel="stylesheet" href="/css/default.css?v=<%= ASSET_COMMIT %>"> <link rel="stylesheet" href="/css/default.css?v=<%= ASSET_COMMIT %>">
<link rel="stylesheet" href="/css/embed.css?v=<%= ASSET_COMMIT %>"> <link rel="stylesheet" href="/css/embed.css?v=<%= ASSET_COMMIT %>">
<title><%= HTML.escape(video.title) %> - Invidious</title> <title><%= HTML.escape(video.title) %> - Invidious</title>
<script src="/<%= JS_PATH %>/_helpers.js?v=<%= ASSET_COMMIT %>"></script> <script src="/js/_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_PATH %>/embed.js?v=<%= ASSET_COMMIT %>"></script> <script src="/js/embed.js?v=<%= ASSET_COMMIT %>"></script>
</body> </body>
</html> </html>

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,6 +1,8 @@
<% <%
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 %>">
@ -22,7 +24,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_PATH %>/_helpers.js?v=<%= ASSET_COMMIT %>"></script> <script src="/js/_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">
@ -32,9 +34,7 @@
<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"> <a href="/" class="index-link pure-menu-heading">Invidious</a>
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,55 +106,31 @@
</div> </div>
</div> </div>
<% <% if !CONFIG.backends.empty? %>
if CONFIG.invidious_companion.present? <% if !CONFIG.backend_domains.includes?(env.request.headers["Host"]) %>
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>
<% if domain %> <% CONFIG.backends.each do | backend | %>
<% CONFIG.invidious_companion.each_with_index do | companion, index | %> <% backend = backend.split(CONFIG.backends_delimiter) %>
<% host_backend = env.request.headers["Host"].sub(/([^.]+)(\d+)/, "\\1#{index+1}") %> <% if current_backend == backend[0] %>
<% is_current_backend_host = host_backend == env.request.headers["Host"] %> <a href="/switchbackend?backend_id=<%= backend[0] %>" style="text-decoration-line: underline; display: inline-block;">
<a href="<%= scheme %>://<%= host_backend %><%= env.request.resource %>" style="<%= is_current_backend_host ? "text-decoration-line: underline;" : "" %> display: inline-block;"> Backend<%= HTML.escape(backend[0]) %>
Backend<%= HTML.escape((index + 1).to_s) %> <%= HTML.escape(companion.note) %> <% if backend.size == 2 %>
<span style="color: <%= HTML.escape(backend[1]) %>
<% if status[index] == 0 %> #fd4848; <% end %> <% end %>
<% if status[index] == 1 %> #d06925; <% end %> </a> <span> | </span>
<% if status[index] == 2 %> #42ae3c; <% end %> <% else %>
">•</span> <a href="/switchbackend?backend_id=<%= backend[0] %>" style="display: inline-block;">
</a> Backend<%= HTML.escape(backend[0]) %>
<% if !(index == CONFIG.invidious_companion.size-1) %> <% if backend.size == 2 %>
<span> | </span> <%= HTML.escape(backend[1]) %>
<% end %> <% end %>
<% end %> </a> <span> | </span>
<% else %> <% end %>
<% CONFIG.invidious_companion.each_with_index do | companion, index | %> <% end %>
<a href="/switchbackend?backend_id=<%= index.to_s %>" style="<%= current_backend == index ? "text-decoration-line: underline;" : "" %> display: inline-block;">
Backend<%= HTML.escape((index + 1).to_s) %> <%= HTML.escape(companion.note) %>
<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 %>
</div> </div>
<% end %> <% end %>
<% end %>
<% if companion_switched %>
<div class="h-box">
<p><%= translate(locale, "backend_unavailable") %></p>
</div>
<% end %>
<% if CONFIG.banner %> <% if CONFIG.banner %>
<div class="h-box"> <div class="h-box">
@ -164,204 +140,202 @@
<%= content %> <%= content %>
<% if buffer_footer %> <% if buffer_footer %>
<div id="footer_buffer"></div> <div id="footer_buffer"></div>
<% end %> <% end %>
</div> </div>
</div> </div>
<script src="/<%= JS_PATH %>/handlers.js?v=<%= ASSET_COMMIT %>"></script> <script src="/js/handlers.js?v=<%= ASSET_COMMIT %>"></script>
<script src="/<%= JS_PATH %>/themes.js?v=<%= ASSET_COMMIT %>"></script> <script src="/js/themes.js?v=<%= ASSET_COMMIT %>"></script>
<% if env.get? "user" %> <% if env.get? "user" %>
<script src="/<%= JS_PATH %>/sse.js?v=<%= ASSET_COMMIT %>"></script> <script src="/js/sse.js?v=<%= ASSET_COMMIT %>"></script>
<script id="notification_data" type="application/json"> <script id="notification_data" type="application/json">
<%= <%=
{ {
"upload_text" => HTML.escape(translate(locale, "`x` uploaded a video")), "upload_text" => HTML.escape(translate(locale, "`x` uploaded a video")),
"live_upload_text" => HTML.escape(translate(locale, "`x` is live")) "live_upload_text" => HTML.escape(translate(locale, "`x` is live"))
}.to_pretty_json }.to_pretty_json
%> %>
</script> </script>
<% if CONFIG.enable_user_notifications %> <% if CONFIG.enable_user_notifications %>
<script src="/<%= JS_PATH %>/notifications.js?v=<%= ASSET_COMMIT %>"></script> <script src="/js/notifications.js?v=<%= ASSET_COMMIT %>"></script>
<% end %> <% end %>
<% end %> <% end %>
<footer class="pure-g"> <footer class="pure-g">
<div class="pure-u-1 pure-u-md-2-24"></div> <div class="pure-u-1 pure-u-md-2-24"></div>
<div class="h-box pure-u-1 pure-u-md-20-24" id="footer-content-container"> <div class="h-box pure-u-1 pure-u-md-20-24" id="footer-content-container">
<div class="pure-u-1 footer-content"> <div class="pure-u-1 footer-content">
<div class="footer-section pure-u-1-4" id="footer-custom-text"> <div class="footer-section pure-u-1-4" id="footer-custom-text">
<b>Invidious</b> <b>Invidious</b>
<% if CONFIG.footer %> <% if CONFIG.footer %>
<p><%=CONFIG.footer%></p> <p><%=CONFIG.footer%></p>
<% else %> <% else %>
<p><%=translate(locale, "default_invidious_footer_text")%></p> <p><%=translate(locale, "default_invidious_footer_text")%></p>
<% end %> <% end %>
</div>
<div class="footer-section">
<b class="footer-section-header"><%= translate(locale, "footer_navigation_section_header")%></b>
<ul class="pure-menu-list footer-section-list">
<li class="pure-menu-item footer-section-item">
<a href="/" title="<%= translate(locale, "footer_home_link")%>">
<%= translate(locale, "footer_home_link") %>
</a>
</li>
<li class="pure-menu-item footer-section-item">
<a href="/feed/popular" title="<%= translate(locale, "Popular")%>">
<%= translate(locale, "Popular") %>
</a>
</li>
<li class="pure-menu-item footer-section-item">
<a href="/feed/trending" title="<%= translate(locale, "Trending")%>" style="">
<%= translate(locale, "Trending") %>
</a>
</li>
<li class="pure-menu-item footer-section-item">
<a href="/search" title="<%= translate(locale, "Search")%>">
<%= translate(locale, "Search") %>
</a>
</li>
</ul>
</div>
<div class="footer-section">
<b class="footer-section-header">Invidious</b>
<ul class="pure-menu-list footer-section-list">
<li class="pure-menu-item footer-section-item">
<a href="https://invidious.io" title="<%= translate(locale, "footer_project_homepage_link")%>">
<%= translate(locale, "footer_project_homepage_link") %>
</a>
</li>
<li class="pure-menu-item footer-section-item">
<a href="https://github.com/iv-org/invidious" title="<%= translate(locale, "footer_source_code_link")%>">
<%= translate(locale, "footer_source_code_link") %>
</a>
</li>
<li class="pure-menu-item footer-section-item">
<a href="https://github.com/iv-org/invidious/issues" title="<%= translate(locale, "footer_issue_tracker_link")%>" style="">
<%= translate(locale, "footer_issue_tracker_link") %>
</a>
</li>
<li class="pure-menu-item footer-section-item">
<a href="https://instances.invidious.io" title="<%= translate(locale, "footer_public_instances_link")%>">
<%= translate(locale, "footer_public_instances_link") %>
</a>
</li>
<li class="pure-menu-item footer-section-item">
<a href="https://invidious.io/donate" title="<%= translate(locale, "footer_donate_link")%>">
<%= translate(locale, "footer_donate_link") %>
</a>
</li>
<li class="pure-menu-item footer-section-item">
<a href="https://matrix.to/#/#invidious:matrix.org" title="<%= translate(locale, "footer_matrix_link")%>">
<%= translate(locale, "footer_matrix_link") %>
</a>
</li>
</div> </ul>
<div class="footer-section"> </div>
<b class="footer-section-header"><%= translate(locale, "footer_navigation_section_header")%></b> <% if CONFIG.instance_maintainer_email || CONFIG.modified_source_code_url || CONFIG.footer_instance_tos_link || CONFIG.footer_instance_privacy_policy_link %>
<ul class="pure-menu-list footer-section-list"> <div class="footer-section">
<li class="pure-menu-item footer-section-item"> <b class="footer-section-header">
<a href="/" title="<%= translate(locale, "footer_home_link")%>"> <% if CONFIG.modified_source_code_url %>
<%= translate(locale, "footer_home_link") %> <%= translate(locale, "footer_instance_section_header_modified_source")%>
</a> <% else %>
</li> <%= translate(locale, "footer_instance_section_header")%>
<li class="pure-menu-item footer-section-item"> <% end %>
<a href="/feed/popular" title="<%= translate(locale, "Popular")%>"> </b>
<%= translate(locale, "Popular") %> <ul class="pure-menu-list footer-section-list">
</a> <% if CONFIG.instance_maintainer_email %>
</li> <li class="pure-menu-item footer-section-item">
<li class="pure-menu-item footer-section-item"> <a href="<%=HTML.escape("mailto:#{CONFIG.instance_maintainer_email.not_nil!}")%>" title="<%= translate(locale, "footer_contact_link")%>">
<a href="/feed/trending" title="<%= translate(locale, "Trending")%>" style=""> <%= translate(locale, "footer_contact_link") %>
<%= translate(locale, "Trending") %> </a>
</a> </li>
</li> <% end %>
<li class="pure-menu-item footer-section-item"> <% if CONFIG.modified_source_code_url %>
<a href="/search" title="<%= translate(locale, "Search")%>"> <li class="pure-menu-item footer-section-item">
<%= translate(locale, "Search") %> <a href="<%=HTML.escape(CONFIG.modified_source_code_url.not_nil!)%>" title="<%= translate(locale, "footer_instance_section_modified_source_code")%>">
</a> <%= translate(locale, "footer_instance_section_modified_source_code") %>
</li> </a>
</ul> </li>
</div> <% end %>
<div class="footer-section"> <% if CONFIG.footer_instance_tos_link %>
<b class="footer-section-header">Invidious</b> <li class="pure-menu-item footer-section-item">
<ul class="pure-menu-list footer-section-list"> <a href="<%=HTML.escape(CONFIG.footer_instance_tos_link.not_nil!)%>" title="<%= translate(locale, "footer_instance_section_tos")%>">
<li class="pure-menu-item footer-section-item"> <%= translate(locale, "footer_instance_section_tos") %>
<a href="https://invidious.io" title="<%= translate(locale, "footer_project_homepage_link")%>"> </a>
<%= translate(locale, "footer_project_homepage_link") %> </li>
</a> <% end %>
</li> <% if CONFIG.footer_instance_privacy_policy_link %>
<li class="pure-menu-item footer-section-item"> <li class="pure-menu-item footer-section-item">
<a href="https://github.com/iv-org/invidious" title="<%= translate(locale, "footer_source_code_link")%>"> <a href="<%=HTML.escape(CONFIG.footer_instance_privacy_policy_link.not_nil!)%>" title="<%= translate(locale, "footer_instance_section_privacy_policy")%>">
<%= translate(locale, "footer_source_code_link") %> <%= translate(locale, "footer_instance_section_privacy_policy") %>
</a> </a>
</li> </li>
<li class="pure-menu-item footer-section-item"> <% end %>
<a href="https://github.com/iv-org/invidious/issues" title="<%= translate(locale, "footer_issue_tracker_link")%>" style="">
<%= translate(locale, "footer_issue_tracker_link") %>
</a>
</li>
<li class="pure-menu-item footer-section-item">
<a href="https://instances.invidious.io" title="<%= translate(locale, "footer_public_instances_link")%>">
<%= translate(locale, "footer_public_instances_link") %>
</a>
</li>
<li class="pure-menu-item footer-section-item">
<a href="https://invidious.io/donate" title="<%= translate(locale, "footer_donate_link")%>">
<%= translate(locale, "footer_donate_link") %>
</a>
</li>
<li class="pure-menu-item footer-section-item">
<a href="https://matrix.to/#/#invidious:matrix.org" title="<%= translate(locale, "footer_matrix_link")%>">
<%= translate(locale, "footer_matrix_link") %>
</a>
</li>
</ul> <% if CONFIG.footer_instance_donate_link %>
</div> <li class="pure-menu-item footer-section-item">
<% if CONFIG.instance_maintainer_email || CONFIG.modified_source_code_url || CONFIG.footer_instance_tos_link || CONFIG.footer_instance_privacy_policy_link %> <a href="<%=HTML.escape(CONFIG.footer_instance_donate_link.not_nil!)%>" title="<%= translate(locale, "footer_instance_section_donate")%>">
<div class="footer-section"> <%= translate(locale, "footer_instance_section_donate") %>
<b class="footer-section-header"> </a>
<% if CONFIG.modified_source_code_url %> </li>
<%= translate(locale, "footer_instance_section_header_modified_source")%> <% end %>
<% else %>
<%= translate(locale, "footer_instance_section_header")%>
<% end %>
</b>
<ul class="pure-menu-list footer-section-list">
<% if CONFIG.instance_maintainer_email %>
<li class="pure-menu-item footer-section-item">
<a href="<%=HTML.escape("mailto:#{CONFIG.instance_maintainer_email.not_nil!}")%>" title="<%= translate(locale, "footer_contact_link")%>">
<%= translate(locale, "footer_contact_link") %>
</a>
</li>
<% end %>
<% if CONFIG.modified_source_code_url %>
<li class="pure-menu-item footer-section-item">
<a href="<%=HTML.escape(CONFIG.modified_source_code_url.not_nil!)%>" title="<%= translate(locale, "footer_instance_section_modified_source_code")%>">
<%= translate(locale, "footer_instance_section_modified_source_code") %>
</a>
</li>
<% end %>
<% if CONFIG.footer_instance_tos_link %>
<li class="pure-menu-item footer-section-item">
<a href="<%=HTML.escape(CONFIG.footer_instance_tos_link.not_nil!)%>" title="<%= translate(locale, "footer_instance_section_tos")%>">
<%= translate(locale, "footer_instance_section_tos") %>
</a>
</li>
<% end %>
<% if CONFIG.footer_instance_privacy_policy_link %>
<li class="pure-menu-item footer-section-item">
<a href="<%=HTML.escape(CONFIG.footer_instance_privacy_policy_link.not_nil!)%>" title="<%= translate(locale, "footer_instance_section_privacy_policy")%>">
<%= translate(locale, "footer_instance_section_privacy_policy") %>
</a>
</li>
<% end %>
<% if CONFIG.footer_instance_donate_link %> <% CONFIG.footer_instance_section_custom_fields.each do | field | %>
<li class="pure-menu-item footer-section-item"> <li class="pure-menu-item footer-section-item">
<a href="<%=HTML.escape(CONFIG.footer_instance_donate_link.not_nil!)%>" title="<%= translate(locale, "footer_instance_section_donate")%>"> <a href="<%=HTML.escape(field[1])%>" title="<%= HTML.escape(field[0]) %>">
<%= translate(locale, "footer_instance_section_donate") %> <%= HTML.escape(field[0]) %>
</a> </a>
</li> </li>
<% end %> <% end %>
</ul>
</div>
<% end %>
<% CONFIG.footer_instance_section_custom_fields.each do | field | %> <div class="footer-section">
<li class="pure-menu-item footer-section-item"> <b class="footer-section-header"><%= translate(locale, "footer_support_section_header")%></b>
<a href="<%=HTML.escape(field[1])%>" title="<%= HTML.escape(field[0]) %>"> <ul class="pure-menu-list footer-section-list">
<%= HTML.escape(field[0]) %> <li class="pure-menu-item footer-section-item">
</a> <a href="https://github.com/iv-org/invidious/issues/new" title="<%= translate(locale, "footer_report_bug_link")%>">
</li> <%= translate(locale, "footer_report_bug_link") %>
<% end %> </a>
</ul> </li>
</div> <li class="pure-menu-item footer-section-item">
<% end %> <a href="#" title="<%= translate(locale, "footer_faq_link")%>" style="">
<%= translate(locale, "footer_faq_link") %>
<div class="footer-section"> </a>
<b class="footer-section-header"><%= translate(locale, "footer_support_section_header")%></b> </li>
<ul class="pure-menu-list footer-section-list"> </ul>
<li class="pure-menu-item footer-section-item"> </div>
<a href="https://github.com/iv-org/invidious/issues/new" title="<%= translate(locale, "footer_report_bug_link")%>"> </div>
<%= translate(locale, "footer_report_bug_link") %> <hr/>
</a> <div class="footer-footer">
</li> <div class="box">You are currently using Backend: <%= current_backend %></div>
<li class="pure-menu-item footer-section-item"> <% if !current_external_videoplayback_proxy.empty? %>
<a href="#" title="<%= translate(locale, "footer_faq_link")%>" style=""> <div class="box">External Videoplayback Proxy: <%= current_external_videoplayback_proxy %></div>
<%= translate(locale, "footer_faq_link") %> <% end %>
</a> <span class="left">
</li> <% if CONFIG.modified_source_code_url %>
</ul> <%= translate(locale, "footer_current_version_modified") %>
</div> <% else %>
</div> <%= translate(locale, "Current version: ") %>
<hr/> <% end %>
<div class="footer-footer">
<% <%= CURRENT_VERSION %>-<%= CURRENT_COMMIT %> @ <%= CURRENT_BRANCH %>
if CONFIG.invidious_companion.present? </span>
companion_public_url = env.get("companion_public_url").as(String) <div class="right">
%> <a href="/privacy" title="<%= translate(locale, "footer_privacy_policy_link")%>"><%= translate(locale, "footer_privacy_policy_link") %></a>
<div class="box">You are currently using Backend: <%= current_backend ? companion_public_url : "Unable to get backend, this is bug, please report it!" %></div> <span> | </span>
<% end %> <a href="/licenses" title="<%= translate(locale, "footer_licences_link")%>"><%= translate(locale, "footer_licences_link") %></a>
<span class="left"> </div>
<% if CONFIG.modified_source_code_url %> </div>
<%= translate(locale, "footer_current_version_modified") %> </div>
<% else %> </div>
<%= translate(locale, "Current version: ") %> <div class="pure-u-1 pure-u-md-2-24"></div>
<% end %> </footer>
<%= CURRENT_VERSION %>-<%= CURRENT_COMMIT %> @ <%= CURRENT_BRANCH %>
</span>
<div class="right">
<a href="/privacy" title="<%= translate(locale, "footer_privacy_policy_link")%>"><%= translate(locale, "footer_privacy_policy_link") %></a>
<span> | </span>
<a href="/licenses" title="<%= translate(locale, "footer_licences_link")%>"><%= translate(locale, "footer_licences_link") %></a>
</div>
</div>
</div>
</div>
<div class="pure-u-1 pure-u-md-2-24"></div>
</footer>
</body> </body>
</html> </html>

View file

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

View file

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

View file

@ -2,32 +2,31 @@
<% 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="<%= scheme %>://<%= host %><%= thumbnail %>"> <meta name="thumbnail" content="<%= 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="<%= scheme %>://<%= host %>/watch?v=<%= video.id %>"> <meta property="og:url" content="<%= host %>/watch?v=<%= video.id %>">
<meta property="og:title" content="<%= title %>"> <meta property="og:title" content="<%= title %>">
<meta property="og:image" content="<%= scheme %>://<%= host %>/vi/<%= video.id %>/maxres.jpg"> <meta property="og:image" content="<%= host %>/vi/<%= video.id %>/maxres.jpg">
<meta property="og:description" content="<%= HTML.escape(video.short_description) %>"> <meta property="og: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. -->
<meta property="og:video" content="<%= video_url %>"> <meta property="og:video" content="<%= video_url %>">
<meta property="og:video:url" content="<%= video_url %>"> <meta property="og:video:url" content="<%= video_url %>">
<meta property="og:video:secure_url" content="<%= video_url %>"> <meta property="og:video:secure_url" content="<%= video_url %>">
<meta property="og:video:type" content="video/mp4"> <meta property="og:video:type" content="video/mp4">
<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="<%= scheme %>://<%= host %>/watch?v=<%= video.id %>"> <meta name="twitter:url" content="<%= 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="<%= scheme %>://<%= host %>/vi/<%= video.id %>/maxres.jpg"> <meta name="twitter:image" content="<%= host %>/vi/<%= video.id %>/maxres.jpg">
<meta name="twitter:player" content="<%= scheme %>://<%= host %>/embed/<%= video.id %>"> <meta name="twitter:player" content="<%= 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 %>">
@ -199,7 +198,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_PATH %>/playlist_widget.js?v=<%= Time.utc.to_unix_ms %>"></script> <script src="/js/playlist_widget.js?v=<%= Time.utc.to_unix_ms %>"></script>
<% end %> <% end %>
<% end %> <% end %>
@ -208,15 +207,13 @@ 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 %> <% else %>
<% else %> <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>
@ -228,16 +225,14 @@ 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 %> <%= translate(locale, "Whitelisted regions: ") %><%= video.allowed_regions.join(", ") %>
<%= translate(locale, "Whitelisted regions: ") %><%= video.allowed_regions.join(", ") %> <% else %>
<% else %> <%= translate(locale, "Blacklisted regions: ") %><%= (REGIONS.to_a - video.allowed_regions).join(", ") %>
<%= translate(locale, "Blacklisted regions: ") %><%= (REGIONS.to_a - video.allowed_regions).join(", ") %> <% end %>
<% end %> </p>
</p>
<% end %>
<% end %> <% end %>
</div> </div>
</div> </div>
@ -391,5 +386,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_PATH %>/comments.js?v=<%= ASSET_COMMIT %>"></script> <script src="/js/comments.js?v=<%= ASSET_COMMIT %>"></script>
<script src="/<%= JS_PATH %>/watch.js?v=<%= ASSET_COMMIT %>"></script> <script src="/js/watch.js?v=<%= ASSET_COMMIT %>"></script>

View file

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

View file

@ -456,7 +456,6 @@ 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 = {
@ -493,7 +492,7 @@ module YoutubeAPI
end end
if CONFIG.invidious_companion.present? if CONFIG.invidious_companion.present?
return self._post_invidious_companion("/youtubei/v1/player", data, env) return self._post_invidious_companion("/youtubei/v1/player", data)
else else
return self._post_json("/youtubei/v1/player", data, client_config) return self._post_json("/youtubei/v1/player", data, client_config)
end end
@ -673,8 +672,7 @@ 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",
@ -688,12 +686,7 @@ module YoutubeAPI
# Send the POST request # Send the POST request
begin begin
if env.nil? response = COMPANION_POOL.client &.post(endpoint, headers: headers, body: data.to_json)
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(