From 654f5fd45e08e5019dd851f8b5d224ca5c47a81c Mon Sep 17 00:00:00 2001 From: syeopite Date: Wed, 12 Jun 2024 10:00:16 -0700 Subject: [PATCH 1/9] Add btn in video desc to show transcript --- assets/css/default.css | 17 +++++++++++++++++ locales/en-US.json | 4 +++- src/invidious/views/watch.ecr | 9 ++++++++- 3 files changed, 28 insertions(+), 2 deletions(-) diff --git a/assets/css/default.css b/assets/css/default.css index 2cedcf0c..3544282a 100644 --- a/assets/css/default.css +++ b/assets/css/default.css @@ -816,3 +816,20 @@ h1, h2, h3, h4, h5, p, #download_widget { width: 100%; } + +.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; +} \ No newline at end of file diff --git a/locales/en-US.json b/locales/en-US.json index 4f2c2770..f5df304f 100644 --- a/locales/en-US.json +++ b/locales/en-US.json @@ -501,5 +501,7 @@ "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`", + "video_description_show_transcript_section_label": "Transcripts", + "video_description_show_transcript_section_button": "Show transcript" } diff --git a/src/invidious/views/watch.ecr b/src/invidious/views/watch.ecr index 6f9ced6f..61ce62cf 100644 --- a/src/invidious/views/watch.ecr +++ b/src/invidious/views/watch.ecr @@ -256,7 +256,14 @@ we're going to need to do it here in order to allow for translations.
<%= video.description_html %>
<% else %> -
<%= video.description_html %>
+
<%-= video.description_html %> + <% if captions %> +
+

<%=HTML.escape(translate(locale, "video_description_show_transcript_section_label"))%>

+ <%=HTML.escape(translate(locale, "video_description_show_transcript_section_button"))%> +
+ <% end %> +
From 786e40afb014d6cacee435e853c28ba971d958f5 Mon Sep 17 00:00:00 2001 From: syeopite Date: Wed, 12 Jun 2024 10:23:48 -0700 Subject: [PATCH 2/9] Add logic to show/hide transcripts without JS --- locales/en-US.json | 4 +++- src/invidious/routes/watch.cr | 12 ++++++++++++ src/invidious/views/watch.ecr | 9 ++++++++- 3 files changed, 23 insertions(+), 2 deletions(-) diff --git a/locales/en-US.json b/locales/en-US.json index f5df304f..5d078169 100644 --- a/locales/en-US.json +++ b/locales/en-US.json @@ -503,5 +503,7 @@ "carousel_skip": "Skip the Carousel", "carousel_go_to": "Go to slide `x`", "video_description_show_transcript_section_label": "Transcripts", - "video_description_show_transcript_section_button": "Show transcript" + "video_description_show_transcript_section_button": "Show transcript", + "video_description_show_transcript_section_button_hide": "Hide transcript", + "error_transcripts_none_available": "No transcripts are available" } diff --git a/src/invidious/routes/watch.cr b/src/invidious/routes/watch.cr index e777b3f1..482112ac 100644 --- a/src/invidious/routes/watch.cr +++ b/src/invidious/routes/watch.cr @@ -38,6 +38,11 @@ module Invidious::Routes::Watch nojs ||= "0" nojs = nojs == "1" + show_transcripts = env.params.query["show_transcripts"]? + + show_transcripts ||= "0" + show_transcripts = show_transcripts == "1" + preferences = env.get("preferences").as(Preferences) user = env.get?("user").try &.as(User) @@ -156,6 +161,13 @@ module Invidious::Routes::Watch } captions = captions - preferred_captions + if show_transcripts + # Placeholder + transcript = true + else + transcript = nil + end + aspect_ratio = "16:9" thumbnail = "/vi/#{video.id}/maxres.jpg" diff --git a/src/invidious/views/watch.ecr b/src/invidious/views/watch.ecr index 61ce62cf..0ca90a68 100644 --- a/src/invidious/views/watch.ecr +++ b/src/invidious/views/watch.ecr @@ -260,7 +260,14 @@ we're going to need to do it here in order to allow for translations. <% if captions %>

