diff --git a/locales/en-US.json b/locales/en-US.json index 4f2c2770..f7c76f69 100644 --- a/locales/en-US.json +++ b/locales/en-US.json @@ -501,5 +501,8 @@ "toggle_theme": "Toggle Theme", "carousel_slide": "Slide {{current}} of {{total}}", "carousel_skip": "Skip the Carousel", - "carousel_go_to": "Go to slide `x`" + "carousel_go_to": "Go to slide `x`", + "error_from_youtube_unplayable": "Video unplayable due to an error from YouTube:", + "error_processing_data_youtube": "Error while processing the data sent by YouTube", + "refresh_page": "Refresh the page" } diff --git a/src/invidious/helpers/errors.cr b/src/invidious/helpers/errors.cr index 900cb0c6..4cfe63f3 100644 --- a/src/invidious/helpers/errors.cr +++ b/src/invidious/helpers/errors.cr @@ -73,10 +73,6 @@ def error_template_helper(env : HTTP::Server::Context, status_code : Int32, exce END_HTML - # Don't show the usual "next steps" widget. The same options are - # proposed above the error message, just worded differently. - next_steps = "" - return templated "error" end @@ -86,8 +82,13 @@ def error_template_helper(env : HTTP::Server::Context, status_code : Int32, mess locale = env.get("preferences").as(Preferences).locale - error_message = translate(locale, message) - next_steps = error_redirect_helper(env) + error_message = <<-END_HTML +
+ END_HTML return templated "error" end diff --git a/src/invidious/videos.cr b/src/invidious/videos.cr index 348a0a66..d7a40cf7 100644 --- a/src/invidious/videos.cr +++ b/src/invidious/videos.cr @@ -313,7 +313,7 @@ def get_video(id, refresh = true, region = nil, force_refresh = false) end else video = fetch_video(id, region) - Invidious::Database::Videos.insert(video) if !region + Invidious::Database::Videos.insert(video) if !region && !video.info.dig?("reason") end return video @@ -326,13 +326,13 @@ end def fetch_video(id, region) info = extract_video_info(video_id: id) - if reason = info["reason"]? + if info["reason"]? && info["subreason"]? + reason = info["reason"].as_s + subreason = info["subreason"].as_s if reason == "Video unavailable" - raise NotFoundException.new(reason.as_s || "") - elsif !reason.as_s.starts_with? "Premieres" - # dont error when it's a premiere. - # we already parsed most of the data and display the premiere date - raise InfoException.new(reason.as_s || "") + raise NotFoundException.new(reason + ": Video not found" || "") + elsif {"Private video"}.any?(reason) + raise InfoException.new(reason + ": " + subreason || "") end end diff --git a/src/invidious/videos/parser.cr b/src/invidious/videos/parser.cr index 26d74f37..387e5015 100644 --- a/src/invidious/videos/parser.cr +++ b/src/invidious/videos/parser.cr @@ -68,18 +68,20 @@ def extract_video_info(video_id : String) playability_status = player_response.dig?("playabilityStatus", "status").try &.as_s if playability_status != "OK" - subreason = player_response.dig?("playabilityStatus", "errorScreen", "playerErrorMessageRenderer", "subreason") - reason = subreason.try &.[]?("simpleText").try &.as_s - reason ||= subreason.try &.[]("runs").as_a.map(&.[]("text")).join("") - reason ||= player_response.dig("playabilityStatus", "reason").as_s + reason = player_response.dig?("playabilityStatus", "reason").try &.as_s + reason ||= player_response.dig("playabilityStatus", "errorScreen", "playerErrorMessageRenderer", "reason", "simpleText").as_s + subreason_main = player_response.dig?("playabilityStatus", "errorScreen", "playerErrorMessageRenderer", "subreason") + subreason = subreason_main.try &.[]?("simpleText").try &.as_s + subreason ||= subreason_main.try &.[]("runs").as_a.map(&.[]("text")).join("") - # Stop here if video is not a scheduled livestream or - # for LOGIN_REQUIRED when videoDetails element is not found because retrying won't help - if !{"LIVE_STREAM_OFFLINE", "LOGIN_REQUIRED"}.any?(playability_status) || - playability_status == "LOGIN_REQUIRED" && !player_response.dig?("videoDetails") + # Stop if private video or video not found. + # But for video unavailable, only stop if playability_status is ERROR because playability_status UNPLAYABLE + # still gives all the necessary info for displaying the video page (title, description and more) + if {"Private video", "Video unavailable"}.any?(reason) && !{"UNPLAYABLE"}.any?(playability_status) return { - "version" => JSON::Any.new(Video::SCHEMA_VERSION.to_i64), - "reason" => JSON::Any.new(reason), + "version" => JSON::Any.new(Video::SCHEMA_VERSION.to_i64), + "reason" => JSON::Any.new(reason), + "subreason" => JSON::Any.new(subreason), } end elsif video_id != player_response.dig("videoDetails", "videoId") @@ -99,11 +101,8 @@ def extract_video_info(video_id : String) reason = nil end - # Don't fetch the next endpoint if the video is unavailable. - if {"OK", "LIVE_STREAM_OFFLINE", "LOGIN_REQUIRED"}.any?(playability_status) - next_response = YoutubeAPI.next({"videoId": video_id, "params": ""}) - player_response = player_response.merge(next_response) - end + next_response = YoutubeAPI.next({"videoId": video_id, "params": ""}) + player_response = player_response.merge(next_response) params = parse_video_info(video_id, player_response) params["reason"] = JSON::Any.new(reason) if reason @@ -197,16 +196,20 @@ def parse_video_info(video_id : String, player_response : Hash(String, JSON::Any end video_details = player_response.dig?("videoDetails") + video_details ||= {} of String => JSON::Any if !(microformat = player_response.dig?("microformat", "playerMicroformatRenderer")) microformat = {} of String => JSON::Any end - raise BrokenTubeException.new("videoDetails") if !video_details - # Basic video infos title = video_details["title"]?.try &.as_s + title ||= extract_text( + video_primary_renderer + .try &.dig?("title") + ) + # We have to try to extract viewCount from videoPrimaryInfoRenderer first, # then from videoDetails, as the latter is "0" for livestreams (we want # to get the amount of viewers watching). @@ -217,12 +220,25 @@ def parse_video_info(video_id : String, player_response : Hash(String, JSON::Any views_txt ||= video_details["viewCount"]?.try &.as_s || "" views = views_txt.gsub(/\D/, "").to_i64? - length_txt = (microformat["lengthSeconds"]? || video_details["lengthSeconds"]) + length_txt = (microformat["lengthSeconds"]? || video_details["lengthSeconds"]?) .try &.as_s.to_i64 published = microformat["publishDate"]? .try { |t| Time.parse(t.as_s, "%Y-%m-%d", Time::Location::UTC) } || Time.utc + if published.nil? + published_txt = video_primary_renderer + .try &.dig?("dateText", "simpleText") + + if published_txt.try &.as_s.includes?("ago") && !published_txt.nil? + published = decode_date(published_txt.as_s.lchop("Started streaming ")) + elsif published_txt && published_txt.try &.as_s.matches?(/(\w{3} \d{1,2}, \d{4})$/) + published = Time.parse(published_txt.as_s.match!(/(\w{3} \d{1,2}, \d{4})$/)[0], "%b %-d, %Y", Time::Location::UTC) + else + published = Time.utc + end + end + premiere_timestamp = microformat.dig?("liveBroadcastDetails", "startTimestamp") .try { |t| Time.parse_rfc3339(t.as_s) } @@ -236,6 +252,10 @@ def parse_video_info(video_id : String, player_response : Hash(String, JSON::Any live_now = microformat.dig?("liveBroadcastDetails", "isLiveNow") .try &.as_bool + if live_now.nil? + live_now = video_primary_renderer + .try &.dig?("viewCount", "videoViewCountRenderer", "isLive").try &.as_bool + end live_now ||= video_details.dig?("isLive").try &.as_bool || false post_live_dvr = video_details.dig?("isPostLiveDvr") @@ -247,8 +267,23 @@ def parse_video_info(video_id : String, player_response : Hash(String, JSON::Any .try &.as_a.map &.as_s || [] of String allow_ratings = video_details["allowRatings"]?.try &.as_bool + family_friendly = microformat["isFamilySafe"]?.try &.as_bool + if family_friendly.nil? + family_friendly = true # if isFamilySafe not found then assume is safe + end + is_listed = video_details["isCrawlable"]?.try &.as_bool + if is_listed.nil? + if video_badges = video_primary_renderer.try &.dig?("badges") + is_listed = !has_unlisted_badge?(video_badges) + else + # If video has no badges and videoDetails is not + # available, then assume isListed + is_listed = true + end + end + is_upcoming = video_details["isUpcoming"]?.try &.as_bool keywords = video_details["keywords"]? @@ -414,6 +449,9 @@ def parse_video_info(video_id : String, player_response : Hash(String, JSON::Any subs_text = author_info["subscriberCountText"]? .try { |t| t["simpleText"]? || t.dig?("runs", 0, "text") } .try &.as_s.split(" ", 2)[0] + + author ||= author_info.dig?("title", "runs", 0, "text").try &.as_s + ucid ||= author_info.dig?("title", "runs", 0, "navigationEndpoint", "browseEndpoint", "browseId").try &.as_s end # Return data @@ -438,8 +476,8 @@ def parse_video_info(video_id : String, player_response : Hash(String, JSON::Any # Extra video infos "allowedRegions" => JSON::Any.new(allowed_regions.map { |v| JSON::Any.new(v) }), "allowRatings" => JSON::Any.new(allow_ratings || false), - "isFamilyFriendly" => JSON::Any.new(family_friendly || false), - "isListed" => JSON::Any.new(is_listed || false), + "isFamilyFriendly" => JSON::Any.new(family_friendly), + "isListed" => JSON::Any.new(is_listed), "isUpcoming" => JSON::Any.new(is_upcoming || false), "keywords" => JSON::Any.new(keywords.map { |v| JSON::Any.new(v) }), "isPostLiveDvr" => JSON::Any.new(post_live_dvr), @@ -448,7 +486,7 @@ def parse_video_info(video_id : String, player_response : Hash(String, JSON::Any # Description "description" => JSON::Any.new(description || ""), "descriptionHtml" => JSON::Any.new(description_html || ""), - "shortDescription" => JSON::Any.new(short_description.try &.as_s || nil), + "shortDescription" => JSON::Any.new(short_description.try &.as_s || ""), # Video metadata "genre" => JSON::Any.new(genre.try &.as_s || ""), "genreUcid" => JSON::Any.new(genre_ucid.try &.as_s?), diff --git a/src/invidious/views/components/player.ecr b/src/invidious/views/components/player.ecr index af352102..78a96da9 100644 --- a/src/invidious/views/components/player.ecr +++ b/src/invidious/views/components/player.ecr @@ -1,3 +1,4 @@ +<% if audio_streams && fmt_stream && preferred_captions && captions %>