mirror of
				https://gitea.invidious.io/iv-org/invidious
				synced 2025-06-05 23:29:12 +02:00 
			
		
		
		
	Merge pull request #2666 from matthewmcgarvey/extract-stuff-1
Move more routes to new pattern
This commit is contained in:
		
							
								
								
									
										507
									
								
								src/invidious.cr
									
									
									
									
									
								
							
							
						
						
									
										507
									
								
								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 | ||||
|  | ||||
|   | ||||
							
								
								
									
										78
									
								
								src/invidious/routes/notifications.cr
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										78
									
								
								src/invidious/routes/notifications.cr
									
									
									
									
									
										Normal file
									
								
							| @@ -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 | ||||
| @@ -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 | ||||
|   | ||||
							
								
								
									
										168
									
								
								src/invidious/routes/subscriptions.cr
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										168
									
								
								src/invidious/routes/subscriptions.cr
									
									
									
									
									
										Normal file
									
								
							| @@ -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 | ||||
| @@ -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 | ||||
|   | ||||
		Reference in New Issue
	
	Block a user