This commit is contained in:
Samantaz Fox 2023-06-07 14:41:58 +02:00 committed by GitHub
commit 2d1fdb9ce4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 214 additions and 181 deletions

View file

@ -50,46 +50,6 @@ struct ChannelVideo
end
end
def to_xml(locale, query_params, xml : XML::Builder)
query_params["v"] = self.id
xml.element("entry") do
xml.element("id") { xml.text "yt:video:#{self.id}" }
xml.element("yt:videoId") { xml.text self.id }
xml.element("yt:channelId") { xml.text self.ucid }
xml.element("title") { xml.text self.title }
xml.element("link", rel: "alternate", href: "#{HOST_URL}/watch?#{query_params}")
xml.element("author") do
xml.element("name") { xml.text self.author }
xml.element("uri") { xml.text "#{HOST_URL}/channel/#{self.ucid}" }
end
xml.element("content", type: "xhtml") do
xml.element("div", xmlns: "http://www.w3.org/1999/xhtml") do
xml.element("a", href: "#{HOST_URL}/watch?#{query_params}") do
xml.element("img", src: "#{HOST_URL}/vi/#{self.id}/mqdefault.jpg")
end
end
end
xml.element("published") { xml.text self.published.to_s("%Y-%m-%dT%H:%M:%S%:z") }
xml.element("updated") { xml.text self.updated.to_s("%Y-%m-%dT%H:%M:%S%:z") }
xml.element("media:group") do
xml.element("media:title") { xml.text self.title }
xml.element("media:thumbnail", url: "#{HOST_URL}/vi/#{self.id}/mqdefault.jpg",
width: "320", height: "180")
end
end
end
def to_xml(locale, _xml : Nil = nil)
XML.build do |xml|
to_xml(locale, xml)
end
end
def to_tuple
{% begin %}
{

View file

@ -14,57 +14,6 @@ struct SearchVideo
property premiere_timestamp : Time?
property author_verified : Bool
def to_xml(auto_generated, query_params, xml : XML::Builder)
query_params["v"] = self.id
xml.element("entry") do
xml.element("id") { xml.text "yt:video:#{self.id}" }
xml.element("yt:videoId") { xml.text self.id }
xml.element("yt:channelId") { xml.text self.ucid }
xml.element("title") { xml.text self.title }
xml.element("link", rel: "alternate", href: "#{HOST_URL}/watch?#{query_params}")
xml.element("author") do
if auto_generated
xml.element("name") { xml.text self.author }
xml.element("uri") { xml.text "#{HOST_URL}/channel/#{self.ucid}" }
else
xml.element("name") { xml.text author }
xml.element("uri") { xml.text "#{HOST_URL}/channel/#{ucid}" }
end
end
xml.element("content", type: "xhtml") do
xml.element("div", xmlns: "http://www.w3.org/1999/xhtml") do
xml.element("a", href: "#{HOST_URL}/watch?#{query_params}") do
xml.element("img", src: "#{HOST_URL}/vi/#{self.id}/mqdefault.jpg")
end
xml.element("p", style: "word-break:break-word;white-space:pre-wrap") { xml.text html_to_content(self.description_html) }
end
end
xml.element("published") { xml.text self.published.to_s("%Y-%m-%dT%H:%M:%S%:z") }
xml.element("media:group") do
xml.element("media:title") { xml.text self.title }
xml.element("media:thumbnail", url: "#{HOST_URL}/vi/#{self.id}/mqdefault.jpg",
width: "320", height: "180")
xml.element("media:description") { xml.text html_to_content(self.description_html) }
end
xml.element("media:community") do
xml.element("media:statistics", views: self.views)
end
end
end
def to_xml(auto_generated, query_params, _xml : Nil)
XML.build do |xml|
to_xml(auto_generated, query_params, xml)
end
end
def to_json(locale : String?, json : JSON::Builder)
json.object do
json.field "type", "video"

View file

@ -11,41 +11,6 @@ struct PlaylistVideo
property index : Int64
property live_now : Bool
def to_xml(xml : XML::Builder)
xml.element("entry") do
xml.element("id") { xml.text "yt:video:#{self.id}" }
xml.element("yt:videoId") { xml.text self.id }
xml.element("yt:channelId") { xml.text self.ucid }
xml.element("title") { xml.text self.title }
xml.element("link", rel: "alternate", href: "#{HOST_URL}/watch?v=#{self.id}")
xml.element("author") do
xml.element("name") { xml.text self.author }
xml.element("uri") { xml.text "#{HOST_URL}/channel/#{self.ucid}" }
end
xml.element("content", type: "xhtml") do
xml.element("div", xmlns: "http://www.w3.org/1999/xhtml") do
xml.element("a", href: "#{HOST_URL}/watch?v=#{self.id}") do
xml.element("img", src: "#{HOST_URL}/vi/#{self.id}/mqdefault.jpg")
end
end
end
xml.element("published") { xml.text self.published.to_s("%Y-%m-%dT%H:%M:%S%:z") }
xml.element("media:group") do
xml.element("media:title") { xml.text self.title }
xml.element("media:thumbnail", url: "#{HOST_URL}/vi/#{self.id}/mqdefault.jpg",
width: "320", height: "180")
end
end
end
def to_xml(_xml : Nil = nil)
XML.build { |xml| to_xml(xml) }
end
def to_json(json : JSON::Builder, index : Int32? = nil)
json.object do
json.field "title", self.title

View file

@ -146,8 +146,6 @@ module Invidious::Routes::Feeds
ucid = env.params.url["ucid"]
params = HTTP::Params.parse(env.params.query["params"]? || "")
begin
channel = get_about_info(ucid, locale)
rescue ex : ChannelRedirect
@ -161,12 +159,13 @@ module Invidious::Routes::Feeds
response = YT_POOL.client &.get("/feeds/videos.xml?channel_id=#{channel.ucid}")
rss = XML.parse_html(response.body)
updated = [] of String
videos = rss.xpath_nodes("//feed/entry").map do |entry|
video_id = entry.xpath_node("videoid").not_nil!.content
title = entry.xpath_node("title").not_nil!.content
published = Time.parse_rfc3339(entry.xpath_node("published").not_nil!.content)
updated = Time.parse_rfc3339(entry.xpath_node("updated").not_nil!.content)
updated << entry.xpath_node("updated").not_nil!.content
author = entry.xpath_node("author/name").not_nil!.content
ucid = entry.xpath_node("channelid").not_nil!.content
@ -190,33 +189,27 @@ module Invidious::Routes::Feeds
})
end
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}")
feed_created = rss.xpath_node("//feed/published").not_nil!.content
feed_updated = updated.size > 0 ? updated[0] : feed_created
xml.element("author") do
xml.element("name") { xml.text channel.author }
xml.element("uri") { xml.text "#{HOST_URL}/channel/#{channel.ucid}" }
end
# Generate Atom feed
xml.element("image") do
xml.element("url") { xml.text channel.author_thumbnail }
xml.element("title") { xml.text channel.author }
xml.element("link", rel: "self", href: "#{HOST_URL}#{env.request.resource}")
end
properties = Invidious::RssAtom::AtomProperties.new(
title: "", # Use channel author name
icon_url: channel.author_thumbnail,
author: channel.author,
author_url: "#{HOST_URL}/channel/#{channel.ucid}",
date_updated: feed_updated,
date_published: feed_created,
alt_links: [{
type: "text/html",
url: "#{HOST_URL}/channel/#{channel.ucid}",
}]
)
videos.each do |video|
video.to_xml(channel.auto_generated, params, xml)
end
end
end
return Invidious::RssAtom.atom_feed_builder(
env, videos, "channel/#{channel.ucid}", properties
)
end
def self.rss_private(env)
@ -247,20 +240,20 @@ module Invidious::Routes::Feeds
videos, notifications = get_subscription_feed(user, max_results, page)
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", "type": "text/html", rel: "alternate", href: "#{HOST_URL}/feed/subscriptions")
xml.element("link", "type": "application/atom+xml", rel: "self",
href: "#{HOST_URL}#{env.request.resource}")
xml.element("title") { xml.text translate(locale, "Invidious Private Feed for `x`", user.email) }
# Generate Atom feed
(notifications + videos).each do |video|
video.to_xml(locale, params, xml)
end
end
end
properties = Invidious::RssAtom::AtomProperties.new(
title: translate(locale, "Invidious Private Feed for `x`", user.email),
author: user.email,
alt_links: [{
type: "text/html",
url: "#{HOST_URL}/feed/subscriptions",
}]
)
return Invidious::RssAtom.atom_feed_builder(
env, (notifications + videos), "subscriptions/#{user.email}", properties
)
end
def self.rss_playlist(env)
@ -278,23 +271,20 @@ module Invidious::Routes::Feeds
if playlist = Invidious::Database::Playlists.select(id: plid)
videos = get_playlist_videos(playlist, offset: 0)
return 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 "iv:playlist:#{plid}" }
xml.element("iv:playlistId") { xml.text plid }
xml.element("title") { xml.text playlist.title }
xml.element("link", rel: "alternate", href: "#{HOST_URL}/playlist?list=#{plid}")
# Generate Atom feed
xml.element("author") do
xml.element("name") { xml.text playlist.author }
end
properties = Invidious::RssAtom::AtomProperties.new(
title: playlist.title,
author: playlist.author,
alt_links: [{
type: "text/html",
url: "#{HOST_URL}/playlist?list=#{plid}",
}]
)
videos.each &.to_xml(xml)
end
end
return Invidious::RssAtom.atom_feed_builder(
env, videos, "playlist/#{plid}", properties
)
else
haltf env, status_code: 404
end

