From c250b9c0b1f947c822a4e0905975eb600352d7d3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 10 Nov 2025 20:23:05 +0000 Subject: [PATCH 01/10] Bump crystal-lang/install-crystal from 1.8.3 to 1.9.1 Bumps [crystal-lang/install-crystal](https://github.com/crystal-lang/install-crystal) from 1.8.3 to 1.9.1. - [Release notes](https://github.com/crystal-lang/install-crystal/releases) - [Commits](https://github.com/crystal-lang/install-crystal/compare/v1.8.3...v1.9.1) --- updated-dependencies: - dependency-name: crystal-lang/install-crystal dependency-version: 1.9.1 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a3c8f4dd..94bcbcfb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -58,7 +58,7 @@ jobs: shell: bash - name: Install Crystal - uses: crystal-lang/install-crystal@v1.8.3 + uses: crystal-lang/install-crystal@v1.9.1 with: crystal: ${{ matrix.crystal }} @@ -134,7 +134,7 @@ jobs: - name: Install Crystal id: lint_step_install_crystal - uses: crystal-lang/install-crystal@v1.8.3 + uses: crystal-lang/install-crystal@v1.9.1 with: crystal: latest From bb9c4a01a192441d6d0956a36a78d2c1baf83d0a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 24 Nov 2025 20:39:14 +0000 Subject: [PATCH 02/10] Bump actions/checkout from 5 to 6 Bumps [actions/checkout](https://github.com/actions/checkout) from 5 to 6. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v5...v6) --- updated-dependencies: - dependency-name: actions/checkout dependency-version: '6' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/build-nightly-container.yml | 2 +- .github/workflows/build-stable-container.yml | 2 +- .github/workflows/ci.yml | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/build-nightly-container.yml b/.github/workflows/build-nightly-container.yml index ba005d9a..44be0bae 100644 --- a/.github/workflows/build-nightly-container.yml +++ b/.github/workflows/build-nightly-container.yml @@ -36,7 +36,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v5 + uses: actions/checkout@v6 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 diff --git a/.github/workflows/build-stable-container.yml b/.github/workflows/build-stable-container.yml index 1423bb69..e119880d 100644 --- a/.github/workflows/build-stable-container.yml +++ b/.github/workflows/build-stable-container.yml @@ -27,7 +27,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v5 + uses: actions/checkout@v6 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 94bcbcfb..ff82a5bd 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -48,7 +48,7 @@ jobs: stable: false steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 with: submodules: true @@ -96,7 +96,7 @@ jobs: runs-on: ${{ matrix.os }} steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - name: Use ARM64 Dockerfile if ARM64 if: ${{ matrix.name == 'ARM64' }} @@ -128,7 +128,7 @@ jobs: continue-on-error: true steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 with: submodules: true From b2ecd8abc3c345642999b7d92b54a6cf241ffdac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89milien=20=28perso=29?= <4016501+unixfox@users.noreply.github.com> Date: Tue, 25 Nov 2025 14:32:15 +0100 Subject: [PATCH 03/10] chore: update healthcheck for /api/v1/stats since /api/v1/trending doesn't work anymore --- docker-compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker-compose.yml b/docker-compose.yml index 0de51feb..cb53bdd6 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -36,7 +36,7 @@ services: # statistics_enabled: false hmac_key: "CHANGE_ME!!" healthcheck: - test: wget -nv --tries=1 --spider http://127.0.0.1:3000/api/v1/trending || exit 1 + test: wget -nv --tries=1 --spider http://127.0.0.1:3000/api/v1/stats || exit 1 interval: 30s timeout: 5s retries: 2 From 35d1d499bc42a9b141b3dc92c4a5827b5f21a3ff Mon Sep 17 00:00:00 2001 From: Fijxu Date: Tue, 2 Dec 2025 18:20:15 -0300 Subject: [PATCH 04/10] chore: Store `preferences` in a variable when reused and rename `prefs` to `preferences` (#5450) A little code cleanup on places where `preferences` is used more than one time and rename `prefs` to `preferences` to maintain consistency. --- src/invidious/frontend/misc.cr | 4 ++-- src/invidious/routes/channels.cr | 6 +++--- src/invidious/routes/embed.cr | 5 ++--- src/invidious/routes/feeds.cr | 5 +++-- src/invidious/routes/playlists.cr | 6 +++--- src/invidious/routes/preferences.cr | 5 ++--- src/invidious/routes/search.cr | 6 +++--- src/invidious/routes/watch.cr | 5 ++--- src/invidious/views/embed.ecr | 2 +- src/invidious/views/post.ecr | 2 +- src/invidious/views/template.ecr | 5 +++-- 11 files changed, 25 insertions(+), 26 deletions(-) diff --git a/src/invidious/frontend/misc.cr b/src/invidious/frontend/misc.cr index 7a6cf79d..9c30724a 100644 --- a/src/invidious/frontend/misc.cr +++ b/src/invidious/frontend/misc.cr @@ -2,9 +2,9 @@ module Invidious::Frontend::Misc extend self def redirect_url(env : HTTP::Server::Context) - prefs = env.get("preferences").as(Preferences) + preferences = env.get("preferences").as(Preferences) - if prefs.automatic_instance_redirect + if preferences.automatic_instance_redirect current_page = env.get?("current_page").as(String) return "/redirect?referer=#{current_page}" else diff --git a/src/invidious/routes/channels.cr b/src/invidious/routes/channels.cr index 6d2b4465..f785de18 100644 --- a/src/invidious/routes/channels.cr +++ b/src/invidious/routes/channels.cr @@ -264,11 +264,11 @@ module Invidious::Routes::Channels id = env.params.url["id"] ucid = env.params.query["ucid"]? - prefs = env.get("preferences").as(Preferences) + preferences = env.get("preferences").as(Preferences) - locale = prefs.locale + locale = preferences.locale - thin_mode = env.params.query["thin_mode"]? || prefs.thin_mode + thin_mode = env.params.query["thin_mode"]? || preferences.thin_mode thin_mode = thin_mode == "true" nojs = env.params.query["nojs"]? diff --git a/src/invidious/routes/embed.cr b/src/invidious/routes/embed.cr index 6b0887d5..d0a3b5c1 100644 --- a/src/invidious/routes/embed.cr +++ b/src/invidious/routes/embed.cr @@ -33,7 +33,8 @@ module Invidious::Routes::Embed end def self.show(env) - locale = env.get("preferences").as(Preferences).locale + preferences = env.get("preferences").as(Preferences) + locale = preferences.locale id = env.params.url["id"] plid = env.params.query["list"]?.try &.gsub(/[^a-zA-Z0-9_-]/, "") @@ -45,8 +46,6 @@ module Invidious::Routes::Embed env.params.query.delete("playlist") end - preferences = env.get("preferences").as(Preferences) - if id.includes?("%20") || id.includes?("+") || env.params.query.to_s.includes?("%20") || env.params.query.to_s.includes?("+") id = env.params.url["id"].gsub("%20", "").delete("+") diff --git a/src/invidious/routes/feeds.cr b/src/invidious/routes/feeds.cr index 070c96eb..ce173760 100644 --- a/src/invidious/routes/feeds.cr +++ b/src/invidious/routes/feeds.cr @@ -43,13 +43,14 @@ module Invidious::Routes::Feeds end def self.trending(env) - locale = env.get("preferences").as(Preferences).locale + preferences = env.get("preferences").as(Preferences) + locale = preferences.locale trending_type = env.params.query["type"]? trending_type ||= "Default" region = env.params.query["region"]? - region ||= env.get("preferences").as(Preferences).region + region ||= preferences.region begin trending, plid = fetch_trending(trending_type, region, locale) diff --git a/src/invidious/routes/playlists.cr b/src/invidious/routes/playlists.cr index f2213da4..56e529b2 100644 --- a/src/invidious/routes/playlists.cr +++ b/src/invidious/routes/playlists.cr @@ -225,10 +225,10 @@ module Invidious::Routes::Playlists end def self.add_playlist_items_page(env) - prefs = env.get("preferences").as(Preferences) - locale = prefs.locale + preferences = env.get("preferences").as(Preferences) + locale = preferences.locale - region = env.params.query["region"]? || prefs.region + region = env.params.query["region"]? || preferences.region user = env.get? "user" sid = env.get? "sid" diff --git a/src/invidious/routes/preferences.cr b/src/invidious/routes/preferences.cr index 9936e523..d9fad1b1 100644 --- a/src/invidious/routes/preferences.cr +++ b/src/invidious/routes/preferences.cr @@ -2,12 +2,11 @@ module Invidious::Routes::PreferencesRoute def self.show(env) - locale = env.get("preferences").as(Preferences).locale + preferences = env.get("preferences").as(Preferences) + locale = preferences.locale referer = get_referer(env) - preferences = env.get("preferences").as(Preferences) - templated "user/preferences" end diff --git a/src/invidious/routes/search.cr b/src/invidious/routes/search.cr index b195c7b3..11e6f171 100644 --- a/src/invidious/routes/search.cr +++ b/src/invidious/routes/search.cr @@ -37,10 +37,10 @@ module Invidious::Routes::Search end def self.search(env) - prefs = env.get("preferences").as(Preferences) - locale = prefs.locale + preferences = env.get("preferences").as(Preferences) + locale = preferences.locale - region = env.params.query["region"]? || prefs.region + region = env.params.query["region"]? || preferences.region query = Invidious::Search::Query.new(env.params.query, :regular, region) diff --git a/src/invidious/routes/watch.cr b/src/invidious/routes/watch.cr index 8a4fa246..4c181503 100644 --- a/src/invidious/routes/watch.cr +++ b/src/invidious/routes/watch.cr @@ -2,7 +2,8 @@ module Invidious::Routes::Watch def self.handle(env) - locale = env.get("preferences").as(Preferences).locale + preferences = env.get("preferences").as(Preferences) + locale = preferences.locale region = env.params.query["region"]? if env.params.query.to_s.includes?("%20") || env.params.query.to_s.includes?("+") @@ -38,8 +39,6 @@ module Invidious::Routes::Watch nojs ||= "0" nojs = nojs == "1" - preferences = env.get("preferences").as(Preferences) - user = env.get?("user").try &.as(User) if user subscriptions = user.subscriptions diff --git a/src/invidious/views/embed.ecr b/src/invidious/views/embed.ecr index 1bf5cc3e..5551cd0a 100644 --- a/src/invidious/views/embed.ecr +++ b/src/invidious/views/embed.ecr @@ -1,5 +1,5 @@ -"> + diff --git a/src/invidious/views/post.ecr b/src/invidious/views/post.ecr index fb03a44c..f644d634 100644 --- a/src/invidious/views/post.ecr +++ b/src/invidious/views/post.ecr @@ -38,7 +38,7 @@ "params" => { "comments": ["youtube"] }, - "preferences" => prefs, + "preferences" => preferences, "base_url" => "/api/v1/post/#{URI.encode_www_form(id)}/comments", "ucid" => ucid }.to_pretty_json diff --git a/src/invidious/views/template.ecr b/src/invidious/views/template.ecr index 9904b4fc..9bf33918 100644 --- a/src/invidious/views/template.ecr +++ b/src/invidious/views/template.ecr @@ -1,6 +1,7 @@ <% - locale = env.get("preferences").as(Preferences).locale - dark_mode = env.get("preferences").as(Preferences).dark_mode + preferences = env.get("preferences").as(Preferences) + locale = preferences.locale + dark_mode = preferences.dark_mode %> From 48765f759d3f8998fca0b5759897688cb0371f90 Mon Sep 17 00:00:00 2001 From: Fijxu Date: Thu, 4 Dec 2025 11:59:55 -0300 Subject: [PATCH 05/10] chore: Update shard.yml to use SPDX license identifier (#5552) --- shard.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shard.yml b/shard.yml index 4dc8aa02..bc6c4bf4 100644 --- a/shard.yml +++ b/shard.yml @@ -38,7 +38,7 @@ development_dependencies: crystal: ">= 1.10.0, < 2.0.0" -license: AGPLv3 +license: AGPL-3.0-only repository: https://github.com/iv-org/invidious homepage: https://invidious.io From 46a9c933be44c4153b8e41155dfbdb334be87200 Mon Sep 17 00:00:00 2001 From: Fijxu Date: Thu, 4 Dec 2025 12:00:58 -0300 Subject: [PATCH 06/10] Fix community posts when there is a unavailable video in a post (#5549) Posts with a video that has been removed returned `ProblematicTimelineItem` type which was not taken in account for community posts. Now community posts with a broken video will not display an embedded video. --- src/invidious/channels/community.cr | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/invidious/channels/community.cr b/src/invidious/channels/community.cr index 43843b11..4256230c 100644 --- a/src/invidious/channels/community.cr +++ b/src/invidious/channels/community.cr @@ -143,7 +143,7 @@ def extract_channel_community(items, *, ucid, locale, format, thin_mode, is_sing case attachment.as_h when .has_key?("videoRenderer") parse_item(attachment) - .as(SearchVideo) + .as(SearchVideo | ProblematicTimelineItem) .to_json(locale, json) when .has_key?("backstageImageRenderer") json.object do From 07f3894a71f565b99477e0e8d817b2259d61ddff Mon Sep 17 00:00:00 2001 From: Fijxu Date: Sat, 6 Dec 2025 16:50:59 -0300 Subject: [PATCH 07/10] Remove signature helper completely from Invidious (#5550) * Remove signature helper completely from Invidious The official way to reproduce video with Invidious now is by using Invidious Companion which uses Youtube.JS with a Javascript Interpreter that can successfully decrypt youtube video URLs. Sig helper has not been used for a long time, is beyond broken and no one has plans to fix it and maintain it. * Remove DECRYPT_FUNCTION and shrink player function * remove `sp = cfr[sp]` * Improve message --- config/config.example.yml | 27 -- src/invidious.cr | 9 - src/invidious/config.cr | 16 +- src/invidious/helpers/sig_helper.cr | 349 ------------------------ src/invidious/helpers/signatures.cr | 53 ---- src/invidious/videos.cr | 8 + src/invidious/videos/parser.cr | 53 +--- src/invidious/yt_backend/youtube_api.cr | 66 +---- 8 files changed, 23 insertions(+), 558 deletions(-) delete mode 100644 src/invidious/helpers/sig_helper.cr delete mode 100644 src/invidious/helpers/signatures.cr diff --git a/config/config.example.yml b/config/config.example.yml index 2b99345b..eedd9539 100644 --- a/config/config.example.yml +++ b/config/config.example.yml @@ -40,20 +40,6 @@ db: ## #check_tables: false - -## -## Path to an external signature resolver, used to emulate -## the Youtube client's Javascript. If no such server is -## available, some videos will not be playable. -## -## When this setting is commented out, no external -## resolver will be used. -## -## Accepted values: a path to a UNIX socket or ":" -## Default: -## -#signature_server: - ## ## Invidious companion is an external program ## for loading the video streams from YouTube servers. @@ -259,19 +245,6 @@ https_only: false ## # use_innertube_for_captions: false -## -## Send Google session informations. This is useful when Invidious is blocked -## by the message "This helps protect our community." -## See https://github.com/iv-org/invidious/issues/4734. -## -## Warning: These strings gives much more identifiable information to Google! -## -## Accepted values: String -## Default: -## -# po_token: "" -# visitor_data: "" - # ----------------------------- # Logging # ----------------------------- diff --git a/src/invidious.cr b/src/invidious.cr index 197b150c..7fa0725e 100644 --- a/src/invidious.cr +++ b/src/invidious.cr @@ -170,15 +170,6 @@ Invidious::Database.check_integrity(CONFIG) {% puts "\nDone checking player dependencies, now compiling Invidious...\n" %} {% end %} -# Misc - -DECRYPT_FUNCTION = - if sig_helper_address = CONFIG.signature_server.presence - IV::DecryptFunction.new(sig_helper_address) - else - nil - end - # Start jobs if CONFIG.channel_threads > 0 diff --git a/src/invidious/config.cr b/src/invidious/config.cr index 92c510d0..7853d9a3 100644 --- a/src/invidious/config.cr +++ b/src/invidious/config.cr @@ -153,9 +153,6 @@ class Config @[YAML::Field(converter: Preferences::FamilyConverter)] property force_resolve : Socket::Family = Socket::Family::UNSPEC - # External signature solver server socket (either a path to a UNIX domain socket or ":") - property signature_server : String? = nil - # Port to listen for connections (overridden by command line argument) property port : Int32 = 3000 # Host to bind (overridden by command line argument) @@ -170,11 +167,6 @@ class Config # Use Innertube's transcripts API instead of timedtext for closed captions property use_innertube_for_captions : Bool = false - # visitor data ID for Google session - property visitor_data : String? = nil - # poToken for passing bot attestation - property po_token : String? = nil - # Invidious companion property invidious_companion : Array(CompanionConfig) = [] of CompanionConfig @@ -262,11 +254,7 @@ class Config {% 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? + 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!!" @@ -284,8 +272,6 @@ class Config companion.builtin_proxy = true end end - elsif config.signature_server - puts("WARNING: inv-sig-helper is deprecated. Please switch to Invidious companion: https://docs.invidious.io/installation/") else puts("WARNING: Invidious companion is required to view and playback videos. For more information see https://docs.invidious.io/installation/") end diff --git a/src/invidious/helpers/sig_helper.cr b/src/invidious/helpers/sig_helper.cr deleted file mode 100644 index 6d198a42..00000000 --- a/src/invidious/helpers/sig_helper.cr +++ /dev/null @@ -1,349 +0,0 @@ -require "uri" -require "socket" -require "socket/tcp_socket" -require "socket/unix_socket" - -{% if flag?(:advanced_debug) %} - require "io/hexdump" -{% end %} - -private alias NetworkEndian = IO::ByteFormat::NetworkEndian - -module Invidious::SigHelper - enum UpdateStatus - Updated - UpdateNotRequired - Error - end - - # ------------------- - # Payload types - # ------------------- - - abstract struct Payload - end - - struct StringPayload < Payload - getter string : String - - def initialize(str : String) - raise Exception.new("SigHelper: String can't be empty") if str.empty? - @string = str - end - - def self.from_bytes(slice : Bytes) - size = IO::ByteFormat::NetworkEndian.decode(UInt16, slice) - if size == 0 # Error code - raise Exception.new("SigHelper: Server encountered an error") - end - - if (slice.bytesize - 2) != size - raise Exception.new("SigHelper: String size mismatch") - end - - if str = String.new(slice[2..]) - return self.new(str) - else - raise Exception.new("SigHelper: Can't read string from socket") - end - end - - def to_io(io) - # `.to_u16` raises if there is an overflow during the conversion - io.write_bytes(@string.bytesize.to_u16, NetworkEndian) - io.write(@string.to_slice) - end - end - - private enum Opcode - FORCE_UPDATE = 0 - DECRYPT_N_SIGNATURE = 1 - DECRYPT_SIGNATURE = 2 - GET_SIGNATURE_TIMESTAMP = 3 - GET_PLAYER_STATUS = 4 - PLAYER_UPDATE_TIMESTAMP = 5 - end - - private record Request, - opcode : Opcode, - payload : Payload? - - # ---------------------- - # High-level functions - # ---------------------- - - class Client - @mux : Multiplexor - - def initialize(uri_or_path) - @mux = Multiplexor.new(uri_or_path) - end - - # Forces the server to re-fetch the YouTube player, and extract the necessary - # components from it (nsig function code, sig function code, signature timestamp). - def force_update : UpdateStatus - request = Request.new(Opcode::FORCE_UPDATE, nil) - - value = send_request(request) do |bytes| - IO::ByteFormat::NetworkEndian.decode(UInt16, bytes) - end - - case value - when 0x0000 then return UpdateStatus::Error - when 0xFFFF then return UpdateStatus::UpdateNotRequired - when 0xF44F then return UpdateStatus::Updated - else - code = value.nil? ? "nil" : value.to_s(base: 16) - raise Exception.new("SigHelper: Invalid status code received #{code}") - end - end - - # Decrypt a provided n signature using the server's current nsig function - # code, and return the result (or an error). - def decrypt_n_param(n : String) : String? - request = Request.new(Opcode::DECRYPT_N_SIGNATURE, StringPayload.new(n)) - - n_dec = self.send_request(request) do |bytes| - StringPayload.from_bytes(bytes).string - end - - return n_dec - end - - # Decrypt a provided s signature using the server's current sig function - # code, and return the result (or an error). - def decrypt_sig(sig : String) : String? - request = Request.new(Opcode::DECRYPT_SIGNATURE, StringPayload.new(sig)) - - sig_dec = self.send_request(request) do |bytes| - StringPayload.from_bytes(bytes).string - end - - return sig_dec - end - - # Return the signature timestamp from the server's current player - def get_signature_timestamp : UInt64? - request = Request.new(Opcode::GET_SIGNATURE_TIMESTAMP, nil) - - return self.send_request(request) do |bytes| - IO::ByteFormat::NetworkEndian.decode(UInt64, bytes) - end - end - - # Return the current player's version - def get_player : UInt32? - request = Request.new(Opcode::GET_PLAYER_STATUS, nil) - - return self.send_request(request) do |bytes| - has_player = (bytes[0] == 0xFF) - player_version = IO::ByteFormat::NetworkEndian.decode(UInt32, bytes[1..4]) - has_player ? player_version : nil - end - end - - # Return when the player was last updated - def get_player_timestamp : UInt64? - request = Request.new(Opcode::PLAYER_UPDATE_TIMESTAMP, nil) - - return self.send_request(request) do |bytes| - IO::ByteFormat::NetworkEndian.decode(UInt64, bytes) - end - end - - private def send_request(request : Request, &) - channel = @mux.send(request) - slice = channel.receive - return yield slice - rescue ex - LOGGER.debug("SigHelper: Error when sending a request") - LOGGER.trace(ex.inspect_with_backtrace) - return nil - end - end - - # --------------------- - # Low level functions - # --------------------- - - class Multiplexor - alias TransactionID = UInt32 - record Transaction, channel = ::Channel(Bytes).new - - @prng = Random.new - @mutex = Mutex.new - @queue = {} of TransactionID => Transaction - - @conn : Connection - @uri_or_path : String - - def initialize(@uri_or_path) - @conn = Connection.new(uri_or_path) - listen - end - - def listen : Nil - raise "Socket is closed" if @conn.closed? - - LOGGER.debug("SigHelper: Multiplexor listening") - - spawn do - loop do - begin - receive_data - rescue ex - LOGGER.info("SigHelper: Connection to helper died with '#{ex.message}' trying to reconnect...") - # We close the socket because for some reason is not closed. - @conn.close - loop do - begin - @conn = Connection.new(@uri_or_path) - LOGGER.info("SigHelper: Reconnected to SigHelper!") - rescue ex - LOGGER.debug("SigHelper: Reconnection to helper unsuccessful with error '#{ex.message}'. Retrying") - sleep 500.milliseconds - next - end - break if !@conn.closed? - end - end - Fiber.yield - end - end - end - - def send(request : Request) - transaction = Transaction.new - transaction_id = @prng.rand(TransactionID) - - # Add transaction to queue - @mutex.synchronize do - # On a 32-bits random integer, this should never happen. Though, just in case, ... - if @queue[transaction_id]? - raise Exception.new("SigHelper: Duplicate transaction ID! You got a shiny pokemon!") - end - - @queue[transaction_id] = transaction - end - - write_packet(transaction_id, request) - - return transaction.channel - end - - def receive_data - transaction_id, slice = read_packet - - @mutex.synchronize do - if transaction = @queue.delete(transaction_id) - # Remove transaction from queue and send data to the channel - transaction.channel.send(slice) - LOGGER.trace("SigHelper: Transaction unqueued and data sent to channel") - else - raise Exception.new("SigHelper: Received transaction was not in queue") - end - end - end - - # Read a single packet from the socket - private def read_packet : {TransactionID, Bytes} - # Header - transaction_id = @conn.read_bytes(UInt32, NetworkEndian) - length = @conn.read_bytes(UInt32, NetworkEndian) - - LOGGER.trace("SigHelper: Recv transaction 0x#{transaction_id.to_s(base: 16)} / length #{length}") - - if length > 67_000 - raise Exception.new("SigHelper: Packet longer than expected (#{length})") - end - - # Payload - slice = Bytes.new(length) - @conn.read(slice) if length > 0 - - LOGGER.trace("SigHelper: payload = #{slice}") - LOGGER.trace("SigHelper: Recv transaction 0x#{transaction_id.to_s(base: 16)} - Done") - - return transaction_id, slice - end - - # Write a single packet to the socket - private def write_packet(transaction_id : TransactionID, request : Request) - LOGGER.trace("SigHelper: Send transaction 0x#{transaction_id.to_s(base: 16)} / opcode #{request.opcode}") - - io = IO::Memory.new(1024) - io.write_bytes(request.opcode.to_u8, NetworkEndian) - io.write_bytes(transaction_id, NetworkEndian) - - if payload = request.payload - payload.to_io(io) - end - - @conn.send(io) - @conn.flush - - LOGGER.trace("SigHelper: Send transaction 0x#{transaction_id.to_s(base: 16)} - Done") - end - end - - class Connection - @socket : UNIXSocket | TCPSocket - - {% if flag?(:advanced_debug) %} - @io : IO::Hexdump - {% end %} - - def initialize(host_or_path : String) - case host_or_path - when .starts_with?('/') - # Make sure that the file exists - if File.exists?(host_or_path) - @socket = UNIXSocket.new(host_or_path) - else - raise Exception.new("SigHelper: '#{host_or_path}' no such file") - end - when .starts_with?("tcp://") - uri = URI.parse(host_or_path) - @socket = TCPSocket.new(uri.host.not_nil!, uri.port.not_nil!) - else - uri = URI.parse("tcp://#{host_or_path}") - @socket = TCPSocket.new(uri.host.not_nil!, uri.port.not_nil!) - end - LOGGER.info("SigHelper: Using helper at '#{host_or_path}'") - - {% if flag?(:advanced_debug) %} - @io = IO::Hexdump.new(@socket, output: STDERR, read: true, write: true) - {% end %} - - @socket.sync = false - @socket.blocking = false - end - - def closed? : Bool - return @socket.closed? - end - - def close : Nil - @socket.close if !@socket.closed? - end - - def flush(*args, **options) - @socket.flush(*args, **options) - end - - def send(*args, **options) - @socket.send(*args, **options) - end - - # Wrap IO functions, with added debug tooling if needed - {% for function in %w(read read_bytes write write_bytes) %} - def {{function.id}}(*args, **options) - {% if flag?(:advanced_debug) %} - @io.{{function.id}}(*args, **options) - {% else %} - @socket.{{function.id}}(*args, **options) - {% end %} - end - {% end %} - end -end diff --git a/src/invidious/helpers/signatures.cr b/src/invidious/helpers/signatures.cr deleted file mode 100644 index 82a28fc0..00000000 --- a/src/invidious/helpers/signatures.cr +++ /dev/null @@ -1,53 +0,0 @@ -require "http/params" -require "./sig_helper" - -class Invidious::DecryptFunction - @last_update : Time = Time.utc - 42.days - - def initialize(uri_or_path) - @client = SigHelper::Client.new(uri_or_path) - self.check_update - end - - def check_update - # If we have updated in the last 5 minutes, do nothing - return if (Time.utc - @last_update) < 5.minutes - - # Get the amount of time elapsed since when the player was updated, in the - # event where multiple invidious processes are run in parallel. - update_time_elapsed = (@client.get_player_timestamp || 301).seconds - - if update_time_elapsed > 5.minutes - LOGGER.debug("Signature: Player might be outdated, updating") - @client.force_update - @last_update = Time.utc - end - end - - def decrypt_nsig(n : String) : String? - self.check_update - return @client.decrypt_n_param(n) - rescue ex - LOGGER.debug(ex.message || "Signature: Unknown error") - LOGGER.trace(ex.inspect_with_backtrace) - return nil - end - - def decrypt_signature(str : String) : String? - self.check_update - return @client.decrypt_sig(str) - rescue ex - LOGGER.debug(ex.message || "Signature: Unknown error") - LOGGER.trace(ex.inspect_with_backtrace) - return nil - end - - def get_sts : UInt64? - self.check_update - return @client.get_signature_timestamp - rescue ex - LOGGER.debug(ex.message || "Signature: Unknown error") - LOGGER.trace(ex.inspect_with_backtrace) - return nil - end -end diff --git a/src/invidious/videos.cr b/src/invidious/videos.cr index 348a0a66..0446922f 100644 --- a/src/invidious/videos.cr +++ b/src/invidious/videos.cr @@ -326,6 +326,14 @@ end def fetch_video(id, region) info = extract_video_info(video_id: id) + if info.nil? + raise InfoException.new("Invidious companion is not available. \ + Video playback cannot continue. \ + If you are the administrator of this instance, install Invidious companion \ + following the installation instructions \ + https://docs.invidious.io/installation/") + end + if reason = info["reason"]? if reason == "Video unavailable" raise NotFoundException.new(reason.as_s || "") diff --git a/src/invidious/videos/parser.cr b/src/invidious/videos/parser.cr index 6038dfcf..8114ad68 100644 --- a/src/invidious/videos/parser.cr +++ b/src/invidious/videos/parser.cr @@ -53,11 +53,12 @@ def parse_related_video(related : JSON::Any) : Hash(String, JSON::Any)? end def extract_video_info(video_id : String) - # Init client config for the API - client_config = YoutubeAPI::ClientConfig.new - # Fetch data from the player endpoint - player_response = YoutubeAPI.player(video_id: video_id, params: "2AMB", client_config: client_config) + player_response = YoutubeAPI.player(video_id: video_id) + + if player_response.nil? + return nil + end playability_status = player_response.dig?("playabilityStatus", "status").try &.as_s @@ -105,37 +106,6 @@ def extract_video_info(video_id : String) params = parse_video_info(video_id, player_response) params["reason"] = JSON::Any.new(reason) if reason - if !CONFIG.invidious_companion.present? - if player_response.dig?("streamingData", "adaptiveFormats", 0, "url").nil? - LOGGER.warn("Missing URLs for adaptive formats, falling back to other YT clients.") - players_fallback = {YoutubeAPI::ClientType::TvSimply, YoutubeAPI::ClientType::WebMobile} - - players_fallback.each do |player_fallback| - client_config.client_type = player_fallback - - next if !(player_fallback_response = try_fetch_streaming_data(video_id, client_config)) - - adaptive_formats = player_fallback_response.dig?("streamingData", "adaptiveFormats") - if adaptive_formats && (adaptive_formats.dig?(0, "url") || adaptive_formats.dig?(0, "signatureCipher")) - streaming_data = player_response["streamingData"].as_h - streaming_data["adaptiveFormats"] = adaptive_formats - player_response["streamingData"] = JSON::Any.new(streaming_data) - break - end - rescue InfoException - next LOGGER.warn("Failed to fetch streams with #{player_fallback}") - end - end - - # Seems like video page can still render even without playable streams. - # its better than nothing. - # - # # Were we able to find playable video streams? - # if player_response.dig?("streamingData", "adaptiveFormats", 0, "url").nil? - # # No :( - # end - end - {"captions", "playabilityStatus", "playerConfig", "storyboards"}.each do |f| params[f] = player_response[f] if player_response[f]? end @@ -163,7 +133,7 @@ end def try_fetch_streaming_data(id : String, client_config : YoutubeAPI::ClientConfig) : Hash(String, JSON::Any)? LOGGER.debug("try_fetch_streaming_data: [#{id}] Using #{client_config.client_type} client.") - response = YoutubeAPI.player(video_id: id, params: "2AMB", client_config: client_config) + response = YoutubeAPI.player(video_id: id) playability_status = response["playabilityStatus"]["status"] LOGGER.debug("try_fetch_streaming_data: [#{id}] Got playabilityStatus == #{playability_status}.") @@ -475,26 +445,15 @@ end private def convert_url(fmt) if cfr = fmt["signatureCipher"]?.try { |json| HTTP::Params.parse(json.as_s) } - sp = cfr["sp"] url = URI.parse(cfr["url"]) params = url.query_params LOGGER.debug("convert_url: Decoding '#{cfr}'") - - unsig = DECRYPT_FUNCTION.try &.decrypt_signature(cfr["s"]) - params[sp] = unsig if unsig else url = URI.parse(fmt["url"].as_s) params = url.query_params end - n = DECRYPT_FUNCTION.try &.decrypt_nsig(params["n"]) - params["n"] = n if n - - if token = CONFIG.po_token - params["pot"] = token - end - url.query_params = params LOGGER.trace("convert_url: new url is '#{url}'") diff --git a/src/invidious/yt_backend/youtube_api.cr b/src/invidious/yt_backend/youtube_api.cr index 6fa8ae0e..dd709920 100644 --- a/src/invidious/yt_backend/youtube_api.cr +++ b/src/invidious/yt_backend/youtube_api.cr @@ -199,10 +199,6 @@ module YoutubeAPI # conf_1 = ClientConfig.new(region: "NO") # YoutubeAPI::search("Kollektivet", params: "", client_config: conf_1) # - # # Use the Android client to request video streams URLs - # conf_2 = ClientConfig.new(client_type: ClientType::Android) - # YoutubeAPI::player(video_id: "dQw4w9WgXcQ", client_config: conf_2) - # # struct ClientConfig # Type of client to emulate. @@ -335,10 +331,6 @@ module YoutubeAPI client_context["client"]["platform"] = platform end - if CONFIG.visitor_data.is_a?(String) - client_context["client"]["visitorData"] = CONFIG.visitor_data.as(String) - end - return client_context end @@ -455,61 +447,23 @@ module YoutubeAPI end #################################################################### - # player(video_id, params, client_config?) + # player(video_id) # - # Requests the youtubei/v1/player endpoint with the required headers - # and POST data in order to get a JSON reply. + # Requests the youtubei/v1/player Invidious Companion endpoint with + # the requested video ID. # - # The requested data is a video ID (`v=` parameter), with some - # additional parameters, formatted as a base64 string. + # The requested data is a video ID (`v=` parameter). # - # An optional ClientConfig parameter can be passed, too (see - # `struct ClientConfig` above for more details). - # - def player( - video_id : String, - *, # Force the following parameters to be passed by name - params : String, - client_config : ClientConfig | Nil = nil, - ) - # Playback context, separate because it can be different between clients - playback_ctx = { - "html5Preference" => "HTML5_PREF_WANTS", - "referer" => "https://www.youtube.com/watch?v=#{video_id}", - } of String => String | Int64 - - if {"WEB", "TVHTML5"}.any? { |s| client_config.name.starts_with? s } - if sts = DECRYPT_FUNCTION.try &.get_sts - playback_ctx["signatureTimestamp"] = sts.to_i64 - end - end - - # JSON Request data, required by the API + def player(video_id : String) + # JSON Request data, required by Invidious Companion data = { - "contentCheckOk" => true, - "videoId" => video_id, - "context" => self.make_context(client_config, video_id), - "racyCheckOk" => true, - "user" => { - "lockedSafetyMode" => false, - }, - "playbackContext" => { - "contentPlaybackContext" => playback_ctx, - }, - "serviceIntegrityDimensions" => { - "poToken" => CONFIG.po_token, - }, + "videoId" => video_id, } - # Append the additional parameters if those were provided - if params != "" - data["params"] = params - end - if CONFIG.invidious_companion.present? return self._post_invidious_companion("/youtubei/v1/player", data) else - return self._post_json("/youtubei/v1/player", data, client_config) + return nil end end @@ -635,10 +589,6 @@ module YoutubeAPI headers["User-Agent"] = user_agent end - if CONFIG.visitor_data.is_a?(String) - headers["X-Goog-Visitor-Id"] = CONFIG.visitor_data.as(String) - end - # Logging LOGGER.debug("YoutubeAPI: Using endpoint: \"#{endpoint}\"") LOGGER.trace("YoutubeAPI: ClientConfig: #{client_config}") From a7935bc3782249b82d44e4b85263ffe457874431 Mon Sep 17 00:00:00 2001 From: Fijxu Date: Sat, 6 Dec 2025 17:15:25 -0300 Subject: [PATCH 08/10] fix: restore dmca_content functionality (#5228) * fix: restore dmca_content functionality This restores (or adds) the functionality of the `dmca_content` config option that at this date, has been unused and makes no effect. * only disable download widget for dmca video ids --- locales/en-US.json | 3 ++- src/invidious/frontend/watch_page.cr | 4 ++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/locales/en-US.json b/locales/en-US.json index 6fd1ab0b..5b2ef8d0 100644 --- a/locales/en-US.json +++ b/locales/en-US.json @@ -505,5 +505,6 @@ "carousel_go_to": "Go to slide `x`", "timeline_parse_error_placeholder_heading": "Unable to parse item", "timeline_parse_error_placeholder_message": "Invidious encountered an error while trying to parse this item. For more information see below:", - "timeline_parse_error_show_technical_details": "Show technical details" + "timeline_parse_error_show_technical_details": "Show technical details", + "dmca_content": "This video cannot be downloaded on this instance due to a DMCA/copyright infringement letter sent to the instance administrator." } diff --git a/src/invidious/frontend/watch_page.cr b/src/invidious/frontend/watch_page.cr index c0926164..14e169e8 100644 --- a/src/invidious/frontend/watch_page.cr +++ b/src/invidious/frontend/watch_page.cr @@ -23,6 +23,10 @@ module Invidious::Frontend::WatchPage return "

#{translate(locale, "Download is disabled")}

" end + if CONFIG.dmca_content.includes?(video.id) + return "

#{translate(locale, "dmca_content")}

" + end + url = "/download" if (CONFIG.invidious_companion.present?) invidious_companion = CONFIG.invidious_companion.sample From 3944d2490c254dac138d75431082320e1ee43b11 Mon Sep 17 00:00:00 2001 From: Fijxu Date: Sat, 6 Dec 2025 20:19:38 -0300 Subject: [PATCH 09/10] Fix trending page by leaving livestream and gaming trending pages (#5555) The livestream trending page is now the default. Adds `content_container = special_category_container["gridRenderer"]?` in the `CategoryRendererParser` needed for the gaming trending page. The JSON structure of the gaming trending page looked like this: ```json "contents": { "twoColumnBrowseResultsRenderer": { "tabs": [ { "tabRenderer": { "selected": true, "content": { "sectionListRenderer": { "contents": [ { "itemSectionRenderer": { "contents": [ { "shelfRenderer": { "title": { "runs": [ { "text": "Trending videos" } ] }, "content": { "gridRenderer": { // <- This was added to the CategoryRendererParser "items": [ { "gridVideoRenderer": { "videoId": "sTWztaLjD20", // More video data // ... } } ] } } } } ] } } ] } } } } ] } } ``` Thanks to https://github.com/TeamNewPipe/NewPipeExtractor/blob/ae2755bf715538dbaed028ecb1a0553c1646710d/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/extractors/kiosk/YoutubeTrendingGamingVideosExtractor.java#L11-L13 for the `browse_id` and `params` needed for the gaming trending page. --- src/invidious/trending.cr | 17 +++++++++-------- src/invidious/views/feeds/trending.ecr | 2 +- src/invidious/yt_backend/extractors.cr | 1 + 3 files changed, 11 insertions(+), 9 deletions(-) diff --git a/src/invidious/trending.cr b/src/invidious/trending.cr index e289ed5b..622fe517 100644 --- a/src/invidious/trending.cr +++ b/src/invidious/trending.cr @@ -4,20 +4,21 @@ def fetch_trending(trending_type, region, locale) plid = nil - browse_id = "FEtrending" + browse_id = "" case trending_type.try &.downcase - when "music" - params = "4gINGgt5dG1hX2NoYXJ0cw%3D%3D" when "gaming" - params = "4gIcGhpnYW1pbmdfY29ycHVzX21vc3RfcG9wdWxhcg%3D%3D" - when "movies" - params = "4gIKGgh0cmFpbGVycw%3D%3D" + browse_id = "UCOpNcN46UbXVtpKMrmU4Abg" + params = "Egh0cmVuZGluZw%3D%3D" when "livestreams" browse_id = "UC4R8DWoMoI7CAwX8_LjQHig" params = "EgdsaXZldGFikgEDCKEK" - else # Default - params = "" + else + # Livestreams is the default one as Youtube removed + # the aggregated trending page + # https://github.com/iv-org/invidious/issues/5397#issuecomment-3218928458 + browse_id = "UC4R8DWoMoI7CAwX8_LjQHig" + params = "EgdsaXZldGFikgEDCKEK" end client_config = YoutubeAPI::ClientConfig.new(region: region) diff --git a/src/invidious/views/feeds/trending.ecr b/src/invidious/views/feeds/trending.ecr index 69483f30..46d02ad4 100644 --- a/src/invidious/views/feeds/trending.ecr +++ b/src/invidious/views/feeds/trending.ecr @@ -21,7 +21,7 @@
- <% {"Default", "Music", "Gaming", "Movies", "Livestreams"}.each do |option| %> + <% {"Livestreams", "Gaming"}.each do |option| %>
<% if trending_type == option %> <%= translate(locale, option) %> diff --git a/src/invidious/yt_backend/extractors.cr b/src/invidious/yt_backend/extractors.cr index 85f6caa5..04e00f20 100644 --- a/src/invidious/yt_backend/extractors.cr +++ b/src/invidious/yt_backend/extractors.cr @@ -442,6 +442,7 @@ private module Parsers if content_container = special_category_container["horizontalListRenderer"]? elsif content_container = special_category_container["expandedShelfContentsRenderer"]? elsif content_container = special_category_container["verticalListRenderer"]? + elsif content_container = special_category_container["gridRenderer"]? else # Anything else, such as `horizontalMovieListRenderer` is currently unsupported. return From ef2290c1fde23af2a13ce50bb6f091e91ad0792d Mon Sep 17 00:00:00 2001 From: Fijxu Date: Sat, 6 Dec 2025 20:20:42 -0300 Subject: [PATCH 10/10] Fix channel name overflow (#5553) --- assets/css/default.css | 3 ++- src/invidious/views/components/channel_info.ecr | 2 +- src/invidious/views/watch.ecr | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/assets/css/default.css b/assets/css/default.css index 644d91c2..78ef7a60 100644 --- a/assets/css/default.css +++ b/assets/css/default.css @@ -404,8 +404,9 @@ input[type="search"]::-webkit-search-cancel-button { .video-card-row { margin: 15px 0; } -p.channel-name { margin: 0; } +p.channel-name { margin: 0; overflow-wrap: anywhere;} p.video-data { margin: 0; font-weight: bold; font-size: 80%; } +.channel-profile > .channel-name { overflow-wrap: anywhere;} /* diff --git a/src/invidious/views/components/channel_info.ecr b/src/invidious/views/components/channel_info.ecr index f4164f31..2c177b59 100644 --- a/src/invidious/views/components/channel_info.ecr +++ b/src/invidious/views/components/channel_info.ecr @@ -12,7 +12,7 @@
- <%= author %><% if !channel.verified.nil? && channel.verified %> <% end %> + <%= author %><% if !channel.verified.nil? && channel.verified %> <% end %>
diff --git a/src/invidious/views/watch.ecr b/src/invidious/views/watch.ecr index 89632dc5..923c2a83 100644 --- a/src/invidious/views/watch.ecr +++ b/src/invidious/views/watch.ecr @@ -230,7 +230,7 @@ we're going to need to do it here in order to allow for translations. <% if !video.author_thumbnail.empty? %> <% end %> - <%= author %><% if !video.author_verified.nil? && video.author_verified %> <% end %> + <%= author %><% if !video.author_verified.nil? && video.author_verified %> <% end %>