mirror of
				https://gitea.invidious.io/iv-org/invidious
				synced 2025-06-05 23:29:12 +02:00 
			
		
		
		
	Extract API routes from invidious.cr (3/3)
- Auth (excluding notifications*) APIs - Mixes *Notifications currently require the "connection_channel" channel for talking with the notifications job. Unfortunately, we cannot access that within the route modules yet.
This commit is contained in:
		
							
								
								
									
										529
									
								
								src/invidious.cr
									
									
									
									
									
								
							
							
						
						
									
										529
									
								
								src/invidious.cr
									
									
									
									
									
								
							| @@ -1639,132 +1639,12 @@ end | ||||
|   end | ||||
| end | ||||
|  | ||||
| # API Endpoints | ||||
|  | ||||
| {"/api/v1/playlists/:plid", "/api/v1/auth/playlists/:plid"}.each do |route| | ||||
|   get route do |env| | ||||
|     locale = LOCALES[env.get("preferences").as(Preferences).locale]? | ||||
|  | ||||
|     env.response.content_type = "application/json" | ||||
|     plid = env.params.url["plid"] | ||||
|  | ||||
|     offset = env.params.query["index"]?.try &.to_i? | ||||
|     offset ||= env.params.query["page"]?.try &.to_i?.try { |page| (page - 1) * 100 } | ||||
|     offset ||= 0 | ||||
|  | ||||
|     continuation = env.params.query["continuation"]? | ||||
|  | ||||
|     format = env.params.query["format"]? | ||||
|     format ||= "json" | ||||
|  | ||||
|     if plid.starts_with? "RD" | ||||
|       next env.redirect "/api/v1/mixes/#{plid}" | ||||
|     end | ||||
|  | ||||
|     begin | ||||
|       playlist = get_playlist(PG_DB, plid, locale) | ||||
|     rescue ex : InfoException | ||||
|       next error_json(404, ex) | ||||
|     rescue ex | ||||
|       next error_json(404, "Playlist does not exist.") | ||||
|     end | ||||
|  | ||||
|     user = env.get?("user").try &.as(User) | ||||
|     if !playlist || playlist.privacy.private? && playlist.author != user.try &.email | ||||
|       next error_json(404, "Playlist does not exist.") | ||||
|     end | ||||
|  | ||||
|     response = playlist.to_json(offset, locale, continuation: continuation) | ||||
|  | ||||
|     if format == "html" | ||||
|       response = JSON.parse(response) | ||||
|       playlist_html = template_playlist(response) | ||||
|       index, next_video = response["videos"].as_a.skip(1).select { |video| !video["author"].as_s.empty? }[0]?.try { |v| {v["index"], v["videoId"]} } || {nil, nil} | ||||
|  | ||||
|       response = { | ||||
|         "playlistHtml" => playlist_html, | ||||
|         "index"        => index, | ||||
|         "nextVideo"    => next_video, | ||||
|       }.to_json | ||||
|     end | ||||
|  | ||||
|     response | ||||
|   end | ||||
| end | ||||
|  | ||||
| get "/api/v1/mixes/:rdid" do |env| | ||||
|   locale = LOCALES[env.get("preferences").as(Preferences).locale]? | ||||
|  | ||||
|   env.response.content_type = "application/json" | ||||
|  | ||||
|   rdid = env.params.url["rdid"] | ||||
|  | ||||
|   continuation = env.params.query["continuation"]? | ||||
|   continuation ||= rdid.lchop("RD")[0, 11] | ||||
|  | ||||
|   format = env.params.query["format"]? | ||||
|   format ||= "json" | ||||
|  | ||||
|   begin | ||||
|     mix = fetch_mix(rdid, continuation, locale: locale) | ||||
|  | ||||
|     if !rdid.ends_with? continuation | ||||
|       mix = fetch_mix(rdid, mix.videos[1].id) | ||||
|       index = mix.videos.index(mix.videos.select { |video| video.id == continuation }[0]?) | ||||
|     end | ||||
|  | ||||
|     mix.videos = mix.videos[index..-1] | ||||
|   rescue ex | ||||
|     next error_json(500, ex) | ||||
|   end | ||||
|  | ||||
|   response = JSON.build do |json| | ||||
|     json.object do | ||||
|       json.field "title", mix.title | ||||
|       json.field "mixId", mix.id | ||||
|  | ||||
|       json.field "videos" do | ||||
|         json.array do | ||||
|           mix.videos.each do |video| | ||||
|             json.object do | ||||
|               json.field "title", video.title | ||||
|               json.field "videoId", video.id | ||||
|               json.field "author", video.author | ||||
|  | ||||
|               json.field "authorId", video.ucid | ||||
|               json.field "authorUrl", "/channel/#{video.ucid}" | ||||
|  | ||||
|               json.field "videoThumbnails" do | ||||
|                 json.array do | ||||
|                   generate_thumbnails(json, video.id) | ||||
|                 end | ||||
|               end | ||||
|  | ||||
|               json.field "index", video.index | ||||
|               json.field "lengthSeconds", video.length_seconds | ||||
|             end | ||||
|           end | ||||
|         end | ||||
|       end | ||||
|     end | ||||
|   end | ||||
|  | ||||
|   if format == "html" | ||||
|     response = JSON.parse(response) | ||||
|     playlist_html = template_mix(response) | ||||
|     next_video = response["videos"].as_a.select { |video| !video["author"].as_s.empty? }[0]?.try &.["videoId"] | ||||
|  | ||||
|     response = { | ||||
|       "playlistHtml" => playlist_html, | ||||
|       "nextVideo"    => next_video, | ||||
|     }.to_json | ||||
|   end | ||||
|  | ||||
|   response | ||||
| end | ||||
|  | ||||
| # Authenticated endpoints | ||||
|  | ||||
| # The notification APIs can't be extracted yet | ||||
| # due to the requirement of the `connection_channel` | ||||
| # used by the `NotificationJob` | ||||
|  | ||||
| get "/api/v1/auth/notifications" do |env| | ||||
|   env.response.content_type = "text/event-stream" | ||||
|  | ||||
| @@ -1783,407 +1663,6 @@ post "/api/v1/auth/notifications" do |env| | ||||
|   create_notification_stream(env, topics, connection_channel) | ||||
| end | ||||
|  | ||||
| get "/api/v1/auth/preferences" do |env| | ||||
|   env.response.content_type = "application/json" | ||||
|   user = env.get("user").as(User) | ||||
|   user.preferences.to_json | ||||
| end | ||||
|  | ||||
| post "/api/v1/auth/preferences" do |env| | ||||
|   env.response.content_type = "application/json" | ||||
|   user = env.get("user").as(User) | ||||
|  | ||||
|   begin | ||||
|     preferences = Preferences.from_json(env.request.body || "{}") | ||||
|   rescue | ||||
|     preferences = user.preferences | ||||
|   end | ||||
|  | ||||
|   PG_DB.exec("UPDATE users SET preferences = $1 WHERE email = $2", preferences.to_json, user.email) | ||||
|  | ||||
|   env.response.status_code = 204 | ||||
| end | ||||
|  | ||||
| get "/api/v1/auth/feed" do |env| | ||||
|   env.response.content_type = "application/json" | ||||
|  | ||||
|   user = env.get("user").as(User) | ||||
|   locale = LOCALES[env.get("preferences").as(Preferences).locale]? | ||||
|  | ||||
|   max_results = env.params.query["max_results"]?.try &.to_i? | ||||
|   max_results ||= user.preferences.max_results | ||||
|   max_results ||= CONFIG.default_user_preferences.max_results | ||||
|  | ||||
|   page = env.params.query["page"]?.try &.to_i? | ||||
|   page ||= 1 | ||||
|  | ||||
|   videos, notifications = get_subscription_feed(PG_DB, user, max_results, page) | ||||
|  | ||||
|   JSON.build do |json| | ||||
|     json.object do | ||||
|       json.field "notifications" do | ||||
|         json.array do | ||||
|           notifications.each do |video| | ||||
|             video.to_json(locale, json) | ||||
|           end | ||||
|         end | ||||
|       end | ||||
|  | ||||
|       json.field "videos" do | ||||
|         json.array do | ||||
|           videos.each do |video| | ||||
|             video.to_json(locale, json) | ||||
|           end | ||||
|         end | ||||
|       end | ||||
|     end | ||||
|   end | ||||
| end | ||||
|  | ||||
| get "/api/v1/auth/subscriptions" do |env| | ||||
|   env.response.content_type = "application/json" | ||||
|   user = env.get("user").as(User) | ||||
|  | ||||
|   if user.subscriptions.empty? | ||||
|     values = "'{}'" | ||||
|   else | ||||
|     values = "VALUES #{user.subscriptions.map { |id| %(('#{id}')) }.join(",")}" | ||||
|   end | ||||
|  | ||||
|   subscriptions = PG_DB.query_all("SELECT * FROM channels WHERE id = ANY(#{values})", as: InvidiousChannel) | ||||
|  | ||||
|   JSON.build do |json| | ||||
|     json.array do | ||||
|       subscriptions.each do |subscription| | ||||
|         json.object do | ||||
|           json.field "author", subscription.author | ||||
|           json.field "authorId", subscription.id | ||||
|         end | ||||
|       end | ||||
|     end | ||||
|   end | ||||
| end | ||||
|  | ||||
| post "/api/v1/auth/subscriptions/:ucid" do |env| | ||||
|   env.response.content_type = "application/json" | ||||
|   user = env.get("user").as(User) | ||||
|  | ||||
|   ucid = env.params.url["ucid"] | ||||
|  | ||||
|   if !user.subscriptions.includes? ucid | ||||
|     get_channel(ucid, PG_DB, false, false) | ||||
|     PG_DB.exec("UPDATE users SET feed_needs_update = true, subscriptions = array_append(subscriptions,$1) WHERE email = $2", ucid, user.email) | ||||
|   end | ||||
|  | ||||
|   # For Google accounts, access tokens don't have enough information to | ||||
|   # make a request on the user's behalf, which is why we don't sync with | ||||
|   # YouTube. | ||||
|  | ||||
|   env.response.status_code = 204 | ||||
| end | ||||
|  | ||||
| delete "/api/v1/auth/subscriptions/:ucid" do |env| | ||||
|   env.response.content_type = "application/json" | ||||
|   user = env.get("user").as(User) | ||||
|  | ||||
|   ucid = env.params.url["ucid"] | ||||
|  | ||||
|   PG_DB.exec("UPDATE users SET feed_needs_update = true, subscriptions = array_remove(subscriptions, $1) WHERE email = $2", ucid, user.email) | ||||
|  | ||||
|   env.response.status_code = 204 | ||||
| end | ||||
|  | ||||
| get "/api/v1/auth/playlists" do |env| | ||||
|   locale = LOCALES[env.get("preferences").as(Preferences).locale]? | ||||
|  | ||||
|   env.response.content_type = "application/json" | ||||
|   user = env.get("user").as(User) | ||||
|  | ||||
|   playlists = PG_DB.query_all("SELECT * FROM playlists WHERE author = $1", user.email, as: InvidiousPlaylist) | ||||
|  | ||||
|   JSON.build do |json| | ||||
|     json.array do | ||||
|       playlists.each do |playlist| | ||||
|         playlist.to_json(0, locale, json) | ||||
|       end | ||||
|     end | ||||
|   end | ||||
| end | ||||
|  | ||||
| post "/api/v1/auth/playlists" do |env| | ||||
|   env.response.content_type = "application/json" | ||||
|   user = env.get("user").as(User) | ||||
|   locale = LOCALES[env.get("preferences").as(Preferences).locale]? | ||||
|  | ||||
|   title = env.params.json["title"]?.try &.as(String).delete("<>").byte_slice(0, 150) | ||||
|   if !title | ||||
|     next error_json(400, "Invalid title.") | ||||
|   end | ||||
|  | ||||
|   privacy = env.params.json["privacy"]?.try { |privacy| PlaylistPrivacy.parse(privacy.as(String).downcase) } | ||||
|   if !privacy | ||||
|     next error_json(400, "Invalid privacy setting.") | ||||
|   end | ||||
|  | ||||
|   if PG_DB.query_one("SELECT count(*) FROM playlists WHERE author = $1", user.email, as: Int64) >= 100 | ||||
|     next error_json(400, "User cannot have more than 100 playlists.") | ||||
|   end | ||||
|  | ||||
|   playlist = create_playlist(PG_DB, title, privacy, user) | ||||
|   env.response.headers["Location"] = "#{HOST_URL}/api/v1/auth/playlists/#{playlist.id}" | ||||
|   env.response.status_code = 201 | ||||
|   { | ||||
|     "title"      => title, | ||||
|     "playlistId" => playlist.id, | ||||
|   }.to_json | ||||
| end | ||||
|  | ||||
| patch "/api/v1/auth/playlists/:plid" do |env| | ||||
|   locale = LOCALES[env.get("preferences").as(Preferences).locale]? | ||||
|  | ||||
|   env.response.content_type = "application/json" | ||||
|   user = env.get("user").as(User) | ||||
|  | ||||
|   plid = env.params.url["plid"] | ||||
|  | ||||
|   playlist = PG_DB.query_one?("SELECT * FROM playlists WHERE id = $1", plid, as: InvidiousPlaylist) | ||||
|   if !playlist || playlist.author != user.email && playlist.privacy.private? | ||||
|     next error_json(404, "Playlist does not exist.") | ||||
|   end | ||||
|  | ||||
|   if playlist.author != user.email | ||||
|     next error_json(403, "Invalid user") | ||||
|   end | ||||
|  | ||||
|   title = env.params.json["title"].try &.as(String).delete("<>").byte_slice(0, 150) || playlist.title | ||||
|   privacy = env.params.json["privacy"]?.try { |privacy| PlaylistPrivacy.parse(privacy.as(String).downcase) } || playlist.privacy | ||||
|   description = env.params.json["description"]?.try &.as(String).delete("\r") || playlist.description | ||||
|  | ||||
|   if title != playlist.title || | ||||
|      privacy != playlist.privacy || | ||||
|      description != playlist.description | ||||
|     updated = Time.utc | ||||
|   else | ||||
|     updated = playlist.updated | ||||
|   end | ||||
|  | ||||
|   PG_DB.exec("UPDATE playlists SET title = $1, privacy = $2, description = $3, updated = $4 WHERE id = $5", title, privacy, description, updated, plid) | ||||
|   env.response.status_code = 204 | ||||
| end | ||||
|  | ||||
| delete "/api/v1/auth/playlists/:plid" do |env| | ||||
|   locale = LOCALES[env.get("preferences").as(Preferences).locale]? | ||||
|  | ||||
|   env.response.content_type = "application/json" | ||||
|   user = env.get("user").as(User) | ||||
|  | ||||
|   plid = env.params.url["plid"] | ||||
|  | ||||
|   playlist = PG_DB.query_one?("SELECT * FROM playlists WHERE id = $1", plid, as: InvidiousPlaylist) | ||||
|   if !playlist || playlist.author != user.email && playlist.privacy.private? | ||||
|     next error_json(404, "Playlist does not exist.") | ||||
|   end | ||||
|  | ||||
|   if playlist.author != user.email | ||||
|     next error_json(403, "Invalid user") | ||||
|   end | ||||
|  | ||||
|   PG_DB.exec("DELETE FROM playlist_videos * WHERE plid = $1", plid) | ||||
|   PG_DB.exec("DELETE FROM playlists * WHERE id = $1", plid) | ||||
|  | ||||
|   env.response.status_code = 204 | ||||
| end | ||||
|  | ||||
| post "/api/v1/auth/playlists/:plid/videos" do |env| | ||||
|   locale = LOCALES[env.get("preferences").as(Preferences).locale]? | ||||
|  | ||||
|   env.response.content_type = "application/json" | ||||
|   user = env.get("user").as(User) | ||||
|  | ||||
|   plid = env.params.url["plid"] | ||||
|  | ||||
|   playlist = PG_DB.query_one?("SELECT * FROM playlists WHERE id = $1", plid, as: InvidiousPlaylist) | ||||
|   if !playlist || playlist.author != user.email && playlist.privacy.private? | ||||
|     next error_json(404, "Playlist does not exist.") | ||||
|   end | ||||
|  | ||||
|   if playlist.author != user.email | ||||
|     next error_json(403, "Invalid user") | ||||
|   end | ||||
|  | ||||
|   if playlist.index.size >= 500 | ||||
|     next error_json(400, "Playlist cannot have more than 500 videos") | ||||
|   end | ||||
|  | ||||
|   video_id = env.params.json["videoId"].try &.as(String) | ||||
|   if !video_id | ||||
|     next error_json(403, "Invalid videoId") | ||||
|   end | ||||
|  | ||||
|   begin | ||||
|     video = get_video(video_id, PG_DB) | ||||
|   rescue ex | ||||
|     next error_json(500, ex) | ||||
|   end | ||||
|  | ||||
|   playlist_video = PlaylistVideo.new({ | ||||
|     title:          video.title, | ||||
|     id:             video.id, | ||||
|     author:         video.author, | ||||
|     ucid:           video.ucid, | ||||
|     length_seconds: video.length_seconds, | ||||
|     published:      video.published, | ||||
|     plid:           plid, | ||||
|     live_now:       video.live_now, | ||||
|     index:          Random::Secure.rand(0_i64..Int64::MAX), | ||||
|   }) | ||||
|  | ||||
|   video_array = playlist_video.to_a | ||||
|   args = arg_array(video_array) | ||||
|  | ||||
|   PG_DB.exec("INSERT INTO playlist_videos VALUES (#{args})", args: video_array) | ||||
|   PG_DB.exec("UPDATE playlists SET index = array_append(index, $1), video_count = cardinality(index) + 1, updated = $2 WHERE id = $3", playlist_video.index, Time.utc, plid) | ||||
|  | ||||
|   env.response.headers["Location"] = "#{HOST_URL}/api/v1/auth/playlists/#{plid}/videos/#{playlist_video.index.to_u64.to_s(16).upcase}" | ||||
|   env.response.status_code = 201 | ||||
|   playlist_video.to_json(locale, index: playlist.index.size) | ||||
| end | ||||
|  | ||||
| delete "/api/v1/auth/playlists/:plid/videos/:index" do |env| | ||||
|   locale = LOCALES[env.get("preferences").as(Preferences).locale]? | ||||
|  | ||||
|   env.response.content_type = "application/json" | ||||
|   user = env.get("user").as(User) | ||||
|  | ||||
|   plid = env.params.url["plid"] | ||||
|   index = env.params.url["index"].to_i64(16) | ||||
|  | ||||
|   playlist = PG_DB.query_one?("SELECT * FROM playlists WHERE id = $1", plid, as: InvidiousPlaylist) | ||||
|   if !playlist || playlist.author != user.email && playlist.privacy.private? | ||||
|     next error_json(404, "Playlist does not exist.") | ||||
|   end | ||||
|  | ||||
|   if playlist.author != user.email | ||||
|     next error_json(403, "Invalid user") | ||||
|   end | ||||
|  | ||||
|   if !playlist.index.includes? index | ||||
|     next error_json(404, "Playlist does not contain index") | ||||
|   end | ||||
|  | ||||
|   PG_DB.exec("DELETE FROM playlist_videos * WHERE index = $1", index) | ||||
|   PG_DB.exec("UPDATE playlists SET index = array_remove(index, $1), video_count = cardinality(index) - 1, updated = $2 WHERE id = $3", index, Time.utc, plid) | ||||
|  | ||||
|   env.response.status_code = 204 | ||||
| end | ||||
|  | ||||
| # patch "/api/v1/auth/playlists/:plid/videos/:index" do |env| | ||||
| # TODO: Playlist stub | ||||
| # end | ||||
|  | ||||
| get "/api/v1/auth/tokens" do |env| | ||||
|   env.response.content_type = "application/json" | ||||
|   user = env.get("user").as(User) | ||||
|   scopes = env.get("scopes").as(Array(String)) | ||||
|  | ||||
|   tokens = PG_DB.query_all("SELECT id, issued FROM session_ids WHERE email = $1", user.email, as: {session: String, issued: Time}) | ||||
|  | ||||
|   JSON.build do |json| | ||||
|     json.array do | ||||
|       tokens.each do |token| | ||||
|         json.object do | ||||
|           json.field "session", token[:session] | ||||
|           json.field "issued", token[:issued].to_unix | ||||
|         end | ||||
|       end | ||||
|     end | ||||
|   end | ||||
| end | ||||
|  | ||||
| post "/api/v1/auth/tokens/register" do |env| | ||||
|   user = env.get("user").as(User) | ||||
|   locale = LOCALES[env.get("preferences").as(Preferences).locale]? | ||||
|  | ||||
|   case env.request.headers["Content-Type"]? | ||||
|   when "application/x-www-form-urlencoded" | ||||
|     scopes = env.params.body.select { |k, v| k.match(/^scopes\[\d+\]$/) }.map { |k, v| v } | ||||
|     callback_url = env.params.body["callbackUrl"]? | ||||
|     expire = env.params.body["expire"]?.try &.to_i? | ||||
|   when "application/json" | ||||
|     scopes = env.params.json["scopes"].as(Array).map { |v| v.as_s } | ||||
|     callback_url = env.params.json["callbackUrl"]?.try &.as(String) | ||||
|     expire = env.params.json["expire"]?.try &.as(Int64) | ||||
|   else | ||||
|     next error_json(400, "Invalid or missing header 'Content-Type'") | ||||
|   end | ||||
|  | ||||
|   if callback_url && callback_url.empty? | ||||
|     callback_url = nil | ||||
|   end | ||||
|  | ||||
|   if callback_url | ||||
|     callback_url = URI.parse(callback_url) | ||||
|   end | ||||
|  | ||||
|   if sid = env.get?("sid").try &.as(String) | ||||
|     env.response.content_type = "text/html" | ||||
|  | ||||
|     csrf_token = generate_response(sid, {":authorize_token"}, HMAC_KEY, PG_DB, use_nonce: true) | ||||
|     next templated "authorize_token" | ||||
|   else | ||||
|     env.response.content_type = "application/json" | ||||
|  | ||||
|     superset_scopes = env.get("scopes").as(Array(String)) | ||||
|  | ||||
|     authorized_scopes = [] of String | ||||
|     scopes.each do |scope| | ||||
|       if scopes_include_scope(superset_scopes, scope) | ||||
|         authorized_scopes << scope | ||||
|       end | ||||
|     end | ||||
|  | ||||
|     access_token = generate_token(user.email, authorized_scopes, expire, HMAC_KEY, PG_DB) | ||||
|  | ||||
|     if callback_url | ||||
|       access_token = URI.encode_www_form(access_token) | ||||
|  | ||||
|       if query = callback_url.query | ||||
|         query = HTTP::Params.parse(query.not_nil!) | ||||
|       else | ||||
|         query = HTTP::Params.new | ||||
|       end | ||||
|  | ||||
|       query["token"] = access_token | ||||
|       callback_url.query = query.to_s | ||||
|  | ||||
|       env.redirect callback_url.to_s | ||||
|     else | ||||
|       access_token | ||||
|     end | ||||
|   end | ||||
| end | ||||
|  | ||||
| post "/api/v1/auth/tokens/unregister" do |env| | ||||
|   locale = LOCALES[env.get("preferences").as(Preferences).locale]? | ||||
|   env.response.content_type = "application/json" | ||||
|   user = env.get("user").as(User) | ||||
|   scopes = env.get("scopes").as(Array(String)) | ||||
|  | ||||
|   session = env.params.json["session"]?.try &.as(String) | ||||
|   session ||= env.get("session").as(String) | ||||
|  | ||||
|   # Allow tokens to revoke other tokens with correct scope | ||||
|   if session == env.get("session").as(String) | ||||
|     PG_DB.exec("DELETE FROM session_ids * WHERE id = $1", session) | ||||
|   elsif scopes_include_scope(scopes, "GET:tokens") | ||||
|     PG_DB.exec("DELETE FROM session_ids * WHERE id = $1", session) | ||||
|   else | ||||
|     next error_json(400, "Cannot revoke session #{session}") | ||||
|   end | ||||
|  | ||||
|   env.response.status_code = 204 | ||||
| end | ||||
|  | ||||
| get "/ggpht/*" do |env| | ||||
|   url = env.request.path.lchop("/ggpht") | ||||
|  | ||||
|   | ||||
							
								
								
									
										412
									
								
								src/invidious/routes/api/v1/authenticated.cr
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										412
									
								
								src/invidious/routes/api/v1/authenticated.cr
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,412 @@ | ||||
| module Invidious::Routes::APIv1::Authenticated | ||||
|   # def self.notifications(env) | ||||
|   #   env.response.content_type = "text/event-stream" | ||||
|  | ||||
|   #   topics = env.params.body["topics"]?.try &.split(",").uniq.first(1000) | ||||
|   #   topics ||= [] of String | ||||
|  | ||||
|   #   create_notification_stream(env, topics, connection_channel) | ||||
|   # end | ||||
|  | ||||
|   def self.get_preferences(env) | ||||
|     env.response.content_type = "application/json" | ||||
|     user = env.get("user").as(User) | ||||
|     user.preferences.to_json | ||||
|   end | ||||
|  | ||||
|   def self.set_preferences(env) | ||||
|     env.response.content_type = "application/json" | ||||
|     user = env.get("user").as(User) | ||||
|  | ||||
|     begin | ||||
|       preferences = Preferences.from_json(env.request.body || "{}") | ||||
|     rescue | ||||
|       preferences = user.preferences | ||||
|     end | ||||
|  | ||||
|     PG_DB.exec("UPDATE users SET preferences = $1 WHERE email = $2", preferences.to_json, user.email) | ||||
|  | ||||
|     env.response.status_code = 204 | ||||
|   end | ||||
|  | ||||
|   def self.feed(env) | ||||
|     env.response.content_type = "application/json" | ||||
|  | ||||
|     user = env.get("user").as(User) | ||||
|     locale = LOCALES[env.get("preferences").as(Preferences).locale]? | ||||
|  | ||||
|     max_results = env.params.query["max_results"]?.try &.to_i? | ||||
|     max_results ||= user.preferences.max_results | ||||
|     max_results ||= CONFIG.default_user_preferences.max_results | ||||
|  | ||||
|     page = env.params.query["page"]?.try &.to_i? | ||||
|     page ||= 1 | ||||
|  | ||||
|     videos, notifications = get_subscription_feed(PG_DB, user, max_results, page) | ||||
|  | ||||
|     JSON.build do |json| | ||||
|       json.object do | ||||
|         json.field "notifications" do | ||||
|           json.array do | ||||
|             notifications.each do |video| | ||||
|               video.to_json(locale, json) | ||||
|             end | ||||
|           end | ||||
|         end | ||||
|  | ||||
|         json.field "videos" do | ||||
|           json.array do | ||||
|             videos.each do |video| | ||||
|               video.to_json(locale, json) | ||||
|             end | ||||
|           end | ||||
|         end | ||||
|       end | ||||
|     end | ||||
|   end | ||||
|  | ||||
|   def self.get_subscriptions(env) | ||||
|     env.response.content_type = "application/json" | ||||
|     user = env.get("user").as(User) | ||||
|  | ||||
|     if user.subscriptions.empty? | ||||
|       values = "'{}'" | ||||
|     else | ||||
|       values = "VALUES #{user.subscriptions.map { |id| %(('#{id}')) }.join(",")}" | ||||
|     end | ||||
|  | ||||
|     subscriptions = PG_DB.query_all("SELECT * FROM channels WHERE id = ANY(#{values})", as: InvidiousChannel) | ||||
|  | ||||
|     JSON.build do |json| | ||||
|       json.array do | ||||
|         subscriptions.each do |subscription| | ||||
|           json.object do | ||||
|             json.field "author", subscription.author | ||||
|             json.field "authorId", subscription.id | ||||
|           end | ||||
|         end | ||||
|       end | ||||
|     end | ||||
|   end | ||||
|  | ||||
|   def self.subscribe_channel(env) | ||||
|     env.response.content_type = "application/json" | ||||
|     user = env.get("user").as(User) | ||||
|  | ||||
|     ucid = env.params.url["ucid"] | ||||
|  | ||||
|     if !user.subscriptions.includes? ucid | ||||
|       get_channel(ucid, PG_DB, false, false) | ||||
|       PG_DB.exec("UPDATE users SET feed_needs_update = true, subscriptions = array_append(subscriptions,$1) WHERE email = $2", ucid, user.email) | ||||
|     end | ||||
|  | ||||
|     # For Google accounts, access tokens don't have enough information to | ||||
|     # make a request on the user's behalf, which is why we don't sync with | ||||
|     # YouTube. | ||||
|  | ||||
|     env.response.status_code = 204 | ||||
|   end | ||||
|  | ||||
|   def self.unsubscribe_channel(env) | ||||
|     env.response.content_type = "application/json" | ||||
|     user = env.get("user").as(User) | ||||
|  | ||||
|     ucid = env.params.url["ucid"] | ||||
|  | ||||
|     PG_DB.exec("UPDATE users SET feed_needs_update = true, subscriptions = array_remove(subscriptions, $1) WHERE email = $2", ucid, user.email) | ||||
|  | ||||
|     env.response.status_code = 204 | ||||
|   end | ||||
|  | ||||
|   def self.list_playlists(env) | ||||
|     locale = LOCALES[env.get("preferences").as(Preferences).locale]? | ||||
|  | ||||
|     env.response.content_type = "application/json" | ||||
|     user = env.get("user").as(User) | ||||
|  | ||||
|     playlists = PG_DB.query_all("SELECT * FROM playlists WHERE author = $1", user.email, as: InvidiousPlaylist) | ||||
|  | ||||
|     JSON.build do |json| | ||||
|       json.array do | ||||
|         playlists.each do |playlist| | ||||
|           playlist.to_json(0, locale, json) | ||||
|         end | ||||
|       end | ||||
|     end | ||||
|   end | ||||
|  | ||||
|   def self.create_playlist(env) | ||||
|     env.response.content_type = "application/json" | ||||
|     user = env.get("user").as(User) | ||||
|     locale = LOCALES[env.get("preferences").as(Preferences).locale]? | ||||
|  | ||||
|     title = env.params.json["title"]?.try &.as(String).delete("<>").byte_slice(0, 150) | ||||
|     if !title | ||||
|       return error_json(400, "Invalid title.") | ||||
|     end | ||||
|  | ||||
|     privacy = env.params.json["privacy"]?.try { |privacy| PlaylistPrivacy.parse(privacy.as(String).downcase) } | ||||
|     if !privacy | ||||
|       return error_json(400, "Invalid privacy setting.") | ||||
|     end | ||||
|  | ||||
|     if PG_DB.query_one("SELECT count(*) FROM playlists WHERE author = $1", user.email, as: Int64) >= 100 | ||||
|       return error_json(400, "User cannot have more than 100 playlists.") | ||||
|     end | ||||
|  | ||||
|     playlist = create_playlist(PG_DB, title, privacy, user) | ||||
|     env.response.headers["Location"] = "#{HOST_URL}/api/v1/auth/playlists/#{playlist.id}" | ||||
|     env.response.status_code = 201 | ||||
|     { | ||||
|       "title"      => title, | ||||
|       "playlistId" => playlist.id, | ||||
|     }.to_json | ||||
|   end | ||||
|  | ||||
|   def self.update_playlist_attribute(env) | ||||
|     locale = LOCALES[env.get("preferences").as(Preferences).locale]? | ||||
|  | ||||
|     env.response.content_type = "application/json" | ||||
|     user = env.get("user").as(User) | ||||
|  | ||||
|     plid = env.params.url["plid"] | ||||
|  | ||||
|     playlist = PG_DB.query_one?("SELECT * FROM playlists WHERE id = $1", plid, as: InvidiousPlaylist) | ||||
|     if !playlist || playlist.author != user.email && playlist.privacy.private? | ||||
|       return error_json(404, "Playlist does not exist.") | ||||
|     end | ||||
|  | ||||
|     if playlist.author != user.email | ||||
|       return error_json(403, "Invalid user") | ||||
|     end | ||||
|  | ||||
|     title = env.params.json["title"].try &.as(String).delete("<>").byte_slice(0, 150) || playlist.title | ||||
|     privacy = env.params.json["privacy"]?.try { |privacy| PlaylistPrivacy.parse(privacy.as(String).downcase) } || playlist.privacy | ||||
|     description = env.params.json["description"]?.try &.as(String).delete("\r") || playlist.description | ||||
|  | ||||
|     if title != playlist.title || | ||||
|        privacy != playlist.privacy || | ||||
|        description != playlist.description | ||||
|       updated = Time.utc | ||||
|     else | ||||
|       updated = playlist.updated | ||||
|     end | ||||
|  | ||||
|     PG_DB.exec("UPDATE playlists SET title = $1, privacy = $2, description = $3, updated = $4 WHERE id = $5", title, privacy, description, updated, plid) | ||||
|     env.response.status_code = 204 | ||||
|   end | ||||
|  | ||||
|   def self.delete_playlist(env) | ||||
|     locale = LOCALES[env.get("preferences").as(Preferences).locale]? | ||||
|  | ||||
|     env.response.content_type = "application/json" | ||||
|     user = env.get("user").as(User) | ||||
|  | ||||
|     plid = env.params.url["plid"] | ||||
|  | ||||
|     playlist = PG_DB.query_one?("SELECT * FROM playlists WHERE id = $1", plid, as: InvidiousPlaylist) | ||||
|     if !playlist || playlist.author != user.email && playlist.privacy.private? | ||||
|       return error_json(404, "Playlist does not exist.") | ||||
|     end | ||||
|  | ||||
|     if playlist.author != user.email | ||||
|       return error_json(403, "Invalid user") | ||||
|     end | ||||
|  | ||||
|     PG_DB.exec("DELETE FROM playlist_videos * WHERE plid = $1", plid) | ||||
|     PG_DB.exec("DELETE FROM playlists * WHERE id = $1", plid) | ||||
|  | ||||
|     env.response.status_code = 204 | ||||
|   end | ||||
|  | ||||
|   def self.insert_video_into_playlist(env) | ||||
|     locale = LOCALES[env.get("preferences").as(Preferences).locale]? | ||||
|  | ||||
|     env.response.content_type = "application/json" | ||||
|     user = env.get("user").as(User) | ||||
|  | ||||
|     plid = env.params.url["plid"] | ||||
|  | ||||
|     playlist = PG_DB.query_one?("SELECT * FROM playlists WHERE id = $1", plid, as: InvidiousPlaylist) | ||||
|     if !playlist || playlist.author != user.email && playlist.privacy.private? | ||||
|       return error_json(404, "Playlist does not exist.") | ||||
|     end | ||||
|  | ||||
|     if playlist.author != user.email | ||||
|       return error_json(403, "Invalid user") | ||||
|     end | ||||
|  | ||||
|     if playlist.index.size >= 500 | ||||
|       return error_json(400, "Playlist cannot have more than 500 videos") | ||||
|     end | ||||
|  | ||||
|     video_id = env.params.json["videoId"].try &.as(String) | ||||
|     if !video_id | ||||
|       return error_json(403, "Invalid videoId") | ||||
|     end | ||||
|  | ||||
|     begin | ||||
|       video = get_video(video_id, PG_DB) | ||||
|     rescue ex | ||||
|       return error_json(500, ex) | ||||
|     end | ||||
|  | ||||
|     playlist_video = PlaylistVideo.new({ | ||||
|       title:          video.title, | ||||
|       id:             video.id, | ||||
|       author:         video.author, | ||||
|       ucid:           video.ucid, | ||||
|       length_seconds: video.length_seconds, | ||||
|       published:      video.published, | ||||
|       plid:           plid, | ||||
|       live_now:       video.live_now, | ||||
|       index:          Random::Secure.rand(0_i64..Int64::MAX), | ||||
|     }) | ||||
|  | ||||
|     video_array = playlist_video.to_a | ||||
|     args = arg_array(video_array) | ||||
|  | ||||
|     PG_DB.exec("INSERT INTO playlist_videos VALUES (#{args})", args: video_array) | ||||
|     PG_DB.exec("UPDATE playlists SET index = array_append(index, $1), video_count = cardinality(index) + 1, updated = $2 WHERE id = $3", playlist_video.index, Time.utc, plid) | ||||
|  | ||||
|     env.response.headers["Location"] = "#{HOST_URL}/api/v1/auth/playlists/#{plid}/videos/#{playlist_video.index.to_u64.to_s(16).upcase}" | ||||
|     env.response.status_code = 201 | ||||
|     playlist_video.to_json(locale, index: playlist.index.size) | ||||
|   end | ||||
|  | ||||
|   def self.delete_video_in_playlist(env) | ||||
|     locale = LOCALES[env.get("preferences").as(Preferences).locale]? | ||||
|  | ||||
|     env.response.content_type = "application/json" | ||||
|     user = env.get("user").as(User) | ||||
|  | ||||
|     plid = env.params.url["plid"] | ||||
|     index = env.params.url["index"].to_i64(16) | ||||
|  | ||||
|     playlist = PG_DB.query_one?("SELECT * FROM playlists WHERE id = $1", plid, as: InvidiousPlaylist) | ||||
|     if !playlist || playlist.author != user.email && playlist.privacy.private? | ||||
|       return error_json(404, "Playlist does not exist.") | ||||
|     end | ||||
|  | ||||
|     if playlist.author != user.email | ||||
|       return error_json(403, "Invalid user") | ||||
|     end | ||||
|  | ||||
|     if !playlist.index.includes? index | ||||
|       return error_json(404, "Playlist does not contain index") | ||||
|     end | ||||
|  | ||||
|     PG_DB.exec("DELETE FROM playlist_videos * WHERE index = $1", index) | ||||
|     PG_DB.exec("UPDATE playlists SET index = array_remove(index, $1), video_count = cardinality(index) - 1, updated = $2 WHERE id = $3", index, Time.utc, plid) | ||||
|  | ||||
|     env.response.status_code = 204 | ||||
|   end | ||||
|  | ||||
|   # Invidious::Routing.patch "/api/v1/auth/playlists/:plid/videos/:index" | ||||
|   # def modify_playlist_at(env) | ||||
|   # TODO | ||||
|   # end | ||||
|  | ||||
|   def self.get_tokens(env) | ||||
|     env.response.content_type = "application/json" | ||||
|     user = env.get("user").as(User) | ||||
|     scopes = env.get("scopes").as(Array(String)) | ||||
|  | ||||
|     tokens = PG_DB.query_all("SELECT id, issued FROM session_ids WHERE email = $1", user.email, as: {session: String, issued: Time}) | ||||
|  | ||||
|     JSON.build do |json| | ||||
|       json.array do | ||||
|         tokens.each do |token| | ||||
|           json.object do | ||||
|             json.field "session", token[:session] | ||||
|             json.field "issued", token[:issued].to_unix | ||||
|           end | ||||
|         end | ||||
|       end | ||||
|     end | ||||
|   end | ||||
|  | ||||
|   def self.register_token(env) | ||||
|     user = env.get("user").as(User) | ||||
|     locale = LOCALES[env.get("preferences").as(Preferences).locale]? | ||||
|  | ||||
|     case env.request.headers["Content-Type"]? | ||||
|     when "application/x-www-form-urlencoded" | ||||
|       scopes = env.params.body.select { |k, v| k.match(/^scopes\[\d+\]$/) }.map { |k, v| v } | ||||
|       callback_url = env.params.body["callbackUrl"]? | ||||
|       expire = env.params.body["expire"]?.try &.to_i? | ||||
|     when "application/json" | ||||
|       scopes = env.params.json["scopes"].as(Array).map { |v| v.as_s } | ||||
|       callback_url = env.params.json["callbackUrl"]?.try &.as(String) | ||||
|       expire = env.params.json["expire"]?.try &.as(Int64) | ||||
|     else | ||||
|       return error_json(400, "Invalid or missing header 'Content-Type'") | ||||
|     end | ||||
|  | ||||
|     if callback_url && callback_url.empty? | ||||
|       callback_url = nil | ||||
|     end | ||||
|  | ||||
|     if callback_url | ||||
|       callback_url = URI.parse(callback_url) | ||||
|     end | ||||
|  | ||||
|     if sid = env.get?("sid").try &.as(String) | ||||
|       env.response.content_type = "text/html" | ||||
|  | ||||
|       csrf_token = generate_response(sid, {":authorize_token"}, HMAC_KEY, PG_DB, use_nonce: true) | ||||
|       return templated "authorize_token" | ||||
|     else | ||||
|       env.response.content_type = "application/json" | ||||
|  | ||||
|       superset_scopes = env.get("scopes").as(Array(String)) | ||||
|  | ||||
|       authorized_scopes = [] of String | ||||
|       scopes.each do |scope| | ||||
|         if scopes_include_scope(superset_scopes, scope) | ||||
|           authorized_scopes << scope | ||||
|         end | ||||
|       end | ||||
|  | ||||
|       access_token = generate_token(user.email, authorized_scopes, expire, HMAC_KEY, PG_DB) | ||||
|  | ||||
|       if callback_url | ||||
|         access_token = URI.encode_www_form(access_token) | ||||
|  | ||||
|         if query = callback_url.query | ||||
|           query = HTTP::Params.parse(query.not_nil!) | ||||
|         else | ||||
|           query = HTTP::Params.new | ||||
|         end | ||||
|  | ||||
|         query["token"] = access_token | ||||
|         callback_url.query = query.to_s | ||||
|  | ||||
|         env.redirect callback_url.to_s | ||||
|       else | ||||
|         access_token | ||||
|       end | ||||
|     end | ||||
|   end | ||||
|  | ||||
|   def self.unregister_token(env) | ||||
|     locale = LOCALES[env.get("preferences").as(Preferences).locale]? | ||||
|     env.response.content_type = "application/json" | ||||
|     user = env.get("user").as(User) | ||||
|     scopes = env.get("scopes").as(Array(String)) | ||||
|  | ||||
|     session = env.params.json["session"]?.try &.as(String) | ||||
|     session ||= env.get("session").as(String) | ||||
|  | ||||
|     # Allow tokens to revoke other tokens with correct scope | ||||
|     if session == env.get("session").as(String) | ||||
|       PG_DB.exec("DELETE FROM session_ids * WHERE id = $1", session) | ||||
|     elsif scopes_include_scope(scopes, "GET:tokens") | ||||
|       PG_DB.exec("DELETE FROM session_ids * WHERE id = $1", session) | ||||
|     else | ||||
|       return error_json(400, "Cannot revoke session #{session}") | ||||
|     end | ||||
|  | ||||
|     env.response.status_code = 204 | ||||
|   end | ||||
| end | ||||
| @@ -10,4 +10,127 @@ module Invidious::Routes::APIv1::Misc | ||||
|  | ||||
|     Invidious::Jobs::StatisticsRefreshJob::STATISTICS.to_json | ||||
|   end | ||||
|  | ||||
|   # APIv1 currently uses the same logic for both | ||||
|   # user playlists and Invidious playlists. This means that we can't | ||||
|   # reasonably split them yet. This should be addressed in APIv2 | ||||
|   def self.get_playlist(env) | ||||
|     locale = LOCALES[env.get("preferences").as(Preferences).locale]? | ||||
|  | ||||
|     env.response.content_type = "application/json" | ||||
|     plid = env.params.url["plid"] | ||||
|  | ||||
|     offset = env.params.query["index"]?.try &.to_i? | ||||
|     offset ||= env.params.query["page"]?.try &.to_i?.try { |page| (page - 1) * 100 } | ||||
|     offset ||= 0 | ||||
|  | ||||
|     continuation = env.params.query["continuation"]? | ||||
|  | ||||
|     format = env.params.query["format"]? | ||||
|     format ||= "json" | ||||
|  | ||||
|     if plid.starts_with? "RD" | ||||
|       return env.redirect "/api/v1/mixes/#{plid}" | ||||
|     end | ||||
|  | ||||
|     begin | ||||
|       playlist = get_playlist(PG_DB, plid, locale) | ||||
|     rescue ex : InfoException | ||||
|       return error_json(404, ex) | ||||
|     rescue ex | ||||
|       return error_json(404, "Playlist does not exist.") | ||||
|     end | ||||
|  | ||||
|     user = env.get?("user").try &.as(User) | ||||
|     if !playlist || playlist.privacy.private? && playlist.author != user.try &.email | ||||
|       return error_json(404, "Playlist does not exist.") | ||||
|     end | ||||
|  | ||||
|     response = playlist.to_json(offset, locale, continuation: continuation) | ||||
|  | ||||
|     if format == "html" | ||||
|       response = JSON.parse(response) | ||||
|       playlist_html = template_playlist(response) | ||||
|       index, next_video = response["videos"].as_a.skip(1).select { |video| !video["author"].as_s.empty? }[0]?.try { |v| {v["index"], v["videoId"]} } || {nil, nil} | ||||
|  | ||||
|       response = { | ||||
|         "playlistHtml" => playlist_html, | ||||
|         "index"        => index, | ||||
|         "nextVideo"    => next_video, | ||||
|       }.to_json | ||||
|     end | ||||
|  | ||||
|     response | ||||
|   end | ||||
|  | ||||
|   def self.mixes(env) | ||||
|     locale = LOCALES[env.get("preferences").as(Preferences).locale]? | ||||
|  | ||||
|     env.response.content_type = "application/json" | ||||
|  | ||||
|     rdid = env.params.url["rdid"] | ||||
|  | ||||
|     continuation = env.params.query["continuation"]? | ||||
|     continuation ||= rdid.lchop("RD")[0, 11] | ||||
|  | ||||
|     format = env.params.query["format"]? | ||||
|     format ||= "json" | ||||
|  | ||||
|     begin | ||||
|       mix = fetch_mix(rdid, continuation, locale: locale) | ||||
|  | ||||
|       if !rdid.ends_with? continuation | ||||
|         mix = fetch_mix(rdid, mix.videos[1].id) | ||||
|         index = mix.videos.index(mix.videos.select { |video| video.id == continuation }[0]?) | ||||
|       end | ||||
|  | ||||
|       mix.videos = mix.videos[index..-1] | ||||
|     rescue ex | ||||
|       return error_json(500, ex) | ||||
|     end | ||||
|  | ||||
|     response = JSON.build do |json| | ||||
|       json.object do | ||||
|         json.field "title", mix.title | ||||
|         json.field "mixId", mix.id | ||||
|  | ||||
|         json.field "videos" do | ||||
|           json.array do | ||||
|             mix.videos.each do |video| | ||||
|               json.object do | ||||
|                 json.field "title", video.title | ||||
|                 json.field "videoId", video.id | ||||
|                 json.field "author", video.author | ||||
|  | ||||
|                 json.field "authorId", video.ucid | ||||
|                 json.field "authorUrl", "/channel/#{video.ucid}" | ||||
|  | ||||
|                 json.field "videoThumbnails" do | ||||
|                   json.array do | ||||
|                     generate_thumbnails(json, video.id) | ||||
|                   end | ||||
|                 end | ||||
|  | ||||
|                 json.field "index", video.index | ||||
|                 json.field "lengthSeconds", video.length_seconds | ||||
|               end | ||||
|             end | ||||
|           end | ||||
|         end | ||||
|       end | ||||
|     end | ||||
|  | ||||
|     if format == "html" | ||||
|       response = JSON.parse(response) | ||||
|       playlist_html = template_mix(response) | ||||
|       next_video = response["videos"].as_a.select { |video| !video["author"].as_s.empty? }[0]?.try &.["videoId"] | ||||
|  | ||||
|       response = { | ||||
|         "playlistHtml" => playlist_html, | ||||
|         "nextVideo"    => next_video, | ||||
|       }.to_json | ||||
|     end | ||||
|  | ||||
|     response | ||||
|   end | ||||
| end | ||||
|   | ||||
| @@ -1,8 +1,6 @@ | ||||
| # There is far too many API routes to define in invidious.cr | ||||
| # so we'll just do it here instead with a macro. | ||||
| macro define_v1_api_routes(base_url = "/api/v1") | ||||
|   Invidious::Routing.get "#{{{base_url}}}/stats", Invidious::Routes::APIv1::Misc, :stats | ||||
|  | ||||
|   # Videos | ||||
|   Invidious::Routing.get "#{{{base_url}}}/videos/:id", Invidious::Routes::APIv1::Videos, :videos | ||||
|   Invidious::Routing.get "#{{{base_url}}}/storyboards/:id", Invidious::Routes::APIv1::Videos, :storyboards | ||||
| @@ -32,4 +30,38 @@ macro define_v1_api_routes(base_url = "/api/v1") | ||||
|   # Search | ||||
|   Invidious::Routing.get "#{{{base_url}}}/search", Invidious::Routes::APIv1::Search, :search | ||||
|   Invidious::Routing.get "#{{{base_url}}}/search/suggestions/:id", Invidious::Routes::APIv1::Search, :search_suggestions | ||||
|  | ||||
|   # Authenticated | ||||
|   # Invidious::Routing.get "#{{{base_url}}}/auth/notifications", Invidious::Routes::APIv1::Authenticated, :notifications | ||||
|   # Invidious::Routing.post "#{{{base_url}}}/auth/notifications", Invidious::Routes::APIv1::Authenticated, :notifications | ||||
|  | ||||
|   Invidious::Routing.get "#{{{base_url}}}/auth/preferences", Invidious::Routes::APIv1::Authenticated, :get_preferences | ||||
|   Invidious::Routing.post "#{{{base_url}}}/auth/preferences", Invidious::Routes::APIv1::Authenticated, :set_preferences | ||||
|  | ||||
|   Invidious::Routing.get "#{{{base_url}}}/auth/feed", Invidious::Routes::APIv1::Authenticated, :feed | ||||
|  | ||||
|   Invidious::Routing.get "#{{{base_url}}}/auth/subscriptions", Invidious::Routes::APIv1::Authenticated, :get_subscriptions | ||||
|   Invidious::Routing.post "#{{{base_url}}}/auth/subscriptions/:ucid", Invidious::Routes::APIv1::Authenticated, :subscribe_channel | ||||
|   Invidious::Routing.delete "#{{{base_url}}}/auth/subscriptions/:ucid", Invidious::Routes::APIv1::Authenticated, :unsubscribe_channel | ||||
|  | ||||
|  | ||||
|   Invidious::Routing.get "#{{{base_url}}}/auth/playlists", Invidious::Routes::APIv1::Authenticated, :list_playlists | ||||
|   Invidious::Routing.post "#{{{base_url}}}/auth/playlists", Invidious::Routes::APIv1::Authenticated, :create_playlist | ||||
|   Invidious::Routing.patch "#{{{base_url}}}/auth/playlists/:ucid", Invidious::Routes::APIv1::Authenticated, :update_playlist_attribute | ||||
|   Invidious::Routing.delete "#{{{base_url}}}/auth/playlists/:ucid", Invidious::Routes::APIv1::Authenticated, :delete_playlist | ||||
|  | ||||
|  | ||||
|   Invidious::Routing.post "#{{{base_url}}}/auth/playlists/:ucid/videos", Invidious::Routes::APIv1::Authenticated, :insert_video_into_playlist | ||||
|   Invidious::Routing.delete "#{{{base_url}}}/auth/playlists/:ucid/videos/:index", Invidious::Routes::APIv1::Authenticated, :delete_video_in_playlist | ||||
|  | ||||
|   Invidious::Routing.get "#{{{base_url}}}/auth/tokens", Invidious::Routes::APIv1::Authenticated, :get_tokens | ||||
|   Invidious::Routing.post "#{{{base_url}}}/auth/tokens/register", Invidious::Routes::APIv1::Authenticated, :register_token | ||||
|   Invidious::Routing.post "#{{{base_url}}}/auth/tokens/unregister", Invidious::Routes::APIv1::Authenticated, :unregister_token | ||||
|  | ||||
|   # Misc | ||||
|   Invidious::Routing.get "#{{{base_url}}}/stats", Invidious::Routes::APIv1::Misc, :stats | ||||
|   Invidious::Routing.get "#{{{base_url}}}/playlists/:plid", Invidious::Routes::APIv1::Misc, :get_playlist | ||||
|   Invidious::Routing.get "#{{{base_url}}}/auth/playlists/:plid", Invidious::Routes::APIv1::Misc, :get_playlist | ||||
|   Invidious::Routing.get "#{{{base_url}}}//mixes/:rdid", Invidious::Routes::APIv1::Misc, :mixes | ||||
|  | ||||
| end | ||||
|   | ||||
		Reference in New Issue
	
	Block a user