summaryrefslogtreecommitdiffstats
path: root/src/invidious.cr
diff options
context:
space:
mode:
authorOmar Roth <omarroth@protonmail.com>2019-08-05 18:49:13 -0500
committerOmar Roth <omarroth@protonmail.com>2019-10-15 21:17:14 -0400
commitbe055d9dcb31fe64cb682d50dc70101484605741 (patch)
tree5e0f15a15a5ceabcff4b5cbc3046bcfb2e0771f4 /src/invidious.cr
parent1e34a61911bf786497793b6fe3f309a411a32aae (diff)
downloadinvidious-be055d9dcb31fe64cb682d50dc70101484605741.tar.gz
invidious-be055d9dcb31fe64cb682d50dc70101484605741.tar.bz2
invidious-be055d9dcb31fe64cb682d50dc70101484605741.zip
Add support for custom playlists
Diffstat (limited to 'src/invidious.cr')
-rw-r--r--src/invidious.cr932
1 files changed, 779 insertions, 153 deletions
diff --git a/src/invidious.cr b/src/invidious.cr
index 4cdf8932..ad313269 100644
--- a/src/invidious.cr
+++ b/src/invidious.cr
@@ -126,15 +126,19 @@ Kemal::CLI.new ARGV
# Check table integrity
if CONFIG.check_tables
- analyze_table(PG_DB, logger, "channels", InvidiousChannel)
- analyze_table(PG_DB, logger, "channel_videos", ChannelVideo)
- analyze_table(PG_DB, logger, "nonces", Nonce)
- analyze_table(PG_DB, logger, "session_ids", SessionId)
- analyze_table(PG_DB, logger, "users", User)
- analyze_table(PG_DB, logger, "videos", Video)
+ check_enum(PG_DB, logger, "privacy", PlaylistPrivacy)
+
+ check_table(PG_DB, logger, "channels", InvidiousChannel)
+ check_table(PG_DB, logger, "channel_videos", ChannelVideo)
+ check_table(PG_DB, logger, "playlists", InvidiousPlaylist)
+ check_table(PG_DB, logger, "playlist_videos", PlaylistVideo)
+ check_table(PG_DB, logger, "nonces", Nonce)
+ check_table(PG_DB, logger, "session_ids", SessionId)
+ check_table(PG_DB, logger, "users", User)
+ check_table(PG_DB, logger, "videos", Video)
if CONFIG.cache_annotations
- analyze_table(PG_DB, logger, "annotations", Annotation)
+ check_table(PG_DB, logger, "annotations", Annotation)
end
end
@@ -248,7 +252,14 @@ before_all do |env|
if !env.request.cookies.has_key? "SSID"
if email = PG_DB.query_one?("SELECT email FROM session_ids WHERE id = $1", sid, as: String)
user = PG_DB.query_one("SELECT * FROM users WHERE email = $1", email, as: User)
- csrf_token = generate_response(sid, {":signout", ":watch_ajax", ":subscription_ajax", ":token_ajax", ":authorize_token"}, HMAC_KEY, PG_DB, 1.week)
+ csrf_token = generate_response(sid, {
+ ":authorize_token",
+ ":playlist_ajax",
+ ":signout",
+ ":subscription_ajax",
+ ":token_ajax",
+ ":watch_ajax",
+ }, HMAC_KEY, PG_DB, 1.week)
preferences = user.preferences
@@ -262,7 +273,14 @@ before_all do |env|
begin
user, sid = get_user(sid, headers, PG_DB, false)
- csrf_token = generate_response(sid, {":signout", ":watch_ajax", ":subscription_ajax", ":token_ajax", ":authorize_token"}, HMAC_KEY, PG_DB, 1.week)
+ csrf_token = generate_response(sid, {
+ ":authorize_token",
+ ":playlist_ajax",
+ ":signout",
+ ":subscription_ajax",
+ ":token_ajax",
+ ":watch_ajax",
+ }, HMAC_KEY, PG_DB, 1.week)
preferences = user.preferences
@@ -371,6 +389,8 @@ get "/watch" do |env|
end
plid = env.params.query["list"]?
+ continuation = process_continuation(PG_DB, env.params.query, plid, id)
+
nojs = env.params.query["nojs"]?
nojs ||= "0"
@@ -555,7 +575,9 @@ get "/embed/" do |env|
if plid = env.params.query["list"]?
begin
- videos = fetch_playlist_videos(plid, 1, 1, locale: locale)
+ playlist = get_playlist(PG_DB, plid, locale: locale)
+ offset = env.params.query["index"]?.try &.to_i? || 0
+ videos = get_playlist_videos(PG_DB, playlist, offset: offset, locale: locale)
rescue ex
error_message = ex.message
env.response.status_code = 500
@@ -577,7 +599,9 @@ end
get "/embed/:id" do |env|
locale = LOCALES[env.get("preferences").as(Preferences).locale]?
id = env.params.url["id"]
+
plid = env.params.query["list"]?
+ continuation = process_continuation(PG_DB, env.params.query, plid, id)
if md = env.params.query["playlist"]?
.try &.match(/[a-zA-Z0-9_-]{11}(,[a-zA-Z0-9_-]{11})*/)
@@ -607,7 +631,9 @@ get "/embed/:id" do |env|
if plid
begin
- videos = fetch_playlist_videos(plid, 1, 1, locale: locale)
+ playlist = get_playlist(PG_DB, plid, locale: locale)
+ offset = env.params.query["index"]?.try &.to_i? || 0
+ videos = get_playlist_videos(PG_DB, playlist, offset: offset, locale: locale)
rescue ex
error_message = ex.message
env.response.status_code = 500
@@ -757,10 +783,447 @@ end
# Playlists
+get "/view_all_playlists" do |env|
+ locale = LOCALES[env.get("preferences").as(Preferences).locale]?
+
+ user = env.get? "user"
+ referer = get_referer(env)
+
+ if !user
+ next env.redirect "/"
+ end
+
+ user = user.as(User)
+
+ items = PG_DB.query_all("SELECT * FROM playlists WHERE author = $1 ORDER BY created", user.email, as: InvidiousPlaylist)
+ items.map! do |item|
+ item.author = ""
+ item
+ end
+
+ templated "view_all_playlists"
+end
+
+get "/create_playlist" 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 "/"
+ end
+
+ user = user.as(User)
+ sid = sid.as(String)
+ csrf_token = generate_response(sid, {":create_playlist"}, HMAC_KEY, PG_DB)
+
+ templated "create_playlist"
+end
+
+post "/create_playlist" 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 "/"
+ 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
+ error_message = ex.message
+ env.response.status_code = 400
+ next templated "error"
+ end
+
+ title = env.params.body["title"]?.try &.as(String)
+ if !title || title.empty?
+ error_message = "Title cannot be empty."
+ next templated "error"
+ end
+
+ privacy = PlaylistPrivacy.parse?(env.params.body["privacy"]?.try &.as(String) || "")
+ if !privacy
+ error_message = "Invalid privacy setting."
+ next templated "error"
+ end
+
+ if PG_DB.query_one("SELECT count(*) FROM playlists WHERE author = $1", user.email, as: Int64) >= 100
+ error_message = "User cannot have more than 100 playlists."
+ next templated "error"
+ end
+
+ playlist = create_playlist(PG_DB, title, privacy, user)
+
+ env.redirect "/playlist?list=#{playlist.id}"
+end
+
+get "/delete_playlist" 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 "/"
+ end
+
+ user = user.as(User)
+ sid = sid.as(String)
+
+ plid = env.params.query["list"]?
+ if !plid || !plid.starts_with?("IV")
+ next env.redirect referer
+ end
+
+ playlist = PG_DB.query_one?("SELECT * FROM playlists WHERE id = $1", plid, as: InvidiousPlaylist)
+ if !playlist || playlist.author != user.email
+ next env.redirect referer
+ end
+
+ csrf_token = generate_response(sid, {":delete_playlist"}, HMAC_KEY, PG_DB)
+
+ templated "delete_playlist"
+end
+
+post "/delete_playlist" 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 "/"
+ end
+
+ plid = env.params.query["list"]?
+ if !plid
+ next env.redirect referer
+ 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
+ error_message = ex.message
+ env.response.status_code = 400
+ next templated "error"
+ end
+
+ playlist = PG_DB.query_one?("SELECT * FROM playlists WHERE id = $1", plid, as: InvidiousPlaylist)
+ if !playlist || playlist.author != user.email
+ next 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
+
+get "/edit_playlist" 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 "/"
+ end
+
+ user = user.as(User)
+ sid = sid.as(String)
+
+ plid = env.params.query["list"]?
+ if !plid || !plid.starts_with?("IV")
+ next 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
+ next env.redirect referer
+ end
+ rescue ex
+ next 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
+
+post "/edit_playlist" 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 "/"
+ end
+
+ plid = env.params.query["list"]?
+ if !plid
+ next env.redirect referer
+ 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
+ error_message = ex.message
+ env.response.status_code = 400
+ next templated "error"
+ end
+
+ playlist = PG_DB.query_one?("SELECT * FROM playlists WHERE id = $1", plid, as: InvidiousPlaylist)
+ if !playlist || playlist.author != user.email
+ next 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
+
+get "/add_playlist_items" 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 "/"
+ end
+
+ user = user.as(User)
+ sid = sid.as(String)
+
+ plid = env.params.query["list"]?
+ if !plid || !plid.starts_with?("IV")
+ next 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
+ next env.redirect referer
+ end
+ rescue ex
+ next 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
+
+post "/playlist_ajax" do |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
+ next env.redirect referer
+ else
+ error_message = {"error" => "No such user"}.to_json
+ env.response.status_code = 403
+ next error_message
+ 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
+ error_message = ex.message
+ env.response.status_code = 400
+ next templated "error"
+ else
+ error_message = {"error" => ex.message}.to_json
+ env.response.status_code = 400
+ next error_message
+ 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
+ next 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
+ error_message = ex.message
+ env.response.status_code = 400
+ next templated "error"
+ else
+ error_message = {"error" => ex.message}.to_json
+ env.response.status_code = 400
+ next error_message
+ 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
+ env.response.status_code = 400
+ if redirect
+ error_message = "Playlist cannot have more than 500 videos"
+ next templated "error"
+ else
+ error_message = {"error" => "Playlist cannot have more than 500 videos"}.to_json
+ next error_message
+ end
+ end
+
+ video_id = env.params.query["video_id"]
+
+ begin
+ video = get_video(video_id, PG_DB)
+ rescue ex
+ env.response.status_code = 500
+ if redirect
+ error_message = ex.message
+ next templated "error"
+ else
+ error_message = {"error" => ex.message}.to_json
+ next error_message
+ 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 = video_count + 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 = video_count - 1, updated = $2 WHERE id = $3", index, Time.utc, playlist_id)
+ when "action_move_video_before"
+ # TODO: Playlist stub
+ end
+
+ if redirect
+ env.redirect referer
+ else
+ env.response.content_type = "application/json"
+ "{}"
+ end
+end
+
get "/playlist" do |env|
locale = LOCALES[env.get("preferences").as(Preferences).locale]?
+ user = env.get?("user").try &.as(User)
plid = env.params.query["list"]?
+ referer = get_referer(env)
+
if !plid
next env.redirect "/"
end
@@ -773,19 +1236,29 @@ get "/playlist" do |env|
end
begin
- playlist = fetch_playlist(plid, locale)
+ playlist = get_playlist(PG_DB, plid, locale)
rescue ex
error_message = ex.message
env.response.status_code = 500
next templated "error"
end
+ if playlist.privacy == PlaylistPrivacy::Private && playlist.author != user.try &.email
+ error_message = "This playlist is private."
+ env.response.status_code = 403
+ next templated "error"
+ end
+
begin
- videos = fetch_playlist_videos(plid, page, playlist.video_count, locale: locale)
+ 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
@@ -864,72 +1337,13 @@ get "/search" do |env|
page ||= 1
user = env.get? "user"
- if user
- user = user.as(User)
- view_name = "subscriptions_#{sha256(user.email)}"
- end
-
- channel = nil
- content_type = "all"
- date = ""
- duration = ""
- features = [] of String
- sort = "relevance"
- subscriptions = nil
-
- operators = query.split(" ").select { |a| a.match(/\w+:[\w,]+/) }
- operators.each do |operator|
- key, value = operator.downcase.split(":")
-
- case key
- when "channel", "user"
- channel = operator.split(":")[-1]
- when "content_type", "type"
- content_type = value
- when "date"
- date = value
- when "duration"
- duration = value
- when "feature", "features"
- features = value.split(",")
- when "sort"
- sort = value
- when "subscriptions"
- subscriptions = value == "true"
- else
- operators.delete(operator)
- end
- end
-
- search_query = (query.split(" ") - operators).join(" ")
- if channel
- count, videos = channel_search(search_query, page, channel)
- elsif subscriptions
- if view_name
- videos = PG_DB.query_all("SELECT id,title,published,updated,ucid,author,length_seconds FROM (
- SELECT *,
- to_tsvector(#{view_name}.title) ||
- to_tsvector(#{view_name}.author)
- as document
- FROM #{view_name}
- ) v_search WHERE v_search.document @@ plainto_tsquery($1) LIMIT 20 OFFSET $2;", search_query, (page - 1) * 20, as: ChannelVideo)
- count = videos.size
- else
- videos = [] of ChannelVideo
- count = 0
- end
- else
- begin
- search_params = produce_search_params(sort: sort, date: date, content_type: content_type,
- duration: duration, features: features)
- rescue ex
- error_message = ex.message
- env.response.status_code = 500
- next templated "error"
- end
-
- count, videos = search(search_query, page, search_params, region).as(Tuple)
+ begin
+ search_query, count, videos = process_search_query(query, page, user, region: nil)
+ rescue ex
+ error_message = ex.message
+ env.response.status_code = 500
+ next templated "error"
end
env.set "search", query
@@ -1746,13 +2160,12 @@ post "/watch_ajax" do |env|
begin
validate_request(token, sid, env.request, HMAC_KEY, PG_DB, locale)
rescue ex
+ env.response.status_code = 400
if redirect
error_message = ex.message
- env.response.status_code = 400
next templated "error"
else
error_message = {"error" => ex.message}.to_json
- env.response.status_code = 400
next error_message
end
end
@@ -2771,6 +3184,35 @@ get "/feed/playlist/:plid" do |env|
host_url = make_host_url(config, Kemal.config)
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(host_url, false, xml)
+ end
+ end
+ end
+ else
+ env.response.status_code = 404
+ next
+ end
+ end
+
client = make_client(YT_URL)
response = client.get("/feeds/videos.xml?playlist_id=#{plid}")
document = XML.parse(response.body)
@@ -4125,92 +4567,58 @@ get "/api/v1/search/suggestions" do |env|
end
end
-get "/api/v1/playlists/:plid" do |env|
- locale = LOCALES[env.get("preferences").as(Preferences).locale]?
-
- env.response.content_type = "application/json"
- plid = env.params.url["plid"]
-
- page = env.params.query["page"]?.try &.to_i?
- page ||= 1
-
- format = env.params.query["format"]?
- format ||= "json"
-
- continuation = env.params.query["continuation"]?
+{"/api/v1/playlists/:plid", "/api/v1/auth/playlists/:plid"}.each do |route|
+ get route do |env|
+ locale = LOCALES[env.get("preferences").as(Preferences).locale]?
- if plid.starts_with? "RD"
- next env.redirect "/api/v1/mixes/#{plid}"
- end
+ env.response.content_type = "application/json"
+ plid = env.params.url["plid"]
- begin
- playlist = fetch_playlist(plid, locale)
- rescue ex
- error_message = {"error" => "Playlist is empty"}.to_json
- env.response.status_code = 410
- next error_message
- end
+ offset = env.params.query["index"]?.try &.to_i?
+ offset ||= env.params.query["page"]?.try &.to_i?.try { |page| (page - 1) * 100 }
+ offset ||= 0
- begin
- videos = fetch_playlist_videos(plid, page, playlist.video_count, continuation, locale)
- rescue ex
- videos = [] of PlaylistVideo
- end
+ continuation = env.params.query["continuation"]?
- response = JSON.build do |json|
- json.object do
- json.field "type", "playlist"
- json.field "title", playlist.title
- json.field "playlistId", playlist.id
- json.field "playlistThumbnail", playlist.thumbnail
+ format = env.params.query["format"]?
+ format ||= "json"
- json.field "author", playlist.author
- json.field "authorId", playlist.ucid
- json.field "authorUrl", "/channel/#{playlist.ucid}"
+ if plid.starts_with? "RD"
+ next env.redirect "/api/v1/mixes/#{plid}"
+ end
- json.field "authorThumbnails" do
- json.array do
- qualities = {32, 48, 76, 100, 176, 512}
+ begin
+ playlist = get_playlist(PG_DB, plid, locale)
+ rescue ex
+ env.response.status_code = 404
+ error_message = {"error" => "Playlist does not exist."}.to_json
+ next error_message
+ end
- qualities.each do |quality|
- json.object do
- json.field "url", playlist.author_thumbnail.gsub(/=\d+/, "=s#{quality}")
- json.field "width", quality
- json.field "height", quality
- end
- end
- end
- end
+ user = env.get?("user").try &.as(User)
+ if !playlist || !playlist.privacy.public? && playlist.author != user.try &.email
+ env.response.status_code = 404
+ error_message = {"error" => "Playlist does not exist."}.to_json
+ next error_message
+ end
- json.field "description", html_to_content(playlist.description_html)
- json.field "descriptionHtml", playlist.description_html
- json.field "videoCount", playlist.video_count
+ response = playlist.to_json(offset, locale, config, Kemal.config, continuation: continuation)
- json.field "viewCount", playlist.views
- json.field "updated", playlist.updated.to_unix
+ if format == "html"
+ response = JSON.parse(response)
+ playlist_html = template_playlist(response)
+ index = response["videos"].as_a[1]?.try &.["index"]
+ next_video = response["videos"].as_a[1]?.try &.["videoId"]
- json.field "videos" do
- json.array do
- videos.each do |video|
- video.to_json(locale, config, Kemal.config, json)
- end
- end
- end
+ response = {
+ "playlistHtml" => playlist_html,
+ "index" => index,
+ "nextVideo" => next_video,
+ }.to_json
end
- end
-
- if format == "html"
- response = JSON.parse(response)
- playlist_html = template_playlist(response)
- next_video = response["videos"].as_a[1]?.try &.["videoId"]
- response = {
- "playlistHtml" => playlist_html,
- "nextVideo" => next_video,
- }.to_json
+ response
end
-
- response
end
get "/api/v1/mixes/:rdid" do |env|
@@ -4418,6 +4826,224 @@ delete "/api/v1/auth/subscriptions/:ucid" do |env|
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, config, Kemal.config, 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
+ error_message = {"error" => "Invalid title."}.to_json
+ env.response.status_code = 400
+ next error_message
+ end
+
+ privacy = env.params.json["privacy"]?.try { |privacy| PlaylistPrivacy.parse(privacy.as(String).downcase) }
+ if !privacy
+ error_message = {"error" => "Invalid privacy setting."}.to_json
+ env.response.status_code = 400
+ next error_message
+ end
+
+ if PG_DB.query_one("SELECT count(*) FROM playlists WHERE author = $1", user.email, as: Int64) >= 100
+ error_message = {"error" => "User cannot have more than 100 playlists."}.to_json
+ env.response.status_code = 400
+ next error_message
+ end
+
+ host_url = make_host_url(config, Kemal.config)
+
+ 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.public?
+ env.response.status_code = 404
+ error_message = {"error" => "Playlist does not exist."}.to_json
+ next error_message
+ end
+
+ if playlist.author != user.email
+ env.response.status_code = 403
+ error_message = {"error" => "Invalid user"}.to_json
+ next error_message
+ 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|
+ 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.public?
+ env.response.status_code = 404
+ error_message = {"error" => "Playlist does not exist."}.to_json
+ next error_message
+ end
+
+ if playlist.author != user.email
+ env.response.status_code = 403
+ error_message = {"error" => "Invalid user"}.to_json
+ next error_message
+ 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.public?
+ env.response.status_code = 404
+ error_message = {"error" => "Playlist does not exist."}.to_json
+ next error_message
+ end
+
+ if playlist.author != user.email
+ env.response.status_code = 403
+ error_message = {"error" => "Invalid user"}.to_json
+ next error_message
+ end
+
+ if playlist.index.size >= 500
+ env.response.status_code = 400
+ error_message = {"error" => "Playlist cannot have more than 500 videos"}.to_json
+ next error_message
+ end
+
+ video_id = env.params.json["videoId"].try &.as(String)
+ if !video_id
+ env.response.status_code = 403
+ error_message = {"error" => "Invalid videoId"}.to_json
+ next error_message
+ end
+
+ begin
+ video = get_video(video_id, PG_DB)
+ rescue ex
+ error_message = {"error" => ex.message}.to_json
+ env.response.status_code = 500
+ next error_message
+ 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 = video_count + 1, updated = $2 WHERE id = $3", playlist_video.index, Time.utc, plid)
+
+ host_url = make_host_url(config, Kemal.config)
+
+ env.response.headers["Location"] = "#{host_url}/api/v1/auth/playlists/#{plid}/videos/#{playlist_video.index}"
+ env.response.status_code = 201
+ playlist_video.to_json(locale, config, Kemal.config, index: playlist.index.size)
+end
+
+delete "/api/v1/auth/playlists/:plid/videos/:index" do |env|
+ 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.public?
+ env.response.status_code = 404
+ error_message = {"error" => "Playlist does not exist."}.to_json
+ next error_message
+ end
+
+ if playlist.author != user.email
+ env.response.status_code = 403
+ error_message = {"error" => "Invalid user"}.to_json
+ next error_message
+ end
+
+ if !playlist.index.includes? index
+ env.response.status_code = 404
+ error_message = {"error" => "Playlist does not contain index"}.to_json
+ next error_message
+ 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 = video_count - 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)