4 Commits

25 changed files with 160 additions and 369 deletions

View File

@ -1,10 +0,0 @@
version: 2
updates:
- package-ecosystem: "docker"
directory: "/docker"
schedule:
interval: "weekly"
- package-ecosystem: github-actions
directory: /
schedule:
interval: "weekly"

View File

@ -17,27 +17,17 @@ on:
jobs: jobs:
release: release:
strategy: runs-on: ubuntu-latest
matrix:
include:
- os: ubuntu-latest
platform: linux/amd64
name: "AMD64"
dockerfile: "docker/Dockerfile"
tag_suffix: ""
# GitHub doesn't have a ubuntu-latest-arm runner
- os: ubuntu-24.04-arm
platform: linux/arm64/v8
name: "ARM64"
dockerfile: "docker/Dockerfile.arm64"
tag_suffix: "-arm64"
runs-on: ${{ matrix.os }}
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
with:
platforms: arm64
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3 uses: docker/setup-buildx-action@v3
@ -53,22 +43,45 @@ jobs:
uses: docker/metadata-action@v5 uses: docker/metadata-action@v5
with: with:
images: quay.io/invidious/invidious images: quay.io/invidious/invidious
flavor: |
suffix=${{ matrix.tag_suffix }}
tags: | tags: |
type=sha,format=short,prefix={{date 'YYYY.MM.DD'}}-,enable=${{ github.ref == format('refs/heads/{0}', 'master') }} type=sha,format=short,prefix={{date 'YYYY.MM.DD'}}-,enable=${{ github.ref == format('refs/heads/{0}', 'master') }}
type=raw,value=master,enable=${{ github.ref == format('refs/heads/{0}', 'master') }} type=raw,value=master,enable=${{ github.ref == format('refs/heads/{0}', 'master') }}
labels: | labels: |
quay.expires-after=12w quay.expires-after=12w
- name: Build and push Docker ${{ matrix.name }} image for Push Event - name: Build and push Docker AMD64 image for Push Event
uses: docker/build-push-action@v6 uses: docker/build-push-action@v5
with: with:
context: . context: .
file: ${{ matrix.dockerfile }} file: docker/Dockerfile
platforms: ${{ matrix.platform }} platforms: linux/amd64
labels: ${{ steps.meta.outputs.labels }} labels: ${{ steps.meta.outputs.labels }}
push: true push: true
tags: ${{ steps.meta.outputs.tags }} tags: ${{ steps.meta.outputs.tags }}
build-args: | build-args: |
"release=1" "release=1"
- name: Docker meta
id: meta-arm64
uses: docker/metadata-action@v5
with:
images: quay.io/invidious/invidious
flavor: |
suffix=-arm64
tags: |
type=sha,format=short,prefix={{date 'YYYY.MM.DD'}}-,enable=${{ github.ref == format('refs/heads/{0}', 'master') }}
type=raw,value=master,enable=${{ github.ref == format('refs/heads/{0}', 'master') }}
labels: |
quay.expires-after=12w
- name: Build and push Docker ARM64 image for Push Event
uses: docker/build-push-action@v5
with:
context: .
file: docker/Dockerfile.arm64
platforms: linux/arm64/v8
labels: ${{ steps.meta-arm64.outputs.labels }}
push: true
tags: ${{ steps.meta-arm64.outputs.tags }}
build-args: |
"release=1"

View File

@ -8,27 +8,17 @@ on:
jobs: jobs:
release: release:
strategy: runs-on: ubuntu-latest
matrix:
include:
- os: ubuntu-latest
platform: linux/amd64
name: "AMD64"
dockerfile: "docker/Dockerfile"
tag_suffix: ""
# GitHub doesn't have a ubuntu-latest-arm runner
- os: ubuntu-24.04-arm
platform: linux/arm64/v8
name: "ARM64"
dockerfile: "docker/Dockerfile.arm64"
tag_suffix: "-arm64"
runs-on: ${{ matrix.os }}
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
with:
platforms: arm64
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3 uses: docker/setup-buildx-action@v3
@ -46,21 +36,46 @@ jobs:
images: quay.io/invidious/invidious images: quay.io/invidious/invidious
flavor: | flavor: |
latest=false latest=false
suffix=${{ matrix.tag_suffix }}
tags: | tags: |
type=semver,pattern={{version}} type=semver,pattern={{version}}
type=raw,value=latest type=raw,value=latest
labels: | labels: |
quay.expires-after=12w quay.expires-after=12w
- name: Build and push Docker ${{ matrix.name }} image for Push Event - name: Build and push Docker AMD64 image for Push Event
uses: docker/build-push-action@v6 uses: docker/build-push-action@v5
with: with:
context: . context: .
file: ${{ matrix.dockerfile }} file: docker/Dockerfile
platforms: ${{ matrix.platform }} platforms: linux/amd64
labels: ${{ steps.meta.outputs.labels }} labels: ${{ steps.meta.outputs.labels }}
push: true push: true
tags: ${{ steps.meta.outputs.tags }} tags: ${{ steps.meta.outputs.tags }}
build-args: | build-args: |
"release=1" "release=1"
- name: Docker meta
id: meta-arm64
uses: docker/metadata-action@v5
with:
images: quay.io/invidious/invidious
flavor: |
latest=false
suffix=-arm64
tags: |
type=semver,pattern={{version}}
type=raw,value=latest
labels: |
quay.expires-after=12w
- name: Build and push Docker ARM64 image for Push Event
uses: docker/build-push-action@v5
with:
context: .
file: docker/Dockerfile.arm64
platforms: linux/arm64/v8
labels: ${{ steps.meta-arm64.outputs.labels }}
push: true
tags: ${{ steps.meta-arm64.outputs.tags }}
build-args: |
"release=1"

