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 }}
platforms: linux/amd64
push: true
cache-from: type=gha
cache-to: type=gha,mode=max
build-args: |
"release=1"

View file

@ -2,102 +2,7 @@
## 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
### Wrap-up

View file

@ -13,49 +13,33 @@ https://git.nadeko.net/Fijxu/-/packages/container/invidious/latest
## Features and changes of this fork:
- ~~[Use a Redis compatible DB for video cache instead of just PostgreSQL](https://git.nadeko.net/Fijxu/invidious/commit/bbc5913b8dacaed4d466bcc466a0782d5e3f5edc): Invidious by default caches the video information for some hours in PostgreSQL. Since the data is accessed a lot, it is better off using an in memory database instead, it's faster and it will not wear out your SSD (due to constant writes to the database).~~
- [Use a Redis compatible DB for video cache instead of just PostgreSQL](https://git.nadeko.net/Fijxu/invidious/commit/bbc5913b8dacaed4d466bcc466a0782d5e3f5edc): Invidious by default caches the video information for some hours in PostgreSQL. Since the data is accessed a lot, it is better off using an in memory database instead, it's faster and it will not wear out your SSD (due to constant writes to the database).
~~It can be set using this on `config.yml`:~~
```yaml
redis_url: tcp://127.0.0.1:6379
```
- [Ability to use different video caching backends](https://git.nadeko.net/Fijxu/invidious/commit/e76867aaba022d64ebab73648a37a0c63b788e0f): If you want, you can the PostgreSQL video cache the Redis one or the built-in in memory one that uses the LRU algorithm. Redis and LRU are recommended for public instances, but since Invidious has memory leaks, the LRU cache is lost if Invidious crashes or it's restarted, so because of this, redis is the default option.
```yaml
video_cache:
enabled: true
backend: 1 # 0 is PSQL, 1 Redis, 2 Built-in LRU
lru_max_size: 18000 # ~500MB (ignored if backend is 0 or 1)
```
If you choose to use Redis, make sure to set the `redis_url` config property:
```yaml
redis_url: tcp://127.0.0.1:6379
```
It can be set using this on `config.yml`:
```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
- 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 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`:
```yaml
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
It can be set using this on `config.yml`:
```yaml
use_innertube_for_feeds: false
```
---

View file

@ -1 +0,0 @@
minified

View file

@ -1072,7 +1072,8 @@ default_user_preferences:
##
#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"
# contact_url: "https://example.com/contact"
# 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
@ -7,7 +7,6 @@ ARG release
WORKDIR /invidious
COPY ./shard.yml ./shard.yml
COPY ./shard.lock ./shard.lock
RUN shards install --production
COPY ./src/ ./src/
@ -20,11 +19,18 @@ COPY ./scripts/ ./scripts/
COPY ./assets/ ./assets/
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 \
--release --mcpu=x86-64-v2 \
--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
RUN apk add --no-cache rsvg-convert ttf-opensans tini tzdata

View file

@ -522,12 +522,6 @@
"carousel_slide": "Slide {{current}} of {{total}}",
"carousel_skip": "Skip the Carousel",
"carousel_go_to": "Go to slide `x`",
"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"
"footer_contact_url": "Contact the Administrator"
}

View file

@ -518,12 +518,5 @@
"carousel_go_to": "Ir a la diapositiva `x`",
"footer_contact_url": "Contactar al Administrador",
"preferences_preload_label": "Precargar datos del vídeo: ",
"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"
"Filipino (auto-generated)": "Filipino (generado automáticamente)"
}

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

View file

@ -1,5 +1,5 @@
name: invidious
version: 2.20250314.0-dev
version: 2.20241110.0-dev
authors:
- Invidious team <contact@invidious.io>
@ -17,7 +17,10 @@ dependencies:
version: ~> 0.21.0
kemal:
github: kemalcr/kemal
version: ~> 1.6.0
version: ~> 1.1.2
kilt:
github: jeromegn/kilt
version: ~> 0.6.1
protodec:
github: iv-org/protodec
version: ~> 0.1.5
@ -25,7 +28,7 @@ dependencies:
github: athena-framework/negotiation
version: ~> 0.1.1
redis:
github: jgaskins/redis
github: stefanwille/crystal-redis
inotify:
github: petoem/inotify.cr
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
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)
if env.request.method == "GET" && env.request.headers.has_key?("Range")

View file

@ -17,8 +17,10 @@
require "digest/md5"
require "file_utils"
# Require kemal, then our own overrides
# Require kemal, kilt, then our own overrides
require "kemal"
require "kilt"
require "./ext/kemal_content_for.cr"
require "./ext/kemal_static_file_handler.cr"
require "http_proxy"
@ -49,8 +51,7 @@ require "./invidious/channels/*"
require "./invidious/user/*"
require "./invidious/search/*"
require "./invidious/routes/**"
require "./invidious/jobs/base_job"
require "./invidious/jobs/*"
require "./invidious/jobs/**"
# Declare the base namespace for 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)
COMPANION_POOL = [] of CompanionConnectionPool
CONFIG.invidious_companion.each do |companion|
COMPANION_POOL << CompanionConnectionPool.new(companion, capacity: CONFIG.pool_size)
end
COMPANION_POOL = CompanionConnectionPool.new(
capacity: CONFIG.pool_size
)
# CLI
Kemal.config.extra_options do |parser|
@ -160,15 +159,6 @@ LOGGER = Invidious::LogHandler.new(OUTPUT, CONFIG.log_level, CONFIG.colorize_log
# Check table integrity
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) %}
# 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
if !CONFIG.external_videoplayback_proxy.empty?
Invidious::Jobs.register Invidious::Jobs::CheckExternalProxy.new
else
# Invidious will it's own videoplayback proxy unless the admin decides to rewrite
# the /videoplayback location in the reverse proxy configuration (NGINX, Caddy, etc)
LOGGER.info("jobs: Disabling CheckExternalProxy job. Invidious will it's own videoplayback proxy")
end
if !CONFIG.tokens_server.empty?
Invidious::Jobs.register Invidious::Jobs::RefreshSessionTokens.new
else
@ -227,8 +225,6 @@ end
if CONFIG.invidious_companion.present?
Invidious::Jobs.register Invidious::Jobs::CheckBackend.new
else
LOGGER.info("jobs: Disabling CheckBackend job. invidious-companion and their respective external video playback proxies (if set on invidious-companion) will not be checked")
end
Invidious::Jobs.start_all
@ -253,8 +249,8 @@ error 500 do |env, ex|
error_template(500, ex)
end
static_headers do |env|
env.response.headers.add("Cache-Control", "max-age=2629800")
static_headers do |response|
response.headers.add("Cache-Control", "max-age=2629800")
end
# Init Kemal

View file

@ -52,7 +52,6 @@ struct ConfigPreferences
property vr_mode : Bool = true
property show_nick : Bool = true
property save_player_pos : Bool = false
property enable_dearrow : Bool = false
def to_tuple
{% begin %}
@ -83,12 +82,6 @@ class Config
@[YAML::Field(converter: Preferences::URIConverter)]
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
# 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
@[YAML::Field(converter: Preferences::URIConverter)]
property database_url : URI = URI.parse("")
@[YAML::Field(converter: Preferences::URIConverter)]
property redis_url : URI = URI.parse("")
property redis_url : String?
property redis_socket : String?
# Use polling to keep decryption function up to date
property decrypt_polling : Bool = false
# Used for crawling channels: threads should check all videos uploaded by a channel
@ -128,6 +121,10 @@ class Config
property domain : String?
# Materialious redirects
property materialious_domain : String?
# Alternative domains. You can add other domains, like TOR and I2P addresses
property alternative_domains : Array(String) = [] of String
# Backend domains. Domains for numbered backends
property backend_domains : Array(String) = [] of String
# Subscribe to channels using PubSubHubbub (requires domain, hmac_key)
property use_pubsub_feeds : Bool | Int32 = false
@ -219,9 +216,13 @@ class Config
# of the backend
property backends_delimiter : String = "|"
# External videoplayback proxies list. They should include `https://`
# at the start of the URI
property external_videoplayback_proxy : Array(String) = [] of String
property pubsub_domain : String = ""
property server_id_cookie_name : String = "COMPANION_ID"
property server_id_cookie_name : String = "INVIDIOUS_SERVER_ID"
property tokens_server : String = ""
@ -236,14 +237,6 @@ class Config
property lru_max_size : Int32 = 18432 # ~512MB
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) %}
property reload_config_automatically : Bool = true
{% end %}
@ -390,14 +383,10 @@ class Config
elsif config.invidious_companion_key == "CHANGE_ME!!"
puts "Config: The value of 'invidious_companion_key' needs to be changed!!"
exit(1)
elsif config.invidious_companion_key.size != 16
puts "Config: The value of 'invidious_companion_key' needs to be a size of 16 characters."
elsif config.invidious_companion_key.size < 16
puts "Config: The value of 'invidious_companion_key' needs to be a size of 16 or more."
exit(1)
end
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
# HMAC_key is mandatory

View file

@ -149,7 +149,7 @@ module Invidious::Database::ChannelVideos
SELECT DISTINCT ON (ucid) *
FROM channel_videos
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
SQL

View file

@ -184,36 +184,6 @@ module Invidious::Database::Users
PG_DB.exec(request, pass, user.email)
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
# -------------------

View file

@ -97,20 +97,21 @@ module Invidious::Database::Videos
end
class Redis_
@redis : Redis::Client
@redis : Redis::PooledClient
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 "Connecting to Redis compatible DB"
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
def set(video : Video, expire_time)
@redis.set(video.id, video.info.to_json, ex: expire_time)
@redis.set(video.id + ":time", video.updated.to_s, ex: expire_time)
@redis.set(video.id, video.info.to_json, expire_time)
@redis.set(video.id + ":time", video.updated, expire_time)
end
def del(id : String)

View file

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

View file

@ -183,8 +183,6 @@ def error_redirect_helper(env : HTTP::Server::Context)
go_to_youtube_embed = translate(locale, "videoinfo_youTube_embed_link")
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
<p style="margin-bottom: 4px;">#{next_steps_text}</p>
<ul>
@ -196,7 +194,7 @@ def error_redirect_helper(env : HTTP::Server::Context)
</li>
<li>
<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>
</ul>
END_HTML

View file

@ -56,11 +56,12 @@ macro templated(_filename, template = "template", navbar_search = true, buffer_f
{{ layout = "src/invidious/views/" + template + ".ecr" }}
__content_filename__ = {{filename}}
render {{filename}}, {{layout}}
content = Kilt.render({{filename}})
Kilt.render({{layout}})
end
macro rendered(filename)
render("src/invidious/views/#{{{filename}}}.ecr")
Kilt.render("src/invidious/views/#{{{filename}}}.ecr")
end
# 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
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)
cipher = OpenSSL::Cipher.new("aes-128-ecb")
cipher.encrypt

View file

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

View file

@ -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
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
# -------------------

View file

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

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
def self.handle(env)
preferences = Preferences.from_json("{}")
host = env.request.headers["Host"]
begin
if prefs_cookie = env.request.cookies["PREFS"]?
@ -25,50 +24,20 @@ module Invidious::Routes::BeforeAll
extra_connect_csp = ""
if CONFIG.invidious_companion.present?
current_companion_d = host.split(".")[0].scan(/(\d+)$/).last?.try &.[0].to_i
if current_companion_d
current_companion_d = current_companion_d - 1
env.set "using_domain", true
env.set "current_companion", current_companion_d
env.set "companion_public_url", CONFIG.invidious_companion[current_companion_d].public_url.to_s
else
if !env.request.cookies[CONFIG.server_id_cookie_name]?
env.response.cookies[CONFIG.server_id_cookie_name] = Invidious::User::Cookies.server_id(host)
end
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
extra_media_csp = " #{CONFIG.invidious_companion.sample.public_url}"
extra_connect_csp = " #{CONFIG.invidious_companion.sample.public_url}"
exvpp_url = BackendInfo.get_exvpp
if !exvpp_url.empty?
extra_media_csp += " #{exvpp_url}"
extra_connect_csp += " #{exvpp_url}"
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
# Allow media resources to be loaded from google servers
@ -84,16 +53,13 @@ module Invidious::Routes::BeforeAll
frame_ancestors = "'none'"
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
# inline styles (<style> [..] </style>, style=" [..] ")
env.response.headers["Content-Security-Policy"] = {
"default-src 'none'",
"script-src 'self'",
"style-src 'self' 'unsafe-inline'",
"img-src 'self' data: " + "#{scheme}://#{env.request.headers["Host"]?}",
"img-src 'self' data: " + HOST_URL,
"font-src 'self' data:",
"connect-src 'self'" + extra_connect_csp,
"manifest-src 'self'",

View file

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

View file

@ -60,7 +60,15 @@ module Invidious::Routes::Login
sid = Base64.urlsafe_encode(Random::Secure.random_bytes(32))
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
return error_template(401, "Wrong username or password")
end
@ -160,7 +168,15 @@ module Invidious::Routes::Login
Invidious::Database::Users.insert(user)
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"]?
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)
end
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
env.redirect referer
@ -259,7 +267,15 @@ module Invidious::Routes::PreferencesRoute
preferences.dark_mode = "dark"
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
if redirect

View file

@ -3,11 +3,6 @@ module Invidious::Routes::VideoPlayback
def self.get_video_playback(env)
locale = env.get("preferences").as(Preferences).locale
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]
protobuf = Bytes.new(array.size)
array.each_with_index do |byte, index|
@ -31,7 +26,7 @@ module Invidious::Routes::VideoPlayback
end
# 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.")
end
@ -267,8 +262,8 @@ module Invidious::Routes::VideoPlayback
# so we have a mechanism here to redirect to the latest version
def self.latest_version(env)
if CONFIG.invidious_companion.present?
companion_public_url = env.get("companion_public_url").as(String)
return env.redirect "#{companion_public_url}/latest_version?#{env.params.query}"
invidious_companion = CONFIG.invidious_companion.sample
return env.redirect "#{invidious_companion.public_url}/latest_version?#{env.params.query}"
end
id = env.params.query["id"]?
@ -312,7 +307,16 @@ module Invidious::Routes::VideoPlayback
end
if local
url = URI.parse(url).request_target.not_nil!
external_proxy = Invidious::HttpServer::Utils.get_external_proxy
if !external_proxy.empty?
url = URI.parse(url)
external_proxy = URI.parse(external_proxy)
url.host = external_proxy.host
url.port = external_proxy.port
url = url.to_s
else
url = URI.parse(url).request_target.not_nil!
end
url += "&title=#{URI.encode_www_form(title, space_to_plus: false)}" if title
end

