invidious-mod-JP/src/invidious/routes/playlists.cr
saltycrys 3dac33ffba
Add backtraces to errors (#1498)
Error handling has been reworked to always go through the new `error_template`,
`error_json` and `error_atom` macros.
They all accept a status code followed by a string message or an exception
object. `error_json` accepts a hash with additional fields as third argument.

If the second argument is an exception a backtrace will be printed, if it is a
string only the string is printed. Since up till now only the exception message
was printed a new `InfoException` class was added for situations where no
backtrace is intended but a string cannot be used.

`error_template` with a string message automatically localizes the message.
Missing error translations have been collected in https://github.com/iv-org/invidious/issues/1497
`error_json` with a string message does not localize the message. This is the
same as previous behavior. If translations are desired for `error_json` they
can be added easily but those error messages have not been collected yet.

Uncaught exceptions previously only printed a generic message ("Looks like
you've found a bug in Invidious. [...]"). They still print that message
but now also include a backtrace.
2020-11-30 10:59:21 +01:00

473 lines
13 KiB
Crystal

class Invidious::Routes::Playlists < Invidious::Routes::BaseRoute
def 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 new(env)
locale = LOCALES[env.get("preferences").as(Preferences).locale]?
user = env.get? "user"
sid = env.get? "sid"
referer = get_referer(env)
return env.redirect "/" if user.nil?
user = user.as(User)
sid = sid.as(String)
csrf_token = generate_response(sid, {":create_playlist"}, HMAC_KEY, PG_DB)
templated "create_playlist"
end
def create(env)
locale = LOCALES[env.get("preferences").as(Preferences).locale]?
user = env.get? "user"
sid = env.get? "sid"
referer = get_referer(env)
return env.redirect "/" if user.nil?
user = user.as(User)
sid = sid.as(String)
token = env.params.body["csrf_token"]?
begin
validate_request(token, sid, env.request, HMAC_KEY, PG_DB, locale)
rescue ex
return error_template(400, ex)
end
title = env.params.body["title"]?.try &.as(String)
if !title || title.empty?
return error_template(400, "Title cannot be empty.")
end
privacy = PlaylistPrivacy.parse?(env.params.body["privacy"]?.try &.as(String) || "")
if !privacy
return error_template(400, "Invalid privacy setting.")
end
if PG_DB.query_one("SELECT count(*) FROM playlists WHERE author = $1", user.email, as: Int64) >= 100
return error_template(400, "User cannot have more than 100 playlists.")
end
playlist = create_playlist(PG_DB, title, privacy, user)
env.redirect "/playlist?list=#{playlist.id}"
end
def subscribe(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)
playlist_id = env.params.query["list"]
playlist = get_playlist(PG_DB, playlist_id, locale)
subscribe_playlist(PG_DB, user, playlist)
env.redirect "/playlist?list=#{playlist.id}"
end
def delete_page(env)
locale = LOCALES[env.get("preferences").as(Preferences).locale]?
user = env.get? "user"
sid = env.get? "sid"
referer = get_referer(env)
return env.redirect "/" if user.nil?
user = user.as(User)
sid = sid.as(String)
plid = env.params.query["list"]?
playlist = PG_DB.query_one?("SELECT * FROM playlists WHERE id = $1", plid, as: InvidiousPlaylist)
if !playlist || playlist.author != user.email
return env.redirect referer
end
csrf_token = generate_response(sid, {":delete_playlist"}, HMAC_KEY, PG_DB)
templated "delete_playlist"
end
def delete(env)
locale = LOCALES[env.get("preferences").as(Preferences).locale]?
user = env.get? "user"
sid = env.get? "sid"
referer = get_referer(env)
return env.redirect "/" if user.nil?
plid = env.params.query["list"]?
return env.redirect referer if plid.nil?
user = user.as(User)
sid = sid.as(String)
token = env.params.body["csrf_token"]?
begin
validate_request(token, sid, env.request, HMAC_KEY, PG_DB, locale)
rescue ex
return error_template(400, ex)
end
playlist = PG_DB.query_one?("SELECT * FROM playlists WHERE id = $1", plid, as: InvidiousPlaylist)
if !playlist || playlist.author != user.email
return env.redirect referer
end
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"
end
def edit(env)
locale = LOCALES[env.get("preferences").as(Preferences).locale]?
user = env.get? "user"
sid = env.get? "sid"
referer = get_referer(env)
return env.redirect "/" if user.nil?
user = user.as(User)
sid = sid.as(String)
plid = env.params.query["list"]?
if !plid || !plid.starts_with?("IV")
return env.redirect referer
end
page = env.params.query["page"]?.try &.to_i?
page ||= 1
begin
playlist = PG_DB.query_one("SELECT * FROM playlists WHERE id = $1", plid, as: InvidiousPlaylist)
if !playlist || playlist.author != user.email
return env.redirect referer
end
rescue ex
return env.redirect referer
end
begin
videos = get_playlist_videos(PG_DB, playlist, offset: (page - 1) * 100, locale: locale)
rescue ex
videos = [] of PlaylistVideo
end
csrf_token = generate_response(sid, {":edit_playlist"}, HMAC_KEY, PG_DB)
templated "edit_playlist"
end
def update(env)
locale = LOCALES[env.get("preferences").as(Preferences).locale]?
user = env.get? "user"
sid = env.get? "sid"
referer = get_referer(env)
return env.redirect "/" if user.nil?
plid = env.params.query["list"]?
return env.redirect referer if plid.nil?
user = user.as(User)
sid = sid.as(String)
token = env.params.body["csrf_token"]?
begin
validate_request(token, sid, env.request, HMAC_KEY, PG_DB, locale)
rescue ex
return error_template(400, ex)
end
playlist = PG_DB.query_one?("SELECT * FROM playlists WHERE id = $1", plid, as: InvidiousPlaylist)
if !playlist || playlist.author != user.email
return env.redirect referer
end
title = env.params.body["title"]?.try &.delete("<>") || ""
privacy = PlaylistPrivacy.parse(env.params.body["privacy"]? || "Public")
description = env.params.body["description"]?.try &.delete("\r") || ""
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.redirect "/playlist?list=#{plid}"
end
def add_playlist_items_page(env)
locale = LOCALES[env.get("preferences").as(Preferences).locale]?
user = env.get? "user"
sid = env.get? "sid"
referer = get_referer(env)
return env.redirect "/" if user.nil?
user = user.as(User)
sid = sid.as(String)
plid = env.params.query["list"]?
if !plid || !plid.starts_with?("IV")
return env.redirect referer
end
page = env.params.query["page"]?.try &.to_i?
page ||= 1
begin
playlist = PG_DB.query_one("SELECT * FROM playlists WHERE id = $1", plid, as: InvidiousPlaylist)
if !playlist || playlist.author != user.email
return env.redirect referer
end
rescue ex
return env.redirect referer
end
query = env.params.query["q"]?
if query
begin
search_query, count, items = process_search_query(query, page, user, region: nil)
videos = items.select { |item| item.is_a? SearchVideo }.map { |item| item.as(SearchVideo) }
rescue ex
videos = [] of SearchVideo
count = 0
end
else
videos = [] of SearchVideo
count = 0
end
env.set "add_playlist_items", plid
templated "add_playlist_items"
end
def playlist_ajax(env)
locale = LOCALES[env.get("preferences").as(Preferences).locale]?
user = env.get? "user"
sid = env.get? "sid"
referer = get_referer(env, "/")
redirect = env.params.query["redirect"]?
redirect ||= "true"
redirect = redirect == "true"
if !user
if redirect
return env.redirect referer
else
return error_json(403, "No such user")
end
end
user = user.as(User)
sid = sid.as(String)
token = env.params.body["csrf_token"]?
begin
validate_request(token, sid, env.request, HMAC_KEY, PG_DB, locale)
rescue ex
if redirect
return error_template(400, ex)
else
return error_json(400, ex)
end
end
if env.params.query["action_create_playlist"]?
action = "action_create_playlist"
elsif env.params.query["action_delete_playlist"]?
action = "action_delete_playlist"
elsif env.params.query["action_edit_playlist"]?
action = "action_edit_playlist"
elsif env.params.query["action_add_video"]?
action = "action_add_video"
video_id = env.params.query["video_id"]
elsif env.params.query["action_remove_video"]?
action = "action_remove_video"
elsif env.params.query["action_move_video_before"]?
action = "action_move_video_before"
else
return env.redirect referer
end
begin
playlist_id = env.params.query["playlist_id"]
playlist = get_playlist(PG_DB, playlist_id, locale).as(InvidiousPlaylist)
raise "Invalid user" if playlist.author != user.email
rescue ex
if redirect
return error_template(400, ex)
else
return error_json(400, ex)
end
end
if !user.password
# TODO: Playlist stub, sync with YouTube for Google accounts
# playlist_ajax(playlist_id, action, env.request.headers)
end
email = user.email
case action
when "action_edit_playlist"
# TODO: Playlist stub
when "action_add_video"
if playlist.index.size >= 500
if redirect
return error_template(400, "Playlist cannot have more than 500 videos")
else
return error_json(400, "Playlist cannot have more than 500 videos")
end
end
video_id = env.params.query["video_id"]
begin
video = get_video(video_id, PG_DB)
rescue ex
if redirect
return error_template(500, ex)
else
return error_json(500, ex)
end
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: playlist_id,
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, playlist_id)
when "action_remove_video"
index = env.params.query["set_video_id"]
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, playlist_id)
when "action_move_video_before"
# TODO: Playlist stub
else
return error_json(400, "Unsupported action #{action}")
end
if redirect
env.redirect referer
else
env.response.content_type = "application/json"
"{}"
end
end
def show(env)
locale = LOCALES[env.get("preferences").as(Preferences).locale]?
user = env.get?("user").try &.as(User)
referer = get_referer(env)
plid = env.params.query["list"]?.try &.gsub(/[^a-zA-Z0-9_-]/, "")
if !plid
return env.redirect "/"
end
page = env.params.query["page"]?.try &.to_i?
page ||= 1
if plid.starts_with? "RD"
return env.redirect "/mix?list=#{plid}"
end
begin
playlist = get_playlist(PG_DB, plid, locale)
rescue ex
return error_template(500, ex)
end
if playlist.privacy == PlaylistPrivacy::Private && playlist.author != user.try &.email
return error_template(403, "This playlist is private.")
end
begin
videos = get_playlist_videos(PG_DB, playlist, offset: (page - 1) * 100, locale: locale)
rescue ex
videos = [] of PlaylistVideo
end
if playlist.author == user.try &.email
env.set "remove_playlist_items", plid
end
templated "playlist"
end
def mix(env)
locale = LOCALES[env.get("preferences").as(Preferences).locale]?
rdid = env.params.query["list"]?
if !rdid
return env.redirect "/"
end
continuation = env.params.query["continuation"]?
continuation ||= rdid.lchop("RD")
begin
mix = fetch_mix(rdid, continuation, locale: locale)
rescue ex
return error_template(500, ex)
end
templated "mix"
end
end