mirror of
https://github.com/iv-org/invidious
synced 2024-11-27 13:33:28 +01:00
Extract feed routes (#2269)
* Extract feed routes from invidious.cr * Removes the deprecated route for /feed/top * Deprecate /view_all_playlist & use /feed/playlists * Move feed views into their own directory * Add haltf method to halt current route context * Change status_code + return blocks to use haltf * Set appropriate response headers for RSS routes
This commit is contained in:
parent
a279d6f433
commit
5005212bec
438
src/invidious.cr
438
src/invidious.cr
@ -349,7 +349,6 @@ Invidious::Routing.get "/redirect", Invidious::Routes::Misc, :cross_instance_red
|
||||
Invidious::Routing.get "/embed/", Invidious::Routes::Embed, :redirect
|
||||
Invidious::Routing.get "/embed/:id", Invidious::Routes::Embed, :show
|
||||
|
||||
Invidious::Routing.get "/view_all_playlists", Invidious::Routes::Playlists, :index
|
||||
Invidious::Routing.get "/create_playlist", Invidious::Routes::Playlists, :new
|
||||
Invidious::Routing.post "/create_playlist", Invidious::Routes::Playlists, :create
|
||||
Invidious::Routing.get "/subscribe_playlist", Invidious::Routes::Playlists, :subscribe
|
||||
@ -374,6 +373,24 @@ Invidious::Routing.get "/preferences", Invidious::Routes::PreferencesRoute, :sho
|
||||
Invidious::Routing.post "/preferences", Invidious::Routes::PreferencesRoute, :update
|
||||
Invidious::Routing.get "/toggle_theme", Invidious::Routes::PreferencesRoute, :toggle_theme
|
||||
|
||||
# Feeds
|
||||
Invidious::Routing.get "/view_all_playlists", Invidious::Routes::Feeds, :view_all_playlists_redirect
|
||||
Invidious::Routing.get "/feed/playlists", Invidious::Routes::Feeds, :playlists
|
||||
Invidious::Routing.get "/feed/popular", Invidious::Routes::Feeds, :popular
|
||||
Invidious::Routing.get "/feed/trending", Invidious::Routes::Feeds, :trending
|
||||
Invidious::Routing.get "/feed/subscriptions", Invidious::Routes::Feeds, :subscriptions
|
||||
Invidious::Routing.get "/feed/history", Invidious::Routes::Feeds, :history
|
||||
|
||||
# RSS Feeds
|
||||
Invidious::Routing.get "/feed/channel/:ucid", Invidious::Routes::Feeds, :rss_channel
|
||||
Invidious::Routing.get "/feed/private", Invidious::Routes::Feeds, :rss_private
|
||||
Invidious::Routing.get "/feed/playlist/:plid", Invidious::Routes::Feeds, :rss_playlist
|
||||
Invidious::Routing.get "/feeds/videos.xml", Invidious::Routes::Feeds, :rss_videos
|
||||
|
||||
# Support push notifications via PubSubHubbub
|
||||
Invidious::Routing.get "/feed/webhook/:token", Invidious::Routes::Feeds, :push_notifications_get
|
||||
Invidious::Routing.post "/feed/webhook/:token", Invidious::Routes::Feeds, :push_notifications_post
|
||||
|
||||
# Users
|
||||
|
||||
post "/watch_ajax" do |env|
|
||||
@ -1190,425 +1207,6 @@ post "/token_ajax" do |env|
|
||||
end
|
||||
end
|
||||
|
||||
# Feeds
|
||||
|
||||
get "/feed/playlists" do |env|
|
||||
env.redirect "/view_all_playlists"
|
||||
end
|
||||
|
||||
get "/feed/top" do |env|
|
||||
locale = LOCALES[env.get("preferences").as(Preferences).locale]?
|
||||
|
||||
message = translate(locale, "The Top feed has been removed from Invidious.")
|
||||
templated "message"
|
||||
end
|
||||
|
||||
get "/feed/popular" do |env|
|
||||
locale = LOCALES[env.get("preferences").as(Preferences).locale]?
|
||||
|
||||
if CONFIG.popular_enabled
|
||||
templated "popular"
|
||||
else
|
||||
message = translate(locale, "The Popular feed has been disabled by the administrator.")
|
||||
templated "message"
|
||||
end
|
||||
end
|
||||
|
||||
get "/feed/trending" do |env|
|
||||
locale = LOCALES[env.get("preferences").as(Preferences).locale]?
|
||||
|
||||
trending_type = env.params.query["type"]?
|
||||
trending_type ||= "Default"
|
||||
|
||||
region = env.params.query["region"]?
|
||||
region ||= "US"
|
||||
|
||||
begin
|
||||
trending, plid = fetch_trending(trending_type, region, locale)
|
||||
rescue ex
|
||||
next error_template(500, ex)
|
||||
end
|
||||
|
||||
templated "trending"
|
||||
end
|
||||
|
||||
get "/feed/subscriptions" do |env|
|
||||
locale = LOCALES[env.get("preferences").as(Preferences).locale]?
|
||||
|
||||
user = env.get? "user"
|
||||
sid = env.get? "sid"
|
||||
referer = get_referer(env)
|
||||
|
||||
if !user
|
||||
next env.redirect referer
|
||||
end
|
||||
|
||||
user = user.as(User)
|
||||
sid = sid.as(String)
|
||||
token = user.token
|
||||
|
||||
if user.preferences.unseen_only
|
||||
env.set "show_watched", true
|
||||
end
|
||||
|
||||
# Refresh account
|
||||
headers = HTTP::Headers.new
|
||||
headers["Cookie"] = env.request.headers["Cookie"]
|
||||
|
||||
if !user.password
|
||||
user, sid = get_user(sid, headers, PG_DB)
|
||||
end
|
||||
|
||||
max_results = env.params.query["max_results"]?.try &.to_i?.try &.clamp(0, MAX_ITEMS_PER_PAGE)
|
||||
max_results ||= user.preferences.max_results
|
||||
max_results ||= CONFIG.default_user_preferences.max_results
|
||||
|
||||
page = env.params.query["page"]?.try &.to_i?
|
||||
page ||= 1
|
||||
|
||||
videos, notifications = get_subscription_feed(PG_DB, user, max_results, page)
|
||||
|
||||
# "updated" here is used for delivering new notifications, so if
|
||||
# we know a user has looked at their feed e.g. in the past 10 minutes,
|
||||
# they've already seen a video posted 20 minutes ago, and don't need
|
||||
# to be notified.
|
||||
PG_DB.exec("UPDATE users SET notifications = $1, updated = $2 WHERE email = $3", [] of String, Time.utc,
|
||||
user.email)
|
||||
user.notifications = [] of String
|
||||
env.set "user", user
|
||||
|
||||
templated "subscriptions"
|
||||
end
|
||||
|
||||
get "/feed/history" do |env|
|
||||
locale = LOCALES[env.get("preferences").as(Preferences).locale]?
|
||||
|
||||
user = env.get? "user"
|
||||
referer = get_referer(env)
|
||||
|
||||
page = env.params.query["page"]?.try &.to_i?
|
||||
page ||= 1
|
||||
|
||||
if !user
|
||||
next env.redirect referer
|
||||
end
|
||||
|
||||
user = user.as(User)
|
||||
|
||||
max_results = env.params.query["max_results"]?.try &.to_i?.try &.clamp(0, MAX_ITEMS_PER_PAGE)
|
||||
max_results ||= user.preferences.max_results
|
||||
max_results ||= CONFIG.default_user_preferences.max_results
|
||||
|
||||
if user.watched[(page - 1) * max_results]?
|
||||
watched = user.watched.reverse[(page - 1) * max_results, max_results]
|
||||
end
|
||||
watched ||= [] of String
|
||||
|
||||
templated "history"
|
||||
end
|
||||
|
||||
get "/feed/channel/:ucid" do |env|
|
||||
locale = LOCALES[env.get("preferences").as(Preferences).locale]?
|
||||
|
||||
env.response.content_type = "application/atom+xml"
|
||||
|
||||
ucid = env.params.url["ucid"]
|
||||
|
||||
params = HTTP::Params.parse(env.params.query["params"]? || "")
|
||||
|
||||
begin
|
||||
channel = get_about_info(ucid, locale)
|
||||
rescue ex : ChannelRedirect
|
||||
next env.redirect env.request.resource.gsub(ucid, ex.channel_id)
|
||||
rescue ex
|
||||
next error_atom(500, ex)
|
||||
end
|
||||
|
||||
response = YT_POOL.client &.get("/feeds/videos.xml?channel_id=#{channel.ucid}")
|
||||
rss = XML.parse_html(response.body)
|
||||
|
||||
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)
|
||||
|
||||
author = entry.xpath_node("author/name").not_nil!.content
|
||||
ucid = entry.xpath_node("channelid").not_nil!.content
|
||||
description_html = entry.xpath_node("group/description").not_nil!.to_s
|
||||
views = entry.xpath_node("group/community/statistics").not_nil!.["views"].to_i64
|
||||
|
||||
SearchVideo.new({
|
||||
title: title,
|
||||
id: video_id,
|
||||
author: author,
|
||||
ucid: ucid,
|
||||
published: published,
|
||||
views: views,
|
||||
description_html: description_html,
|
||||
length_seconds: 0,
|
||||
live_now: false,
|
||||
premium: false,
|
||||
premiere_timestamp: nil,
|
||||
})
|
||||
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}")
|
||||
|
||||
xml.element("author") do
|
||||
xml.element("name") { xml.text channel.author }
|
||||
xml.element("uri") { xml.text "#{HOST_URL}/channel/#{channel.ucid}" }
|
||||
end
|
||||
|
||||
videos.each do |video|
|
||||
video.to_xml(channel.auto_generated, params, xml)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
get "/feed/private" do |env|
|
||||
locale = LOCALES[env.get("preferences").as(Preferences).locale]?
|
||||
|
||||
env.response.content_type = "application/atom+xml"
|
||||
|
||||
token = env.params.query["token"]?
|
||||
|
||||
if !token
|
||||
env.response.status_code = 403
|
||||
next
|
||||
end
|
||||
|
||||
user = PG_DB.query_one?("SELECT * FROM users WHERE token = $1", token.strip, as: User)
|
||||
if !user
|
||||
env.response.status_code = 403
|
||||
next
|
||||
end
|
||||
|
||||
max_results = env.params.query["max_results"]?.try &.to_i?.try &.clamp(0, MAX_ITEMS_PER_PAGE)
|
||||
max_results ||= user.preferences.max_results
|
||||
max_results ||= CONFIG.default_user_preferences.max_results
|
||||
|
||||
page = env.params.query["page"]?.try &.to_i?
|
||||
page ||= 1
|
||||
|
||||
params = HTTP::Params.parse(env.params.query["params"]? || "")
|
||||
|
||||
videos, notifications = get_subscription_feed(PG_DB, 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) }
|
||||
|
||||
(notifications + videos).each do |video|
|
||||
video.to_xml(locale, params, xml)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
get "/feed/playlist/:plid" do |env|
|
||||
locale = LOCALES[env.get("preferences").as(Preferences).locale]?
|
||||
|
||||
env.response.content_type = "application/atom+xml"
|
||||
|
||||
plid = env.params.url["plid"]
|
||||
|
||||
params = HTTP::Params.parse(env.params.query["params"]? || "")
|
||||
path = env.request.path
|
||||
|
||||
if plid.starts_with? "IV"
|
||||
if playlist = PG_DB.query_one?("SELECT * FROM playlists WHERE id = $1", plid, as: InvidiousPlaylist)
|
||||
videos = get_playlist_videos(PG_DB, playlist, offset: 0, locale: locale)
|
||||
|
||||
next 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}")
|
||||
|
||||
xml.element("author") do
|
||||
xml.element("name") { xml.text playlist.author }
|
||||
end
|
||||
|
||||
videos.each do |video|
|
||||
video.to_xml(false, xml)
|
||||
end
|
||||
end
|
||||
end
|
||||
else
|
||||
env.response.status_code = 404
|
||||
next
|
||||
end
|
||||
end
|
||||
|
||||
response = YT_POOL.client &.get("/feeds/videos.xml?playlist_id=#{plid}")
|
||||
document = XML.parse(response.body)
|
||||
|
||||
document.xpath_nodes(%q(//*[@href]|//*[@url])).each do |node|
|
||||
node.attributes.each do |attribute|
|
||||
case attribute.name
|
||||
when "url", "href"
|
||||
request_target = URI.parse(node[attribute.name]).request_target
|
||||
query_string_opt = request_target.starts_with?("/watch?v=") ? "&#{params}" : ""
|
||||
node[attribute.name] = "#{HOST_URL}#{request_target}#{query_string_opt}"
|
||||
else nil # Skip
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
document = document.to_xml(options: XML::SaveOptions::NO_DECL)
|
||||
|
||||
document.scan(/<uri>(?<url>[^<]+)<\/uri>/).each do |match|
|
||||
content = "#{HOST_URL}#{URI.parse(match["url"]).request_target}"
|
||||
document = document.gsub(match[0], "<uri>#{content}</uri>")
|
||||
end
|
||||
|
||||
document
|
||||
end
|
||||
|
||||
get "/feeds/videos.xml" do |env|
|
||||
if ucid = env.params.query["channel_id"]?
|
||||
env.redirect "/feed/channel/#{ucid}"
|
||||
elsif user = env.params.query["user"]?
|
||||
env.redirect "/feed/channel/#{user}"
|
||||
elsif plid = env.params.query["playlist_id"]?
|
||||
env.redirect "/feed/playlist/#{plid}"
|
||||
end
|
||||
end
|
||||
|
||||
# Support push notifications via PubSubHubbub
|
||||
|
||||
get "/feed/webhook/:token" do |env|
|
||||
verify_token = env.params.url["token"]
|
||||
|
||||
mode = env.params.query["hub.mode"]?
|
||||
topic = env.params.query["hub.topic"]?
|
||||
challenge = env.params.query["hub.challenge"]?
|
||||
|
||||
if !mode || !topic || !challenge
|
||||
env.response.status_code = 400
|
||||
next
|
||||
else
|
||||
mode = mode.not_nil!
|
||||
topic = topic.not_nil!
|
||||
challenge = challenge.not_nil!
|
||||
end
|
||||
|
||||
case verify_token
|
||||
when .starts_with? "v1"
|
||||
_, time, nonce, signature = verify_token.split(":")
|
||||
data = "#{time}:#{nonce}"
|
||||
when .starts_with? "v2"
|
||||
time, signature = verify_token.split(":")
|
||||
data = "#{time}"
|
||||
else
|
||||
env.response.status_code = 400
|
||||
next
|
||||
end
|
||||
|
||||
# The hub will sometimes check if we're still subscribed after delivery errors,
|
||||
# so we reply with a 200 as long as the request hasn't expired
|
||||
if Time.utc.to_unix - time.to_i > 432000
|
||||
env.response.status_code = 400
|
||||
next
|
||||
end
|
||||
|
||||
if OpenSSL::HMAC.hexdigest(:sha1, HMAC_KEY, data) != signature
|
||||
env.response.status_code = 400
|
||||
next
|
||||
end
|
||||
|
||||
if ucid = HTTP::Params.parse(URI.parse(topic).query.not_nil!)["channel_id"]?
|
||||
PG_DB.exec("UPDATE channels SET subscribed = $1 WHERE id = $2", Time.utc, ucid)
|
||||
elsif plid = HTTP::Params.parse(URI.parse(topic).query.not_nil!)["playlist_id"]?
|
||||
PG_DB.exec("UPDATE playlists SET subscribed = $1 WHERE id = $2", Time.utc, ucid)
|
||||
else
|
||||
env.response.status_code = 400
|
||||
next
|
||||
end
|
||||
|
||||
env.response.status_code = 200
|
||||
challenge
|
||||
end
|
||||
|
||||
post "/feed/webhook/:token" do |env|
|
||||
locale = LOCALES[env.get("preferences").as(Preferences).locale]?
|
||||
|
||||
token = env.params.url["token"]
|
||||
body = env.request.body.not_nil!.gets_to_end
|
||||
signature = env.request.headers["X-Hub-Signature"].lchop("sha1=")
|
||||
|
||||
if signature != OpenSSL::HMAC.hexdigest(:sha1, HMAC_KEY, body)
|
||||
LOGGER.error("/feed/webhook/#{token} : Invalid signature")
|
||||
env.response.status_code = 200
|
||||
next
|
||||
end
|
||||
|
||||
spawn do
|
||||
rss = XML.parse_html(body)
|
||||
rss.xpath_nodes("//feed/entry").each do |entry|
|
||||
id = entry.xpath_node("videoid").not_nil!.content
|
||||
author = entry.xpath_node("author/name").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)
|
||||
|
||||
video = get_video(id, PG_DB, force_refresh: true)
|
||||
|
||||
# Deliver notifications to `/api/v1/auth/notifications`
|
||||
payload = {
|
||||
"topic" => video.ucid,
|
||||
"videoId" => video.id,
|
||||
"published" => published.to_unix,
|
||||
}.to_json
|
||||
PG_DB.exec("NOTIFY notifications, E'#{payload}'")
|
||||
|
||||
video = ChannelVideo.new({
|
||||
id: id,
|
||||
title: video.title,
|
||||
published: published,
|
||||
updated: updated,
|
||||
ucid: video.ucid,
|
||||
author: author,
|
||||
length_seconds: video.length_seconds,
|
||||
live_now: video.live_now,
|
||||
premiere_timestamp: video.premiere_timestamp,
|
||||
views: video.views,
|
||||
})
|
||||
|
||||
was_insert = PG_DB.query_one("INSERT INTO channel_videos VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
|
||||
ON CONFLICT (id) DO UPDATE SET title = $2, published = $3,
|
||||
updated = $4, ucid = $5, author = $6, length_seconds = $7,
|
||||
live_now = $8, premiere_timestamp = $9, views = $10 returning (xmax=0) as was_insert", *video.to_tuple, as: Bool)
|
||||
|
||||
PG_DB.exec("UPDATE users SET notifications = array_append(notifications, $1),
|
||||
feed_needs_update = true WHERE $2 = ANY(subscriptions)", video.id, video.ucid) if was_insert
|
||||
end
|
||||
end
|
||||
|
||||
env.response.status_code = 200
|
||||
next
|
||||
end
|
||||
|
||||
# Channels
|
||||
|
||||
{"/channel/:ucid/live", "/user/:user/live", "/c/:user/live"}.each do |route|
|
||||
|
@ -56,3 +56,12 @@ end
|
||||
macro rendered(filename)
|
||||
render "src/invidious/views/#{{{filename}}}.ecr"
|
||||
end
|
||||
|
||||
# Similar to Kemals halt method but works in a
|
||||
# method.
|
||||
macro haltf(env, status_code = 200, response = "")
|
||||
{{env}}.response.status_code = {{status_code}}
|
||||
{{env}}.response.print {{response}}
|
||||
{{env}}.response.close
|
||||
return
|
||||
end
|
||||
|
431
src/invidious/routes/feeds.cr
Normal file
431
src/invidious/routes/feeds.cr
Normal file
@ -0,0 +1,431 @@
|
||||
module Invidious::Routes::Feeds
|
||||
def self.view_all_playlists_redirect(env)
|
||||
env.redirect "/feed/playlists"
|
||||
end
|
||||
|
||||
def self.playlists(env)
|
||||
locale = LOCALES[env.get("preferences").as(Preferences).locale]?
|
||||
|
||||
user = env.get? "user"
|
||||
referer = get_referer(env)
|
||||
|
||||
return env.redirect "/" if user.nil?
|
||||
|
||||
user = user.as(User)
|
||||
|
||||
items_created = PG_DB.query_all("SELECT * FROM playlists WHERE author = $1 AND id LIKE 'IV%' ORDER BY created", user.email, as: InvidiousPlaylist)
|
||||
items_created.map! do |item|
|
||||
item.author = ""
|
||||
item
|
||||
end
|
||||
|
||||
items_saved = PG_DB.query_all("SELECT * FROM playlists WHERE author = $1 AND id NOT LIKE 'IV%' ORDER BY created", user.email, as: InvidiousPlaylist)
|
||||
items_saved.map! do |item|
|
||||
item.author = ""
|
||||
item
|
||||
end
|
||||
|
||||
templated "feeds/playlists"
|
||||
end
|
||||
|
||||
def self.popular(env)
|
||||
locale = LOCALES[env.get("preferences").as(Preferences).locale]?
|
||||
|
||||
if CONFIG.popular_enabled
|
||||
templated "feeds/popular"
|
||||
else
|
||||
message = translate(locale, "The Popular feed has been disabled by the administrator.")
|
||||
templated "message"
|
||||
end
|
||||
end
|
||||
|
||||
def self.trending(env)
|
||||
locale = LOCALES[env.get("preferences").as(Preferences).locale]?
|
||||
|
||||
trending_type = env.params.query["type"]?
|
||||
trending_type ||= "Default"
|
||||
|
||||
region = env.params.query["region"]?
|
||||
region ||= "US"
|
||||
|
||||
begin
|
||||
trending, plid = fetch_trending(trending_type, region, locale)
|
||||
rescue ex
|
||||
return error_template(500, ex)
|
||||
end
|
||||
|
||||
templated "feeds/trending"
|
||||
end
|
||||
|
||||
def self.subscriptions(env)
|
||||
locale = LOCALES[env.get("preferences").as(Preferences).locale]?
|
||||
|
||||
user = env.get? "user"
|
||||
sid = env.get? "sid"
|
||||
referer = get_referer(env)
|
||||
|
||||
if !user
|
||||
return env.redirect referer
|
||||
end
|
||||
|
||||
user = user.as(User)
|
||||
sid = sid.as(String)
|
||||
token = user.token
|
||||
|
||||
if user.preferences.unseen_only
|
||||
env.set "show_watched", true
|
||||
end
|
||||
|
||||
# Refresh account
|
||||
headers = HTTP::Headers.new
|
||||
headers["Cookie"] = env.request.headers["Cookie"]
|
||||
|
||||
if !user.password
|
||||
user, sid = get_user(sid, headers, PG_DB)
|
||||
end
|
||||
|
||||
max_results = env.params.query["max_results"]?.try &.to_i?.try &.clamp(0, MAX_ITEMS_PER_PAGE)
|
||||
max_results ||= user.preferences.max_results
|
||||
max_results ||= CONFIG.default_user_preferences.max_results
|
||||
|
||||
page = env.params.query["page"]?.try &.to_i?
|
||||
page ||= 1
|
||||
|
||||
videos, notifications = get_subscription_feed(PG_DB, user, max_results, page)
|
||||
|
||||
# "updated" here is used for delivering new notifications, so if
|
||||
# we know a user has looked at their feed e.g. in the past 10 minutes,
|
||||
# they've already seen a video posted 20 minutes ago, and don't need
|
||||
# to be notified.
|
||||
PG_DB.exec("UPDATE users SET notifications = $1, updated = $2 WHERE email = $3", [] of String, Time.utc,
|
||||
user.email)
|
||||
user.notifications = [] of String
|
||||
env.set "user", user
|
||||
|
||||
templated "feeds/subscriptions"
|
||||
end
|
||||
|
||||
def self.history(env)
|
||||
locale = LOCALES[env.get("preferences").as(Preferences).locale]?
|
||||
|
||||
user = env.get? "user"
|
||||
referer = get_referer(env)
|
||||
|
||||
page = env.params.query["page"]?.try &.to_i?
|
||||
page ||= 1
|
||||
|
||||
if !user
|
||||
return env.redirect referer
|
||||
end
|
||||
|
||||
user = user.as(User)
|
||||
|
||||
max_results = env.params.query["max_results"]?.try &.to_i?.try &.clamp(0, MAX_ITEMS_PER_PAGE)
|
||||
max_results ||= user.preferences.max_results
|
||||
max_results ||= CONFIG.default_user_preferences.max_results
|
||||
|
||||
if user.watched[(page - 1) * max_results]?
|
||||
watched = user.watched.reverse[(page - 1) * max_results, max_results]
|
||||
end
|
||||
watched ||= [] of String
|
||||
|
||||
templated "feeds/history"
|
||||
end
|
||||
|
||||
# RSS feeds
|
||||
|
||||
def self.rss_channel(env)
|
||||
locale = LOCALES[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"]
|
||||
|
||||
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
|
||||
return error_atom(500, ex)
|
||||
end
|
||||
|
||||
response = YT_POOL.client &.get("/feeds/videos.xml?channel_id=#{channel.ucid}")
|
||||
rss = XML.parse_html(response.body)
|
||||
|
||||
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)
|
||||
|
||||
author = entry.xpath_node("author/name").not_nil!.content
|
||||
ucid = entry.xpath_node("channelid").not_nil!.content
|
||||
description_html = entry.xpath_node("group/description").not_nil!.to_s
|
||||
views = entry.xpath_node("group/community/statistics").not_nil!.["views"].to_i64
|
||||
|
||||
SearchVideo.new({
|
||||
title: title,
|
||||
id: video_id,
|
||||
author: author,
|
||||
ucid: ucid,
|
||||
published: published,
|
||||
views: views,
|
||||
description_html: description_html,
|
||||
length_seconds: 0,
|
||||
live_now: false,
|
||||
paid: false,
|
||||
premium: false,
|
||||
premiere_timestamp: nil,
|
||||
})
|
||||
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}")
|
||||
|
||||
xml.element("author") do
|
||||
xml.element("name") { xml.text channel.author }
|
||||
xml.element("uri") { xml.text "#{HOST_URL}/channel/#{channel.ucid}" }
|
||||
end
|
||||
|
||||
videos.each do |video|
|
||||
video.to_xml(channel.auto_generated, params, xml)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def self.rss_private(env)
|
||||
locale = LOCALES[env.get("preferences").as(Preferences).locale]?
|
||||
|
||||
env.response.headers["Content-Type"] = "application/atom+xml"
|
||||
env.response.content_type = "application/atom+xml"
|
||||
|
||||
token = env.params.query["token"]?
|
||||
|
||||
if !token
|
||||
haltf env, status_code: 403
|
||||
end
|
||||
|
||||
user = PG_DB.query_one?("SELECT * FROM users WHERE token = $1", token.strip, as: User)
|
||||
if !user
|
||||
haltf env, status_code: 403
|
||||
end
|
||||
|
||||
max_results = env.params.query["max_results"]?.try &.to_i?.try &.clamp(0, MAX_ITEMS_PER_PAGE)
|
||||
max_results ||= user.preferences.max_results
|
||||
max_results ||= CONFIG.default_user_preferences.max_results
|
||||
|
||||
page = env.params.query["page"]?.try &.to_i?
|
||||
page ||= 1
|
||||
|
||||
params = HTTP::Params.parse(env.params.query["params"]? || "")
|
||||
|
||||
videos, notifications = get_subscription_feed(PG_DB, 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) }
|
||||
|
||||
(notifications + videos).each do |video|
|
||||
video.to_xml(locale, params, xml)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def self.rss_playlist(env)
|
||||
locale = LOCALES[env.get("preferences").as(Preferences).locale]?
|
||||
|
||||
env.response.headers["Content-Type"] = "application/atom+xml"
|
||||
env.response.content_type = "application/atom+xml"
|
||||
|
||||
plid = env.params.url["plid"]
|
||||
|
||||
params = HTTP::Params.parse(env.params.query["params"]? || "")
|
||||
path = env.request.path
|
||||
|
||||
if plid.starts_with? "IV"
|
||||
if playlist = PG_DB.query_one?("SELECT * FROM playlists WHERE id = $1", plid, as: InvidiousPlaylist)
|
||||
videos = get_playlist_videos(PG_DB, playlist, offset: 0, locale: locale)
|
||||
|
||||
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}")
|
||||
|
||||
xml.element("author") do
|
||||
xml.element("name") { xml.text playlist.author }
|
||||
end
|
||||
|
||||
videos.each do |video|
|
||||
video.to_xml(false, xml)
|
||||
end
|
||||
end
|
||||
end
|
||||
else
|
||||
haltf env, status_code: 404
|
||||
end
|
||||
end
|
||||
|
||||
response = YT_POOL.client &.get("/feeds/videos.xml?playlist_id=#{plid}")
|
||||
document = XML.parse(response.body)
|
||||
|
||||
document.xpath_nodes(%q(//*[@href]|//*[@url])).each do |node|
|
||||
node.attributes.each do |attribute|
|
||||
case attribute.name
|
||||
when "url", "href"
|
||||
request_target = URI.parse(node[attribute.name]).request_target
|
||||
query_string_opt = request_target.starts_with?("/watch?v=") ? "&#{params}" : ""
|
||||
node[attribute.name] = "#{HOST_URL}#{request_target}#{query_string_opt}"
|
||||
else nil # Skip
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
document = document.to_xml(options: XML::SaveOptions::NO_DECL)
|
||||
|
||||
document.scan(/<uri>(?<url>[^<]+)<\/uri>/).each do |match|
|
||||
content = "#{HOST_URL}#{URI.parse(match["url"]).request_target}"
|
||||
document = document.gsub(match[0], "<uri>#{content}</uri>")
|
||||
end
|
||||
document
|
||||
end
|
||||
|
||||
def self.rss_videos(env)
|
||||
if ucid = env.params.query["channel_id"]?
|
||||
env.redirect "/feed/channel/#{ucid}"
|
||||
elsif user = env.params.query["user"]?
|
||||
env.redirect "/feed/channel/#{user}"
|
||||
elsif plid = env.params.query["playlist_id"]?
|
||||
env.redirect "/feed/playlist/#{plid}"
|
||||
end
|
||||
end
|
||||
|
||||
# Push notifications via PubSub
|
||||
|
||||
def self.push_notifications_get(env)
|
||||
verify_token = env.params.url["token"]
|
||||
|
||||
mode = env.params.query["hub.mode"]?
|
||||
topic = env.params.query["hub.topic"]?
|
||||
challenge = env.params.query["hub.challenge"]?
|
||||
|
||||
if !mode || !topic || !challenge
|
||||
haltf env, status_code: 400
|
||||
else
|
||||
mode = mode.not_nil!
|
||||
topic = topic.not_nil!
|
||||
challenge = challenge.not_nil!
|
||||
end
|
||||
|
||||
case verify_token
|
||||
when .starts_with? "v1"
|
||||
_, time, nonce, signature = verify_token.split(":")
|
||||
data = "#{time}:#{nonce}"
|
||||
when .starts_with? "v2"
|
||||
time, signature = verify_token.split(":")
|
||||
data = "#{time}"
|
||||
else
|
||||
haltf env, status_code: 400
|
||||
end
|
||||
|
||||
# The hub will sometimes check if we're still subscribed after delivery errors,
|
||||
# so we reply with a 200 as long as the request hasn't expired
|
||||
if Time.utc.to_unix - time.to_i > 432000
|
||||
haltf env, status_code: 400
|
||||
end
|
||||
|
||||
if OpenSSL::HMAC.hexdigest(:sha1, HMAC_KEY, data) != signature
|
||||
haltf env, status_code: 400
|
||||
end
|
||||
|
||||
if ucid = HTTP::Params.parse(URI.parse(topic).query.not_nil!)["channel_id"]?
|
||||
PG_DB.exec("UPDATE channels SET subscribed = $1 WHERE id = $2", Time.utc, ucid)
|
||||
elsif plid = HTTP::Params.parse(URI.parse(topic).query.not_nil!)["playlist_id"]?
|
||||
PG_DB.exec("UPDATE playlists SET subscribed = $1 WHERE id = $2", Time.utc, ucid)
|
||||
else
|
||||
haltf env, status_code: 400
|
||||
end
|
||||
|
||||
env.response.status_code = 200
|
||||
challenge
|
||||
end
|
||||
|
||||
def self.push_notifications_post(env)
|
||||
locale = LOCALES[env.get("preferences").as(Preferences).locale]?
|
||||
|
||||
token = env.params.url["token"]
|
||||
body = env.request.body.not_nil!.gets_to_end
|
||||
signature = env.request.headers["X-Hub-Signature"].lchop("sha1=")
|
||||
|
||||
if signature != OpenSSL::HMAC.hexdigest(:sha1, HMAC_KEY, body)
|
||||
LOGGER.error("/feed/webhook/#{token} : Invalid signature")
|
||||
haltf env, status_code: 200
|
||||
end
|
||||
|
||||
spawn do
|
||||
rss = XML.parse_html(body)
|
||||
rss.xpath_nodes("//feed/entry").each do |entry|
|
||||
id = entry.xpath_node("videoid").not_nil!.content
|
||||
author = entry.xpath_node("author/name").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)
|
||||
|
||||
video = get_video(id, PG_DB, force_refresh: true)
|
||||
|
||||
# Deliver notifications to `/api/v1/auth/notifications`
|
||||
payload = {
|
||||
"topic" => video.ucid,
|
||||
"videoId" => video.id,
|
||||
"published" => published.to_unix,
|
||||
}.to_json
|
||||
PG_DB.exec("NOTIFY notifications, E'#{payload}'")
|
||||
|
||||
video = ChannelVideo.new({
|
||||
id: id,
|
||||
title: video.title,
|
||||
published: published,
|
||||
updated: updated,
|
||||
ucid: video.ucid,
|
||||
author: author,
|
||||
length_seconds: video.length_seconds,
|
||||
live_now: video.live_now,
|
||||
premiere_timestamp: video.premiere_timestamp,
|
||||
views: video.views,
|
||||
})
|
||||
|
||||
was_insert = PG_DB.query_one("INSERT INTO channel_videos VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
|
||||
ON CONFLICT (id) DO UPDATE SET title = $2, published = $3,
|
||||
updated = $4, ucid = $5, author = $6, length_seconds = $7,
|
||||
live_now = $8, premiere_timestamp = $9, views = $10 returning (xmax=0) as was_insert", *video.to_tuple, as: Bool)
|
||||
|
||||
PG_DB.exec("UPDATE users SET notifications = array_append(notifications, $1),
|
||||
feed_needs_update = true WHERE $2 = ANY(subscriptions)", video.id, video.ucid) if was_insert
|
||||
end
|
||||
end
|
||||
|
||||
env.response.status_code = 200
|
||||
end
|
||||
end
|
@ -17,7 +17,7 @@ module Invidious::Routes::Misc
|
||||
end
|
||||
when "Playlists"
|
||||
if user
|
||||
env.redirect "/view_all_playlists"
|
||||
env.redirect "/feed/playlists"
|
||||
else
|
||||
env.redirect "/feed/popular"
|
||||
end
|
||||
|
@ -1,29 +1,4 @@
|
||||
module Invidious::Routes::Playlists
|
||||
def self.index(env)
|
||||
locale = LOCALES[env.get("preferences").as(Preferences).locale]?
|
||||
|
||||
user = env.get? "user"
|
||||
referer = get_referer(env)
|
||||
|
||||
return env.redirect "/" if user.nil?
|
||||
|
||||
user = user.as(User)
|
||||
|
||||
items_created = PG_DB.query_all("SELECT * FROM playlists WHERE author = $1 AND id LIKE 'IV%' ORDER BY created", user.email, as: InvidiousPlaylist)
|
||||
items_created.map! do |item|
|
||||
item.author = ""
|
||||
item
|
||||
end
|
||||
|
||||
items_saved = PG_DB.query_all("SELECT * FROM playlists WHERE author = $1 AND id NOT LIKE 'IV%' ORDER BY created", user.email, as: InvidiousPlaylist)
|
||||
items_saved.map! do |item|
|
||||
item.author = ""
|
||||
item
|
||||
end
|
||||
|
||||
templated "view_all_playlists"
|
||||
end
|
||||
|
||||
def self.new(env)
|
||||
locale = LOCALES[env.get("preferences").as(Preferences).locale]?
|
||||
|
||||
@ -148,7 +123,7 @@ module Invidious::Routes::Playlists
|
||||
PG_DB.exec("DELETE FROM playlist_videos * WHERE plid = $1", plid)
|
||||
PG_DB.exec("DELETE FROM playlists * WHERE id = $1", plid)
|
||||
|
||||
env.redirect "/view_all_playlists"
|
||||
env.redirect "/feed/playlists"
|
||||
end
|
||||
|
||||
def self.edit(env)
|
||||
|
@ -12,7 +12,7 @@
|
||||
<% if playlist.is_a? InvidiousPlaylist %>
|
||||
<b>
|
||||
<% if playlist.author == user.try &.email %>
|
||||
<a href="/view_all_playlists"><%= author %></a> |
|
||||
<a href="/feed/playlists"><%= author %></a> |
|
||||
<% else %>
|
||||
<%= author %> |
|
||||
<% end %>
|
||||
|
@ -312,7 +312,7 @@
|
||||
</div>
|
||||
|
||||
<div class="pure-control-group">
|
||||
<a href="/view_all_playlists"><%= translate(locale, "View all playlists") %></a>
|
||||
<a href="/feed/playlists"><%= translate(locale, "View all playlists") %></a>
|
||||
</div>
|
||||
|
||||
<div class="pure-control-group">
|
||||
|
Loading…
Reference in New Issue
Block a user