View file

@ -52,7 +52,7 @@ module Invidious::Routes::Watch
env.params.query.delete_all("listen")
begin
video = get_video(id, region: params.region, env: env)
video = get_video(id, region: params.region)
rescue ex : NotFoundException
LOGGER.error("get_video not found: #{id} : #{ex.message}")
return error_template(404, ex)
@ -61,10 +61,6 @@ module Invidious::Routes::Watch
return error_template(500, ex)
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 &&
subscriptions.includes?(video.ucid) &&
(env.params.query["iv_load_policy"]? || "1") == "1"
@ -213,11 +209,6 @@ module Invidious::Routes::Watch
video_url = nil
end
if CONFIG.invidious_companion.present?
current_companion = env.get("current_companion").as(Int32)
invidious_companion = CONFIG.invidious_companion[current_companion]
end
templated "watch"
end
@ -347,9 +338,8 @@ module Invidious::Routes::Watch
env.params.query["local"] = "true"
if (CONFIG.invidious_companion.present?)
video = get_video(video_id, env: env)
companion_public_url = env.get("companion_public_url").as(String)
return env.redirect "#{companion_public_url}/latest_version?#{env.params.query}"
video = get_video(video_id)
return env.redirect "#{video.invidious_companion["baseUrl"].as_s}/latest_version?#{env.params.query}"
else
return Invidious::Routes::VideoPlayback.latest_version(env)
end

