mirror of
https://git.nadeko.net/Fijxu/invidious.git
synced 2025-06-27 17:38:25 +00:00
Merge branch 'transcripts-support'
Some checks failed
Build and release container directly from master / release (push) Has been cancelled
Invidious CI / build - crystal: ${{ matrix.crystal }}, stable: ${{ matrix.stable }} (1.12.1, true) (push) Has been cancelled
Invidious CI / build - crystal: ${{ matrix.crystal }}, stable: ${{ matrix.stable }} (1.13.2, true) (push) Has been cancelled
Invidious CI / build - crystal: ${{ matrix.crystal }}, stable: ${{ matrix.stable }} (1.14.0, true) (push) Has been cancelled
Invidious CI / build - crystal: ${{ matrix.crystal }}, stable: ${{ matrix.stable }} (1.15.0, true) (push) Has been cancelled
Invidious CI / build - crystal: ${{ matrix.crystal }}, stable: ${{ matrix.stable }} (nightly, false) (push) Has been cancelled
Invidious CI / build-docker (push) Has been cancelled
Invidious CI / build-docker-arm64 (push) Has been cancelled
Invidious CI / lint (push) Has been cancelled
Some checks failed
Build and release container directly from master / release (push) Has been cancelled
Invidious CI / build - crystal: ${{ matrix.crystal }}, stable: ${{ matrix.stable }} (1.12.1, true) (push) Has been cancelled
Invidious CI / build - crystal: ${{ matrix.crystal }}, stable: ${{ matrix.stable }} (1.13.2, true) (push) Has been cancelled
Invidious CI / build - crystal: ${{ matrix.crystal }}, stable: ${{ matrix.stable }} (1.14.0, true) (push) Has been cancelled
Invidious CI / build - crystal: ${{ matrix.crystal }}, stable: ${{ matrix.stable }} (1.15.0, true) (push) Has been cancelled
Invidious CI / build - crystal: ${{ matrix.crystal }}, stable: ${{ matrix.stable }} (nightly, false) (push) Has been cancelled
Invidious CI / build-docker (push) Has been cancelled
Invidious CI / build-docker-arm64 (push) Has been cancelled
Invidious CI / lint (push) Has been cancelled
From: https://github.com/iv-org/invidious/pull/5298/
This commit is contained in:
commit
b2c3583663
@ -992,3 +992,80 @@ h1, h2, h3, h4, h5, p,
|
|||||||
#comments-turned-off-on-video-message > p, #comments-disabled-message > p {
|
#comments-turned-off-on-video-message > p, #comments-disabled-message > p {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.description-widget {
|
||||||
|
display: flex;
|
||||||
|
white-space: normal;
|
||||||
|
gap: 15px;
|
||||||
|
|
||||||
|
border-top: 1px solid black;
|
||||||
|
}
|
||||||
|
|
||||||
|
.description-show-transcript-widget {
|
||||||
|
padding: 10px;
|
||||||
|
flex-direction: column
|
||||||
|
}
|
||||||
|
|
||||||
|
.description-show-transcript-widget > * {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-transcript {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 25px;
|
||||||
|
height: 30em;
|
||||||
|
padding: 10px;
|
||||||
|
border: 1px solid #a0a0a0;
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-transcript header {
|
||||||
|
padding-bottom: 5px;
|
||||||
|
border-bottom: 1px solid #a0a0a0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-transcript > #lines {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: scroll;
|
||||||
|
}
|
||||||
|
|
||||||
|
.transcript-line, .transcript-title-line {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 20px 10px;
|
||||||
|
gap: 10px;
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.transcript-line > .length {
|
||||||
|
padding: 0px 5px;
|
||||||
|
background: #363636 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.transcript-line > p, .video-transcript > header > h3, .transcript-title-line > h4 {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.transcript-line:hover, .selected.transcript-line, .transcript-title-line:hover, .selected.transcript-title-line {
|
||||||
|
background: #4f4f4f;
|
||||||
|
}
|
||||||
|
|
||||||
|
.light-theme .transcript-line:hover, .selected.transcript-line, .transcript-title-line:hover, .selected.transcript-title-line {
|
||||||
|
background: #cacaca;
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-transcript > footer {
|
||||||
|
padding-bottom: 14px;
|
||||||
|
border-top: 1px solid #a0a0a0
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-transcript > footer > form {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-transcript > footer select {
|
||||||
|
width: 75%;
|
||||||
|
}
|
||||||
|
@ -197,4 +197,14 @@ addEventListener('load', function (e) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
document.getElementById("try-reddit-comments-link").onclick = swap_comments;
|
addEventListener("DOMContentLoaded", () => {
|
||||||
|
const transcriptLines = document.getElementById("lines");
|
||||||
|
for (const transcriptLine of transcriptLines.children) {
|
||||||
|
if (transcriptLine.nodeName != "A") continue
|
||||||
|
|
||||||
|
transcriptLine.addEventListener("click", (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
player.currentTime(transcriptLine.getAttribute('data-jump-time'));
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
@ -536,5 +536,11 @@
|
|||||||
"backend_unavailable": "The backend you selected is unavailable. You have been redirected to the next one",
|
"backend_unavailable": "The backend you selected is unavailable. You have been redirected to the next one",
|
||||||
"timeline_parse_error_placeholder_heading": "Unable to parse item",
|
"timeline_parse_error_placeholder_heading": "Unable to parse item",
|
||||||
"timeline_parse_error_placeholder_message": "Invidious encountered an error while trying to parse this item. For more information see below:",
|
"timeline_parse_error_placeholder_message": "Invidious encountered an error while trying to parse this item. For more information see below:",
|
||||||
"timeline_parse_error_show_technical_details": "Show technical details"
|
"timeline_parse_error_show_technical_details": "Show technical details",
|
||||||
|
"video_description_toggle_transcript_widget_label": "Transcripts",
|
||||||
|
"video_description_toggle_transcript_widget_button_label_show": "Show transcript",
|
||||||
|
"video_description_toggle_transcript_widget_button_label_hide": "Hide transcript",
|
||||||
|
"error_transcripts_none_available": "No transcripts are available",
|
||||||
|
"transcript_widget_title": "Transcript",
|
||||||
|
"transcript_widget_no_js_change_transcript_btn": "Swap"
|
||||||
}
|
}
|
||||||
|
@ -38,6 +38,14 @@ module Invidious::Routes::Watch
|
|||||||
nojs ||= "0"
|
nojs ||= "0"
|
||||||
nojs = nojs == "1"
|
nojs = nojs == "1"
|
||||||
|
|
||||||
|
show_transcripts = env.params.query["show_transcripts"]?
|
||||||
|
|
||||||
|
show_transcripts ||= "0"
|
||||||
|
show_transcripts = show_transcripts == "1"
|
||||||
|
|
||||||
|
# Equal to a `caption.name` when set
|
||||||
|
selected_transcript = env.params.query["use_this_transcript"]?
|
||||||
|
|
||||||
preferences = env.get("preferences").as(Preferences)
|
preferences = env.get("preferences").as(Preferences)
|
||||||
|
|
||||||
user = env.get?("user").try &.as(User)
|
user = env.get?("user").try &.as(User)
|
||||||
@ -172,6 +180,60 @@ module Invidious::Routes::Watch
|
|||||||
}
|
}
|
||||||
captions = captions - preferred_captions
|
captions = captions - preferred_captions
|
||||||
|
|
||||||
|
if show_transcripts
|
||||||
|
# Transcripts can be mapped 1:1 to a video's captions.
|
||||||
|
# As such the amount of transcripts available is the same as the amount of captions available.
|
||||||
|
#
|
||||||
|
# To request transcripts we have to give a language code, and a boolean dictating whether or not
|
||||||
|
# it is auto-generated. These attributes can be retrieved from the video's caption metadata.
|
||||||
|
|
||||||
|
# First we check if a transcript has been explicitly selected.
|
||||||
|
# The `use_this_transcript` url parameter provides the label of the transcript the user wants.
|
||||||
|
if selected_transcript
|
||||||
|
selected_transcript = URI.decode_www_form(selected_transcript)
|
||||||
|
target_transcript = captions.select(&.name.== selected_transcript)
|
||||||
|
else
|
||||||
|
target_transcript = nil
|
||||||
|
end
|
||||||
|
|
||||||
|
# If the selected transcript has a match then we'll request that.
|
||||||
|
#
|
||||||
|
# If it does not match we'll try and request a transcript based on the user's
|
||||||
|
# preferred transcript
|
||||||
|
#
|
||||||
|
# If that also does not match then we'll just select the first transcript
|
||||||
|
# out of everything that's available.
|
||||||
|
#
|
||||||
|
# Raises when no matches are found
|
||||||
|
if target_transcript.is_a?(Array) && !target_transcript.empty?
|
||||||
|
target_transcript = target_transcript[0]
|
||||||
|
else
|
||||||
|
if !preferred_captions.empty?
|
||||||
|
target_transcript = preferred_captions[0]
|
||||||
|
elsif !captions.empty?
|
||||||
|
target_transcript = captions[0]
|
||||||
|
else
|
||||||
|
return error_template(404, "error_transcripts_none_available")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
transcript_request_param = Invidious::Videos::Transcript.generate_param(
|
||||||
|
id, target_transcript.language_code, target_transcript.auto_generated
|
||||||
|
)
|
||||||
|
|
||||||
|
begin
|
||||||
|
transcript = Invidious::Videos::Transcript.from_raw(
|
||||||
|
YoutubeAPI.get_transcript(transcript_request_param),
|
||||||
|
target_transcript.language_code,
|
||||||
|
target_transcript.auto_generated,
|
||||||
|
)
|
||||||
|
rescue NotFoundException
|
||||||
|
return error_template(404, "error_transcripts_none_available")
|
||||||
|
end
|
||||||
|
else
|
||||||
|
transcript = nil
|
||||||
|
end
|
||||||
|
|
||||||
aspect_ratio = "16:9"
|
aspect_ratio = "16:9"
|
||||||
|
|
||||||
thumbnail = "/vi/#{video.id}/maxres.jpg"
|
thumbnail = "/vi/#{video.id}/maxres.jpg"
|
||||||
|
@ -80,14 +80,15 @@ module Invidious::Videos
|
|||||||
initial_segments.each do |line|
|
initial_segments.each do |line|
|
||||||
if unpacked_line = line["transcriptSectionHeaderRenderer"]?
|
if unpacked_line = line["transcriptSectionHeaderRenderer"]?
|
||||||
line_type = HeadingLine
|
line_type = HeadingLine
|
||||||
|
text = (unpacked_line.dig?("sectionHeader", "sectionHeaderViewModel", "headline", "content").try &.as_s) || ""
|
||||||
else
|
else
|
||||||
unpacked_line = line["transcriptSegmentRenderer"]
|
unpacked_line = line["transcriptSegmentRenderer"]
|
||||||
|
text = extract_text(unpacked_line["snippet"]) || ""
|
||||||
line_type = RegularLine
|
line_type = RegularLine
|
||||||
end
|
end
|
||||||
|
|
||||||
start_ms = unpacked_line["startMs"].as_s.to_i.millisecond
|
start_ms = unpacked_line["startMs"].as_s.to_i.millisecond
|
||||||
end_ms = unpacked_line["endMs"].as_s.to_i.millisecond
|
end_ms = unpacked_line["endMs"].as_s.to_i.millisecond
|
||||||
text = extract_text(unpacked_line["snippet"]) || ""
|
|
||||||
|
|
||||||
lines << line_type.new(start_ms, end_ms, text)
|
lines << line_type.new(start_ms, end_ms, text)
|
||||||
end
|
end
|
||||||
|
@ -0,0 +1,13 @@
|
|||||||
|
<% if captions %>
|
||||||
|
<div class="description-widget description-show-transcript-widget">
|
||||||
|
<h4><%=HTML.escape(translate(locale, "video_description_toggle_transcript_widget_label"))%></h4>
|
||||||
|
<% if transcript %>
|
||||||
|
<% hide_transcripts_param = env.params.query.dup %>
|
||||||
|
<% hide_transcripts_param.delete_all("show_transcripts") %>
|
||||||
|
|
||||||
|
<a class="pure-button pure-button-secondary" href="/watch?<%= hide_transcripts_param %>"><%=HTML.escape(translate(locale, "video_description_toggle_transcript_widget_button_label_hide"))%></a>
|
||||||
|
<% else %>
|
||||||
|
<a class="pure-button pure-button-secondary" href="/watch?<%= env.params.query %>&show_transcripts=1"><%=HTML.escape(translate(locale, "video_description_toggle_transcript_widget_button_label_show"))%></a>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
46
src/invidious/views/components/no-js-transcript-ui.ecr
Normal file
46
src/invidious/views/components/no-js-transcript-ui.ecr
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
<section class="video-transcript">
|
||||||
|
<header><h3><%=translate(locale, "transcript_widget_title")%></h3></header>
|
||||||
|
<div id="lines">
|
||||||
|
<% transcript_time_url_param = env.params.query.dup %>
|
||||||
|
<% transcript_time_url_param.delete_all("t") %>
|
||||||
|
|
||||||
|
<% transcript.lines.each do | line | %>
|
||||||
|
<% if line.is_a? Invidious::Videos::Transcript::HeadingLine %>
|
||||||
|
<div class="transcript-title-line"> <h4> <%= line.line %> </h4> </div>
|
||||||
|
<% else %>
|
||||||
|
<% jump_time = line.start_ms.total_seconds.to_s %>
|
||||||
|
<% transcript_time_url_param["t"] = jump_time %>
|
||||||
|
<a href="/watch?<%= transcript_time_url_param %>" data-onclick="jump_to_time" data-jump-time="<%=jump_time%>">
|
||||||
|
<div class="transcript-line">
|
||||||
|
<p class="length"><%= recode_length_seconds(line.start_ms.total_seconds) %></p>
|
||||||
|
<p><%= line.line %></p>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
<% end %>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
<footer>
|
||||||
|
<% transcript_select_args = env.params.query.dup %>
|
||||||
|
<% transcript_select_args.delete_all("use_this_transcript") %>
|
||||||
|
<form class="select-transcript" method="get" action="/watch?<%=transcript_select_args%>">
|
||||||
|
<% # Preserve query parameters %>
|
||||||
|
<% transcript_select_args.each do | k, v | %>
|
||||||
|
<input type="hidden" name="<%=k%>" value="<%=v%>">
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
<select name="use_this_transcript">
|
||||||
|
<% {preferred_captions, captions}.each do | transcript_list |%>
|
||||||
|
<% transcript_list.each do | transcript_option | %>
|
||||||
|
<% if target_transcript.not_nil!.name == transcript_option.name %>
|
||||||
|
<option value="<%=URI.encode_www_form(transcript_option.name)%>" selected><%=transcript_option.name%></option>
|
||||||
|
<% else%>
|
||||||
|
<option value="<%=URI.encode_www_form(transcript_option.name)%>"><%=transcript_option.name%></option>
|
||||||
|
<% end %>
|
||||||
|
<% end %>
|
||||||
|
<% end %>
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<input type="submit" value="<%= translate(locale, "transcript_widget_no_js_change_transcript_btn") %>"/>
|
||||||
|
</form>
|
||||||
|
</footer>
|
||||||
|
</section>
|
@ -284,10 +284,14 @@ we're going to need to do it here in order to allow for translations.
|
|||||||
|
|
||||||
<div id="description-box"> <!-- Description -->
|
<div id="description-box"> <!-- Description -->
|
||||||
<% if video.description.size < 200 || params.extend_desc %>
|
<% if video.description.size < 200 || params.extend_desc %>
|
||||||
<div id="descriptionWrapper"><%= video.description_html %></div>
|
<div id="descriptionWrapper"><%= video.description_html %>
|
||||||
|
<%= rendered "components/description_toggle_transcripts_widget" %>
|
||||||
|
</div>
|
||||||
<% else %>
|
<% else %>
|
||||||
<input id="descexpansionbutton" type="checkbox"/>
|
<input id="descexpansionbutton" type="checkbox"/>
|
||||||
<div id="descriptionWrapper"><%= video.description_html %></div>
|
<div id="descriptionWrapper"><%-= video.description_html %>
|
||||||
|
<%= rendered "components/description_toggle_transcripts_widget" %>
|
||||||
|
</div>
|
||||||
<label for="descexpansionbutton">
|
<label for="descexpansionbutton">
|
||||||
<a></a>
|
<a></a>
|
||||||
</label>
|
</label>
|
||||||
@ -361,6 +365,10 @@ we're going to need to do it here in order to allow for translations.
|
|||||||
|
|
||||||
<% if params.related_videos || plid %>
|
<% if params.related_videos || plid %>
|
||||||
<div class="pure-u-1 pure-u-lg-1-5">
|
<div class="pure-u-1 pure-u-lg-1-5">
|
||||||
|
<% if transcript %>
|
||||||
|
<%= rendered "components/no-js-transcript-ui" %>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
<% if plid %>
|
<% if plid %>
|
||||||
<div id="playlist" class="h-box"></div>
|
<div id="playlist" class="h-box"></div>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
Loading…
Reference in New Issue
Block a user