diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/invidious.cr | 749 | ||||
| -rw-r--r-- | src/invidious/channels.cr | 35 | ||||
| -rw-r--r-- | src/invidious/helpers/helpers.cr | 12 | ||||
| -rw-r--r-- | src/invidious/jobs/bypass_captcha_job.cr | 14 | ||||
| -rw-r--r-- | src/invidious/routes/base_route.cr | 2 | ||||
| -rw-r--r-- | src/invidious/routes/embed/index.cr | 27 | ||||
| -rw-r--r-- | src/invidious/routes/embed/show.cr | 174 | ||||
| -rw-r--r-- | src/invidious/routes/playlists.cr | 505 | ||||
| -rw-r--r-- | src/invidious/routing.cr | 11 | ||||
| -rw-r--r-- | src/invidious/videos.cr | 3 | ||||
| -rw-r--r-- | src/invidious/views/components/player_sources.ecr | 1 | ||||
| -rw-r--r-- | src/invidious/views/preferences.ecr | 14 | ||||
| -rw-r--r-- | src/invidious/views/search.ecr | 18 | ||||
| -rw-r--r-- | src/invidious/views/template.ecr | 1 |
14 files changed, 787 insertions, 779 deletions
diff --git a/src/invidious.cr b/src/invidious.cr index 4855eecf..a9a3a963 100644 --- a/src/invidious.cr +++ b/src/invidious.cr @@ -297,737 +297,20 @@ Invidious::Routing.get "/", Invidious::Routes::Home Invidious::Routing.get "/privacy", Invidious::Routes::Privacy Invidious::Routing.get "/licenses", Invidious::Routes::Licenses Invidious::Routing.get "/watch", Invidious::Routes::Watch - -get "/embed/" do |env| - locale = LOCALES[env.get("preferences").as(Preferences).locale]? - - if plid = env.params.query["list"]?.try &.gsub(/[^a-zA-Z0-9_-]/, "") - begin - playlist = get_playlist(PG_DB, plid, locale: locale) - offset = env.params.query["index"]?.try &.to_i? || 0 - videos = get_playlist_videos(PG_DB, playlist, offset: offset, locale: locale) - rescue ex - error_message = ex.message - env.response.status_code = 500 - next templated "error" - end - - url = "/embed/#{videos[0].id}?#{env.params.query}" - - if env.params.query.size > 0 - url += "?#{env.params.query}" - end - else - url = "/" - end - - env.redirect url -end - -get "/embed/:id" do |env| - locale = LOCALES[env.get("preferences").as(Preferences).locale]? - id = env.params.url["id"] - - plid = env.params.query["list"]?.try &.gsub(/[^a-zA-Z0-9_-]/, "") - continuation = process_continuation(PG_DB, env.params.query, plid, id) - - if md = env.params.query["playlist"]? - .try &.match(/[a-zA-Z0-9_-]{11}(,[a-zA-Z0-9_-]{11})*/) - video_series = md[0].split(",") - env.params.query.delete("playlist") - end - - preferences = env.get("preferences").as(Preferences) - - if id.includes?("%20") || id.includes?("+") || env.params.query.to_s.includes?("%20") || env.params.query.to_s.includes?("+") - id = env.params.url["id"].gsub("%20", "").delete("+") - - url = "/embed/#{id}" - - if env.params.query.size > 0 - url += "?#{env.params.query.to_s.gsub("%20", "").delete("+")}" - end - - next env.redirect url - end - - # YouTube embed supports `videoseries` with either `list=PLID` - # or `playlist=VIDEO_ID,VIDEO_ID` - case id - when "videoseries" - url = "" - - if plid - begin - playlist = get_playlist(PG_DB, plid, locale: locale) - offset = env.params.query["index"]?.try &.to_i? || 0 - videos = get_playlist_videos(PG_DB, playlist, offset: offset, locale: locale) - rescue ex - error_message = ex.message - env.response.status_code = 500 - next templated "error" - end - - url = "/embed/#{videos[0].id}" - elsif video_series - url = "/embed/#{video_series.shift}" - env.params.query["playlist"] = video_series.join(",") - else - next env.redirect "/" - end - - if env.params.query.size > 0 - url += "?#{env.params.query}" - end - - next env.redirect url - when "live_stream" - response = YT_POOL.client &.get("/embed/live_stream?channel=#{env.params.query["channel"]? || ""}") - video_id = response.body.match(/"video_id":"(?<video_id>[a-zA-Z0-9_-]{11})"/).try &.["video_id"] - - env.params.query.delete_all("channel") - - if !video_id || video_id == "live_stream" - error_message = "Video is unavailable." - next templated "error" - end - - url = "/embed/#{video_id}" - - if env.params.query.size > 0 - url += "?#{env.params.query}" - end - - next env.redirect url - when id.size > 11 - url = "/embed/#{id[0, 11]}" - - if env.params.query.size > 0 - url += "?#{env.params.query}" - end - - next env.redirect url - else nil # Continue - end - - params = process_video_params(env.params.query, preferences) - - user = env.get?("user").try &.as(User) - if user - subscriptions = user.subscriptions - watched = user.watched - notifications = user.notifications - end - subscriptions ||= [] of String - - begin - video = get_video(id, PG_DB, region: params.region) - rescue ex : VideoRedirect - next env.redirect env.request.resource.gsub(id, ex.video_id) - rescue ex - error_message = ex.message - env.response.status_code = 500 - next templated "error" - end - - if preferences.annotations_subscribed && - subscriptions.includes?(video.ucid) && - (env.params.query["iv_load_policy"]? || "1") == "1" - params.annotations = true - end - - # if watched && !watched.includes? id - # PG_DB.exec("UPDATE users SET watched = array_append(watched, $1) WHERE email = $2", id, user.as(User).email) - # end - - if notifications && notifications.includes? id - PG_DB.exec("UPDATE users SET notifications = array_remove(notifications, $1) WHERE email = $2", id, user.as(User).email) - env.get("user").as(User).notifications.delete(id) - notifications.delete(id) - end - - fmt_stream = video.fmt_stream - adaptive_fmts = video.adaptive_fmts - - if params.local - fmt_stream.each { |fmt| fmt["url"] = JSON::Any.new(URI.parse(fmt["url"].as_s).full_path) } - adaptive_fmts.each { |fmt| fmt["url"] = JSON::Any.new(URI.parse(fmt["url"].as_s).full_path) } - end - - video_streams = video.video_streams - audio_streams = video.audio_streams - - if audio_streams.empty? && !video.live_now - if params.quality == "dash" - env.params.query.delete_all("quality") - next env.redirect "/embed/#{id}?#{env.params.query}" - elsif params.listen - env.params.query.delete_all("listen") - env.params.query["listen"] = "0" - next env.redirect "/embed/#{id}?#{env.params.query}" - end - end - - captions = video.captions - - preferred_captions = captions.select { |caption| - params.preferred_captions.includes?(caption.name.simpleText) || - params.preferred_captions.includes?(caption.languageCode.split("-")[0]) - } - preferred_captions.sort_by! { |caption| - (params.preferred_captions.index(caption.name.simpleText) || - params.preferred_captions.index(caption.languageCode.split("-")[0])).not_nil! - } - captions = captions - preferred_captions - - aspect_ratio = nil - - thumbnail = "/vi/#{video.id}/maxres.jpg" - - if params.raw - url = fmt_stream[0]["url"].as_s - - fmt_stream.each do |fmt| - url = fmt["url"].as_s if fmt["quality"].as_s == params.quality - end - - next env.redirect url - end - - rendered "embed" -end - -# Playlists - -get "/feed/playlists" do |env| - env.redirect "/view_all_playlists" -end - -get "/view_all_playlists" do |env| - locale = LOCALES[env.get("preferences").as(Preferences).locale]? - - user = env.get? "user" - referer = get_referer(env) - - if !user - next env.redirect "/" - end - - user = user.as(User) - - items_created = PG_DB.query_all("SELECT * FROM playlists WHERE author = $1 AND id LIKE 'IV%' ORDER BY created", user.email, as: InvidiousPlaylist) - items_created.map! do |item| - item.author = "" - item - end - - items_saved = PG_DB.query_all("SELECT * FROM playlists WHERE author = $1 AND id NOT LIKE 'IV%' ORDER BY created", user.email, as: InvidiousPlaylist) - items_saved.map! do |item| - item.author = "" - item - end - - templated "view_all_playlists" -end - -get "/create_playlist" do |env| - locale = LOCALES[env.get("preferences").as(Preferences).locale]? - - user = env.get? "user" - sid = env.get? "sid" - referer = get_referer(env) - - if !user - next env.redirect "/" - end - - user = user.as(User) - sid = sid.as(String) - csrf_token = generate_response(sid, {":create_playlist"}, HMAC_KEY, PG_DB) - - templated "create_playlist" -end - -post "/create_playlist" do |env| - locale = LOCALES[env.get("preferences").as(Preferences).locale]? - - user = env.get? "user" - sid = env.get? "sid" - referer = get_referer(env) - - if !user - next env.redirect "/" - end - - user = user.as(User) - sid = sid.as(String) - token = env.params.body["csrf_token"]? - - begin - validate_request(token, sid, env.request, HMAC_KEY, PG_DB, locale) - rescue ex - error_message = ex.message - env.response.status_code = 400 - next templated "error" - end - - title = env.params.body["title"]?.try &.as(String) - if !title || title.empty? - error_message = "Title cannot be empty." - next templated "error" - end - - privacy = PlaylistPrivacy.parse?(env.params.body["privacy"]?.try &.as(String) || "") - if !privacy - error_message = "Invalid privacy setting." - next templated "error" - end - - if PG_DB.query_one("SELECT count(*) FROM playlists WHERE author = $1", user.email, as: Int64) >= 100 - error_message = "User cannot have more than 100 playlists." - next templated "error" - end - - playlist = create_playlist(PG_DB, title, privacy, user) - - env.redirect "/playlist?list=#{playlist.id}" -end - -get "/subscribe_playlist" do |env| - locale = LOCALES[env.get("preferences").as(Preferences).locale]? - - user = env.get? "user" - referer = get_referer(env) - - if !user - next env.redirect "/" - end - - user = user.as(User) - - playlist_id = env.params.query["list"] - playlist = get_playlist(PG_DB, playlist_id, locale) - subscribe_playlist(PG_DB, user, playlist) - - env.redirect "/playlist?list=#{playlist.id}" -end - -get "/delete_playlist" do |env| - locale = LOCALES[env.get("preferences").as(Preferences).locale]? - - user = env.get? "user" - sid = env.get? "sid" - referer = get_referer(env) - - if !user - next env.redirect "/" - end - - user = user.as(User) - sid = sid.as(String) - - plid = env.params.query["list"]? - playlist = PG_DB.query_one?("SELECT * FROM playlists WHERE id = $1", plid, as: InvidiousPlaylist) - if !playlist || playlist.author != user.email - next env.redirect referer - end - - csrf_token = generate_response(sid, {":delete_playlist"}, HMAC_KEY, PG_DB) - - templated "delete_playlist" -end - -post "/delete_playlist" do |env| - locale = LOCALES[env.get("preferences").as(Preferences).locale]? - - user = env.get? "user" - sid = env.get? "sid" - referer = get_referer(env) - - if !user - next env.redirect "/" - end - - plid = env.params.query["list"]? - if !plid - next env.redirect referer - end - - user = user.as(User) - sid = sid.as(String) - token = env.params.body["csrf_token"]? - - begin - validate_request(token, sid, env.request, HMAC_KEY, PG_DB, locale) - rescue ex - error_message = ex.message - env.response.status_code = 400 - next templated "error" - end - - playlist = PG_DB.query_one?("SELECT * FROM playlists WHERE id = $1", plid, as: InvidiousPlaylist) - if !playlist || playlist.author != user.email - next env.redirect referer - end - - PG_DB.exec("DELETE FROM playlist_videos * WHERE plid = $1", plid) - PG_DB.exec("DELETE FROM playlists * WHERE id = $1", plid) - - env.redirect "/view_all_playlists" -end - -get "/edit_playlist" do |env| - locale = LOCALES[env.get("preferences").as(Preferences).locale]? - - user = env.get? "user" - sid = env.get? "sid" - referer = get_referer(env) - - if !user - next env.redirect "/" - end - - user = user.as(User) - sid = sid.as(String) - - plid = env.params.query["list"]? - if !plid || !plid.starts_with?("IV") - next env.redirect referer - end - - page = env.params.query["page"]?.try &.to_i? - page ||= 1 - - begin - playlist = PG_DB.query_one("SELECT * FROM playlists WHERE id = $1", plid, as: InvidiousPlaylist) - if !playlist || playlist.author != user.email - next env.redirect referer - end - rescue ex - next env.redirect referer - end - - begin - videos = get_playlist_videos(PG_DB, playlist, offset: (page - 1) * 100, locale: locale) - rescue ex - videos = [] of PlaylistVideo - end - - csrf_token = generate_response(sid, {":edit_playlist"}, HMAC_KEY, PG_DB) - - templated "edit_playlist" -end - -post "/edit_playlist" do |env| - locale = LOCALES[env.get("preferences").as(Preferences).locale]? - - user = env.get? "user" - sid = env.get? "sid" - referer = get_referer(env) - - if !user - next env.redirect "/" - end - - plid = env.params.query["list"]? - if !plid - next env.redirect referer - end - - user = user.as(User) - sid = sid.as(String) - token = env.params.body["csrf_token"]? - - begin - validate_request(token, sid, env.request, HMAC_KEY, PG_DB, locale) - rescue ex - error_message = ex.message - env.response.status_code = 400 - next templated "error" - end - - playlist = PG_DB.query_one?("SELECT * FROM playlists WHERE id = $1", plid, as: InvidiousPlaylist) - if !playlist || playlist.author != user.email - next env.redirect referer - end - - title = env.params.body["title"]?.try &.delete("<>") || "" - privacy = PlaylistPrivacy.parse(env.params.body["privacy"]? || "Public") - description = env.params.body["description"]?.try &.delete("\r") || "" - - 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.redirect "/playlist?list=#{plid}" -end - -get "/add_playlist_items" do |env| - locale = LOCALES[env.get("preferences").as(Preferences).locale]? - - user = env.get? "user" - sid = env.get? "sid" - referer = get_referer(env) - - if !user - next env.redirect "/" - end - - user = user.as(User) - sid = sid.as(String) - - plid = env.params.query["list"]? - if !plid || !plid.starts_with?("IV") - next env.redirect referer - end - - page = env.params.query["page"]?.try &.to_i? - page ||= 1 - - begin - playlist = PG_DB.query_one("SELECT * FROM playlists WHERE id = $1", plid, as: InvidiousPlaylist) - if !playlist || playlist.author != user.email - next env.redirect referer - end - rescue ex - next env.redirect referer - end - - query = env.params.query["q"]? - if query - begin - search_query, count, items = process_search_query(query, page, user, region: nil) - videos = items.select { |item| item.is_a? SearchVideo }.map { |item| item.as(SearchVideo) } - rescue ex - videos = [] of SearchVideo - count = 0 - end - else - videos = [] of SearchVideo - count = 0 - end - - env.set "add_playlist_items", plid - templated "add_playlist_items" -end - -post "/playlist_ajax" do |env| - locale = LOCALES[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 - error_message = {"error" => "No such user"}.to_json - env.response.status_code = 403 - next error_message - 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, PG_DB, locale) - rescue ex - if redirect - error_message = ex.message - env.response.status_code = 400 - next templated "error" - else - error_message = {"error" => ex.message}.to_json - env.response.status_code = 400 - next error_message - end - end - - if env.params.query["action_create_playlist"]? - action = "action_create_playlist" - elsif env.params.query["action_delete_playlist"]? - action = "action_delete_playlist" - elsif env.params.query["action_edit_playlist"]? - action = "action_edit_playlist" - elsif env.params.query["action_add_video"]? - action = "action_add_video" - video_id = env.params.query["video_id"] - elsif env.params.query["action_remove_video"]? - action = "action_remove_video" - elsif env.params.query["action_move_video_before"]? - action = "action_move_video_before" - else - next env.redirect referer - end - - begin - playlist_id = env.params.query["playlist_id"] - playlist = get_playlist(PG_DB, playlist_id, locale).as(InvidiousPlaylist) - raise "Invalid user" if playlist.author != user.email - rescue ex - if redirect - error_message = ex.message - env.response.status_code = 400 - next templated "error" - else - error_message = {"error" => ex.message}.to_json - env.response.status_code = 400 - next error_message - end - end - - if !user.password - # TODO: Playlist stub, sync with YouTube for Google accounts - # playlist_ajax(playlist_id, action, env.request.headers) - end - email = user.email - - case action - when "action_edit_playlist" - # TODO: Playlist stub - when "action_add_video" - if playlist.index.size >= 500 - env.response.status_code = 400 - if redirect - error_message = "Playlist cannot have more than 500 videos" - next templated "error" - else - error_message = {"error" => "Playlist cannot have more than 500 videos"}.to_json - next error_message - end - end - - video_id = env.params.query["video_id"] - - begin - video = get_video(video_id, PG_DB) - rescue ex - env.response.status_code = 500 - if redirect - error_message = ex.message - next templated "error" - else - error_message = {"error" => ex.message}.to_json - next error_message - end - 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), - }) - - 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, playlist_id) - when "action_remove_video" - index = env.params.query["set_video_id"] - 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, playlist_id) - when "action_move_video_before" - # TODO: Playlist stub - else - error_message = {"error" => "Unsupported action #{action}"}.to_json - env.response.status_code = 400 - next error_message - end - - if redirect - env.redirect referer - else - env.response.content_type = "application/json" - "{}" - end -end - -get "/playlist" do |env| - locale = LOCALES[env.get("preferences").as(Preferences).locale]? - - user = env.get?("user").try &.as(User) - referer = get_referer(env) - - plid = env.params.query["list"]?.try &.gsub(/[^a-zA-Z0-9_-]/, "") - if !plid - next env.redirect "/" - end - - page = env.params.query["page"]?.try &.to_i? - page ||= 1 - - if plid.starts_with? "RD" - next env.redirect "/mix?list=#{plid}" - end - - begin - playlist = get_playlist(PG_DB, plid, locale) - rescue ex - error_message = ex.message - env.response.status_code = 500 - next templated "error" - end - - if playlist.privacy == PlaylistPrivacy::Private && playlist.author != user.try &.email - error_message = "This playlist is private." - env.response.status_code = 403 - next templated "error" - end - - begin - videos = get_playlist_videos(PG_DB, playlist, offset: (page - 1) * 100, locale: locale) - rescue ex - videos = [] of PlaylistVideo - end - - if playlist.author == user.try &.email - env.set "remove_playlist_items", plid - end - - templated "playlist" -end - -get "/mix" do |env| - locale = LOCALES[env.get("preferences").as(Preferences).locale]? - - rdid = env.params.query["list"]? - if !rdid - next env.redirect "/" - end - - continuation = env.params.query["continuation"]? - continuation ||= rdid.lchop("RD") - - begin - mix = fetch_mix(rdid, continuation, locale: locale) - rescue ex - error_message = ex.message - env.response.status_code = 500 - next templated "error" - end - - templated "mix" -end +Invidious::Routing.get "/embed/", Invidious::Routes::Embed::Index +Invidious::Routing.get "/embed/:id", Invidious::Routes::Embed::Show +Invidious::Routing.get "/view_all_playlists", Invidious::Routes::Playlists, :index +Invidious::Routing.get "/create_playlist", Invidious::Routes::Playlists, :new +Invidious::Routing.post "/create_playlist", Invidious::Routes::Playlists, :create +Invidious::Routing.get "/subscribe_playlist", Invidious::Routes::Playlists, :subscribe +Invidious::Routing.get "/delete_playlist", Invidious::Routes::Playlists, :delete_page +Invidious::Routing.post "/delete_playlist", Invidious::Routes::Playlists, :delete +Invidious::Routing.get "/edit_playlist", Invidious::Routes::Playlists, :edit +Invidious::Routing.post "/edit_playlist", Invidious::Routes::Playlists, :update +Invidious::Routing.get "/add_playlist_items", Invidious::Routes::Playlists, :add_playlist_items_page +Invidious::Routing.post "/playlist_ajax", Invidious::Routes::Playlists, :playlist_ajax +Invidious::Routing.get "/playlist", Invidious::Routes::Playlists, :show +Invidious::Routing.get "/mix", Invidious::Routes::Playlists, :mix # Search @@ -2738,6 +2021,10 @@ end # Feeds +get "/feed/playlists" do |env| + env.redirect "/view_all_playlists" +end + get "/feed/top" do |env| locale = LOCALES[env.get("preferences").as(Preferences).locale]? env.redirect "/" diff --git a/src/invidious/channels.cr b/src/invidious/channels.cr index dce9e6aa..392c44ee 100644 --- a/src/invidious/channels.cr +++ b/src/invidious/channels.cr @@ -775,38 +775,31 @@ def extract_channel_community_cursor(continuation) cursor end -INITDATA_PREQUERY = "window[\"ytInitialData\"] = {" - def get_about_info(ucid, locale) - about = YT_POOL.client &.get("/channel/#{ucid}/about?gl=US&hl=en") - if about.status_code != 200 - about = YT_POOL.client &.get("/user/#{ucid}/about?gl=US&hl=en") + result = YT_POOL.client &.get("/channel/#{ucid}/about?gl=US&hl=en") + if result.status_code != 200 + result = YT_POOL.client &.get("/user/#{ucid}/about?gl=US&hl=en") end - if md = about.headers["location"]?.try &.match(/\/channel\/(?<ucid>UC[a-zA-Z0-9_-]{22})/) + if md = result.headers["location"]?.try &.match(/\/channel\/(?<ucid>UC[a-zA-Z0-9_-]{22})/) raise ChannelRedirect.new(channel_id: md["ucid"]) end - if about.status_code != 200 + if result.status_code != 200 error_message = translate(locale, "This channel does not exist.") raise error_message end - initdata_pre = about.body.index(INITDATA_PREQUERY) - initdata_post = initdata_pre.nil? ? nil : about.body.index("};", initdata_pre) - if initdata_post.nil? - about = XML.parse_html(about.body) - error_message = about.xpath_node(%q(//div[@class="yt-alert-content"])).try &.content.strip - error_message ||= translate(locale, "Could not get channel info.") + about = XML.parse_html(result.body) + if about.xpath_node(%q(//div[contains(@class, "channel-empty-message")])) + error_message = translate(locale, "This channel does not exist.") raise error_message end - initdata_pre = initdata_pre.not_nil! + INITDATA_PREQUERY.size - 1 - initdata = JSON.parse(about.body[initdata_pre, initdata_post - initdata_pre + 1]) - about = XML.parse_html(about.body) - - if about.xpath_node(%q(//div[contains(@class, "channel-empty-message")])) - error_message = translate(locale, "This channel does not exist.") + initdata = extract_initial_data(result.body) + if initdata.empty? + error_message = about.xpath_node(%q(//div[@class="yt-alert-content"])).try &.content.strip + error_message ||= translate(locale, "Could not get channel info.") raise error_message end @@ -887,8 +880,8 @@ def get_about_info(ucid, locale) # Auto-generated channels # https://support.google.com/youtube/answer/2579942 # For auto-generated channels, channel_about_meta only has ["description"]["simpleText"] and ["primaryLinks"][0]["title"]["simpleText"] - if (channel_about_meta["primaryLinks"]?.try &.size || 0) == 1 && (channel_about_meta["primaryLinks"][0]?) - (channel_about_meta["primaryLinks"][0]["title"]?.try &.["simpleText"]?.try &.as_s? || "") == "Auto-generated by YouTube" + if (channel_about_meta["primaryLinks"]?.try &.size || 0) == 1 && (channel_about_meta["primaryLinks"][0]?) && + (channel_about_meta["primaryLinks"][0]["title"]?.try &.["simpleText"]?.try &.as_s? || "") == "Auto-generated by YouTube" auto_generated = true end end diff --git a/src/invidious/helpers/helpers.cr b/src/invidious/helpers/helpers.cr index 62c24f3e..7a0cb3d3 100644 --- a/src/invidious/helpers/helpers.cr +++ b/src/invidious/helpers/helpers.cr @@ -93,8 +93,9 @@ struct Config property admin_email : String = "omarroth@protonmail.com" # Email for bug reports @[YAML::Field(converter: Preferences::StringToCookies)] - property cookies : HTTP::Cookies = HTTP::Cookies.new # Saved cookies in "name1=value1; name2=value2..." format - property captcha_key : String? = nil # Key for Anti-Captcha + property cookies : HTTP::Cookies = HTTP::Cookies.new # Saved cookies in "name1=value1; name2=value2..." format + property captcha_key : String? = nil # Key for Anti-Captcha + property captcha_api_url : String = "https://api.anti-captcha.com" # API URL for Anti-Captcha def disabled?(option) case disabled = CONFIG.disable_proxy @@ -597,12 +598,7 @@ def create_notification_stream(env, topics, connection_channel) end def extract_initial_data(body) : Hash(String, JSON::Any) - initial_data = body.match(/(window\["ytInitialData"\]|var\s+ytInitialData)\s*=\s*(?<info>.*?);+\s*\n/).try &.["info"] || "{}" - if initial_data.starts_with?("JSON.parse(\"") - return JSON.parse(JSON.parse(%({"initial_data":"#{initial_data[12..-3]}"}))["initial_data"].as_s).as_h - else - return JSON.parse(initial_data).as_h - end + return JSON.parse(body.match(/(window\["ytInitialData"\]|var\s*ytInitialData)\s*=\s*(JSON\.parse\(")?(?<info>\{.*?\})("\))?;/m).try &.["info"] || "{}").as_h end def proxy_file(response, env) diff --git a/src/invidious/jobs/bypass_captcha_job.cr b/src/invidious/jobs/bypass_captcha_job.cr index 8b69e01a..6778f7c3 100644 --- a/src/invidious/jobs/bypass_captcha_job.cr +++ b/src/invidious/jobs/bypass_captcha_job.cr @@ -23,7 +23,8 @@ class Invidious::Jobs::BypassCaptchaJob < Invidious::Jobs::BaseJob headers = response.cookies.add_request_headers(HTTP::Headers.new) - response = JSON.parse(HTTP::Client.post("https://api.anti-captcha.com/createTask", body: { + response = JSON.parse(HTTP::Client.post(config.captcha_api_url + "/createTask", + headers: HTTP::Headers{"Content-Type" => "application/json"}, body: { "clientKey" => config.captcha_key, "task" => { "type" => "NoCaptchaTaskProxyless", @@ -39,7 +40,8 @@ class Invidious::Jobs::BypassCaptchaJob < Invidious::Jobs::BaseJob loop do sleep 10.seconds - response = JSON.parse(HTTP::Client.post("https://api.anti-captcha.com/getTaskResult", body: { + response = JSON.parse(HTTP::Client.post(config.captcha_api_url + "/getTaskResult", + headers: HTTP::Headers{"Content-Type" => "application/json"}, body: { "clientKey" => config.captcha_key, "taskId" => task_id, }.to_json).body) @@ -76,9 +78,10 @@ class Invidious::Jobs::BypassCaptchaJob < Invidious::Jobs::BaseJob inputs[node["name"]] = node["value"] end - captcha_client = HTTPClient.new(URI.parse("https://api.anti-captcha.com")) + captcha_client = HTTPClient.new(URI.parse(config.captcha_api_url)) captcha_client.family = config.force_resolve || Socket::Family::INET - response = JSON.parse(captcha_client.post("/createTask", body: { + response = JSON.parse(captcha_client.post("/createTask", + headers: HTTP::Headers{"Content-Type" => "application/json"}, body: { "clientKey" => config.captcha_key, "task" => { "type" => "NoCaptchaTaskProxyless", @@ -94,7 +97,8 @@ class Invidious::Jobs::BypassCaptchaJob < Invidious::Jobs::BaseJob loop do sleep 10.seconds - response = JSON.parse(captcha_client.post("/getTaskResult", body: { + response = JSON.parse(captcha_client.post("/getTaskResult", + headers: HTTP::Headers{"Content-Type" => "application/json"}, body: { "clientKey" => config.captcha_key, "taskId" => task_id, }.to_json).body) diff --git a/src/invidious/routes/base_route.cr b/src/invidious/routes/base_route.cr index c6e6667e..2852cb04 100644 --- a/src/invidious/routes/base_route.cr +++ b/src/invidious/routes/base_route.cr @@ -4,6 +4,4 @@ abstract class Invidious::Routes::BaseRoute def initialize(@config, @logger) end - - abstract def handle(env) end diff --git a/src/invidious/routes/embed/index.cr b/src/invidious/routes/embed/index.cr new file mode 100644 index 00000000..79c91d86 --- /dev/null +++ b/src/invidious/routes/embed/index.cr @@ -0,0 +1,27 @@ +class Invidious::Routes::Embed::Index < Invidious::Routes::BaseRoute + def handle(env) + locale = LOCALES[env.get("preferences").as(Preferences).locale]? + + if plid = env.params.query["list"]?.try &.gsub(/[^a-zA-Z0-9_-]/, "") + begin + playlist = get_playlist(PG_DB, plid, locale: locale) + offset = env.params.query["index"]?.try &.to_i? || 0 + videos = get_playlist_videos(PG_DB, playlist, offset: offset, locale: locale) + rescue ex + error_message = ex.message + env.response.status_code = 500 + return templated "error" + end + + url = "/embed/#{videos[0].id}?#{env.params.query}" + + if env.params.query.size > 0 + url += "?#{env.params.query}" + end + else + url = "/" + end + + env.redirect url + end +end diff --git a/src/invidious/routes/embed/show.cr b/src/invidious/routes/embed/show.cr new file mode 100644 index 00000000..23c2b86f --- /dev/null +++ b/src/invidious/routes/embed/show.cr @@ -0,0 +1,174 @@ +class Invidious::Routes::Embed::Show < Invidious::Routes::BaseRoute + def handle(env) + locale = LOCALES[env.get("preferences").as(Preferences).locale]? + id = env.params.url["id"] + + plid = env.params.query["list"]?.try &.gsub(/[^a-zA-Z0-9_-]/, "") + continuation = process_continuation(PG_DB, env.params.query, plid, id) + + if md = env.params.query["playlist"]? + .try &.match(/[a-zA-Z0-9_-]{11}(,[a-zA-Z0-9_-]{11})*/) + video_series = md[0].split(",") + env.params.query.delete("playlist") + end + + preferences = env.get("preferences").as(Preferences) + + if id.includes?("%20") || id.includes?("+") || env.params.query.to_s.includes?("%20") || env.params.query.to_s.includes?("+") + id = env.params.url["id"].gsub("%20", "").delete("+") + + url = "/embed/#{id}" + + if env.params.query.size > 0 + url += "?#{env.params.query.to_s.gsub("%20", "").delete("+")}" + end + + return env.redirect url + end + + # YouTube embed supports `videoseries` with either `list=PLID` + # or `playlist=VIDEO_ID,VIDEO_ID` + case id + when "videoseries" + url = "" + + if plid + begin + playlist = get_playlist(PG_DB, plid, locale: locale) + offset = env.params.query["index"]?.try &.to_i? || 0 + videos = get_playlist_videos(PG_DB, playlist, offset: offset, locale: locale) + rescue ex + error_message = ex.message + env.response.status_code = 500 + return templated "error" + end + + url = "/embed/#{videos[0].id}" + elsif video_series + url = "/embed/#{video_series.shift}" + env.params.query["playlist"] = video_series.join(",") + else + return env.redirect "/" + end + + if env.params.query.size > 0 + url += "?#{env.params.query}" + end + + return env.redirect url + when "live_stream" + response = YT_POOL.client &.get("/embed/live_stream?channel=#{env.params.query["channel"]? || ""}") + video_id = response.body.match(/"video_id":"(?<video_id>[a-zA-Z0-9_-]{11})"/).try &.["video_id"] + + env.params.query.delete_all("channel") + + if !video_id || video_id == "live_stream" + error_message = "Video is unavailable." + return templated "error" + end + + url = "/embed/#{video_id}" + + if env.params.query.size > 0 + url += "?#{env.params.query}" + end + + return env.redirect url + when id.size > 11 + url = "/embed/#{id[0, 11]}" + + if env.params.query.size > 0 + url += "?#{env.params.query}" + end + + return env.redirect url + else nil # Continue + end + + params = process_video_params(env.params.query, preferences) + + user = env.get?("user").try &.as(User) + if user + subscriptions = user.subscriptions + watched = user.watched + notifications = user.notifications + end + subscriptions ||= [] of String + + begin + video = get_video(id, PG_DB, region: params.region) + rescue ex : VideoRedirect + return env.redirect env.request.resource.gsub(id, ex.video_id) + rescue ex + error_message = ex.message + env.response.status_code = 500 + return templated "error" + end + + if preferences.annotations_subscribed && + subscriptions.includes?(video.ucid) && + (env.params.query["iv_load_policy"]? || "1") == "1" + params.annotations = true + end + + # if watched && !watched.includes? id + # PG_DB.exec("UPDATE users SET watched = array_append(watched, $1) WHERE email = $2", id, user.as(User).email) + # end + + if notifications && notifications.includes? id + PG_DB.exec("UPDATE users SET notifications = array_remove(notifications, $1) WHERE email = $2", id, user.as(User).email) + env.get("user").as(User).notifications.delete(id) + notifications.delete(id) + end + + fmt_stream = video.fmt_stream + adaptive_fmts = video.adaptive_fmts + + if params.local + fmt_stream.each { |fmt| fmt["url"] = JSON::Any.new(URI.parse(fmt["url"].as_s).full_path) } + adaptive_fmts.each { |fmt| fmt["url"] = JSON::Any.new(URI.parse(fmt["url"].as_s).full_path) } + end + + video_streams = video.video_streams + audio_streams = video.audio_streams + + if audio_streams.empty? && !video.live_now + if params.quality == "dash" + env.params.query.delete_all("quality") + return env.redirect "/embed/#{id}?#{env.params.query}" + elsif params.listen + env.params.query.delete_all("listen") + env.params.query["listen"] = "0" + return env.redirect "/embed/#{id}?#{env.params.query}" + end + end + + captions = video.captions + + preferred_captions = captions.select { |caption| + params.preferred_captions.includes?(caption.name.simpleText) || + params.preferred_captions.includes?(caption.languageCode.split("-")[0]) + } + preferred_captions.sort_by! { |caption| + (params.preferred_captions.index(caption.name.simpleText) || + params.preferred_captions.index(caption.languageCode.split("-")[0])).not_nil! + } + captions = captions - preferred_captions + + aspect_ratio = nil + + thumbnail = "/vi/#{video.id}/maxres.jpg" + + if params.raw + url = fmt_stream[0]["url"].as_s + + fmt_stream.each do |fmt| + url = fmt["url"].as_s if fmt["quality"].as_s == params.quality + end + + return env.redirect url + end + + rendered "embed" + end +end diff --git a/src/invidious/routes/playlists.cr b/src/invidious/routes/playlists.cr new file mode 100644 index 00000000..52acf266 --- /dev/null +++ b/src/invidious/routes/playlists.cr @@ -0,0 +1,505 @@ +class Invidious::Routes::Playlists < Invidious::Routes::BaseRoute + def index(env) + locale = LOCALES[env.get("preferences").as(Preferences).locale]? + + user = env.get? "user" + referer = get_referer(env) + + return env.redirect "/" if user.nil? + + user = user.as(User) + + items_created = PG_DB.query_all("SELECT * FROM playlists WHERE author = $1 AND id LIKE 'IV%' ORDER BY created", user.email, as: InvidiousPlaylist) + items_created.map! do |item| + item.author = "" + item + end + + items_saved = PG_DB.query_all("SELECT * FROM playlists WHERE author = $1 AND id NOT LIKE 'IV%' ORDER BY created", user.email, as: InvidiousPlaylist) + items_saved.map! do |item| + item.author = "" + item + end + + templated "view_all_playlists" + end + + def new(env) + locale = LOCALES[env.get("preferences").as(Preferences).locale]? + + user = env.get? "user" + sid = env.get? "sid" + referer = get_referer(env) + + return env.redirect "/" if user.nil? + + user = user.as(User) + sid = sid.as(String) + csrf_token = generate_response(sid, {":create_playlist"}, HMAC_KEY, PG_DB) + + templated "create_playlist" + end + + def create(env) + locale = LOCALES[env.get("preferences").as(Preferences).locale]? + + user = env.get? "user" + sid = env.get? "sid" + referer = get_referer(env) + + return env.redirect "/" if user.nil? + + user = user.as(User) + sid = sid.as(String) + token = env.params.body["csrf_token"]? + + begin + validate_request(token, sid, env.request, HMAC_KEY, PG_DB, locale) + rescue ex + error_message = ex.message + env.response.status_code = 400 + return templated "error" + end + + title = env.params.body["title"]?.try &.as(String) + if !title || title.empty? + error_message = "Title cannot be empty." + return templated "error" + end + + privacy = PlaylistPrivacy.parse?(env.params.body["privacy"]?.try &.as(String) || "") + if !privacy + error_message = "Invalid privacy setting." + return templated "error" + end + + if PG_DB.query_one("SELECT count(*) FROM playlists WHERE author = $1", user.email, as: Int64) >= 100 + error_message = "User cannot have more than 100 playlists." + return templated "error" + end + + playlist = create_playlist(PG_DB, title, privacy, user) + + env.redirect "/playlist?list=#{playlist.id}" + end + + def subscribe(env) + locale = LOCALES[env.get("preferences").as(Preferences).locale]? + + user = env.get? "user" + referer = get_referer(env) + + return env.redirect "/" if user.nil? + + user = user.as(User) + + playlist_id = env.params.query["list"] + playlist = get_playlist(PG_DB, playlist_id, locale) + subscribe_playlist(PG_DB, user, playlist) + + env.redirect "/playlist?list=#{playlist.id}" + end + + def delete_page(env) + locale = LOCALES[env.get("preferences").as(Preferences).locale]? + + user = env.get? "user" + sid = env.get? "sid" + referer = get_referer(env) + + return env.redirect "/" if user.nil? + + user = user.as(User) + sid = sid.as(String) + + plid = env.params.query["list"]? + playlist = PG_DB.query_one?("SELECT * FROM playlists WHERE id = $1", plid, as: InvidiousPlaylist) + if !playlist || playlist.author != user.email + return env.redirect referer + end + + csrf_token = generate_response(sid, {":delete_playlist"}, HMAC_KEY, PG_DB) + + templated "delete_playlist" + end + + def delete(env) + locale = LOCALES[env.get("preferences").as(Preferences).locale]? + + user = env.get? "user" + sid = env.get? "sid" + referer = get_referer(env) + + return env.redirect "/" if user.nil? + + plid = env.params.query["list"]? + return env.redirect referer if plid.nil? + + user = user.as(User) + sid = sid.as(String) + token = env.params.body["csrf_token"]? + + begin + validate_request(token, sid, env.request, HMAC_KEY, PG_DB, locale) + rescue ex + error_message = ex.message + env.response.status_code = 400 + return templated "error" + end + + playlist = PG_DB.query_one?("SELECT * FROM playlists WHERE id = $1", plid, as: InvidiousPlaylist) + if !playlist || playlist.author != user.email + return env.redirect referer + end + + PG_DB.exec("DELETE FROM playlist_videos * WHERE plid = $1", plid) + PG_DB.exec("DELETE FROM playlists * WHERE id = $1", plid) + + env.redirect "/view_all_playlists" + end + + def edit(env) + locale = LOCALES[env.get("preferences").as(Preferences).locale]? + + user = env.get? "user" + sid = env.get? "sid" + referer = get_referer(env) + + return env.redirect "/" if user.nil? + + user = user.as(User) + sid = sid.as(String) + + plid = env.params.query["list"]? + if !plid || !plid.starts_with?("IV") + return env.redirect referer + end + + page = env.params.query["page"]?.try &.to_i? + page ||= 1 + + begin + playlist = PG_DB.query_one("SELECT * FROM playlists WHERE id = $1", plid, as: InvidiousPlaylist) + if !playlist || playlist.author != user.email + return env.redirect referer + end + rescue ex + return env.redirect referer + end + + begin + videos = get_playlist_videos(PG_DB, playlist, offset: (page - 1) * 100, locale: locale) + rescue ex + videos = [] of PlaylistVideo + end + + csrf_token = generate_response(sid, {":edit_playlist"}, HMAC_KEY, PG_DB) + + templated "edit_playlist" + end + + def update(env) + locale = LOCALES[env.get("preferences").as(Preferences).locale]? + + user = env.get? "user" + sid = env.get? "sid" + referer = get_referer(env) + + return env.redirect "/" if user.nil? + + plid = env.params.query["list"]? + return env.redirect referer if plid.nil? + + user = user.as(User) + sid = sid.as(String) + token = env.params.body["csrf_token"]? + + begin + validate_request(token, sid, env.request, HMAC_KEY, PG_DB, locale) + rescue ex + error_message = ex.message + env.response.status_code = 400 + return templated "error" + end + + playlist = PG_DB.query_one?("SELECT * FROM playlists WHERE id = $1", plid, as: InvidiousPlaylist) + if !playlist || playlist.author != user.email + return env.redirect referer + end + + title = env.params.body["title"]?.try &.delete("<>") || "" + privacy = PlaylistPrivacy.parse(env.params.body["privacy"]? || "Public") + description = env.params.body["description"]?.try &.delete("\r") || "" + + 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.redirect "/playlist?list=#{plid}" + end + + def add_playlist_items_page(env) + locale = LOCALES[env.get("preferences").as(Preferences).locale]? + + user = env.get? "user" + sid = env.get? "sid" + referer = get_referer(env) + + return env.redirect "/" if user.nil? + + user = user.as(User) + sid = sid.as(String) + + plid = env.params.query["list"]? + if !plid || !plid.starts_with?("IV") + return env.redirect referer + end + + page = env.params.query["page"]?.try &.to_i? + page ||= 1 + + begin + playlist = PG_DB.query_one("SELECT * FROM playlists WHERE id = $1", plid, as: InvidiousPlaylist) + if !playlist || playlist.author != user.email + return env.redirect referer + end + rescue ex + return env.redirect referer + end + + query = env.params.query["q"]? + if query + begin + search_query, count, items = process_search_query(query, page, user, region: nil) + videos = items.select { |item| item.is_a? SearchVideo }.map { |item| item.as(SearchVideo) } + rescue ex + videos = [] of SearchVideo + count = 0 + end + else + videos = [] of SearchVideo + count = 0 + end + + env.set "add_playlist_items", plid + templated "add_playlist_items" + end + + def playlist_ajax(env) + locale = LOCALES[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 + error_message = {"error" => "No such user"}.to_json + env.response.status_code = 403 + return error_message + 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, PG_DB, locale) + rescue ex + if redirect + error_message = ex.message + env.response.status_code = 400 + return templated "error" + else + error_message = {"error" => ex.message}.to_json + env.response.status_code = 400 + return error_message + end + end + + if env.params.query["action_create_playlist"]? + action = "action_create_playlist" + elsif env.params.query["action_delete_playlist"]? + action = "action_delete_playlist" + elsif env.params.query["action_edit_playlist"]? + action = "action_edit_playlist" + elsif env.params.query["action_add_video"]? + action = "action_add_video" + video_id = env.params.query["video_id"] + elsif env.params.query["action_remove_video"]? + action = "action_remove_video" + elsif env.params.query["action_move_video_before"]? + action = "action_move_video_before" + else + return env.redirect referer + end + + begin + playlist_id = env.params.query["playlist_id"] + playlist = get_playlist(PG_DB, playlist_id, locale).as(InvidiousPlaylist) + raise "Invalid user" if playlist.author != user.email + rescue ex + if redirect + error_message = ex.message + env.response.status_code = 400 + return templated "error" + else + error_message = {"error" => ex.message}.to_json + env.response.status_code = 400 + return error_message + end + end + + if !user.password + # TODO: Playlist stub, sync with YouTube for Google accounts + # playlist_ajax(playlist_id, action, env.request.headers) + end + email = user.email + + case action + when "action_edit_playlist" + # TODO: Playlist stub + when "action_add_video" + if playlist.index.size >= 500 + env.response.status_code = 400 + if redirect + error_message = "Playlist cannot have more than 500 videos" + return templated "error" + else + error_message = {"error" => "Playlist cannot have more than 500 videos"}.to_json + return error_message + end + end + + video_id = env.params.query["video_id"] + + begin + video = get_video(video_id, PG_DB) + rescue ex + env.response.status_code = 500 + if redirect + error_message = ex.message + return templated "error" + else + error_message = {"error" => ex.message}.to_json + return error_message + end + 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), + }) + + 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, playlist_id) + when "action_remove_video" + index = env.params.query["set_video_id"] + 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, playlist_id) + when "action_move_video_before" + # TODO: Playlist stub + else + error_message = {"error" => "Unsupported action #{action}"}.to_json + env.response.status_code = 400 + return error_message + end + + if redirect + env.redirect referer + else + env.response.content_type = "application/json" + "{}" + end + end + + def show(env) + locale = LOCALES[env.get("preferences").as(Preferences).locale]? + + user = env.get?("user").try &.as(User) + referer = get_referer(env) + + plid = env.params.query["list"]?.try &.gsub(/[^a-zA-Z0-9_-]/, "") + if !plid + return env.redirect "/" + end + + page = env.params.query["page"]?.try &.to_i? + page ||= 1 + + if plid.starts_with? "RD" + return env.redirect "/mix?list=#{plid}" + end + + begin + playlist = get_playlist(PG_DB, plid, locale) + rescue ex + error_message = ex.message + env.response.status_code = 500 + return templated "error" + end + + if playlist.privacy == PlaylistPrivacy::Private && playlist.author != user.try &.email + error_message = "This playlist is private." + env.response.status_code = 403 + return templated "error" + end + + begin + videos = get_playlist_videos(PG_DB, playlist, offset: (page - 1) * 100, locale: locale) + rescue ex + videos = [] of PlaylistVideo + end + + if playlist.author == user.try &.email + env.set "remove_playlist_items", plid + end + + templated "playlist" + end + + def mix(env) + locale = LOCALES[env.get("preferences").as(Preferences).locale]? + + rdid = env.params.query["list"]? + if !rdid + return env.redirect "/" + end + + continuation = env.params.query["continuation"]? + continuation ||= rdid.lchop("RD") + + begin + mix = fetch_mix(rdid, continuation, locale: locale) + rescue ex + error_message = ex.message + env.response.status_code = 500 + return templated "error" + end + + templated "mix" + end +end diff --git a/src/invidious/routing.cr b/src/invidious/routing.cr index c09dda38..602e6ae5 100644 --- a/src/invidious/routing.cr +++ b/src/invidious/routing.cr @@ -1,8 +1,15 @@ module Invidious::Routing - macro get(path, controller) + macro get(path, controller, method = :handle) get {{ path }} do |env| controller_instance = {{ controller }}.new(config, logger) - controller_instance.handle(env) + controller_instance.{{ method.id }}(env) + end + end + + macro post(path, controller, method = :handle) + post {{ path }} do |env| + controller_instance = {{ controller }}.new(config, logger) + controller_instance.{{ method.id }}(env) end end end diff --git a/src/invidious/videos.cr b/src/invidious/videos.cr index 8e314fe0..20048460 100644 --- a/src/invidious/videos.cr +++ b/src/invidious/videos.cr @@ -839,8 +839,7 @@ def extract_polymer_config(body) params[f] = player_response[f] if player_response[f]? end - yt_initial_data = body.match(/(window\["ytInitialData"\]|var\s+ytInitialData)\s*=\s*(?<info>.*?);\s*\n/) - .try { |r| JSON.parse(r["info"]).as_h } + yt_initial_data = extract_initial_data(body) params["relatedVideos"] = yt_initial_data.try &.["playerOverlays"]?.try &.["playerOverlayRenderer"]? .try &.["endScreen"]?.try &.["watchNextEndScreenRenderer"]?.try &.["results"]?.try &.as_a.compact_map { |r| diff --git a/src/invidious/views/components/player_sources.ecr b/src/invidious/views/components/player_sources.ecr index 8162546e..d02f82d2 100644 --- a/src/invidious/views/components/player_sources.ecr +++ b/src/invidious/views/components/player_sources.ecr @@ -3,6 +3,7 @@ <link rel="stylesheet" href="/css/videojs.markers.min.css?v=<%= ASSET_COMMIT %>"> <link rel="stylesheet" href="/css/videojs-share.css?v=<%= ASSET_COMMIT %>"> <link rel="stylesheet" href="/css/videojs-vtt-thumbnails.css?v=<%= ASSET_COMMIT %>"> +<link rel="stylesheet" href="/css/videojs-vtt-thumbnails-fix.css?v=<%= ASSET_COMMIT %>"> <script src="/js/global.js?v=<%= ASSET_COMMIT %>"></script> <script src="/js/video.min.js?v=<%= ASSET_COMMIT %>"></script> <script src="/js/videojs-contrib-quality-levels.min.js?v=<%= ASSET_COMMIT %>"></script> diff --git a/src/invidious/views/preferences.ecr b/src/invidious/views/preferences.ecr index fb5bd44b..5384e067 100644 --- a/src/invidious/views/preferences.ecr +++ b/src/invidious/views/preferences.ecr @@ -68,7 +68,7 @@ <% preferences.comments.each_with_index do |comments, index| %> <select name="comments[<%= index %>]" id="comments[<%= index %>]"> <% {"", "youtube", "reddit"}.each do |option| %> - <option value="<%= option %>" <% if preferences.comments[index] == option %> selected <% end %>><%= translate(locale, option) %></option> + <option value="<%= option %>" <% if preferences.comments[index] == option %> selected <% end %>><%= translate(locale, option.blank? ? "none" : option) %></option> <% end %> </select> <% end %> @@ -79,7 +79,7 @@ <% preferences.captions.each_with_index do |caption, index| %> <select class="pure-u-1-6" name="captions[<%= index %>]" id="captions[<%= index %>]"> <% CAPTION_LANGUAGES.each do |option| %> - <option value="<%= option %>" <% if preferences.captions[index] == option %> selected <% end %>><%= translate(locale, option) %></option> + <option value="<%= option %>" <% if preferences.captions[index] == option %> selected <% end %>><%= translate(locale, option.blank? ? "none" : option) %></option> <% end %> </select> <% end %> @@ -119,7 +119,7 @@ <label for="dark_mode"><%= translate(locale, "Theme: ") %></label> <select name="dark_mode" id="dark_mode"> <% {"", "light", "dark"}.each do |option| %> - <option value="<%= option %>" <% if preferences.dark_mode == option %> selected <% end %>><%= translate(locale, option) %></option> + <option value="<%= option %>" <% if preferences.dark_mode == option %> selected <% end %>><%= translate(locale, option.blank? ? "auto" : option) %></option> <% end %> </select> </div> @@ -139,7 +139,7 @@ <label for="default_home"><%= translate(locale, "Default homepage: ") %></label> <select name="default_home" id="default_home"> <% feed_options.each do |option| %> - <option value="<%= option %>" <% if preferences.default_home == option %> selected <% end %>><%= translate(locale, option) %></option> + <option value="<%= option %>" <% if preferences.default_home == option %> selected <% end %>><%= translate(locale, option.blank? ? "none" : option) %></option> <% end %> </select> </div> @@ -149,7 +149,7 @@ <% (feed_options.size - 1).times do |index| %> <select name="feed_menu[<%= index %>]" id="feed_menu[<%= index %>]"> <% feed_options.each do |option| %> - <option value="<%= option %>" <% if preferences.feed_menu[index]? == option %> selected <% end %>><%= translate(locale, option) %></option> + <option value="<%= option %>" <% if preferences.feed_menu[index]? == option %> selected <% end %>><%= translate(locale, option.blank? ? "none" : option) %></option> <% end %> </select> <% end %> @@ -211,7 +211,7 @@ <label for="admin_default_home"><%= translate(locale, "Default homepage: ") %></label> <select name="admin_default_home" id="admin_default_home"> <% feed_options.each do |option| %> - <option value="<%= option %>" <% if CONFIG.default_user_preferences.default_home == option %> selected <% end %>><%= translate(locale, option) %></option> + <option value="<%= option %>" <% if CONFIG.default_user_preferences.default_home == option %> selected <% end %>><%= translate(locale, option.blank? ? "none" : option) %></option> <% end %> </select> </div> @@ -221,7 +221,7 @@ <% (feed_options.size - 1).times do |index| %> <select name="admin_feed_menu[<%= index %>]" id="admin_feed_menu[<%= index %>]"> <% feed_options.each do |option| %> - <option value="<%= option %>" <% if CONFIG.default_user_preferences.feed_menu[index]? == option %> selected <% end %>><%= translate(locale, option) %></option> + <option value="<%= option %>" <% if CONFIG.default_user_preferences.feed_menu[index]? == option %> selected <% end %>><%= translate(locale, option.blank? ? "none" : option) %></option> <% end %> </select> <% end %> diff --git a/src/invidious/views/search.ecr b/src/invidious/views/search.ecr index d084bd31..bc13b7ea 100644 --- a/src/invidious/views/search.ecr +++ b/src/invidious/views/search.ecr @@ -2,6 +2,24 @@ <title><%= search_query.not_nil!.size > 30 ? HTML.escape(query.not_nil![0,30].rstrip(".") + "...") : HTML.escape(query.not_nil!) %> - Invidious</title> <% end %> +<div class="pure-g h-box v-box"> + <div class="pure-u-1 pure-u-lg-1-5"> + <% if page > 1 %> + <a href="/search?q=<%= HTML.escape(query.not_nil!) %>&page=<%= page - 1 %>"> + <%= translate(locale, "Previous page") %> + </a> + <% end %> + </div> + <div class="pure-u-1 pure-u-lg-3-5"></div> + <div class="pure-u-1 pure-u-lg-1-5" style="text-align:right"> + <% if count >= 20 %> + <a href="/search?q=<%= HTML.escape(query.not_nil!) %>&page=<%= page + 1 %>"> + <%= translate(locale, "Next page") %> + </a> + <% end %> + </div> +</div> + <div class="pure-g"> <% videos.each_slice(4) do |slice| %> <% slice.each do |item| %> diff --git a/src/invidious/views/template.ecr b/src/invidious/views/template.ecr index 42067bb4..f6361d2d 100644 --- a/src/invidious/views/template.ecr +++ b/src/invidious/views/template.ecr @@ -4,7 +4,6 @@ <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> - <meta name="referrer" content="no-referrer"> <%= yield_content "header" %> <link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png?v=<%= ASSET_COMMIT %>"> <link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png?v=<%= ASSET_COMMIT %>"> |