View file

@ -21,7 +21,6 @@ module Invidious::Routing
get "/privacy", Routes::Misc, :privacy
get "/licenses", Routes::Misc, :licenses
get "/redirect", Routes::Misc, :cross_instance_redirect
get "/switchbackend", Routes::BackendSwitcher, :switch
self.register_channel_routes
self.register_watch_routes
@ -69,8 +68,6 @@ module Invidious::Routing
# User account management
get "/change_password", Routes::Account, :get_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
post "/delete_account", Routes::Account, :post_delete
get "/clear_watch_history", Routes::Account, :get_clear_history

View file

@ -11,10 +11,6 @@ struct Invidious::User
# Session ID (SID) cookie
# Parameter "domain" comes from the global config
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
# Browsers expect the domain to include https. On I2P there is no HTTPS
if domain.not_nil!.split(".").last == "i2p"
@ -34,10 +30,6 @@ struct Invidious::User
# Preferences (PREFS) cookie
# Parameter "domain" comes from the global config
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
# Browsers expect the domain to include https. On I2P there is no HTTPS
if domain.not_nil!.split(".").last == "i2p"
@ -53,31 +45,5 @@ struct Invidious::User
samesite: HTTP::Cookie::SameSite::Lax
)
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

View file

@ -298,7 +298,7 @@ struct Video
predicate_bool upcoming, isUpcoming
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 record was last updated over 10 minutes ago, or video has since premiered,
# 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 ||
video.schema_version != Video::SCHEMA_VERSION # cache control
begin
video = fetch_video(id, region, env)
video = fetch_video(id, region)
Invidious::Database::Videos.insert(video)
rescue ex
Invidious::Database::Videos.delete(id)
@ -316,7 +316,7 @@ def get_video(id, refresh = true, region = nil, force_refresh = false, env : HTT
end
end
else
video = fetch_video(id, region, env)
video = fetch_video(id, region)
Invidious::Database::Videos.insert(video) if !region
end
@ -324,11 +324,11 @@ def get_video(id, refresh = true, region = nil, force_refresh = false, env : HTT
rescue DB::Error
# Avoid common `DB::PoolRetryAttemptsExceeded` error and friends
# Note: All DB errors inherit from `DB::Error`
return fetch_video(id, region, env)
return fetch_video(id, region)
end
def fetch_video(id, region, env)
info = extract_video_info(video_id: id, env: env)
def fetch_video(id, region)
info = extract_video_info(video_id: id)
if reason = info["reason"]?
if reason == "Video unavailable"

View file

@ -58,12 +58,12 @@ def parse_related_video(related : JSON::Any) : Hash(String, JSON::Any)?
}
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
client_config = YoutubeAPI::ClientConfig.new
# 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
@ -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["reason"] = JSON::Any.new(reason) if reason
if !CONFIG.invidious_companion.present?
if CONFIG.invidious_companion.present?
new_player_response = nil
# 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:
# https://github.com/TeamNewPipe/NewPipeExtractor/issues/562
client_config.client_type = YoutubeAPI::ClientType::AndroidTestSuite
new_player_response = try_fetch_streaming_data(video_id, client_config, env)
new_player_response = try_fetch_streaming_data(video_id, client_config)
end
# 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
{"captions", "playabilityStatus", "playerConfig", "storyboards"}.each do |f|
{"captions", "playabilityStatus", "playerConfig", "storyboards", "invidiousCompanion"}.each do |f|
params[f] = player_response[f] if player_response[f]?
end
@ -154,9 +154,9 @@ def extract_video_info(video_id : String, env : HTTP::Server::Context | Nil = ni
return params
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.")
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"]
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")
raise BrokenTubeException.new("videoDetails") if !video_details
raise BrokenTubeException.new("microformat") if !microformat
# 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 = 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
published = microformat.try &.["publishDate"]?
published = microformat["publishDate"]?
.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) }
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 { |t| Time.unix(t) }
live_now = microformat.try &.dig?("liveBroadcastDetails", "isLiveNow")
live_now = microformat.dig?("liveBroadcastDetails", "isLiveNow")
.try &.as_bool
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
allowed_regions = microformat.try &.["availableCountries"]?
allowed_regions = microformat["availableCountries"]?
.try &.as_a.map &.as_s || [] of String
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_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 = microformat.dig?("description", "simpleText").try &.as_s || ""
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")
# .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 &.as_a
genre = microformat.try &.["category"]?
genre = microformat["category"]?
genre_ucid = nil
license = nil