View File

@ -38,11 +38,10 @@ jobs:
matrix: matrix:
stable: [true] stable: [true]
crystal: crystal:
- 1.12.2 - 1.12.1
- 1.13.3 - 1.13.2
- 1.14.1 - 1.14.0
- 1.15.1 - 1.15.0
- 1.16.3
include: include:
- crystal: nightly - crystal: nightly
stable: false stable: false
@ -58,12 +57,12 @@ jobs:
shell: bash shell: bash
- name: Install Crystal - name: Install Crystal
uses: crystal-lang/install-crystal@v1.8.2 uses: crystal-lang/install-crystal@v1.8.0
with: with:
crystal: ${{ matrix.crystal }} crystal: ${{ matrix.crystal }}
- name: Cache Shards - name: Cache Shards
uses: actions/cache@v4 uses: actions/cache@v3
with: with:
path: | path: |
./lib ./lib
@ -83,43 +82,46 @@ jobs:
run: crystal build --warnings all --error-on-warnings --error-trace src/invidious.cr run: crystal build --warnings all --error-on-warnings --error-trace src/invidious.cr
build-docker: build-docker:
strategy:
matrix:
include:
- os: ubuntu-latest
name: "AMD64"
# GitHub doesn't have a ubuntu-latest-arm runner
- os: ubuntu-24.04-arm
name: "ARM64"
name: Test ${{ matrix.name }} Docker build runs-on: ubuntu-latest
runs-on: ${{ matrix.os }}
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Use ARM64 Dockerfile if ARM64
if: ${{ matrix.name }} == "ARM64"
run: sed -i 's/Dockerfile/Dockerfile.arm64/' docker-compose.yml
- name: Build Docker - name: Build Docker
run: docker compose build run: docker compose build --build-arg release=0
- name: Change hmac_key on docker-compose.yml
run: sed -i '/hmac_key/s/CHANGE_ME!!/docker-build-hmac-key/' docker-compose.yml
- name: Run Docker - name: Run Docker
run: docker compose up -d run: docker compose up -d
- name: Test Docker - name: Test Docker
id: test run: while curl -Isf http://localhost:3000; do sleep 1; done
run: curl -If http://localhost:3000 --retry 5 --retry-delay 1 --retry-all-errors
- name: Print Invidious container logs build-docker-arm64:
# Tells Github Actions to always run this step regardless of whether the previous step has failed
# Without this expression this step would simply be skipped when the previous step fails. runs-on: ubuntu-latest
if: success() || steps.test.conclusion == 'failure'
run: docker compose logs steps:
- uses: actions/checkout@v4
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
with:
platforms: arm64
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Build Docker ARM64 image
uses: docker/build-push-action@v5
with:
context: .
file: docker/Dockerfile.arm64
platforms: linux/arm64/v8
build-args: release=0
- name: Test Docker
run: while curl -Isf http://localhost:3000; do sleep 1; done
lint: lint:
@ -134,12 +136,12 @@ jobs:
- name: Install Crystal - name: Install Crystal
id: lint_step_install_crystal id: lint_step_install_crystal
uses: crystal-lang/install-crystal@v1.8.2 uses: crystal-lang/install-crystal@v1.8.0
with: with:
crystal: latest crystal: latest
- name: Cache Shards - name: Cache Shards
uses: actions/cache@v4 uses: actions/cache@v3
with: with:
path: | path: |
./lib ./lib

View File

@ -10,7 +10,7 @@ jobs:
stale: stale:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/stale@v9 - uses: actions/stale@v8
with: with:
repo-token: ${{ secrets.GITHUB_TOKEN }} repo-token: ${{ secrets.GITHUB_TOKEN }}
days-before-stale: 730 days-before-stale: 730

