summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--src/invidious.cr507
-rw-r--r--src/invidious/routes/notifications.cr78
-rw-r--r--src/invidious/routes/preferences.cr187
-rw-r--r--src/invidious/routes/subscriptions.cr168
-rw-r--r--src/invidious/routes/watch.cr66
5 files changed, 507 insertions, 499 deletions
diff --git a/src/invidious.cr b/src/invidious.cr
index 7a324bd1..01cfcae3 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
@@ -390,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
@@ -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>[^"]+)"/)
- 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(%(<?xml version="1.0"?>\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":"(?<channel_id>[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\/(?<channel>UC[a-zA-Z0-9_-]{22})/)
- next match["channel"]
- elsif match = channel["url"].as_s.match(/\/user\/(?<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>[^"]+)"/)
+ 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..faae03bc 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"]
+
+ 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
+ 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":"(?<channel_id>[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\/(?<channel>UC[a-zA-Z0-9_-]{22})/)
+ next match["channel"]
+ elsif match = channel["url"].as_s.match(/\/user\/(?<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
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(%(<?xml version="1.0"?>\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