<%=HTML.escape(translate(locale, "video_description_show_transcript_section_label"))%>

- <%=HTML.escape(translate(locale, "video_description_show_transcript_section_button"))%> + <% if transcript %> + <% hide_transcripts_param = env.params.query.dup %> + <% hide_transcripts_param.delete_all("show_transcripts") %> + + <%=HTML.escape(translate(locale, "video_description_show_transcript_section_button_hide"))%> + <% else %> + <%=HTML.escape(translate(locale, "video_description_show_transcript_section_button"))%> + <% end %>
<% end %> From 01b21d9d525d8386ce5c92861a24aff26cc31316 Mon Sep 17 00:00:00 2001 From: syeopite Date: Wed, 12 Jun 2024 10:44:07 -0700 Subject: [PATCH 3/9] Add logic to request transcripts in watch handler --- src/invidious/routes/watch.cr | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/src/invidious/routes/watch.cr b/src/invidious/routes/watch.cr index 482112ac..57fb2029 100644 --- a/src/invidious/routes/watch.cr +++ b/src/invidious/routes/watch.cr @@ -162,8 +162,26 @@ module Invidious::Routes::Watch captions = captions - preferred_captions if show_transcripts - # Placeholder - transcript = true + # The transcripts available are the exact same as the amount of captions available. Thus: + if !preferred_captions.empty? + chosen_transcript = preferred_captions[0] + transcript_request_param = Invidious::Videos::Transcript.generate_param( + id, chosen_transcript.language_code, chosen_transcript.auto_generated + ) + elsif !captions.empty? + chosen_transcript = captions[0] + transcript_request_param = Invidious::Videos::Transcript.generate_param( + id, chosen_transcript.language_code, chosen_transcript.auto_generated + ) + else + return error_template(404, "error_transcripts_none_available") + end + + transcript = Invidious::Videos::Transcript.from_raw( + YoutubeAPI.get_transcript(transcript_request_param), + chosen_transcript.language_code, + chosen_transcript.auto_generated, + ) else transcript = nil end From 5ba6baea193226826496936089457db368891967 Mon Sep 17 00:00:00 2001 From: syeopite Date: Wed, 12 Jun 2024 12:42:18 -0700 Subject: [PATCH 4/9] Add UI for transcripts for JS-disabled users This commit adds a server-side UI template that renders a transcript element. This is used to render transcripts for users without JS. --- assets/css/default.css | 42 +++++++++++++++++++ locales/en-US.json | 3 +- .../views/components/no-js-transcript-ui.ecr | 23 ++++++++++ src/invidious/views/watch.ecr | 4 ++ 4 files changed, 71 insertions(+), 1 deletion(-) create mode 100644 src/invidious/views/components/no-js-transcript-ui.ecr diff --git a/assets/css/default.css b/assets/css/default.css index 3544282a..7db5a74a 100644 --- a/assets/css/default.css +++ b/assets/css/default.css @@ -832,4 +832,46 @@ h1, h2, h3, h4, h5, p, .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: #cacaca; } \ No newline at end of file diff --git a/locales/en-US.json b/locales/en-US.json index 5d078169..54ae6ab7 100644 --- a/locales/en-US.json +++ b/locales/en-US.json @@ -505,5 +505,6 @@ "video_description_show_transcript_section_label": "Transcripts", "video_description_show_transcript_section_button": "Show transcript", "video_description_show_transcript_section_button_hide": "Hide transcript", - "error_transcripts_none_available": "No transcripts are available" + "error_transcripts_none_available": "No transcripts are available", + "transcript_widget_title": "Transcript" } diff --git a/src/invidious/views/components/no-js-transcript-ui.ecr b/src/invidious/views/components/no-js-transcript-ui.ecr new file mode 100644 index 00000000..743ea570 --- /dev/null +++ b/src/invidious/views/components/no-js-transcript-ui.ecr @@ -0,0 +1,23 @@ +
+

<%=translate(locale, "transcript_widget_title")%>

+
+ <% 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 %> +

<%= line.line %>

+ <% else %> + <% jump_time = line.start_ms.total_seconds.to_s %> + <% transcript_time_url_param["t"] = jump_time %> + +
+