View file

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

View file

@ -29,7 +29,7 @@
}.to_pretty_json
%>
</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" %>

View file

@ -3,7 +3,6 @@
author = HTML.escape(channel.author)
channel_profile_pic = URI.parse(channel.author_thumbnail).request_target
host = env.request.headers["Host"]
scheme = env.get("scheme")
relative_url =
case selected_tab
@ -33,19 +32,19 @@
<%- if selected_tab.videos? -%>
<meta name="description" content="<%= channel.description %>">
<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: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 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: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 %>" />
<%- 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 %>">
<title><%= author %> - Invidious</title>

View file

@ -43,4 +43,4 @@
}.to_pretty_json
%>
</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 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|
src_url = "/latest_version?id=#{video.id}&itag=#{fmt["itag"]}"
src_url += "&local=true" if params.local
companion_public_url = env.get("companion_public_url").as(String)
src_url = companion_public_url + src_url +
"&check=#{invidious_companion_encrypt(video.id)}" if (invidious_companion)
src_url = video.invidious_companion["baseUrl"].as_s + src_url +
"&check=#{invidious_companion_encrypt(video.id)}" if (CONFIG.invidious_companion.present?)
bitrate = fmt["bitrate"]
mimetype = HTML.escape(fmt["mimeType"].as_s)
@ -39,9 +38,8 @@
<% else %>
<% if params.quality == "dash"
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 = companion_public_url + src_url +
"&check=#{invidious_companion_encrypt(video.id)}" if (invidious_companion)
src_url = video.invidious_companion["baseUrl"].as_s + src_url +
"&check=#{invidious_companion_encrypt(video.id)}" if (CONFIG.invidious_companion.present?)
%>
<source src="<%= src_url %>" type='application/dash+xml' label="dash">
<% end %>
@ -52,9 +50,8 @@
fmt_stream.each_with_index do |fmt, i|
src_url = "/latest_version?id=#{video.id}&itag=#{fmt["itag"]}"
src_url += "&local=true" if params.local
companion_public_url = env.get("companion_public_url").as(String)
src_url = companion_public_url + src_url +
"&check=#{invidious_companion_encrypt(video.id)}" if (invidious_companion)
src_url = video.invidious_companion["baseUrl"].as_s + src_url +
"&check=#{invidious_companion_encrypt(video.id)}" if (CONFIG.invidious_companion.present?)
quality = fmt["quality"]
mimetype = HTML.escape(fmt["mimeType"].as_s)
@ -89,4 +86,4 @@
}.to_pretty_json
%>
</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
%>
</script>
<script src="/<%= JS_PATH %>/subscribe_widget.js?v=<%= ASSET_COMMIT %>"></script>
<script src="/js/subscribe_widget.js?v=<%= ASSET_COMMIT %>"></script>
<% else %>
<a id="subscribe" class="pure-button pure-button-primary"
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/embed.css?v=<%= ASSET_COMMIT %>">
<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>
<body class="dark-theme">
@ -32,6 +32,6 @@
</script>
<%= rendered "components/player" %>
<script src="/<%= JS_PATH %>/embed.js?v=<%= ASSET_COMMIT %>"></script>
<script src="/js/embed.js?v=<%= ASSET_COMMIT %>"></script>
</body>
</html>

