Extract API routes from invidious.cr (3/3)
- Auth (excluding notifications*) APIs - Mixes *Notifications currently require the "connection_channel" channel for talking with the notifications job. Unfortunately, we cannot access that within the route modules yet.
This commit is contained in:
parent
b3426fdc94
commit
39b34eece8
4 changed files with 573 additions and 527 deletions
529
src/invidious.cr
529
src/invidious.cr
|
@ -1639,132 +1639,12 @@ end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# API Endpoints
|
|
||||||
|
|
||||||
{"/api/v1/playlists/:plid", "/api/v1/auth/playlists/:plid"}.each do |route|
|
|
||||||
get route do |env|
|
|
||||||
locale = LOCALES[env.get("preferences").as(Preferences).locale]?
|
|
||||||
|
|
||||||
env.response.content_type = "application/json"
|
|
||||||
plid = env.params.url["plid"]
|
|
||||||
|
|
||||||
offset = env.params.query["index"]?.try &.to_i?
|
|
||||||
offset ||= env.params.query["page"]?.try &.to_i?.try { |page| (page - 1) * 100 }
|
|
||||||
offset ||= 0
|
|
||||||
|
|
||||||
continuation = env.params.query["continuation"]?
|
|
||||||
|
|
||||||
format = env.params.query["format"]?
|
|
||||||
format ||= "json"
|
|
||||||
|
|
||||||
if plid.starts_with? "RD"
|
|
||||||
next env.redirect "/api/v1/mixes/#{plid}"
|
|
||||||
end
|
|
||||||
|
|
||||||
begin
|
|
||||||
playlist = get_playlist(PG_DB, plid, locale)
|
|
||||||
rescue ex : InfoException
|
|
||||||
next error_json(404, ex)
|
|
||||||
rescue ex
|
|
||||||
next error_json(404, "Playlist does not exist.")
|
|
||||||
end
|
|
||||||
|
|
||||||
user = env.get?("user").try &.as(User)
|
|
||||||
if !playlist || playlist.privacy.private? && playlist.author != user.try &.email
|
|
||||||
next error_json(404, "Playlist does not exist.")
|
|
||||||
end
|
|
||||||
|
|
||||||
response = playlist.to_json(offset, locale, continuation: continuation)
|
|
||||||
|
|
||||||
if format == "html"
|
|
||||||
response = JSON.parse(response)
|
|
||||||
playlist_html = template_playlist(response)
|
|
||||||
index, next_video = response["videos"].as_a.skip(1).select { |video| !video["author"].as_s.empty? }[0]?.try { |v| {v["index"], v["videoId"]} } || {nil, nil}
|
|
||||||
|
|
||||||
response = {
|
|
||||||
"playlistHtml" => playlist_html,
|
|
||||||
"index" => index,
|
|
||||||
"nextVideo" => next_video,
|
|
||||||
}.to_json
|
|
||||||
end
|
|
||||||
|
|
||||||
response
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
get "/api/v1/mixes/:rdid" do |env|
|
|
||||||
locale = LOCALES[env.get("preferences").as(Preferences).locale]?
|
|
||||||
|
|
||||||
env.response.content_type = "application/json"
|
|
||||||
|
|
||||||
rdid = env.params.url["rdid"]
|
|
||||||
|
|
||||||
continuation = env.params.query["continuation"]?
|
|
||||||
continuation ||= rdid.lchop("RD")[0, 11]
|
|
||||||
|
|
||||||
format = env.params.query["format"]?
|
|
||||||
format ||= "json"
|
|
||||||
|
|
||||||
begin
|
|
||||||
mix = fetch_mix(rdid, continuation, locale: locale)
|
|
||||||
|
|
||||||
if !rdid.ends_with? continuation
|
|
||||||
mix = fetch_mix(rdid, mix.videos[1].id)
|
|
||||||
index = mix.videos.index(mix.videos.select { |video| video.id == continuation }[0]?)
|
|
||||||
end
|
|
||||||
|
|
||||||
mix.videos = mix.videos[index..-1]
|
|
||||||
rescue ex
|
|
||||||
next error_json(500, ex)
|
|
||||||
end
|
|
||||||
|
|
||||||
response = JSON.build do |json|
|
|
||||||
json.object do
|
|
||||||
json.field "title", mix.title
|
|
||||||
json.field "mixId", mix.id
|
|
||||||
|
|
||||||
json.field "videos" do
|
|
||||||
json.array do
|
|
||||||
mix.videos.each do |video|
|
|
||||||
json.object do
|
|
||||||
json.field "title", video.title
|
|
||||||
json.field "videoId", video.id
|
|
||||||
json.field "author", video.author
|
|
||||||
|
|
||||||
json.field "authorId", video.ucid
|
|
||||||
json.field "authorUrl", "/channel/#{video.ucid}"
|
|
||||||
|
|
||||||
json.field "videoThumbnails" do
|
|
||||||
json.array do
|
|
||||||
generate_thumbnails(json, video.id)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
json.field "index", video.index
|
|
||||||
json.field "lengthSeconds", video.length_seconds
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
if format == "html"
|
|
||||||
response = JSON.parse(response)
|
|
||||||
playlist_html = template_mix(response)
|
|
||||||
next_video = response["videos"].as_a.select { |video| !video["author"].as_s.empty? }[0]?.try &.["videoId"]
|
|
||||||
|
|
||||||
response = {
|
|
||||||
"playlistHtml" => playlist_html,
|
|
||||||
"nextVideo" => next_video,
|
|
||||||
}.to_json
|
|
||||||
end
|
|
||||||
|
|
||||||
response
|
|
||||||
end
|
|
||||||
|
|
||||||
# Authenticated endpoints
|
# Authenticated endpoints
|
||||||
|
|
||||||
|
# The notification APIs can't be extracted yet
|
||||||
|
# due to the requirement of the `connection_channel`
|
||||||
|
# used by the `NotificationJob`
|
||||||
|
|
||||||
get "/api/v1/auth/notifications" do |env|
|
get "/api/v1/auth/notifications" do |env|
|
||||||
env.response.content_type = "text/event-stream"
|
env.response.content_type = "text/event-stream"
|
||||||
|
|
||||||
|
@ -1783,407 +1663,6 @@ post "/api/v1/auth/notifications" do |env|
|
||||||
create_notification_stream(env, topics, connection_channel)
|
create_notification_stream(env, topics, connection_channel)
|
||||||
end
|
end
|
||||||
|
|
||||||
get "/api/v1/auth/preferences" do |env|
|
|
||||||
env.response.content_type = "application/json"
|
|
||||||
user = env.get("user").as(User)
|
|
||||||
user.preferences.to_json
|
|
||||||
end
|
|
||||||
|
|
||||||
post "/api/v1/auth/preferences" do |env|
|
|
||||||
env.response.content_type = "application/json"
|
|
||||||
user = env.get("user").as(User)
|
|
||||||
|
|
||||||
begin
|
|
||||||
preferences = Preferences.from_json(env.request.body || "{}")
|
|
||||||
rescue
|
|
||||||
preferences = user.preferences
|
|
||||||
end
|
|
||||||
|
|
||||||
PG_DB.exec("UPDATE users SET preferences = $1 WHERE email = $2", preferences.to_json, user.email)
|
|
||||||
|
|
||||||
env.response.status_code = 204
|
|
||||||
end
|
|
||||||
|
|
||||||
get "/api/v1/auth/feed" do |env|
|
|
||||||
env.response.content_type = "application/json"
|
|
||||||
|
|
||||||
user = env.get("user").as(User)
|
|
||||||
locale = LOCALES[env.get("preferences").as(Preferences).locale]?
|
|
||||||
|
|
||||||
max_results = env.params.query["max_results"]?.try &.to_i?
|
|
||||||
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)
|
|
||||||
|
|
||||||
JSON.build do |json|
|
|
||||||
json.object do
|
|
||||||
json.field "notifications" do
|
|
||||||
json.array do
|
|
||||||
notifications.each do |video|
|
|
||||||
video.to_json(locale, json)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
json.field "videos" do
|
|
||||||
json.array do
|
|
||||||
videos.each do |video|
|
|
||||||
video.to_json(locale, json)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
get "/api/v1/auth/subscriptions" do |env|
|
|
||||||
env.response.content_type = "application/json"
|
|
||||||
user = env.get("user").as(User)
|
|
||||||
|
|
||||||
if user.subscriptions.empty?
|
|
||||||
values = "'{}'"
|
|
||||||
else
|
|
||||||
values = "VALUES #{user.subscriptions.map { |id| %(('#{id}')) }.join(",")}"
|
|
||||||
end
|
|
||||||
|
|
||||||
subscriptions = PG_DB.query_all("SELECT * FROM channels WHERE id = ANY(#{values})", as: InvidiousChannel)
|
|
||||||
|
|
||||||
JSON.build do |json|
|
|
||||||
json.array do
|
|
||||||
subscriptions.each do |subscription|
|
|
||||||
json.object do
|
|
||||||
json.field "author", subscription.author
|
|
||||||
json.field "authorId", subscription.id
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
post "/api/v1/auth/subscriptions/:ucid" do |env|
|
|
||||||
env.response.content_type = "application/json"
|
|
||||||
user = env.get("user").as(User)
|
|
||||||
|
|
||||||
ucid = env.params.url["ucid"]
|
|
||||||
|
|
||||||
if !user.subscriptions.includes? ucid
|
|
||||||
get_channel(ucid, PG_DB, false, false)
|
|
||||||
PG_DB.exec("UPDATE users SET feed_needs_update = true, subscriptions = array_append(subscriptions,$1) WHERE email = $2", ucid, user.email)
|
|
||||||
end
|
|
||||||
|
|
||||||
# For Google accounts, access tokens don't have enough information to
|
|
||||||
# make a request on the user's behalf, which is why we don't sync with
|
|
||||||
# YouTube.
|
|
||||||
|
|
||||||
env.response.status_code = 204
|
|
||||||
end
|
|
||||||
|
|
||||||
delete "/api/v1/auth/subscriptions/:ucid" do |env|
|
|
||||||
env.response.content_type = "application/json"
|
|
||||||
user = env.get("user").as(User)
|
|
||||||
|
|
||||||
ucid = env.params.url["ucid"]
|
|
||||||
|
|
||||||
PG_DB.exec("UPDATE users SET feed_needs_update = true, subscriptions = array_remove(subscriptions, $1) WHERE email = $2", ucid, user.email)
|
|
||||||
|
|
||||||
env.response.status_code = 204
|
|
||||||
end
|
|
||||||
|
|
||||||
get "/api/v1/auth/playlists" do |env|
|
|
||||||
locale = LOCALES[env.get("preferences").as(Preferences).locale]?
|
|
||||||
|
|
||||||
env.response.content_type = "application/json"
|
|
||||||
user = env.get("user").as(User)
|
|
||||||
|
|
||||||
playlists = PG_DB.query_all("SELECT * FROM playlists WHERE author = $1", user.email, as: InvidiousPlaylist)
|
|
||||||
|
|
||||||
JSON.build do |json|
|
|
||||||
json.array do
|
|
||||||
playlists.each do |playlist|
|
|
||||||
playlist.to_json(0, locale, json)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
post "/api/v1/auth/playlists" do |env|
|
|
||||||
env.response.content_type = "application/json"
|
|
||||||
user = env.get("user").as(User)
|
|
||||||
locale = LOCALES[env.get("preferences").as(Preferences).locale]?
|
|
||||||
|
|
||||||
title = env.params.json["title"]?.try &.as(String).delete("<>").byte_slice(0, 150)
|
|
||||||
if !title
|
|
||||||
next error_json(400, "Invalid title.")
|
|
||||||
end
|
|
||||||
|
|
||||||
privacy = env.params.json["privacy"]?.try { |privacy| PlaylistPrivacy.parse(privacy.as(String).downcase) }
|
|
||||||
if !privacy
|
|
||||||
next error_json(400, "Invalid privacy setting.")
|
|
||||||
end
|
|
||||||
|
|
||||||
if PG_DB.query_one("SELECT count(*) FROM playlists WHERE author = $1", user.email, as: Int64) >= 100
|
|
||||||
next error_json(400, "User cannot have more than 100 playlists.")
|
|
||||||
end
|
|
||||||
|
|
||||||
playlist = create_playlist(PG_DB, title, privacy, user)
|
|
||||||
env.response.headers["Location"] = "#{HOST_URL}/api/v1/auth/playlists/#{playlist.id}"
|
|
||||||
env.response.status_code = 201
|
|
||||||
{
|
|
||||||
"title" => title,
|
|
||||||
"playlistId" => playlist.id,
|
|
||||||
}.to_json
|
|
||||||
end
|
|
||||||
|
|
||||||
patch "/api/v1/auth/playlists/:plid" do |env|
|
|
||||||
locale = LOCALES[env.get("preferences").as(Preferences).locale]?
|
|
||||||
|
|
||||||
env.response.content_type = "application/json"
|
|
||||||
user = env.get("user").as(User)
|
|
||||||
|
|
||||||
plid = env.params.url["plid"]
|
|
||||||
|
|
||||||
playlist = PG_DB.query_one?("SELECT * FROM playlists WHERE id = $1", plid, as: InvidiousPlaylist)
|
|
||||||
if !playlist || playlist.author != user.email && playlist.privacy.private?
|
|
||||||
next error_json(404, "Playlist does not exist.")
|
|
||||||
end
|
|
||||||
|
|
||||||
if playlist.author != user.email
|
|
||||||
next error_json(403, "Invalid user")
|
|
||||||
end
|
|
||||||
|
|
||||||
title = env.params.json["title"].try &.as(String).delete("<>").byte_slice(0, 150) || playlist.title
|
|
||||||
privacy = env.params.json["privacy"]?.try { |privacy| PlaylistPrivacy.parse(privacy.as(String).downcase) } || playlist.privacy
|
|
||||||
description = env.params.json["description"]?.try &.as(String).delete("\r") || playlist.description
|
|
||||||
|
|
||||||
if title != playlist.title ||
|
|
||||||
privacy != playlist.privacy ||
|
|
||||||
description != playlist.description
|
|
||||||
updated = Time.utc
|
|
||||||
else
|
|
||||||
updated = playlist.updated
|
|
||||||
end
|
|
||||||
|
|
||||||
PG_DB.exec("UPDATE playlists SET title = $1, privacy = $2, description = $3, updated = $4 WHERE id = $5", title, privacy, description, updated, plid)
|
|
||||||
env.response.status_code = 204
|
|
||||||
end
|
|
||||||
|
|
||||||
delete "/api/v1/auth/playlists/:plid" do |env|
|
|
||||||
locale = LOCALES[env.get("preferences").as(Preferences).locale]?
|
|
||||||
|
|
||||||
env.response.content_type = "application/json"
|
|
||||||
user = env.get("user").as(User)
|
|
||||||
|
|
||||||
plid = env.params.url["plid"]
|
|
||||||
|
|
||||||
playlist = PG_DB.query_one?("SELECT * FROM playlists WHERE id = $1", plid, as: InvidiousPlaylist)
|
|
||||||
if !playlist || playlist.author != user.email && playlist.privacy.private?
|
|
||||||
next error_json(404, "Playlist does not exist.")
|
|
||||||
end
|
|
||||||
|
|
||||||
if playlist.author != user.email
|
|
||||||
next error_json(403, "Invalid user")
|
|
||||||
end
|
|
||||||
|
|
||||||
PG_DB.exec("DELETE FROM playlist_videos * WHERE plid = $1", plid)
|
|
||||||
PG_DB.exec("DELETE FROM playlists * WHERE id = $1", plid)
|
|
||||||
|
|
||||||
env.response.status_code = 204
|
|
||||||
end
|
|
||||||
|
|
||||||
post "/api/v1/auth/playlists/:plid/videos" do |env|
|
|
||||||
locale = LOCALES[env.get("preferences").as(Preferences).locale]?
|
|
||||||
|
|
||||||
env.response.content_type = "application/json"
|
|
||||||
user = env.get("user").as(User)
|
|
||||||
|
|
||||||
plid = env.params.url["plid"]
|
|
||||||
|
|
||||||
playlist = PG_DB.query_one?("SELECT * FROM playlists WHERE id = $1", plid, as: InvidiousPlaylist)
|
|
||||||
if !playlist || playlist.author != user.email && playlist.privacy.private?
|
|
||||||
next error_json(404, "Playlist does not exist.")
|
|
||||||
end
|
|
||||||
|
|
||||||
if playlist.author != user.email
|
|
||||||
next error_json(403, "Invalid user")
|
|
||||||
end
|
|
||||||
|
|
||||||
if playlist.index.size >= 500
|
|
||||||
next error_json(400, "Playlist cannot have more than 500 videos")
|
|
||||||
end
|
|
||||||
|
|
||||||
video_id = env.params.json["videoId"].try &.as(String)
|
|
||||||
if !video_id
|
|
||||||
next error_json(403, "Invalid videoId")
|
|
||||||
end
|
|
||||||
|
|
||||||
begin
|
|
||||||
video = get_video(video_id, PG_DB)
|
|
||||||
rescue ex
|
|
||||||
next error_json(500, ex)
|
|
||||||
end
|
|
||||||
|
|
||||||
playlist_video = PlaylistVideo.new({
|
|
||||||
title: video.title,
|
|
||||||
id: video.id,
|
|
||||||
author: video.author,
|
|
||||||
ucid: video.ucid,
|
|
||||||
length_seconds: video.length_seconds,
|
|
||||||
published: video.published,
|
|
||||||
plid: plid,
|
|
||||||
live_now: video.live_now,
|
|
||||||
index: Random::Secure.rand(0_i64..Int64::MAX),
|
|
||||||
})
|
|
||||||
|
|
||||||
video_array = playlist_video.to_a
|
|
||||||
args = arg_array(video_array)
|
|
||||||
|
|
||||||
PG_DB.exec("INSERT INTO playlist_videos VALUES (#{args})", args: video_array)
|
|
||||||
PG_DB.exec("UPDATE playlists SET index = array_append(index, $1), video_count = cardinality(index) + 1, updated = $2 WHERE id = $3", playlist_video.index, Time.utc, plid)
|
|
||||||
|
|
||||||
env.response.headers["Location"] = "#{HOST_URL}/api/v1/auth/playlists/#{plid}/videos/#{playlist_video.index.to_u64.to_s(16).upcase}"
|
|
||||||
env.response.status_code = 201
|
|
||||||
playlist_video.to_json(locale, index: playlist.index.size)
|
|
||||||
end
|
|
||||||
|
|
||||||
delete "/api/v1/auth/playlists/:plid/videos/:index" do |env|
|
|
||||||
locale = LOCALES[env.get("preferences").as(Preferences).locale]?
|
|
||||||
|
|
||||||
env.response.content_type = "application/json"
|
|
||||||
user = env.get("user").as(User)
|
|
||||||
|
|
||||||
plid = env.params.url["plid"]
|
|
||||||
index = env.params.url["index"].to_i64(16)
|
|
||||||
|
|
||||||
playlist = PG_DB.query_one?("SELECT * FROM playlists WHERE id = $1", plid, as: InvidiousPlaylist)
|
|
||||||
if !playlist || playlist.author != user.email && playlist.privacy.private?
|
|
||||||
next error_json(404, "Playlist does not exist.")
|
|
||||||
end
|
|
||||||
|
|
||||||
if playlist.author != user.email
|
|
||||||
next error_json(403, "Invalid user")
|
|
||||||
end
|
|
||||||
|
|
||||||
if !playlist.index.includes? index
|
|
||||||
next error_json(404, "Playlist does not contain index")
|
|
||||||
end
|
|
||||||
|
|
||||||
PG_DB.exec("DELETE FROM playlist_videos * WHERE index = $1", index)
|
|
||||||
PG_DB.exec("UPDATE playlists SET index = array_remove(index, $1), video_count = cardinality(index) - 1, updated = $2 WHERE id = $3", index, Time.utc, plid)
|
|
||||||
|
|
||||||
env.response.status_code = 204
|
|
||||||
end
|
|
||||||
|
|
||||||
# patch "/api/v1/auth/playlists/:plid/videos/:index" do |env|
|
|
||||||
# TODO: Playlist stub
|
|
||||||
# end
|
|
||||||
|
|
||||||
get "/api/v1/auth/tokens" do |env|
|
|
||||||
env.response.content_type = "application/json"
|
|
||||||
user = env.get("user").as(User)
|
|
||||||
scopes = env.get("scopes").as(Array(String))
|
|
||||||
|
|
||||||
tokens = PG_DB.query_all("SELECT id, issued FROM session_ids WHERE email = $1", user.email, as: {session: String, issued: Time})
|
|
||||||
|
|
||||||
JSON.build do |json|
|
|
||||||
json.array do
|
|
||||||
tokens.each do |token|
|
|
||||||
json.object do
|
|
||||||
json.field "session", token[:session]
|
|
||||||
json.field "issued", token[:issued].to_unix
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
post "/api/v1/auth/tokens/register" do |env|
|
|
||||||
user = env.get("user").as(User)
|
|
||||||
locale = LOCALES[env.get("preferences").as(Preferences).locale]?
|
|
||||||
|
|
||||||
case env.request.headers["Content-Type"]?
|
|
||||||
when "application/x-www-form-urlencoded"
|
|
||||||
scopes = env.params.body.select { |k, v| k.match(/^scopes\[\d+\]$/) }.map { |k, v| v }
|
|
||||||
callback_url = env.params.body["callbackUrl"]?
|
|
||||||
expire = env.params.body["expire"]?.try &.to_i?
|
|
||||||
when "application/json"
|
|
||||||
scopes = env.params.json["scopes"].as(Array).map { |v| v.as_s }
|
|
||||||
callback_url = env.params.json["callbackUrl"]?.try &.as(String)
|
|
||||||
expire = env.params.json["expire"]?.try &.as(Int64)
|
|
||||||
else
|
|
||||||
next error_json(400, "Invalid or missing header 'Content-Type'")
|
|
||||||
end
|
|
||||||
|
|
||||||
if callback_url && callback_url.empty?
|
|
||||||
callback_url = nil
|
|
||||||
end
|
|
||||||
|
|
||||||
if callback_url
|
|
||||||
callback_url = URI.parse(callback_url)
|
|
||||||
end
|
|
||||||
|
|
||||||
if sid = env.get?("sid").try &.as(String)
|
|
||||||
env.response.content_type = "text/html"
|
|
||||||
|
|
||||||
csrf_token = generate_response(sid, {":authorize_token"}, HMAC_KEY, PG_DB, use_nonce: true)
|
|
||||||
next templated "authorize_token"
|
|
||||||
else
|
|
||||||
env.response.content_type = "application/json"
|
|
||||||
|
|
||||||
superset_scopes = env.get("scopes").as(Array(String))
|
|
||||||
|
|
||||||
authorized_scopes = [] of String
|
|
||||||
scopes.each do |scope|
|
|
||||||
if scopes_include_scope(superset_scopes, scope)
|
|
||||||
authorized_scopes << scope
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
access_token = generate_token(user.email, authorized_scopes, expire, HMAC_KEY, PG_DB)
|
|
||||||
|
|
||||||
if callback_url
|
|
||||||
access_token = URI.encode_www_form(access_token)
|
|
||||||
|
|
||||||
if query = callback_url.query
|
|
||||||
query = HTTP::Params.parse(query.not_nil!)
|
|
||||||
else
|
|
||||||
query = HTTP::Params.new
|
|
||||||
end
|
|
||||||
|
|
||||||
query["token"] = access_token
|
|
||||||
callback_url.query = query.to_s
|
|
||||||
|
|
||||||
env.redirect callback_url.to_s
|
|
||||||
else
|
|
||||||
access_token
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
post "/api/v1/auth/tokens/unregister" do |env|
|
|
||||||
locale = LOCALES[env.get("preferences").as(Preferences).locale]?
|
|
||||||
env.response.content_type = "application/json"
|
|
||||||
user = env.get("user").as(User)
|
|
||||||
scopes = env.get("scopes").as(Array(String))
|
|
||||||
|
|
||||||
session = env.params.json["session"]?.try &.as(String)
|
|
||||||
session ||= env.get("session").as(String)
|
|
||||||
|
|
||||||
# Allow tokens to revoke other tokens with correct scope
|
|
||||||
if session == env.get("session").as(String)
|
|
||||||
PG_DB.exec("DELETE FROM session_ids * WHERE id = $1", session)
|
|
||||||
elsif scopes_include_scope(scopes, "GET:tokens")
|
|
||||||
PG_DB.exec("DELETE FROM session_ids * WHERE id = $1", session)
|
|
||||||
else
|
|
||||||
next error_json(400, "Cannot revoke session #{session}")
|
|
||||||
end
|
|
||||||
|
|
||||||
env.response.status_code = 204
|
|
||||||
end
|
|
||||||
|
|
||||||
get "/ggpht/*" do |env|
|
get "/ggpht/*" do |env|
|
||||||
url = env.request.path.lchop("/ggpht")
|
url = env.request.path.lchop("/ggpht")
|
||||||
|
|
||||||
|
|
412
src/invidious/routes/api/v1/authenticated.cr
Normal file
412
src/invidious/routes/api/v1/authenticated.cr
Normal file
|
@ -0,0 +1,412 @@
|
||||||
|
module Invidious::Routes::APIv1::Authenticated
|
||||||
|
# def self.notifications(env)
|
||||||
|
# env.response.content_type = "text/event-stream"
|
||||||
|
|
||||||
|
# topics = env.params.body["topics"]?.try &.split(",").uniq.first(1000)
|
||||||
|
# topics ||= [] of String
|
||||||
|
|
||||||
|
# create_notification_stream(env, topics, connection_channel)
|
||||||
|
# end
|
||||||
|
|
||||||
|
def self.get_preferences(env)
|
||||||
|
env.response.content_type = "application/json"
|
||||||
|
user = env.get("user").as(User)
|
||||||
|
user.preferences.to_json
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.set_preferences(env)
|
||||||
|
env.response.content_type = "application/json"
|
||||||
|
user = env.get("user").as(User)
|
||||||
|
|
||||||
|
begin
|
||||||
|
preferences = Preferences.from_json(env.request.body || "{}")
|
||||||
|
rescue
|
||||||
|
preferences = user.preferences
|
||||||
|
end
|
||||||
|
|
||||||
|
PG_DB.exec("UPDATE users SET preferences = $1 WHERE email = $2", preferences.to_json, user.email)
|
||||||
|
|
||||||
|
env.response.status_code = 204
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.feed(env)
|
||||||
|
env.response.content_type = "application/json"
|
||||||
|
|
||||||
|
user = env.get("user").as(User)
|
||||||
|
locale = LOCALES[env.get("preferences").as(Preferences).locale]?
|
||||||
|
|
||||||
|
max_results = env.params.query["max_results"]?.try &.to_i?
|
||||||
|
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)
|
||||||
|
|
||||||
|
JSON.build do |json|
|
||||||
|
json.object do
|
||||||
|
json.field "notifications" do
|
||||||
|
json.array do
|
||||||
|
notifications.each do |video|
|
||||||
|
video.to_json(locale, json)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
json.field "videos" do
|
||||||
|
json.array do
|
||||||
|
videos.each do |video|
|
||||||
|
video.to_json(locale, json)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.get_subscriptions(env)
|
||||||
|
env.response.content_type = "application/json"
|
||||||
|
user = env.get("user").as(User)
|
||||||
|
|
||||||
|
if user.subscriptions.empty?
|
||||||
|
values = "'{}'"
|
||||||
|
else
|
||||||
|
values = "VALUES #{user.subscriptions.map { |id| %(('#{id}')) }.join(",")}"
|
||||||
|
end
|
||||||
|
|
||||||
|
subscriptions = PG_DB.query_all("SELECT * FROM channels WHERE id = ANY(#{values})", as: InvidiousChannel)
|
||||||
|
|
||||||
|
JSON.build do |json|
|
||||||
|
json.array do
|
||||||
|
subscriptions.each do |subscription|
|
||||||
|
json.object do
|
||||||
|
json.field "author", subscription.author
|
||||||
|
json.field "authorId", subscription.id
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.subscribe_channel(env)
|
||||||
|
env.response.content_type = "application/json"
|
||||||
|
user = env.get("user").as(User)
|
||||||
|
|
||||||
|
ucid = env.params.url["ucid"]
|
||||||
|
|
||||||
|
if !user.subscriptions.includes? ucid
|
||||||
|
get_channel(ucid, PG_DB, false, false)
|
||||||
|
PG_DB.exec("UPDATE users SET feed_needs_update = true, subscriptions = array_append(subscriptions,$1) WHERE email = $2", ucid, user.email)
|
||||||
|
end
|
||||||
|
|
||||||
|
# For Google accounts, access tokens don't have enough information to
|
||||||
|
# make a request on the user's behalf, which is why we don't sync with
|
||||||
|
# YouTube.
|
||||||
|
|
||||||
|
env.response.status_code = 204
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.unsubscribe_channel(env)
|
||||||
|
env.response.content_type = "application/json"
|
||||||
|
user = env.get("user").as(User)
|
||||||
|
|
||||||
|
ucid = env.params.url["ucid"]
|
||||||
|
|
||||||
|
PG_DB.exec("UPDATE users SET feed_needs_update = true, subscriptions = array_remove(subscriptions, $1) WHERE email = $2", ucid, user.email)
|
||||||
|
|
||||||
|
env.response.status_code = 204
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.list_playlists(env)
|
||||||
|
locale = LOCALES[env.get("preferences").as(Preferences).locale]?
|
||||||
|
|
||||||
|
env.response.content_type = "application/json"
|
||||||
|
user = env.get("user").as(User)
|
||||||
|
|
||||||
|
playlists = PG_DB.query_all("SELECT * FROM playlists WHERE author = $1", user.email, as: InvidiousPlaylist)
|
||||||
|
|
||||||
|
JSON.build do |json|
|
||||||
|
json.array do
|
||||||
|
playlists.each do |playlist|
|
||||||
|
playlist.to_json(0, locale, json)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.create_playlist(env)
|
||||||
|
env.response.content_type = "application/json"
|
||||||
|
user = env.get("user").as(User)
|
||||||
|
locale = LOCALES[env.get("preferences").as(Preferences).locale]?
|
||||||
|
|
||||||
|
title = env.params.json["title"]?.try &.as(String).delete("<>").byte_slice(0, 150)
|
||||||
|
if !title
|
||||||
|
return error_json(400, "Invalid title.")
|
||||||
|
end
|
||||||
|
|
||||||
|
privacy = env.params.json["privacy"]?.try { |privacy| PlaylistPrivacy.parse(privacy.as(String).downcase) }
|
||||||
|
if !privacy
|
||||||
|
return error_json(400, "Invalid privacy setting.")
|
||||||
|
end
|
||||||
|
|
||||||
|
if PG_DB.query_one("SELECT count(*) FROM playlists WHERE author = $1", user.email, as: Int64) >= 100
|
||||||
|
return error_json(400, "User cannot have more than 100 playlists.")
|
||||||
|
end
|
||||||
|
|
||||||
|
playlist = create_playlist(PG_DB, title, privacy, user)
|
||||||
|
env.response.headers["Location"] = "#{HOST_URL}/api/v1/auth/playlists/#{playlist.id}"
|
||||||
|
env.response.status_code = 201
|
||||||
|
{
|
||||||
|
"title" => title,
|
||||||
|
"playlistId" => playlist.id,
|
||||||
|
}.to_json
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.update_playlist_attribute(env)
|
||||||
|
locale = LOCALES[env.get("preferences").as(Preferences).locale]?
|
||||||
|
|
||||||
|
env.response.content_type = "application/json"
|
||||||
|
user = env.get("user").as(User)
|
||||||
|
|
||||||
|
plid = env.params.url["plid"]
|
||||||
|
|
||||||
|
playlist = PG_DB.query_one?("SELECT * FROM playlists WHERE id = $1", plid, as: InvidiousPlaylist)
|
||||||
|
if !playlist || playlist.author != user.email && playlist.privacy.private?
|
||||||
|
return error_json(404, "Playlist does not exist.")
|
||||||
|
end
|
||||||
|
|
||||||
|
if playlist.author != user.email
|
||||||
|
return error_json(403, "Invalid user")
|
||||||
|
end
|
||||||
|
|
||||||
|
title = env.params.json["title"].try &.as(String).delete("<>").byte_slice(0, 150) || playlist.title
|
||||||
|
privacy = env.params.json["privacy"]?.try { |privacy| PlaylistPrivacy.parse(privacy.as(String).downcase) } || playlist.privacy
|
||||||
|
description = env.params.json["description"]?.try &.as(String).delete("\r") || playlist.description
|
||||||
|
|
||||||
|
if title != playlist.title ||
|
||||||
|
privacy != playlist.privacy ||
|
||||||
|
description != playlist.description
|
||||||
|
updated = Time.utc
|
||||||
|
else
|
||||||
|
updated = playlist.updated
|
||||||
|
end
|
||||||
|
|
||||||
|
PG_DB.exec("UPDATE playlists SET title = $1, privacy = $2, description = $3, updated = $4 WHERE id = $5", title, privacy, description, updated, plid)
|
||||||
|
env.response.status_code = 204
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.delete_playlist(env)
|
||||||
|
locale = LOCALES[env.get("preferences").as(Preferences).locale]?
|
||||||
|
|
||||||
|
env.response.content_type = "application/json"
|
||||||
|
user = env.get("user").as(User)
|
||||||
|
|
||||||
|
plid = env.params.url["plid"]
|
||||||
|
|
||||||
|
playlist = PG_DB.query_one?("SELECT * FROM playlists WHERE id = $1", plid, as: InvidiousPlaylist)
|
||||||
|
if !playlist || playlist.author != user.email && playlist.privacy.private?
|
||||||
|
return error_json(404, "Playlist does not exist.")
|
||||||
|
end
|
||||||
|
|
||||||
|
if playlist.author != user.email
|
||||||
|
return error_json(403, "Invalid user")
|
||||||
|
end
|
||||||
|
|
||||||
|
PG_DB.exec("DELETE FROM playlist_videos * WHERE plid = $1", plid)
|
||||||
|
PG_DB.exec("DELETE FROM playlists * WHERE id = $1", plid)
|
||||||
|
|
||||||
|
env.response.status_code = 204
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.insert_video_into_playlist(env)
|
||||||
|
locale = LOCALES[env.get("preferences").as(Preferences).locale]?
|
||||||
|
|
||||||
|
env.response.content_type = "application/json"
|
||||||
|
user = env.get("user").as(User)
|
||||||
|
|
||||||
|
plid = env.params.url["plid"]
|
||||||
|
|
||||||
|
playlist = PG_DB.query_one?("SELECT * FROM playlists WHERE id = $1", plid, as: InvidiousPlaylist)
|
||||||
|
if !playlist || playlist.author != user.email && playlist.privacy.private?
|
||||||
|
return error_json(404, "Playlist does not exist.")
|
||||||
|
end
|
||||||
|
|
||||||
|
if playlist.author != user.email
|
||||||
|
return error_json(403, "Invalid user")
|
||||||
|
end
|
||||||
|
|
||||||
|
if playlist.index.size >= 500
|
||||||
|
return error_json(400, "Playlist cannot have more than 500 videos")
|
||||||
|
end
|
||||||
|
|
||||||
|
video_id = env.params.json["videoId"].try &.as(String)
|
||||||
|
if !video_id
|
||||||
|
return error_json(403, "Invalid videoId")
|
||||||
|
end
|
||||||
|
|
||||||
|
begin
|
||||||
|
video = get_video(video_id, PG_DB)
|
||||||
|
rescue ex
|
||||||
|
return error_json(500, ex)
|
||||||
|
end
|
||||||
|
|
||||||
|
playlist_video = PlaylistVideo.new({
|
||||||
|
title: video.title,
|
||||||
|
id: video.id,
|
||||||
|
author: video.author,
|
||||||
|
ucid: video.ucid,
|
||||||
|
length_seconds: video.length_seconds,
|
||||||
|
published: video.published,
|
||||||
|
plid: plid,
|
||||||
|
live_now: video.live_now,
|
||||||
|
index: Random::Secure.rand(0_i64..Int64::MAX),
|
||||||
|
})
|
||||||
|
|
||||||
|
video_array = playlist_video.to_a
|
||||||
|
args = arg_array(video_array)
|
||||||
|
|
||||||
|
PG_DB.exec("INSERT INTO playlist_videos VALUES (#{args})", args: video_array)
|
||||||
|
PG_DB.exec("UPDATE playlists SET index = array_append(index, $1), video_count = cardinality(index) + 1, updated = $2 WHERE id = $3", playlist_video.index, Time.utc, plid)
|
||||||
|
|
||||||
|
env.response.headers["Location"] = "#{HOST_URL}/api/v1/auth/playlists/#{plid}/videos/#{playlist_video.index.to_u64.to_s(16).upcase}"
|
||||||
|
env.response.status_code = 201
|
||||||
|
playlist_video.to_json(locale, index: playlist.index.size)
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.delete_video_in_playlist(env)
|
||||||
|
locale = LOCALES[env.get("preferences").as(Preferences).locale]?
|
||||||
|
|
||||||
|
env.response.content_type = "application/json"
|
||||||
|
user = env.get("user").as(User)
|
||||||
|
|
||||||
|
plid = env.params.url["plid"]
|
||||||
|
index = env.params.url["index"].to_i64(16)
|
||||||
|
|
||||||
|
playlist = PG_DB.query_one?("SELECT * FROM playlists WHERE id = $1", plid, as: InvidiousPlaylist)
|
||||||
|
if !playlist || playlist.author != user.email && playlist.privacy.private?
|
||||||
|
return error_json(404, "Playlist does not exist.")
|
||||||
|
end
|
||||||
|
|
||||||
|
if playlist.author != user.email
|
||||||
|
return error_json(403, "Invalid user")
|
||||||
|
end
|
||||||
|
|
||||||
|
if !playlist.index.includes? index
|
||||||
|
return error_json(404, "Playlist does not contain index")
|
||||||
|
end
|
||||||
|
|
||||||
|
PG_DB.exec("DELETE FROM playlist_videos * WHERE index = $1", index)
|
||||||
|
PG_DB.exec("UPDATE playlists SET index = array_remove(index, $1), video_count = cardinality(index) - 1, updated = $2 WHERE id = $3", index, Time.utc, plid)
|
||||||
|
|
||||||
|
env.response.status_code = 204
|
||||||
|
end
|
||||||
|
|
||||||
|
# Invidious::Routing.patch "/api/v1/auth/playlists/:plid/videos/:index"
|
||||||
|
# def modify_playlist_at(env)
|
||||||
|
# TODO
|
||||||
|
# end
|
||||||
|
|
||||||
|
def self.get_tokens(env)
|
||||||
|
env.response.content_type = "application/json"
|
||||||
|
user = env.get("user").as(User)
|
||||||
|
scopes = env.get("scopes").as(Array(String))
|
||||||
|
|
||||||
|
tokens = PG_DB.query_all("SELECT id, issued FROM session_ids WHERE email = $1", user.email, as: {session: String, issued: Time})
|
||||||
|
|
||||||
|
JSON.build do |json|
|
||||||
|
json.array do
|
||||||
|
tokens.each do |token|
|
||||||
|
json.object do
|
||||||
|
json.field "session", token[:session]
|
||||||
|
json.field "issued", token[:issued].to_unix
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.register_token(env)
|
||||||
|
user = env.get("user").as(User)
|
||||||
|
locale = LOCALES[env.get("preferences").as(Preferences).locale]?
|
||||||
|
|
||||||
|
case env.request.headers["Content-Type"]?
|
||||||
|
when "application/x-www-form-urlencoded"
|
||||||
|
scopes = env.params.body.select { |k, v| k.match(/^scopes\[\d+\]$/) }.map { |k, v| v }
|
||||||
|
callback_url = env.params.body["callbackUrl"]?
|
||||||
|
expire = env.params.body["expire"]?.try &.to_i?
|
||||||
|
when "application/json"
|
||||||
|
scopes = env.params.json["scopes"].as(Array).map { |v| v.as_s }
|
||||||
|
callback_url = env.params.json["callbackUrl"]?.try &.as(String)
|
||||||
|
expire = env.params.json["expire"]?.try &.as(Int64)
|
||||||
|
else
|
||||||
|
return error_json(400, "Invalid or missing header 'Content-Type'")
|
||||||
|
end
|
||||||
|
|
||||||
|
if callback_url && callback_url.empty?
|
||||||
|
callback_url = nil
|
||||||
|
end
|
||||||
|
|
||||||
|
if callback_url
|
||||||
|
callback_url = URI.parse(callback_url)
|
||||||
|
end
|
||||||
|
|
||||||
|
if sid = env.get?("sid").try &.as(String)
|
||||||
|
env.response.content_type = "text/html"
|
||||||
|
|
||||||
|
csrf_token = generate_response(sid, {":authorize_token"}, HMAC_KEY, PG_DB, use_nonce: true)
|
||||||
|
return templated "authorize_token"
|
||||||
|
else
|
||||||
|
env.response.content_type = "application/json"
|
||||||
|
|
||||||
|
superset_scopes = env.get("scopes").as(Array(String))
|
||||||
|
|
||||||
|
authorized_scopes = [] of String
|
||||||
|
scopes.each do |scope|
|
||||||
|
if scopes_include_scope(superset_scopes, scope)
|
||||||
|
authorized_scopes << scope
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
access_token = generate_token(user.email, authorized_scopes, expire, HMAC_KEY, PG_DB)
|
||||||
|
|
||||||
|
if callback_url
|
||||||
|
access_token = URI.encode_www_form(access_token)
|
||||||
|
|
||||||
|
if query = callback_url.query
|
||||||
|
query = HTTP::Params.parse(query.not_nil!)
|
||||||
|
else
|
||||||
|
query = HTTP::Params.new
|
||||||
|
end
|
||||||
|
|
||||||
|
query["token"] = access_token
|
||||||
|
callback_url.query = query.to_s
|
||||||
|
|
||||||
|
env.redirect callback_url.to_s
|
||||||
|
else
|
||||||
|
access_token
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.unregister_token(env)
|
||||||
|
locale = LOCALES[env.get("preferences").as(Preferences).locale]?
|
||||||
|
env.response.content_type = "application/json"
|
||||||
|
user = env.get("user").as(User)
|
||||||
|
scopes = env.get("scopes").as(Array(String))
|
||||||
|
|
||||||
|
session = env.params.json["session"]?.try &.as(String)
|
||||||
|
session ||= env.get("session").as(String)
|
||||||
|
|
||||||
|
# Allow tokens to revoke other tokens with correct scope
|
||||||
|
if session == env.get("session").as(String)
|
||||||
|
PG_DB.exec("DELETE FROM session_ids * WHERE id = $1", session)
|
||||||
|
elsif scopes_include_scope(scopes, "GET:tokens")
|
||||||
|
PG_DB.exec("DELETE FROM session_ids * WHERE id = $1", session)
|
||||||
|
else
|
||||||
|
return error_json(400, "Cannot revoke session #{session}")
|
||||||
|
end
|
||||||
|
|
||||||
|
env.response.status_code = 204
|
||||||
|
end
|
||||||
|
end
|
|
@ -10,4 +10,127 @@ module Invidious::Routes::APIv1::Misc
|
||||||
|
|
||||||
Invidious::Jobs::StatisticsRefreshJob::STATISTICS.to_json
|
Invidious::Jobs::StatisticsRefreshJob::STATISTICS.to_json
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# APIv1 currently uses the same logic for both
|
||||||
|
# user playlists and Invidious playlists. This means that we can't
|
||||||
|
# reasonably split them yet. This should be addressed in APIv2
|
||||||
|
def self.get_playlist(env)
|
||||||
|
locale = LOCALES[env.get("preferences").as(Preferences).locale]?
|
||||||
|
|
||||||
|
env.response.content_type = "application/json"
|
||||||
|
plid = env.params.url["plid"]
|
||||||
|
|
||||||
|
offset = env.params.query["index"]?.try &.to_i?
|
||||||
|
offset ||= env.params.query["page"]?.try &.to_i?.try { |page| (page - 1) * 100 }
|
||||||
|
offset ||= 0
|
||||||
|
|
||||||
|
continuation = env.params.query["continuation"]?
|
||||||
|
|
||||||
|
format = env.params.query["format"]?
|
||||||
|
format ||= "json"
|
||||||
|
|
||||||
|
if plid.starts_with? "RD"
|
||||||
|
return env.redirect "/api/v1/mixes/#{plid}"
|
||||||
|
end
|
||||||
|
|
||||||
|
begin
|
||||||
|
playlist = get_playlist(PG_DB, plid, locale)
|
||||||
|
rescue ex : InfoException
|
||||||
|
return error_json(404, ex)
|
||||||
|
rescue ex
|
||||||
|
return error_json(404, "Playlist does not exist.")
|
||||||
|
end
|
||||||
|
|
||||||
|
user = env.get?("user").try &.as(User)
|
||||||
|
if !playlist || playlist.privacy.private? && playlist.author != user.try &.email
|
||||||
|
return error_json(404, "Playlist does not exist.")
|
||||||
|
end
|
||||||
|
|
||||||
|
response = playlist.to_json(offset, locale, continuation: continuation)
|
||||||
|
|
||||||
|
if format == "html"
|
||||||
|
response = JSON.parse(response)
|
||||||
|
playlist_html = template_playlist(response)
|
||||||
|
index, next_video = response["videos"].as_a.skip(1).select { |video| !video["author"].as_s.empty? }[0]?.try { |v| {v["index"], v["videoId"]} } || {nil, nil}
|
||||||
|
|
||||||
|
response = {
|
||||||
|
"playlistHtml" => playlist_html,
|
||||||
|
"index" => index,
|
||||||
|
"nextVideo" => next_video,
|
||||||
|
}.to_json
|
||||||
|
end
|
||||||
|
|
||||||
|
response
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.mixes(env)
|
||||||
|
locale = LOCALES[env.get("preferences").as(Preferences).locale]?
|
||||||
|
|
||||||
|
env.response.content_type = "application/json"
|
||||||
|
|
||||||
|
rdid = env.params.url["rdid"]
|
||||||
|
|
||||||
|
continuation = env.params.query["continuation"]?
|
||||||
|
continuation ||= rdid.lchop("RD")[0, 11]
|
||||||
|
|
||||||
|
format = env.params.query["format"]?
|
||||||
|
format ||= "json"
|
||||||
|
|
||||||
|
begin
|
||||||
|
mix = fetch_mix(rdid, continuation, locale: locale)
|
||||||
|
|
||||||
|
if !rdid.ends_with? continuation
|
||||||
|
mix = fetch_mix(rdid, mix.videos[1].id)
|
||||||
|
index = mix.videos.index(mix.videos.select { |video| video.id == continuation }[0]?)
|
||||||
|
end
|
||||||
|
|
||||||
|
mix.videos = mix.videos[index..-1]
|
||||||
|
rescue ex
|
||||||
|
return error_json(500, ex)
|
||||||
|
end
|
||||||
|
|
||||||
|
response = JSON.build do |json|
|
||||||
|
json.object do
|
||||||
|
json.field "title", mix.title
|
||||||
|
json.field "mixId", mix.id
|
||||||
|
|
||||||
|
json.field "videos" do
|
||||||
|
json.array do
|
||||||
|
mix.videos.each do |video|
|
||||||
|
json.object do
|
||||||
|
json.field "title", video.title
|
||||||
|
json.field "videoId", video.id
|
||||||
|
json.field "author", video.author
|
||||||
|
|
||||||
|
json.field "authorId", video.ucid
|
||||||
|
json.field "authorUrl", "/channel/#{video.ucid}"
|
||||||
|
|
||||||
|
json.field "videoThumbnails" do
|
||||||
|
json.array do
|
||||||
|
generate_thumbnails(json, video.id)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
json.field "index", video.index
|
||||||
|
json.field "lengthSeconds", video.length_seconds
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if format == "html"
|
||||||
|
response = JSON.parse(response)
|
||||||
|
playlist_html = template_mix(response)
|
||||||
|
next_video = response["videos"].as_a.select { |video| !video["author"].as_s.empty? }[0]?.try &.["videoId"]
|
||||||
|
|
||||||
|
response = {
|
||||||
|
"playlistHtml" => playlist_html,
|
||||||
|
"nextVideo" => next_video,
|
||||||
|
}.to_json
|
||||||
|
end
|
||||||
|
|
||||||
|
response
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,8 +1,6 @@
|
||||||
# There is far too many API routes to define in invidious.cr
|
# There is far too many API routes to define in invidious.cr
|
||||||
# so we'll just do it here instead with a macro.
|
# so we'll just do it here instead with a macro.
|
||||||
macro define_v1_api_routes(base_url = "/api/v1")
|
macro define_v1_api_routes(base_url = "/api/v1")
|
||||||
Invidious::Routing.get "#{{{base_url}}}/stats", Invidious::Routes::APIv1::Misc, :stats
|
|
||||||
|
|
||||||
# Videos
|
# Videos
|
||||||
Invidious::Routing.get "#{{{base_url}}}/videos/:id", Invidious::Routes::APIv1::Videos, :videos
|
Invidious::Routing.get "#{{{base_url}}}/videos/:id", Invidious::Routes::APIv1::Videos, :videos
|
||||||
Invidious::Routing.get "#{{{base_url}}}/storyboards/:id", Invidious::Routes::APIv1::Videos, :storyboards
|
Invidious::Routing.get "#{{{base_url}}}/storyboards/:id", Invidious::Routes::APIv1::Videos, :storyboards
|
||||||
|
@ -32,4 +30,38 @@ macro define_v1_api_routes(base_url = "/api/v1")
|
||||||
# Search
|
# Search
|
||||||
Invidious::Routing.get "#{{{base_url}}}/search", Invidious::Routes::APIv1::Search, :search
|
Invidious::Routing.get "#{{{base_url}}}/search", Invidious::Routes::APIv1::Search, :search
|
||||||
Invidious::Routing.get "#{{{base_url}}}/search/suggestions/:id", Invidious::Routes::APIv1::Search, :search_suggestions
|
Invidious::Routing.get "#{{{base_url}}}/search/suggestions/:id", Invidious::Routes::APIv1::Search, :search_suggestions
|
||||||
|
|
||||||
|
# Authenticated
|
||||||
|
# Invidious::Routing.get "#{{{base_url}}}/auth/notifications", Invidious::Routes::APIv1::Authenticated, :notifications
|
||||||
|
# Invidious::Routing.post "#{{{base_url}}}/auth/notifications", Invidious::Routes::APIv1::Authenticated, :notifications
|
||||||
|
|
||||||
|
Invidious::Routing.get "#{{{base_url}}}/auth/preferences", Invidious::Routes::APIv1::Authenticated, :get_preferences
|
||||||
|
Invidious::Routing.post "#{{{base_url}}}/auth/preferences", Invidious::Routes::APIv1::Authenticated, :set_preferences
|
||||||
|
|
||||||
|
Invidious::Routing.get "#{{{base_url}}}/auth/feed", Invidious::Routes::APIv1::Authenticated, :feed
|
||||||
|
|
||||||
|
Invidious::Routing.get "#{{{base_url}}}/auth/subscriptions", Invidious::Routes::APIv1::Authenticated, :get_subscriptions
|
||||||
|
Invidious::Routing.post "#{{{base_url}}}/auth/subscriptions/:ucid", Invidious::Routes::APIv1::Authenticated, :subscribe_channel
|
||||||
|
Invidious::Routing.delete "#{{{base_url}}}/auth/subscriptions/:ucid", Invidious::Routes::APIv1::Authenticated, :unsubscribe_channel
|
||||||
|
|
||||||
|
|
||||||
|
Invidious::Routing.get "#{{{base_url}}}/auth/playlists", Invidious::Routes::APIv1::Authenticated, :list_playlists
|
||||||
|
Invidious::Routing.post "#{{{base_url}}}/auth/playlists", Invidious::Routes::APIv1::Authenticated, :create_playlist
|
||||||
|
Invidious::Routing.patch "#{{{base_url}}}/auth/playlists/:ucid", Invidious::Routes::APIv1::Authenticated, :update_playlist_attribute
|
||||||
|
Invidious::Routing.delete "#{{{base_url}}}/auth/playlists/:ucid", Invidious::Routes::APIv1::Authenticated, :delete_playlist
|
||||||
|
|
||||||
|
|
||||||
|
Invidious::Routing.post "#{{{base_url}}}/auth/playlists/:ucid/videos", Invidious::Routes::APIv1::Authenticated, :insert_video_into_playlist
|
||||||
|
Invidious::Routing.delete "#{{{base_url}}}/auth/playlists/:ucid/videos/:index", Invidious::Routes::APIv1::Authenticated, :delete_video_in_playlist
|
||||||
|
|
||||||
|
Invidious::Routing.get "#{{{base_url}}}/auth/tokens", Invidious::Routes::APIv1::Authenticated, :get_tokens
|
||||||
|
Invidious::Routing.post "#{{{base_url}}}/auth/tokens/register", Invidious::Routes::APIv1::Authenticated, :register_token
|
||||||
|
Invidious::Routing.post "#{{{base_url}}}/auth/tokens/unregister", Invidious::Routes::APIv1::Authenticated, :unregister_token
|
||||||
|
|
||||||
|
# Misc
|
||||||
|
Invidious::Routing.get "#{{{base_url}}}/stats", Invidious::Routes::APIv1::Misc, :stats
|
||||||
|
Invidious::Routing.get "#{{{base_url}}}/playlists/:plid", Invidious::Routes::APIv1::Misc, :get_playlist
|
||||||
|
Invidious::Routing.get "#{{{base_url}}}/auth/playlists/:plid", Invidious::Routes::APIv1::Misc, :get_playlist
|
||||||
|
Invidious::Routing.get "#{{{base_url}}}//mixes/:rdid", Invidious::Routes::APIv1::Misc, :mixes
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
Loading…
Reference in a new issue