<%= recode_length_seconds(line.start_ms.total_seconds) %>

+

<%= line.line %>

+
+
+ <% end %> + <% end %> +
+
+
\ No newline at end of file diff --git a/src/invidious/views/watch.ecr b/src/invidious/views/watch.ecr index 0ca90a68..3e90c4f5 100644 --- a/src/invidious/views/watch.ecr +++ b/src/invidious/views/watch.ecr @@ -317,6 +317,10 @@ we're going to need to do it here in order to allow for translations. <% if params.related_videos || plid %>
+ <% if transcript %> + <%= rendered "components/no-js-transcript-ui" %> + <% end %> + <% if plid %>
<% end %> From b7810364047d341fab5e77a438081015a329bed8 Mon Sep 17 00:00:00 2001 From: syeopite Date: Thu, 13 Jun 2024 09:33:34 -0700 Subject: [PATCH 5/9] Add logic to swap languages in transcript widget --- assets/css/default.css | 14 ++++ locales/en-US.json | 3 +- src/invidious/routes/watch.cr | 64 ++++++++++++++----- .../views/components/no-js-transcript-ui.ecr | 25 +++++++- 4 files changed, 88 insertions(+), 18 deletions(-) diff --git a/assets/css/default.css b/assets/css/default.css index 7db5a74a..90a7cc2e 100644 --- a/assets/css/default.css +++ b/assets/css/default.css @@ -874,4 +874,18 @@ h1, h2, h3, h4, h5, p, .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%; } \ No newline at end of file diff --git a/locales/en-US.json b/locales/en-US.json index 54ae6ab7..424dc08b 100644 --- a/locales/en-US.json +++ b/locales/en-US.json @@ -506,5 +506,6 @@ "video_description_show_transcript_section_button": "Show transcript", "video_description_show_transcript_section_button_hide": "Hide transcript", "error_transcripts_none_available": "No transcripts are available", - "transcript_widget_title": "Transcript" + "transcript_widget_title": "Transcript", + "transcript_widget_no_js_change_transcript_btn": "Swap" } diff --git a/src/invidious/routes/watch.cr b/src/invidious/routes/watch.cr index 57fb2029..04e1df63 100644 --- a/src/invidious/routes/watch.cr +++ b/src/invidious/routes/watch.cr @@ -43,6 +43,9 @@ module Invidious::Routes::Watch 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) user = env.get?("user").try &.as(User) @@ -162,26 +165,55 @@ module Invidious::Routes::Watch captions = captions - preferred_captions if show_transcripts - # The transcripts available are the exact same as the amount of captions available. Thus: - if !preferred_captions.empty? - chosen_transcript = preferred_captions[0] - transcript_request_param = Invidious::Videos::Transcript.generate_param( - id, chosen_transcript.language_code, chosen_transcript.auto_generated - ) - elsif !captions.empty? - chosen_transcript = captions[0] - transcript_request_param = Invidious::Videos::Transcript.generate_param( - id, chosen_transcript.language_code, chosen_transcript.auto_generated - ) + # 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 - return error_template(404, "error_transcripts_none_available") + target_transcript = nil end - transcript = Invidious::Videos::Transcript.from_raw( - YoutubeAPI.get_transcript(transcript_request_param), - chosen_transcript.language_code, - chosen_transcript.auto_generated, + # 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 diff --git a/src/invidious/views/components/no-js-transcript-ui.ecr b/src/invidious/views/components/no-js-transcript-ui.ecr index 743ea570..1622a47c 100644 --- a/src/invidious/views/components/no-js-transcript-ui.ecr +++ b/src/invidious/views/components/no-js-transcript-ui.ecr @@ -19,5 +19,28 @@ <% end %> <% end %>
-
+
+ <% transcript_select_args = env.params.query.dup %> + <% transcript_select_args.delete_all("use_this_transcript") %> +
+ <% # Preserve query parameters %> + <% transcript_select_args.each do | k, v | %> + + <% end %> + + + + "/> +
+
\ No newline at end of file From f317e6620a0a3d763fb4b4e992e3b3eccd6358a7 Mon Sep 17 00:00:00 2001 From: syeopite Date: Sat, 15 Mar 2025 18:30:25 -0700 Subject: [PATCH 6/9] Fix extraction of transcript headings Innertube structure was changed --- src/invidious/videos/transcript.cr | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/invidious/videos/transcript.cr b/src/invidious/videos/transcript.cr index ee1272d1..c9becc34 100644 --- a/src/invidious/videos/transcript.cr +++ b/src/invidious/videos/transcript.cr @@ -80,14 +80,15 @@ module Invidious::Videos initial_segments.each do |line| if unpacked_line = line["transcriptSectionHeaderRenderer"]? line_type = HeadingLine + text = (unpacked_line.dig?("sectionHeader", "sectionHeaderViewModel", "headline", "content").try &.as_s) || "" else unpacked_line = line["transcriptSegmentRenderer"] + text = extract_text(unpacked_line["snippet"]) || "" line_type = RegularLine end start_ms = unpacked_line["startMs"].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) end From 759371770729f1277848548180f7a1b15ca44f4e Mon Sep 17 00:00:00 2001 From: syeopite Date: Sat, 15 Mar 2025 18:35:05 -0700 Subject: [PATCH 7/9] Use custom click handler for transcript lines --- assets/js/watch.js | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/assets/js/watch.js b/assets/js/watch.js index d869d40d..cbd2cab9 100644 --- a/assets/js/watch.js +++ b/assets/js/watch.js @@ -195,3 +195,15 @@ addEventListener('load', function (e) { comments.innerHTML = ''; } }); + +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')); + }) + } +}) From 6ecae19477e111c84426e7a8d6e3b004bb5835da Mon Sep 17 00:00:00 2001 From: syeopite Date: Sat, 10 May 2025 20:32:16 -0700 Subject: [PATCH 8/9] Fix missing transcripts btn when desc is expanded --- .../description_toggle_transcripts_widget.ecr | 13 +++++++++++++ src/invidious/views/watch.ecr | 18 ++++-------------- 2 files changed, 17 insertions(+), 14 deletions(-) create mode 100644 src/invidious/views/components/description_toggle_transcripts_widget.ecr diff --git a/src/invidious/views/components/description_toggle_transcripts_widget.ecr b/src/invidious/views/components/description_toggle_transcripts_widget.ecr new file mode 100644 index 00000000..0a80178a --- /dev/null +++ b/src/invidious/views/components/description_toggle_transcripts_widget.ecr @@ -0,0 +1,13 @@ +<% if captions %> +
+

