From c644d73def638deb25d3b867134607f3c4c1b4d8 Mon Sep 17 00:00:00 2001 From: NeskireDK <10115530+NeskireDK@users.noreply.github.com> Date: Wed, 12 Aug 2026 18:47:52 +0200 Subject: [PATCH] Add playlist-backed Trending/Popular feeds MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New config lists trending_playlists / popular_playlists. When set, the feed serves the merged content of those local PUBLIC playlists instead of the stock feed: - Swap at the data layer (fetch_trending, popular_videos), so the HTML views and /api/v1/trending|popular emit the same items and API clients (Yattee, Materialious) need no changes. - Playlists merge in config order, each in its own order, duplicates dropped. Missing or non-public entries are skipped and logged, never fatal. - One local Postgres query per playlist — no YouTube call. - PlaylistVideo converts to SearchVideo with neutral defaults for metadata a playlist row lacks (views, description, thumbnails). - Trending category tabs and region are ignored while set. With a single trending playlist, 'View as playlist' links to it. Co-Authored-By: Claude Fable 5 --- config/config.example.yml | 18 ++++++++++ src/invidious.cr | 5 +++ src/invidious/config.cr | 5 +++ src/invidious/playlist_feeds.cr | 58 +++++++++++++++++++++++++++++++++ src/invidious/trending.cr | 8 +++++ 5 files changed, 94 insertions(+) create mode 100644 src/invidious/playlist_feeds.cr diff --git a/config/config.example.yml b/config/config.example.yml index a2f10a608..5b4a14662 100644 --- a/config/config.example.yml +++ b/config/config.example.yml @@ -391,6 +391,24 @@ https_only: false # - 192.168.1.101 # logout_url: "https://auth.example.com/logout" +## +## Playlist-backed feeds (ArikTube extension). +## +## When set, the Trending and/or Popular feed serves the content of these +## local PUBLIC playlists instead of the stock feed. Playlists merge in +## the listed order, each in its own order, duplicates dropped. The swap +## is at the data layer, so /api/v1/trending and /api/v1/popular serve +## the same items — API clients need no changes. Missing or non-public +## playlists are skipped and logged. Trending category tabs and region +## are ignored while a list is set. +## +## Default: [] (stock feeds) +## +#trending_playlists: +# - IVPLxxxxxxxxxxxxxxxxxxxx +#popular_playlists: +# - IVPLxxxxxxxxxxxxxxxxxxxx + ## ## Enable/Disable the captcha challenge on the login page. ## diff --git a/src/invidious.cr b/src/invidious.cr index 09fbb6244..60e7ab6c1 100644 --- a/src/invidious.cr +++ b/src/invidious.cr @@ -193,6 +193,11 @@ Invidious::Jobs.register Invidious::Jobs::InstanceListRefreshJob.new Invidious::Jobs.start_all def popular_videos + # ArikTube: playlist-backed Popular + unless CONFIG.popular_playlists.empty? + return Invidious::PlaylistFeeds.feed_videos(CONFIG.popular_playlists) + end + Invidious::Jobs::PullPopularVideosJob::POPULAR_VIDEOS.get end diff --git a/src/invidious/config.cr b/src/invidious/config.cr index 04ddd0cbf..7f02bb792 100644 --- a/src/invidious/config.cr +++ b/src/invidious/config.cr @@ -147,6 +147,11 @@ class Config # Subscribe to channels using PubSubHubbub (requires domain, hmac_key) property use_pubsub_feeds : Bool | Int32 = false property popular_enabled : Bool = true + # Playlist-backed feeds (ArikTube extension): when set, the Trending or + # Popular feed serves these local PUBLIC playlists (merged in order, + # duplicates dropped) instead of the stock feed content. + property trending_playlists : Array(String) = [] of String + property popular_playlists : Array(String) = [] of String property captcha_enabled : Bool = true property login_enabled : Bool = true property registration_enabled : Bool = true diff --git a/src/invidious/playlist_feeds.cr b/src/invidious/playlist_feeds.cr new file mode 100644 index 000000000..ac1d33988 --- /dev/null +++ b/src/invidious/playlist_feeds.cr @@ -0,0 +1,58 @@ +# Playlist-backed Trending/Popular feeds (ArikTube extension). +# +# On a personal instance "Trending" and "Popular" mean "what this server's +# owner pushes". When `trending_playlists` / `popular_playlists` name local +# public playlists, those feeds serve the merged playlist content instead. +# The swap happens at the data layer (fetch_trending / popular_videos), so +# the HTML views and /api/v1/trending|popular emit the same items and every +# API client shows them without client-side support. +module Invidious::PlaylistFeeds + extend self + + # Merged videos of the configured playlists: config order, each playlist + # in its own order, duplicates dropped. Missing or non-public playlists + # are skipped with a log line so one bad entry cannot break the feed. + def feed_videos(plids : Array(String)) : Array(SearchVideo) + videos = [] of SearchVideo + seen = Set(String).new + + plids.each do |plid| + playlist = Invidious::Database::Playlists.select(id: plid) + if playlist.nil? + LOGGER.warn("PlaylistFeeds: playlist #{plid} does not exist, skipping") + next + end + if playlist.privacy != PlaylistPrivacy::Public + LOGGER.warn("PlaylistFeeds: playlist #{plid} is not public, skipping") + next + end + + Invidious::Database::PlaylistVideos.select(plid, playlist.index, 0, limit: 200).each do |video| + next if seen.includes?(video.id) + seen << video.id + videos << as_search_video(video) + end + end + + videos + end + + # PlaylistVideo lacks views/description/thumbnails metadata; neutral + # defaults keep the SearchVideo JSON shape the feed endpoints promise. + private def as_search_video(video : PlaylistVideo) : SearchVideo + SearchVideo.new({ + title: video.title, + id: video.id, + author: video.author, + ucid: video.ucid, + published: video.published, + views: 0_i64, + description_html: "", + length_seconds: video.length_seconds, + premiere_timestamp: nil, + author_verified: false, + author_thumbnail: nil, + badges: video.live_now ? VideoBadges::LiveNow : VideoBadges::None, + }) + end +end diff --git a/src/invidious/trending.cr b/src/invidious/trending.cr index 622fe5172..73790aefd 100644 --- a/src/invidious/trending.cr +++ b/src/invidious/trending.cr @@ -1,4 +1,12 @@ def fetch_trending(trending_type, region, locale) + # ArikTube: playlist-backed Trending. Category tabs and region are + # ignored on purpose — the owner's list is the feed. + unless CONFIG.trending_playlists.empty? + plids = CONFIG.trending_playlists + plid = plids.size == 1 ? plids[0] : nil + return {Invidious::PlaylistFeeds.feed_videos(plids), plid} + end + region ||= "US" region = region.upcase