Compare commits

..

33 commits

Author SHA1 Message Date
Samantaz Fox
45695edeef
Update CHANGELOG.md 2024-11-09 23:34:48 -03:00
Samantaz Fox
71b19d915c
Channels: Use the same structure as in the other ctoken functions
Change explanation, courtesy of iBicha:
The \n is basically a decimal 10, which is 1010 binary. That is a field number
1, and a wire type 2 (length-delimited). Then the $ is a decimal 36, which is
exactly the length of 00000000-0000-0000-0000-000000000000.
So both objects end up being encoded into the same data.
2024-11-09 23:33:31 -03:00
Samantaz Fox
23cd7940ea
Channels: lockupViewModel is also used in the "playlists" tab 2024-11-09 23:33:31 -03:00
Samantaz Fox
f5a10f470c
Channels: Multiple small fixes
Fix the "newest" link not being bold when 'sort_by' uses the default value
Show 60 videos per page, rather than 30
2024-11-09 23:33:31 -03:00
Samantaz Fox
9bbe2b98de
Channels: Rename ctoken generator functions as requested 2024-11-09 23:33:31 -03:00
Samantaz Fox
404761748b
Extractors: Add support for shortsLockupViewModel
The 'shortsLockupViewModel' structure is used in the channel "shorts" tab
2024-11-09 23:33:31 -03:00
Samantaz Fox
f25d483db0
Extractors: Add support for lockupViewModel
The 'lockupViewModel' structure is used in the channel "podcasts" tab
2024-11-09 23:33:30 -03:00
syeopite
4053c5c5ef
Prevent discussion issues from being staled 2024-11-09 23:33:30 -03:00
syeopite
c5149e381e
Double stale timer for issues
Days before staling is increased to 730 days
Days before closing is increased to 60 days
2024-11-09 23:33:30 -03:00
syeopite
6f426013e0
Prevent PRs from being considered stale 2024-11-09 23:33:30 -03:00
Samantaz Fox
7e9e45c85d
Channels: Add sort options to shorts 2024-11-09 23:33:30 -03:00
Samantaz Fox
e9fecd56a0
Channels: Add function to generate the new ctoken objects 2024-11-09 23:33:30 -03:00
Samantaz Fox
448ed939fe
CI: Check Crystal lint only on latest version (#5042)
* CI: Check Crystal lint only on latest version

* Apply suggestion from code review

Co-authored-by: syeopite <70992037+syeopite@users.noreply.github.com>

---------

Co-authored-by: syeopite <70992037+syeopite@users.noreply.github.com>
2024-11-09 23:33:30 -03:00
Émilien (perso)
1538131679
checking the status code returned by youtube (#5052)
* checking the status code returned by youtube

* add documentation link

* Update src/invidious/yt_backend/youtube_api.cr

Co-authored-by: syeopite <70992037+syeopite@users.noreply.github.com>

---------

Co-authored-by: syeopite <70992037+syeopite@users.noreply.github.com>
2024-11-09 23:33:30 -03:00
Samantaz Fox
50859b42c6
Locales: Add Bulgarian, Welsh and Lombard to the list 2024-11-09 23:33:30 -03:00
Brahim Hadriche
4120e19d32
refactor 2024-11-09 23:33:29 -03:00
Samantaz Fox
8f4424fe79
Shards: Update database dependencies 2024-11-09 23:33:28 -03:00
Brahim Hadriche
fe7c745667
[Alternative] Fix for channel live videos 2024-11-09 23:33:00 -03:00
4cdeb283c7
fixup! Logger: Add color support for different log levels 2024-11-09 23:33:00 -03:00
44ed00592c
Logger: colorize_logs false by default 2024-11-09 23:33:00 -03:00
bbad70dd5e
Logger: Make colorize_logs true by default 2024-11-09 23:33:00 -03:00
500b1f6c38
Logger: Add color support for different log levels 2024-11-09 23:32:23 -03:00
syeopite
6078232bbe
make_client: add YouTube headers on *.youtube.com 2024-11-09 23:31:21 -03:00
syeopite
82bd79bb0f
Pool: Use force_resolve in fallback new client 2024-11-09 23:31:20 -03:00
syeopite
2cbf245aae
Ensure IP family is always used when force_resolve 2024-11-09 23:31:20 -03:00
syeopite
d608ad185e
Search API: Fix named arg syntax to make_client 2024-11-09 23:31:20 -03:00
syeopite
dc575ee798
Typo
Co-authored-by: Samantaz Fox <coding@samantaz.fr>
2024-11-09 23:31:20 -03:00
syeopite
7977dc3c8b
Fix typo in argument to make_client
Co-authored-by: ChunkyProgrammer <78101139+ChunkyProgrammer@users.noreply.github.com>
2024-11-09 23:31:20 -03:00
syeopite
4125dfb566
Use make_client instead of calling HTTP::Client
Using `make_client` to create `HTTP::Client`, allows for a simple way to
easily add logic to all `HTTP::Client` initialized within Invidious.
2024-11-09 23:31:18 -03:00
syeopite
53c4ffbdf3
Fix named arg syntax when passing force_resolve 2024-11-09 23:30:55 -03:00
73ec78dfe2
Remove old code that is done on the Openresty side 2024-11-09 23:28:24 -03:00
cf7d95b375
Update CI 2024-11-09 23:28:24 -03:00
0a08700b48
Videos: Add support for OpenGraph videos
To support OpenGraph clients like Discord and other platforms able to
pull the video from the OpenGraph metadata.
2024-11-09 23:28:22 -03:00
40 changed files with 386 additions and 551 deletions

View file

@ -3,84 +3,8 @@
## vX.Y.0 (future) ## vX.Y.0 (future)
## v2.20241110.0
### Wrap-up
This release is most importantly here to fix to the annoying "Youtube API returned error 400"
error that prevented all channel pages from loading.
If you're updating from the previous release, it provides no improvements on the ability to play
videos. If updating from a commit in-between release, it removes the "Please sign in" error caused
by a previous attempt at restoring video playback on large instances.
In the preferences, a new option allows for control of video preload. When enabled, this option
tells the browser to load the video as soon as the page is loaded (this used to be the default).
When disabled, the video starts loading only when the "play" button is pressed.
New interface languages available: Bulgarian, Welsh and Lombard
New dependency required: `tzdata`.
An HTTP proxy can be configured directly in Invidious, if needed. \
**NOTE:** In that case, it is recommended to comment out `force_resolve`.
### New features & important changes
#### For users
* Channels: Fix "Youtube API returned error 400" error preventing channel pages from loading
* Channels: Shorts can now be sorted by "newest", "oldest" and "popular"
* Preferences: Addition of the new "preload" option
* New interface languages available: Bulgarian, Welsh and Lombard
* Added "Filipino (auto-generated)" to the list of caption languages available
* Lots of new translations from Weblate
#### For instance owners
* Allow the configuration of an HTTP proxy to talk to Youtube
* Invidious tries to reconnect to `inv_sig_helper` if the socket is closed
* The instance list is downloaded in the background to improve redirection speed
* New `colorize_logs` option makes each log level a different color
#### For developpers
* `/api/v1/channels/{id}/shorts` now supports the `sort-by` parameter with the following values:
`newest`, `oldest` and `popular`
* Older `/api/v1/channels/xyz/{id}` (tab name before UCID) were removed
* API/Search: New video metadata available: `isNew`, `is4k`, `is8k`, `isVr180`, `isVr360`,
`is3d` and `hasCaptions`
### Bugs fixed
#### User-side
* Channels: The second page of shorts now loads as expected
* Channels: Fixed intermittent empty "playlists" tab
* Search: Fixed `youtu.be` URLs not being properly redirected to the watch page
* Fixed `DB::MappingException` error on the subscriptions feed (due to missing `tzdata` in docker)
* Switching to another instance is much faster
* Fixed an "invalid byte sequence" error when subscribing to a playlist
* Videos: Playback URLs were sometimes broken when cached and `inv_sig_helper` was used
#### For instance owners
* Fix `force_resolve` being ignored in some cases
#### API
* API/Videos: Fixed `live_now` and `premiere_timestamp` sometimes not having the right values
### Full list of pull requests merged since the last release (newest first) ### Full list of pull requests merged since the last release (newest first)
* API: Add "sort_by" parameter to channels/shorts endpoint ([#5071], thanks @iBicha)
* Docker: Install tzdata in Dockerfile ([#5070], by @SamantazFox)
* Videos: Stop using TVHTML5_SIMPLY_EMBEDDED_PLAYER ([#5063], thanks @unixfox)
* Routing: Deprecate old channel API routes ([#5045], by @SamantazFox)
* Videos: use WEB client instead of WEB CREATOR ([#4984], thanks @unixfox)
* Parsers: Fix parsing live_now and premiere_timestamp ([#4934], thanks @absidue)
* Stale bot updates ([#5060], thanks @syeopite) * Stale bot updates ([#5060], thanks @syeopite)
* Channels: Fix "Youtube API returned error 400" ([#5059], by @SamantazFox) * Channels: Fix "Youtube API returned error 400" ([#5059], by @SamantazFox)
* Channels: Fix for live videos ([#5027], thanks @iBicha) * Channels: Fix for live videos ([#5027], thanks @iBicha)
@ -128,21 +52,15 @@ An HTTP proxy can be configured directly in Invidious, if needed. \
[#4928]: https://github.com/iv-org/invidious/pull/4928 [#4928]: https://github.com/iv-org/invidious/pull/4928
[#4930]: https://github.com/iv-org/invidious/pull/4930 [#4930]: https://github.com/iv-org/invidious/pull/4930
[#4931]: https://github.com/iv-org/invidious/pull/4931 [#4931]: https://github.com/iv-org/invidious/pull/4931
[#4934]: https://github.com/iv-org/invidious/pull/4934
[#4942]: https://github.com/iv-org/invidious/pull/4942 [#4942]: https://github.com/iv-org/invidious/pull/4942
[#4984]: https://github.com/iv-org/invidious/pull/4984
[#4991]: https://github.com/iv-org/invidious/pull/4991 [#4991]: https://github.com/iv-org/invidious/pull/4991
[#4993]: https://github.com/iv-org/invidious/pull/4993 [#4993]: https://github.com/iv-org/invidious/pull/4993
[#4995]: https://github.com/iv-org/invidious/pull/4995 [#4995]: https://github.com/iv-org/invidious/pull/4995
[#5027]: https://github.com/iv-org/invidious/pull/5027 [#5027]: https://github.com/iv-org/invidious/pull/5027
[#5034]: https://github.com/iv-org/invidious/pull/5034 [#5034]: https://github.com/iv-org/invidious/pull/5034
[#5045]: https://github.com/iv-org/invidious/pull/5045
[#5046]: https://github.com/iv-org/invidious/pull/5046 [#5046]: https://github.com/iv-org/invidious/pull/5046
[#5059]: https://github.com/iv-org/invidious/pull/5059 [#5059]: https://github.com/iv-org/invidious/pull/5059
[#5060]: https://github.com/iv-org/invidious/pull/5060 [#5060]: https://github.com/iv-org/invidious/pull/5060
[#5063]: https://github.com/iv-org/invidious/pull/5063
[#5070]: https://github.com/iv-org/invidious/pull/5070
[#5071]: https://github.com/iv-org/invidious/pull/5071
## v2.20240825.2 (2024-08-26) ## v2.20240825.2 (2024-08-26)

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 size of the key needs to be more or equal to 16.
##
## 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
## Default: <none>
##
#invidious_companion_key: "CHANGE_ME!!"
######################################### #########################################
# #

View file

@ -1,4 +1,4 @@
FROM mirror.gcr.io/crystallang/crystal:1.14.0-alpine AS builder FROM 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
@ -23,7 +23,7 @@ RUN crystal spec --warnings all \
--link-flags "-lxml2 -llzma" --link-flags "-lxml2 -llzma"
RUN if [[ "${release}" == 1 ]] ; then \ RUN if [[ "${release}" == 1 ]] ; then \
crystal build ./src/invidious.cr \ crystal build ./src/invidious.cr \
--release --mcpu=x86-64-v2 \ --release --mcpu=x86-64-v3 \
--static --warnings all \ --static --warnings all \
--link-flags "-lxml2 -llzma"; \ --link-flags "-lxml2 -llzma"; \
else \ else \
@ -32,8 +32,8 @@ RUN if [[ "${release}" == 1 ]] ; then \
--link-flags "-lxml2 -llzma"; \ --link-flags "-lxml2 -llzma"; \
fi fi
FROM mirror.gcr.io/alpine:3.20 FROM alpine:3.18
RUN apk add --no-cache rsvg-convert ttf-opensans tini tzdata RUN apk add --no-cache rsvg-convert ttf-opensans tini
WORKDIR /invidious WORKDIR /invidious
RUN addgroup -g 1000 -S invidious && \ RUN addgroup -g 1000 -S invidious && \
adduser -u 1000 -S invidious -G invidious adduser -u 1000 -S invidious -G invidious

View file

@ -1,6 +1,5 @@
FROM alpine:3.20 AS builder FROM alpine:3.19 AS builder
RUN apk add --no-cache 'crystal=1.12.2-r0' shards sqlite-static yaml-static yaml-dev libxml2-static \ RUN apk add --no-cache 'crystal=1.10.1-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,8 +32,8 @@ RUN if [[ "${release}" == 1 ]] ; then \
--link-flags "-lxml2 -llzma"; \ --link-flags "-lxml2 -llzma"; \
fi fi
FROM alpine:3.20 FROM alpine:3.18
RUN apk add --no-cache rsvg-convert ttf-opensans tini tzdata RUN apk add --no-cache rsvg-convert ttf-opensans tini
WORKDIR /invidious WORKDIR /invidious
RUN addgroup -g 1000 -S invidious && \ RUN addgroup -g 1000 -S invidious && \
adduser -u 1000 -S invidious -G invidious adduser -u 1000 -S invidious -G invidious

View file

@ -1,12 +1,13 @@
name: invidious name: invidious
version: 2.20241110.0-dev version: 0.20.1
authors: authors:
- Invidious team <contact@invidious.io> - Omar Roth <omarroth@protonmail.com>
- Contributors! - Invidious team
description: | targets:
Invidious is an alternative front-end to YouTube invidious:
main: src/invidious.cr
dependencies: dependencies:
pg: pg:
@ -44,10 +45,6 @@ development_dependencies:
github: crystal-ameba/ameba github: crystal-ameba/ameba
version: ~> 1.6.1 version: ~> 1.6.1
crystal: ">= 1.10.0, < 2.0.0" crystal: ">= 1.0.0, < 2.0.0"
license: AGPLv3 license: AGPLv3
repository: https://github.com/iv-org/invidious
homepage: https://invidious.io
documentation: https://docs.invidious.io

View file

@ -88,6 +88,7 @@ REDDIT_URL = URI.parse("https://www.reddit.com")
YT_URL = URI.parse("https://www.youtube.com") YT_URL = URI.parse("https://www.youtube.com")
PUBSUB_HOST_URL = CONFIG.pubsub_domain PUBSUB_HOST_URL = CONFIG.pubsub_domain
HOST_URL = make_host_url(Kemal.config) HOST_URL = make_host_url(Kemal.config)
EXT_VIDEOP_LIST = gen_videoplayback_proxy_list()
CHARS_SAFE = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_" CHARS_SAFE = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"
TEST_IDS = {"AgbeGFYluEA", "BaW_jenozKc", "a9LDPn-MO4I", "ddFvjfvPnqk", "iqKdEhx-dD4"} TEST_IDS = {"AgbeGFYluEA", "BaW_jenozKc", "a9LDPn-MO4I", "ddFvjfvPnqk", "iqKdEhx-dD4"}
@ -210,16 +211,10 @@ 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.tokens_server.empty? if CONFIG.refresh_tokens
Invidious::Jobs.register Invidious::Jobs::RefreshSessionTokens.new Invidious::Jobs.register Invidious::Jobs::RefreshTokens.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

View file

@ -45,6 +45,8 @@ struct ConfigPreferences
property vr_mode : Bool = true property vr_mode : Bool = true
property show_nick : Bool = true property show_nick : Bool = true
property save_player_pos : Bool = false property save_player_pos : Bool = false
property po_token : String = ""
property visitor_data : String = ""
def to_tuple def to_tuple
{% begin %} {% begin %}
@ -67,16 +69,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).
@ -116,12 +108,12 @@ 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
# Backend domains. Domains for numbered backends property donation_url : String?
property backend_domains : Array(String) = [] of String property contact_url : 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
property use_innertube_for_feeds : Bool = true
property popular_enabled : Bool = true property popular_enabled : Bool = true
property captcha_enabled : Bool = true property captcha_enabled : Bool = true
property login_enabled : Bool = true property login_enabled : Bool = true
@ -185,12 +177,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
@ -209,7 +195,10 @@ class Config
# External videoplayback proxies list. They should include `https://` # External videoplayback proxies list. They should include `https://`
# at the start of the URI # at the start of the URI
property external_videoplayback_proxy : Array(String) = [] of String 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 = ""
@ -217,8 +206,6 @@ class Config
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 %}
@ -345,23 +332,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 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?

View file

@ -4,11 +4,22 @@ module Invidious::Database::Videos
extend self extend self
def insert(video : Video) def insert(video : Video)
request = <<-SQL
INSERT INTO videos
VALUES ($1, $2, $3)
ON CONFLICT (id) DO NOTHING
SQL
REDIS_DB.set(video.id, video.info.to_json, ex: 14400) REDIS_DB.set(video.id, video.info.to_json, ex: 14400)
REDIS_DB.set(video.id + ":time", video.updated, ex: 14400) REDIS_DB.set(video.id + ":time", video.updated, ex: 14400)
end end
def delete(id) def delete(id)
request = <<-SQL
DELETE FROM videos *
WHERE id = $1
SQL
REDIS_DB.del(id) REDIS_DB.del(id)
REDIS_DB.del(id + ":time") REDIS_DB.del(id + ":time")
end end
@ -33,6 +44,11 @@ module Invidious::Database::Videos
end end
def select(id : String) : Video? def select(id : String) : Video?
request = <<-SQL
SELECT * FROM videos
WHERE id = $1
SQL
if ((info = REDIS_DB.get(id)) && (time = REDIS_DB.get(id + ":time"))) if ((info = REDIS_DB.get(id)) && (time = REDIS_DB.get(id + ":time")))
return Video.new({ return Video.new({
id: id, id: id,

View file

@ -180,7 +180,6 @@ def error_redirect_helper(env : HTTP::Server::Context)
next_steps_text = translate(locale, "next_steps_error_message") next_steps_text = translate(locale, "next_steps_error_message")
refresh = translate(locale, "next_steps_error_message_refresh") refresh = translate(locale, "next_steps_error_message_refresh")
go_to_youtube = translate(locale, "next_steps_error_message_go_to_youtube") go_to_youtube = translate(locale, "next_steps_error_message_go_to_youtube")
go_to_youtube_embed = translate(locale, "videoinfo_youTube_embed_link")
switch_instance = translate(locale, "Switch Invidious Instance") switch_instance = translate(locale, "Switch Invidious Instance")
return <<-END_HTML return <<-END_HTML
@ -194,7 +193,6 @@ def error_redirect_helper(env : HTTP::Server::Context)
</li> </li>
<li> <li>
<a rel="noreferrer noopener" href="https://youtube.com#{env.request.resource}">#{go_to_youtube}</a> <a rel="noreferrer noopener" href="https://youtube.com#{env.request.resource}">#{go_to_youtube}</a>
(<a rel="noreferrer noopener" href="https://youtube.com/embed/#{env.params.query["v"]}">#{go_to_youtube_embed}</a>)
</li> </li>
</ul> </ul>
END_HTML END_HTML

View file

@ -0,0 +1,54 @@
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?
LOGGER.debug("RefreshTokens: Successfully updated tokens")
else
LOGGER.warn("RefreshTokens: Tokens are empty!")
end
LOGGER.trace("RefreshTokens: Tokens are:")
LOGGER.trace("RefreshTokens: po_token: #{@@po_token}")
LOGGER.trace("RefreshTokens: 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
def generate_tokens(user : String)
po_token = ""
visitor_data = ""
attempts = 0
LOGGER.debug("Generating po_token and visitor_data for user: '#{user}'")
REDIS_DB.publish("generate-token", "#{user}")
while REDIS_DB.get("invidious:#{user}:po_token").nil? && REDIS_DB.get("invidious:#{user}:visitor_data").nil?
if attempts > 50
break
end
LOGGER.debug("Waiting for tokens to arrive at redis for user: '#{user}'")
attempts += 1
sleep 250.milliseconds
end
po_token = REDIS_DB.get("invidious:#{user}:po_token")
visitor_data = REDIS_DB.get("invidious:#{user}:visitor_data")
LOGGER.debug("Tokens successfully generated for user: '#{user}'")
return {po_token, visitor_data}
end
end

View file

@ -1,35 +0,0 @@
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

View file

@ -384,21 +384,16 @@ def parse_link_endpoint(endpoint : JSON::Any, text : String, video_id : String)
return text return text
end end
def encrypt_ecb_without_salt(data, key) # Generates a list of external videoplayback proxies for
cipher = OpenSSL::Cipher.new("aes-128-ecb") # CSP
cipher.encrypt def gen_videoplayback_proxy_list
cipher.key = key if !CONFIG.external_videoplayback_proxy.empty?
external_videoplayback_proxy = ""
io = IO::Memory.new CONFIG.external_videoplayback_proxy.each do |proxy|
io.write(cipher.update(data)) external_videoplayback_proxy += " #{proxy[:url]}"
io.write(cipher.final) end
io.rewind else
external_videoplayback_proxy = ""
return io end
end return external_videoplayback_proxy
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 end

View file

@ -9,19 +9,19 @@ module Invidious::HttpServer
def check_external_proxy def check_external_proxy
CONFIG.external_videoplayback_proxy.each do |proxy| CONFIG.external_videoplayback_proxy.each do |proxy|
begin begin
response = HTTP::Client.get("#{proxy}/health") response = HTTP::Client.get("#{proxy[:url]}/health")
if response.status_code == 200 if response.status_code == 200
@@proxy_alive = proxy @@proxy_alive = proxy[:url]
LOGGER.debug("CheckExternalProxy: Proxy set to: '#{proxy}'") LOGGER.debug("CheckExternalProxy: Proxy set to: '#{proxy[:url]}'")
break break
end end
rescue rescue
LOGGER.debug("CheckExternalProxy: Proxy '#{proxy}' is not available") LOGGER.debug("CheckExternalProxy: Proxy '#{proxy[:url]}' is not available")
end end
end end
if @@proxy_alive.empty? if @@proxy_alive.empty?
LOGGER.warn("CheckExternalProxy: No proxies alive! Using own server proxy") LOGGER.warn("CheckExternalProxy: No proxies alive! Using own server proxy")
end end
end end
def get_external_proxy def get_external_proxy

View file

@ -1,10 +1,10 @@
class Invidious::Jobs::RefreshSessionTokens < Invidious::Jobs::BaseJob class Invidious::Jobs::RefreshTokens < Invidious::Jobs::BaseJob
def initialize def initialize
end end
def begin def begin
loop do loop do
SessionTokens.refresh_tokens Tokens.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

View file

@ -8,7 +8,7 @@ module Invidious::JSONify::APIv1
build_thumbnails(id).each do |thumbnail| build_thumbnails(id).each do |thumbnail|
json.object do json.object do
json.field "quality", thumbnail[:name] json.field "quality", thumbnail[:name]
json.field "url", "/vi/#{id}/#{thumbnail["url"]}.jpg" json.field "url", "#{thumbnail[:host]}/vi/#{id}/#{thumbnail["url"]}.jpg"
json.field "width", thumbnail[:width] json.field "width", thumbnail[:width]
json.field "height", thumbnail[:height] json.field "height", thumbnail[:height]
end end

View file

@ -349,4 +349,40 @@ module Invidious::Routes::Account
return "{}" return "{}"
end end
end end
# -------------------
# poToken and visitorData tokens generation
# -------------------
# Generates a poToken & visitorData for the user, server side
def generate_tokens(env)
locale = env.get("preferences").as(Preferences).locale
preferences = env.get("preferences").as(Preferences)
user = env.get? "user"
sid = env.get? "sid"
referer = get_referer(env)
if !user
return env.redirect referer
end
user = user.as(User)
po_token, visitor_data = Tokens.generate_tokens(user.email)
if po_token.nil? || visitor_data.nil?
return error_template(500, "Internal server error. Please submit an issue here IF THE ISSUE PERSISTS: https://git.nadeko.net/Fijxu/invidious/issues")
end
user.preferences.po_token = po_token
user.preferences.visitor_data = visitor_data
Invidious::Database::Users.update_preferences(user)
REDIS_DB.del("invidious:#{user.email}:po_token")
REDIS_DB.del("invidious:#{user.email}:visitor_data")
templated "user/tokens"
end
end 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 }
@ -186,8 +181,9 @@ module Invidious::Routes::API::Manifest
manifest = response.body manifest = response.body
if local if local
manifest = manifest.gsub(/^https:\/\/\w+---.{11}\.c\.youtube\.com[^\n]*/m) do |match| manifest = manifest.gsub(/^https:\/\/[^\n"]*/m) do |match|
path = URI.parse(match).path uri = URI.parse(match)
path = uri.path
path = path.lchop("/videoplayback/") path = path.lchop("/videoplayback/")
path = path.rchop("/") path = path.rchop("/")
@ -216,20 +212,14 @@ module Invidious::Routes::API::Manifest
raw_params["fvip"] = fvip["fvip"] raw_params["fvip"] = fvip["fvip"]
end end
raw_params["local"] = "true" raw_params["host"] = uri.host.not_nil!
proxy = Invidious::HttpServer::Utils.get_external_proxy proxy = Invidious::HttpServer::Utils.get_external_proxy
if CONFIG.https_only
scheme = "https://"
else
scheme = "http://"
end
if !proxy.empty? if !proxy.empty?
"#{proxy}/videoplayback?#{raw_params}" "#{proxy}/videoplayback?#{raw_params}"
else else
"#{scheme}#{env.request.headers["Host"]}/videoplayback?#{raw_params}" "#{HOST_URL}/videoplayback?#{raw_params}"
end end
end end
end end
@ -253,12 +243,7 @@ module Invidious::Routes::API::Manifest
manifest = response.body manifest = response.body
if local if local
if CONFIG.https_only manifest = manifest.gsub("https://www.youtube.com", HOST_URL)
scheme = "https://"
else
scheme = "http://"
end
manifest = manifest.gsub("https://www.youtube.com", "#{scheme}#{env.request.headers["Host"]}")
manifest = manifest.gsub("index.m3u8", "index.m3u8?local=true") manifest = manifest.gsub("index.m3u8", "index.m3u8?local=true")
end end

View file

@ -226,7 +226,7 @@ module Invidious::Routes::API::V1::Authenticated
end end
playlist = create_playlist(title, privacy, user) playlist = create_playlist(title, privacy, user)
env.response.headers["Location"] = "#{env.request.headers["Host"]}/api/v1/auth/playlists/#{playlist.id}" env.response.headers["Location"] = "#{HOST_URL}/api/v1/auth/playlists/#{playlist.id}"
env.response.status_code = 201 env.response.status_code = 201
{ {
"title" => title, "title" => title,
@ -482,7 +482,7 @@ module Invidious::Routes::API::V1::Authenticated
env.response.content_type = "text/event-stream" env.response.content_type = "text/event-stream"
raw_topics = env.params.body["topics"]? || env.params.query["topics"]? raw_topics = env.params.body["topics"]? || env.params.query["topics"]?
topics = raw_topics.try &.split(",").uniq!.first(1000) topics = raw_topics.try &.split(",").uniq.first(1000)
topics ||= [] of String topics ||= [] of String
create_notification_stream(env, topics, CONNECTION_CHANNEL) create_notification_stream(env, topics, CONNECTION_CHANNEL)

View file

@ -197,7 +197,6 @@ module Invidious::Routes::API::V1::Channels
get_channel() get_channel()
# Retrieve continuation from URL parameters # Retrieve continuation from URL parameters
sort_by = env.params.query["sort_by"]?.try &.downcase || "newest"
continuation = env.params.query["continuation"]? continuation = env.params.query["continuation"]?
if channel.is_age_gated if channel.is_age_gated
@ -212,7 +211,7 @@ module Invidious::Routes::API::V1::Channels
else else
begin begin
videos, next_continuation = Channel::Tabs.get_shorts( videos, next_continuation = Channel::Tabs.get_shorts(
channel, continuation: continuation, sort_by: sort_by channel, continuation: continuation
) )
rescue ex rescue ex
return error_json(500, ex) return error_json(500, ex)

View file

@ -263,59 +263,60 @@ module Invidious::Routes::API::V1::Videos
annotations = "" annotations = ""
case source # case source
when "archive" # when "archive"
if CONFIG.cache_annotations && (cached_annotation = Invidious::Database::Annotations.select(id)) # if CONFIG.cache_annotations && (cached_annotation = Invidious::Database::Annotations.select(id))
annotations = cached_annotation.annotations # annotations = cached_annotation.annotations
else # else
index = CHARS_SAFE.index!(id[0]).to_s.rjust(2, '0') # index = CHARS_SAFE.index!(id[0]).to_s.rjust(2, '0')
# IA doesn't handle leading hyphens, # # IA doesn't handle leading hyphens,
# so we use https://archive.org/details/youtubeannotations_64 # # so we use https://archive.org/details/youtubeannotations_64
if index == "62" # if index == "62"
index = "64" # index = "64"
id = id.sub(/^-/, 'A') # id = id.sub(/^-/, 'A')
end # end
file = URI.encode_www_form("#{id[0, 3]}/#{id}.xml") # file = URI.encode_www_form("#{id[0, 3]}/#{id}.xml")
location = make_client(ARCHIVE_URL, &.get("/download/youtubeannotations_#{index}/#{id[0, 2]}.tar/#{file}")) # location = make_client(ARCHIVE_URL, &.get("/download/youtubeannotations_#{index}/#{id[0, 2]}.tar/#{file}"))
if !location.headers["Location"]? # if !location.headers["Location"]?
env.response.status_code = location.status_code # env.response.status_code = location.status_code
end # end
response = make_client(URI.parse(location.headers["Location"]), &.get(location.headers["Location"])) # response = make_client(URI.parse(location.headers["Location"]), &.get(location.headers["Location"]))
if response.body.empty? # if response.body.empty?
haltf env, 404 # haltf env, 404
end # end
if response.status_code != 200 # if response.status_code != 200
haltf env, response.status_code # haltf env, response.status_code
end # end
annotations = response.body # annotations = response.body
cache_annotation(id, annotations) # cache_annotation(id, annotations)
end # end
else # "youtube" # else # "youtube"
response = YT_POOL.client &.get("/annotations_invideo?video_id=#{id}") # response = YT_POOL.client &.get("/annotations_invideo?video_id=#{id}")
if response.status_code != 200 # if response.status_code != 200
haltf env, response.status_code # haltf env, response.status_code
end # end
annotations = response.body # annotations = response.body
end # end
etag = sha256(annotations)[0, 16] # etag = sha256(annotations)[0, 16]
if env.request.headers["If-None-Match"]?.try &.== etag # if env.request.headers["If-None-Match"]?.try &.== etag
haltf env, 304 # haltf env, 304
else # else
env.response.headers["ETag"] = etag # env.response.headers["ETag"] = etag
annotations # annotations
end # end
annotations
end end
def self.comments(env) def self.comments(env)

View file

@ -20,25 +20,12 @@ module Invidious::Routes::BeforeAll
env.response.headers["X-XSS-Protection"] = "1; mode=block" env.response.headers["X-XSS-Protection"] = "1; mode=block"
env.response.headers["X-Content-Type-Options"] = "nosniff" env.response.headers["X-Content-Type-Options"] = "nosniff"
extra_media_csp = ""
extra_connect_csp = ""
if CONFIG.invidious_companion.present?
extra_media_csp = " #{CONFIG.invidious_companion.sample.public_url}"
extra_connect_csp = " #{CONFIG.invidious_companion.sample.public_url}"
end
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 # Allow media resources to be loaded from google servers
# TODO: check if *.youtube.com can be removed # TODO: check if *.youtube.com can be removed
if CONFIG.disabled?("local") || !preferences.local if CONFIG.disabled?("local") || !preferences.local
extra_media_csp += " https://*.googlevideo.com:443 https://*.youtube.com:443" extra_media_csp = " https://*.googlevideo.com:443 https://*.youtube.com:443"
else
extra_media_csp = ""
end end
# Only allow the pages at /embed/* to be embedded # Only allow the pages at /embed/* to be embedded
@ -56,9 +43,9 @@ module Invidious::Routes::BeforeAll
"style-src 'self' 'unsafe-inline'", "style-src 'self' 'unsafe-inline'",
"img-src 'self' data:", "img-src 'self' data:",
"font-src 'self' data:", "font-src 'self' data:",
"connect-src 'self'" + extra_connect_csp, "connect-src 'self'" + EXT_VIDEOP_LIST,
"manifest-src 'self'", "manifest-src 'self'",
"media-src 'self' blob:" + extra_media_csp, "media-src 'self' blob:" + extra_media_csp + EXT_VIDEOP_LIST,
"child-src 'self' blob:", "child-src 'self' blob:",
"frame-src 'self'", "frame-src 'self'",
"frame-ancestors " + frame_ancestors, "frame-ancestors " + frame_ancestors,

View file

@ -416,22 +416,18 @@ module Invidious::Routes::Feeds
author = entry.xpath_node("default:author/default:name", namespaces).not_nil!.content author = entry.xpath_node("default:author/default:name", namespaces).not_nil!.content
published = Time.parse_rfc3339(entry.xpath_node("default:published", namespaces).not_nil!.content) published = Time.parse_rfc3339(entry.xpath_node("default:published", namespaces).not_nil!.content)
updated = Time.parse_rfc3339(entry.xpath_node("default:updated", namespaces).not_nil!.content) updated = Time.parse_rfc3339(entry.xpath_node("default:updated", namespaces).not_nil!.content)
ucid = entry.xpath_node("yt:channelId", namespaces).not_nil!.content
title = entry.xpath_node("default:title", namespaces).not_nil!.content
if CONFIG.use_innertube_for_feeds begin
begin video = get_video(id, force_refresh: true)
video_ = get_video(id, force_refresh: true) rescue
rescue next # skip this video since it raised an exception (e.g. it is a scheduled live event)
next # skip this video since it raised an exception (e.g. it is a scheduled live event)
end
end end
if CONFIG.enable_user_notifications if CONFIG.enable_user_notifications
# Deliver notifications to `/api/v1/auth/notifications` # Deliver notifications to `/api/v1/auth/notifications`
payload = { payload = {
"topic" => ucid, "topic" => video.ucid,
"videoId" => id, "videoId" => video.id,
"published" => published.to_unix, "published" => published.to_unix,
}.to_json }.to_json
PG_DB.exec("NOTIFY notifications, E'#{payload}'") PG_DB.exec("NOTIFY notifications, E'#{payload}'")
@ -439,15 +435,15 @@ module Invidious::Routes::Feeds
video = ChannelVideo.new({ video = ChannelVideo.new({
id: id, id: id,
title: title, title: video.title,
published: published, published: published,
updated: updated, updated: updated,
ucid: ucid, ucid: video.ucid,
author: author, author: author,
length_seconds: video_.try &.length_seconds || 0, length_seconds: video.length_seconds,
live_now: video_.try &.live_now || false, live_now: video.live_now,
premiere_timestamp: video_.try &.premiere_timestamp || nil, premiere_timestamp: video.premiere_timestamp,
views: video_.try &.views || nil, views: video.views,
}) })
was_insert = Invidious::Database::ChannelVideos.insert(video, with_premiere_timestamp: true) was_insert = Invidious::Database::ChannelVideos.insert(video, with_premiere_timestamp: true)

View file

@ -64,8 +64,6 @@ 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
@ -172,8 +170,6 @@ 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

View file

@ -86,6 +86,12 @@ module Invidious::Routes::PreferencesRoute
show_nick ||= "off" show_nick ||= "off"
show_nick = show_nick == "on" show_nick = show_nick == "on"
po_token = env.params.body["po_token"]?.try &.as(String)
po_token ||= CONFIG.default_user_preferences.po_token
visitor_data = env.params.body["visitor_data"]?.try &.as(String)
visitor_data ||= CONFIG.default_user_preferences.visitor_data
comments = [] of String comments = [] of String
2.times do |i| 2.times do |i|
comments << (env.params.body["comments[#{i}]"]?.try &.as(String) || CONFIG.default_user_preferences.comments[i]) comments << (env.params.body["comments[#{i}]"]?.try &.as(String) || CONFIG.default_user_preferences.comments[i])
@ -180,6 +186,8 @@ module Invidious::Routes::PreferencesRoute
vr_mode: vr_mode, vr_mode: vr_mode,
show_nick: show_nick, show_nick: show_nick,
save_player_pos: save_player_pos, save_player_pos: save_player_pos,
po_token: po_token,
visitor_data: visitor_data,
}.to_json) }.to_json)
if user = env.get? "user" if user = env.get? "user"
@ -228,8 +236,6 @@ 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
@ -271,8 +277,6 @@ 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

View file

@ -258,11 +258,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

@ -3,6 +3,14 @@
module Invidious::Routes::Watch module Invidious::Routes::Watch
def self.handle(env) def self.handle(env)
locale = env.get("preferences").as(Preferences).locale locale = env.get("preferences").as(Preferences).locale
if !CONFIG.ignore_user_tokens
user_po_token = env.get("preferences").as(Preferences).po_token
user_visitor_data = env.get("preferences").as(Preferences).visitor_data
else
user_po_token = ""
user_visitor_data = ""
end
region = env.params.query["region"]? region = env.params.query["region"]?
if env.params.query.to_s.includes?("%20") || env.params.query.to_s.includes?("+") if env.params.query.to_s.includes?("%20") || env.params.query.to_s.includes?("+")
@ -52,7 +60,7 @@ module Invidious::Routes::Watch
env.params.query.delete_all("listen") env.params.query.delete_all("listen")
begin begin
video = get_video(id, region: params.region) video = get_video(id, region: params.region, po_token: user_po_token, visitor_data: user_visitor_data)
rescue ex : NotFoundException rescue ex : NotFoundException
LOGGER.error("get_video not found: #{id} : #{ex.message}") LOGGER.error("get_video not found: #{id} : #{ex.message}")
return error_template(404, ex) return error_template(404, ex)
@ -144,7 +152,7 @@ module Invidious::Routes::Watch
end end
end end
# Removes non default audio tracks # Removes non default audio tracks
audio_streams.reject! do |z| audio_streams.reject! do |z|
z if z.dig?("audioTrack", "audioIsDefault") == false z if z.dig?("audioTrack", "audioIsDefault") == false
end end
@ -211,11 +219,11 @@ module Invidious::Routes::Watch
captions: video.captions captions: video.captions
) )
begin begin
video_url = fmt_stream[0]["url"].to_s video_url = fmt_stream[0]["url"].to_s
rescue rescue
video_url = nil video_url = nil
end end
templated "watch" templated "watch"
end end
@ -347,18 +355,14 @@ 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"
if (CONFIG.invidious_companion.present?) return Invidious::Routes::VideoPlayback.latest_version(env)
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

View file

@ -76,6 +76,7 @@ module Invidious::Routing
post "/authorize_token", Routes::Account, :post_authorize_token post "/authorize_token", Routes::Account, :post_authorize_token
get "/token_manager", Routes::Account, :token_manager get "/token_manager", Routes::Account, :token_manager
post "/token_ajax", Routes::Account, :token_ajax post "/token_ajax", Routes::Account, :token_ajax
get "/generate_tokens", Routes::Account, :generate_tokens
post "/subscription_ajax", Routes::Subscriptions, :toggle_subscription post "/subscription_ajax", Routes::Subscriptions, :toggle_subscription
get "/subscription_manager", Routes::Subscriptions, :subscription_manager get "/subscription_manager", Routes::Subscriptions, :subscription_manager
end end
@ -243,16 +244,17 @@ module Invidious::Routing
# Channels # Channels
get "/api/v1/channels/:ucid", {{namespace}}::Channels, :home get "/api/v1/channels/:ucid", {{namespace}}::Channels, :home
get "/api/v1/channels/:ucid/latest", {{namespace}}::Channels, :latest
get "/api/v1/channels/:ucid/videos", {{namespace}}::Channels, :videos
get "/api/v1/channels/:ucid/shorts", {{namespace}}::Channels, :shorts get "/api/v1/channels/:ucid/shorts", {{namespace}}::Channels, :shorts
get "/api/v1/channels/:ucid/streams", {{namespace}}::Channels, :streams get "/api/v1/channels/:ucid/streams", {{namespace}}::Channels, :streams
get "/api/v1/channels/:ucid/podcasts", {{namespace}}::Channels, :podcasts get "/api/v1/channels/:ucid/podcasts", {{namespace}}::Channels, :podcasts
get "/api/v1/channels/:ucid/releases", {{namespace}}::Channels, :releases get "/api/v1/channels/:ucid/releases", {{namespace}}::Channels, :releases
get "/api/v1/channels/:ucid/playlists", {{namespace}}::Channels, :playlists
get "/api/v1/channels/:ucid/community", {{namespace}}::Channels, :community
get "/api/v1/channels/:ucid/channels", {{namespace}}::Channels, :channels get "/api/v1/channels/:ucid/channels", {{namespace}}::Channels, :channels
get "/api/v1/channels/:ucid/search", {{namespace}}::Channels, :search
{% for route in {"videos", "latest", "playlists", "community", "search"} %}
get "/api/v1/channels/#{{{route}}}/:ucid", {{namespace}}::Channels, :{{route}}
get "/api/v1/channels/:ucid/#{{{route}}}", {{namespace}}::Channels, :{{route}}
{% end %}
# Posts # Posts
get "/api/v1/post/:id", {{namespace}}::Channels, :post get "/api/v1/post/:id", {{namespace}}::Channels, :post
@ -270,6 +272,11 @@ module Invidious::Routing
# Authenticated # Authenticated
# The notification APIs cannot be extracted yet! They require the *local* notifications constant defined in invidious.cr
#
# Invidious::Routing.get "/api/v1/auth/notifications", {{namespace}}::Authenticated, :notifications
# Invidious::Routing.post "/api/v1/auth/notifications", {{namespace}}::Authenticated, :notifications
get "/api/v1/auth/preferences", {{namespace}}::Authenticated, :get_preferences get "/api/v1/auth/preferences", {{namespace}}::Authenticated, :get_preferences
post "/api/v1/auth/preferences", {{namespace}}::Authenticated, :set_preferences post "/api/v1/auth/preferences", {{namespace}}::Authenticated, :set_preferences

View file

@ -57,6 +57,10 @@ struct Preferences
property volume : Int32 = CONFIG.default_user_preferences.volume property volume : Int32 = CONFIG.default_user_preferences.volume
property save_player_pos : Bool = CONFIG.default_user_preferences.save_player_pos property save_player_pos : Bool = CONFIG.default_user_preferences.save_player_pos
@[YAML::Field(converter: Preferences::ProcessString)]
property po_token : String = ""
property visitor_data : String = ""
module BoolToString module BoolToString
def self.to_json(value : String, json : JSON::Builder) def self.to_json(value : String, json : JSON::Builder)
json.string value json.string value

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
@ -192,10 +192,6 @@ 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)
@ -298,7 +294,7 @@ struct Video
predicate_bool upcoming, isUpcoming predicate_bool upcoming, isUpcoming
end end
def get_video(id, refresh = true, region = nil, force_refresh = false) def get_video(id, refresh = true, region = nil, force_refresh = false, po_token = "", visitor_data = "")
if (video = Invidious::Database::Videos.select(id)) && !region if (video = Invidious::Database::Videos.select(id)) && !region
# If record was last updated over 10 minutes ago, or video has since premiered, # If record was last updated over 10 minutes ago, or video has since premiered,
# refresh (expire param in response lasts for 6 hours) # refresh (expire param in response lasts for 6 hours)
@ -308,7 +304,7 @@ def get_video(id, refresh = true, region = nil, force_refresh = false)
force_refresh || force_refresh ||
video.schema_version != Video::SCHEMA_VERSION # cache control video.schema_version != Video::SCHEMA_VERSION # cache control
begin begin
video = fetch_video(id, region) video = fetch_video(id, region, po_token, visitor_data)
Invidious::Database::Videos.insert(video) Invidious::Database::Videos.insert(video)
rescue ex rescue ex
Invidious::Database::Videos.delete(id) Invidious::Database::Videos.delete(id)
@ -316,7 +312,7 @@ def get_video(id, refresh = true, region = nil, force_refresh = false)
end end
end end
else else
video = fetch_video(id, region) video = fetch_video(id, region, po_token, visitor_data)
Invidious::Database::Videos.insert(video) if !region Invidious::Database::Videos.insert(video) if !region
end end
@ -324,11 +320,11 @@ def get_video(id, refresh = true, region = nil, force_refresh = false)
rescue DB::Error rescue DB::Error
# Avoid common `DB::PoolRetryAttemptsExceeded` error and friends # Avoid common `DB::PoolRetryAttemptsExceeded` error and friends
# Note: All DB errors inherit from `DB::Error` # Note: All DB errors inherit from `DB::Error`
return fetch_video(id, region) return fetch_video(id, region, po_token, visitor_data)
end end
def fetch_video(id, region) def fetch_video(id, region, po_token, visitor_data)
info = extract_video_info(video_id: id) info = extract_video_info(video_id: id, user_po_token: po_token, user_visitor_data: visitor_data)
if reason = info["reason"]? if reason = info["reason"]?
if reason == "Video unavailable" if reason == "Video unavailable"

View file

@ -50,12 +50,17 @@ def parse_related_video(related : JSON::Any) : Hash(String, JSON::Any)?
} }
end end
def extract_video_info(video_id : String) def extract_video_info(video_id : String, user_po_token, user_visitor_data)
# 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 = (user_po_token if !user_po_token.empty?) || redis_po_token || CONFIG.po_token
visitor_data = (user_visitor_data if !user_visitor_data.empty?) || 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, po_token: po_token, visitor_data: visitor_data)
playability_status = player_response.dig?("playabilityStatus", "status").try &.as_s playability_status = player_response.dig?("playabilityStatus", "status").try &.as_s
@ -100,32 +105,39 @@ 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? new_player_response = nil
new_player_response = nil
# Don't use Android test suite client if po_token is passed because po_token doesn't # Don't use Android client if po_token is passed because po_token doesn't
# work for Android test suite client. # work for Android client.
if reason.nil? && CONFIG.po_token.nil? if reason.nil? && 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, po_token, visitor_data)
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
{"captions", "playabilityStatus", "playerConfig", "storyboards", "invidiousCompanion"}.each do |f| # Last hope
# Only trigger if reason found and po_token or didn't work wth Android client.
# TvHtml5ScreenEmbed now requires sig helper for it to work but po_token is not required
# if the IP address is not blocked.
if po_token.nil? && reason || po_token.nil? && new_player_response.nil?
client_config.client_type = YoutubeAPI::ClientType::TvHtml5ScreenEmbed
new_player_response = try_fetch_streaming_data(video_id, client_config, po_token, visitor_data)
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
{"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
@ -133,7 +145,7 @@ def extract_video_info(video_id : String)
if streaming_data = player_response["streamingData"]? if streaming_data = player_response["streamingData"]?
%w[formats adaptiveFormats].each do |key| %w[formats adaptiveFormats].each do |key|
streaming_data.as_h[key]?.try &.as_a.each do |format| streaming_data.as_h[key]?.try &.as_a.each do |format|
format.as_h["url"] = JSON::Any.new(convert_url(format)) format.as_h["url"] = JSON::Any.new(convert_url(format, po_token))
end end
end end
@ -146,9 +158,9 @@ def extract_video_info(video_id : String)
return params return params
end end
def try_fetch_streaming_data(id : String, client_config : YoutubeAPI::ClientConfig) : Hash(String, JSON::Any)? def try_fetch_streaming_data(id : String, client_config : YoutubeAPI::ClientConfig, po_token, visitor_data) : Hash(String, JSON::Any)?
LOGGER.debug("try_fetch_streaming_data: [#{id}] Using #{client_config.client_type} client.") LOGGER.debug("try_fetch_streaming_data: [#{id}] Using #{client_config.client_type} client.")
response = YoutubeAPI.player(video_id: id, params: "2AMB", client_config: client_config) response = YoutubeAPI.player(video_id: id, params: "2AMB", client_config: client_config, po_token: po_token, visitor_data: visitor_data)
playability_status = response["playabilityStatus"]["status"] playability_status = response["playabilityStatus"]["status"]
LOGGER.debug("try_fetch_streaming_data: [#{id}] Got playabilityStatus == #{playability_status}.") LOGGER.debug("try_fetch_streaming_data: [#{id}] Got playabilityStatus == #{playability_status}.")
@ -217,17 +229,8 @@ def parse_video_info(video_id : String, player_response : Hash(String, JSON::Any
premiere_timestamp = microformat.dig?("liveBroadcastDetails", "startTimestamp") premiere_timestamp = microformat.dig?("liveBroadcastDetails", "startTimestamp")
.try { |t| Time.parse_rfc3339(t.as_s) } .try { |t| Time.parse_rfc3339(t.as_s) }
premiere_timestamp ||= player_response.dig?(
"playabilityStatus", "liveStreamability",
"liveStreamabilityRenderer", "offlineSlate",
"liveStreamOfflineSlateRenderer", "scheduledStartTime"
)
.try &.as_s.to_i64
.try { |t| Time.unix(t) }
live_now = microformat.dig?("liveBroadcastDetails", "isLiveNow") live_now = microformat.dig?("liveBroadcastDetails", "isLiveNow")
.try &.as_bool .try &.as_bool || false
live_now ||= video_details.dig?("isLive").try &.as_bool || false
post_live_dvr = video_details.dig?("isPostLiveDvr") post_live_dvr = video_details.dig?("isPostLiveDvr")
.try &.as_bool || false .try &.as_bool || false
@ -457,7 +460,7 @@ def parse_video_info(video_id : String, player_response : Hash(String, JSON::Any
return params return params
end end
private def convert_url(fmt) private def convert_url(fmt, po_token)
if cfr = fmt["signatureCipher"]?.try { |json| HTTP::Params.parse(json.as_s) } if cfr = fmt["signatureCipher"]?.try { |json| HTTP::Params.parse(json.as_s) }
sp = cfr["sp"] sp = cfr["sp"]
url = URI.parse(cfr["url"]) url = URI.parse(cfr["url"])
@ -475,7 +478,9 @@ private def convert_url(fmt)
n = DECRYPT_FUNCTION.try &.decrypt_nsig(params["n"]) n = DECRYPT_FUNCTION.try &.decrypt_nsig(params["n"])
params["n"] = n if n params["n"] = n if n
if token = CONFIG.po_token if !po_token.nil?
params["pot"] = po_token
elsif token = CONFIG.po_token
params["pot"] = token params["pot"] = token
end end

View file

@ -2,7 +2,6 @@
ucid = channel.ucid ucid = channel.ucid
author = HTML.escape(channel.author) author = HTML.escape(channel.author)
channel_profile_pic = URI.parse(channel.author_thumbnail).request_target channel_profile_pic = URI.parse(channel.author_thumbnail).request_target
host = env.request.headers["Host"]
relative_url = relative_url =
case selected_tab case selected_tab
@ -29,15 +28,15 @@
<%- if selected_tab.videos? -%> <%- if selected_tab.videos? -%>
<meta name="description" content="<%= channel.description %>"> <meta name="description" content="<%= channel.description %>">
<meta property="og:site_name" content="Invidious"> <meta property="og:site_name" content="Invidious">
<meta property="og:url" content="<%= host %>/channel/<%= ucid %>"> <meta property="og:url" content="<%= HOST_URL %>/channel/<%= ucid %>">
<meta property="og:title" content="<%= author %>"> <meta property="og:title" content="<%= author %>">
<meta property="og:image" content="<%= host %>/ggpht<%= channel_profile_pic %>"> <meta property="og:image" content="<%= HOST_URL %>/ggpht<%= channel_profile_pic %>">
<meta property="og:description" content="<%= channel.description %>"> <meta property="og:description" content="<%= channel.description %>">
<meta name="twitter:card" content="summary"> <meta name="twitter:card" content="summary">
<meta name="twitter:url" content="<%= host %>/channel/<%= ucid %>"> <meta name="twitter:url" content="<%= HOST_URL %>/channel/<%= ucid %>">
<meta name="twitter:title" content="<%= author %>"> <meta name="twitter:title" content="<%= author %>">
<meta name="twitter:description" content="<%= channel.description %>"> <meta name="twitter:description" content="<%= channel.description %>">
<meta name="twitter:image" content="<%= host %>/ggpht<%= channel_profile_pic %>"> <meta name="twitter:image" content="<%= HOST_URL %>/ggpht<%= channel_profile_pic %>">
<link rel="alternate" type="application/rss+xml" title="RSS" href="/feed/channel/<%= ucid %>" /> <link rel="alternate" type="application/rss+xml" title="RSS" href="/feed/channel/<%= ucid %>" />
<%- end -%> <%- end -%>

View file

@ -103,15 +103,12 @@
if item.is_a?(PlaylistVideo) if item.is_a?(PlaylistVideo)
link_url = "/watch?v=#{item.id}&list=#{item.plid}&index=#{item.index}" link_url = "/watch?v=#{item.id}&list=#{item.plid}&index=#{item.index}"
endpoint_params = "?v=#{item.id}&list=#{item.plid}" endpoint_params = "?v=#{item.id}&list=#{item.plid}"
embed_id = item.id
elsif item.is_a?(MixVideo) elsif item.is_a?(MixVideo)
link_url = "/watch?v=#{item.id}&list=#{item.rdid}" link_url = "/watch?v=#{item.id}&list=#{item.rdid}"
endpoint_params = "?v=#{item.id}&list=#{item.rdid}" endpoint_params = "?v=#{item.id}&list=#{item.rdid}"
embed_id = item.id
else else
link_url = "/watch?v=#{item.id}" link_url = "/watch?v=#{item.id}"
endpoint_params = "?v=#{item.id}" endpoint_params = "?v=#{item.id}"
embed_id = item.id
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 = video.invidious_companion["baseUrl"].as_s + src_url +
"&check=#{invidious_companion_encrypt(video.id)}" if (CONFIG.invidious_companion.present?)
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 = 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 %> <% 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 = video.invidious_companion["baseUrl"].as_s + src_url +
"&check=#{invidious_companion_encrypt(video.id)}" if (CONFIG.invidious_companion.present?)
quality = fmt["quality"] quality = fmt["quality"]
mimetype = HTML.escape(fmt["mimeType"].as_s) mimetype = HTML.escape(fmt["mimeType"].as_s)

View file

@ -1,8 +1,5 @@
<div class="flex-right flexible"> <div class="flex-right flexible">
<div class="icon-buttons"> <div class="icon-buttons">
<a title="<%=translate(locale, "videoinfo_youTube_embed_link")%>" rel="noreferrer noopener" href="https://www.youtube.com/embed/<%=embed_id%>">
<i class="icon ion-md-open"></i>
</a>
<a title="<%=translate(locale, "videoinfo_watch_on_youTube")%>" rel="noreferrer noopener" href="https://www.youtube.com/watch<%=endpoint_params%>"> <a title="<%=translate(locale, "videoinfo_watch_on_youTube")%>" rel="noreferrer noopener" href="https://www.youtube.com/watch<%=endpoint_params%>">
<i class="icon ion-logo-youtube"></i> <i class="icon ion-logo-youtube"></i>
</a> </a>

View file

@ -1,8 +1,7 @@
<% <%
locale = env.get("preferences").as(Preferences).locale locale = env.get("preferences").as(Preferences).locale
dark_mode = env.get("preferences").as(Preferences).dark_mode dark_mode = env.get("preferences").as(Preferences).dark_mode
current_backend = env.request.cookies[CONFIG.server_id_cookie_name]?.try &.value || env.request.headers["Host"] current_backend = env.request.cookies[CONFIG.server_id_cookie_name]?.try &.value
current_external_videoplayback_proxy = Invidious::HttpServer::Utils.get_external_proxy()
%> %>
<!DOCTYPE html> <!DOCTYPE html>
<html lang="<%= locale %>"> <html lang="<%= locale %>">
@ -107,7 +106,6 @@
</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 | %>
@ -130,7 +128,6 @@
<% end %> <% end %>
</div> </div>
<% end %> <% end %>
<% end %>
<% if CONFIG.banner %> <% if CONFIG.banner %>
<div class="h-box"> <div class="h-box">
@ -314,9 +311,6 @@
<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") %>

View file

@ -126,6 +126,24 @@
<input name="save_player_pos" id="save_player_pos" type="checkbox" <% if preferences.save_player_pos %>checked<% end %>> <input name="save_player_pos" id="save_player_pos" type="checkbox" <% if preferences.save_player_pos %>checked<% end %>>
</div> </div>
<% if !CONFIG.ignore_user_tokens %>
<div class="pure-control-group">
<label for="po_token"><%= translate(locale, "preferences_po_token") %></label>
<input name="po_token" id="po_token" type="text" value="<%= preferences.po_token %>">
</div>
<div class="pure-control-group">
<label for="visitor_data"><%= translate(locale, "preferences_visitor_data") %></label>
<input name="visitor_data" id="visitor_data" type="text" value="<%= preferences.visitor_data %>">
</div>
<% if env.get?("user") %>
<div class="pure-control-group">
<a href="/generate_tokens?referer=<%= URI.encode_www_form(referer) %>"><%= translate(locale, "Generate po_token and visitor_data for your account") %></a>
</div>
<% end %>
<% end %>
<legend><%= translate(locale, "preferences_category_visual") %></legend> <legend><%= translate(locale, "preferences_category_visual") %></legend>
<div class="pure-control-group"> <div class="pure-control-group">

View file

@ -0,0 +1,15 @@
<% content_for "header" do %>
<title><%= translate(locale, "Invidious token generator") %> - 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">
<p>po_token and visitor_data successfully generated!</p>
<p>po_token: <%= po_token %></p>
<p>visitor_data: <%= visitor_data %></p>
</div>
</div>
<div class="pure-u-1 pure-u-lg-1-5"></div>
</div>

View file

@ -1,7 +1,6 @@
<% ucid = video.ucid %> <% ucid = video.ucid %>
<% title = HTML.escape(video.title) %> <% title = HTML.escape(video.title) %>
<% author = HTML.escape(video.author) %> <% author = HTML.escape(video.author) %>
<% host = env.request.headers["Host"] %>
<% content_for "header" do %> <% content_for "header" do %>
@ -9,9 +8,9 @@
<meta name="description" content="<%= HTML.escape(video.short_description) %>"> <meta name="description" content="<%= HTML.escape(video.short_description) %>">
<meta name="keywords" content="<%= video.keywords.join(",") %>"> <meta name="keywords" content="<%= video.keywords.join(",") %>">
<meta property="og:site_name" content="<%= author %> | Invidious"> <meta property="og:site_name" content="<%= author %> | Invidious">
<meta property="og:url" content="<%= host %>/watch?v=<%= video.id %>"> <meta property="og:url" content="<%= HOST_URL %>/watch?v=<%= video.id %>">
<meta property="og:title" content="<%= title %>"> <meta property="og:title" content="<%= title %>">
<meta property="og:image" content="<%= host %>/vi/<%= video.id %>/maxres.jpg"> <meta property="og:image" content="<%= HOST_URL %>/vi/<%= video.id %>/maxres.jpg">
<meta property="og:description" content="<%= HTML.escape(video.short_description) %>"> <meta property="og:description" content="<%= HTML.escape(video.short_description) %>">
<meta property="og:type" content="video.other"> <meta property="og:type" content="video.other">
<!-- This shouldn't be empty, ever. --> <!-- This shouldn't be empty, ever. -->
@ -22,11 +21,11 @@
<meta property="og:video:width" content="640"> <meta property="og:video:width" content="640">
<meta property="og:video:height" content="360"> <meta property="og:video:height" content="360">
<meta name="twitter:card" content="player"> <meta name="twitter:card" content="player">
<meta name="twitter:url" content="<%= host %>/watch?v=<%= video.id %>"> <meta name="twitter:url" content="<%= HOST_URL %>/watch?v=<%= video.id %>">
<meta name="twitter:title" content="<%= title %>"> <meta name="twitter:title" content="<%= title %>">
<meta name="twitter:description" content="<%= HTML.escape(video.short_description) %>"> <meta name="twitter:description" content="<%= HTML.escape(video.short_description) %>">
<meta name="twitter:image" content="<%= host %>/vi/<%= video.id %>/maxres.jpg"> <meta name="twitter:image" content="<%= HOST_URL %>/vi/<%= video.id %>/maxres.jpg">
<meta name="twitter:player" content="<%= host %>/embed/<%= video.id %>"> <meta name="twitter:player" content="<%= HOST_URL %>/embed/<%= video.id %>">
<meta name="twitter:player:width" content="1280"> <meta name="twitter:player:width" content="1280">
<meta name="twitter:player:height" content="720"> <meta name="twitter:player:height" content="720">
<link rel="alternate" href="https://www.youtube.com/watch?v=<%= video.id %>"> <link rel="alternate" href="https://www.youtube.com/watch?v=<%= video.id %>">

View file

@ -23,7 +23,6 @@ struct YoutubeConnectionPool
rescue ex rescue ex
conn.close conn.close
conn = make_client(url, force_resolve: true) conn = make_client(url, force_resolve: true)
response = yield conn response = yield conn
ensure ensure
pool.release(conn) pool.release(conn)
@ -41,29 +40,17 @@ struct YoutubeConnectionPool
) )
DB::Pool(HTTP::Client).new(options) do DB::Pool(HTTP::Client).new(options) do
next make_client(url, force_resolve: true) conn = HTTP::Client.new(url)
conn.family = CONFIG.force_resolve
conn.family = Socket::Family::INET if conn.family == Socket::Family::UNSPEC
conn.before_request { |r| add_yt_headers(r) } if url.host == "www.youtube.com"
conn
end end
end end
end end
def add_yt_headers(request) def make_client(url : URI, region = nil, force_resolve : Bool = false, force_youtube_headers : Bool = false)
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["Accept-Charset"] ||= "ISO-8859-1,utf-8;q=0.7,*;q=0.7"
request.headers["Accept"] ||= "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"
request.headers["Accept-Language"] ||= "en-us,en;q=0.5"
# Preserve original cookies and add new YT consent cookie for EU servers
request.headers["Cookie"] = "#{request.headers["cookie"]?}; CONSENT=PENDING+#{Random.rand(100..999)}"
if !CONFIG.cookies.empty?
request.headers["Cookie"] = "#{(CONFIG.cookies.map { |c| "#{c.name}=#{c.value}" }).join("; ")}; #{request.headers["cookie"]?}"
end
end
def make_client(url : URI, region = nil, force_resolve : Bool = false, force_youtube_headers : Bool = false, use_http_proxy : Bool = true)
client = HTTP::Client.new(url) client = HTTP::Client.new(url)
client.proxy = make_configured_http_proxy_client() if CONFIG.http_proxy && use_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
@ -78,8 +65,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, &block)
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

@ -3,6 +3,8 @@
# #
module YoutubeAPI module YoutubeAPI
@@visitor_data : String = ""
extend self extend self
# For Android versions, see https://en.wikipedia.org/wiki/Android_version_history # For Android versions, see https://en.wikipedia.org/wiki/Android_version_history
@ -320,7 +322,9 @@ module YoutubeAPI
client_context["client"]["platform"] = platform client_context["client"]["platform"] = platform
end end
if CONFIG.visitor_data.is_a?(String) if !@@visitor_data.empty?
client_context["client"]["visitorData"] = @@visitor_data
elsif CONFIG.visitor_data.is_a?(String)
client_context["client"]["visitorData"] = CONFIG.visitor_data.as(String) client_context["client"]["visitorData"] = CONFIG.visitor_data.as(String)
end end
@ -456,7 +460,12 @@ module YoutubeAPI
*, # Force the following parameters to be passed by name *, # Force the following parameters to be passed by name
params : String, params : String,
client_config : ClientConfig | Nil = nil, client_config : ClientConfig | Nil = nil,
po_token : String | Nil,
visitor_data : String | Nil,
) )
if visitor_data
@@visitor_data = visitor_data
end
# Playback context, separate because it can be different between clients # Playback context, separate because it can be different between clients
playback_ctx = { playback_ctx = {
"html5Preference" => "HTML5_PREF_WANTS", "html5Preference" => "HTML5_PREF_WANTS",
@ -482,7 +491,7 @@ module YoutubeAPI
"contentPlaybackContext" => playback_ctx, "contentPlaybackContext" => playback_ctx,
}, },
"serviceIntegrityDimensions" => { "serviceIntegrityDimensions" => {
"poToken" => CONFIG.po_token, "poToken" => po_token || CONFIG.po_token,
}, },
} }
@ -491,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
#################################################################### ####################################################################
@ -620,7 +625,9 @@ module YoutubeAPI
headers["User-Agent"] = user_agent headers["User-Agent"] = user_agent
end end
if CONFIG.visitor_data.is_a?(String) if !@@visitor_data.empty?
headers["X-Goog-Visitor-Id"] = @@visitor_data
elsif CONFIG.visitor_data.is_a?(String)
headers["X-Goog-Visitor-Id"] = CONFIG.visitor_data.as(String) headers["X-Goog-Visitor-Id"] = CONFIG.visitor_data.as(String)
end end
@ -661,51 +668,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
invidious_companion = CONFIG.invidious_companion.sample
response = make_client(invidious_companion.private_url, use_http_proxy: false,
&.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)
# #
@ -724,7 +686,7 @@ module YoutubeAPI
# Multiple encodings can be combined, and are listed in the order # Multiple encodings can be combined, and are listed in the order
# in which they were applied. E.g: "deflate, gzip" means that the # in which they were applied. E.g: "deflate, gzip" means that the
# content must be first "gunzipped", then "defated". # content must be first "gunzipped", then "defated".
encodings.split(',').reverse!.each do |enc| encodings.split(',').reverse.each do |enc|
case enc.strip(' ') case enc.strip(' ')
when "gzip" when "gzip"
body_io = Compress::Gzip::Reader.new(body_io, sync_close: true) body_io = Compress::Gzip::Reader.new(body_io, sync_close: true)