Merge remote-tracking branch 'upstream/master'

This commit is contained in:
Fijxu
2025-02-27 02:02:47 -03:00
54 changed files with 1157 additions and 152 deletions

View File

@@ -368,6 +368,35 @@ module Invidious::Routes::API::V1::Channels
end
end
def self.courses(env)
locale = env.get("preferences").as(Preferences).locale
env.response.content_type = "application/json"
ucid = env.params.url["ucid"]
continuation = env.params.query["continuation"]?
# Use the macro defined above
channel = nil # Make the compiler happy
get_channel()
items, next_continuation = fetch_channel_courses(channel.ucid, channel.author, continuation)
JSON.build do |json|
json.object do
json.field "playlists" do
json.array do
items.each do |item|
item.to_json(locale, json) if item.is_a?(SearchPlaylist)
end
end
end
json.field "continuation", next_continuation if next_continuation
end
end
end
def self.community(env)
locale = env.get("preferences").as(Preferences).locale

View File

@@ -429,4 +429,90 @@ module Invidious::Routes::API::V1::Videos
end
end
end
# Fetches transcripts from YouTube
#
# Use the `lang` and `autogen` query parameter to select which transcript to fetch
# Request without any URL parameters to see all the available transcripts.
def self.transcripts(env)
env.response.content_type = "application/json"
id = env.params.url["id"]
lang = env.params.query["lang"]?
label = env.params.query["label"]?
auto_generated = env.params.query["autogen"]? ? true : false
# Return all available transcript options when none is given
if !label && !lang
begin
video = get_video(id)
rescue ex : NotFoundException
return error_json(404, ex)
rescue ex
return error_json(500, ex)
end
response = JSON.build do |json|
# The amount of transcripts available to fetch is the
# same as the amount of captions available.
available_transcripts = video.captions
json.object do
json.field "transcripts" do
json.array do
available_transcripts.each do |transcript|
json.object do
json.field "label", transcript.name
json.field "languageCode", transcript.language_code
json.field "autoGenerated", transcript.auto_generated
if transcript.auto_generated
json.field "url", "/api/v1/transcripts/#{id}?lang=#{URI.encode_www_form(transcript.language_code)}&autogen"
else
json.field "url", "/api/v1/transcripts/#{id}?lang=#{URI.encode_www_form(transcript.language_code)}"
end
end
end
end
end
end
end
return response
end
# If lang is not given then we attempt to fetch
# the transcript through the given label
if lang.nil?
begin
video = get_video(id)
rescue ex : NotFoundException
return error_json(404, ex)
rescue ex
return error_json(500, ex)
end
target_transcript = video.captions.select(&.name.== label)
if target_transcript.empty?
return error_json(404, NotFoundException.new("Requested transcript does not exist"))
else
target_transcript = target_transcript[0]
lang, auto_generated = target_transcript.language_code, target_transcript.auto_generated
end
end
params = Invidious::Videos::Transcript.generate_param(id, lang, auto_generated)
begin
transcript = Invidious::Videos::Transcript.from_raw(
YoutubeAPI.get_transcript(params), lang, auto_generated
)
rescue ex : NotFoundException
return error_json(404, ex)
rescue ex
return error_json(500, ex)
end
return transcript.to_json
end
end

View File

@@ -197,6 +197,26 @@ module Invidious::Routes::Channels
templated "channel"
end
def self.courses(env)
data = self.fetch_basic_information(env)
return data if !data.is_a?(Tuple)
locale, user, subscriptions, continuation, ucid, channel = data
sort_by = ""
sort_options = [] of String
items, next_continuation = fetch_channel_courses(
channel.ucid, channel.author, continuation
)
items = items.select(SearchPlaylist)
items.each(&.author = "")
selected_tab = Frontend::ChannelPage::TabsAvailable::Courses
templated "channel"
end
def self.community(env)
return env.redirect env.request.path.sub("posts", "community") if env.request.path.split("/").last == "posts"
@@ -309,7 +329,7 @@ module Invidious::Routes::Channels
private KNOWN_TABS = {
"home", "videos", "shorts", "streams", "podcasts",
"releases", "playlists", "community", "channels", "about",
"releases", "courses", "playlists", "community", "channels", "about",
"posts",
}

View File