View file

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

View file

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

View file

@ -17,4 +17,4 @@
<% end %>
</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
%>
</script>
<script src="/<%= JS_PATH %>/watched_widget.js"></script>
<script src="/js/watched_widget.js"></script>
<div class="pure-g">
@ -62,7 +62,7 @@
<% end %>
</div>
<script src="/<%= JS_PATH %>/watched_indicator.js"></script>
<script src="/js/watched_indicator.js"></script>
<%=
IV::Frontend::Pagination.nav_numeric(locale,

View file

@ -46,4 +46,4 @@
<% end %>
</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
%>
</script>
<script src="/<%= JS_PATH %>/playlist_widget.js?v=<%= ASSET_COMMIT %>"></script>
<script src="/js/playlist_widget.js?v=<%= ASSET_COMMIT %>"></script>
<% end %>

View file

@ -44,5 +44,5 @@
}.to_pretty_json
%>
</script>
<script src="/<%= JS_PATH %>/comments.js?v=<%= ASSET_COMMIT %>"></script>
<script src="/<%= JS_PATH %>/post.js?v=<%= ASSET_COMMIT %>"></script>
<script src="/js/comments.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
dark_mode = env.get("preferences").as(Preferences).dark_mode
current_backend = env.request.cookies[CONFIG.server_id_cookie_name]?.try &.value || env.request.headers["Host"]
current_external_videoplayback_proxy = Invidious::HttpServer::Utils.get_external_proxy()
%>
<!DOCTYPE html>
<html lang="<%= locale %>">
@ -22,7 +24,7 @@
<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/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>
<body class="<%= dark_mode.blank? ? "no" : dark_mode %>-theme">
@ -32,9 +34,7 @@
<div class="pure-g navbar h-box">
<% if navbar_search %>
<div class="pure-u-1 pure-u-md-4-24">
<a href="/" class="index-link pure-menu-heading">
Invidious
</a>
<a href="/" class="index-link pure-menu-heading">Invidious</a>
</div>
<div class="pure-u-1 pure-u-md-12-24 searchbar">
<% autofocus = false %><%= rendered "components/search_box" %>
@ -106,55 +106,31 @@
</div>
</div>
<%
if CONFIG.invidious_companion.present?
current_backend = env.get?("current_companion").try &.as(Int32)
domain = env.get?("using_domain")
scheme = env.get("scheme")
status = BackendInfo.get_status
companion_switched = env.get?("companion_switched")
%>
<% if !CONFIG.backends.empty? %>
<% if !CONFIG.backend_domains.includes?(env.request.headers["Host"]) %>
<div class="h-box" style="margin-bottom: 10px;">
<b>Switch Backend:</b>
<% if domain %>
<% CONFIG.invidious_companion.each_with_index do | companion, index | %>
<% host_backend = env.request.headers["Host"].sub(/([^.]+)(\d+)/, "\\1#{index+1}") %>
<% is_current_backend_host = host_backend == env.request.headers["Host"] %>
<a href="<%= scheme %>://<%= host_backend %><%= env.request.resource %>" style="<%= is_current_backend_host ? "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 %>
<% else %>
<% CONFIG.invidious_companion.each_with_index do | companion, index | %>
<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 %>
<% CONFIG.backends.each do | backend | %>
<% backend = backend.split(CONFIG.backends_delimiter) %>
<% if current_backend == backend[0] %>
<a href="/switchbackend?backend_id=<%= backend[0] %>" style="text-decoration-line: underline; display: inline-block;">
Backend<%= HTML.escape(backend[0]) %>
<% if backend.size == 2 %>
<%= HTML.escape(backend[1]) %>
<% end %>
</a> <span> | </span>
<% else %>
<a href="/switchbackend?backend_id=<%= backend[0] %>" style="display: inline-block;">
Backend<%= HTML.escape(backend[0]) %>
<% if backend.size == 2 %>
<%= HTML.escape(backend[1]) %>
<% end %>
</a> <span> | </span>
<% end %>
<% end %>
</div>
<% end %>
<% if companion_switched %>
<div class="h-box">
<p><%= translate(locale, "backend_unavailable") %></p>
</div>
<% end %>
<% end %>
<% if CONFIG.banner %>
<div class="h-box">
@ -164,204 +140,202 @@
<%= content %>
<% if buffer_footer %>
<div id="footer_buffer"></div>
<% end %>
<% if buffer_footer %>
<div id="footer_buffer"></div>
<% end %>
</div>
</div>
<script src="/<%= JS_PATH %>/handlers.js?v=<%= ASSET_COMMIT %>"></script>
<script src="/<%= JS_PATH %>/themes.js?v=<%= ASSET_COMMIT %>"></script>
<% if env.get? "user" %>
<script src="/<%= JS_PATH %>/sse.js?v=<%= ASSET_COMMIT %>"></script>
<script id="notification_data" type="application/json">
<%=
{
"upload_text" => HTML.escape(translate(locale, "`x` uploaded a video")),
"live_upload_text" => HTML.escape(translate(locale, "`x` is live"))
}.to_pretty_json
%>
</script>
<% if CONFIG.enable_user_notifications %>
<script src="/<%= JS_PATH %>/notifications.js?v=<%= ASSET_COMMIT %>"></script>
<% end %>
<% end %>
</div>
</div>
<script src="/js/handlers.js?v=<%= ASSET_COMMIT %>"></script>
<script src="/js/themes.js?v=<%= ASSET_COMMIT %>"></script>
<% if env.get? "user" %>
<script src="/js/sse.js?v=<%= ASSET_COMMIT %>"></script>
<script id="notification_data" type="application/json">
<%=
{
"upload_text" => HTML.escape(translate(locale, "`x` uploaded a video")),
"live_upload_text" => HTML.escape(translate(locale, "`x` is live"))
}.to_pretty_json
%>
</script>
<% if CONFIG.enable_user_notifications %>
<script src="/js/notifications.js?v=<%= ASSET_COMMIT %>"></script>
<% end %>
<% end %>
<footer class="pure-g">
<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="pure-u-1 footer-content">
<div class="footer-section pure-u-1-4" id="footer-custom-text">
<b>Invidious</b>
<% if CONFIG.footer %>
<p><%=CONFIG.footer%></p>
<% else %>
<p><%=translate(locale, "default_invidious_footer_text")%></p>
<% end %>
<footer class="pure-g">
<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="pure-u-1 footer-content">
<div class="footer-section pure-u-1-4" id="footer-custom-text">
<b>Invidious</b>
<% if CONFIG.footer %>
<p><%=CONFIG.footer%></p>
<% else %>
<p><%=translate(locale, "default_invidious_footer_text")%></p>
<% 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>
<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>
</ul>
</div>
<% if CONFIG.instance_maintainer_email || CONFIG.modified_source_code_url || CONFIG.footer_instance_tos_link || CONFIG.footer_instance_privacy_policy_link %>
<div class="footer-section">
<b class="footer-section-header">
<% if CONFIG.modified_source_code_url %>
<%= translate(locale, "footer_instance_section_header_modified_source")%>
<% 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 %>
</ul>
</div>
<% if CONFIG.instance_maintainer_email || CONFIG.modified_source_code_url || CONFIG.footer_instance_tos_link || CONFIG.footer_instance_privacy_policy_link %>
<div class="footer-section">
<b class="footer-section-header">
<% if CONFIG.modified_source_code_url %>
<%= translate(locale, "footer_instance_section_header_modified_source")%>
<% 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 %>
<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")%>">
<%= translate(locale, "footer_instance_section_donate") %>
</a>
</li>
<% end %>
<% if CONFIG.footer_instance_donate_link %>
<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")%>">
<%= translate(locale, "footer_instance_section_donate") %>
</a>
</li>
<% end %>
<% CONFIG.footer_instance_section_custom_fields.each do | field | %>
<li class="pure-menu-item footer-section-item">
<a href="<%=HTML.escape(field[1])%>" title="<%= HTML.escape(field[0]) %>">
<%= HTML.escape(field[0]) %>
</a>
</li>
<% end %>
</ul>
</div>
<% end %>
<% CONFIG.footer_instance_section_custom_fields.each do | field | %>
<li class="pure-menu-item footer-section-item">
<a href="<%=HTML.escape(field[1])%>" title="<%= HTML.escape(field[0]) %>">
<%= HTML.escape(field[0]) %>
</a>
</li>
<% end %>
</ul>
</div>
<% end %>
<div class="footer-section">
<b class="footer-section-header"><%= translate(locale, "footer_support_section_header")%></b>
<ul class="pure-menu-list footer-section-list">
<li class="pure-menu-item footer-section-item">
<a href="https://github.com/iv-org/invidious/issues/new" title="<%= translate(locale, "footer_report_bug_link")%>">
<%= translate(locale, "footer_report_bug_link") %>
</a>
</li>
<li class="pure-menu-item footer-section-item">
<a href="#" title="<%= translate(locale, "footer_faq_link")%>" style="">
<%= translate(locale, "footer_faq_link") %>
</a>
</li>
</ul>
</div>
</div>
<hr/>
<div class="footer-footer">
<%
if CONFIG.invidious_companion.present?
companion_public_url = env.get("companion_public_url").as(String)
%>
<div class="box">You are currently using Backend: <%= current_backend ? companion_public_url : "Unable to get backend, this is bug, please report it!" %></div>
<% end %>
<span class="left">
<% if CONFIG.modified_source_code_url %>
<%= translate(locale, "footer_current_version_modified") %>
<% else %>
<%= translate(locale, "Current version: ") %>
<% end %>
<%= 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>
<div class="footer-section">
<b class="footer-section-header"><%= translate(locale, "footer_support_section_header")%></b>
<ul class="pure-menu-list footer-section-list">
<li class="pure-menu-item footer-section-item">
<a href="https://github.com/iv-org/invidious/issues/new" title="<%= translate(locale, "footer_report_bug_link")%>">
<%= translate(locale, "footer_report_bug_link") %>
</a>
</li>
<li class="pure-menu-item footer-section-item">
<a href="#" title="<%= translate(locale, "footer_faq_link")%>" style="">
<%= translate(locale, "footer_faq_link") %>
</a>
</li>
</ul>
</div>
</div>
<hr/>
<div class="footer-footer">
<div class="box">You are currently using Backend: <%= current_backend %></div>
<% if !current_external_videoplayback_proxy.empty? %>
<div class="box">External Videoplayback Proxy: <%= current_external_videoplayback_proxy %></div>
<% end %>
<span class="left">
<% if CONFIG.modified_source_code_url %>
<%= translate(locale, "footer_current_version_modified") %>
<% else %>
<%= translate(locale, "Current version: ") %>
<% end %>
<%= 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>
</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">
<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 %>>
<% if CONFIG.force_local %>
<label for="local">(All videos are proxied by default)</label>
<% end %>
<input name="local" id="local" type="checkbox" <% if preferences.local && !CONFIG.disabled?("local") %>checked<% end %> <% if CONFIG.disabled?("local") %>disabled<% end %>>
</div>
<div class="pure-control-group">
@ -333,10 +330,6 @@
<a href="/change_password?referer=<%= URI.encode_www_form(referer) %>"><%= translate(locale, "Change password") %></a>
</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">
<a href="/data_control?referer=<%= URI.encode_www_form(referer) %>"><%= translate(locale, "Import/export data") %></a>
</div>

