Watch view: Better error message when video is unplayable.

This commit is contained in:
mk-pmb feat. Emilien Devos 2025-04-11 15:30:00 +02:00
parent ed2827b25f
commit bcd1f85961
7 changed files with 62 additions and 37 deletions

View File

@ -501,5 +501,8 @@
"toggle_theme": "Toggle Theme", "toggle_theme": "Toggle Theme",
"carousel_slide": "Slide {{current}} of {{total}}", "carousel_slide": "Slide {{current}} of {{total}}",
"carousel_skip": "Skip the Carousel", "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"
} }

View File

@ -73,10 +73,6 @@ def error_template_helper(env : HTTP::Server::Context, status_code : Int32, exce
</div> </div>
END_HTML 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" return templated "error"
end end
@ -86,8 +82,13 @@ def error_template_helper(env : HTTP::Server::Context, status_code : Int32, mess
locale = env.get("preferences").as(Preferences).locale locale = env.get("preferences").as(Preferences).locale
error_message = translate(locale, message) error_message = <<-END_HTML
next_steps = error_redirect_helper(env) <div class="error_message">
<h2>#{translate(locale, "error_processing_data_youtube")}</h2>
<p>#{translate(locale, message)}</p>
#{error_redirect_helper(env)}
</div>
END_HTML
return templated "error" return templated "error"
end end

View File

@ -313,7 +313,7 @@ def get_video(id, refresh = true, region = nil, force_refresh = false)
end end
else else
video = fetch_video(id, region) video = fetch_video(id, region)
Invidious::Database::Videos.insert(video) if !region Invidious::Database::Videos.insert(video) if !region && !video.info.dig?("reason")
end end
return video return video
@ -326,13 +326,13 @@ end
def fetch_video(id, region) def fetch_video(id, region)
info = extract_video_info(video_id: id) 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" if reason == "Video unavailable"
raise NotFoundException.new(reason.as_s || "") raise NotFoundException.new(reason + ": Video not found" || "")
elsif !reason.as_s.starts_with? "Premieres" elsif {"Private video"}.any?(reason)
# dont error when it's a premiere. raise InfoException.new(reason + ": " + subreason || "")
# we already parsed most of the data and display the premiere date
raise InfoException.new(reason.as_s || "")
end end
end end

View File

@ -68,18 +68,20 @@ def extract_video_info(video_id : String)
playability_status = player_response.dig?("playabilityStatus", "status").try &.as_s playability_status = player_response.dig?("playabilityStatus", "status").try &.as_s
if playability_status != "OK" if playability_status != "OK"
subreason = player_response.dig?("playabilityStatus", "errorScreen", "playerErrorMessageRenderer", "subreason") reason = player_response.dig?("playabilityStatus", "reason").try &.as_s
reason = subreason.try &.[]?("simpleText").try &.as_s reason ||= player_response.dig("playabilityStatus", "errorScreen", "playerErrorMessageRenderer", "reason", "simpleText").as_s
reason ||= subreason.try &.[]("runs").as_a.map(&.[]("text")).join("") subreason_main = player_response.dig?("playabilityStatus", "errorScreen", "playerErrorMessageRenderer", "subreason")
reason ||= player_response.dig("playabilityStatus", "reason").as_s 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 # Stop if private video or video not found.
# for LOGIN_REQUIRED when videoDetails element is not found because retrying won't help # But for video unavailable, only stop if playability_status is ERROR because playability_status UNPLAYABLE
if !{"LIVE_STREAM_OFFLINE", "LOGIN_REQUIRED"}.any?(playability_status) || # still gives all the necessary info for displaying the video page (title, description and more)
playability_status == "LOGIN_REQUIRED" && !player_response.dig?("videoDetails") if {"Private video", "Video unavailable"}.any?(reason) && !{"UNPLAYABLE"}.any?(playability_status)
return { return {
"version" => JSON::Any.new(Video::SCHEMA_VERSION.to_i64), "version" => JSON::Any.new(Video::SCHEMA_VERSION.to_i64),
"reason" => JSON::Any.new(reason), "reason" => JSON::Any.new(reason),
"subreason" => JSON::Any.new(subreason),
} }
end end
elsif video_id != player_response.dig("videoDetails", "videoId") elsif video_id != player_response.dig("videoDetails", "videoId")
@ -99,11 +101,8 @@ def extract_video_info(video_id : String)
reason = nil reason = nil
end 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": ""}) next_response = YoutubeAPI.next({"videoId": video_id, "params": ""})
player_response = player_response.merge(next_response) player_response = player_response.merge(next_response)
end
params = parse_video_info(video_id, player_response) params = parse_video_info(video_id, player_response)
params["reason"] = JSON::Any.new(reason) if reason 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 end
video_details = player_response.dig?("videoDetails") video_details = player_response.dig?("videoDetails")
video_details ||= {} of String => JSON::Any
if !(microformat = player_response.dig?("microformat", "playerMicroformatRenderer")) if !(microformat = player_response.dig?("microformat", "playerMicroformatRenderer"))
microformat = {} of String => JSON::Any microformat = {} of String => JSON::Any
end end
raise BrokenTubeException.new("videoDetails") if !video_details
# Basic video infos # Basic video infos
title = video_details["title"]?.try &.as_s 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, # We have to try to extract viewCount from videoPrimaryInfoRenderer first,
# then from videoDetails, as the latter is "0" for livestreams (we want # then from videoDetails, as the latter is "0" for livestreams (we want
# to get the amount of viewers watching). # to get the amount of viewers watching).
@ -483,7 +486,7 @@ def parse_video_info(video_id : String, player_response : Hash(String, JSON::Any
# Description # Description
"description" => JSON::Any.new(description || ""), "description" => JSON::Any.new(description || ""),
"descriptionHtml" => JSON::Any.new(description_html || "<p></p>"), "descriptionHtml" => JSON::Any.new(description_html || "<p></p>"),
"shortDescription" => JSON::Any.new(short_description.try &.as_s || nil), "shortDescription" => JSON::Any.new(short_description.try &.as_s || ""),
# Video metadata # Video metadata
"genre" => JSON::Any.new(genre.try &.as_s || ""), "genre" => JSON::Any.new(genre.try &.as_s || ""),
"genreUcid" => JSON::Any.new(genre_ucid.try &.as_s?), "genreUcid" => JSON::Any.new(genre_ucid.try &.as_s?),

View File

@ -31,7 +31,17 @@
%> %>
</script> </script>
<% if video.reason.nil? %>
<div id="player-container" class="h-box">
<%= rendered "components/player" %> <%= rendered "components/player" %>
</div>
<% else %>
<div id="player-error-container" class="h-box">
<h3>
<%= video.reason %>
</h3>
</div>
<% end %>
<script src="/js/embed.js?v=<%= ASSET_COMMIT %>"></script> <script src="/js/embed.js?v=<%= ASSET_COMMIT %>"></script>
</body> </body>
</html> </html>

View File

@ -4,5 +4,4 @@
<div class="h-box"> <div class="h-box">
<%= error_message %> <%= error_message %>
<%= next_steps %>
</div> </div>

View File

@ -70,9 +70,11 @@ we're going to need to do it here in order to allow for translations.
%> %>
</script> </script>
<% if video.reason.nil? %>
<div id="player-container" class="h-box"> <div id="player-container" class="h-box">
<%= rendered "components/player" %> <%= rendered "components/player" %>
</div> </div>
<% end %>
<div class="h-box"> <div class="h-box">
<h1> <h1>
@ -96,7 +98,10 @@ we're going to need to do it here in order to allow for translations.
<% if video.reason %> <% if video.reason %>
<h3> <h3>
<%= video.reason %> <%= translate(locale, "error_from_youtube_unplayable") %> <%= video.reason %>
</h3>
<h3>
<%= translate(locale, "next_steps_error_message") %>
</h3> </h3>
<% elsif video.premiere_timestamp.try &.> Time.utc %> <% elsif video.premiere_timestamp.try &.> Time.utc %>
<h3> <h3>
@ -112,7 +117,7 @@ we're going to need to do it here in order to allow for translations.
<div class="pure-g"> <div class="pure-g">
<div class="pure-u-1 pure-u-lg-1-5"> <div class="pure-u-1 pure-u-lg-1-5">
<div class="h-box"> <div class="h-box">
<span id="watch-on-youtube"> <p id="watch-on-youtube">
<%- <%-
link_yt_watch = URI.new(scheme: "https", host: "www.youtube.com", path: "/watch", query: "v=#{video.id}") link_yt_watch = URI.new(scheme: "https", host: "www.youtube.com", path: "/watch", query: "v=#{video.id}")
link_yt_embed = URI.new(scheme: "https", host: "www.youtube.com", path: "/embed/#{video.id}") link_yt_embed = URI.new(scheme: "https", host: "www.youtube.com", path: "/embed/#{video.id}")
@ -125,7 +130,7 @@ we're going to need to do it here in order to allow for translations.
-%> -%>
<a id="link-yt-watch" rel="noreferrer noopener" data-base-url="<%= link_yt_watch %>" href="<%= link_yt_watch %>"><%= translate(locale, "videoinfo_watch_on_youTube") %></a> <a id="link-yt-watch" rel="noreferrer noopener" data-base-url="<%= link_yt_watch %>" href="<%= link_yt_watch %>"><%= translate(locale, "videoinfo_watch_on_youTube") %></a>
(<a id="link-yt-embed" rel="noreferrer noopener" data-base-url="<%= link_yt_embed %>" href="<%= link_yt_embed %>"><%= translate(locale, "videoinfo_youTube_embed_link") %></a>) (<a id="link-yt-embed" rel="noreferrer noopener" data-base-url="<%= link_yt_embed %>" href="<%= link_yt_embed %>"><%= translate(locale, "videoinfo_youTube_embed_link") %></a>)
</span> </p>
<p id="watch-on-another-invidious-instance"> <p id="watch-on-another-invidious-instance">
<%- link_iv_other = IV::Frontend::Misc.redirect_url(env) -%> <%- link_iv_other = IV::Frontend::Misc.redirect_url(env) -%>
@ -185,11 +190,14 @@ we're going to need to do it here in order to allow for translations.
<% end %> <% end %>
<% end %> <% end %>
<% if video_assets %>
<%= Invidious::Frontend::WatchPage.download_widget(locale, video, video_assets) %> <%= Invidious::Frontend::WatchPage.download_widget(locale, video, video_assets) %>
<% end %>
<p id="views"><i class="icon ion-ios-eye"></i> <%= number_with_separator(video.views) %></p> <p id="views"><i class="icon ion-ios-eye"></i> <%= number_with_separator(video.views) %></p>
<p id="likes"><i class="icon ion-ios-thumbs-up"></i> <%= number_with_separator(video.likes) %></p> <p id="likes"><i class="icon ion-ios-thumbs-up"></i> <%= number_with_separator(video.likes) %></p>
<p id="dislikes" style="display: none; visibility: hidden;"></p> <p id="dislikes" style="display: none; visibility: hidden;"></p>
<% if video.genre %>
<p id="genre"><%= translate(locale, "Genre: ") %> <p id="genre"><%= translate(locale, "Genre: ") %>
<% if !video.genre_url %> <% if !video.genre_url %>
<%= video.genre %> <%= video.genre %>
@ -197,6 +205,7 @@ we're going to need to do it here in order to allow for translations.
<a href="<%= video.genre_url %>"><%= video.genre %></a> <a href="<%= video.genre_url %>"><%= video.genre %></a>
<% end %> <% end %>
</p> </p>
<% end %>
<% if video.license %> <% if video.license %>
<% if video.license.empty? %> <% if video.license.empty? %>
<p id="license"><%= translate(locale, "License: ") %><%= translate(locale, "Standard YouTube license") %></p> <p id="license"><%= translate(locale, "License: ") %><%= translate(locale, "Standard YouTube license") %></p>