feat(channels): fetch channel videos via InnerTube instead of RSS

fetch_channel no longer depends on the /feeds/videos.xml endpoint, which is rate limited and periodically unavailable (see #5679, #5651, #4657). Channel metadata (author, auto-generated) now comes from get_about_info, and the first page of the videos tab is inserted directly, mirroring the existing pull_all_videos continuation loop. Channel redirects are followed explicitly.
This commit is contained in:
NDilanka 2026-08-22 08:50:42 +05:30
parent 821365cf71
commit 43936283db

View File

@ -159,36 +159,19 @@ def fetch_channel(ucid, pull_all_videos : Bool)
LOGGER.debug("fetch_channel: #{ucid}") LOGGER.debug("fetch_channel: #{ucid}")
LOGGER.trace("fetch_channel: #{ucid} : pull_all_videos = #{pull_all_videos}") LOGGER.trace("fetch_channel: #{ucid} : pull_all_videos = #{pull_all_videos}")
namespaces = { begin
"yt" => "http://www.youtube.com/xml/schemas/2015", about_channel = get_about_info(ucid)
"media" => "http://search.yahoo.com/mrss/", rescue ex : ChannelRedirect
"default" => "http://www.w3.org/2005/Atom", # Old-style UCIDs can be redirected to a new one by YouTube. The RSS
} # endpoint used to follow those redirects transparently.
about_channel = get_about_info(ex.channel_id)
LOGGER.trace("fetch_channel: #{ucid} : Downloading RSS feed")
rss = YT_POOL.client &.get("/feeds/videos.xml?channel_id=#{ucid}").body
LOGGER.trace("fetch_channel: #{ucid} : Parsing RSS feed")
rss = XML.parse(rss)
author = rss.xpath_node("//default:feed/default:title", namespaces)
if !author
raise InfoException.new("Deleted or invalid channel")
end end
author = author.content LOGGER.trace("fetch_channel: #{ucid} : author = #{about_channel.author}, auto_generated = #{about_channel.auto_generated}")
# Auto-generated channels
# https://support.google.com/youtube/answer/2579942
if author.ends_with?(" - Topic") ||
{"Popular on YouTube", "Music", "Sports", "Gaming"}.includes? author
auto_generated = true
end
LOGGER.trace("fetch_channel: #{ucid} : author = #{author}, auto_generated = #{auto_generated}")
channel = InvidiousChannel.new({ channel = InvidiousChannel.new({
id: ucid, id: ucid,
author: author, author: about_channel.author,
updated: Time.utc, updated: Time.utc,
deleted: false, deleted: false,
subscribed: nil, subscribed: nil,
@ -197,61 +180,30 @@ def fetch_channel(ucid, pull_all_videos : Bool)
LOGGER.trace("fetch_channel: #{ucid} : Downloading channel videos page") LOGGER.trace("fetch_channel: #{ucid} : Downloading channel videos page")
videos, continuation = IV::Channel::Tabs.get_videos(channel) videos, continuation = IV::Channel::Tabs.get_videos(channel)
LOGGER.trace("fetch_channel: #{ucid} : Extracting videos from channel RSS feed") LOGGER.trace("fetch_channel: #{ucid} : Extracting videos from the channel videos tab")
rss.xpath_nodes("//default:feed/default:entry", namespaces).each do |entry| videos.select(SearchVideo).each do |video|
video_id = entry.xpath_node("yt:videoId", namespaces).not_nil!.content channel_video = ChannelVideo.new({
title = entry.xpath_node("default:title", namespaces).not_nil!.content id: video.id,
title: video.title,
published = Time.parse_rfc3339( published: video.published,
entry.xpath_node("default:published", namespaces).not_nil!.content updated: Time.utc,
) ucid: video.ucid,
updated = Time.parse_rfc3339( author: video.author,
entry.xpath_node("default:updated", namespaces).not_nil!.content length_seconds: video.length_seconds,
) live_now: video.badges.live_now?,
premiere_timestamp: video.premiere_timestamp,
author = entry.xpath_node("default:author/default:name", namespaces).not_nil!.content views: video.views,
ucid = entry.xpath_node("yt:channelId", namespaces).not_nil!.content
views = entry
.xpath_node("media:group/media:community/media:statistics", namespaces)
.try &.["views"]?.try &.to_i64? || 0_i64
channel_video = videos
.select(SearchVideo)
.select(&.id.== video_id)[0]?
length_seconds = channel_video.try &.length_seconds
length_seconds ||= 0
live_now = channel_video.try &.badges.live_now?
live_now ||= false
premiere_timestamp = channel_video.try &.premiere_timestamp
video = ChannelVideo.new({
id: video_id,
title: title,
published: published,
updated: updated,
ucid: ucid,
author: author,
length_seconds: length_seconds,
live_now: live_now,
premiere_timestamp: premiere_timestamp,
views: views,
}) })
LOGGER.trace("fetch_channel: #{ucid} : video #{video_id} : Updating or inserting video") LOGGER.trace("fetch_channel: #{ucid} : video #{video.id} : Updating or inserting video")
# We don't include the 'premiere_timestamp' here because channel pages don't include them, was_insert = Invidious::Database::ChannelVideos.insert(channel_video)
# meaning the above timestamp is always null
was_insert = Invidious::Database::ChannelVideos.insert(video)
if was_insert if was_insert
LOGGER.trace("fetch_channel: #{ucid} : video #{video_id} : Inserted, updating subscriptions") LOGGER.trace("fetch_channel: #{ucid} : video #{video.id} : Inserted, updating subscriptions")
NOTIFICATION_CHANNEL.send(VideoNotification.from_video(video)) NOTIFICATION_CHANNEL.send(VideoNotification.from_video(channel_video))
else else
LOGGER.trace("fetch_channel: #{ucid} : video #{video_id} : Updated") LOGGER.trace("fetch_channel: #{ucid} : video #{video.id} : Updated")
end end
end end