diff --git a/assets/js/compilation_widget.js b/assets/js/compilation_widget.js new file mode 100644 index 000000000..7e8e6356e --- /dev/null +++ b/assets/js/compilation_widget.js @@ -0,0 +1,48 @@ +'use strict'; +var compilation_data = JSON.parse(document.getElementById('compilation_data').textContent); +var payload = 'csrf_token=' + compilation_data.csrf_token; + +function add_compilation_video(target) { + var select = target.parentNode.children[0].children[1]; + var option = select.children[select.selectedIndex]; + + var url = '/compilation_ajax?action_add_video=1&redirect=false' + + '&video_id=' + target.getAttribute('data-id') + + '&compilation_id=' + option.getAttribute('data-compid'); + + helpers.xhr('POST', url, {payload: payload}, { + on200: function (response) { + option.textContent = '✓' + option.textContent; + } + }); +} + +function add_compilation_item(target) { + var tile = target.parentNode.parentNode.parentNode.parentNode.parentNode; + tile.style.display = 'none'; + + var url = '/compilation_ajax?action_add_video=1&redirect=false' + + '&video_id=' + target.getAttribute('data-id') + + '&compilation_id=' + target.getAttribute('data-compid'); + + helpers.xhr('POST', url, {payload: payload}, { + onNon200: function (xhr) { + tile.style.display = ''; + } + }); +} + +function remove_compilation_item(target) { + var tile = target.parentNode.parentNode.parentNode.parentNode.parentNode; + tile.style.display = 'none'; + + var url = '/compilation_ajax?action_remove_video=1&redirect=false' + + '&set_video_id=' + target.getAttribute('data-index') + + '&compilation_id=' + target.getAttribute('data-compid'); + + helpers.xhr('POST', url, {payload: payload}, { + onNon200: function (xhr) { + tile.style.display = ''; + } + }); +} diff --git a/config/config.example.yml b/config/config.example.yml index e925a5e3f..ff568a5b9 100644 --- a/config/config.example.yml +++ b/config/config.example.yml @@ -483,6 +483,14 @@ hmac_key: "CHANGE_ME!!" ## #playlist_length_limit: 500 +## +## Maximum custom compilation length limit. +## +## Accepted values: Integer +## Default: 500 +## +#compilation_length_limit: 500 + ######################################### # # Default user preferences diff --git a/locales/en-US.json b/locales/en-US.json index e206bc0e5..3905de6ec 100644 --- a/locales/en-US.json +++ b/locales/en-US.json @@ -177,6 +177,7 @@ "Create compilation": "Create compilation", "Title": "Title", "Playlist privacy": "Playlist privacy", + "Compilation privacy": "Compilation privacy", "Editing playlist `x`": "Editing playlist `x`", "playlist_button_add_items": "Add videos", "Show more": "Show more", diff --git a/src/invidious/routes/compilations.cr b/src/invidious/routes/compilations.cr new file mode 100644 index 000000000..0189b60bc --- /dev/null +++ b/src/invidious/routes/compilations.cr @@ -0,0 +1,410 @@ +{% skip_file if flag?(:api_only) %} + +module Invidious::Routes::Compilations + def self.new(env) + locale = 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_compilation"}, HMAC_KEY) + + templated "create_compilation" + end + + def self.create(env) + locale = 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, 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 = CompilationPrivacy.parse?(env.params.body["privacy"]?.try &.as(String) || "") + if !privacy + return error_template(400, "Invalid privacy setting.") + end + + if Invidious::Database::Compilations.count_owned_by(user.email) >= 100 + return error_template(400, "User cannot have more than 100 compilations.") + end + + compilation = create_compilation(title, privacy, user) + + env.redirect "/compilation?list=#{compilation.id}" + end + + def self.delete_page(env) + locale = 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) + + compid = env.params.query["list"]? + if !compid || compid.empty? + return error_template(400, "A compilation ID is required") + end + + compilation = Invidious::Database::Compilations.select(id: compid) + if !compilation || compilation.author != user.email + return env.redirect referer + end + + csrf_token = generate_response(sid, {":delete_compilation"}, HMAC_KEY) + + templated "delete_compilation" + end + + def self.delete(env) + locale = env.get("preferences").as(Preferences).locale + + user = env.get? "user" + sid = env.get? "sid" + referer = get_referer(env) + + return env.redirect "/" if user.nil? + + compid = env.params.query["list"]? + return env.redirect referer if compid.nil? + + user = user.as(User) + sid = sid.as(String) + token = env.params.body["csrf_token"]? + + begin + validate_request(token, sid, env.request, HMAC_KEY, locale) + rescue ex + return error_template(400, ex) + end + + compilation = Invidious::Database::Compilations.select(id: compid) + if !compilation || compilation.author != user.email + return env.redirect referer + end + + Invidious::Database::Compilations.delete(compid) + + env.redirect "/feed/compilations" + end + + def self.edit(env) + locale = 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) + + compid = env.params.query["list"]? + if !compid || !compid.starts_with?("IV") + return env.redirect referer + end + + page = env.params.query["page"]?.try &.to_i? + page ||= 1 + + compilation = Invidious::Database::Compilations.select(id: compid) + if !compilation || compilation.author != user.email + return env.redirect referer + end + + begin + videos = get_compilation_videos(compilation, offset: (page - 1) * 100) + rescue ex + videos = [] of CompilationVideo + end + + csrf_token = generate_response(sid, {":edit_compilation"}, HMAC_KEY) + + templated "edit_compilation" + end + + def self.update(env) + locale = env.get("preferences").as(Preferences).locale + + user = env.get? "user" + sid = env.get? "sid" + referer = get_referer(env) + + return env.redirect "/" if user.nil? + + compid = env.params.query["list"]? + return env.redirect referer if compid.nil? + + user = user.as(User) + sid = sid.as(String) + token = env.params.body["csrf_token"]? + + begin + validate_request(token, sid, env.request, HMAC_KEY, locale) + rescue ex + return error_template(400, ex) + end + + compilation = Invidious::Database::Compilations.select(id: compid) + if !compilation || compilation.author != user.email + return env.redirect referer + end + + title = env.params.body["title"]?.try &.delete("<>") || "" + privacy = CompilationPrivacy.parse(env.params.body["privacy"]? || "Unlisted") + description = env.params.body["description"]?.try &.delete("\r") || "" + + if title != compilation.title || + compilation != compilation.privacy || + description != compilation.description + updated = Time.utc + else + updated = compilation.updated + end + + Invidious::Database::Compilations.update(compid, title, privacy, description, updated) + + env.redirect "/compilation?list=#{compid}" + end + + def self.add_compilation_items_page(env) + prefs = env.get("preferences").as(Preferences) + locale = prefs.locale + + region = env.params.query["region"]? || prefs.region + + 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) + + compid = env.params.query["list"]? + if !compid || !compid.starts_with?("IV") + return env.redirect referer + end + + page = env.params.query["page"]?.try &.to_i? + page ||= 1 + + compilation = Invidious::Database::Compilations.select(id: compid) + if !compilation || compilation.author != user.email + return env.redirect referer + end + + begin + query = Invidious::Search::Query.new(env.params.query, :compilation, region) + videos = query.process.select(SearchVideo).map(&.as(SearchVideo)) + rescue ex + videos = [] of SearchVideo + end + + env.set "add_compilation_items", compid + templated "add_compilation_items" + end + + def self.compilation_ajax(env) + locale = 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, locale) + rescue ex + if redirect + return error_template(400, ex) + else + return error_json(400, ex) + end + end + + if env.params.query["action_create_compilation"]? + action = "action_create_compilation" + elsif env.params.query["action_delete_compilation"]? + action = "action_delete_compilation" + elsif env.params.query["action_edit_compilation"]? + action = "action_edit_compilation" + 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 + compilation_id = env.params.query["compilation_id"] + compilation = get_compilation(compilation_id).as(InvidiousCompilation) + raise "Invalid user" if compilation.author != user.email + rescue ex : NotFoundException + return error_json(404, ex) + rescue ex + if redirect + return error_template(400, ex) + else + return error_json(400, ex) + end + end + + email = user.email + + case action + when "action_edit_compilation" + # TODO: Compilation stub + when "action_add_video" + if compilation.index.size >= CONFIG.compilation_length_limit + if redirect + return error_template(400, "Compilation cannot have more than #{CONFIG.compilation_length_limit} videos") + else + return error_json(400, "Compilation cannot have more than #{CONFIG.compilation_length_limit} videos") + end + end + + video_id = env.params.query["video_id"] + + begin + video = get_video(video_id) + rescue ex : NotFoundException + return error_json(404, ex) + rescue ex + if redirect + return error_template(500, ex) + else + return error_json(500, ex) + end + end + + compilation_video = CompilationVideo.new({ + title: video.title, + id: video.id, + author: video.author, + ucid: video.ucid, + length_seconds: video.length_seconds, + starting_timestamp_seconds: video.length_seconds, + ending_timestamp_seconds: video.length_seconds, + published: video.published, + compid: compilation_id, + live_now: video.live_now, + index: Random::Secure.rand(0_i64..Int64::MAX), + }) + + Invidious::Database::CompilationVideos.insert(compilation_video) + Invidious::Database::Compilations.update_video_added(compilation_id, compilation_video.index) + when "action_remove_video" + index = env.params.query["set_video_id"] + Invidious::Database::CompilationVideos.delete(index) + Invidious::Database::Compilations.update_video_removed(compilation_id, index) + when "action_move_video_before" + # TODO: Compilation 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 self.show(env) + locale = env.get("preferences").as(Preferences).locale + + user = env.get?("user").try &.as(User) + referer = get_referer(env) + + compid = env.params.query["list"]?.try &.gsub(/[^a-zA-Z0-9_-]/, "") + if !compid + return env.redirect "/" + end + + page = env.params.query["page"]?.try &.to_i? + page ||= 1 + + if compid.starts_with? "RD" + return env.redirect "/mix?list=#{compid}" + end + + begin + compilation = get_compilation(compid) + rescue ex : NotFoundException + return error_template(404, ex) + rescue ex + return error_template(500, ex) + end + + page_count = (compilation.video_count / 200).to_i + page_count += 1 if (compilation.video_count % 200) > 0 + + if page > page_count + return env.redirect "/compilation?list=#{compid}&page=#{page_count}" + end + + if compilation.privacy == CompilationPrivacy::Private && compilation.author != user.try &.email + return error_template(403, "This compilation is private.") + end + + begin + videos = get_compilation_videos(compilation, offset: (page - 1) * 200) + rescue ex + return error_template(500, "Error encountered while retrieving compilation videos.
#{ex.message}") + end + + if compilation.author == user.try &.email + env.set "remove_compilation_items", compid + end + + templated "compilation" + end +end \ No newline at end of file diff --git a/src/invidious/views/add_compilation_items.ecr b/src/invidious/views/add_compilation_items.ecr new file mode 100644 index 000000000..28494d983 --- /dev/null +++ b/src/invidious/views/add_compilation_items.ecr @@ -0,0 +1,40 @@ +<% content_for "header" do %> +<%= compilation.title %> - Invidious + +<% end %> + +
+
+
+
+
+ <%= translate(locale, "Editing compilation `x`", %|"#{HTML.escape(compilation.title)}"|) %> + +
+ value="<%= HTML.escape(query.text) %>"<% end %> + placeholder="<%= translate(locale, "Search for videos") %>"> + +
+
+
+
+
+
+ + + + +
+ <% videos.each_slice(4) do |slice| %> + <% slice.each do |item| %> + <%= rendered "components/item" %> + <% end %> + <% end %> +
\ No newline at end of file diff --git a/src/invidious/views/components/feed_menu.ecr b/src/invidious/views/components/feed_menu.ecr index 3fb200866..ff3b2474d 100644 --- a/src/invidious/views/components/feed_menu.ecr +++ b/src/invidious/views/components/feed_menu.ecr @@ -1,7 +1,7 @@
<% feed_menu = env.get("preferences").as(Preferences).feed_menu.dup %> <% if !env.get?("user") %> - <% feed_menu.reject! {|item| {"Subscriptions", "Playlists"}.includes? item} %> + <% feed_menu.reject! {|item| {"Subscriptions", "Playlists", "Compilations"}.includes? item} %> <% end %> <% feed_menu.each do |feed| %> diff --git a/src/invidious/views/create_compilation.ecr b/src/invidious/views/create_compilation.ecr index 296d873e7..4d8fc03c6 100644 --- a/src/invidious/views/create_compilation.ecr +++ b/src/invidious/views/create_compilation.ecr @@ -1,3 +1,39 @@ <% content_for "header" do %> <%= translate(locale, "Create compilation") %> - Invidious -<% end %> \ No newline at end of file +<% end %> + +
+
+
+
+
+
+ <%= translate(locale, "Create compilation") %> + +
+ + "> +
+ +
+ + +
+ +
+ +
+ + +
+
+
+
+
+