Compare commits

...
Author SHA1 Message Date
github-actions[bot] 4e9d20ddae Release v2.20261114.0 2026-11-14 11:34:42 -03:00
dependabot[bot] 93e9573b00 chore(deps): bump flake-utils from b7a79d3 to bd48884 in /nix (#6145)
Bumps [flake-utils](https://github.com/numtide/flake-utils) from `b7a79d3` to `bd48884`.
- [Release notes](https://github.com/numtide/flake-utils/releases)
- [Commits](https://github.com/numtide/flake-utils/compare/b7a79d3fea1e214f6d668d521cecec0c01e26544...bd4888409339808dbaa94e2358c2c4df74b8e657)

---
updated-dependencies:
- dependency-name: flake-utils
  dependency-version: bd4888409339808dbaa94e2358c2c4df74b8e657
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-11-14 11:05:48 -03:00
dependabot[bot] 1e855775c0 chore(deps): bump nixpkgs from 3ec6363 to f4a03c0 in /nix (#6144)
Bumps [nixpkgs](https://github.com/NixOS/nixpkgs) from `3ec6363` to `f4a03c0`.
- [Commits](https://github.com/NixOS/nixpkgs/compare/3ec636362e1816bc2f0888fa0fa2c69f1bc9e504...f4a03c008967841f2a0b3cb7c436fc6fc31671f6)

---
updated-dependencies:
- dependency-name: nixpkgs
  dependency-version: f4a03c008967841f2a0b3cb7c436fc6fc31671f6
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-11-14 11:05:37 -03:00
Fijxu 23b3baf53e chore: Update to Crystal 1.22.3 (#6148)
* chore: update to Crystal 1.22.3

* chore: update Docker image to Crystal 1.22.3
2026-11-14 10:00:40 -03:00
Fijxu 85ac546824 Prepare for next release 2026-11-13 20:11:42 -03:00
github-actions[bot] 1f59da354e Release v2.20261113.0 2026-11-13 20:09:46 -03:00
Fijxu 82e3bc424f fix: fix ggpht route after commit 3df8616 (#6143)
* fix: fix ggpht route after commit 3df8616

Closes: https://github.com/iv-org/invidious/issues/6125

* add /ytc/ too
2026-11-13 15:57:36 -03:00
Fijxu c514dcbd45 chore(deps): update http_proxy to 0.17.0 (#6142) 2026-11-13 15:24:23 -03:00
Fijxu fc17ae60c9 chore(deps): update athena-negotiation to 0.2.0 (#6141) 2026-11-13 15:24:13 -03:00
Fijxu 9274eb229b fix: correctly parse premiere_timestamp on channel videos (#6140)
Closes: https://github.com/iv-org/invidious/issues/6135
2026-11-13 15:14:49 -03:00
dependabot[bot] 200e73556e chore(deps): bump crystal-lang/install-crystal from 1.9.4 to 1.9.5 (#6129)
Bumps [crystal-lang/install-crystal](https://github.com/crystal-lang/install-crystal) from 1.9.4 to 1.9.5.
- [Release notes](https://github.com/crystal-lang/install-crystal/releases)
- [Commits](https://github.com/crystal-lang/install-crystal/compare/v1.9.4...v1.9.5)

---
updated-dependencies:
- dependency-name: crystal-lang/install-crystal
  dependency-version: 1.9.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-11-12 12:48:25 -03:00
Fijxu d165dde536 fix: also check for "video" string in the video count of playlists
Closes https://github.com/iv-org/invidious/issues/6136
2026-11-10 22:07:24 -03:00
Fijxu 21d972dcfc fix: detect members only videos and skip them
Closes https://github.com/iv-org/invidious/issues/6127
Closes https://github.com/iv-org/invidious/issues/6133
2026-11-10 15:29:08 -03:00
Fijxu 79761abc94 feat: add AI made indicator for videos that have the AI badge (#6113)
* feat: add AI made indicator for videos that have the AI badge

* forgot to add the api endpoint!

* bump schema version, since aiMade was added
2026-11-04 21:29:37 -03:00
Fijxu 863a07b14a chore: Display a nice message when the config.yml file is missing (#6121) 2026-11-04 21:28:55 -03:00
15 changed files with 140 additions and 26 deletions

No files matched your search

+4 -4
View File
@@ -50,7 +50,7 @@ jobs:
- 1.19.2
- 1.20.3
- 1.21.1
- 1.22.2
- 1.22.3
include:
- crystal: nightly
stable: false
@@ -66,7 +66,7 @@ jobs:
shell: bash
- name: Install Crystal
uses: crystal-lang/install-crystal@v1.9.4
uses: crystal-lang/install-crystal@v1.9.5
with:
crystal: ${{ matrix.crystal }}
@@ -138,9 +138,9 @@ jobs:
- name: Install Crystal
id: lint_step_install_crystal
uses: crystal-lang/install-crystal@v1.9.4
uses: crystal-lang/install-crystal@v1.9.5
with:
crystal: 1.22.2
crystal: 1.22.3
- name: Cache Shards
uses: actions/cache@v6
+59
View File
@@ -7,6 +7,65 @@
## v2.20261114.0
### Wrap-up
This release updates the project to Crystal 1.22.3, ensuring compatibility with the latest language features and improvements. No user-facing changes are included; this is a maintenance release for developers and instance owners.
### New features & important changes
#### For Developers
- Updated to Crystal 1.22.3 (#6148)
### Full list of pull requests merged since the last release (newest first)
* chore: Update to Crystal 1.22.3 (https://github.com/iv-org/invidious/pull/6148, by @Fijxu)
* Release v2.20261113.0 (https://github.com/iv-org/invidious/pull/6146, by @github-actions[bot])
## v2.20261113.0
### Wrap-up
This release adds an AI-made indicator for videos that carry the AI badge and continues support for custom channel handle URLs with tab paths. It also fixes parsing of premiere timestamps on channel videos, corrects the ggpht route, and resolves parsing of published dates and view counts for collab videos.
For instance owners, Invidious now displays a friendly message when the config.yml file is missing. Dependency updates include http_proxy 0.17.0 and athena-negotiation 0.2.0, and CI bumps crystal-lang/install-crystal to 1.9.5.
### New features & important changes
#### For Users
- Added an AI-made indicator for videos that carry the AI badge (#6113)
- Support for custom channel handle URLs with tab paths, e.g. `{INSTANCE}/markrober/streams` (#5811)
#### For instance owners
- Display a friendly message when the config.yml file is missing (#6121)
- Updated http_proxy to 0.17.0 (#6142)
- Updated athena-negotiation to 0.2.0 (#6141)
#### For developers
- Bumped crystal-lang/install-crystal from 1.9.4 to 1.9.5 (#6129)
### Bugs fixed
#### For Users
- Fixed parsing of premiere_timestamp on channel videos (#6140)
- Fixed the ggpht route (#6143)
- Fixed parsing of published date and view count for collab videos (#5812)
### Full list of pull requests merged since the last release (newest first)
* fix: fix ggpht route after commit 3df8616 (https://github.com/iv-org/invidious/pull/6143, by @Fijxu)
* chore(deps): update http_proxy to 0.17.0 (https://github.com/iv-org/invidious/pull/6142, by @Fijxu)
* chore(deps): update athena-negotiation to 0.2.0 (https://github.com/iv-org/invidious/pull/6141, by @Fijxu)
* fix: correctly parse premiere_timestamp on channel videos (https://github.com/iv-org/invidious/pull/6140, by @Fijxu)
* chore(deps): bump crystal-lang/install-crystal from 1.9.4 to 1.9.5 (https://github.com/iv-org/invidious/pull/6129, by @dependabot[bot])
* feat: add AI made indicator for videos that have the AI badge (https://github.com/iv-org/invidious/pull/6113, by @Fijxu)
* chore: Display a nice message when the config.yml file is missing (https://github.com/iv-org/invidious/pull/6121, by @Fijxu)
* Release v2.20261103.0 (https://github.com/iv-org/invidious/pull/6118, by @github-actions[bot])
* fix(parser): parse published date and view count for collab videos (https://github.com/iv-org/invidious/pull/5812, by @radmorecameron)
* feat: Adds support for custom channel handle urls with tab paths (ex: `{INSTANCE}/markrober/streams`) (https://github.com/iv-org/invidious/pull/5811, by @radmorecameron)
## v2.20261103.0
### Wrap-up
+1 -1
View File
@@ -2,7 +2,7 @@
ARG OPENSSL_VERSION='3.6.5'
ARG OPENSSL_SHA256='a2157c2830efdec3788939b00c9b0638306d3f0bbb76dc4832ee503bb397df98'
FROM 84codes/crystal:1.22.2-alpine AS dependabot-crystal
FROM 84codes/crystal:1.22.3-alpine AS dependabot-crystal
# We compile openssl ourselves due to a memory leak in how crystal interacts
# with openssl
+2 -1
View File
@@ -507,5 +507,6 @@
"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",
"dmca_content": "This video cannot be downloaded on this instance due to a DMCA/copyright infringement letter sent to the instance administrator."
"dmca_content": "This video cannot be downloaded on this instance due to a DMCA/copyright infringement letter sent to the instance administrator.",
"video_details_ai_made": "AI Made video?:"
}
+9 -9
View File
@@ -5,11 +5,11 @@
"systems": "systems"
},
"locked": {
"lastModified": 1791914995,
"narHash": "sha256-Zf/xfQEAasnwnmEfHqW/4hjDE/s44yn2hD4RuVIZRjA=",
"lastModified": 1792709690,
"narHash": "sha256-o7ey9gEilsxaifqJWnBDlYSKbNWYQYtKwDyXeZU2kaU=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "b7a79d3fea1e214f6d668d521cecec0c01e26544",
"rev": "bd4888409339808dbaa94e2358c2c4df74b8e657",
"type": "github"
},
"original": {
@@ -20,11 +20,11 @@
},
"nixpkgs": {
"locked": {
"lastModified": 1792773339,
"narHash": "sha256-rZLrzrC6i+jnTAlvPZHogKDulY7WWDEv9hJeHieO7Zw=",
"lastModified": 1794042290,
"narHash": "sha256-ziZn2PvCJE5JEEpqTdxXwJpPB+L3YqoCpTAMCzyaP3U=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "3ec636362e1816bc2f0888fa0fa2c69f1bc9e504",
"rev": "f4a03c008967841f2a0b3cb7c436fc6fc31671f6",
"type": "github"
},
"original": {
@@ -42,11 +42,11 @@
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"lastModified": 1791244623,
"narHash": "sha256-uuanbkFk8a313Zfrxdcz8Us/yQtaxNnKJtX8v0ZPpd8=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"rev": "d36eeed142f88b5a636f2e3856a53c61654cf29b",
"type": "github"
},
"original": {
+2 -2
View File
@@ -6,7 +6,7 @@ shards:
athena-negotiation:
git: https://github.com/athena-framework/negotiation.git
version: 0.1.1
version: 0.2.0
backtracer:
git: https://github.com/sija/backtracer.cr.git
@@ -22,7 +22,7 @@ shards:
http_proxy:
git: https://github.com/mamantoha/http_proxy.git
version: 0.10.3
version: 0.17.0
kemal:
git: https://github.com/kemalcr/kemal.git
+3 -3
View File
@@ -1,5 +1,5 @@
name: invidious
version: 2.20261103.0-dev
version: 2.20261114.0
authors:
- Invidious team <contact@invidious.io>
@@ -29,10 +29,10 @@ dependencies:
version: ~> 0.1.5
athena-negotiation:
github: athena-framework/negotiation
version: ~> 0.1.1
version: ~> 0.2.0
http_proxy:
github: mamantoha/http_proxy
version: ~> 0.10.3
version: ~> 0.17.0
development_dependencies:
spectator:
+10 -1
View File
@@ -228,7 +228,16 @@ class Config
env_config_yaml = "INVIDIOUS_CONFIG"
config_file = ENV.has_key?(env_config_file) ? ENV.fetch(env_config_file) : "config/config.yml"
config_yaml = ENV.has_key?(env_config_yaml) ? ENV.fetch(env_config_yaml) : File.read(config_file)
config_yaml = if ENV.has_key?(env_config_yaml)
ENV.fetch(env_config_yaml)
else
begin
File.read(config_file)
rescue ex
puts "Config: Failed to load configuration file: #{ex.message}"
exit(1)
end
end
config = Config.from_yaml(config_yaml)
+14
View File
@@ -94,6 +94,20 @@ def decode_date(string : String)
return Time.utc(string.to_i, 1, 1)
end
# Try to parse "Premieres 6/29/27, 9:00 PM"
# For some reason, the Innertube response contains a '\u202F'
# character in the separation between the time and the period
# so the raw string returned by Innertube is something like this:
# "Premieres 6/29/27, 9:00\u202FPM". Instead of using a space character
# it uses the '\u202F' character.
#
# Raw: "Premieres 6/29/27, 9:00 PM"
begin
string2 = string.lchop("Premieres ").gsub('\u202F', " ")
return Time.parse(string2, "%m/%d/%y, %l:%M %p", Time::Location.local)
rescue
end
# Try to parse as format Jul 10, 2000
begin
return Time.parse(string, "%b %-d, %Y", Time::Location.local)
@@ -64,6 +64,7 @@ module Invidious::JSONify::APIv1
json.field "liveNow", video.live_now
json.field "isPostLiveDvr", video.post_live_dvr
json.field "isUpcoming", video.upcoming?
json.field "aiMade", video.ai_made
if video.premiere_timestamp
json.field "premiereTimestamp", video.premiere_timestamp.try &.to_unix
+2 -1
View File
@@ -222,7 +222,8 @@ module Invidious::Routing
end
def register_image_routes
get "/ggpht/*", Routes::Images, :ggpht
get "/ggpht/:id", Routes::Images, :ggpht
get "/ggpht/ytc/:id", Routes::Images, :ggpht
options "/sb/:authority/:id/:storyboard/:index", Routes::Images, :options_storyboard
get "/sb/:authority/:id/:storyboard/:index", Routes::Images, :get_storyboard
get "/s_p/:id/:name", Routes::Images, :s_p_image
+5
View File
@@ -234,6 +234,10 @@ module Invidious::Videos::Parser
keywords = video_details["keywords"]?
.try &.as_a.map &.as_s || [] of String
ai_made = video_primary_renderer
.try &.dig?("badges")
.try { |badges| badges.as_a.any? { |badge| badge.dig?("metadataBadgeRenderer", "label").try(&.as_s) == "AI" } } || false
# Related videos
LOGGER.debug("extract_video_info: parsing related videos...")
@@ -454,6 +458,7 @@ module Invidious::Videos::Parser
"authorThumbnail" => JSON::Any.new(author_thumbnail.try &.as_s || ""),
"authorVerified" => JSON::Any.new(author_verified || false),
"subCountText" => JSON::Any.new(subs_text || "-"),
"aiMade" => JSON::Any.new(ai_made),
}
return params
+2 -1
View File
@@ -15,7 +15,7 @@ struct Video
# NOTE: don't forget to bump this number if any change is made to
# the `params` structure in videos/parser.cr!!!
#
SCHEMA_VERSION = 3
SCHEMA_VERSION = 4
property id : String
@@ -297,6 +297,7 @@ struct Video
getset_bool authorVerified
getset_bool isFamilyFriendly
getset_bool isListed
getset_bool aiMade
predicate_bool upcoming, isUpcoming
end
+5
View File
@@ -202,6 +202,11 @@ we're going to need to do it here in order to allow for translations.
<p id="views"><i class="icon ion-ios-eye"></i> <%= number_with_separator(video.views) %></p>
<p id="likes"><i class="icon ion-ios-thumbs-up"></i> <%= number_with_separator(video.likes) %></p>
<p id="dislikes" style="display: none; visibility: hidden;"></p>
<% if video.ai_made %>
<p id="ai_made"><%= I18n.translate(locale, "video_details_ai_made") %>
<%= I18n.translate_bool(locale, video.ai_made) %>
</p>
<% end %>
<p id="genre"><%= I18n.translate(locale, "Genre: ") %>
<% if !video.genre_url %>
<%= video.genre %>
+21 -3
View File
@@ -712,9 +712,17 @@ private module Parsers
title = metadata.dig("title", "content").as_s
published = nil
view_count_text = nil
premiere_timestamp = nil
# Contains the views of the video and the published time of the video.
if (metadata_rows = metadata.dig("metadata", "contentMetadataViewModel", "metadataRows").try &.as_a)
metadata_rows.each do |row|
# To filter members only videos, they do not have metadataParts available.
# This videos are not hidden from the channel videos list.
badges = row.dig?("badges").try &.as_a
if badges.try &.any? { |item| item.dig?("badgeViewModel", "badgeStyle").try &.as_s == "BADGE_MEMBERS_ONLY" }
next
end
metadata_parts = row.dig("metadataParts").try &.as_a
if view_count_text.nil?
@@ -725,8 +733,12 @@ private module Parsers
published = metadata_parts.try &.find { |item| item["icon"]?.nil? && item.dig?("text", "content").try &.as_s.includes?("ago") }
.try { |item| decode_date(item.dig("text", "content").as_s) }
end
if premiere_timestamp.nil?
premiere_timestamp = metadata_parts.try &.find { |item| item["icon"]?.nil? && item.dig?("text", "content").try &.as_s.includes?("Premieres") }
.try { |item| decode_date(item.dig("text", "content").as_s) }
end
break if !view_count_text.nil? && !published.nil?
break if !view_count_text.nil? && !published.nil? && !premiere_timestamp.nil?
end
end
@@ -745,7 +757,7 @@ private module Parsers
views: view_count,
description_html: "",
length_seconds: length_seconds || 0,
premiere_timestamp: Time.unix(0),
premiere_timestamp: premiere_timestamp,
author_verified: false,
author_thumbnail: nil,
badges: VideoBadges::None,
@@ -801,7 +813,13 @@ private module Parsers
.compact_map(&.dig?("thumbnailOverlayBadgeViewModel", "thumbnailBadges").try &.as_a)
.flatten
.find(nil, &.dig?("thumbnailBadgeViewModel", "text").try do |node|
{"episodes", "videos", "lessons"}.any? { |str| node.as_s.ends_with?(str) }
# Playlists that contain MORE THAN ONE video, use "videos" for their video count.
# "text" => "8 videos"
#
# Playlists that contain ONLY ONE video, use "video" for their video count.
# "text" => "1 video"
#
{"episodes", "videos", "video", "lessons"}.any? { |str| node.as_s.ends_with?(str) }
end)
.try &.dig("thumbnailBadgeViewModel", "text").as_s.to_i(strict: false)