View file

@ -2,32 +2,31 @@
<% title = HTML.escape(video.title) %>
<% author = HTML.escape(video.author) %>
<% host = env.request.headers["Host"] %>
<% scheme = env.get("scheme") %>
<% 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="keywords" content="<%= video.keywords.join(",") %>">
<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: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:type" content="video.other">
<!-- 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:secure_url" content="<%= video_url %>">
<meta property="og:video:type" content="video/mp4">
<meta property="og:video:width" content="640">
<meta property="og:video:height" content="360">
<meta name="twitter:card" content="player">
<meta name="twitter:url" content="<%= 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:description" content="<%= HTML.escape(video.short_description) %>">
<meta name="twitter:image" content="<%= scheme %>://<%= host %>/vi/<%= video.id %>/maxres.jpg">
<meta name="twitter:player" content="<%= scheme %>://<%= host %>/embed/<%= video.id %>">
<meta name="twitter:image" content="<%= host %>/vi/<%= video.id %>/maxres.jpg">
<meta name="twitter:player" content="<%= host %>/embed/<%= video.id %>">
<meta name="twitter:player:width" content="1280">
<meta name="twitter:player:height" content="720">
<link rel="alternate" href="https://www.youtube.com/watch?v=<%= video.id %>">
@ -199,7 +198,7 @@ we're going to need to do it here in order to allow for translations.
}.to_pretty_json
%>
</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 %>
@ -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="likes"><i class="icon ion-ios-thumbs-up"></i> <%= number_with_separator(video.likes) %></p>
<p id="dislikes" style="display: none; visibility: hidden;"></p>
<% if !video.genre.nil? && !video.genre.empty? %>
<p id="genre"><%= translate(locale, "Genre: ") %>
<% if !video.genre_url %>
<%= video.genre %>
<% else %>
<a href="<%= video.genre_url %>"><%= video.genre %></a>
<% end %>
</p>
<% end %>
<p id="genre"><%= translate(locale, "Genre: ") %>
<% if !video.genre_url %>
<%= video.genre %>
<% else %>
<a href="<%= video.genre_url %>"><%= video.genre %></a>
<% end %>
</p>
<% if video.license %>
<% if video.license.empty? %>
<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="rating" 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 %>
<p id="allowed_regions">
<% if video.allowed_regions.size < REGIONS.size // 2 %>
<%= translate(locale, "Whitelisted regions: ") %><%= video.allowed_regions.join(", ") %>
<% else %>
<%= translate(locale, "Blacklisted regions: ") %><%= (REGIONS.to_a - video.allowed_regions).join(", ") %>
<% end %>
</p>
<% end %>
<% if video.allowed_regions.size != REGIONS.size %>
<p id="allowed_regions">
<% if video.allowed_regions.size < REGIONS.size // 2 %>
<%= translate(locale, "Whitelisted regions: ") %><%= video.allowed_regions.join(", ") %>
<% else %>
<%= translate(locale, "Blacklisted regions: ") %><%= (REGIONS.to_a - video.allowed_regions).join(", ") %>
<% end %>
</p>
<% end %>
</div>
</div>
@ -391,5 +386,5 @@ we're going to need to do it here in order to allow for translations.
</div>
<% end %>
</div>
<script src="/<%= JS_PATH %>/comments.js?v=<%= ASSET_COMMIT %>"></script>
<script src="/<%= JS_PATH %>/watch.js?v=<%= ASSET_COMMIT %>"></script>
<script src="/js/comments.js?v=<%= ASSET_COMMIT %>"></script>
<script src="/js/watch.js?v=<%= ASSET_COMMIT %>"></script>

View file

@ -49,7 +49,7 @@ end
struct CompanionConnectionPool
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(
initial_pool_size: 0,
max_pool_size: capacity,
@ -58,22 +58,23 @@ struct CompanionConnectionPool
)
@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
def client(&)
conn = pool.checkout
# Proxy needs to be reinstated every time we get a client from the pool
conn.proxy = make_configured_http_proxy_client() if CONFIG.http_proxy
begin
response = yield conn
rescue ex
conn.close
scheme = "https" if conn.tls || "http"
same_companion = "#{scheme}://#{conn.host}:#{conn.port}"
conn = make_client(URI.parse(same_companion), use_http_proxy: false)
companion = CONFIG.invidious_companion.sample
conn = make_client(companion.private_url, force_resolve: true)
response = yield conn
ensure

View file

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