View File

@ -2,6 +2,10 @@
## vX.Y.0 (future) ## vX.Y.0 (future)
## v2.20250517.0
Inverse fallback for the YouTube client from TVHTML then MWEB. Fixes https://github.com/iv-org/invidious/issues/5273
## v2.20250504.0 ## v2.20250504.0
Small release with quick workaround fix for issue #4251 (Nil assertion failed). Small release with quick workaround fix for issue #4251 (Nil assertion failed).

View File

@ -81,9 +81,9 @@
- [Available in many languages](locales/), thanks to [our translators](#contribute) - [Available in many languages](locales/), thanks to [our translators](#contribute)
**Data import/export** **Data import/export**
- Import subscriptions from YouTube, NewPipe and FreeTube - Import subscriptions from YouTube, NewPipe and Freetube
- Import watch history from YouTube and NewPipe - Import watch history from YouTube and NewPipe
- Export subscriptions to NewPipe and FreeTube - Export subscriptions to NewPipe and Freetube
- Import/Export Invidious user data - Import/Export Invidious user data
**Technical features** **Technical features**
@ -95,11 +95,11 @@
## Quick start ## Quick start
**Using Invidious:** **Using invidious:**
- [Select a public instance from the list](https://instances.invidious.io) and start watching videos right now! - [Select a public instance from the list](https://instances.invidious.io) and start watching videos right now!
**Hosting Invidious:** **Hosting invidious:**
- [Follow the installation instructions](https://docs.invidious.io/installation/) - [Follow the installation instructions](https://docs.invidious.io/installation/)
@ -114,8 +114,8 @@ https://github.com/iv-org/documentation
### Extensions ### Extensions
We highly recommend the use of [Privacy Redirect](https://github.com/SimonBrazell/privacy-redirect#get), We highly recommend the use of [Privacy Redirect](https://github.com/SimonBrazell/privacy-redirect#get),
a browser extension that automatically redirects YouTube URLs to any Invidious instance and replaces a browser extension that automatically redirects Youtube URLs to any Invidious instance and replaces
embedded YouTube videos on other websites with Invidious. embedded youtube videos on other websites with invidious.
The documentation contains a list of browser extensions that we recommended to use along with Invidious. The documentation contains a list of browser extensions that we recommended to use along with Invidious.
@ -140,7 +140,7 @@ We use [Weblate](https://weblate.org) to manage Invidious translations.
You can suggest new translations and/or correction here: https://hosted.weblate.org/engage/invidious/. You can suggest new translations and/or correction here: https://hosted.weblate.org/engage/invidious/.
Creating an account is not required, but recommended, especially if you want to contribute regularly. Creating an account is not required, but recommended, especially if you want to contribute regularly.
Weblate also allows you to log-in with major SSO providers like GitHub, GitLab, BitBucket, Google, ... Weblate also allows you to log-in with major SSO providers like Github, Gitlab, BitBucket, Google, ...
## Projects using Invidious ## Projects using Invidious

View File

@ -54,53 +54,6 @@ db:
## ##
#signature_server: #signature_server:
##
## Invidious companion is an external program
## for loading the video streams from YouTube servers.
##
## When this setting is commented out, Invidious companion is not used.
## Otherwise, Invidious will proxy the requests to Invidious companion.
##
## Note: multiple URL can be configured. In this case, invidious will
## randomly pick one every time video data needs to be retrieved. This
## URL is then kept in the video metadata cache to allow video playback
## to work. Once said cache has expired, requesting that video's data
## again will cause a new companion URL to be picked.
##
## The parameter private_url needs to be configured for the internal
## communication between the companion and Invidious.
## And public_url is the public URL from which companion is listening
## to the requests from the user(s).
##
## If you are using a reverse proxy then you will probably need to
## configure the public_url to be the same as the domain used for Invidious.
## Also apply when used from an external IP address (without a domain).
## Examples: https://MYINVIDIOUSDOMAIN or http://192.168.1.100:8282
##
## Both parameter can have identical URL when Invidious is hosted in
## an internal network or at home or locally (localhost).
##
## Accepted values: "http(s)://<IP-HOSTNAME>:<Port>"
## Default: <none>
##
#invidious_companion:
# - private_url: "http://localhost:8282"
# public_url: "http://localhost:8282"
##
## API key for Invidious companion, used for securing the communication
## between Invidious and Invidious companion.
## The key needs to be exactly 16 characters long.
##
## Note: This parameter is mandatory when Invidious companion is enabled
## and should be a random string.
## Such random string can be generated on linux with the following
## command: `pwgen 16 1`
##
## Accepted values: a string (of length 16)
## Default: <none>
##
#invidious_companion_key: "CHANGE_ME!!"
######################################### #########################################
# #
@ -858,9 +811,9 @@ default_user_preferences:
## Default video quality. ## Default video quality.
## ##
## Accepted values: dash, hd720, medium, small ## Accepted values: dash, hd720, medium, small
## Default: dash ## Default: hd720
## ##
#quality: dash #quality: hd720
## ##
## Default dash video quality. ## Default dash video quality.

View File

@ -14,10 +14,6 @@ services:
restart: unless-stopped restart: unless-stopped
ports: ports:
- "127.0.0.1:3000:3000" - "127.0.0.1:3000:3000"
depends_on:
invidious-db:
condition: service_healthy
restart: true
environment: environment:
# Please read the following file for a comprehensive list of all available # Please read the following file for a comprehensive list of all available
# configuration options and their associated syntax: # configuration options and their associated syntax:

View File

@ -1,4 +1,4 @@
FROM crystallang/crystal:1.16.3-alpine AS builder FROM crystallang/crystal:1.12.2-alpine AS builder
RUN apk add --no-cache sqlite-static yaml-static RUN apk add --no-cache sqlite-static yaml-static
@ -32,7 +32,7 @@ RUN --mount=type=cache,target=/root/.cache/crystal if [[ "${release}" == 1 ]] ;
--link-flags "-lxml2 -llzma"; \ --link-flags "-lxml2 -llzma"; \
fi fi
FROM alpine:3.21 FROM alpine:3.20
RUN apk add --no-cache rsvg-convert ttf-opensans tini tzdata RUN apk add --no-cache rsvg-convert ttf-opensans tini tzdata
WORKDIR /invidious WORKDIR /invidious
RUN addgroup -g 1000 -S invidious && \ RUN addgroup -g 1000 -S invidious && \

View File

@ -1,5 +1,5 @@
FROM alpine:3.21 AS builder FROM alpine:3.20 AS builder
RUN apk add --no-cache 'crystal=1.14.0-r0' shards sqlite-static yaml-static yaml-dev libxml2-static \ RUN apk add --no-cache 'crystal=1.12.2-r0' shards sqlite-static yaml-static yaml-dev libxml2-static \
zlib-static openssl-libs-static openssl-dev musl-dev xz-static zlib-static openssl-libs-static openssl-dev musl-dev xz-static
ARG release ARG release
@ -33,7 +33,7 @@ RUN --mount=type=cache,target=/root/.cache/crystal if [[ "${release}" == 1 ]] ;
--link-flags "-lxml2 -llzma"; \ --link-flags "-lxml2 -llzma"; \
fi fi
FROM alpine:3.21 FROM alpine:3.20
RUN apk add --no-cache rsvg-convert ttf-opensans tini tzdata RUN apk add --no-cache rsvg-convert ttf-opensans tini tzdata
WORKDIR /invidious WORKDIR /invidious
RUN addgroup -g 1000 -S invidious && \ RUN addgroup -g 1000 -S invidious && \

View File

@ -1,5 +1,5 @@
name: invidious name: invidious
version: 2.20250314.0-dev version: 2.20250314.0
authors: authors:
- Invidious team <contact@invidious.io> - Invidious team <contact@invidious.io>

View File

@ -97,10 +97,6 @@ YT_POOL = YoutubeConnectionPool.new(YT_URL, capacity: CONFIG.pool_size)
GGPHT_POOL = YoutubeConnectionPool.new(URI.parse("https://yt3.ggpht.com"), capacity: CONFIG.pool_size) GGPHT_POOL = YoutubeConnectionPool.new(URI.parse("https://yt3.ggpht.com"), capacity: CONFIG.pool_size)
COMPANION_POOL = CompanionConnectionPool.new(
capacity: CONFIG.pool_size
)
# CLI # CLI
Kemal.config.extra_options do |parser| Kemal.config.extra_options do |parser|
parser.banner = "Usage: invidious [arguments]" parser.banner = "Usage: invidious [arguments]"
@ -171,9 +167,16 @@ DECRYPT_FUNCTION =
if sig_helper_address = CONFIG.signature_server.presence if sig_helper_address = CONFIG.signature_server.presence
IV::DecryptFunction.new(sig_helper_address) IV::DecryptFunction.new(sig_helper_address)
else else
LOGGER.warn("WARNING: inv-sig-helper is required for video playback. For more information see https://docs.invidious.io/installation")
nil nil
end end
{% for field in %w(po_token visitor_data) %}
if !CONFIG.{{field.id}}
LOGGER.warn("WARNING: {{field.id}} is required to view and playback videos. For more information see https://docs.invidious.io/installation")
end
{% end %}
# Start jobs # Start jobs
if CONFIG.channel_threads > 0 if CONFIG.channel_threads > 0

View File

@ -35,7 +35,7 @@ struct ConfigPreferences
property max_results : Int32 = 40 property max_results : Int32 = 40
property notifications_only : Bool = false property notifications_only : Bool = false
property player_style : String = "invidious" property player_style : String = "invidious"
property quality : String = "dash" property quality : String = "hd720"
property quality_dash : String = "auto" property quality_dash : String = "auto"
property default_home : String? = "Popular" property default_home : String? = "Popular"
property feed_menu : Array(String) = ["Popular", "Trending", "Subscriptions", "Playlists"] property feed_menu : Array(String) = ["Popular", "Trending", "Subscriptions", "Playlists"]
@ -74,16 +74,6 @@ end
class Config class Config
include YAML::Serializable include YAML::Serializable
class CompanionConfig
include YAML::Serializable
@[YAML::Field(converter: Preferences::URIConverter)]
property private_url : URI = URI.parse("")
@[YAML::Field(converter: Preferences::URIConverter)]
property public_url : URI = URI.parse("")
end
# Number of threads to use for crawling videos from channels (for updating subscriptions) # Number of threads to use for crawling videos from channels (for updating subscriptions)
property channel_threads : Int32 = 1 property channel_threads : Int32 = 1
# Time interval between two executions of the job that crawls channel videos (subscriptions update). # Time interval between two executions of the job that crawls channel videos (subscriptions update).
@ -170,12 +160,6 @@ class Config
# poToken for passing bot attestation # poToken for passing bot attestation
property po_token : String? = nil property po_token : String? = nil
# Invidious companion
property invidious_companion : Array(CompanionConfig) = [] of CompanionConfig
# Invidious companion API key
property invidious_companion_key : String = ""
# Saved cookies in "name1=value1; name2=value2..." format # Saved cookies in "name1=value1; name2=value2..." format
@[YAML::Field(converter: Preferences::StringToCookies)] @[YAML::Field(converter: Preferences::StringToCookies)]
property cookies : HTTP::Cookies = HTTP::Cookies.new property cookies : HTTP::Cookies = HTTP::Cookies.new
@ -256,27 +240,6 @@ class Config
end end
{% end %} {% end %}
if config.invidious_companion.present?
# invidious_companion and signature_server can't work together
if config.signature_server
puts "Config: You can not run inv_sig_helper and invidious_companion at the same time."
exit(1)
elsif config.invidious_companion_key.empty?
puts "Config: Please configure a key if you are using invidious companion."
exit(1)
elsif config.invidious_companion_key == "CHANGE_ME!!"
puts "Config: The value of 'invidious_companion_key' needs to be changed!!"
exit(1)
elsif config.invidious_companion_key.size != 16
puts "Config: The value of 'invidious_companion_key' needs to be a size of 16 characters."
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 # HMAC_key is mandatory
# See: https://github.com/iv-org/invidious/issues/3854 # See: https://github.com/iv-org/invidious/issues/3854
if config.hmac_key.empty? if config.hmac_key.empty?

View File

@ -23,16 +23,10 @@ module Invidious::Frontend::WatchPage
return "<p id=\"download\">#{translate(locale, "Download is disabled")}</p>" return "<p id=\"download\">#{translate(locale, "Download is disabled")}</p>"
end end
url = "/download"
if (CONFIG.invidious_companion.present?)
invidious_companion = CONFIG.invidious_companion.sample
url = "#{invidious_companion.public_url}/download?check=#{invidious_companion_encrypt(video.id)}"
end
return String.build(4000) do |str| return String.build(4000) do |str|
str << "<form" str << "<form"
str << " class=\"pure-form pure-form-stacked\"" str << " class=\"pure-form pure-form-stacked\""
str << " action='#{url}'" str << " action='/download'"
str << " method='post'" str << " method='post'"
str << " rel='noopener'" str << " rel='noopener'"
str << " target='_blank'>" str << " target='_blank'>"

View File

@ -383,22 +383,3 @@ def parse_link_endpoint(endpoint : JSON::Any, text : String, video_id : String)
end end
return text return text
end end
def encrypt_ecb_without_salt(data, key)
cipher = OpenSSL::Cipher.new("aes-128-ecb")
cipher.encrypt
cipher.key = key
io = IO::Memory.new
io.write(cipher.update(data))
io.write(cipher.final)
io.rewind
return io
end
def invidious_companion_encrypt(data)
timestamp = Time.utc.to_unix
encrypted_data = encrypt_ecb_without_salt("#{timestamp}|#{data}", CONFIG.invidious_companion_key)
return Base64.urlsafe_encode(encrypted_data)
end

View File

@ -8,11 +8,6 @@ module Invidious::Routes::API::Manifest
id = env.params.url["id"] id = env.params.url["id"]
region = env.params.query["region"]? region = env.params.query["region"]?
if CONFIG.invidious_companion.present?
invidious_companion = CONFIG.invidious_companion.sample
return env.redirect "#{invidious_companion.public_url}/api/manifest/dash/id/#{id}?#{env.params.query}"
end
# Since some implementations create playlists based on resolution regardless of different codecs, # Since some implementations create playlists based on resolution regardless of different codecs,
# we can opt to only add a source to a representation if it has a unique height within that representation # we can opt to only add a source to a representation if it has a unique height within that representation
unique_res = env.params.query["unique_res"]?.try { |q| (q == "true" || q == "1").to_unsafe } unique_res = env.params.query["unique_res"]?.try { |q| (q == "true" || q == "1").to_unsafe }

View File

@ -203,14 +203,6 @@ module Invidious::Routes::Embed
return env.redirect url return env.redirect url
end end
if CONFIG.invidious_companion.present?
invidious_companion = CONFIG.invidious_companion.sample
env.response.headers["Content-Security-Policy"] =
env.response.headers["Content-Security-Policy"]
.gsub("media-src", "media-src #{invidious_companion.public_url}")
.gsub("connect-src", "connect-src #{invidious_companion.public_url}")
end
rendered "embed" rendered "embed"
end end
end end

View File

@ -21,7 +21,7 @@ module Invidious::Routes::VideoPlayback
end end
# Sanity check, to avoid being used as an open proxy # Sanity check, to avoid being used as an open proxy
if !host.matches?(/[\w-]+\.(?:googlevideo|c\.youtube)\.com/) if !host.matches?(/[\w-]+.googlevideo.com/)
return error_template(400, "Invalid \"host\" parameter.") return error_template(400, "Invalid \"host\" parameter.")
end end
@ -37,8 +37,7 @@ module Invidious::Routes::VideoPlayback
# See: https://github.com/iv-org/invidious/issues/3302 # See: https://github.com/iv-org/invidious/issues/3302
range_header = env.request.headers["Range"]? range_header = env.request.headers["Range"]?
sq = query_params["sq"]? if range_header.nil?
if range_header.nil? && sq.nil?
range_for_head = query_params["range"]? || "0-640" range_for_head = query_params["range"]? || "0-640"
headers["Range"] = "bytes=#{range_for_head}" headers["Range"] = "bytes=#{range_for_head}"
end end
@ -257,11 +256,6 @@ module Invidious::Routes::VideoPlayback
# YouTube /videoplayback links expire after 6 hours, # YouTube /videoplayback links expire after 6 hours,
# so we have a mechanism here to redirect to the latest version # so we have a mechanism here to redirect to the latest version
def self.latest_version(env) def self.latest_version(env)
if CONFIG.invidious_companion.present?
invidious_companion = CONFIG.invidious_companion.sample
return env.redirect "#{invidious_companion.public_url}/latest_version?#{env.params.query}"
end
id = env.params.query["id"]? id = env.params.query["id"]?
itag = env.params.query["itag"]?.try &.to_i? itag = env.params.query["itag"]?.try &.to_i?

View File

@ -192,14 +192,6 @@ module Invidious::Routes::Watch
captions: video.captions captions: video.captions
) )
if CONFIG.invidious_companion.present?
invidious_companion = CONFIG.invidious_companion.sample
env.response.headers["Content-Security-Policy"] =
env.response.headers["Content-Security-Policy"]
.gsub("media-src", "media-src #{invidious_companion.public_url}")
.gsub("connect-src", "connect-src #{invidious_companion.public_url}")
end
templated "watch" templated "watch"
end end
@ -293,9 +285,6 @@ module Invidious::Routes::Watch
if CONFIG.disabled?("downloads") if CONFIG.disabled?("downloads")
return error_template(403, "Administrator has disabled this endpoint.") return error_template(403, "Administrator has disabled this endpoint.")
end end
if CONFIG.invidious_companion.present?
return error_template(403, "Downloads should be routed through Companion when present")
end
title = env.params.body["title"]? || "" title = env.params.body["title"]? || ""
video_id = env.params.body["id"]? || "" video_id = env.params.body["id"]? || ""
@ -325,9 +314,10 @@ module Invidious::Routes::Watch
env.params.query["label"] = URI.decode_www_form(label.as_s) env.params.query["label"] = URI.decode_www_form(label.as_s)
return Invidious::Routes::API::V1::Videos.captions(env) return Invidious::Routes::API::V1::Videos.captions(env)
elsif itag = download_widget["itag"]?.try &.as_i.to_s elsif itag = download_widget["itag"]?.try &.as_i
# URL params specific to /latest_version # URL params specific to /latest_version
env.params.query["id"] = video_id env.params.query["id"] = video_id
env.params.query["itag"] = itag.to_s
env.params.query["title"] = filename env.params.query["title"] = filename
env.params.query["local"] = "true" env.params.query["local"] = "true"

View File

@ -15,7 +15,7 @@ struct Video
# NOTE: don't forget to bump this number if any change is made to # NOTE: don't forget to bump this number if any change is made to
# the `params` structure in videos/parser.cr!!! # the `params` structure in videos/parser.cr!!!
# #
SCHEMA_VERSION = 3 SCHEMA_VERSION = 2
property id : String property id : String

View File

@ -108,20 +108,18 @@ def extract_video_info(video_id : String)
params = parse_video_info(video_id, player_response) params = parse_video_info(video_id, player_response)
params["reason"] = JSON::Any.new(reason) if reason params["reason"] = JSON::Any.new(reason) if reason
if !CONFIG.invidious_companion.present? if player_response["streamingData"]? && player_response.dig?("streamingData", "adaptiveFormats", 0, "url").nil?
if player_response["streamingData"]? && player_response.dig?("streamingData", "adaptiveFormats", 0, "url").nil? LOGGER.warn("Missing URLs for adaptive formats, falling back to other YT clients.")
LOGGER.warn("Missing URLs for adaptive formats, falling back to other YT clients.") players_fallback = [YoutubeAPI::ClientType::TvHtml5, YoutubeAPI::ClientType::WebMobile]
players_fallback = [YoutubeAPI::ClientType::TvHtml5, YoutubeAPI::ClientType::WebMobile] players_fallback.each do |player_fallback|
players_fallback.each do |player_fallback| client_config.client_type = player_fallback
client_config.client_type = player_fallback player_fallback_response = try_fetch_streaming_data(video_id, client_config)
player_fallback_response = try_fetch_streaming_data(video_id, client_config) if player_fallback_response && player_fallback_response["streamingData"]? &&
if player_fallback_response && player_fallback_response["streamingData"]? && player_fallback_response.dig?("streamingData", "adaptiveFormats", 0, "url")
player_fallback_response.dig?("streamingData", "adaptiveFormats", 0, "url") streaming_data = player_response["streamingData"].as_h
streaming_data = player_response["streamingData"].as_h streaming_data["adaptiveFormats"] = player_fallback_response["streamingData"]["adaptiveFormats"]
streaming_data["adaptiveFormats"] = player_fallback_response["streamingData"]["adaptiveFormats"] player_response["streamingData"] = JSON::Any.new(streaming_data)
player_response["streamingData"] = JSON::Any.new(streaming_data) break
break
end
end end
end end
end end

View File

@ -22,8 +22,6 @@
audio_streams.each_with_index do |fmt, i| audio_streams.each_with_index do |fmt, i|
src_url = "/latest_version?id=#{video.id}&itag=#{fmt["itag"]}" src_url = "/latest_version?id=#{video.id}&itag=#{fmt["itag"]}"
src_url += "&local=true" if params.local src_url += "&local=true" if params.local
src_url = invidious_companion.public_url.to_s + src_url +
"&check=#{invidious_companion_encrypt(video.id)}" if (invidious_companion)
bitrate = fmt["bitrate"] bitrate = fmt["bitrate"]
mimetype = HTML.escape(fmt["mimeType"].as_s) mimetype = HTML.escape(fmt["mimeType"].as_s)
@ -36,12 +34,8 @@
<% end %> <% end %>
<% end %> <% end %>
<% else %> <% else %>
<% if params.quality == "dash" <% if params.quality == "dash" %>
src_url = "/api/manifest/dash/id/" + video.id + "?local=true&unique_res=1" <source src="/api/manifest/dash/id/<%= video.id %>?local=true&unique_res=1" type='application/dash+xml' label="dash">
src_url = invidious_companion.public_url.to_s + src_url +
"&check=#{invidious_companion_encrypt(video.id)}" if (invidious_companion)
%>
<source src="<%= src_url %>" type='application/dash+xml' label="dash">
<% end %> <% end %>
<% <%
@ -50,8 +44,6 @@
fmt_stream.each_with_index do |fmt, i| fmt_stream.each_with_index do |fmt, i|
src_url = "/latest_version?id=#{video.id}&itag=#{fmt["itag"]}" src_url = "/latest_version?id=#{video.id}&itag=#{fmt["itag"]}"
src_url += "&local=true" if params.local src_url += "&local=true" if params.local
src_url = invidious_companion.public_url.to_s + src_url +
"&check=#{invidious_companion_encrypt(video.id)}" if (invidious_companion)
quality = fmt["quality"] quality = fmt["quality"]
mimetype = HTML.escape(fmt["mimeType"].as_s) mimetype = HTML.escape(fmt["mimeType"].as_s)

View File

@ -46,43 +46,6 @@ struct YoutubeConnectionPool
end end
end end
struct CompanionConnectionPool
property pool : DB::Pool(HTTP::Client)
def initialize(capacity = 5, timeout = 5.0)
options = DB::Pool::Options.new(
initial_pool_size: 0,
max_pool_size: capacity,
max_idle_pool_size: capacity,
checkout_timeout: timeout
)
@pool = DB::Pool(HTTP::Client).new(options) do
companion = CONFIG.invidious_companion.sample
next make_client(companion.private_url, use_http_proxy: false)
end
end
def client(&)
conn = pool.checkout
begin
response = yield conn
rescue ex
conn.close
companion = CONFIG.invidious_companion.sample
conn = make_client(companion.private_url, use_http_proxy: false)
response = yield conn
ensure
pool.release(conn)
end
response
end
end
def add_yt_headers(request) def add_yt_headers(request)
request.headers.delete("User-Agent") if request.headers["User-Agent"] == "Crystal" request.headers.delete("User-Agent") if request.headers["User-Agent"] == "Crystal"
request.headers["User-Agent"] ||= "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36" request.headers["User-Agent"] ||= "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36"
@ -98,9 +61,9 @@ def add_yt_headers(request)
end end
end end
def make_client(url : URI, region = nil, force_resolve : Bool = false, force_youtube_headers : Bool = false, use_http_proxy : Bool = true) def make_client(url : URI, region = nil, force_resolve : Bool = false, force_youtube_headers : Bool = false)
client = HTTP::Client.new(url) client = HTTP::Client.new(url)
client.proxy = make_configured_http_proxy_client() if CONFIG.http_proxy && use_http_proxy client.proxy = make_configured_http_proxy_client() if CONFIG.http_proxy
# Force the usage of a specific configured IP Family # Force the usage of a specific configured IP Family
if force_resolve if force_resolve
@ -115,8 +78,8 @@ def make_client(url : URI, region = nil, force_resolve : Bool = false, force_you
return client return client
end end
def make_client(url : URI, region = nil, force_resolve : Bool = false, use_http_proxy : Bool = true, &) def make_client(url : URI, region = nil, force_resolve : Bool = false, &)
client = make_client(url, region, force_resolve: force_resolve, use_http_proxy: use_http_proxy) client = make_client(url, region, force_resolve: force_resolve)
begin begin
yield client yield client
ensure ensure

View File

@ -500,11 +500,7 @@ module YoutubeAPI
data["params"] = params data["params"] = params
end end
if CONFIG.invidious_companion.present? return self._post_json("/youtubei/v1/player", data, client_config)
return self._post_invidious_companion("/youtubei/v1/player", data)
else
return self._post_json("/youtubei/v1/player", data, client_config)
end
end end
#################################################################### ####################################################################
@ -670,49 +666,6 @@ module YoutubeAPI
return initial_data return initial_data
end end
####################################################################
# _post_invidious_companion(endpoint, data)
#
# Internal function that does the actual request to Invidious companion
# and handles errors.
#
# The requested data is an endpoint (URL without the domain part)
# and the data as a Hash object.
#
def _post_invidious_companion(
endpoint : String,
data : Hash,
) : Hash(String, JSON::Any)
headers = HTTP::Headers{
"Content-Type" => "application/json; charset=UTF-8",
"Authorization" => "Bearer #{CONFIG.invidious_companion_key}",
}
# Logging
LOGGER.debug("Invidious companion: Using endpoint: \"#{endpoint}\"")
LOGGER.trace("Invidious companion: POST data: #{data}")
# Send the POST request
begin
response = COMPANION_POOL.client &.post(endpoint, headers: headers, body: data.to_json)
body = response.body
if (response.status_code != 200)
raise Exception.new(
"Error while communicating with Invidious companion: \
status code: #{response.status_code} and body: #{body.dump}"
)
end
rescue ex
raise InfoException.new("Error while communicating with Invidious companion: " + (ex.message || "no extra info found"))
end
# Convert result to Hash
initial_data = JSON.parse(body).as_h
return initial_data
end
#################################################################### ####################################################################
# _decompress(body_io, headers) # _decompress(body_io, headers)
# #