169
src/invidious/rss_atom.cr Normal file
View file

@ -0,0 +1,169 @@
require "xml"
require "http/server"
module Invidious::RssAtom
extend self
# TODO: Merge all of those in a single type
alias AnyVideo = SearchVideo | ChannelVideo | PlaylistVideo
#
# Feed properties structure
#
alias AltLink = NamedTuple(type: String, url: String)
struct AtomProperties
getter title : String
getter icon_url : String
getter author : String
getter author_url : String
getter date_published : String
getter date_updated : String
getter alt_links : Array(AltLink)
def initialize(
*, # All parameters must be named
@title = "", @icon_url = "",
@author = "", @author_url = "",
date_updated : Time | String = Time.utc,
date_published : Time | String = "",
@alt_links = [] of AltLink
)
# Convert publication date if needed
if date_published.is_a?(Time)
@date_published = date_published.to_rfc3339
else
@date_published = date_published
end
# Convert update date if needed
if date_updated.is_a?(Time)
@date_updated = date_updated.to_rfc3339
else
@date_updated = date_updated
end
end
end
#
# Atom Feed builder
#
def atom_feed_builder(
# Mandatory parameters
env : HTTP::Server::Context,
videos : Array(AnyVideo),
id : String,
properties : AtomProperties
)
return XML.build(indent: " ", encoding: "UTF-8") do |xml|
xml.element("feed",
xmlns: "http://www.w3.org/2005/Atom",
"xmlns:media": "http://search.yahoo.com/mrss/",
"xml:lang": "en-US"
) do
# The id must be unique, and an IANA-approved IRI, so use "ni://"
# Relevant RFC documents:
# - https://datatracker.ietf.org/doc/html/rfc4287#section-4.2.6
# - https://datatracker.ietf.org/doc/html/rfc6920
#
xml.element("id") { xml.text "ni://invidious/sha-256;" + sha256(id) }
# Feed title. Use author name if no title was provided
xml.element("title") do
xml.text(properties.title.empty? ? properties.author : properties.title)
end
if !properties.icon_url.empty?
icon_url = "#{HOST_URL}/gghpt/#{URI.parse(properties.icon_url).request_target}"
xml.element("icon") { xml.text icon_url }
xml.element("logo") { xml.text icon_url }
end
# Feed creation (if available) and update (mandatory) dates
if !properties.date_published.empty?
xml.element("published") { xml.text properties.date_published }
end
xml.element("updated") { xml.text properties.date_updated }
# Links
xml.element("link", rel: "self",
type: "application/atom+xml",
href: "#{HOST_URL}#{env.request.resource}"
)
properties.alt_links.each do |link|
xml.element("link", rel: "alternate", type: link[:type], href: link[:url])
end
# Author infos
xml.element("author") do
xml.element("name") { xml.text properties.author }
xml.element("uri") { xml.text properties.author_url } if !properties.author_url.empty?
end
# Video entries
videos.each do |video|
xml.element("entry") { atom_video(xml, video, env.params.query) }
end
end
end
end
def atom_video(xml : XML::Builder, video : AnyVideo, query_params : HTTP::Params)
# URLs that are reused below
video_url = "#{HOST_URL}/watch?v=#{video.id}&#{query_params}"
video_thumb = "#{HOST_URL}/vi/#{video.id}/mqdefault.jpg"
description = video.is_a?(SearchVideo) ? video.description_html.gsub('\n', "<br/>\n") : ""
xml.element("id") { xml.text "ni://invidious/sha-256;" + sha256("video/#{video.id}") }
xml.element("title") { xml.text video.title }
xml.element("link", rel: "alternate", href: video_url)
xml.element("author") do
xml.element("name") { xml.text video.author }
xml.element("uri") { xml.text "#{HOST_URL}/channel/#{video.ucid}" }
end
xml.element("content", type: "xhtml") do
xml.element("div", xmlns: "http://www.w3.org/1999/xhtml") do
# Link to video
xml.element("a", href: video_url) do
xml.element("img", src: video_thumb)
end
# Video sescription (SearchVideo only)
if video.is_a?(SearchVideo)
xml.element("p", style: "white-space:pre-wrap") { xml.text description }
end
end
end
# Feed creation (if available) and update (ChannelVideo only) dates
xml.element("published") { xml.text video.published.to_rfc3339 }
xml.element("updated") { xml.text video.updated.to_rfc3339 } if video.is_a?(ChannelVideo)
# Media properties
xml.element("media:group") do
xml.element("media:title") { xml.text video.title }
xml.element("media:thumbnail", url: video_thumb, width: "320", height: "180")
# Video sescription (SearchVideo only)
if video.is_a?(SearchVideo)
xml.element("media:description") { xml.text description }
end
end
# Views count (all except PlaylistVideo)
if !video.is_a?(PlaylistVideo)
xml.element("media:community") do
xml.element("media:statistics", views: video.views)
end
end
end
end