@@ -143,32 +143,25 @@ module Invidious::Routes::Feeds
# RSS feeds
def self.rss_channel(env)
locale = env.get("preferences").as(Preferences).locale
env.response.headers["Content-Type"] = "application/atom+xml"
env.response.content_type = "application/atom+xml"
ucid = env.params.url["ucid"]
if env.params.url["ucid"].matches?(/^[\w-]+$/)
ucid = env.params.url["ucid"]
else
return error_atom(400, InfoException.new("Invalid channel ucid provided."))
end
params = HTTP::Params.parse(env.params.query["params"]? || "")
begin
channel = get_about_info(ucid, locale)
rescue ex : ChannelRedirect
return env.redirect env.request.resource.gsub(ucid, ex.channel_id)
rescue ex : NotFoundException
return error_atom(404, ex)
rescue ex
return error_atom(500, ex)
end
namespaces = {
"yt" => "http://www.youtube.com/xml/schemas/2015",
"media" => "http://search.yahoo.com/mrss/",
"default" => "http://www.w3.org/2005/Atom",
}
response = YT_POOL.client &.get("/feeds/videos.xml?channel_id=#{channel.ucid}")
response = YT_POOL.client &.get("/feeds/videos.xml?channel_id=#{ucid}")
return error_atom(404, NotFoundException.new("Channel does not exist.")) if response.status_code == 404
rss = XML.parse(response.body)
videos = rss.xpath_nodes("//default:feed/default:entry", namespaces).map do |entry|
@@ -179,7 +172,7 @@ module Invidious::Routes::Feeds
updated = Time.parse_rfc3339(entry.xpath_node("default:updated", namespaces).not_nil!.content)
author = entry.xpath_node("default:author/default:name", namespaces).not_nil!.content
ucid = entry.xpath_node("yt:channelId", namespaces).not_nil!.content
video_ucid = entry.xpath_node("yt:channelId", namespaces).not_nil!.content
description_html = entry.xpath_node("media:group/media:description", namespaces).not_nil!.to_s
views = entry.xpath_node("media:group/media:community/media:statistics", namespaces).not_nil!.["views"].to_i64
@@ -187,7 +180,7 @@ module Invidious::Routes::Feeds
title: title,
id: video_id,
author: author,
ucid: ucid,
ucid: video_ucid,
published: published,
views: views,
description_html: description_html,
@@ -199,30 +192,32 @@ module Invidious::Routes::Feeds
})
end
author = ""
author = videos[0].author if videos.size > 0
XML.build(indent: " ", encoding: "UTF-8") do |xml|
xml.element("feed", "xmlns:yt": "http://www.youtube.com/xml/schemas/2015",
"xmlns:media": "http://search.yahoo.com/mrss/", xmlns: "http://www.w3.org/2005/Atom",
"xml:lang": "en-US") do
xml.element("link", rel: "self", href: "#{HOST_URL}#{env.request.resource}")
xml.element("id") { xml.text "yt:channel:#{channel.ucid}" }
xml.element("yt:channelId") { xml.text channel.ucid }
xml.element("icon") { xml.text channel.author_thumbnail }
xml.element("title") { xml.text channel.author }
xml.element("link", rel: "alternate", href: "#{HOST_URL}/channel/#{channel.ucid}")
xml.element("id") { xml.text "yt:channel:#{ucid}" }
xml.element("yt:channelId") { xml.text ucid }
xml.element("title") { author }
xml.element("link", rel: "alternate", href: "#{HOST_URL}/channel/#{ucid}")
xml.element("author") do
xml.element("name") { xml.text channel.author }
xml.element("uri") { xml.text "#{HOST_URL}/channel/#{channel.ucid}" }
xml.element("name") { xml.text author }
xml.element("uri") { xml.text "#{HOST_URL}/channel/#{ucid}" }
end
xml.element("image") do
xml.element("url") { xml.text channel.author_thumbnail }
xml.element("title") { xml.text channel.author }
xml.element("url") { xml.text "" }
xml.element("title") { xml.text author }
xml.element("link", rel: "self", href: "#{HOST_URL}#{env.request.resource}")
end
videos.each do |video|
video.to_xml(channel.auto_generated, params, xml)
video.to_xml(false, params, xml)
end
end
end
@@ -310,8 +305,9 @@ module Invidious::Routes::Feeds
end
response = YT_POOL.client &.get("/feeds/videos.xml?playlist_id=#{plid}")
document = XML.parse(response.body)
return error_atom(404, NotFoundException.new("Playlist does not exist.")) if response.status_code == 404
document = XML.parse(response.body)
document.xpath_nodes(%q(//*[@href]|//*[@url])).each do |node|
node.attributes.each do |attribute|
case attribute.name
@@ -428,16 +424,6 @@ module Invidious::Routes::Feeds
end
end
if CONFIG.enable_user_notifications
# Deliver notifications to `/api/v1/auth/notifications`
payload = {
"topic" => ucid,
"videoId" => id,
"published" => published.to_unix,
}.to_json
PG_DB.exec("NOTIFY notifications, E'#{payload}'")
end
video = ChannelVideo.new({
id: id,
title: title,
@@ -453,11 +439,7 @@ module Invidious::Routes::Feeds
was_insert = Invidious::Database::ChannelVideos.insert(video, with_premiere_timestamp: true)
if was_insert
if CONFIG.enable_user_notifications
Invidious::Database::Users.add_notification(video)
# else
# Invidious::Database::Users.feed_needs_update(video)
end
NOTIFICATION_CHANNEL.send(VideoNotification.from_video(video))
end
end
end

View File

@@ -48,12 +48,17 @@ module Invidious::Routes::Misc
referer = get_referer(env)
instance_list = Invidious::Jobs::InstanceListRefreshJob::INSTANCES["INSTANCES"]
if instance_list.empty?
# Filter out the current instance
other_available_instances = instance_list.reject { |_, domain| domain == CONFIG.domain }
if other_available_instances.empty?
# If the current instance is the only one, use the redirect URL as fallback
instance_url = "redirect.invidious.io"
else
# Select other random instance
# Sample returns an array
# Instances are packaged as {region, domain} in the instance list
instance_url = instance_list.sample(1)[0][1]
instance_url = other_available_instances.sample(1)[0][1]
end
env.redirect "https://#{instance_url}#{referer}"