From d755d05f88232be3e8290eb7d6a1ac363b93b735 Mon Sep 17 00:00:00 2001 From: matthewmcgarvey Date: Sat, 27 Nov 2021 01:16:09 -0600 Subject: [PATCH 1/3] Move more routes to new pattern --- src/invidious.cr | 507 +------------------------- src/invidious/routes/notifications.cr | 78 ++++ src/invidious/routes/preferences.cr | 187 ++++++++++ src/invidious/routes/subscriptions.cr | 168 +++++++++ src/invidious/routes/watch.cr | 66 ++++ 5 files changed, 507 insertions(+), 499 deletions(-) create mode 100644 src/invidious/routes/notifications.cr create mode 100644 src/invidious/routes/subscriptions.cr diff --git a/src/invidious.cr b/src/invidious.cr index 7a324bd1..0a3bad8b 100644 --- a/src/invidious.cr +++ b/src/invidious.cr @@ -339,6 +339,7 @@ end end Invidious::Routing.get "/watch", Invidious::Routes::Watch, :handle + Invidious::Routing.post "/watch_ajax", Invidious::Routes::Watch, :mark_watched Invidious::Routing.get "/watch/:id", Invidious::Routes::Watch, :redirect Invidious::Routing.get "/shorts/:id", Invidious::Routes::Watch, :redirect Invidious::Routing.get "/w/:id", Invidious::Routes::Watch, :redirect @@ -372,6 +373,8 @@ end Invidious::Routing.get "/preferences", Invidious::Routes::PreferencesRoute, :show Invidious::Routing.post "/preferences", Invidious::Routes::PreferencesRoute, :update Invidious::Routing.get "/toggle_theme", Invidious::Routes::PreferencesRoute, :toggle_theme + Invidious::Routing.get "/data_control", Invidious::Routes::PreferencesRoute, :data_control + Invidious::Routing.post "/data_control", Invidious::Routes::PreferencesRoute, :update_data_control # Feeds Invidious::Routing.get "/view_all_playlists", Invidious::Routes::Feeds, :view_all_playlists_redirect @@ -399,6 +402,11 @@ Invidious::Routing.get "/s_p/:id/:name", Invidious::Routes::Images, :s_p_image Invidious::Routing.get "/yts/img/:name", Invidious::Routes::Images, :yts_image Invidious::Routing.get "/vi/:id/:name", Invidious::Routes::Images, :thumbnails +Invidious::Routing.get "/modify_notifications", Invidious::Routes::Notifications, :modify + +Invidious::Routing.post "/subscription_ajax", Invidious::Routes::Subscriptions, :toggle_subscription +Invidious::Routing.get "/subscription_manager", Invidious::Routes::Subscriptions, :subscription_manager + # API routes (macro) define_v1_api_routes() @@ -406,505 +414,6 @@ define_v1_api_routes() define_api_manifest_routes() define_video_playback_routes() -# Users - -post "/watch_ajax" do |env| - locale = env.get("preferences").as(Preferences).locale - - user = env.get? "user" - sid = env.get? "sid" - referer = get_referer(env, "/feed/subscriptions") - - redirect = env.params.query["redirect"]? - redirect ||= "true" - redirect = redirect == "true" - - if !user - if redirect - next env.redirect referer - else - next error_json(403, "No such user") - end - end - - user = user.as(User) - sid = sid.as(String) - token = env.params.body["csrf_token"]? - - id = env.params.query["id"]? - if !id - env.response.status_code = 400 - next - end - - begin - validate_request(token, sid, env.request, HMAC_KEY, locale) - rescue ex - if redirect - next error_template(400, ex) - else - next error_json(400, ex) - end - end - - if env.params.query["action_mark_watched"]? - action = "action_mark_watched" - elsif env.params.query["action_mark_unwatched"]? - action = "action_mark_unwatched" - else - next env.redirect referer - end - - case action - when "action_mark_watched" - if !user.watched.includes? id - Invidious::Database::Users.mark_watched(user, id) - end - when "action_mark_unwatched" - Invidious::Database::Users.mark_unwatched(user, id) - else - next error_json(400, "Unsupported action #{action}") - end - - if redirect - env.redirect referer - else - env.response.content_type = "application/json" - "{}" - end -end - -# /modify_notifications -# will "ding" all subscriptions. -# /modify_notifications?receive_all_updates=false&receive_no_updates=false -# will "unding" all subscriptions. -get "/modify_notifications" do |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 ||= "false" - redirect = redirect == "true" - - if !user - if redirect - next env.redirect referer - else - next error_json(403, "No such user") - end - end - - user = user.as(User) - - if !user.password - channel_req = {} of String => String - - channel_req["receive_all_updates"] = env.params.query["receive_all_updates"]? || "true" - channel_req["receive_no_updates"] = env.params.query["receive_no_updates"]? || "" - channel_req["receive_post_updates"] = env.params.query["receive_post_updates"]? || "true" - - channel_req.reject! { |k, v| v != "true" && v != "false" } - - headers = HTTP::Headers.new - headers["Cookie"] = env.request.headers["Cookie"] - - html = YT_POOL.client &.get("/subscription_manager?disable_polymer=1", headers) - - cookies = HTTP::Cookies.from_client_headers(headers) - html.cookies.each do |cookie| - if {"VISITOR_INFO1_LIVE", "YSC", "SIDCC"}.includes? cookie.name - if cookies[cookie.name]? - cookies[cookie.name] = cookie - else - cookies << cookie - end - end - end - headers = cookies.add_request_headers(headers) - - if match = html.body.match(/'XSRF_TOKEN': "(?[^"]+)"/) - session_token = match["session_token"] - else - next env.redirect referer - end - - headers["content-type"] = "application/x-www-form-urlencoded" - channel_req["session_token"] = session_token - - subs = XML.parse_html(html.body) - subs.xpath_nodes(%q(//a[@class="subscription-title yt-uix-sessionlink"]/@href)).each do |channel| - channel_id = channel.content.lstrip("/channel/").not_nil! - channel_req["channel_id"] = channel_id - - YT_POOL.client &.post("/subscription_ajax?action_update_subscription_preferences=1", headers, form: channel_req) - end - end - - if redirect - env.redirect referer - else - env.response.content_type = "application/json" - "{}" - end -end - -post "/subscription_ajax" do |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 - next env.redirect referer - else - next 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 - next error_template(400, ex) - else - next error_json(400, ex) - end - end - - if env.params.query["action_create_subscription_to_channel"]?.try &.to_i?.try &.== 1 - action = "action_create_subscription_to_channel" - elsif env.params.query["action_remove_subscriptions"]?.try &.to_i?.try &.== 1 - action = "action_remove_subscriptions" - else - next env.redirect referer - end - - channel_id = env.params.query["c"]? - channel_id ||= "" - - if !user.password - # Sync subscriptions with YouTube - subscribe_ajax(channel_id, action, env.request.headers) - end - - case action - when "action_create_subscription_to_channel" - if !user.subscriptions.includes? channel_id - get_channel(channel_id, false, false) - Invidious::Database::Users.subscribe_channel(user, channel_id) - end - when "action_remove_subscriptions" - Invidious::Database::Users.unsubscribe_channel(user, channel_id) - else - next error_json(400, "Unsupported action #{action}") - end - - if redirect - env.redirect referer - else - env.response.content_type = "application/json" - "{}" - end -end - -get "/subscription_manager" do |env| - locale = env.get("preferences").as(Preferences).locale - - user = env.get? "user" - sid = env.get? "sid" - referer = get_referer(env) - - if !user - next env.redirect referer - end - - user = user.as(User) - sid = sid.as(String) - - if !user.password - # Refresh account - headers = HTTP::Headers.new - headers["Cookie"] = env.request.headers["Cookie"] - - user, sid = get_user(sid, headers) - end - - action_takeout = env.params.query["action_takeout"]?.try &.to_i? - action_takeout ||= 0 - action_takeout = action_takeout == 1 - - format = env.params.query["format"]? - format ||= "rss" - - subscriptions = Invidious::Database::Channels.select(user.subscriptions) - subscriptions.sort_by!(&.author.downcase) - - if action_takeout - if format == "json" - env.response.content_type = "application/json" - env.response.headers["content-disposition"] = "attachment" - playlists = Invidious::Database::Playlists.select_like_iv(user.email) - - next JSON.build do |json| - json.object do - json.field "subscriptions", user.subscriptions - json.field "watch_history", user.watched - json.field "preferences", user.preferences - json.field "playlists" do - json.array do - playlists.each do |playlist| - json.object do - json.field "title", playlist.title - json.field "description", html_to_content(playlist.description_html) - json.field "privacy", playlist.privacy.to_s - json.field "videos" do - json.array do - Invidious::Database::PlaylistVideos.select_ids(playlist.id, playlist.index, limit: 500).each do |video_id| - json.string video_id - end - end - end - end - end - end - end - end - end - else - env.response.content_type = "application/xml" - env.response.headers["content-disposition"] = "attachment" - export = XML.build do |xml| - xml.element("opml", version: "1.1") do - xml.element("body") do - if format == "newpipe" - title = "YouTube Subscriptions" - else - title = "Invidious Subscriptions" - end - - xml.element("outline", text: title, title: title) do - subscriptions.each do |channel| - if format == "newpipe" - xml_url = "https://www.youtube.com/feeds/videos.xml?channel_id=#{channel.id}" - else - xml_url = "#{HOST_URL}/feed/channel/#{channel.id}" - end - - xml.element("outline", text: channel.author, title: channel.author, - "type": "rss", xmlUrl: xml_url) - end - end - end - end - end - - next export.gsub(%(\n), "") - end - end - - templated "subscription_manager" -end - -get "/data_control" do |env| - locale = env.get("preferences").as(Preferences).locale - - user = env.get? "user" - referer = get_referer(env) - - if !user - next env.redirect referer - end - - user = user.as(User) - - templated "data_control" -end - -post "/data_control" do |env| - locale = env.get("preferences").as(Preferences).locale - - user = env.get? "user" - referer = get_referer(env) - - if user - user = user.as(User) - - # TODO: Find a way to prevent browser timeout - - HTTP::FormData.parse(env.request) do |part| - body = part.body.gets_to_end - type = part.headers["Content-Type"] - - next if body.empty? - - # TODO: Unify into single import based on content-type - case part.name - when "import_invidious" - body = JSON.parse(body) - - if body["subscriptions"]? - user.subscriptions += body["subscriptions"].as_a.map(&.as_s) - user.subscriptions.uniq! - - user.subscriptions = get_batch_channels(user.subscriptions, false, false) - - Invidious::Database::Users.update_subscriptions(user) - end - - if body["watch_history"]? - user.watched += body["watch_history"].as_a.map(&.as_s) - user.watched.uniq! - Invidious::Database::Users.update_watch_history(user) - end - - if body["preferences"]? - user.preferences = Preferences.from_json(body["preferences"].to_json) - Invidious::Database::Users.update_preferences(user) - end - - if playlists = body["playlists"]?.try &.as_a? - playlists.each do |item| - title = item["title"]?.try &.as_s?.try &.delete("<>") - description = item["description"]?.try &.as_s?.try &.delete("\r") - privacy = item["privacy"]?.try &.as_s?.try { |privacy| PlaylistPrivacy.parse? privacy } - - next if !title - next if !description - next if !privacy - - playlist = create_playlist(title, privacy, user) - Invidious::Database::Playlists.update_description(playlist.id, description) - - videos = item["videos"]?.try &.as_a?.try &.each_with_index do |video_id, idx| - raise InfoException.new("Playlist cannot have more than 500 videos") if idx > 500 - - video_id = video_id.try &.as_s? - next if !video_id - - begin - video = get_video(video_id) - rescue ex - next - 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), - }) - - Invidious::Database::PlaylistVideos.insert(playlist_video) - Invidious::Database::Playlists.update_video_added(playlist.id, playlist_video.index) - end - end - end - when "import_youtube" - filename = part.filename || "" - extension = filename.split(".").last - - if extension == "xml" || type == "application/xml" || type == "text/xml" - subscriptions = XML.parse(body) - user.subscriptions += subscriptions.xpath_nodes(%q(//outline[@type="rss"])).map do |channel| - channel["xmlUrl"].match(/UC[a-zA-Z0-9_-]{22}/).not_nil![0] - end - elsif extension == "json" || type == "application/json" - subscriptions = JSON.parse(body) - user.subscriptions += subscriptions.as_a.compact_map do |entry| - entry["snippet"]["resourceId"]["channelId"].as_s - end - elsif extension == "csv" || type == "text/csv" - subscriptions = parse_subscription_export_csv(body) - user.subscriptions += subscriptions - else - halt(env, status_code: 415, - response: error_template(415, "Invalid subscription file uploaded") - ) - end - - user.subscriptions.uniq! - user.subscriptions = get_batch_channels(user.subscriptions, false, false) - - Invidious::Database::Users.update_subscriptions(user) - when "import_freetube" - user.subscriptions += body.scan(/"channelId":"(?[a-zA-Z0-9_-]{24})"/).map do |md| - md["channel_id"] - end - user.subscriptions.uniq! - - user.subscriptions = get_batch_channels(user.subscriptions, false, false) - - Invidious::Database::Users.update_subscriptions(user) - when "import_newpipe_subscriptions" - body = JSON.parse(body) - user.subscriptions += body["subscriptions"].as_a.compact_map do |channel| - if match = channel["url"].as_s.match(/\/channel\/(?UC[a-zA-Z0-9_-]{22})/) - next match["channel"] - elsif match = channel["url"].as_s.match(/\/user\/(?.+)/) - response = YT_POOL.client &.get("/user/#{match["user"]}?disable_polymer=1&hl=en&gl=US") - html = XML.parse_html(response.body) - ucid = html.xpath_node(%q(//link[@rel="canonical"])).try &.["href"].split("/")[-1] - next ucid if ucid - end - - nil - end - user.subscriptions.uniq! - - user.subscriptions = get_batch_channels(user.subscriptions, false, false) - - Invidious::Database::Users.update_subscriptions(user) - when "import_newpipe" - Compress::Zip::Reader.open(IO::Memory.new(body)) do |file| - file.each_entry do |entry| - if entry.filename == "newpipe.db" - tempfile = File.tempfile(".db") - File.write(tempfile.path, entry.io.gets_to_end) - db = DB.open("sqlite3://" + tempfile.path) - - user.watched += db.query_all("SELECT url FROM streams", as: String).map(&.lchop("https://www.youtube.com/watch?v=")) - user.watched.uniq! - - Invidious::Database::Users.update_watch_history(user) - - user.subscriptions += db.query_all("SELECT url FROM subscriptions", as: String).map(&.lchop("https://www.youtube.com/channel/")) - user.subscriptions.uniq! - - user.subscriptions = get_batch_channels(user.subscriptions, false, false) - - Invidious::Database::Users.update_subscriptions(user) - - db.close - tempfile.delete - end - end - end - else nil # Ignore - end - end - end - - env.redirect referer -end - get "/change_password" do |env| locale = env.get("preferences").as(Preferences).locale diff --git a/src/invidious/routes/notifications.cr b/src/invidious/routes/notifications.cr new file mode 100644 index 00000000..272a3dc7 --- /dev/null +++ b/src/invidious/routes/notifications.cr @@ -0,0 +1,78 @@ +module Invidious::Routes::Notifications + # /modify_notifications + # will "ding" all subscriptions. + # /modify_notifications?receive_all_updates=false&receive_no_updates=false + # will "unding" all subscriptions. + def self.modify(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 ||= "false" + 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) + + if !user.password + channel_req = {} of String => String + + channel_req["receive_all_updates"] = env.params.query["receive_all_updates"]? || "true" + channel_req["receive_no_updates"] = env.params.query["receive_no_updates"]? || "" + channel_req["receive_post_updates"] = env.params.query["receive_post_updates"]? || "true" + + channel_req.reject! { |k, v| v != "true" && v != "false" } + + headers = HTTP::Headers.new + headers["Cookie"] = env.request.headers["Cookie"] + + html = YT_POOL.client &.get("/subscription_manager?disable_polymer=1", headers) + + cookies = HTTP::Cookies.from_client_headers(headers) + html.cookies.each do |cookie| + if {"VISITOR_INFO1_LIVE", "YSC", "SIDCC"}.includes? cookie.name + if cookies[cookie.name]? + cookies[cookie.name] = cookie + else + cookies << cookie + end + end + end + headers = cookies.add_request_headers(headers) + + if match = html.body.match(/'XSRF_TOKEN': "(?[^"]+)"/) + session_token = match["session_token"] + else + return env.redirect referer + end + + headers["content-type"] = "application/x-www-form-urlencoded" + channel_req["session_token"] = session_token + + subs = XML.parse_html(html.body) + subs.xpath_nodes(%q(//a[@class="subscription-title yt-uix-sessionlink"]/@href)).each do |channel| + channel_id = channel.content.lstrip("/channel/").not_nil! + channel_req["channel_id"] = channel_id + + YT_POOL.client &.post("/subscription_ajax?action_update_subscription_preferences=1", headers, form: channel_req) + end + end + + if redirect + env.redirect referer + else + env.response.content_type = "application/json" + "{}" + end + end +end diff --git a/src/invidious/routes/preferences.cr b/src/invidious/routes/preferences.cr index a832076c..f7bc5a07 100644 --- a/src/invidious/routes/preferences.cr +++ b/src/invidious/routes/preferences.cr @@ -285,4 +285,191 @@ module Invidious::Routes::PreferencesRoute "{}" end end + + def self.data_control(env) + locale = env.get("preferences").as(Preferences).locale + + user = env.get? "user" + referer = get_referer(env) + + if !user + return env.redirect referer + end + + user = user.as(User) + + templated "data_control" + end + + def self.update_data_control(env) + locale = env.get("preferences").as(Preferences).locale + + user = env.get? "user" + referer = get_referer(env) + + if user + user = user.as(User) + + # TODO: Find a way to prevent browser timeout + + HTTP::FormData.parse(env.request) do |part| + body = part.body.gets_to_end + type = part.headers["Content-Type"] + + return if body.empty? + + # TODO: Unify into single import based on content-type + case part.name + when "import_invidious" + body = JSON.parse(body) + + if body["subscriptions"]? + user.subscriptions += body["subscriptions"].as_a.map(&.as_s) + user.subscriptions.uniq! + + user.subscriptions = get_batch_channels(user.subscriptions, false, false) + + Invidious::Database::Users.update_subscriptions(user) + end + + if body["watch_history"]? + user.watched += body["watch_history"].as_a.map(&.as_s) + user.watched.uniq! + Invidious::Database::Users.update_watch_history(user) + end + + if body["preferences"]? + user.preferences = Preferences.from_json(body["preferences"].to_json) + Invidious::Database::Users.update_preferences(user) + end + + if playlists = body["playlists"]?.try &.as_a? + playlists.each do |item| + title = item["title"]?.try &.as_s?.try &.delete("<>") + description = item["description"]?.try &.as_s?.try &.delete("\r") + privacy = item["privacy"]?.try &.as_s?.try { |privacy| PlaylistPrivacy.parse? privacy } + + return if !title + return if !description + return if !privacy + + playlist = create_playlist(title, privacy, user) + Invidious::Database::Playlists.update_description(playlist.id, description) + + videos = item["videos"]?.try &.as_a?.try &.each_with_index do |video_id, idx| + raise InfoException.new("Playlist cannot have more than 500 videos") if idx > 500 + + video_id = video_id.try &.as_s? + return if !video_id + + begin + video = get_video(video_id) + rescue ex + return + 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), + }) + + Invidious::Database::PlaylistVideos.insert(playlist_video) + Invidious::Database::Playlists.update_video_added(playlist.id, playlist_video.index) + end + end + end + when "import_youtube" + filename = part.filename || "" + extension = filename.split(".").last + + if extension == "xml" || type == "application/xml" || type == "text/xml" + subscriptions = XML.parse(body) + user.subscriptions += subscriptions.xpath_nodes(%q(//outline[@type="rss"])).map do |channel| + channel["xmlUrl"].match(/UC[a-zA-Z0-9_-]{22}/).not_nil![0] + end + elsif extension == "json" || type == "application/json" + subscriptions = JSON.parse(body) + user.subscriptions += subscriptions.as_a.compact_map do |entry| + entry["snippet"]["resourceId"]["channelId"].as_s + end + elsif extension == "csv" || type == "text/csv" + subscriptions = parse_subscription_export_csv(body) + user.subscriptions += subscriptions + else + haltf(env, status_code: 415, + response: error_template(415, "Invalid subscription file uploaded") + ) + end + + user.subscriptions.uniq! + user.subscriptions = get_batch_channels(user.subscriptions, false, false) + + Invidious::Database::Users.update_subscriptions(user) + when "import_freetube" + user.subscriptions += body.scan(/"channelId":"(?[a-zA-Z0-9_-]{24})"/).map do |md| + md["channel_id"] + end + user.subscriptions.uniq! + + user.subscriptions = get_batch_channels(user.subscriptions, false, false) + + Invidious::Database::Users.update_subscriptions(user) + when "import_newpipe_subscriptions" + body = JSON.parse(body) + user.subscriptions += body["subscriptions"].as_a.compact_map do |channel| + if match = channel["url"].as_s.match(/\/channel\/(?UC[a-zA-Z0-9_-]{22})/) + return match["channel"] + elsif match = channel["url"].as_s.match(/\/user\/(?.+)/) + response = YT_POOL.client &.get("/user/#{match["user"]}?disable_polymer=1&hl=en&gl=US") + html = XML.parse_html(response.body) + ucid = html.xpath_node(%q(//link[@rel="canonical"])).try &.["href"].split("/")[-1] + return ucid if ucid + end + + nil + end + user.subscriptions.uniq! + + user.subscriptions = get_batch_channels(user.subscriptions, false, false) + + Invidious::Database::Users.update_subscriptions(user) + when "import_newpipe" + Compress::Zip::Reader.open(IO::Memory.new(body)) do |file| + file.each_entry do |entry| + if entry.filename == "newpipe.db" + tempfile = File.tempfile(".db") + File.write(tempfile.path, entry.io.gets_to_end) + db = DB.open("sqlite3://" + tempfile.path) + + user.watched += db.query_all("SELECT url FROM streams", as: String).map(&.lchop("https://www.youtube.com/watch?v=")) + user.watched.uniq! + + Invidious::Database::Users.update_watch_history(user) + + user.subscriptions += db.query_all("SELECT url FROM subscriptions", as: String).map(&.lchop("https://www.youtube.com/channel/")) + user.subscriptions.uniq! + + user.subscriptions = get_batch_channels(user.subscriptions, false, false) + + Invidious::Database::Users.update_subscriptions(user) + + db.close + tempfile.delete + end + end + end + else nil # Ignore + end + end + end + + env.redirect referer + end end diff --git a/src/invidious/routes/subscriptions.cr b/src/invidious/routes/subscriptions.cr new file mode 100644 index 00000000..29152afb --- /dev/null +++ b/src/invidious/routes/subscriptions.cr @@ -0,0 +1,168 @@ +module Invidious::Routes::Subscriptions + def self.toggle_subscription(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_subscription_to_channel"]?.try &.to_i?.try &.== 1 + action = "action_create_subscription_to_channel" + elsif env.params.query["action_remove_subscriptions"]?.try &.to_i?.try &.== 1 + action = "action_remove_subscriptions" + else + return env.redirect referer + end + + channel_id = env.params.query["c"]? + channel_id ||= "" + + if !user.password + # Sync subscriptions with YouTube + subscribe_ajax(channel_id, action, env.request.headers) + end + + case action + when "action_create_subscription_to_channel" + if !user.subscriptions.includes? channel_id + get_channel(channel_id, false, false) + Invidious::Database::Users.subscribe_channel(user, channel_id) + end + when "action_remove_subscriptions" + Invidious::Database::Users.unsubscribe_channel(user, channel_id) + 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.subscription_manager(env) + locale = env.get("preferences").as(Preferences).locale + + user = env.get? "user" + sid = env.get? "sid" + referer = get_referer(env) + + if !user + return env.redirect referer + end + + user = user.as(User) + sid = sid.as(String) + + if !user.password + # Refresh account + headers = HTTP::Headers.new + headers["Cookie"] = env.request.headers["Cookie"] + + user, sid = get_user(sid, headers) + end + + action_takeout = env.params.query["action_takeout"]?.try &.to_i? + action_takeout ||= 0 + action_takeout = action_takeout == 1 + + format = env.params.query["format"]? + format ||= "rss" + + subscriptions = Invidious::Database::Channels.select(user.subscriptions) + subscriptions.sort_by!(&.author.downcase) + + if action_takeout + if format == "json" + env.response.content_type = "application/json" + env.response.headers["content-disposition"] = "attachment" + playlists = Invidious::Database::Playlists.select_like_iv(user.email) + + return JSON.build do |json| + json.object do + json.field "subscriptions", user.subscriptions + json.field "watch_history", user.watched + json.field "preferences", user.preferences + json.field "playlists" do + json.array do + playlists.each do |playlist| + json.object do + json.field "title", playlist.title + json.field "description", html_to_content(playlist.description_html) + json.field "privacy", playlist.privacy.to_s + json.field "videos" do + json.array do + Invidious::Database::PlaylistVideos.select_ids(playlist.id, playlist.index, limit: 500).each do |video_id| + json.string video_id + end + end + end + end + end + end + end + end + end + else + env.response.content_type = "application/xml" + env.response.headers["content-disposition"] = "attachment" + export = XML.build do |xml| + xml.element("opml", version: "1.1") do + xml.element("body") do + if format == "newpipe" + title = "YouTube Subscriptions" + else + title = "Invidious Subscriptions" + end + + xml.element("outline", text: title, title: title) do + subscriptions.each do |channel| + if format == "newpipe" + xml_url = "https://www.youtube.com/feeds/videos.xml?channel_id=#{channel.id}" + else + xml_url = "#{HOST_URL}/feed/channel/#{channel.id}" + end + + xml.element("outline", text: channel.author, title: channel.author, + "type": "rss", xmlUrl: xml_url) + end + end + end + end + end + + return export.gsub(%(\n), "") + end + end + + templated "subscription_manager" + end +end diff --git a/src/invidious/routes/watch.cr b/src/invidious/routes/watch.cr index 1198f48f..7d048ce8 100644 --- a/src/invidious/routes/watch.cr +++ b/src/invidious/routes/watch.cr @@ -200,4 +200,70 @@ module Invidious::Routes::Watch return env.redirect url end + + def self.mark_watched(env) + locale = env.get("preferences").as(Preferences).locale + + user = env.get? "user" + sid = env.get? "sid" + referer = get_referer(env, "/feed/subscriptions") + + 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"]? + + id = env.params.query["id"]? + if !id + env.response.status_code = 400 + return + end + + 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_mark_watched"]? + action = "action_mark_watched" + elsif env.params.query["action_mark_unwatched"]? + action = "action_mark_unwatched" + else + return env.redirect referer + end + + case action + when "action_mark_watched" + if !user.watched.includes? id + Invidious::Database::Users.mark_watched(user, id) + end + when "action_mark_unwatched" + Invidious::Database::Users.mark_unwatched(user, id) + else + return error_json(400, "Unsupported action #{action}") + end + + if redirect + env.redirect referer + else + env.response.content_type = "application/json" + "{}" + end + end end From 8ef1e81294a3426009e705a61a69ef91e1e8f7c2 Mon Sep 17 00:00:00 2001 From: matthewmcgarvey Date: Tue, 18 Jan 2022 18:34:32 -0600 Subject: [PATCH 2/3] Make certain routes ignored if api only --- src/invidious.cr | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/invidious.cr b/src/invidious.cr index 0a3bad8b..01cfcae3 100644 --- a/src/invidious.cr +++ b/src/invidious.cr @@ -393,6 +393,11 @@ end # Support push notifications via PubSubHubbub Invidious::Routing.get "/feed/webhook/:token", Invidious::Routes::Feeds, :push_notifications_get Invidious::Routing.post "/feed/webhook/:token", Invidious::Routes::Feeds, :push_notifications_post + + Invidious::Routing.get "/modify_notifications", Invidious::Routes::Notifications, :modify + + Invidious::Routing.post "/subscription_ajax", Invidious::Routes::Subscriptions, :toggle_subscription + Invidious::Routing.get "/subscription_manager", Invidious::Routes::Subscriptions, :subscription_manager {% end %} Invidious::Routing.get "/ggpht/*", Invidious::Routes::Images, :ggpht @@ -402,11 +407,6 @@ Invidious::Routing.get "/s_p/:id/:name", Invidious::Routes::Images, :s_p_image Invidious::Routing.get "/yts/img/:name", Invidious::Routes::Images, :yts_image Invidious::Routing.get "/vi/:id/:name", Invidious::Routes::Images, :thumbnails -Invidious::Routing.get "/modify_notifications", Invidious::Routes::Notifications, :modify - -Invidious::Routing.post "/subscription_ajax", Invidious::Routes::Subscriptions, :toggle_subscription -Invidious::Routing.get "/subscription_manager", Invidious::Routes::Subscriptions, :subscription_manager - # API routes (macro) define_v1_api_routes() From df9f897ebeb6db5779f3f489645be759c9ed9760 Mon Sep 17 00:00:00 2001 From: matthewmcgarvey Date: Tue, 25 Jan 2022 19:28:16 -0600 Subject: [PATCH 3/3] Fix code broken when extracting data control route --- src/invidious/routes/preferences.cr | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/invidious/routes/preferences.cr b/src/invidious/routes/preferences.cr index f7bc5a07..faae03bc 100644 --- a/src/invidious/routes/preferences.cr +++ b/src/invidious/routes/preferences.cr @@ -316,7 +316,7 @@ module Invidious::Routes::PreferencesRoute body = part.body.gets_to_end type = part.headers["Content-Type"] - return if body.empty? + next if body.empty? # TODO: Unify into single import based on content-type case part.name @@ -349,9 +349,9 @@ module Invidious::Routes::PreferencesRoute description = item["description"]?.try &.as_s?.try &.delete("\r") privacy = item["privacy"]?.try &.as_s?.try { |privacy| PlaylistPrivacy.parse? privacy } - return if !title - return if !description - return if !privacy + next if !title + next if !description + next if !privacy playlist = create_playlist(title, privacy, user) Invidious::Database::Playlists.update_description(playlist.id, description) @@ -360,12 +360,12 @@ module Invidious::Routes::PreferencesRoute raise InfoException.new("Playlist cannot have more than 500 videos") if idx > 500 video_id = video_id.try &.as_s? - return if !video_id + next if !video_id begin video = get_video(video_id) rescue ex - return + next end playlist_video = PlaylistVideo.new({ @@ -425,12 +425,12 @@ module Invidious::Routes::PreferencesRoute body = JSON.parse(body) user.subscriptions += body["subscriptions"].as_a.compact_map do |channel| if match = channel["url"].as_s.match(/\/channel\/(?UC[a-zA-Z0-9_-]{22})/) - return match["channel"] + next match["channel"] elsif match = channel["url"].as_s.match(/\/user\/(?.+)/) response = YT_POOL.client &.get("/user/#{match["user"]}?disable_polymer=1&hl=en&gl=US") html = XML.parse_html(response.body) ucid = html.xpath_node(%q(//link[@rel="canonical"])).try &.["href"].split("/")[-1] - return ucid if ucid + next ucid if ucid end nil