Compare commits
28 commits
041d6cc9d3
...
caa4d2f79c
Author | SHA1 | Date | |
---|---|---|---|
caa4d2f79c | |||
1a129468e9 | |||
83256b2af1 | |||
31219ce196 | |||
a01c8c63d3 | |||
58c4d8c951 | |||
91bcec72c8 | |||
a63300e284 | |||
|
1de2054618 | ||
|
ab72bbad7a | ||
|
a571eeaa38 | ||
|
f710dd37bf | ||
|
7a070fa710 | ||
|
1f51edd0b9 | ||
|
734e72503f | ||
|
bb2e3b2a3e | ||
|
b51770dbdb | ||
|
9f846127ae | ||
|
1aa154b978 | ||
|
ff3305d521 | ||
|
409df4cff3 | ||
|
27b24f51ab | ||
|
1c9f5b0a2b | ||
|
2cc204a045 | ||
|
c612423a4d | ||
|
1954463371 | ||
|
73c84baf9f | ||
|
3dff7a76cf |
20 changed files with 293 additions and 88 deletions
|
@ -14,9 +14,23 @@ on:
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
runs-on: runner
|
runs-on: runner
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: https://code.forgejo.org/actions/checkout@v2
|
- uses: https://code.forgejo.org/actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set up cache
|
||||||
|
uses: https://code.forgejo.org/actions/cache@v3
|
||||||
|
id: ccache-restore
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
./lib
|
||||||
|
key: ${{ runner.os }}-invidious-${{ github.sha }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-invidious-
|
||||||
|
|
||||||
|
- name: Create cache directory
|
||||||
|
if: steps.ccache-restore.outputs.cache-hit != 'true'
|
||||||
|
run: mkdir -p ./lib
|
||||||
|
|
||||||
- uses: https://code.forgejo.org/docker/setup-buildx-action@v3
|
- uses: https://code.forgejo.org/docker/setup-buildx-action@v3
|
||||||
name: Setup Docker BuildX system
|
name: Setup Docker BuildX system
|
||||||
|
@ -32,7 +46,7 @@ jobs:
|
||||||
id: meta
|
id: meta
|
||||||
uses: https://github.com/docker/metadata-action@v5
|
uses: https://github.com/docker/metadata-action@v5
|
||||||
with:
|
with:
|
||||||
images: git.nadeko.net/fijxu/invidious
|
images: git.nadeko.net/fijxu/invidious-with-companion
|
||||||
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=latest,enable=${{ github.ref == format('refs/heads/{0}', 'master') }}
|
type=raw,value=latest,enable=${{ github.ref == format('refs/heads/{0}', 'master') }}
|
||||||
|
@ -44,9 +58,6 @@ jobs:
|
||||||
file: docker/Dockerfile
|
file: docker/Dockerfile
|
||||||
tags: ${{ steps.meta.outputs.tags }}
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
platforms: linux/amd64
|
platforms: linux/amd64
|
||||||
# cache-from: type=gha
|
|
||||||
# cache-to: type=gha,mode=max
|
|
||||||
push: true
|
push: true
|
||||||
build-args: |
|
build-args: |
|
||||||
"release=1"
|
"release=1"
|
||||||
|
|
||||||
|
|
|
@ -54,6 +54,44 @@ 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).
|
||||||
|
## Both parameter can have identical URL when Invidious is hosted in
|
||||||
|
## an internal network or at home.
|
||||||
|
##
|
||||||
|
## 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
|
||||||
|
## The size of the key needs to be more or equal to 16.
|
||||||
|
##
|
||||||
|
## Needed when invidious_companion is configured
|
||||||
|
##
|
||||||
|
## Accepted values: "http(s)://<IP-HOSTNAME>:<Port>"
|
||||||
|
## Default: <none>
|
||||||
|
##
|
||||||
|
|
||||||
|
#invidious_companion_key: "CHANGE_ME!!"
|
||||||
|
|
||||||
#########################################
|
#########################################
|
||||||
#
|
#
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
FROM crystallang/crystal:1.14.0-alpine AS builder
|
FROM mirror.gcr.io/crystallang/crystal:1.14.0-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 if [[ "${release}" == 1 ]] ; then \
|
||||||
--link-flags "-lxml2 -llzma"; \
|
--link-flags "-lxml2 -llzma"; \
|
||||||
fi
|
fi
|
||||||
|
|
||||||
FROM alpine:3.20
|
FROM mirror.gcr.io/alpine:3.20
|
||||||
RUN apk add --no-cache rsvg-convert ttf-opensans tini tzdata
|
RUN apk add --no-cache rsvg-convert ttf-opensans tini tzdata
|
||||||
WORKDIR /invidious
|
WORKDIR /invidious
|
||||||
RUN addgroup -g 1000 -S invidious && \
|
RUN addgroup -g 1000 -S invidious && \
|
||||||
|
|
|
@ -211,10 +211,16 @@ Invidious::Jobs.register Invidious::Jobs::InstanceListRefreshJob.new
|
||||||
|
|
||||||
if !CONFIG.external_videoplayback_proxy.empty?
|
if !CONFIG.external_videoplayback_proxy.empty?
|
||||||
Invidious::Jobs.register Invidious::Jobs::CheckExternalProxy.new
|
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
|
end
|
||||||
|
|
||||||
if CONFIG.refresh_tokens
|
if !CONFIG.tokens_server.empty?
|
||||||
Invidious::Jobs.register Invidious::Jobs::RefreshTokens.new
|
Invidious::Jobs.register Invidious::Jobs::RefreshSessionTokens.new
|
||||||
|
else
|
||||||
|
LOGGER.info("jobs: Disabling RefreshSessionTokens job. Invidious will use the tokens that are on the configuration file")
|
||||||
end
|
end
|
||||||
|
|
||||||
Invidious::Jobs.start_all
|
Invidious::Jobs.start_all
|
||||||
|
|
|
@ -67,6 +67,16 @@ 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).
|
||||||
|
@ -106,9 +116,8 @@ class Config
|
||||||
property materialious_domain : String?
|
property materialious_domain : String?
|
||||||
# Alternative domains. You can add other domains, like TOR and I2P addresses
|
# Alternative domains. You can add other domains, like TOR and I2P addresses
|
||||||
property alternative_domains : Array(String) = [] of String
|
property alternative_domains : Array(String) = [] of String
|
||||||
property donation_url : String?
|
# Backend domains. Domains for numbered backends
|
||||||
property contact_url : String?
|
property backend_domains : Array(String) = [] of String
|
||||||
property home_domain : String?
|
|
||||||
|
|
||||||
# Subscribe to channels using PubSubHubbub (requires domain, hmac_key)
|
# Subscribe to channels using PubSubHubbub (requires domain, hmac_key)
|
||||||
property use_pubsub_feeds : Bool | Int32 = false
|
property use_pubsub_feeds : Bool | Int32 = false
|
||||||
|
@ -175,6 +184,12 @@ 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
|
||||||
|
@ -195,15 +210,14 @@ class Config
|
||||||
# at the start of the URI
|
# at the start of the URI
|
||||||
property external_videoplayback_proxy : Array(NamedTuple(url: String, balance: Bool)) = [] of NamedTuple(url: String, balance: Bool)
|
property external_videoplayback_proxy : Array(NamedTuple(url: String, balance: Bool)) = [] of NamedTuple(url: String, balance: Bool)
|
||||||
|
|
||||||
# Job to refresh tokens from a Redis compatible DB
|
|
||||||
property refresh_tokens : Bool = true
|
|
||||||
|
|
||||||
property pubsub_domain : String = ""
|
property pubsub_domain : String = ""
|
||||||
|
|
||||||
property ignore_user_tokens : Bool = false
|
property ignore_user_tokens : Bool = false
|
||||||
|
|
||||||
property server_id_cookie_name : String = "INVIDIOUS_SERVER_ID"
|
property server_id_cookie_name : String = "INVIDIOUS_SERVER_ID"
|
||||||
|
|
||||||
|
property tokens_server : String = ""
|
||||||
|
|
||||||
{% if flag?(:linux) %}
|
{% if flag?(:linux) %}
|
||||||
property reload_config_automatically : Bool = true
|
property reload_config_automatically : Bool = true
|
||||||
{% end %}
|
{% end %}
|
||||||
|
@ -330,6 +344,24 @@ class Config
|
||||||
end
|
end
|
||||||
{% end %}
|
{% end %}
|
||||||
|
|
||||||
|
if !config.invidious_companion.empty?
|
||||||
|
# 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)
|
||||||
|
end
|
||||||
|
if 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 or more."
|
||||||
|
exit(1)
|
||||||
|
end
|
||||||
|
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?
|
||||||
|
|
|
@ -1,36 +0,0 @@
|
||||||
module Tokens
|
|
||||||
extend self
|
|
||||||
@@po_token : String | Nil
|
|
||||||
@@visitor_data : String | Nil
|
|
||||||
|
|
||||||
def refresh_tokens
|
|
||||||
@@po_token = REDIS_DB.get("invidious:po_token")
|
|
||||||
@@visitor_data = REDIS_DB.get("invidious:visitor_data")
|
|
||||||
if !@@po_token.nil? && !@@visitor_data.nil?
|
|
||||||
set_tokens
|
|
||||||
LOGGER.debug("RefreshTokens: Successfully updated po_token and visitor_data")
|
|
||||||
else
|
|
||||||
LOGGER.warn("RefreshTokens: Tokens are empty!")
|
|
||||||
end
|
|
||||||
LOGGER.trace("RefreshTokens: Tokens are:")
|
|
||||||
LOGGER.trace("RefreshTokens: po_token: #{CONFIG.po_token}")
|
|
||||||
LOGGER.trace("RefreshTokens: visitor_data: #{CONFIG.visitor_data}")
|
|
||||||
end
|
|
||||||
|
|
||||||
def set_tokens
|
|
||||||
CONFIG.po_token = @@po_token
|
|
||||||
CONFIG.visitor_data = @@visitor_data
|
|
||||||
end
|
|
||||||
|
|
||||||
def get_tokens
|
|
||||||
return {@@po_token, @@visitor_data}
|
|
||||||
end
|
|
||||||
|
|
||||||
def get_po_token
|
|
||||||
return @@po_token
|
|
||||||
end
|
|
||||||
|
|
||||||
def get_visitor_data
|
|
||||||
return @@visitor_data
|
|
||||||
end
|
|
||||||
end
|
|
35
src/invidious/helpers/session_tokens.cr
Normal file
35
src/invidious/helpers/session_tokens.cr
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
module SessionTokens
|
||||||
|
extend self
|
||||||
|
@@po_token : String | Nil
|
||||||
|
@@visitor_data : String | Nil
|
||||||
|
|
||||||
|
def refresh_tokens
|
||||||
|
begin
|
||||||
|
response = HTTP::Client.get "#{CONFIG.tokens_server}/generate"
|
||||||
|
if !response.status_code == 200
|
||||||
|
LOGGER.error("RefreshSessionTokens: Expected response to have status code 200 but got #{response.status_code} from #{CONFIG.tokens_server}")
|
||||||
|
end
|
||||||
|
json = JSON.parse(response.body)
|
||||||
|
@@po_token = json.try &.["potoken"].as_s || nil
|
||||||
|
@@visitor_data = json.try &.["visitorData"].as_s || nil
|
||||||
|
rescue ex
|
||||||
|
LOGGER.error("RefreshSessionTokens: Failed to fetch tokens from #{CONFIG.tokens_server}: #{ex.message}")
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
if !@@po_token.nil? && !@@visitor_data.nil?
|
||||||
|
set_tokens
|
||||||
|
LOGGER.debug("RefreshSessionTokens: Successfully updated po_token and visitor_data")
|
||||||
|
else
|
||||||
|
LOGGER.warn("RefreshSessionTokens: Tokens are empty!. Invidious will use the tokens that are on the configuration file")
|
||||||
|
end
|
||||||
|
LOGGER.trace("RefreshSessionTokens: Tokens are:")
|
||||||
|
LOGGER.trace("RefreshSessionTokens: po_token: #{CONFIG.po_token}")
|
||||||
|
LOGGER.trace("RefreshSessionTokens: visitor_data: #{CONFIG.visitor_data}")
|
||||||
|
end
|
||||||
|
|
||||||
|
def set_tokens
|
||||||
|
CONFIG.po_token = @@po_token
|
||||||
|
CONFIG.visitor_data = @@visitor_data
|
||||||
|
end
|
||||||
|
end
|
|
@ -397,3 +397,22 @@ def gen_videoplayback_proxy_list
|
||||||
end
|
end
|
||||||
return external_videoplayback_proxy
|
return external_videoplayback_proxy
|
||||||
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
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
class Invidious::Jobs::RefreshTokens < Invidious::Jobs::BaseJob
|
class Invidious::Jobs::RefreshSessionTokens < Invidious::Jobs::BaseJob
|
||||||
def initialize
|
def initialize
|
||||||
end
|
end
|
||||||
|
|
||||||
def begin
|
def begin
|
||||||
loop do
|
loop do
|
||||||
Tokens.refresh_tokens
|
SessionTokens.refresh_tokens
|
||||||
LOGGER.info("RefreshTokens: Done, sleeping for 5 seconds")
|
LOGGER.info("RefreshTokens: Done, sleeping for 5 seconds")
|
||||||
sleep 5.seconds
|
sleep 5.seconds
|
||||||
Fiber.yield
|
Fiber.yield
|
||||||
|
|
|
@ -8,6 +8,11 @@ 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.empty?
|
||||||
|
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 }
|
||||||
|
|
|
@ -203,6 +203,13 @@ module Invidious::Routes::Embed
|
||||||
return env.redirect url
|
return env.redirect url
|
||||||
end
|
end
|
||||||
|
|
||||||
|
if companion_base_url = video.invidious_companion.try &.["baseUrl"].as_s
|
||||||
|
env.response.headers["Content-Security-Policy"] =
|
||||||
|
env.response.headers["Content-Security-Policy"]
|
||||||
|
.gsub("media-src", "media-src #{companion_base_url}")
|
||||||
|
.gsub("connect-src", "connect-src #{companion_base_url}")
|
||||||
|
end
|
||||||
|
|
||||||
rendered "embed"
|
rendered "embed"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -64,6 +64,8 @@ module Invidious::Routes::Login
|
||||||
# TOR or I2P address
|
# TOR or I2P address
|
||||||
if alt = CONFIG.alternative_domains.index(env.request.headers["Host"])
|
if alt = CONFIG.alternative_domains.index(env.request.headers["Host"])
|
||||||
env.response.cookies["SID"] = Invidious::User::Cookies.sid(CONFIG.alternative_domains[alt], sid)
|
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
|
else
|
||||||
env.response.cookies["SID"] = Invidious::User::Cookies.sid(CONFIG.domain, sid)
|
env.response.cookies["SID"] = Invidious::User::Cookies.sid(CONFIG.domain, sid)
|
||||||
end
|
end
|
||||||
|
@ -170,6 +172,8 @@ module Invidious::Routes::Login
|
||||||
# TOR or I2P address
|
# TOR or I2P address
|
||||||
if alt = CONFIG.alternative_domains.index(env.request.headers["Host"])
|
if alt = CONFIG.alternative_domains.index(env.request.headers["Host"])
|
||||||
env.response.cookies["SID"] = Invidious::User::Cookies.sid(CONFIG.alternative_domains[alt], sid)
|
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
|
else
|
||||||
env.response.cookies["SID"] = Invidious::User::Cookies.sid(CONFIG.domain, sid)
|
env.response.cookies["SID"] = Invidious::User::Cookies.sid(CONFIG.domain, sid)
|
||||||
end
|
end
|
||||||
|
|
|
@ -228,6 +228,8 @@ module Invidious::Routes::PreferencesRoute
|
||||||
# TOR or I2P address
|
# TOR or I2P address
|
||||||
if alt = CONFIG.alternative_domains.index(env.request.headers["Host"])
|
if alt = CONFIG.alternative_domains.index(env.request.headers["Host"])
|
||||||
env.response.cookies["PREFS"] = Invidious::User::Cookies.prefs(CONFIG.alternative_domains[alt], preferences)
|
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
|
else
|
||||||
env.response.cookies["PREFS"] = Invidious::User::Cookies.prefs(CONFIG.domain, preferences)
|
env.response.cookies["PREFS"] = Invidious::User::Cookies.prefs(CONFIG.domain, preferences)
|
||||||
end
|
end
|
||||||
|
@ -269,6 +271,8 @@ module Invidious::Routes::PreferencesRoute
|
||||||
# TOR or I2P address
|
# TOR or I2P address
|
||||||
if alt = CONFIG.alternative_domains.index(env.request.headers["Host"])
|
if alt = CONFIG.alternative_domains.index(env.request.headers["Host"])
|
||||||
env.response.cookies["PREFS"] = Invidious::User::Cookies.prefs(CONFIG.alternative_domains[alt], preferences)
|
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
|
else
|
||||||
env.response.cookies["PREFS"] = Invidious::User::Cookies.prefs(CONFIG.domain, preferences)
|
env.response.cookies["PREFS"] = Invidious::User::Cookies.prefs(CONFIG.domain, preferences)
|
||||||
end
|
end
|
||||||
|
|
|
@ -258,6 +258,11 @@ 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.empty?
|
||||||
|
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?
|
||||||
|
|
||||||
|
|
|
@ -217,6 +217,13 @@ module Invidious::Routes::Watch
|
||||||
video_url = nil
|
video_url = nil
|
||||||
end
|
end
|
||||||
|
|
||||||
|
if companion_base_url = video.invidious_companion.try &.["baseUrl"].as_s
|
||||||
|
env.response.headers["Content-Security-Policy"] =
|
||||||
|
env.response.headers["Content-Security-Policy"]
|
||||||
|
.gsub("media-src", "media-src #{companion_base_url}")
|
||||||
|
.gsub("connect-src", "connect-src #{companion_base_url}")
|
||||||
|
end
|
||||||
|
|
||||||
templated "watch"
|
templated "watch"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -347,14 +354,18 @@ 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
|
elsif itag = download_widget["itag"]?.try &.as_i.to_s
|
||||||
# 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"
|
||||||
|
|
||||||
return Invidious::Routes::VideoPlayback.latest_version(env)
|
if (!CONFIG.invidious_companion.empty?)
|
||||||
|
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
|
||||||
else
|
else
|
||||||
return error_template(400, "Invalid label or itag")
|
return error_template(400, "Invalid label or itag")
|
||||||
end
|
end
|
||||||
|
|
|
@ -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 = 2
|
SCHEMA_VERSION = 3
|
||||||
|
|
||||||
property id : String
|
property id : String
|
||||||
|
|
||||||
|
@ -192,6 +192,10 @@ struct Video
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def invidious_companion : Hash(String, JSON::Any)?
|
||||||
|
info["invidiousCompanion"]?.try &.as_h || {} of String => JSON::Any
|
||||||
|
end
|
||||||
|
|
||||||
# Macros defining getters/setters for various types of data
|
# Macros defining getters/setters for various types of data
|
||||||
|
|
||||||
private macro getset_string(name)
|
private macro getset_string(name)
|
||||||
|
|
|
@ -54,11 +54,6 @@ def extract_video_info(video_id : String)
|
||||||
# Init client config for the API
|
# Init client config for the API
|
||||||
client_config = YoutubeAPI::ClientConfig.new
|
client_config = YoutubeAPI::ClientConfig.new
|
||||||
|
|
||||||
redis_po_token, redis_visitor_data = Tokens.get_tokens
|
|
||||||
|
|
||||||
po_token = redis_po_token || CONFIG.po_token
|
|
||||||
visitor_data = redis_visitor_data || CONFIG.visitor_data
|
|
||||||
|
|
||||||
# Fetch data from the player endpoint
|
# Fetch data from the player endpoint
|
||||||
player_response = YoutubeAPI.player(video_id: video_id, params: "2AMB", client_config: client_config)
|
player_response = YoutubeAPI.player(video_id: video_id, params: "2AMB", client_config: client_config)
|
||||||
|
|
||||||
|
@ -105,30 +100,32 @@ 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
|
||||||
|
|
||||||
new_player_response = nil
|
if !CONFIG.invidious_companion.empty?
|
||||||
|
new_player_response = nil
|
||||||
|
|
||||||
# Don't use Android test suite client if po_token is passed because po_token doesn't
|
# Don't use Android test suite client if po_token is passed because po_token doesn't
|
||||||
# work for Android test suite client.
|
# work for Android test suite client.
|
||||||
if reason.nil? && po_token.nil?
|
if reason.nil? && CONFIG.po_token.nil?
|
||||||
# Fetch the video streams using an Android client in order to get the
|
# Fetch the video streams using an Android client in order to get the
|
||||||
# decrypted URLs and maybe fix throttling issues (#2194). See the
|
# decrypted URLs and maybe fix throttling issues (#2194). See the
|
||||||
# following issue for an explanation about decrypted URLs:
|
# following issue for an explanation about decrypted URLs:
|
||||||
# https://github.com/TeamNewPipe/NewPipeExtractor/issues/562
|
# https://github.com/TeamNewPipe/NewPipeExtractor/issues/562
|
||||||
client_config.client_type = YoutubeAPI::ClientType::AndroidTestSuite
|
client_config.client_type = YoutubeAPI::ClientType::AndroidTestSuite
|
||||||
new_player_response = try_fetch_streaming_data(video_id, client_config)
|
new_player_response = try_fetch_streaming_data(video_id, client_config)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Replace player response and reset reason
|
||||||
|
if !new_player_response.nil?
|
||||||
|
# Preserve captions & storyboard data before replacement
|
||||||
|
new_player_response["storyboards"] = player_response["storyboards"] if player_response["storyboards"]?
|
||||||
|
new_player_response["captions"] = player_response["captions"] if player_response["captions"]?
|
||||||
|
|
||||||
|
player_response = new_player_response
|
||||||
|
params.delete("reason")
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# Replace player response and reset reason
|
{"captions", "playabilityStatus", "playerConfig", "storyboards", "invidiousCompanion"}.each do |f|
|
||||||
if !new_player_response.nil?
|
|
||||||
# Preserve captions & storyboard data before replacement
|
|
||||||
new_player_response["storyboards"] = player_response["storyboards"] if player_response["storyboards"]?
|
|
||||||
new_player_response["captions"] = player_response["captions"] if player_response["captions"]?
|
|
||||||
|
|
||||||
player_response = new_player_response
|
|
||||||
params.delete("reason")
|
|
||||||
end
|
|
||||||
|
|
||||||
{"captions", "playabilityStatus", "playerConfig", "storyboards"}.each do |f|
|
|
||||||
params[f] = player_response[f] if player_response[f]?
|
params[f] = player_response[f] if player_response[f]?
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -22,6 +22,8 @@
|
||||||
audio_streams.each_with_index do |fmt, i|
|
audio_streams.each_with_index do |fmt, i|
|
||||||
src_url = "/latest_version?id=#{video.id}&itag=#{fmt["itag"]}"
|
src_url = "/latest_version?id=#{video.id}&itag=#{fmt["itag"]}"
|
||||||
src_url += "&local=true" if params.local
|
src_url += "&local=true" if params.local
|
||||||
|
src_url = video.invidious_companion["baseUrl"].as_s + src_url +
|
||||||
|
"&check=#{invidious_companion_encrypt(video.id)}" if (!CONFIG.invidious_companion.empty?)
|
||||||
|
|
||||||
bitrate = fmt["bitrate"]
|
bitrate = fmt["bitrate"]
|
||||||
mimetype = HTML.escape(fmt["mimeType"].as_s)
|
mimetype = HTML.escape(fmt["mimeType"].as_s)
|
||||||
|
@ -34,8 +36,12 @@
|
||||||
<% end %>
|
<% end %>
|
||||||
<% end %>
|
<% end %>
|
||||||
<% else %>
|
<% else %>
|
||||||
<% if params.quality == "dash" %>
|
<% if params.quality == "dash"
|
||||||
<source src="/api/manifest/dash/id/<%= video.id %>?local=true&unique_res=1" type='application/dash+xml' label="dash">
|
src_url = "/api/manifest/dash/id/" + video.id + "?local=true&unique_res=1"
|
||||||
|
src_url = video.invidious_companion["baseUrl"].as_s + src_url +
|
||||||
|
"&check=#{invidious_companion_encrypt(video.id)}" if (!CONFIG.invidious_companion.empty?)
|
||||||
|
%>
|
||||||
|
<source src="<%= src_url %>" type='application/dash+xml' label="dash">
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
||||||
<%
|
<%
|
||||||
|
@ -44,6 +50,8 @@
|
||||||
fmt_stream.each_with_index do |fmt, i|
|
fmt_stream.each_with_index do |fmt, i|
|
||||||
src_url = "/latest_version?id=#{video.id}&itag=#{fmt["itag"]}"
|
src_url = "/latest_version?id=#{video.id}&itag=#{fmt["itag"]}"
|
||||||
src_url += "&local=true" if params.local
|
src_url += "&local=true" if params.local
|
||||||
|
src_url = video.invidious_companion["baseUrl"].as_s + src_url +
|
||||||
|
"&check=#{invidious_companion_encrypt(video.id)}" if (!CONFIG.invidious_companion.empty?)
|
||||||
|
|
||||||
quality = fmt["quality"]
|
quality = fmt["quality"]
|
||||||
mimetype = HTML.escape(fmt["mimeType"].as_s)
|
mimetype = HTML.escape(fmt["mimeType"].as_s)
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
<%
|
<%
|
||||||
locale = env.get("preferences").as(Preferences).locale
|
locale = env.get("preferences").as(Preferences).locale
|
||||||
dark_mode = env.get("preferences").as(Preferences).dark_mode
|
dark_mode = env.get("preferences").as(Preferences).dark_mode
|
||||||
current_backend = env.request.cookies[CONFIG.server_id_cookie_name]?.try &.value
|
current_backend = env.request.cookies[CONFIG.server_id_cookie_name]?.try &.value || env.request.headers["Host"]
|
||||||
|
current_external_videoplayback_proxy = Invidious::HttpServer::Utils.get_external_proxy()
|
||||||
%>
|
%>
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="<%= locale %>">
|
<html lang="<%= locale %>">
|
||||||
|
@ -106,6 +107,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<% if !CONFIG.backends.empty? %>
|
<% if !CONFIG.backends.empty? %>
|
||||||
|
<% if !CONFIG.backend_domains.includes?(env.request.headers["Host"]) %>
|
||||||
<div class="h-box">
|
<div class="h-box">
|
||||||
<b>Switch Backend:</b>
|
<b>Switch Backend:</b>
|
||||||
<% CONFIG.backends.each do | backend | %>
|
<% CONFIG.backends.each do | backend | %>
|
||||||
|
@ -128,6 +130,7 @@
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
<% if CONFIG.banner %>
|
<% if CONFIG.banner %>
|
||||||
<div class="h-box">
|
<div class="h-box">
|
||||||
|
@ -311,6 +314,9 @@
|
||||||
<hr/>
|
<hr/>
|
||||||
<div class="footer-footer">
|
<div class="footer-footer">
|
||||||
<div class="box">You are currently using Backend: <%= current_backend %></div>
|
<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">
|
<span class="left">
|
||||||
<% if CONFIG.modified_source_code_url %>
|
<% if CONFIG.modified_source_code_url %>
|
||||||
<%= translate(locale, "footer_current_version_modified") %>
|
<%= translate(locale, "footer_current_version_modified") %>
|
||||||
|
|
|
@ -491,7 +491,11 @@ module YoutubeAPI
|
||||||
data["params"] = params
|
data["params"] = params
|
||||||
end
|
end
|
||||||
|
|
||||||
return self._post_json("/youtubei/v1/player", data, client_config)
|
if !CONFIG.invidious_companion.empty?
|
||||||
|
return self._post_invidious_companion("/youtubei/v1/player", data)
|
||||||
|
else
|
||||||
|
return self._post_json("/youtubei/v1/player", data, client_config)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
####################################################################
|
####################################################################
|
||||||
|
@ -657,6 +661,51 @@ 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
|
||||||
|
invidious_companion = CONFIG.invidious_companion.sample
|
||||||
|
response = make_client(invidious_companion.private_url,
|
||||||
|
&.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)
|
||||||
#
|
#
|
||||||
|
|
Loading…
Reference in a new issue