<%=HTML.escape(translate(locale, "video_description_show_transcript_section_label"))%>

+ <% if transcript %> + <% hide_transcripts_param = env.params.query.dup %> + <% hide_transcripts_param.delete_all("show_transcripts") %> + + <%=HTML.escape(translate(locale, "video_description_show_transcript_section_button_hide"))%> + <% else %> + <%=HTML.escape(translate(locale, "video_description_show_transcript_section_button"))%> + <% end %> +
+<% end %> diff --git a/src/invidious/views/watch.ecr b/src/invidious/views/watch.ecr index 3e90c4f5..cd4526aa 100644 --- a/src/invidious/views/watch.ecr +++ b/src/invidious/views/watch.ecr @@ -253,23 +253,13 @@ we're going to need to do it here in order to allow for translations.
<% if video.description.size < 200 || params.extend_desc %> -
<%= video.description_html %>
+
<%= video.description_html %> + <%= rendered "components/description_toggle_transcripts_widget" %> +
<% else %>
<%-= video.description_html %> - <% if captions %> -
-

<%=HTML.escape(translate(locale, "video_description_show_transcript_section_label"))%>

- <% if transcript %> - <% hide_transcripts_param = env.params.query.dup %> - <% hide_transcripts_param.delete_all("show_transcripts") %> - - <%=HTML.escape(translate(locale, "video_description_show_transcript_section_button_hide"))%> - <% else %> - <%=HTML.escape(translate(locale, "video_description_show_transcript_section_button"))%> - <% end %> -
- <% end %> + <%= rendered "components/description_toggle_transcripts_widget" %>