From d16477602448f7f5ca0f04ffcebf3100575bf703 Mon Sep 17 00:00:00 2001 From: Chunky programmer <78101139+ChunkyProgrammer@users.noreply.github.com> Date: Tue, 6 Jun 2023 16:27:26 -0400 Subject: [PATCH 001/143] Playlists: Fix paging for Invidious playlists --- src/invidious/routes/playlists.cr | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/src/invidious/routes/playlists.cr b/src/invidious/routes/playlists.cr index 8675fa45..a65ff64c 100644 --- a/src/invidious/routes/playlists.cr +++ b/src/invidious/routes/playlists.cr @@ -410,8 +410,13 @@ module Invidious::Routes::Playlists return error_template(500, ex) end - page_count = (playlist.video_count / 200).to_i - page_count += 1 if (playlist.video_count % 200) > 0 + if playlist.is_a? InvidiousPlaylist + page_count = (playlist.video_count / 100).to_i + page_count += 1 if (playlist.video_count % 100) > 0 + else + page_count = (playlist.video_count / 200).to_i + page_count += 1 if (playlist.video_count % 200) > 0 + end if page > page_count return env.redirect "/playlist?list=#{plid}&page=#{page_count}" @@ -422,7 +427,11 @@ module Invidious::Routes::Playlists end begin - videos = get_playlist_videos(playlist, offset: (page - 1) * 200) + if playlist.is_a? InvidiousPlaylist + videos = get_playlist_videos(playlist, offset: (page - 1) * 100) + else + videos = get_playlist_videos(playlist, offset: (page - 1) * 200) + end rescue ex return error_template(500, "Error encountered while retrieving playlist videos.
#{ex.message}") end From a38edd733002166d334261abb39a220c8972ca25 Mon Sep 17 00:00:00 2001 From: Omer Naveed Date: Sat, 1 Jul 2023 12:29:02 -0500 Subject: [PATCH 002/143] Fix Nil assertion failed in RSS feeds --- src/invidious/routes/feeds.cr | 26 ++++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/src/invidious/routes/feeds.cr b/src/invidious/routes/feeds.cr index fc62c5a3..60f8db05 100644 --- a/src/invidious/routes/feeds.cr +++ b/src/invidious/routes/feeds.cr @@ -154,20 +154,26 @@ module Invidious::Routes::Feeds return error_atom(500, ex) end + namespaces = { + "yt" => "http://www.youtube.com/xml/schemas/2015", + "media" => "http://search.yahoo.com/mrss/", + "default" => "http://www.w3.org/2005/Atom", + } + response = YT_POOL.client &.get("/feeds/videos.xml?channel_id=#{channel.ucid}") - rss = XML.parse_html(response.body) + rss = XML.parse(response.body) - videos = rss.xpath_nodes("//feed/entry").map do |entry| - video_id = entry.xpath_node("videoid").not_nil!.content - title = entry.xpath_node("title").not_nil!.content + videos = rss.xpath_nodes("//default:feed/default:entry", namespaces).map do |entry| + video_id = entry.xpath_node("yt:videoId", namespaces).not_nil!.content + title = entry.xpath_node("default:title", namespaces).not_nil!.content - published = Time.parse_rfc3339(entry.xpath_node("published").not_nil!.content) - updated = Time.parse_rfc3339(entry.xpath_node("updated").not_nil!.content) + published = Time.parse_rfc3339(entry.xpath_node("default:published", namespaces).not_nil!.content) + updated = Time.parse_rfc3339(entry.xpath_node("default:updated", namespaces).not_nil!.content) - author = entry.xpath_node("author/name").not_nil!.content - ucid = entry.xpath_node("channelid").not_nil!.content - description_html = entry.xpath_node("group/description").not_nil!.to_s - views = entry.xpath_node("group/community/statistics").not_nil!.["views"].to_i64 + author = entry.xpath_node("default:author/default:name", namespaces).not_nil!.content + ucid = entry.xpath_node("yt:channelId", namespaces).not_nil!.content + description_html = entry.xpath_node("media:group/media:description", namespaces).not_nil!.to_s + views = entry.xpath_node("media:group/media:community/media:statistics", namespaces).not_nil!.["views"].to_i64 SearchVideo.new({ title: title, From 0ba22ef391a7b350d139dfd256aa20a7e1f812ed Mon Sep 17 00:00:00 2001 From: Samantaz Fox Date: Tue, 18 Apr 2023 00:04:49 +0200 Subject: [PATCH 003/143] I18n: Add a function to determine if a given locale is RTL --- src/invidious/helpers/i18n.cr | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/invidious/helpers/i18n.cr b/src/invidious/helpers/i18n.cr index a9ed1f64..76e477a4 100644 --- a/src/invidious/helpers/i18n.cr +++ b/src/invidious/helpers/i18n.cr @@ -165,3 +165,12 @@ def translate_bool(locale : String?, translation : Bool) return translate(locale, "No") end end + +def locale_is_rtl?(locale : String?) + # Fallback to en-US + return false if locale.nil? + + # Arabic, Persian, Hebrew + # See https://en.wikipedia.org/wiki/Right-to-left_script#List_of_RTL_scripts + return {"ar", "fa", "he"}.includes? locale +end From 462609d90d38ec8e9aada1d700cfbca46e906552 Mon Sep 17 00:00:00 2001 From: Samantaz Fox Date: Wed, 26 Apr 2023 22:30:13 +0200 Subject: [PATCH 004/143] Utils: Create a function to append parameters to a base URL --- src/invidious/http_server/utils.cr | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/invidious/http_server/utils.cr b/src/invidious/http_server/utils.cr index e3f1fa0f..222dfc4a 100644 --- a/src/invidious/http_server/utils.cr +++ b/src/invidious/http_server/utils.cr @@ -1,3 +1,5 @@ +require "uri" + module Invidious::HttpServer module Utils extend self @@ -16,5 +18,23 @@ module Invidious::HttpServer return "#{url.request_target}?#{params}" end end + + def add_params_to_url(url : String | URI, params : URI::Params) : URI + url = URI.parse(url) if url.is_a?(String) + + url_query = url.query || "" + + # Append the parameters + url.query = String.build do |str| + if !url_query.empty? + str << url_query + str << '&' + end + + str << params + end + + return url + end end end From c0887497447a24cad1f1e8b8268b8ccfbc78ae77 Mon Sep 17 00:00:00 2001 From: Samantaz Fox Date: Mon, 17 Apr 2023 21:25:00 +0200 Subject: [PATCH 005/143] HTML: Add code to generate page nav buttons --- src/invidious/frontend/pagination.cr | 97 ++++++++++++++++++++++++++++ 1 file changed, 97 insertions(+) create mode 100644 src/invidious/frontend/pagination.cr diff --git a/src/invidious/frontend/pagination.cr b/src/invidious/frontend/pagination.cr new file mode 100644 index 00000000..3f931f4e --- /dev/null +++ b/src/invidious/frontend/pagination.cr @@ -0,0 +1,97 @@ +require "uri" + +module Invidious::Frontend::Pagination + extend self + + private def previous_page(str : String::Builder, locale : String?, url : String) + # Link + str << %() + + if locale_is_rtl?(locale) + # Inverted arrow ("previous" points to the right) + str << translate(locale, "Previous page") + str << "  " + str << %() + else + # Regular arrow ("previous" points to the left) + str << %() + str << "  " + str << translate(locale, "Previous page") + end + + str << "" + end + + private def next_page(str : String::Builder, locale : String?, url : String) + # Link + str << %() + + if locale_is_rtl?(locale) + # Inverted arrow ("next" points to the left) + str << %() + str << "  " + str << translate(locale, "Next page") + else + # Regular arrow ("next" points to the right) + str << translate(locale, "Next page") + str << "  " + str << %() + end + + str << "" + end + + def nav_numeric(locale : String?, *, base_url : String | URI, current_page : Int, show_next : Bool = true) + return String.build do |str| + str << %(
\n) + str << %(\n) + str << %(
\n\n) + end + end + + def nav_ctoken(locale : String?, *, base_url : String | URI, ctoken : String?) + return String.build do |str| + str << %(
\n) + str << %(\n) + str << %(
\n\n) + end + end +end From 57c7b922f7c3cd04d08bb6be9793464d31213fb1 Mon Sep 17 00:00:00 2001 From: Samantaz Fox Date: Mon, 17 Apr 2023 21:26:04 +0200 Subject: [PATCH 006/143] HTML: Make a dedicated ECR component for items + pagination --- src/invidious/views/components/items_paginated.ecr | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 src/invidious/views/components/items_paginated.ecr diff --git a/src/invidious/views/components/items_paginated.ecr b/src/invidious/views/components/items_paginated.ecr new file mode 100644 index 00000000..c82b1772 --- /dev/null +++ b/src/invidious/views/components/items_paginated.ecr @@ -0,0 +1,11 @@ +<%= page_nav_html %> + +
+ <%- videos.each do |item| -%> + <%= rendered "components/item" %> + <%- end -%> +
+ +<%= page_nav_html %> + + From 77d401cec257b1f8b1b5c233134789441083fcdc Mon Sep 17 00:00:00 2001 From: Samantaz Fox Date: Thu, 20 Apr 2023 18:55:35 +0200 Subject: [PATCH 007/143] CSS: add styling for the new buttons --- assets/css/default.css | 54 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 53 insertions(+), 1 deletion(-) diff --git a/assets/css/default.css b/assets/css/default.css index 431a0427..eb90c09c 100644 --- a/assets/css/default.css +++ b/assets/css/default.css @@ -115,6 +115,11 @@ div { padding-right: 10px; } + +/* + * Buttons + */ + body a.pure-button { color: rgba(0,0,0,.8); } @@ -127,14 +132,36 @@ body a.pure-button-primary, color: rgba(35, 35, 35, 1); } +.pure-button-primary, +.pure-button-secondary { + border: 1px solid #a0a0a0; + border-radius: 3px; + margin: 0 .4em; +} + +.dark-theme .pure-button-secondary { + background-color: #0002; + color: #ddd; +} + button.pure-button-primary:hover, -body a.pure-button-primary:hover, button.pure-button-primary:focus, +body a.pure-button-primary:hover, body a.pure-button-primary:focus { background-color: rgba(0, 182, 240, 1); color: #fff; } +button.pure-button-secondary:hover, +button.pure-button-secondary:focus { + border-color: rgba(0, 182, 240, 1); +} + + +/* + * Thumbnails + */ + div.thumbnail { padding: 28.125%; position: relative; @@ -192,6 +219,7 @@ div.watched-indicator { top: -0.7em; } + /* * Navbar */ @@ -347,6 +375,22 @@ p.video-data { margin: 0; font-weight: bold; font-size: 80%; } border: none; } + +/* + * Page navigation + */ + +.page-nav-container { margin: 15px 0 30px 0; } + +.page-prev-container { text-align: start; } +.page-next-container { text-align: end; } + +.page-prev-container, +.page-next-container { + display: inline-block; +} + + /* * Footer */ @@ -389,6 +433,7 @@ span > select { word-wrap: normal; } + /* * Light theme */ @@ -453,6 +498,7 @@ span > select { } } + /* * Dark theme */ @@ -539,6 +585,12 @@ body.dark-theme { } } + +/* + * Miscellanous + */ + + /*With commit d9528f5 all contents of the page is now within a flexbox. However, the hr element is rendered improperly within one. See https://stackoverflow.com/a/34372979 for more info */ From c4ef3bed9556700c4c4e8c02c394d16fd3aae03d Mon Sep 17 00:00:00 2001 From: Samantaz Fox Date: Tue, 18 Apr 2023 00:04:01 +0200 Subject: [PATCH 008/143] HTML: Use the new pagination component for playlists --- src/invidious/routes/playlists.cr | 22 ++++++++++++++++ src/invidious/views/add_playlist_items.ecr | 30 +--------------------- src/invidious/views/edit_playlist.ecr | 25 +----------------- src/invidious/views/playlist.ecr | 25 +----------------- 4 files changed, 25 insertions(+), 77 deletions(-) diff --git a/src/invidious/routes/playlists.cr b/src/invidious/routes/playlists.cr index 1dd3f32e..604fe4e1 100644 --- a/src/invidious/routes/playlists.cr +++ b/src/invidious/routes/playlists.cr @@ -170,6 +170,13 @@ module Invidious::Routes::Playlists csrf_token = generate_response(sid, {":edit_playlist"}, HMAC_KEY) + # Pagination + page_nav_html = Frontend::Pagination.nav_numeric(locale, + base_url: "/playlist?list=#{playlist.id}", + current_page: page, + show_next: (videos.size == 100) + ) + templated "edit_playlist" end @@ -252,6 +259,14 @@ module Invidious::Routes::Playlists videos = [] of SearchVideo end + # Pagination + query_encoded = URI.encode_www_form(query.try &.text || "", space_to_plus: true) + page_nav_html = Frontend::Pagination.nav_numeric(locale, + base_url: "/add_playlist_items?list=#{playlist.id}&q=#{query_encoded}", + current_page: page, + show_next: (videos.size >= 20) + ) + env.set "add_playlist_items", plid templated "add_playlist_items" end @@ -427,6 +442,13 @@ module Invidious::Routes::Playlists env.set "remove_playlist_items", plid end + # Pagination + page_nav_html = Frontend::Pagination.nav_numeric(locale, + base_url: "/playlist?list=#{playlist.id}", + current_page: page, + show_next: (page_count != 1 && page < page_count) + ) + templated "playlist" end diff --git a/src/invidious/views/add_playlist_items.ecr b/src/invidious/views/add_playlist_items.ecr index bcba74cf..6aea82ae 100644 --- a/src/invidious/views/add_playlist_items.ecr +++ b/src/invidious/views/add_playlist_items.ecr @@ -31,33 +31,5 @@ -
- <% videos.each_slice(4) do |slice| %> - <% slice.each do |item| %> - <%= rendered "components/item" %> - <% end %> - <% end %> -
- - -<% if query %> - <%- query_encoded = URI.encode_www_form(query.text, space_to_plus: true) -%> -
-
- <% if query.page > 1 %> - - <%= translate(locale, "Previous page") %> - - <% end %> -
-
-
- <% if videos.size >= 20 %> - - <%= translate(locale, "Next page") %> - - <% end %> -
-
-<% end %> +<%= rendered "components/items_paginated" %> diff --git a/src/invidious/views/edit_playlist.ecr b/src/invidious/views/edit_playlist.ecr index 548104c8..d2981886 100644 --- a/src/invidious/views/edit_playlist.ecr +++ b/src/invidious/views/edit_playlist.ecr @@ -56,28 +56,5 @@
-
-<% videos.each do |item| %> - <%= rendered "components/item" %> -<% end %> -
- - -
-
- <% if page > 1 %> - - <%= translate(locale, "Previous page") %> - - <% end %> -
-
-
- <% if videos.size == 100 %> - - <%= translate(locale, "Next page") %> - - <% end %> -
-
+<%= rendered "components/items_paginated" %> diff --git a/src/invidious/views/playlist.ecr b/src/invidious/views/playlist.ecr index a04acf4c..08995a83 100644 --- a/src/invidious/views/playlist.ecr +++ b/src/invidious/views/playlist.ecr @@ -100,28 +100,5 @@ <% end %> -
-<% videos.each do |item| %> - <%= rendered "components/item" %> -<% end %> -
- - -
-
- <% if page > 1 %> - - <%= translate(locale, "Previous page") %> - - <% end %> -
-
-
- <% if page_count != 1 && page < page_count %> - - <%= translate(locale, "Next page") %> - - <% end %> -
-
+<%= rendered "components/items_paginated" %> From efaf7cb09c8aad606d59cacab71c4a0a269d785b Mon Sep 17 00:00:00 2001 From: Samantaz Fox Date: Tue, 18 Apr 2023 00:12:56 +0200 Subject: [PATCH 009/143] HTML: Use the new pagination component for search results --- src/invidious/routes/search.cr | 22 +++++++++++++-------- src/invidious/views/hashtag.ecr | 35 +-------------------------------- src/invidious/views/search.ecr | 35 +-------------------------------- 3 files changed, 16 insertions(+), 76 deletions(-) diff --git a/src/invidious/routes/search.cr b/src/invidious/routes/search.cr index 6c3088de..edf0351c 100644 --- a/src/invidious/routes/search.cr +++ b/src/invidious/routes/search.cr @@ -59,17 +59,21 @@ module Invidious::Routes::Search return error_template(500, ex) end - params = query.to_http_params - url_prev_page = "/search?#{params}&page=#{query.page - 1}" - url_next_page = "/search?#{params}&page=#{query.page + 1}" - redirect_url = Invidious::Frontend::Misc.redirect_url(env) + # Pagination + page_nav_html = Frontend::Pagination.nav_numeric(locale, + base_url: "/search?#{query.to_http_params}", + current_page: query.page, + show_next: (videos.size >= 20) + ) + if query.type == Invidious::Search::Query::Type::Channel env.set "search", "channel:#{query.channel} #{query.text}" else env.set "search", query.text end + templated "search" end end @@ -96,11 +100,13 @@ module Invidious::Routes::Search return error_template(500, ex) end - params = env.params.query.empty? ? "" : "&#{env.params.query}" - + # Pagination hashtag_encoded = URI.encode_www_form(hashtag, space_to_plus: false) - url_prev_page = "/hashtag/#{hashtag_encoded}?page=#{page - 1}#{params}" - url_next_page = "/hashtag/#{hashtag_encoded}?page=#{page + 1}#{params}" + page_nav_html = Frontend::Pagination.nav_numeric(locale, + base_url: "/hashtag/#{hashtag_encoded}", + current_page: page, + show_next: (videos.size >= 60) + ) templated "hashtag" end diff --git a/src/invidious/views/hashtag.ecr b/src/invidious/views/hashtag.ecr index 3351c21c..2000337e 100644 --- a/src/invidious/views/hashtag.ecr +++ b/src/invidious/views/hashtag.ecr @@ -4,38 +4,5 @@
-
-
- <%- if page > 1 -%> - <%= translate(locale, "Previous page") %> - <%- end -%> -
-
-
- <%- if videos.size >= 60 -%> - <%= translate(locale, "Next page") %> - <%- end -%> -
-
-
- <%- videos.each do |item| -%> - <%= rendered "components/item" %> - <%- end -%> -
- - - -
-
- <%- if page > 1 -%> - <%= translate(locale, "Previous page") %> - <%- end -%> -
-
-
- <%- if videos.size >= 60 -%> - <%= translate(locale, "Next page") %> - <%- end -%> -
-
+<%= rendered "components/items_paginated" %> diff --git a/src/invidious/views/search.ecr b/src/invidious/views/search.ecr index a7469e36..627a13b0 100644 --- a/src/invidious/views/search.ecr +++ b/src/invidious/views/search.ecr @@ -7,19 +7,6 @@ <%= Invidious::Frontend::SearchFilters.generate(query.filters, query.text, query.page, locale) %>
-
-
- <%- if query.page > 1 -%> - <%= translate(locale, "Previous page") %> - <%- end -%> -
-
-
- <%- if videos.size >= 20 -%> - <%= translate(locale, "Next page") %> - <%- end -%> -
-
<%- if videos.empty? -%>
@@ -30,25 +17,5 @@
<%- else -%> -
- <%- videos.each do |item| -%> - <%= rendered "components/item" %> - <%- end -%> -
+ <%= rendered "components/items_paginated" %> <%- end -%> - - - -
-
- <%- if query.page > 1 -%> - <%= translate(locale, "Previous page") %> - <%- end -%> -
-
-
- <%- if videos.size >= 20 -%> - <%= translate(locale, "Next page") %> - <%- end -%> -
-
From 7bd6d0ac4961e7f2433eb3268a45b78642229896 Mon Sep 17 00:00:00 2001 From: Samantaz Fox Date: Fri, 21 Apr 2023 00:28:11 +0200 Subject: [PATCH 010/143] HTML: Use the new pagination component for channel pages --- src/invidious/routes/playlists.cr | 14 +++++------ src/invidious/routes/search.cr | 8 +++--- src/invidious/views/channel.ecr | 25 ++++++------------- .../views/components/items_paginated.ecr | 2 +- src/invidious/views/search.ecr | 2 +- 5 files changed, 20 insertions(+), 31 deletions(-) diff --git a/src/invidious/routes/playlists.cr b/src/invidious/routes/playlists.cr index 604fe4e1..5cb96809 100644 --- a/src/invidious/routes/playlists.cr +++ b/src/invidious/routes/playlists.cr @@ -163,9 +163,9 @@ module Invidious::Routes::Playlists end begin - videos = get_playlist_videos(playlist, offset: (page - 1) * 100) + items = get_playlist_videos(playlist, offset: (page - 1) * 100) rescue ex - videos = [] of PlaylistVideo + items = [] of PlaylistVideo end csrf_token = generate_response(sid, {":edit_playlist"}, HMAC_KEY) @@ -174,7 +174,7 @@ module Invidious::Routes::Playlists page_nav_html = Frontend::Pagination.nav_numeric(locale, base_url: "/playlist?list=#{playlist.id}", current_page: page, - show_next: (videos.size == 100) + show_next: (items.size == 100) ) templated "edit_playlist" @@ -254,9 +254,9 @@ module Invidious::Routes::Playlists begin query = Invidious::Search::Query.new(env.params.query, :playlist, region) - videos = query.process.select(SearchVideo).map(&.as(SearchVideo)) + items = query.process.select(SearchVideo).map(&.as(SearchVideo)) rescue ex - videos = [] of SearchVideo + items = [] of SearchVideo end # Pagination @@ -264,7 +264,7 @@ module Invidious::Routes::Playlists page_nav_html = Frontend::Pagination.nav_numeric(locale, base_url: "/add_playlist_items?list=#{playlist.id}&q=#{query_encoded}", current_page: page, - show_next: (videos.size >= 20) + show_next: (items.size >= 20) ) env.set "add_playlist_items", plid @@ -433,7 +433,7 @@ module Invidious::Routes::Playlists end begin - videos = get_playlist_videos(playlist, offset: (page - 1) * 200) + items = get_playlist_videos(playlist, offset: (page - 1) * 200) rescue ex return error_template(500, "Error encountered while retrieving playlist videos.
#{ex.message}") end diff --git a/src/invidious/routes/search.cr b/src/invidious/routes/search.cr index edf0351c..5be33533 100644 --- a/src/invidious/routes/search.cr +++ b/src/invidious/routes/search.cr @@ -52,7 +52,7 @@ module Invidious::Routes::Search user = env.get? "user" begin - videos = query.process + items = query.process rescue ex : ChannelSearchException return error_template(404, "Unable to find channel with id of '#{HTML.escape(ex.channel)}'. Are you sure that's an actual channel id? It should look like 'UC4QobU6STFB0P71PMvOGN5A'.") rescue ex @@ -65,7 +65,7 @@ module Invidious::Routes::Search page_nav_html = Frontend::Pagination.nav_numeric(locale, base_url: "/search?#{query.to_http_params}", current_page: query.page, - show_next: (videos.size >= 20) + show_next: (items.size >= 20) ) if query.type == Invidious::Search::Query::Type::Channel @@ -95,7 +95,7 @@ module Invidious::Routes::Search end begin - videos = Invidious::Hashtag.fetch(hashtag, page) + items = Invidious::Hashtag.fetch(hashtag, page) rescue ex return error_template(500, ex) end @@ -105,7 +105,7 @@ module Invidious::Routes::Search page_nav_html = Frontend::Pagination.nav_numeric(locale, base_url: "/hashtag/#{hashtag_encoded}", current_page: page, - show_next: (videos.size >= 60) + show_next: (items.size >= 60) ) templated "hashtag" diff --git a/src/invidious/views/channel.ecr b/src/invidious/views/channel.ecr index 6e62a471..91fe40b9 100644 --- a/src/invidious/views/channel.ecr +++ b/src/invidious/views/channel.ecr @@ -15,7 +15,12 @@ youtube_url = "https://www.youtube.com#{relative_url}" redirect_url = Invidious::Frontend::Misc.redirect_url(env) --%> + + page_nav_html = IV::Frontend::Pagination.nav_ctoken(locale, + base_url: relative_url, + ctoken: next_continuation + ) +%> <% content_for "header" do %> <%- if selected_tab.videos? -%> @@ -43,21 +48,5 @@
-
-<% items.each do |item| %> - <%= rendered "components/item" %> -<% end %> -
- - -
-
-
- <% if next_continuation %> - - <%= translate(locale, "Next page") %> - - <% end %> -
-
+<%= rendered "components/items_paginated" %> diff --git a/src/invidious/views/components/items_paginated.ecr b/src/invidious/views/components/items_paginated.ecr index c82b1772..4534a0a3 100644 --- a/src/invidious/views/components/items_paginated.ecr +++ b/src/invidious/views/components/items_paginated.ecr @@ -1,7 +1,7 @@ <%= page_nav_html %>
- <%- videos.each do |item| -%> + <%- items.each do |item| -%> <%= rendered "components/item" %> <%- end -%>
diff --git a/src/invidious/views/search.ecr b/src/invidious/views/search.ecr index 627a13b0..b1300214 100644 --- a/src/invidious/views/search.ecr +++ b/src/invidious/views/search.ecr @@ -8,7 +8,7 @@
-<%- if videos.empty? -%> +<%- if items.empty? -%>
<%= translate(locale, "search_message_no_results") %>

From b6bbfb9b200fc920854ce91835026da0fd6552db Mon Sep 17 00:00:00 2001 From: Samantaz Fox Date: Sat, 22 Apr 2023 12:58:46 +0200 Subject: [PATCH 011/143] HTML: Use new buttons for thumbnail overlays In addition, this commit also heavily changes the structure of the generic "video card" item. Main benefits: * Improved accessibility for keyboard users * Many styling glitches were fixed * PlaylistVideos now use the same items as the rest * Elements all have distinct CSS classes * Design can be expanded to add more icons --- assets/css/default.css | 51 ++++---- src/invidious/views/components/item.ecr | 157 ++++++++++-------------- src/invidious/views/feeds/history.ecr | 8 +- 3 files changed, 98 insertions(+), 118 deletions(-) diff --git a/assets/css/default.css b/assets/css/default.css index eb90c09c..48cb4264 100644 --- a/assets/css/default.css +++ b/assets/css/default.css @@ -152,9 +152,15 @@ body a.pure-button-primary:focus { color: #fff; } -button.pure-button-secondary:hover, -button.pure-button-secondary:focus { - border-color: rgba(0, 182, 240, 1); +.pure-button-secondary:hover, +.pure-button-secondary:focus { + color: rgb(0, 182, 240); + border-color: rgb(0, 182, 240); +} + +.pure-button-secondary.low-profile { + padding: 5px 10px; + margin: 0; } @@ -163,21 +169,19 @@ button.pure-button-secondary:focus { */ div.thumbnail { - padding: 28.125%; position: relative; + width: 100%; box-sizing: border-box; } img.thumbnail { - position: absolute; + display: block; /* See: https://stackoverflow.com/a/11635197 */ width: 100%; - height: 100%; - left: 0; - top: 0; object-fit: cover; } div.watched-overlay { + z-index: 50; position: absolute; top: 0; left: 0; @@ -195,28 +199,27 @@ div.watched-indicator { background-color: red; } -.length { +div.thumbnail > .top-left-overlay, +div.thumbnail > .bottom-right-overlay { z-index: 100; position: absolute; - background-color: rgba(35, 35, 35, 0.75); - color: #fff; - border-radius: 2px; - padding: 2px; + padding: 0; + margin: 0; font-size: 16px; - right: 0.25em; - bottom: -0.75em; } -.watched { - z-index: 100; - position: absolute; - background-color: rgba(35, 35, 35, 0.75); +.top-left-overlay { top: 0.6em; left: 0.6em; } +.bottom-right-overlay { bottom: 0.6em; right: 0.6em; } + +.length { + padding: 1px; + margin: -2px 0; color: #fff; - border-radius: 2px; - padding: 4px 8px 4px 8px; - font-size: 16px; - left: 0.2em; - top: -0.7em; + border-radius: 3px; +} + +.length, .top-left-overlay button { + background-color: rgba(35, 35, 35, 0.85); } diff --git a/src/invidious/views/components/item.ecr b/src/invidious/views/components/item.ecr index 7cfd38db..f05e1338 100644 --- a/src/invidious/views/components/item.ecr +++ b/src/invidious/views/components/item.ecr @@ -7,7 +7,7 @@ <% if !env.get("preferences").as(Preferences).thin_mode %>
- " alt="" /> + " alt="" />
<% end %>

<%= HTML.escape(item.author) %><% if !item.author_verified.nil? && item.author_verified %> <% end %>

@@ -25,7 +25,7 @@
<% if !env.get("preferences").as(Preferences).thin_mode %>
- " alt="" /> + " alt="" />

<%= translate_count(locale, "generic_videos_count", item.video_count, NumberFormatting::Separator) %>

<% end %> @@ -38,7 +38,7 @@
<% if !env.get("preferences").as(Preferences).thin_mode %>
- + <% if item.length_seconds != 0 %>

<%= recode_length_seconds(item.length_seconds) %>

<% end %> @@ -54,104 +54,79 @@

<%= HTML.escape(item.author) %>

- <% when PlaylistVideo %> - - <% if !env.get("preferences").as(Preferences).thin_mode %> -
- - - <% if plid_form = env.get?("remove_playlist_items") %> -
" method="post"> - "> -

- -

-
- <% end %> - - <% if item.responds_to?(:live_now) && item.live_now %> -

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

- <% elsif item.length_seconds != 0 %> -

<%= recode_length_seconds(item.length_seconds) %>

- <% end %> - - <% if item_watched %> -
-
- <% end %> -
- <% end %> -

<%= HTML.escape(item.title) %>

-
- -
- - <% endpoint_params = "?v=#{item.id}&list=#{item.plid}" %> - <%= rendered "components/video-context-buttons" %> -
- -
-
- <% if item.responds_to?(:premiere_timestamp) && item.premiere_timestamp.try &.> Time.utc %> -

<%= translate(locale, "Premieres in `x`", recode_date((item.premiere_timestamp.as(Time) - Time.utc).ago, locale)) %>

- <% elsif Time.utc - item.published > 1.minute %> -

<%= translate(locale, "Shared `x` ago", recode_date(item.published, locale)) %>

- <% end %> -
- - <% if item.responds_to?(:views) && item.views %> -
-

<%= translate_count(locale, "generic_views_count", item.views || 0, NumberFormatting::Short) %>

-
- <% end %> -
<% when Category %> <% else %> - - <% if !env.get("preferences").as(Preferences).thin_mode %> -
- - <% if env.get? "show_watched" %> -
" method="post"> - "> -

- -

-
- <% elsif plid_form = env.get? "add_playlist_items" %> -
" method="post"> - "> -

- -

-
- <% end %> + <%- + # `endpoint_params` is used for the "video-context-buttons" component + if item.is_a?(PlaylistVideo) + link_url = "/watch?v=#{item.id}&list=#{item.plid}&index=#{item.index}" + endpoint_params = "?v=#{item.id}&list=#{item.plid}" + else + link_url = "/watch?v=#{item.id}" + endpoint_params = "?v=#{item.id}" + end + -%> - <% if item.responds_to?(:live_now) && item.live_now %> -

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

- <% elsif item.length_seconds != 0 %> -

<%= recode_length_seconds(item.length_seconds) %>

- <% end %> +
+ <%- if !env.get("preferences").as(Preferences).thin_mode -%> + + + + <%- end -%> - <% if item_watched %> -
-
- <% end %> -
+
+ <%- if env.get? "show_watched" -%> +
" method="post"> + "> + +
+ <%- end -%> + + <%- if plid_form = env.get?("add_playlist_items") -%> + <%- form_parameters = "action_add_video=1&video_id=#{item.id}&playlist_id=#{plid_form}&referer=#{env.get("current_page")}" -%> +
+ "> + +
+ <%- elsif item.is_a?(PlaylistVideo) && (plid_form = env.get?("remove_playlist_items")) -%> + <%- form_parameters = "action_remove_video=1&set_video_id=#{item.index}&playlist_id=#{plid_form}&referer=#{env.get("current_page")}" -%> +
+ "> + +
+ <%- end -%> +
+ +
+ <%- if item.responds_to?(:live_now) && item.live_now -%> +

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

+ <%- elsif item.length_seconds != 0 -%> +

<%= recode_length_seconds(item.length_seconds) %>

+ <%- end -%> +
+ + <% if item_watched %> +
+
<% end %> -

<%= HTML.escape(item.title) %>

- +
+ + diff --git a/src/invidious/views/feeds/history.ecr b/src/invidious/views/feeds/history.ecr index 2234b297..5301a232 100644 --- a/src/invidious/views/feeds/history.ecr +++ b/src/invidious/views/feeds/history.ecr @@ -35,12 +35,14 @@ <% if !env.get("preferences").as(Preferences).thin_mode %>
+ +
" method="post"> "> -

- -

+
+

<% end %> From 080c7446c6c26c5d8670107cf4161ba4609e5e4a Mon Sep 17 00:00:00 2001 From: Samantaz Fox Date: Sat, 22 Apr 2023 17:50:34 +0200 Subject: [PATCH 012/143] HTML: Use new buttons for playlists (save/delete/add videos/etc...) --- assets/css/default.css | 2 +- locales/en-US.json | 6 +++ locales/fr.json | 6 +++ src/invidious/views/components/item.ecr | 10 ++-- src/invidious/views/edit_playlist.ecr | 64 +++++++++++----------- src/invidious/views/playlist.ecr | 70 ++++++++++++++++--------- 6 files changed, 94 insertions(+), 64 deletions(-) diff --git a/assets/css/default.css b/assets/css/default.css index 48cb4264..7a99a0db 100644 --- a/assets/css/default.css +++ b/assets/css/default.css @@ -219,7 +219,7 @@ div.thumbnail > .bottom-right-overlay { } .length, .top-left-overlay button { - background-color: rgba(35, 35, 35, 0.85); + background-color: rgba(35, 35, 35, 0.85) !important; } diff --git a/locales/en-US.json b/locales/en-US.json index e13ba968..c41a631a 100644 --- a/locales/en-US.json +++ b/locales/en-US.json @@ -9,6 +9,11 @@ "generic_subscribers_count_plural": "{{count}} subscribers", "generic_subscriptions_count": "{{count}} subscription", "generic_subscriptions_count_plural": "{{count}} subscriptions", + "generic_button_delete": "Delete", + "generic_button_edit": "Edit", + "generic_button_save": "Save", + "generic_button_cancel": "Cancel", + "generic_button_rss": "RSS", "LIVE": "LIVE", "Shared `x` ago": "Shared `x` ago", "Unsubscribe": "Unsubscribe", @@ -170,6 +175,7 @@ "Title": "Title", "Playlist privacy": "Playlist privacy", "Editing playlist `x`": "Editing playlist `x`", + "playlist_button_add_items": "Add videos", "Show more": "Show more", "Show less": "Show less", "Watch on YouTube": "Watch on YouTube", diff --git a/locales/fr.json b/locales/fr.json index d2607a49..2eb4dd2b 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -9,6 +9,11 @@ "generic_subscribers_count_plural": "{{count}} abonnés", "generic_subscriptions_count": "{{count}} abonnement", "generic_subscriptions_count_plural": "{{count}} abonnements", + "generic_button_delete": "Supprimer", + "generic_button_edit": "Editer", + "generic_button_save": "Enregistrer", + "generic_button_cancel": "Annuler", + "generic_button_rss": "RSS", "LIVE": "EN DIRECT", "Shared `x` ago": "Ajoutée il y a `x`", "Unsubscribe": "Se désabonner", @@ -149,6 +154,7 @@ "Title": "Titre", "Playlist privacy": "Paramètres de confidentialité de la liste de lecture", "Editing playlist `x`": "Modifier la liste de lecture `x`", + "playlist_button_add_items": "Ajouter des vidéos", "Show more": "Afficher plus", "Show less": "Afficher moins", "Watch on YouTube": "Voir la vidéo sur Youtube", diff --git a/src/invidious/views/components/item.ecr b/src/invidious/views/components/item.ecr index f05e1338..decdcb2f 100644 --- a/src/invidious/views/components/item.ecr +++ b/src/invidious/views/components/item.ecr @@ -71,6 +71,11 @@ <%- if !env.get("preferences").as(Preferences).thin_mode -%> + + <% if item_watched %> +
+
+ <% end %>
<%- end -%> @@ -109,11 +114,6 @@

<%= recode_length_seconds(item.length_seconds) %>

<%- end -%>
- - <% if item_watched %> -
-
- <% end %>
diff --git a/src/invidious/views/edit_playlist.ecr b/src/invidious/views/edit_playlist.ecr index d2981886..34157c67 100644 --- a/src/invidious/views/edit_playlist.ecr +++ b/src/invidious/views/edit_playlist.ecr @@ -6,35 +6,43 @@ <% end %>
-
-
+
+ +
+ +
+

+
+
+ +
+
<%= HTML.escape(playlist.author) %> | <%= translate_count(locale, "generic_videos_count", playlist.video_count) %> | - <%= translate(locale, "Updated `x` ago", recode_date(playlist.updated, locale)) %> | - "> - -
-
-

-
- -
-
-
-

+
@@ -44,14 +52,6 @@ -<% if playlist.is_a?(InvidiousPlaylist) && playlist.author == user.try &.email %> -
-

- -

-
-<% end %> -

diff --git a/src/invidious/views/playlist.ecr b/src/invidious/views/playlist.ecr index 08995a83..8d4d116d 100644 --- a/src/invidious/views/playlist.ecr +++ b/src/invidious/views/playlist.ecr @@ -7,8 +7,51 @@ <% end %>
-
+

<%= title %>

+
+ +
+ <%- if playlist.is_a?(InvidiousPlaylist) && playlist.author == user.try &.email -%> + + + + <%- else -%> +
+ <%- if IV::Database::Playlists.exists?(playlist.id) -%> + +  <%= translate(locale, "Subscribe") %> + + <%- else -%> + +  <%= translate(locale, "Unsubscribe") %> + + <%- end -%> +
+ <%- end -%> + + +
+
+ +
+
<% if playlist.is_a? InvidiousPlaylist %> <% if playlist.author == user.try &.email %> @@ -54,37 +97,12 @@
<% end %>
-
-

-
- <% if playlist.is_a?(InvidiousPlaylist) && playlist.author == user.try &.email %> -
-
- <% else %> - <% if Invidious::Database::Playlists.exists?(playlist.id) %> -
- <% else %> -
- <% end %> - <% end %> -
-
-

-
<%= playlist.description_html %>
-<% if playlist.is_a?(InvidiousPlaylist) && playlist.author == user.try &.email %> -
-

- -

-
-<% end %> -

From 43dcab225caca7034346a79da340e434cdb4d407 Mon Sep 17 00:00:00 2001 From: Samantaz Fox Date: Sat, 22 Apr 2023 19:45:45 +0200 Subject: [PATCH 013/143] HTML: merge MixVideo with other types in item.ecr --- src/invidious/views/components/item.ecr | 25 ++++--------------------- 1 file changed, 4 insertions(+), 21 deletions(-) diff --git a/src/invidious/views/components/item.ecr b/src/invidious/views/components/item.ecr index decdcb2f..0fa9c807 100644 --- a/src/invidious/views/components/item.ecr +++ b/src/invidious/views/components/item.ecr @@ -34,26 +34,6 @@

<%= HTML.escape(item.author) %><% if !item.is_a?(InvidiousPlaylist) && !item.author_verified.nil? && item.author_verified %> <% end %>

- <% when MixVideo %> - - <% if !env.get("preferences").as(Preferences).thin_mode %> -
- - <% if item.length_seconds != 0 %> -

<%= recode_length_seconds(item.length_seconds) %>

- <% end %> - - <% if item_watched %> -
-
- <% end %> -
- <% end %> -

<%= HTML.escape(item.title) %>

-
- -

<%= HTML.escape(item.author) %>

-
<% when Category %> <% else %> <%- @@ -61,6 +41,9 @@ if item.is_a?(PlaylistVideo) link_url = "/watch?v=#{item.id}&list=#{item.plid}&index=#{item.index}" endpoint_params = "?v=#{item.id}&list=#{item.plid}" + elsif item.is_a?(MixVideo) + link_url = "/watch?v=#{item.id}&list=#{item.rdid}" + endpoint_params = "?v=#{item.id}&list=#{item.rdid}" else link_url = "/watch?v=#{item.id}" endpoint_params = "?v=#{item.id}" @@ -134,7 +117,7 @@
<% if item.responds_to?(:premiere_timestamp) && item.premiere_timestamp.try &.> Time.utc %>

<%= translate(locale, "Premieres in `x`", recode_date((item.premiere_timestamp.as(Time) - Time.utc).ago, locale)) %>

- <% elsif Time.utc - item.published > 1.minute %> + <% elsif item.responds_to?(:published) && (Time.utc - item.published) > 1.minute %>

<%= translate(locale, "Shared `x` ago", recode_date(item.published, locale)) %>

<% end %>
From 8718f2068859b12174cecf4af11c30bfe64103a6 Mon Sep 17 00:00:00 2001 From: Samantaz Fox Date: Sat, 22 Apr 2023 19:59:01 +0200 Subject: [PATCH 014/143] HTML: Fix thin mode/thumbnail on other items --- src/invidious/views/components/item.ecr | 71 +++++++++++++++++-------- src/invidious/views/feeds/history.ecr | 28 +++++----- 2 files changed, 61 insertions(+), 38 deletions(-) diff --git a/src/invidious/views/components/item.ecr b/src/invidious/views/components/item.ecr index 0fa9c807..9b73f7ee 100644 --- a/src/invidious/views/components/item.ecr +++ b/src/invidious/views/components/item.ecr @@ -1,39 +1,64 @@ -<% item_watched = !item.is_a?(SearchChannel | SearchPlaylist | InvidiousPlaylist | Category) && env.get?("user").try &.as(User).watched.index(item.id) != nil %> +<%- + thin_mode = env.get("preferences").as(Preferences).thin_mode + item_watched = !item.is_a?(SearchChannel | SearchPlaylist | InvidiousPlaylist | Category) && env.get?("user").try &.as(User).watched.index(item.id) != nil + author_verified = item.responds_to?(:author_verified) && item.author_verified +-%>
<% case item when %> <% when SearchChannel %> - - <% if !env.get("preferences").as(Preferences).thin_mode %> + <% if !thin_mode %> +
" alt="" />
- <% end %> -

<%= HTML.escape(item.author) %><% if !item.author_verified.nil? && item.author_verified %> <% end %>

-
+ + <% end %> + + +

<%= translate_count(locale, "generic_subscribers_count", item.subscriber_count, NumberFormatting::Separator) %>

<% if !item.auto_generated %>

<%= translate_count(locale, "generic_videos_count", item.video_count, NumberFormatting::Separator) %>

<% end %>
<%= item.description_html %>
<% when SearchPlaylist, InvidiousPlaylist %> - <% if item.id.starts_with? "RD" %> - <% url = "/mix?list=#{item.id}&continuation=#{URI.parse(item.thumbnail || "/vi/-----------").request_target.split("/")[2]}" %> - <% else %> - <% url = "/playlist?list=#{item.id}" %> - <% end %> + <%- + if item.id.starts_with? "RD" + link_url = "/mix?list=#{item.id}&continuation=#{URI.parse(item.thumbnail || "/vi/-----------").request_target.split("/")[2]}" + else + link_url = "/playlist?list=#{item.id}" + end + -%> - - <% if !env.get("preferences").as(Preferences).thin_mode %> - + + + + <% when Category %> <% else %> <%- @@ -106,7 +131,7 @@
diff --git a/src/invidious/views/feeds/history.ecr b/src/invidious/views/feeds/history.ecr index 5301a232..83ea7238 100644 --- a/src/invidious/views/feeds/history.ecr +++ b/src/invidious/views/feeds/history.ecr @@ -31,22 +31,20 @@ <% watched.each do |item| %>
- - <% if !env.get("preferences").as(Preferences).thin_mode %> -
- +
+ + + -
-
" method="post"> - "> - -
-
-
-

- <% end %> - +
+
" method="post"> + "> + +
+
+
+

<% end %> From cc30b00f8ca00572348c1ee266df907c69726c13 Mon Sep 17 00:00:00 2001 From: Samantaz Fox Date: Sat, 22 Apr 2023 20:25:26 +0200 Subject: [PATCH 015/143] CSS: fix light/dark themes for pure buttons --- assets/css/default.css | 74 +++++++++++++++++++++++++++++------------- 1 file changed, 51 insertions(+), 23 deletions(-) diff --git a/assets/css/default.css b/assets/css/default.css index 7a99a0db..f671c3bf 100644 --- a/assets/css/default.css +++ b/assets/css/default.css @@ -139,25 +139,6 @@ body a.pure-button-primary, margin: 0 .4em; } -.dark-theme .pure-button-secondary { - background-color: #0002; - color: #ddd; -} - -button.pure-button-primary:hover, -button.pure-button-primary:focus, -body a.pure-button-primary:hover, -body a.pure-button-primary:focus { - background-color: rgba(0, 182, 240, 1); - color: #fff; -} - -.pure-button-secondary:hover, -.pure-button-secondary:focus { - color: rgb(0, 182, 240); - border-color: rgb(0, 182, 240); -} - .pure-button-secondary.low-profile { padding: 5px 10px; margin: 0; @@ -219,6 +200,7 @@ div.thumbnail > .bottom-right-overlay { } .length, .top-left-overlay button { + color: #eee; background-color: rgba(35, 35, 35, 0.85) !important; } @@ -449,9 +431,18 @@ span > select { color: #075A9E !important; } -.light-theme a.pure-button-primary:hover, -.light-theme a.pure-button-primary:focus { +.light-theme .pure-button-primary:hover, +.light-theme .pure-button-primary:focus, +.light-theme .pure-button-secondary:hover, +.light-theme .pure-button-secondary:focus { color: #fff !important; + border-color: rgba(0, 182, 240, 0.75) !important; + background-color: rgba(0, 182, 240, 0.75) !important; +} + +.light-theme .pure-button-secondary:not(.low-profile) { + color: #335d7a; + background-color: #fff2; } .light-theme a { @@ -479,9 +470,18 @@ span > select { color: #075A9E !important; } - .no-theme a.pure-button-primary:hover, - .no-theme a.pure-button-primary:focus { + .no-theme .pure-button-primary:hover, + .no-theme .pure-button-primary:focus, + .no-theme .pure-button-secondary:hover, + .no-theme .pure-button-secondary:focus { color: #fff !important; + border-color: rgba(0, 182, 240, 0.75) !important; + background-color: rgba(0, 182, 240, 0.75) !important; + } + + .no-theme .pure-button-secondary:not(.low-profile) { + color: #335d7a; + background-color: #fff2; } .no-theme a { @@ -514,6 +514,20 @@ span > select { color: rgb(0, 182, 240); } +.dark-theme .pure-button-primary:hover, +.dark-theme .pure-button-primary:focus, +.dark-theme .pure-button-secondary:hover, +.dark-theme .pure-button-secondary:focus { + color: #fff !important; + border-color: rgb(0, 182, 240) !important; + background-color: rgba(0, 182, 240, 1) !important; +} + +.dark-theme .pure-button-secondary { + background-color: #0002; + color: #ddd; +} + .dark-theme a { color: #a0a0a0; text-decoration: none; @@ -554,6 +568,20 @@ body.dark-theme { color: rgb(0, 182, 240); } + .no-theme .pure-button-primary:hover, + .no-theme .pure-button-primary:focus, + .no-theme .pure-button-secondary:hover, + .no-theme .pure-button-secondary:focus { + color: #fff !important; + border-color: rgb(0, 182, 240) !important; + background-color: rgba(0, 182, 240, 1) !important; + } + + .no-theme .pure-button-secondary { + background-color: #0002; + color: #ddd; + } + .no-theme a { color: #a0a0a0; text-decoration: none; From 42fa6ad2a30038cd7cdc705f5da2bffdc9714349 Mon Sep 17 00:00:00 2001 From: Samantaz Fox Date: Mon, 24 Apr 2023 19:44:15 +0200 Subject: [PATCH 016/143] HTML/CSS: Fix buttons' responsiveness --- assets/css/default.css | 94 ++++++++++++++----- .../components/video-context-buttons.ecr | 4 +- src/invidious/views/playlist.ecr | 18 ++-- 3 files changed, 78 insertions(+), 38 deletions(-) diff --git a/assets/css/default.css b/assets/css/default.css index f671c3bf..21121f4d 100644 --- a/assets/css/default.css +++ b/assets/css/default.css @@ -1,3 +1,7 @@ +/* + * Common attributes + */ + html, body { font-family: BlinkMacSystemFont, -apple-system, "Segoe UI", Roboto, Oxygen, @@ -11,6 +15,16 @@ body { min-height: 100vh; } +.h-box { + padding-left: 1em; + padding-right: 1em; +} + +.v-box { + padding-top: 1em; + padding-bottom: 1em; +} + .deleted { background-color: rgb(255, 0, 0, 0.5); } @@ -20,6 +34,34 @@ body { margin-bottom: 20px; } +.title { + margin: 0.5em 0 1em 0; +} + +/* A flex container */ +.flexible { + display: flex; + align-items: center; +} + +.flex-left { + display: flex; + flex: 1 1 auto; + flex-flow: row wrap; + justify-content: flex-start; +} +.flex-right { + display: flex; + flex: 2 0 auto; + flex-flow: row nowrap; + justify-content: flex-end; +} + + +/* + * Channel page + */ + .channel-profile > * { font-size: 1.17em; font-weight: bold; @@ -90,16 +132,6 @@ body a.channel-owner { } } -.h-box { - padding-left: 1em; - padding-right: 1em; -} - -.v-box { - padding-top: 1em; - padding-bottom: 1em; -} - div { overflow-wrap: break-word; word-wrap: break-word; @@ -144,9 +176,15 @@ body a.pure-button-primary, margin: 0; } +/* Has to be combined with flex-left/right */ +.button-container { + flex-flow: wrap; + gap: 0.5em 0.75em; +} + /* - * Thumbnails + * Video thumbnails */ div.thumbnail { @@ -280,6 +318,11 @@ input[type="search"]::-webkit-search-cancel-button { margin-right: 1em; } + +/* + * Responsive rules + */ + @media only screen and (max-aspect-ratio: 16/9) { .player-dimensions.vjs-fluid { padding-top: 46.86% !important; @@ -298,20 +341,28 @@ input[type="search"]::-webkit-search-cancel-button { .navbar > div { display: flex; justify-content: center; - } - - .navbar > div:not(:last-child) { - margin-bottom: 1em; + margin-bottom: 25px; } .navbar > .searchbar > form { - width: 60%; + width: 75%; } h1 { font-size: 1.25em; margin: 0.42em 0; } + + /* Space out the subscribe & RSS buttons and align them to the left */ + .title.flexible { display: block; } + .title.flexible > .flex-right { margin: 0.75em 0; justify-content: flex-start; } + + /* Space out buttons to make them easier to tap */ + .user-field { font-size: 125%; } + .user-field > :not(:last-child) { margin-right: 1.75em; } + + .icon-buttons { font-size: 125%; } + .icon-buttons > :not(:last-child) { margin-right: 0.75em; } } @media screen and (max-width: 320px) { @@ -328,10 +379,6 @@ input[type="search"]::-webkit-search-cancel-button { .video-card-row { margin: 15px 0; } -.flexible { display: flex; } -.flex-left { flex: 1 1 100%; flex-wrap: wrap; } -.flex-right { flex: 1 0 auto; flex-wrap: nowrap; } - p.channel-name { margin: 0; } p.video-data { margin: 0; font-weight: bold; font-size: 80%; } @@ -659,12 +706,7 @@ label[for="music-desc-expansion"]:hover { } /* Bidi (bidirectional text) support */ -h1, -h2, -h3, -h4, -h5, -p, +h1, h2, h3, h4, h5, p, #descriptionWrapper, #description-box, #music-description-box { diff --git a/src/invidious/views/components/video-context-buttons.ecr b/src/invidious/views/components/video-context-buttons.ecr index ddb6c983..385ed6b3 100644 --- a/src/invidious/views/components/video-context-buttons.ecr +++ b/src/invidious/views/components/video-context-buttons.ecr @@ -1,4 +1,4 @@ -
+
" href="https://www.youtube.com/watch<%=endpoint_params%>"> @@ -6,7 +6,7 @@ " href="/watch<%=endpoint_params%>&listen=1"> - + <% if env.get("preferences").as(Preferences).automatic_instance_redirect%> " href="/redirect?referer=%2Fwatch<%=URI.encode_www_form(endpoint_params)%>"> diff --git a/src/invidious/views/playlist.ecr b/src/invidious/views/playlist.ecr index 8d4d116d..ee9ba87b 100644 --- a/src/invidious/views/playlist.ecr +++ b/src/invidious/views/playlist.ecr @@ -6,30 +6,28 @@ <% end %> -
-
-

<%= title %>

-
+
+

<%= title %>

-
+
<%- if playlist.is_a?(InvidiousPlaylist) && playlist.author == user.try &.email -%> -
+ -
+ -
+ <%- else -%> -
+
<%- if IV::Database::Playlists.exists?(playlist.id) -%>  <%= translate(locale, "Subscribe") %> @@ -42,7 +40,7 @@
<%- end -%> -
+
 <%= translate(locale, "generic_button_rss") %> From 411208bbd211d7effe278eabe23d5e2f502b5ea6 Mon Sep 17 00:00:00 2001 From: Samantaz Fox Date: Mon, 24 Apr 2023 20:28:40 +0200 Subject: [PATCH 017/143] HTML: Reorder buttons on the channel and watch pages --- .../views/components/channel_info.ecr | 29 ++++++++--------- .../views/components/subscribe_widget.ecr | 6 ---- src/invidious/views/watch.ecr | 31 ++++++++++++------- 3 files changed, 35 insertions(+), 31 deletions(-) diff --git a/src/invidious/views/components/channel_info.ecr b/src/invidious/views/components/channel_info.ecr index 59888760..f4164f31 100644 --- a/src/invidious/views/components/channel_info.ecr +++ b/src/invidious/views/components/channel_info.ecr @@ -8,29 +8,30 @@
<% end %> -
-
+
+
<%= author %><% if !channel.verified.nil? && channel.verified %> <% end %>
-
-

- -

+ +
+
+ <% sub_count_text = number_to_short_text(channel.sub_count) %> + <%= rendered "components/subscribe_widget" %> +
+ +
-
-

<%= channel.description_html %>

-
-
- -
- <% sub_count_text = number_to_short_text(channel.sub_count) %> - <%= rendered "components/subscribe_widget" %> +

<%= channel.description_html %>

diff --git a/src/invidious/views/components/subscribe_widget.ecr b/src/invidious/views/components/subscribe_widget.ecr index b9d5f783..05e4e253 100644 --- a/src/invidious/views/components/subscribe_widget.ecr +++ b/src/invidious/views/components/subscribe_widget.ecr @@ -1,22 +1,18 @@ <% if user %> <% if subscriptions.includes? ucid %> -

" method="post"> ">
-

<% else %> -

" method="post"> ">
-

<% end %> <% else %> -

"> <%= translate(locale, "Subscribe") %> | <%= sub_count_text %> -

<% end %> diff --git a/src/invidious/views/watch.ecr b/src/invidious/views/watch.ecr index 5b3190f3..4f4354a9 100644 --- a/src/invidious/views/watch.ecr +++ b/src/invidious/views/watch.ecr @@ -204,19 +204,28 @@ we're going to need to do it here in order to allow for translations.
-
- -
- <% if !video.author_thumbnail.empty? %> - - <% end %> - <%= author %><% if !video.author_verified.nil? && video.author_verified %> <% end %> + +
+ + +
+
+ <% sub_count_text = video.sub_count_text %> + <%= rendered "components/subscribe_widget" %>
- - - <% sub_count_text = video.sub_count_text %> - <%= rendered "components/subscribe_widget" %> +
+
+

<% if video.premiere_timestamp.try &.> Time.utc %> <%= video.premiere_timestamp.try { |t| translate(locale, "Premieres `x`", t.to_s("%B %-d, %R UTC")) } %> From 06b2bab795ebf54e9c6a396e37a129a87d39675a Mon Sep 17 00:00:00 2001 From: Samantaz Fox Date: Mon, 24 Apr 2023 22:19:46 +0200 Subject: [PATCH 018/143] HTML: Fix thumbnails of related videos (watch page) --- src/invidious/views/watch.ecr | 29 +++++++++++++++++++++-------- 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/src/invidious/views/watch.ecr b/src/invidious/views/watch.ecr index 4f4354a9..9275631c 100644 --- a/src/invidious/views/watch.ecr +++ b/src/invidious/views/watch.ecr @@ -304,15 +304,26 @@ we're going to need to do it here in order to allow for translations. <% video.related_videos.each do |rv| %> <% if rv["id"]? %> - &listen=<%= params.listen %>"> - <% if !env.get("preferences").as(Preferences).thin_mode %> -

+
+ + - <% end %> -

<%= rv["title"] %>

-
+ + <%- end -%> + +
+ <%- if (length_seconds = rv["length_seconds"]?.try &.to_i?) && length_seconds != 0 -%> +

<%= recode_length_seconds(length_seconds) %>

+ <%- end -%> +
+
+ + +
<% if rv["ucid"]? %> @@ -330,6 +341,8 @@ we're going to need to do it here in order to allow for translations. %>
+ +
<% end %> <% end %>
From c17404890ca9618ebc828a06bc88ff2bd79e811e Mon Sep 17 00:00:00 2001 From: Samantaz Fox Date: Tue, 23 May 2023 22:49:44 +0200 Subject: [PATCH 019/143] HTML: Use the new pagination component for history/subscriptions --- src/invidious/routes/feeds.cr | 8 +++++++ src/invidious/views/feeds/history.ecr | 24 ++++++-------------- src/invidious/views/feeds/subscriptions.ecr | 25 +++++++-------------- 3 files changed, 23 insertions(+), 34 deletions(-) diff --git a/src/invidious/routes/feeds.cr b/src/invidious/routes/feeds.cr index fc62c5a3..a8246b2e 100644 --- a/src/invidious/routes/feeds.cr +++ b/src/invidious/routes/feeds.cr @@ -102,6 +102,10 @@ module Invidious::Routes::Feeds end env.set "user", user + # Used for pagination links + base_url = "/feed/subscriptions" + base_url += "?max_results=#{max_results}" if env.params.query.has_key?("max_results") + templated "feeds/subscriptions" end @@ -129,6 +133,10 @@ module Invidious::Routes::Feeds end watched ||= [] of String + # Used for pagination links + base_url = "/feed/history" + base_url += "?max_results=#{max_results}" if env.params.query.has_key?("max_results") + templated "feeds/history" end diff --git a/src/invidious/views/feeds/history.ecr b/src/invidious/views/feeds/history.ecr index 83ea7238..bda4e1f3 100644 --- a/src/invidious/views/feeds/history.ecr +++ b/src/invidious/views/feeds/history.ecr @@ -50,20 +50,10 @@ <% end %>
- +<%= + IV::Frontend::Pagination.nav_numeric(locale, + base_url: base_url, + current_page: page, + show_next: (watched.size >= max_results) + ) +%> diff --git a/src/invidious/views/feeds/subscriptions.ecr b/src/invidious/views/feeds/subscriptions.ecr index 9c69c5b0..c36bd00f 100644 --- a/src/invidious/views/feeds/subscriptions.ecr +++ b/src/invidious/views/feeds/subscriptions.ecr @@ -56,6 +56,7 @@ +
<% videos.each do |item| %> <%= rendered "components/item" %> @@ -64,20 +65,10 @@ -
- -
-
- <% if (videos.size + notifications.size) == max_results %> - &max_results=<%= max_results %><% end %>"> - <%= translate(locale, "Next page") %> - - <% end %> -
-
+<%= + IV::Frontend::Pagination.nav_numeric(locale, + base_url: base_url, + current_page: page, + show_next: ((videos.size + notifications.size) == max_results) + ) +%> From 9b75f79fb553403d0af7b2f9a1212a1e93bcf85b Mon Sep 17 00:00:00 2001 From: Samantaz Fox Date: Sat, 8 Jul 2023 21:17:44 +0200 Subject: [PATCH 020/143] HTML/CSS: Add thumbnail placeholder in thin mode This change is required to make the overlay buttons functional (add to and delete from playlist, mark as watched, etc.) --- assets/css/default.css | 5 +++++ src/invidious/views/components/item.ecr | 8 +++++++- src/invidious/views/watch.ecr | 2 ++ 3 files changed, 14 insertions(+), 1 deletion(-) diff --git a/assets/css/default.css b/assets/css/default.css index 21121f4d..c31b24e5 100644 --- a/assets/css/default.css +++ b/assets/css/default.css @@ -199,6 +199,11 @@ img.thumbnail { object-fit: cover; } +.thumbnail-placeholder { + min-height: 50px; + border: 2px dotted; +} + div.watched-overlay { z-index: 50; position: absolute; diff --git a/src/invidious/views/components/item.ecr b/src/invidious/views/components/item.ecr index 9b73f7ee..7ffd2d93 100644 --- a/src/invidious/views/components/item.ecr +++ b/src/invidious/views/components/item.ecr @@ -14,6 +14,8 @@ " alt="" /> + <%- else -%> +
<% end %>
@@ -41,6 +43,8 @@ " alt="" /> + <%- else -%> +
<%- end -%>
@@ -76,7 +80,7 @@ -%>
- <%- if !env.get("preferences").as(Preferences).thin_mode -%> + <%- if !thin_mode -%> @@ -85,6 +89,8 @@
<% end %>
+ <%- else -%> +
<%- end -%>
diff --git a/src/invidious/views/watch.ecr b/src/invidious/views/watch.ecr index 9275631c..498d57a1 100644 --- a/src/invidious/views/watch.ecr +++ b/src/invidious/views/watch.ecr @@ -311,6 +311,8 @@ we're going to need to do it here in order to allow for translations. &listen=<%= params.listen %>"> /mqdefault.jpg" alt="" /> + <%- else -%> +
<%- end -%>
From 0110f865c39fd0a1d416502422110430f92f4ef3 Mon Sep 17 00:00:00 2001 From: Brahim Hadriche Date: Sat, 8 Jul 2023 16:51:19 -0400 Subject: [PATCH 021/143] Playlist import no refresh --- src/invidious/user/imports.cr | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/invidious/user/imports.cr b/src/invidious/user/imports.cr index 0a2fe1e2..86d0ce6e 100644 --- a/src/invidious/user/imports.cr +++ b/src/invidious/user/imports.cr @@ -133,7 +133,7 @@ struct Invidious::User next if !video_id begin - video = get_video(video_id) + video = get_video(video_id, false) rescue ex next end From f2fa3da9d2f8ffc1684997526ddd5b3357d88897 Mon Sep 17 00:00:00 2001 From: ChunkyProgrammer <78101139+ChunkyProgrammer@users.noreply.github.com> Date: Wed, 12 Jul 2023 11:06:34 -0700 Subject: [PATCH 022/143] Add support for releases and podcasts tabs --- locales/en-US.json | 2 + src/invidious/channels/playlists.cr | 18 +++++++ src/invidious/frontend/channel_page.cr | 2 + src/invidious/routes/api/v1/channels.cr | 62 ++++++++++++++++++++++++- src/invidious/routes/channels.cr | 44 +++++++++++++++++- src/invidious/routing.cr | 5 ++ src/invidious/views/channel.ecr | 2 + src/invidious/yt_backend/extractors.cr | 5 +- 8 files changed, 134 insertions(+), 6 deletions(-) diff --git a/locales/en-US.json b/locales/en-US.json index e13ba968..29dd7a40 100644 --- a/locales/en-US.json +++ b/locales/en-US.json @@ -474,6 +474,8 @@ "channel_tab_videos_label": "Videos", "channel_tab_shorts_label": "Shorts", "channel_tab_streams_label": "Livestreams", + "channel_tab_podcasts_label": "Podcasts", + "channel_tab_releases_label": "Releases", "channel_tab_playlists_label": "Playlists", "channel_tab_community_label": "Community", "channel_tab_channels_label": "Channels" diff --git a/src/invidious/channels/playlists.cr b/src/invidious/channels/playlists.cr index 8dc824b2..91029fe3 100644 --- a/src/invidious/channels/playlists.cr +++ b/src/invidious/channels/playlists.cr @@ -26,3 +26,21 @@ def fetch_channel_playlists(ucid, author, continuation, sort_by) return extract_items(initial_data, author, ucid) end + +def fetch_channel_podcasts(ucid, author, continuation) + if continuation + initial_data = YoutubeAPI.browse(continuation) + else + initial_data = YoutubeAPI.browse(ucid, params: "Eghwb2RjYXN0c_IGBQoDugEA") + end + return extract_items(initial_data, author, ucid) +end + +def fetch_channel_releases(ucid, author, continuation) + if continuation + initial_data = YoutubeAPI.browse(continuation) + else + initial_data = YoutubeAPI.browse(ucid, params: "EghyZWxlYXNlc_IGBQoDsgEA") + end + return extract_items(initial_data, author, ucid) +end diff --git a/src/invidious/frontend/channel_page.cr b/src/invidious/frontend/channel_page.cr index 53745dd5..fe7d6d6e 100644 --- a/src/invidious/frontend/channel_page.cr +++ b/src/invidious/frontend/channel_page.cr @@ -5,6 +5,8 @@ module Invidious::Frontend::ChannelPage Videos Shorts Streams + Podcasts + Releases Playlists Community Channels diff --git a/src/invidious/routes/api/v1/channels.cr b/src/invidious/routes/api/v1/channels.cr index bcb4db2c..adf05d30 100644 --- a/src/invidious/routes/api/v1/channels.cr +++ b/src/invidious/routes/api/v1/channels.cr @@ -245,7 +245,7 @@ module Invidious::Routes::API::V1::Channels channel = nil # Make the compiler happy get_channel() - items, continuation = fetch_channel_playlists(channel.ucid, channel.author, continuation, sort_by) + items, next_continuation = fetch_channel_playlists(channel.ucid, channel.author, continuation, sort_by) JSON.build do |json| json.object do @@ -257,7 +257,65 @@ module Invidious::Routes::API::V1::Channels end end - json.field "continuation", continuation + json.field "continuation", next_continuation if next_continuation + end + end + end + + def self.podcasts(env) + locale = env.get("preferences").as(Preferences).locale + + env.response.content_type = "application/json" + + ucid = env.params.url["ucid"] + continuation = env.params.query["continuation"]? + + # Use the macro defined above + channel = nil # Make the compiler happy + get_channel() + + items, next_continuation = fetch_channel_podcasts(channel.ucid, channel.author, continuation) + + JSON.build do |json| + json.object do + json.field "playlists" do + json.array do + items.each do |item| + item.to_json(locale, json) if item.is_a?(SearchPlaylist) + end + end + end + + json.field "continuation", next_continuation if next_continuation + end + end + end + + def self.releases(env) + locale = env.get("preferences").as(Preferences).locale + + env.response.content_type = "application/json" + + ucid = env.params.url["ucid"] + continuation = env.params.query["continuation"]? + + # Use the macro defined above + channel = nil # Make the compiler happy + get_channel() + + items, next_continuation = fetch_channel_releases(channel.ucid, channel.author, continuation) + + JSON.build do |json| + json.object do + json.field "playlists" do + json.array do + items.each do |item| + item.to_json(locale, json) if item.is_a?(SearchPlaylist) + end + end + end + + json.field "continuation", next_continuation if next_continuation end end end diff --git a/src/invidious/routes/channels.cr b/src/invidious/routes/channels.cr index 16621994..9892ae2a 100644 --- a/src/invidious/routes/channels.cr +++ b/src/invidious/routes/channels.cr @@ -27,7 +27,7 @@ module Invidious::Routes::Channels item.author end end - items = items.select(SearchPlaylist).map(&.as(SearchPlaylist)) + items = items.select(SearchPlaylist) items.each(&.author = "") else sort_options = {"newest", "oldest", "popular"} @@ -105,13 +105,53 @@ module Invidious::Routes::Channels channel.ucid, channel.author, continuation, (sort_by || "last") ) - items = items.select(SearchPlaylist).map(&.as(SearchPlaylist)) + items = items.select(SearchPlaylist) items.each(&.author = "") selected_tab = Frontend::ChannelPage::TabsAvailable::Playlists templated "channel" end + def self.podcasts(env) + data = self.fetch_basic_information(env) + return data if !data.is_a?(Tuple) + + locale, user, subscriptions, continuation, ucid, channel = data + + sort_by = "" + sort_options = [] of String + + items, next_continuation = fetch_channel_podcasts( + channel.ucid, channel.author, continuation + ) + + items = items.select(SearchPlaylist) + items.each(&.author = "") + + selected_tab = Frontend::ChannelPage::TabsAvailable::Podcasts + templated "channel" + end + + def self.releases(env) + data = self.fetch_basic_information(env) + return data if !data.is_a?(Tuple) + + locale, user, subscriptions, continuation, ucid, channel = data + + sort_by = "" + sort_options = [] of String + + items, next_continuation = fetch_channel_releases( + channel.ucid, channel.author, continuation + ) + + items = items.select(SearchPlaylist) + items.each(&.author = "") + + selected_tab = Frontend::ChannelPage::TabsAvailable::Releases + templated "channel" + end + def self.community(env) data = self.fetch_basic_information(env) if !data.is_a?(Tuple) diff --git a/src/invidious/routing.cr b/src/invidious/routing.cr index daaf4d88..9c43171c 100644 --- a/src/invidious/routing.cr +++ b/src/invidious/routing.cr @@ -118,6 +118,8 @@ module Invidious::Routing get "/channel/:ucid/videos", Routes::Channels, :videos get "/channel/:ucid/shorts", Routes::Channels, :shorts get "/channel/:ucid/streams", Routes::Channels, :streams + get "/channel/:ucid/podcasts", Routes::Channels, :podcasts + get "/channel/:ucid/releases", Routes::Channels, :releases get "/channel/:ucid/playlists", Routes::Channels, :playlists get "/channel/:ucid/community", Routes::Channels, :community get "/channel/:ucid/channels", Routes::Channels, :channels @@ -228,6 +230,9 @@ module Invidious::Routing get "/api/v1/channels/:ucid", {{namespace}}::Channels, :home get "/api/v1/channels/:ucid/shorts", {{namespace}}::Channels, :shorts get "/api/v1/channels/:ucid/streams", {{namespace}}::Channels, :streams + get "/api/v1/channels/:ucid/podcasts", {{namespace}}::Channels, :podcasts + get "/api/v1/channels/:ucid/releases", {{namespace}}::Channels, :releases + get "/api/v1/channels/:ucid/channels", {{namespace}}::Channels, :channels {% for route in {"videos", "latest", "playlists", "community", "search"} %} diff --git a/src/invidious/views/channel.ecr b/src/invidious/views/channel.ecr index 6e62a471..066e25b5 100644 --- a/src/invidious/views/channel.ecr +++ b/src/invidious/views/channel.ecr @@ -9,6 +9,8 @@ when .streams? then "/channel/#{ucid}/streams" when .playlists? then "/channel/#{ucid}/playlists" when .channels? then "/channel/#{ucid}/channels" + when .podcasts? then "/channel/#{ucid}/podcasts" + when .releases? then "/channel/#{ucid}/releases" else "/channel/#{ucid}" end diff --git a/src/invidious/yt_backend/extractors.cr b/src/invidious/yt_backend/extractors.cr index 6686e6e7..e5029dc5 100644 --- a/src/invidious/yt_backend/extractors.cr +++ b/src/invidious/yt_backend/extractors.cr @@ -408,8 +408,8 @@ private module Parsers # Returns nil when the given object isn't a RichItemRenderer # # A richItemRenderer seems to be a simple wrapper for a videoRenderer, used - # by the result page for hashtags. It is located inside a continuationItems - # container. + # by the result page for hashtags and for the podcast tab on channels. + # It is located inside a continuationItems container for hashtags. # module RichItemRendererParser def self.process(item : JSON::Any, author_fallback : AuthorFallback) @@ -421,6 +421,7 @@ private module Parsers private def self.parse(item_contents, author_fallback) child = VideoRendererParser.process(item_contents, author_fallback) child ||= ReelItemRendererParser.process(item_contents, author_fallback) + child ||= PlaylistRendererParser.process(item_contents, author_fallback) return child end From 05cc5033910cabe7008832e8917b93ee3112a540 Mon Sep 17 00:00:00 2001 From: Samantaz Fox Date: Sat, 15 Jul 2023 12:57:26 +0000 Subject: [PATCH 023/143] Fix lint --- src/invidious/views/channel.ecr | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/invidious/views/channel.ecr b/src/invidious/views/channel.ecr index 066e25b5..4b50e7a0 100644 --- a/src/invidious/views/channel.ecr +++ b/src/invidious/views/channel.ecr @@ -9,8 +9,8 @@ when .streams? then "/channel/#{ucid}/streams" when .playlists? then "/channel/#{ucid}/playlists" when .channels? then "/channel/#{ucid}/channels" - when .podcasts? then "/channel/#{ucid}/podcasts" - when .releases? then "/channel/#{ucid}/releases" + when .podcasts? then "/channel/#{ucid}/podcasts" + when .releases? then "/channel/#{ucid}/releases" else "/channel/#{ucid}" end From 70145cba31fb7fa14dafa3493c9133c01f642116 Mon Sep 17 00:00:00 2001 From: ChunkyProgrammer <78101139+ChunkyProgrammer@users.noreply.github.com> Date: Tue, 11 Jul 2023 20:49:36 -0700 Subject: [PATCH 024/143] Community: Parse `Quiz` attachments --- src/invidious/channels/community.cr | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/invidious/channels/community.cr b/src/invidious/channels/community.cr index aac4bc8a..671f6dee 100644 --- a/src/invidious/channels/community.cr +++ b/src/invidious/channels/community.cr @@ -216,6 +216,22 @@ def fetch_channel_community(ucid, continuation, locale, format, thin_mode) parse_item(attachment) .as(SearchPlaylist) .to_json(locale, json) + when .has_key?("quizRenderer") + json.object do + attachment = attachment["quizRenderer"] + json.field "type", "quiz" + json.field "totalVotes", short_text_to_number(attachment["totalVotes"]["simpleText"].as_s.split(" ")[0]) + json.field "choices" do + json.array do + attachment["choices"].as_a.each do |choice| + json.object do + json.field "text", choice.dig("text", "runs", 0, "text").as_s + json.field "isCorrect", choice["isCorrect"].as_bool + end + end + end + end + end else json.object do json.field "type", "unknown" From c8ecfaabe156e41999cf3a130a28a67a62b37ccb Mon Sep 17 00:00:00 2001 From: Samantaz Fox Date: Sun, 16 Jul 2023 17:28:37 +0200 Subject: [PATCH 025/143] Assets: Add SVG image for hashtag results --- assets/hashtag.svg | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 assets/hashtag.svg diff --git a/assets/hashtag.svg b/assets/hashtag.svg new file mode 100644 index 00000000..55109825 --- /dev/null +++ b/assets/hashtag.svg @@ -0,0 +1,9 @@ + + + + + + + + + From 839e90aeff93a18d59cb4fc53eb25cc5c152b44a Mon Sep 17 00:00:00 2001 From: Samantaz Fox Date: Sat, 15 Jul 2023 15:41:04 +0200 Subject: [PATCH 026/143] Extractors: Add module for 'hashtagTileRenderer' --- src/invidious/helpers/serialized_yt_data.cr | 21 +++++++- src/invidious/yt_backend/extractors.cr | 53 ++++++++++++++++++++- 2 files changed, 72 insertions(+), 2 deletions(-) diff --git a/src/invidious/helpers/serialized_yt_data.cr b/src/invidious/helpers/serialized_yt_data.cr index 7c12ad0e..e0bd7279 100644 --- a/src/invidious/helpers/serialized_yt_data.cr +++ b/src/invidious/helpers/serialized_yt_data.cr @@ -232,6 +232,25 @@ struct SearchChannel end end +struct SearchHashtag + include DB::Serializable + + property title : String + property url : String + property video_count : Int64 + property channel_count : Int64 + + def to_json(locale : String?, json : JSON::Builder) + json.object do + json.field "type", "hashtag" + json.field "title", self.title + json.field "url", self.url + json.field "videoCount", self.video_count + json.field "channelCount", self.channel_count + end + end +end + class Category include DB::Serializable @@ -274,4 +293,4 @@ struct Continuation end end -alias SearchItem = SearchVideo | SearchChannel | SearchPlaylist | Category +alias SearchItem = SearchVideo | SearchChannel | SearchPlaylist | SearchHashtag | Category diff --git a/src/invidious/yt_backend/extractors.cr b/src/invidious/yt_backend/extractors.cr index e5029dc5..8456313b 100644 --- a/src/invidious/yt_backend/extractors.cr +++ b/src/invidious/yt_backend/extractors.cr @@ -11,15 +11,16 @@ private ITEM_CONTAINER_EXTRACTOR = { } private ITEM_PARSERS = { + Parsers::RichItemRendererParser, Parsers::VideoRendererParser, Parsers::ChannelRendererParser, Parsers::GridPlaylistRendererParser, Parsers::PlaylistRendererParser, Parsers::CategoryRendererParser, - Parsers::RichItemRendererParser, Parsers::ReelItemRendererParser, Parsers::ItemSectionRendererParser, Parsers::ContinuationItemRendererParser, + Parsers::HashtagRendererParser, } private alias InitialData = Hash(String, JSON::Any) @@ -210,6 +211,56 @@ private module Parsers end end + # Parses an Innertube `hashtagTileRenderer` into a `SearchHashtag`. + # Returns `nil` when the given object is not a `hashtagTileRenderer`. + # + # A `hashtagTileRenderer` is a kind of search result. + # It can be found when searching for any hashtag (e.g "#hi" or "#shorts") + module HashtagRendererParser + def self.process(item : JSON::Any, author_fallback : AuthorFallback) + if item_contents = item["hashtagTileRenderer"]? + return self.parse(item_contents) + end + end + + private def self.parse(item_contents) + title = extract_text(item_contents["hashtag"]).not_nil! # E.g "#hi" + + # E.g "/hashtag/hi" + url = item_contents.dig?("onTapCommand", "commandMetadata", "webCommandMetadata", "url").try &.as_s + url ||= URI.encode_path("/hashtag/#{title.lchop('#')}") + + video_count_txt = extract_text(item_contents["hashtagVideoCount"]?) # E.g "203K videos" + channel_count_txt = extract_text(item_contents["hashtagChannelCount"]?) # E.g "81K channels" + + # Fallback for video/channel counts + if channel_count_txt.nil? || video_count_txt.nil? + # E.g: "203K videos • 81K channels" + info_text = extract_text(item_contents["hashtagInfoText"]?).try &.split(" • ") + + if info_text && info_text.size == 2 + video_count_txt ||= info_text[0] + channel_count_txt ||= info_text[1] + end + end + + return SearchHashtag.new({ + title: title, + url: url, + video_count: short_text_to_number(video_count_txt || ""), + channel_count: short_text_to_number(channel_count_txt || ""), + }) + rescue ex + LOGGER.debug("HashtagRendererParser: Failed to extract renderer.") + LOGGER.debug("HashtagRendererParser: Got exception: #{ex.message}") + return nil + end + + def self.parser_name + return {{@type.name}} + end + end + # Parses a InnerTube gridPlaylistRenderer into a SearchPlaylist. Returns nil when the given object isn't a gridPlaylistRenderer # # A gridPlaylistRenderer renders a playlist, that is located in a grid, to click on within the YouTube and Invidious UI. From f38d1f33b140a1de13e20d14b7a1ff0fcf0a40b4 Mon Sep 17 00:00:00 2001 From: Samantaz Fox Date: Sat, 15 Jul 2023 15:42:46 +0200 Subject: [PATCH 027/143] HTML: Add UI element for 'SearchHashtag' in item.ecr --- src/invidious/views/components/item.ecr | 26 ++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/src/invidious/views/components/item.ecr b/src/invidious/views/components/item.ecr index 7ffd2d93..c29ec47b 100644 --- a/src/invidious/views/components/item.ecr +++ b/src/invidious/views/components/item.ecr @@ -1,6 +1,6 @@ <%- thin_mode = env.get("preferences").as(Preferences).thin_mode - item_watched = !item.is_a?(SearchChannel | SearchPlaylist | InvidiousPlaylist | Category) && env.get?("user").try &.as(User).watched.index(item.id) != nil + item_watched = !item.is_a?(SearchChannel | SearchHashtag | SearchPlaylist | InvidiousPlaylist | Category) && env.get?("user").try &.as(User).watched.index(item.id) != nil author_verified = item.responds_to?(:author_verified) && item.author_verified -%> @@ -29,6 +29,30 @@

<%= translate_count(locale, "generic_subscribers_count", item.subscriber_count, NumberFormatting::Separator) %>

<% if !item.auto_generated %>

<%= translate_count(locale, "generic_videos_count", item.video_count, NumberFormatting::Separator) %>

<% end %>
<%= item.description_html %>
+ <% when SearchHashtag %> + <% if !thin_mode %> + +
+
+ <%- else -%> +
+ <% end %> + + + +
+ <%- if item.video_count != 0 -%> +

<%= translate_count(locale, "generic_videos_count", item.video_count, NumberFormatting::Separator) %>

+ <%- end -%> +
+ +
+ <%- if item.channel_count != 0 -%> +

<%= translate_count(locale, "generic_channels_count", item.channel_count, NumberFormatting::Separator) %>

+ <%- end -%> +
<% when SearchPlaylist, InvidiousPlaylist %> <%- if item.id.starts_with? "RD" From c1a69e4a4a8b581ec743b7b3f741097d6596cb3b Mon Sep 17 00:00:00 2001 From: Samantaz Fox Date: Sun, 16 Jul 2023 17:23:23 +0200 Subject: [PATCH 028/143] Channels: Use innertube to fetch the community tab --- src/invidious/channels/community.cr | 54 +++++++++----------------- src/invidious/yt_backend/extractors.cr | 26 ++++++++----- 2 files changed, 34 insertions(+), 46 deletions(-) diff --git a/src/invidious/channels/community.cr b/src/invidious/channels/community.cr index aac4bc8a..1a54a946 100644 --- a/src/invidious/channels/community.cr +++ b/src/invidious/channels/community.cr @@ -1,49 +1,31 @@ private IMAGE_QUALITIES = {320, 560, 640, 1280, 2000} # TODO: Add "sort_by" -def fetch_channel_community(ucid, continuation, locale, format, thin_mode) - response = YT_POOL.client &.get("/channel/#{ucid}/community?gl=US&hl=en") - if response.status_code != 200 - response = YT_POOL.client &.get("/user/#{ucid}/community?gl=US&hl=en") - end +def fetch_channel_community(ucid, cursor, locale, format, thin_mode) + if cursor.nil? + # Egljb21tdW5pdHk%3D is the protobuf object to load "community" + initial_data = YoutubeAPI.browse(ucid, params: "Egljb21tdW5pdHk%3D") - if response.status_code != 200 - raise NotFoundException.new("This channel does not exist.") - end - - ucid = response.body.match(/https:\/\/www.youtube.com\/channel\/(?UC[a-zA-Z0-9_-]{22})/).not_nil!["ucid"] - - if !continuation || continuation.empty? - initial_data = extract_initial_data(response.body) - body = extract_selected_tab(initial_data["contents"]["twoColumnBrowseResultsRenderer"]["tabs"])["content"]["sectionListRenderer"]["contents"][0]["itemSectionRenderer"] - - if !body - raise InfoException.new("Could not extract community tab.") + items = [] of JSON::Any + extract_items(initial_data) do |item| + items << item end else - continuation = produce_channel_community_continuation(ucid, continuation) + continuation = produce_channel_community_continuation(ucid, cursor) + initial_data = YoutubeAPI.browse(continuation: continuation) - headers = HTTP::Headers.new - headers["cookie"] = response.cookies.add_request_headers(headers)["cookie"] + container = initial_data.dig?("continuationContents", "itemSectionContinuation", "contents") - session_token = response.body.match(/"XSRF_TOKEN":"(?[^"]+)"/).try &.["session_token"]? || "" - post_req = { - session_token: session_token, - } + raise InfoException.new("Can't extract community data") if container.nil? - body = YoutubeAPI.browse(continuation) - - body = body.dig?("continuationContents", "itemSectionContinuation") || - body.dig?("continuationContents", "backstageCommentsContinuation") - - if !body - raise InfoException.new("Could not extract continuation.") - end + items = container.as_a end - posts = body["contents"].as_a + return extract_channel_community(items, ucid: ucid, locale: locale, format: format, thin_mode: thin_mode) +end - if message = posts[0]["messageRenderer"]? +def extract_channel_community(items, *, ucid, locale, format, thin_mode) + if message = items[0]["messageRenderer"]? error_message = (message["text"]["simpleText"]? || message["text"]["runs"]?.try &.[0]?.try &.["text"]?) .try &.as_s || "" @@ -59,7 +41,7 @@ def fetch_channel_community(ucid, continuation, locale, format, thin_mode) json.field "authorId", ucid json.field "comments" do json.array do - posts.each do |post| + items.each do |post| comments = post["backstagePostThreadRenderer"]?.try &.["comments"]? || post["backstageCommentsContinuation"]? @@ -242,7 +224,7 @@ def fetch_channel_community(ucid, continuation, locale, format, thin_mode) end end end - if cont = posts.dig?(-1, "continuationItemRenderer", "continuationEndpoint", "continuationCommand", "token") + if cont = items.dig?(-1, "continuationItemRenderer", "continuationEndpoint", "continuationCommand", "token") json.field "continuation", extract_channel_community_cursor(cont.as_s) end end diff --git a/src/invidious/yt_backend/extractors.cr b/src/invidious/yt_backend/extractors.cr index e5029dc5..8cf59d50 100644 --- a/src/invidious/yt_backend/extractors.cr +++ b/src/invidious/yt_backend/extractors.cr @@ -608,19 +608,25 @@ private module Extractors private def self.unpack_section_list(contents) raw_items = [] of JSON::Any - contents.as_a.each do |renderer_container| - renderer_container_contents = renderer_container["itemSectionRenderer"]["contents"][0] - - # Category extraction - if items_container = renderer_container_contents["shelfRenderer"]? - raw_items << renderer_container_contents - next - elsif items_container = renderer_container_contents["gridRenderer"]? + contents.as_a.each do |item| + if item_section_content = item.dig?("itemSectionRenderer", "contents") + raw_items += self.unpack_item_section(item_section_content) else - items_container = renderer_container_contents + raw_items << item end + end - items_container["items"]?.try &.as_a.each do |item| + return raw_items + end + + private def self.unpack_item_section(contents) + raw_items = [] of JSON::Any + + contents.as_a.each do |item| + # Category extraction + if container = item.dig?("gridRenderer", "items") || item.dig?("items") + raw_items += container.as_a + else raw_items << item end end From 2e67b90540d35ede212866e1fb597fd57ced35d5 Mon Sep 17 00:00:00 2001 From: syeopite Date: Sat, 22 Jul 2023 23:55:05 -0700 Subject: [PATCH 029/143] Add method to query /youtubei/v1/get_transcript --- src/invidious/yt_backend/youtube_api.cr | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/src/invidious/yt_backend/youtube_api.cr b/src/invidious/yt_backend/youtube_api.cr index 3dd9e9d8..f8aca04d 100644 --- a/src/invidious/yt_backend/youtube_api.cr +++ b/src/invidious/yt_backend/youtube_api.cr @@ -557,6 +557,30 @@ module YoutubeAPI return self._post_json("/youtubei/v1/search", data, client_config) end + #################################################################### + # transcript(params) + # + # Requests the youtubei/v1/get_transcript endpoint with the required headers + # and POST data in order to get a JSON reply. + # + # The requested data is a specially encoded protobuf string that denotes the specific language requested. + # + # An optional ClientConfig parameter can be passed, too (see + # `struct ClientConfig` above for more details). + # + + def transcript( + params : String, + client_config : ClientConfig | Nil = nil + ) : Hash(String, JSON::Any) + data = { + "context" => self.make_context(client_config), + "params" => params, + } + + return self._post_json("/youtubei/v1/get_transcript", data, client_config) + end + #################################################################### # _post_json(endpoint, data, client_config?) # From 7e5935a9da5355bbdd4c047edf692b0ce57722c7 Mon Sep 17 00:00:00 2001 From: syeopite Date: Sun, 23 Jul 2023 00:54:43 -0700 Subject: [PATCH 030/143] Rename Caption struct to CaptionMetadata The Caption object does not actually store any text lines for the subtitles. Instead it stores the metadata needed to display and fetch the actual captions from the YT timedtext API. Therefore it may be wiser to rename the struct to be more reflective of its current usage as well as the future usage once the current caption retrival system is replaced via InnerTube's transcript API --- src/invidious/frontend/watch_page.cr | 2 +- src/invidious/videos.cr | 6 +++--- src/invidious/videos/caption.cr | 8 ++++---- src/invidious/views/user/preferences.ecr | 2 +- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/invidious/frontend/watch_page.cr b/src/invidious/frontend/watch_page.cr index e3214469..b860dba7 100644 --- a/src/invidious/frontend/watch_page.cr +++ b/src/invidious/frontend/watch_page.cr @@ -7,7 +7,7 @@ module Invidious::Frontend::WatchPage getter full_videos : Array(Hash(String, JSON::Any)) getter video_streams : Array(Hash(String, JSON::Any)) getter audio_streams : Array(Hash(String, JSON::Any)) - getter captions : Array(Invidious::Videos::Caption) + getter captions : Array(Invidious::Videos::CaptionMetadata) def initialize( @full_videos, diff --git a/src/invidious/videos.cr b/src/invidious/videos.cr index f38b33e5..2b1d2603 100644 --- a/src/invidious/videos.cr +++ b/src/invidious/videos.cr @@ -24,7 +24,7 @@ struct Video property updated : Time @[DB::Field(ignore: true)] - @captions = [] of Invidious::Videos::Caption + @captions = [] of Invidious::Videos::CaptionMetadata @[DB::Field(ignore: true)] property adaptive_fmts : Array(Hash(String, JSON::Any))? @@ -215,9 +215,9 @@ struct Video keywords.includes? "YouTube Red" end - def captions : Array(Invidious::Videos::Caption) + def captions : Array(Invidious::Videos::CaptionMetadata) if @captions.empty? && @info.has_key?("captions") - @captions = Invidious::Videos::Caption.from_yt_json(info["captions"]) + @captions = Invidious::Videos::CaptionMetadata.from_yt_json(info["captions"]) end return @captions diff --git a/src/invidious/videos/caption.cr b/src/invidious/videos/caption.cr index 13f81a31..c85b46c3 100644 --- a/src/invidious/videos/caption.cr +++ b/src/invidious/videos/caption.cr @@ -1,7 +1,7 @@ require "json" module Invidious::Videos - struct Caption + struct CaptionMetadata property name : String property language_code : String property base_url : String @@ -10,12 +10,12 @@ module Invidious::Videos end # Parse the JSON structure from Youtube - def self.from_yt_json(container : JSON::Any) : Array(Caption) + def self.from_yt_json(container : JSON::Any) : Array(CaptionMetadata) caption_tracks = container .dig?("playerCaptionsTracklistRenderer", "captionTracks") .try &.as_a - captions_list = [] of Caption + captions_list = [] of CaptionMetadata return captions_list if caption_tracks.nil? caption_tracks.each do |caption| @@ -25,7 +25,7 @@ module Invidious::Videos language_code = caption["languageCode"].to_s base_url = caption["baseUrl"].to_s - captions_list << Caption.new(name, language_code, base_url) + captions_list << CaptionMetadata.new(name, language_code, base_url) end return captions_list diff --git a/src/invidious/views/user/preferences.ecr b/src/invidious/views/user/preferences.ecr index dfda1434..b1061ee8 100644 --- a/src/invidious/views/user/preferences.ecr +++ b/src/invidious/views/user/preferences.ecr @@ -89,7 +89,7 @@ <% preferences.captions.each_with_index do |caption, index| %> From 8e18d445a7adf9a0c0887249003a7b84f0fb95af Mon Sep 17 00:00:00 2001 From: syeopite Date: Sun, 23 Jul 2023 01:52:53 -0700 Subject: [PATCH 031/143] Add method to generate params for transcripts api --- src/invidious/videos/transcript.cr | 34 ++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 src/invidious/videos/transcript.cr diff --git a/src/invidious/videos/transcript.cr b/src/invidious/videos/transcript.cr new file mode 100644 index 00000000..c50f7569 --- /dev/null +++ b/src/invidious/videos/transcript.cr @@ -0,0 +1,34 @@ +module Invidious::Videos + # Namespace for methods primarily relating to Transcripts + module Transcript + def self.generate_param(video_id : String, language_code : String, auto_generated : Bool) : String + if !auto_generated + is_auto_generated = "" + elsif is_auto_generated = "asr" + end + + object = { + "1:0:string" => video_id, + + "2:base64" => { + "1:string" => is_auto_generated, + "2:string" => language_code, + "3:string" => "", + }, + + "3:varint" => 1_i64, + "5:string" => "engagement-panel-searchable-transcript-search-panel", + "6:varint" => 1_i64, + "7:varint" => 1_i64, + "8:varint" => 1_i64, + } + + params = object.try { |i| Protodec::Any.cast_json(i) } + .try { |i| Protodec::Any.from_json(i) } + .try { |i| Base64.urlsafe_encode(i) } + .try { |i| URI.encode_www_form(i) } + + return params + end + end +end From 4b3ac1a757a5ee14919e83a84de31a3d0bd14a4c Mon Sep 17 00:00:00 2001 From: syeopite Date: Sun, 23 Jul 2023 03:22:19 -0700 Subject: [PATCH 032/143] Add method to parse transcript JSON into structs --- src/invidious/videos/transcript.cr | 37 ++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/src/invidious/videos/transcript.cr b/src/invidious/videos/transcript.cr index c50f7569..0d8b0b25 100644 --- a/src/invidious/videos/transcript.cr +++ b/src/invidious/videos/transcript.cr @@ -1,6 +1,8 @@ module Invidious::Videos # Namespace for methods primarily relating to Transcripts module Transcript + record TranscriptLine, start_ms : Time::Span, end_ms : Time::Span, line : String + def self.generate_param(video_id : String, language_code : String, auto_generated : Bool) : String if !auto_generated is_auto_generated = "" @@ -30,5 +32,40 @@ module Invidious::Videos return params end + + def self.convert_transcripts_to_vtt(initial_data : JSON::Any, target_language : String) : String + # Convert into TranscriptLine + + vtt = String.build do |vtt| + result << <<-END_VTT + WEBVTT + Kind: captions + Language: #{tlang} + + + END_VTT + + vtt << "\n\n" + end + end + + def self.parse(initial_data : Hash(String, JSON::Any)) + body = initial_data.dig("actions", 0, "updateEngagementPanelAction", "content", "transcriptRenderer", + "content", "transcriptSearchPanelRenderer", "body", "transcriptSegmentListRenderer", + "initialSegments").as_a + + lines = [] of TranscriptLine + body.each do |line| + line = line["transcriptSegmentRenderer"] + start_ms = line["startMs"].as_s.to_i.millisecond + end_ms = line["endMs"].as_s.to_i.millisecond + + text = extract_text(line["snippet"]) || "" + + lines << TranscriptLine.new(start_ms, end_ms, text) + end + + return lines + end end end From caac7e21668dd88eaf3d57ddc300427885af0a23 Mon Sep 17 00:00:00 2001 From: syeopite Date: Sun, 23 Jul 2023 03:52:26 -0700 Subject: [PATCH 033/143] Add method to convert transcripts response to vtt --- src/invidious/videos/transcript.cr | 39 ++++++++++++++++++++++++++---- 1 file changed, 34 insertions(+), 5 deletions(-) diff --git a/src/invidious/videos/transcript.cr b/src/invidious/videos/transcript.cr index 0d8b0b25..ec990883 100644 --- a/src/invidious/videos/transcript.cr +++ b/src/invidious/videos/transcript.cr @@ -33,23 +33,52 @@ module Invidious::Videos return params end - def self.convert_transcripts_to_vtt(initial_data : JSON::Any, target_language : String) : String - # Convert into TranscriptLine + def self.convert_transcripts_to_vtt(initial_data : Hash(String, JSON::Any), target_language : String) : String + # Convert into array of TranscriptLine + lines = self.parse(initial_data) + # Taken from Invidious::Videos::CaptionMetadata.timedtext_to_vtt() vtt = String.build do |vtt| - result << <<-END_VTT + vtt << <<-END_VTT WEBVTT Kind: captions - Language: #{tlang} + Language: #{target_language} END_VTT vtt << "\n\n" + + lines.each do |line| + start_time = line.start_ms + end_time = line.end_ms + + # start_time + vtt << start_time.hours.to_s.rjust(2, '0') + vtt << ':' << start_time.minutes.to_s.rjust(2, '0') + vtt << ':' << start_time.seconds.to_s.rjust(2, '0') + vtt << '.' << start_time.milliseconds.to_s.rjust(3, '0') + + vtt << " --> " + + # end_time + vtt << end_time.hours.to_s.rjust(2, '0') + vtt << ':' << end_time.minutes.to_s.rjust(2, '0') + vtt << ':' << end_time.seconds.to_s.rjust(2, '0') + vtt << '.' << end_time.milliseconds.to_s.rjust(3, '0') + + vtt << "\n" + vtt << line.line + + vtt << "\n" + vtt << "\n" + end end + + return vtt end - def self.parse(initial_data : Hash(String, JSON::Any)) + private def self.parse(initial_data : Hash(String, JSON::Any)) body = initial_data.dig("actions", 0, "updateEngagementPanelAction", "content", "transcriptRenderer", "content", "transcriptSearchPanelRenderer", "body", "transcriptSegmentListRenderer", "initialSegments").as_a From e4942b188f5c192d5693687698db9b106571332c Mon Sep 17 00:00:00 2001 From: syeopite Date: Sun, 23 Jul 2023 05:02:02 -0700 Subject: [PATCH 034/143] Integrate transcript captions into captions API --- config/config.example.yml | 13 +++ src/invidious/config.cr | 3 + src/invidious/routes/api/v1/videos.cr | 112 ++++++++++++++------------ src/invidious/videos/caption.cr | 11 ++- src/invidious/videos/transcript.cr | 6 ++ 5 files changed, 91 insertions(+), 54 deletions(-) diff --git a/config/config.example.yml b/config/config.example.yml index 34070fe5..51beab89 100644 --- a/config/config.example.yml +++ b/config/config.example.yml @@ -182,6 +182,19 @@ https_only: false #force_resolve: +## +## Use Innertube's transcripts API instead of timedtext for closed captions +## +## Useful for larger instances as InnerTube is **not ratelimited**. See https://github.com/iv-org/invidious/issues/2567 +## +## Subtitle experience may differ slightly on Invidious. +## +## Accepted values: true, false +## Default: false +## +# use_innertube_for_captions: false + + # ----------------------------- # Logging # ----------------------------- diff --git a/src/invidious/config.cr b/src/invidious/config.cr index e5f1e822..c88a4837 100644 --- a/src/invidious/config.cr +++ b/src/invidious/config.cr @@ -129,6 +129,9 @@ class Config # Use quic transport for youtube api property use_quic : Bool = false + # Use Innertube's transcripts API instead of timedtext for closed captions + property use_innertube_for_captions : Bool = false + # Saved cookies in "name1=value1; name2=value2..." format @[YAML::Field(converter: Preferences::StringToCookies)] property cookies : HTTP::Cookies = HTTP::Cookies.new diff --git a/src/invidious/routes/api/v1/videos.cr b/src/invidious/routes/api/v1/videos.cr index af4fc806..000e64b9 100644 --- a/src/invidious/routes/api/v1/videos.cr +++ b/src/invidious/routes/api/v1/videos.cr @@ -87,70 +87,78 @@ module Invidious::Routes::API::V1::Videos caption = caption[0] end - url = URI.parse("#{caption.base_url}&tlang=#{tlang}").request_target + if CONFIG.use_innertube_for_captions + params = Invidious::Videos::Transcript.generate_param(id, caption.language_code, caption.auto_generated) + initial_data = YoutubeAPI.transcript(params.to_s) - # Auto-generated captions often have cues that aren't aligned properly with the video, - # as well as some other markup that makes it cumbersome, so we try to fix that here - if caption.name.includes? "auto-generated" - caption_xml = YT_POOL.client &.get(url).body + webvtt = Invidious::Videos::Transcript.convert_transcripts_to_vtt(initial_data, caption.language_code) + else + # Timedtext API handling + url = URI.parse("#{caption.base_url}&tlang=#{tlang}").request_target - if caption_xml.starts_with?(" i + 1 - end_time = caption_nodes[i + 1]["start"].to_f.seconds - else - end_time = start_time + duration + if caption_nodes.size > i + 1 + end_time = caption_nodes[i + 1]["start"].to_f.seconds + else + end_time = start_time + duration + end + + start_time = "#{start_time.hours.to_s.rjust(2, '0')}:#{start_time.minutes.to_s.rjust(2, '0')}:#{start_time.seconds.to_s.rjust(2, '0')}.#{start_time.milliseconds.to_s.rjust(3, '0')}" + end_time = "#{end_time.hours.to_s.rjust(2, '0')}:#{end_time.minutes.to_s.rjust(2, '0')}:#{end_time.seconds.to_s.rjust(2, '0')}.#{end_time.milliseconds.to_s.rjust(3, '0')}" + + text = HTML.unescape(node.content) + text = text.gsub(//, "") + text = text.gsub(/<\/font>/, "") + if md = text.match(/(?.*) : (?.*)/) + text = "#{md["text"]}" + end + + str << <<-END_CUE + #{start_time} --> #{end_time} + #{text} + + + END_CUE end - - start_time = "#{start_time.hours.to_s.rjust(2, '0')}:#{start_time.minutes.to_s.rjust(2, '0')}:#{start_time.seconds.to_s.rjust(2, '0')}.#{start_time.milliseconds.to_s.rjust(3, '0')}" - end_time = "#{end_time.hours.to_s.rjust(2, '0')}:#{end_time.minutes.to_s.rjust(2, '0')}:#{end_time.seconds.to_s.rjust(2, '0')}.#{end_time.milliseconds.to_s.rjust(3, '0')}" - - text = HTML.unescape(node.content) - text = text.gsub(//, "") - text = text.gsub(/<\/font>/, "") - if md = text.match(/(?.*) : (?.*)/) - text = "#{md["text"]}" - end - - str << <<-END_CUE - #{start_time} --> #{end_time} - #{text} - - - END_CUE end end - end - else - # Some captions have "align:[start/end]" and "position:[num]%" - # attributes. Those are causing issues with VideoJS, which is unable - # to properly align the captions on the video, so we remove them. - # - # See: https://github.com/iv-org/invidious/issues/2391 - webvtt = YT_POOL.client &.get("#{url}&format=vtt").body - if webvtt.starts_with?(" [0-9:.]{12}).+/, "\\1") + if webvtt.starts_with?(" [0-9:.]{12}).+/, "\\1") + end end end diff --git a/src/invidious/videos/caption.cr b/src/invidious/videos/caption.cr index c85b46c3..1e2abde9 100644 --- a/src/invidious/videos/caption.cr +++ b/src/invidious/videos/caption.cr @@ -6,7 +6,9 @@ module Invidious::Videos property language_code : String property base_url : String - def initialize(@name, @language_code, @base_url) + property auto_generated : Bool + + def initialize(@name, @language_code, @base_url, @auto_generated) end # Parse the JSON structure from Youtube @@ -25,7 +27,12 @@ module Invidious::Videos language_code = caption["languageCode"].to_s base_url = caption["baseUrl"].to_s - captions_list << CaptionMetadata.new(name, language_code, base_url) + auto_generated = false + if caption["kind"]? && caption["kind"] == "asr" + auto_generated = true + end + + captions_list << CaptionMetadata.new(name, language_code, base_url, auto_generated) end return captions_list diff --git a/src/invidious/videos/transcript.cr b/src/invidious/videos/transcript.cr index ec990883..ba2728cd 100644 --- a/src/invidious/videos/transcript.cr +++ b/src/invidious/videos/transcript.cr @@ -85,7 +85,13 @@ module Invidious::Videos lines = [] of TranscriptLine body.each do |line| + # Transcript section headers. They are not apart of the captions and as such we can safely skip them. + if line.as_h.has_key?("transcriptSectionHeaderRenderer") + next + end + line = line["transcriptSegmentRenderer"] + start_ms = line["startMs"].as_s.to_i.millisecond end_ms = line["endMs"].as_s.to_i.millisecond From 3509752b791b12bcf20e12656e3b871e5034b1a7 Mon Sep 17 00:00:00 2001 From: syeopite Date: Sun, 23 Jul 2023 16:50:40 -0700 Subject: [PATCH 035/143] Rename transcript() to get_transcript() in YT API --- src/invidious/routes/api/v1/videos.cr | 2 +- src/invidious/yt_backend/youtube_api.cr | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/invidious/routes/api/v1/videos.cr b/src/invidious/routes/api/v1/videos.cr index 000e64b9..25e766d2 100644 --- a/src/invidious/routes/api/v1/videos.cr +++ b/src/invidious/routes/api/v1/videos.cr @@ -89,7 +89,7 @@ module Invidious::Routes::API::V1::Videos if CONFIG.use_innertube_for_captions params = Invidious::Videos::Transcript.generate_param(id, caption.language_code, caption.auto_generated) - initial_data = YoutubeAPI.transcript(params.to_s) + initial_data = YoutubeAPI.get_transcript(params) webvtt = Invidious::Videos::Transcript.convert_transcripts_to_vtt(initial_data, caption.language_code) else diff --git a/src/invidious/yt_backend/youtube_api.cr b/src/invidious/yt_backend/youtube_api.cr index f8aca04d..a3335bbf 100644 --- a/src/invidious/yt_backend/youtube_api.cr +++ b/src/invidious/yt_backend/youtube_api.cr @@ -558,7 +558,7 @@ module YoutubeAPI end #################################################################### - # transcript(params) + # get_transcript(params, client_config?) # # Requests the youtubei/v1/get_transcript endpoint with the required headers # and POST data in order to get a JSON reply. @@ -569,7 +569,7 @@ module YoutubeAPI # `struct ClientConfig` above for more details). # - def transcript( + def get_transcript( params : String, client_config : ClientConfig | Nil = nil ) : Hash(String, JSON::Any) From c5fe96e93603db58d6767928eedc658e8b58e59f Mon Sep 17 00:00:00 2001 From: syeopite Date: Wed, 26 Jul 2023 07:19:12 -0700 Subject: [PATCH 036/143] Remove lsquic from codebase --- config/config.example.yml | 21 --- shard.lock | 4 - shard.yml | 3 - src/invidious.cr | 2 +- src/invidious/config.cr | 2 - src/invidious/routes/images.cr | 142 +++----------------- src/invidious/yt_backend/connection_pool.cr | 37 +---- src/invidious/yt_backend/youtube_api.cr | 14 +- 8 files changed, 32 insertions(+), 193 deletions(-) diff --git a/config/config.example.yml b/config/config.example.yml index 34070fe5..e925a5e3 100644 --- a/config/config.example.yml +++ b/config/config.example.yml @@ -140,27 +140,6 @@ https_only: false ## #pool_size: 100 -## -## Enable/Disable the use of QUIC (HTTP/3) when connecting -## to the youtube API and websites ('youtube.com', 'ytimg.com'). -## QUIC's main advantages are its lower latency and lower bandwidth -## use, compared to its predecessors. However, the current version -## of QUIC used in invidious is still based on the IETF draft 31, -## meaning that the underlying library may still not be fully -## optimized. You can read more about QUIC at the link below: -## https://datatracker.ietf.org/doc/html/draft-ietf-quic-transport-31 -## -## Note: you should try both options and see what is the best for your -## instance. In general QUIC is recommended for public instances. Your -## mileage may vary. -## -## Note 2: Using QUIC prevents some captcha challenges from appearing. -## See: https://github.com/iv-org/invidious/issues/957#issuecomment-576424042 -## -## Accepted values: true, false -## Default: false -## -#use_quic: false ## ## Additional cookies to be sent when requesting the youtube API. diff --git a/shard.lock b/shard.lock index 235e4c25..55fcfe46 100644 --- a/shard.lock +++ b/shard.lock @@ -24,10 +24,6 @@ shards: git: https://github.com/jeromegn/kilt.git version: 0.6.1 - lsquic: - git: https://github.com/iv-org/lsquic.cr.git - version: 2.18.1-2 - pg: git: https://github.com/will/crystal-pg.git version: 0.24.0 diff --git a/shard.yml b/shard.yml index 7ee0bb2a..e929160d 100644 --- a/shard.yml +++ b/shard.yml @@ -25,9 +25,6 @@ dependencies: protodec: github: iv-org/protodec version: ~> 0.1.5 - lsquic: - github: iv-org/lsquic.cr - version: ~> 2.18.1-2 athena-negotiation: github: athena-framework/negotiation version: ~> 0.1.1 diff --git a/src/invidious.cr b/src/invidious.cr index 84e1895d..e0bd0101 100644 --- a/src/invidious.cr +++ b/src/invidious.cr @@ -90,7 +90,7 @@ SOFTWARE = { "branch" => "#{CURRENT_BRANCH}", } -YT_POOL = YoutubeConnectionPool.new(YT_URL, capacity: CONFIG.pool_size, use_quic: CONFIG.use_quic) +YT_POOL = YoutubeConnectionPool.new(YT_URL, capacity: CONFIG.pool_size) # CLI Kemal.config.extra_options do |parser| diff --git a/src/invidious/config.cr b/src/invidious/config.cr index e5f1e822..cee33ce1 100644 --- a/src/invidious/config.cr +++ b/src/invidious/config.cr @@ -126,8 +126,6 @@ class Config property host_binding : String = "0.0.0.0" # Pool size for HTTP requests to youtube.com and ytimg.com (each domain has a separate pool of `pool_size`) property pool_size : Int32 = 100 - # Use quic transport for youtube api - property use_quic : Bool = false # Saved cookies in "name1=value1; name2=value2..." format @[YAML::Field(converter: Preferences::StringToCookies)] diff --git a/src/invidious/routes/images.cr b/src/invidious/routes/images.cr index 594a7869..b6a2e110 100644 --- a/src/invidious/routes/images.cr +++ b/src/invidious/routes/images.cr @@ -3,17 +3,7 @@ module Invidious::Routes::Images def self.ggpht(env) url = env.request.path.lchop("/ggpht") - headers = ( - {% unless flag?(:disable_quic) %} - if CONFIG.use_quic - HTTP::Headers{":authority" => "yt3.ggpht.com"} - else - HTTP::Headers.new - end - {% else %} - HTTP::Headers.new - {% end %} - ) + headers = HTTP::Headers.new REQUEST_HEADERS_WHITELIST.each do |header| if env.request.headers[header]? @@ -42,22 +32,9 @@ module Invidious::Routes::Images } begin - {% unless flag?(:disable_quic) %} - if CONFIG.use_quic - YT_POOL.client &.get(url, headers) do |resp| - return request_proc.call(resp) - end - else - HTTP::Client.get("https://yt3.ggpht.com#{url}") do |resp| - return request_proc.call(resp) - end - end - {% else %} - # This can likely be optimized into a (small) pool sometime in the future. - HTTP::Client.get("https://yt3.ggpht.com#{url}") do |resp| - return request_proc.call(resp) - end - {% end %} + HTTP::Client.get("https://yt3.ggpht.com#{url}") do |resp| + return request_proc.call(resp) + end rescue ex end end @@ -78,10 +55,6 @@ module Invidious::Routes::Images headers = HTTP::Headers.new - {% unless flag?(:disable_quic) %} - headers[":authority"] = "#{authority}.ytimg.com" - {% end %} - REQUEST_HEADERS_WHITELIST.each do |header| if env.request.headers[header]? headers[header] = env.request.headers[header] @@ -107,22 +80,9 @@ module Invidious::Routes::Images } begin - {% unless flag?(:disable_quic) %} - if CONFIG.use_quic - YT_POOL.client &.get(url, headers) do |resp| - return request_proc.call(resp) - end - else - HTTP::Client.get("https://#{authority}.ytimg.com#{url}") do |resp| - return request_proc.call(resp) - end - end - {% else %} - # This can likely be optimized into a (small) pool sometime in the future. - HTTP::Client.get("https://#{authority}.ytimg.com#{url}") do |resp| - return request_proc.call(resp) - end - {% end %} + HTTP::Client.get("https://#{authority}.ytimg.com#{url}") do |resp| + return request_proc.call(resp) + end rescue ex end end @@ -133,17 +93,7 @@ module Invidious::Routes::Images name = env.params.url["name"] url = env.request.resource - headers = ( - {% unless flag?(:disable_quic) %} - if CONFIG.use_quic - HTTP::Headers{":authority" => "i9.ytimg.com"} - else - HTTP::Headers.new - end - {% else %} - HTTP::Headers.new - {% end %} - ) + headers = HTTP::Headers.new REQUEST_HEADERS_WHITELIST.each do |header| if env.request.headers[header]? @@ -169,22 +119,9 @@ module Invidious::Routes::Images } begin - {% unless flag?(:disable_quic) %} - if CONFIG.use_quic - YT_POOL.client &.get(url, headers) do |resp| - return request_proc.call(resp) - end - else - HTTP::Client.get("https://i9.ytimg.com#{url}") do |resp| - return request_proc.call(resp) - end - end - {% else %} - # This can likely be optimized into a (small) pool sometime in the future. - HTTP::Client.get("https://i9.ytimg.com#{url}") do |resp| - return request_proc.call(resp) - end - {% end %} + HTTP::Client.get("https://i9.ytimg.com#{url}") do |resp| + return request_proc.call(resp) + end rescue ex end end @@ -223,41 +160,16 @@ module Invidious::Routes::Images id = env.params.url["id"] name = env.params.url["name"] - headers = ( - {% unless flag?(:disable_quic) %} - if CONFIG.use_quic - HTTP::Headers{":authority" => "i.ytimg.com"} - else - HTTP::Headers.new - end - {% else %} - HTTP::Headers.new - {% end %} - ) + headers = HTTP::Headers.new if name == "maxres.jpg" build_thumbnails(id).each do |thumb| thumbnail_resource_path = "/vi/#{id}/#{thumb[:url]}.jpg" - # Logic here is short enough that manually typing them out should be fine. - {% unless flag?(:disable_quic) %} - if CONFIG.use_quic - if YT_POOL.client &.head(thumbnail_resource_path, headers).status_code == 200 - name = thumb[:url] + ".jpg" - break - end - else - if HTTP::Client.head("https://i.ytimg.com#{thumbnail_resource_path}").status_code == 200 - name = thumb[:url] + ".jpg" - break - end - end - {% else %} - # This can likely be optimized into a (small) pool sometime in the future. - if HTTP::Client.head("https://i.ytimg.com#{thumbnail_resource_path}").status_code == 200 - name = thumb[:url] + ".jpg" - break - end - {% end %} + # This can likely be optimized into a (small) pool sometime in the future. + if HTTP::Client.head("https://i.ytimg.com#{thumbnail_resource_path}").status_code == 200 + name = thumb[:url] + ".jpg" + break + end end end @@ -287,22 +199,10 @@ module Invidious::Routes::Images } begin - {% unless flag?(:disable_quic) %} - if CONFIG.use_quic - YT_POOL.client &.get(url, headers) do |resp| - return request_proc.call(resp) - end - else - HTTP::Client.get("https://i.ytimg.com#{url}") do |resp| - return request_proc.call(resp) - end - end - {% else %} - # This can likely be optimized into a (small) pool sometime in the future. - HTTP::Client.get("https://i.ytimg.com#{url}") do |resp| - return request_proc.call(resp) - end - {% end %} + # This can likely be optimized into a (small) pool sometime in the future. + HTTP::Client.get("https://i.ytimg.com#{url}") do |resp| + return request_proc.call(resp) + end rescue ex end end diff --git a/src/invidious/yt_backend/connection_pool.cr b/src/invidious/yt_backend/connection_pool.cr index 658731cf..e9eb726c 100644 --- a/src/invidious/yt_backend/connection_pool.cr +++ b/src/invidious/yt_backend/connection_pool.cr @@ -1,11 +1,3 @@ -{% unless flag?(:disable_quic) %} - require "lsquic" - - alias HTTPClientType = QUIC::Client | HTTP::Client -{% else %} - alias HTTPClientType = HTTP::Client -{% end %} - def add_yt_headers(request) if request.headers["User-Agent"] == "Crystal" request.headers["User-Agent"] ||= "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36" @@ -26,11 +18,11 @@ struct YoutubeConnectionPool property! url : URI property! capacity : Int32 property! timeout : Float64 - property pool : DB::Pool(HTTPClientType) + property pool : DB::Pool(HTTP::Client) - def initialize(url : URI, @capacity = 5, @timeout = 5.0, use_quic = true) + def initialize(url : URI, @capacity = 5, @timeout = 5.0) @url = url - @pool = build_pool(use_quic) + @pool = build_pool() end def client(region = nil, &block) @@ -43,11 +35,7 @@ struct YoutubeConnectionPool response = yield conn rescue ex conn.close - {% unless flag?(:disable_quic) %} - conn = CONFIG.use_quic ? QUIC::Client.new(url) : HTTP::Client.new(url) - {% else %} - conn = HTTP::Client.new(url) - {% end %} + conn = HTTP::Client.new(url) conn.family = (url.host == "www.youtube.com") ? CONFIG.force_resolve : Socket::Family::INET conn.family = Socket::Family::INET if conn.family == Socket::Family::UNSPEC @@ -61,19 +49,9 @@ struct YoutubeConnectionPool response end - private def build_pool(use_quic) - DB::Pool(HTTPClientType).new(initial_pool_size: 0, max_pool_size: capacity, max_idle_pool_size: capacity, checkout_timeout: timeout) do - conn = nil # Declare - {% unless flag?(:disable_quic) %} - if use_quic - conn = QUIC::Client.new(url) - else - conn = HTTP::Client.new(url) - end - {% else %} - conn = HTTP::Client.new(url) - {% end %} - + private def build_pool + DB::Pool(HTTP::Client).new(initial_pool_size: 0, max_pool_size: capacity, max_idle_pool_size: capacity, checkout_timeout: timeout) do + conn = HTTP::Client.new(url) conn.family = (url.host == "www.youtube.com") ? CONFIG.force_resolve : Socket::Family::INET conn.family = Socket::Family::INET if conn.family == Socket::Family::UNSPEC conn.before_request { |r| add_yt_headers(r) } if url.host == "www.youtube.com" @@ -83,7 +61,6 @@ struct YoutubeConnectionPool end def make_client(url : URI, region = nil) - # TODO: Migrate any applicable endpoints to QUIC client = HTTPClient.new(url, OpenSSL::SSL::Context::Client.insecure) client.family = (url.host == "www.youtube.com") ? CONFIG.force_resolve : Socket::Family::UNSPEC client.before_request { |r| add_yt_headers(r) } if url.host == "www.youtube.com" diff --git a/src/invidious/yt_backend/youtube_api.cr b/src/invidious/yt_backend/youtube_api.cr index 3dd9e9d8..aef9ddd9 100644 --- a/src/invidious/yt_backend/youtube_api.cr +++ b/src/invidious/yt_backend/youtube_api.cr @@ -595,17 +595,9 @@ module YoutubeAPI LOGGER.trace("YoutubeAPI: POST data: #{data}") # Send the POST request - if {{ !flag?(:disable_quic) }} && CONFIG.use_quic - # Using QUIC client - body = YT_POOL.client(client_config.proxy_region, - &.post(url, headers: headers, body: data.to_json) - ).body - else - # Using HTTP client - body = YT_POOL.client(client_config.proxy_region) do |client| - client.post(url, headers: headers, body: data.to_json) do |response| - self._decompress(response.body_io, response.headers["Content-Encoding"]?) - end + body = YT_POOL.client(client_config.proxy_region) do |client| + client.post(url, headers: headers, body: data.to_json) do |response| + self._decompress(response.body_io, response.headers["Content-Encoding"]?) end end From a8ba02051b261a634050ea7f621451d84ca61607 Mon Sep 17 00:00:00 2001 From: syeopite Date: Wed, 26 Jul 2023 07:25:19 -0700 Subject: [PATCH 037/143] Remove(?) lsquic from make and docker files --- .github/workflows/container-release.yml | 29 ++----------------------- Makefile | 6 ----- docker/Dockerfile | 11 +--------- docker/Dockerfile.arm64 | 11 +--------- 4 files changed, 4 insertions(+), 53 deletions(-) diff --git a/.github/workflows/container-release.yml b/.github/workflows/container-release.yml index 86aec94f..13bbf34c 100644 --- a/.github/workflows/container-release.yml +++ b/.github/workflows/container-release.yml @@ -52,7 +52,7 @@ jobs: username: ${{ secrets.QUAY_USERNAME }} password: ${{ secrets.QUAY_PASSWORD }} - - name: Build and push Docker AMD64 image without QUIC for Push Event + - name: Build and push Docker AMD64 image for Push Event if: github.ref == 'refs/heads/master' uses: docker/build-push-action@v3 with: @@ -64,9 +64,8 @@ jobs: tags: quay.io/invidious/invidious:${{ github.sha }},quay.io/invidious/invidious:latest build-args: | "release=1" - "disable_quic=1" - - name: Build and push Docker ARM64 image without QUIC for Push Event + - name: Build and push Docker ARM64 image for Push Event if: github.ref == 'refs/heads/master' uses: docker/build-push-action@v3 with: @@ -78,28 +77,4 @@ jobs: tags: quay.io/invidious/invidious:${{ github.sha }}-arm64,quay.io/invidious/invidious:latest-arm64 build-args: | "release=1" - "disable_quic=1" - - name: Build and push Docker AMD64 image with QUIC for Push Event - if: github.ref == 'refs/heads/master' - uses: docker/build-push-action@v3 - with: - context: . - file: docker/Dockerfile - platforms: linux/amd64 - labels: quay.expires-after=12w - push: true - tags: quay.io/invidious/invidious:${{ github.sha }}-quic,quay.io/invidious/invidious:latest-quic - build-args: release=1 - - - name: Build and push Docker ARM64 image with QUIC for Push Event - if: github.ref == 'refs/heads/master' - uses: docker/build-push-action@v3 - with: - context: . - file: docker/Dockerfile.arm64 - platforms: linux/arm64/v8 - labels: quay.expires-after=12w - push: true - tags: quay.io/invidious/invidious:${{ github.sha }}-arm64-quic,quay.io/invidious/invidious:latest-arm64-quic - build-args: release=1 diff --git a/Makefile b/Makefile index d4657792..9eb195df 100644 --- a/Makefile +++ b/Makefile @@ -5,7 +5,6 @@ RELEASE := 1 STATIC := 0 -DISABLE_QUIC := 1 NO_DBG_SYMBOLS := 0 @@ -27,10 +26,6 @@ else FLAGS += --debug endif -ifeq ($(DISABLE_QUIC), 1) - FLAGS += -Ddisable_quic -endif - ifeq ($(API_ONLY), 1) FLAGS += -Dapi_only endif @@ -115,7 +110,6 @@ help: @echo " STATIC Link libraries statically (Default: 0)" @echo "" @echo " API_ONLY Build invidious without a GUI (Default: 0)" - @echo " DISABLE_QUIC Disable support for QUIC (Default: 0)" @echo " NO_DBG_SYMBOLS Strip debug symbols (Default: 0)" diff --git a/docker/Dockerfile b/docker/Dockerfile index 57864883..761bbdca 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -2,15 +2,12 @@ FROM crystallang/crystal:1.4.1-alpine AS builder RUN apk add --no-cache sqlite-static yaml-static ARG release -ARG disable_quic WORKDIR /invidious COPY ./shard.yml ./shard.yml COPY ./shard.lock ./shard.lock RUN shards install --production -COPY --from=quay.io/invidious/lsquic-compiled /root/liblsquic.a ./lib/lsquic/src/lsquic/ext/liblsquic.a - COPY ./src/ ./src/ # TODO: .git folder is required for building – this is destructive. # See definition of CURRENT_BRANCH, CURRENT_COMMIT and CURRENT_VERSION. @@ -24,13 +21,7 @@ COPY ./videojs-dependencies.yml ./videojs-dependencies.yml RUN crystal spec --warnings all \ --link-flags "-lxml2 -llzma" -RUN if [[ "${release}" == 1 && "${disable_quic}" == 1 ]] ; then \ - crystal build ./src/invidious.cr \ - --release \ - -Ddisable_quic \ - --static --warnings all \ - --link-flags "-lxml2 -llzma"; \ - elif [[ "${release}" == 1 ]] ; then \ +RUN if [[ "${release}" == 1 ]] ; then \ crystal build ./src/invidious.cr \ --release \ --static --warnings all \ diff --git a/docker/Dockerfile.arm64 b/docker/Dockerfile.arm64 index 10135efb..cf9231fb 100644 --- a/docker/Dockerfile.arm64 +++ b/docker/Dockerfile.arm64 @@ -2,15 +2,12 @@ FROM alpine:3.16 AS builder RUN apk add --no-cache 'crystal=1.4.1-r0' shards sqlite-static yaml-static yaml-dev libxml2-dev zlib-static openssl-libs-static openssl-dev musl-dev ARG release -ARG disable_quic WORKDIR /invidious COPY ./shard.yml ./shard.yml COPY ./shard.lock ./shard.lock RUN shards install --production -COPY --from=quay.io/invidious/lsquic-compiled /root/liblsquic.a ./lib/lsquic/src/lsquic/ext/liblsquic.a - COPY ./src/ ./src/ # TODO: .git folder is required for building – this is destructive. # See definition of CURRENT_BRANCH, CURRENT_COMMIT and CURRENT_VERSION. @@ -24,13 +21,7 @@ COPY ./videojs-dependencies.yml ./videojs-dependencies.yml RUN crystal spec --warnings all \ --link-flags "-lxml2 -llzma" -RUN if [[ "${release}" == 1 && "${disable_quic}" == 1 ]] ; then \ - crystal build ./src/invidious.cr \ - --release \ - -Ddisable_quic \ - --static --warnings all \ - --link-flags "-lxml2 -llzma"; \ - elif [[ "${release}" == 1 ]] ; then \ +RUN if [[ "${release}" == 1 ]] ; then \ crystal build ./src/invidious.cr \ --release \ --static --warnings all \ From 70b80ce8ad5ad9e5eb57a8f2f8e72a2274f8523f Mon Sep 17 00:00:00 2001 From: Samantaz Fox Date: Fri, 28 Jul 2023 08:11:15 +0200 Subject: [PATCH 038/143] I18n: Add translation strings for new feature (fr/en) --- locales/en-US.json | 2 ++ locales/fr.json | 2 ++ 2 files changed, 4 insertions(+) diff --git a/locales/en-US.json b/locales/en-US.json index 74f43d90..06d095dc 100644 --- a/locales/en-US.json +++ b/locales/en-US.json @@ -1,4 +1,6 @@ { + "generic_channels_count": "{{count}} channel", + "generic_channels_count_plural": "{{count}} channels", "generic_views_count": "{{count}} view", "generic_views_count_plural": "{{count}} views", "generic_videos_count": "{{count}} video", diff --git a/locales/fr.json b/locales/fr.json index 2eb4dd2b..c48c8be5 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -1,4 +1,6 @@ { + "generic_channels_count": "{{count}} chaîne", + "generic_channels_count_plural": "{{count}} chaînes", "generic_views_count": "{{count}} vue", "generic_views_count_plural": "{{count}} vues", "generic_videos_count": "{{count}} vidéo", From 0d27eef047d24f8c7b3f9528502bc5828cad3c73 Mon Sep 17 00:00:00 2001 From: Fabio Henrique Date: Sun, 6 Aug 2023 12:29:19 +0000 Subject: [PATCH 039/143] update ameba version fix shard.yml authors typo --- shard.lock | 7 ++++--- shard.yml | 4 ++-- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/shard.lock b/shard.lock index 55fcfe46..efb60a59 100644 --- a/shard.lock +++ b/shard.lock @@ -1,5 +1,9 @@ version: 2.0 shards: + ameba: + git: https://github.com/crystal-ameba/ameba.git + version: 1.5.0 + athena-negotiation: git: https://github.com/athena-framework/negotiation.git version: 0.1.1 @@ -44,6 +48,3 @@ shards: git: https://github.com/crystal-lang/crystal-sqlite3.git version: 0.18.0 - ameba: - git: https://github.com/crystal-ameba/ameba.git - version: 0.14.3 diff --git a/shard.yml b/shard.yml index e929160d..be06a7df 100644 --- a/shard.yml +++ b/shard.yml @@ -3,7 +3,7 @@ version: 0.20.1 authors: - Omar Roth - - Invidous team + - Invidious team targets: invidious: @@ -35,7 +35,7 @@ development_dependencies: version: ~> 0.10.4 ameba: github: crystal-ameba/ameba - version: ~> 0.14.3 + version: ~> 1.5.0 crystal: ">= 1.0.0, < 2.0.0" From 2f6b2688bb8042c29942e46767dc78836f21fb57 Mon Sep 17 00:00:00 2001 From: syeopite Date: Sun, 6 Aug 2023 12:20:05 -0700 Subject: [PATCH 040/143] Use workaround for fetching streaming URLs YouTube appears to be A/B testing some new integrity checks. Adding the parameter "CgIQBg" to InnerTube player requests appears to workaround the problem See https://github.com/TeamNewPipe/NewPipeExtractor/pull/1084 --- src/invidious/videos/parser.cr | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/invidious/videos/parser.cr b/src/invidious/videos/parser.cr index 9cc0ffdc..2a09d187 100644 --- a/src/invidious/videos/parser.cr +++ b/src/invidious/videos/parser.cr @@ -55,8 +55,9 @@ def extract_video_info(video_id : String, proxy_region : String? = nil) client_config = YoutubeAPI::ClientConfig.new(proxy_region: proxy_region) # Fetch data from the player endpoint - # 8AEB param is used to fetch YouTube stories - player_response = YoutubeAPI.player(video_id: video_id, params: "8AEB", client_config: client_config) + # CgIQBg is a workaround for streaming URLs that returns a 403. + # See https://github.com/iv-org/invidious/issues/4027#issuecomment-1666944520 + player_response = YoutubeAPI.player(video_id: video_id, params: "CgIQBg", client_config: client_config) playability_status = player_response.dig?("playabilityStatus", "status").try &.as_s @@ -135,8 +136,9 @@ end def try_fetch_streaming_data(id : String, client_config : YoutubeAPI::ClientConfig) : Hash(String, JSON::Any)? LOGGER.debug("try_fetch_streaming_data: [#{id}] Using #{client_config.client_type} client.") - # 8AEB param is used to fetch YouTube stories - response = YoutubeAPI.player(video_id: id, params: "8AEB", client_config: client_config) + # CgIQBg is a workaround for streaming URLs that returns a 403. + # See https://github.com/iv-org/invidious/issues/4027#issuecomment-1666944520 + response = YoutubeAPI.player(video_id: id, params: "CgIQBg", client_config: client_config) playability_status = response["playabilityStatus"]["status"] LOGGER.debug("try_fetch_streaming_data: [#{id}] Got playabilityStatus == #{playability_status}.") From 71693ba6063c06efd1b9780313246b8dbc020f72 Mon Sep 17 00:00:00 2001 From: atilluF <110931720+atilluF@users.noreply.github.com> Date: Mon, 10 Jul 2023 17:50:47 +0000 Subject: [PATCH 041/143] Update Italian translation --- locales/it.json | 75 +++++++++++++++++++++++++++++-------------------- 1 file changed, 45 insertions(+), 30 deletions(-) diff --git a/locales/it.json b/locales/it.json index a3d0f5da..9d633264 100644 --- a/locales/it.json +++ b/locales/it.json @@ -1,10 +1,13 @@ { - "generic_subscribers_count": "{{count}} iscritto", - "generic_subscribers_count_plural": "{{count}} iscritti", - "generic_videos_count": "{{count}} video", - "generic_videos_count_plural": "{{count}} video", - "generic_playlists_count": "{{count}} playlist", - "generic_playlists_count_plural": "{{count}} playlist", + "generic_subscribers_count_0": "{{count}} iscritto", + "generic_subscribers_count_1": "{{count}} iscritti", + "generic_subscribers_count_2": "{{count}} iscritti", + "generic_videos_count_0": "{{count}} video", + "generic_videos_count_1": "{{count}} video", + "generic_videos_count_2": "{{count}} video", + "generic_playlists_count_0": "{{count}} playlist", + "generic_playlists_count_1": "{{count}} playlist", + "generic_playlists_count_2": "{{count}} playlist", "LIVE": "IN DIRETTA", "Shared `x` ago": "Condiviso `x` fa", "Unsubscribe": "Disiscriviti", @@ -113,16 +116,18 @@ "Subscription manager": "Gestione delle iscrizioni", "Token manager": "Gestione dei gettoni", "Token": "Gettone", - "generic_subscriptions_count": "{{count}} iscrizione", - "generic_subscriptions_count_plural": "{{count}} iscrizioni", + "generic_subscriptions_count_0": "{{count}} iscrizione", + "generic_subscriptions_count_1": "{{count}} iscrizioni", + "generic_subscriptions_count_2": "{{count}} iscrizioni", "tokens_count": "{{count}} gettone", "tokens_count_plural": "{{count}} gettoni", "Import/export": "Importa/esporta", "unsubscribe": "disiscriviti", "revoke": "revoca", "Subscriptions": "Iscrizioni", - "subscriptions_unseen_notifs_count": "{{count}} notifica non visualizzata", - "subscriptions_unseen_notifs_count_plural": "{{count}} notifiche non visualizzate", + "subscriptions_unseen_notifs_count_0": "{{count}} notifica non visualizzata", + "subscriptions_unseen_notifs_count_1": "{{count}} notifiche non visualizzate", + "subscriptions_unseen_notifs_count_2": "{{count}} notifiche non visualizzate", "search": "Cerca", "Log out": "Esci", "Source available here.": "Codice sorgente.", @@ -151,8 +156,9 @@ "Whitelisted regions: ": "Regioni in lista bianca: ", "Blacklisted regions: ": "Regioni in lista nera: ", "Shared `x`": "Condiviso `x`", - "generic_views_count": "{{count}} visualizzazione", - "generic_views_count_plural": "{{count}} visualizzazioni", + "generic_views_count_0": "{{count}} visualizzazione", + "generic_views_count_1": "{{count}} visualizzazioni", + "generic_views_count_2": "{{count}} visualizzazioni", "Premieres in `x`": "In anteprima in `x`", "Premieres `x`": "In anteprima `x`", "Hi! Looks like you have JavaScript turned off. Click here to view comments, keep in mind they may take a bit longer to load.": "Ciao, Sembra che tu abbia disattivato JavaScript. Clicca qui per visualizzare i commenti, ma considera che il caricamento potrebbe richiedere più tempo.", @@ -300,20 +306,27 @@ "Yiddish": "Yiddish", "Yoruba": "Yoruba", "Zulu": "Zulu", - "generic_count_years": "{{count}} anno", - "generic_count_years_plural": "{{count}} anni", - "generic_count_months": "{{count}} mese", - "generic_count_months_plural": "{{count}} mesi", - "generic_count_weeks": "{{count}} settimana", - "generic_count_weeks_plural": "{{count}} settimane", - "generic_count_days": "{{count}} giorno", - "generic_count_days_plural": "{{count}} giorni", - "generic_count_hours": "{{count}} ora", - "generic_count_hours_plural": "{{count}} ore", - "generic_count_minutes": "{{count}} minuto", - "generic_count_minutes_plural": "{{count}} minuti", - "generic_count_seconds": "{{count}} secondo", - "generic_count_seconds_plural": "{{count}} secondi", + "generic_count_years_0": "{{count}} anno", + "generic_count_years_1": "{{count}} anni", + "generic_count_years_2": "{{count}} anni", + "generic_count_months_0": "{{count}} mese", + "generic_count_months_1": "{{count}} mesi", + "generic_count_months_2": "{{count}} mesi", + "generic_count_weeks_0": "{{count}} settimana", + "generic_count_weeks_1": "{{count}} settimane", + "generic_count_weeks_2": "{{count}} settimane", + "generic_count_days_0": "{{count}} giorno", + "generic_count_days_1": "{{count}} giorni", + "generic_count_days_2": "{{count}} giorni", + "generic_count_hours_0": "{{count}} ora", + "generic_count_hours_1": "{{count}} ore", + "generic_count_hours_2": "{{count}} ore", + "generic_count_minutes_0": "{{count}} minuto", + "generic_count_minutes_1": "{{count}} minuti", + "generic_count_minutes_2": "{{count}} minuti", + "generic_count_seconds_0": "{{count}} secondo", + "generic_count_seconds_1": "{{count}} secondi", + "generic_count_seconds_2": "{{count}} secondi", "Fallback comments: ": "Commenti alternativi: ", "Popular": "Popolare", "Search": "Cerca", @@ -417,10 +430,12 @@ "search_filters_duration_option_short": "Corto (< 4 minuti)", "search_filters_duration_option_long": "Lungo (> 20 minuti)", "search_filters_features_option_purchased": "Acquistato", - "comments_view_x_replies": "Vedi {{count}} risposta", - "comments_view_x_replies_plural": "Vedi {{count}} risposte", - "comments_points_count": "{{count}} punto", - "comments_points_count_plural": "{{count}} punti", + "comments_view_x_replies_0": "Vedi {{count}} risposta", + "comments_view_x_replies_1": "Vedi {{count}} risposte", + "comments_view_x_replies_2": "Vedi {{count}} risposte", + "comments_points_count_0": "{{count}} punto", + "comments_points_count_1": "{{count}} punti", + "comments_points_count_2": "{{count}} punti", "Portuguese (auto-generated)": "Portoghese (generati automaticamente)", "crash_page_you_found_a_bug": "Sembra che tu abbia trovato un bug in Invidious!", "crash_page_switch_instance": "provato a usare un'altra istanza", From 0697b3787ff19939fda1bc5c12ada8729dbf960a Mon Sep 17 00:00:00 2001 From: Jorge Maldonado Ventura Date: Sun, 9 Jul 2023 22:14:47 +0000 Subject: [PATCH 042/143] Update Esperanto translation --- locales/eo.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/locales/eo.json b/locales/eo.json index a4b46bef..e2a7b7b1 100644 --- a/locales/eo.json +++ b/locales/eo.json @@ -447,8 +447,8 @@ "French (auto-generated)": "Franca (aŭtomate generita)", "Spanish (Mexico)": "Hispana (Meksiko)", "Spanish (auto-generated)": "Hispana (aŭtomate generita)", - "generic_count_days": "{{count}} jaro", - "generic_count_days_plural": "{{count}} jaroj", + "generic_count_days": "{{count}} tago", + "generic_count_days_plural": "{{count}} tagoj", "search_filters_type_option_all": "Ajna speco", "search_filters_duration_option_none": "Ajna daŭro", "search_filters_apply_button": "Uzi elektitajn filtrilojn", From cb09f46e04c91a0e02073228dc720c572b69aad1 Mon Sep 17 00:00:00 2001 From: CRW Date: Thu, 13 Jul 2023 14:10:15 +0200 Subject: [PATCH 043/143] Add Latin translation --- locales/la.json | 1 + 1 file changed, 1 insertion(+) create mode 100644 locales/la.json diff --git a/locales/la.json b/locales/la.json new file mode 100644 index 00000000..0967ef42 --- /dev/null +++ b/locales/la.json @@ -0,0 +1 @@ +{} From 1837467aeb77d57c57f5e7ccf81693d61d7c2d69 Mon Sep 17 00:00:00 2001 From: maboroshin Date: Thu, 13 Jul 2023 00:17:04 +0000 Subject: [PATCH 044/143] Update Japanese translation --- locales/ja.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/locales/ja.json b/locales/ja.json index 8adcbf6a..b489ece0 100644 --- a/locales/ja.json +++ b/locales/ja.json @@ -366,13 +366,13 @@ "next_steps_error_message": "下記のものを試して下さい: ", "next_steps_error_message_refresh": "再読込", "next_steps_error_message_go_to_youtube": "YouTubeへ", - "search_filters_duration_option_short": "4 分未満", + "search_filters_duration_option_short": "4分未満", "footer_documentation": "説明書", "footer_source_code": "ソースコード", "footer_original_source_code": "元のソースコード", "footer_modfied_source_code": "改変して使用", "adminprefs_modified_source_code_url_label": "改変されたソースコードのレポジトリのURL", - "search_filters_duration_option_long": "20 分以上", + "search_filters_duration_option_long": "20分以上", "preferences_region_label": "地域: ", "footer_donate_page": "寄付する", "preferences_quality_dash_label": "優先するDASH画質: ", @@ -443,7 +443,7 @@ "search_filters_date_option_none": "すべて", "search_filters_type_option_all": "すべての種類", "search_filters_duration_option_none": "すべての長さ", - "search_filters_duration_option_medium": "4 ~ 20 分", + "search_filters_duration_option_medium": "4 ~ 20分", "preferences_save_player_pos_label": "再生位置を保存: ", "crash_page_before_reporting": "バグを報告する前に、次のことを確認してください。", "crash_page_report_issue": "上記が助けにならないなら、GitHub に新しい issue を作成し(英語が好ましい)、メッセージに次のテキストを含めてください(テキストは翻訳しない)。", From ab475718c8b2c3fb87cf718e39cfcab3b21312ef Mon Sep 17 00:00:00 2001 From: Eryk Michalak Date: Sat, 15 Jul 2023 08:33:40 +0000 Subject: [PATCH 045/143] Update Polish translation --- locales/pl.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/locales/pl.json b/locales/pl.json index e237db8b..6337465b 100644 --- a/locales/pl.json +++ b/locales/pl.json @@ -148,12 +148,12 @@ "Blacklisted regions: ": "Niedostępny na obszarach: ", "Shared `x`": "Udostępniono `x`", "Premieres in `x`": "Publikacja za `x`", - "Premieres `x`": "Publikacja za `x`", + "Premieres `x`": "Publikacja `x`", "Hi! Looks like you have JavaScript turned off. Click here to view comments, keep in mind they may take a bit longer to load.": "Cześć! Wygląda na to, że masz wyłączoną obsługę JavaScriptu. Kliknij tutaj, żeby zobaczyć komentarze. Pamiętaj, że wczytywanie może potrwać dłużej.", "View YouTube comments": "Wyświetl komentarze z YouTube", "View more comments on Reddit": "Wyświetl więcej komentarzy na Reddicie", "View `x` comments": { - "([^.,0-9]|^)1([^.,0-9]|$)": "Wyświetl `x` komentarzy", + "([^.,0-9]|^)1([^.,0-9]|$)": "Wyświetl `x` komentarz", "": "Wyświetl `x` komentarzy" }, "View Reddit comments": "Wyświetl komentarze z Redditta", From f993b1e119ac4284ae1e94c1504c31ba8c06b0a6 Mon Sep 17 00:00:00 2001 From: Rex_sa Date: Sun, 16 Jul 2023 15:41:28 +0000 Subject: [PATCH 046/143] Update Arabic translation --- locales/ar.json | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/locales/ar.json b/locales/ar.json index c137d1a3..877fb9ff 100644 --- a/locales/ar.json +++ b/locales/ar.json @@ -540,5 +540,13 @@ "Channel Sponsor": "راعي القناة", "Standard YouTube license": "ترخيص YouTube القياسي", "Download is disabled": "تم تعطيل التحميلات", - "Import YouTube playlist (.csv)": "استيراد قائمة تشغيل YouTube (.csv)" + "Import YouTube playlist (.csv)": "استيراد قائمة تشغيل YouTube (.csv)", + "generic_button_save": "حفظ", + "generic_button_delete": "حذف", + "generic_button_edit": "تحرير", + "generic_button_cancel": "الغاء", + "generic_button_rss": "RSS", + "channel_tab_releases_label": "الإصدارات", + "playlist_button_add_items": "إضافة مقاطع فيديو", + "channel_tab_podcasts_label": "البودكاست" } From 7a5f5173ddebd9c3286ac0e7b80bca5004993040 Mon Sep 17 00:00:00 2001 From: Jorge Maldonado Ventura Date: Sun, 16 Jul 2023 16:10:15 +0000 Subject: [PATCH 047/143] Update Spanish translation --- locales/es.json | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/locales/es.json b/locales/es.json index b3103a25..f1697d30 100644 --- a/locales/es.json +++ b/locales/es.json @@ -476,5 +476,13 @@ "Channel Sponsor": "Patrocinador del canal", "Standard YouTube license": "Licencia de YouTube estándar", "Download is disabled": "La descarga está deshabilitada", - "Import YouTube playlist (.csv)": "Importar lista de reproducción de YouTube (.csv)" + "Import YouTube playlist (.csv)": "Importar lista de reproducción de YouTube (.csv)", + "playlist_button_add_items": "Añadir vídeos", + "generic_button_edit": "Editar", + "generic_button_save": "Guardar", + "generic_button_delete": "Borrar", + "generic_button_cancel": "Cancelar", + "generic_button_rss": "RSS", + "channel_tab_podcasts_label": "Podcasts", + "channel_tab_releases_label": "Publicaciones" } From e3fe6c44f88c934b2066e1a2909002c5e35ee1c8 Mon Sep 17 00:00:00 2001 From: Matthaiks Date: Sun, 16 Jul 2023 16:48:29 +0000 Subject: [PATCH 048/143] Update Polish translation --- locales/pl.json | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/locales/pl.json b/locales/pl.json index 6337465b..f1924c8a 100644 --- a/locales/pl.json +++ b/locales/pl.json @@ -492,5 +492,13 @@ "Song: ": "Piosenka: ", "Channel Sponsor": "Sponsor kanału", "Standard YouTube license": "Standardowa licencja YouTube", - "Import YouTube playlist (.csv)": "Importuj playlistę YouTube (.csv)" + "Import YouTube playlist (.csv)": "Importuj playlistę YouTube (.csv)", + "generic_button_edit": "Edytuj", + "generic_button_cancel": "Anuluj", + "generic_button_rss": "RSS", + "channel_tab_podcasts_label": "Podkasty", + "channel_tab_releases_label": "Wydania", + "generic_button_delete": "Usuń", + "generic_button_save": "Zapisz", + "playlist_button_add_items": "Dodaj filmy" } From a5a5422014aa4723c6d0c4d83de554127608a783 Mon Sep 17 00:00:00 2001 From: Jorge Maldonado Ventura Date: Sun, 16 Jul 2023 16:16:04 +0000 Subject: [PATCH 049/143] Update Spanish translation --- locales/es.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/locales/es.json b/locales/es.json index f1697d30..b4a56030 100644 --- a/locales/es.json +++ b/locales/es.json @@ -113,7 +113,7 @@ "Token manager": "Gestor de tokens", "Token": "Ficha", "Import/export": "Importar/Exportar", - "unsubscribe": "Desuscribirse", + "unsubscribe": "desuscribirse", "revoke": "revocar", "Subscriptions": "Suscripciones", "search": "buscar", @@ -154,7 +154,7 @@ "View YouTube comments": "Ver los comentarios de YouTube", "View more comments on Reddit": "Ver más comentarios en Reddit", "View `x` comments": { - "([^.,0-9]|^)1([^.,0-9]|$)": "Ver `x` comentarios", + "([^.,0-9]|^)1([^.,0-9]|$)": "Ver `x` comentario", "": "Ver `x` comentarios" }, "View Reddit comments": "Ver los comentarios de Reddit", From 552893a3c1e19f473003d0b5694d7e7af03238c9 Mon Sep 17 00:00:00 2001 From: Jorge Maldonado Ventura Date: Sun, 16 Jul 2023 16:18:13 +0000 Subject: [PATCH 050/143] Update Esperanto translation --- locales/eo.json | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/locales/eo.json b/locales/eo.json index e2a7b7b1..6d1b0bc1 100644 --- a/locales/eo.json +++ b/locales/eo.json @@ -154,7 +154,7 @@ "View YouTube comments": "Vidi komentojn de JuTubo", "View more comments on Reddit": "Vidi pli komentoj en Reddit", "View `x` comments": { - "([^.,0-9]|^)1([^.,0-9]|$)": "Vidi `x` komentojn", + "([^.,0-9]|^)1([^.,0-9]|$)": "Vidi `x` komenton", "": "Vidi `x` komentojn" }, "View Reddit comments": "Vidi komentojn de Reddit", @@ -476,5 +476,13 @@ "Song: ": "Muzikaĵo: ", "Standard YouTube license": "Implicita YouTube-licenco", "Download is disabled": "Elŝuto estas malebligita", - "Import YouTube playlist (.csv)": "Importi YouTube-ludliston (.csv)" + "Import YouTube playlist (.csv)": "Importi YouTube-ludliston (.csv)", + "generic_button_edit": "Redakti", + "playlist_button_add_items": "Aldoni videojn", + "generic_button_rss": "RSS", + "generic_button_delete": "Forigi", + "channel_tab_podcasts_label": "Podkastoj", + "generic_button_cancel": "Nuligi", + "channel_tab_releases_label": "Eldonoj", + "generic_button_save": "Konservi" } From 625d8c00ba063539719fb92fd986ef9aafd3cc86 Mon Sep 17 00:00:00 2001 From: Ihor Hordiichuk Date: Sun, 16 Jul 2023 21:12:19 +0000 Subject: [PATCH 051/143] Update Ukrainian translation --- locales/uk.json | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/locales/uk.json b/locales/uk.json index 308b10ca..4d8f06a5 100644 --- a/locales/uk.json +++ b/locales/uk.json @@ -492,5 +492,13 @@ "Channel Sponsor": "Спонсор каналу", "Standard YouTube license": "Стандартна ліцензія YouTube", "Download is disabled": "Завантаження вимкнено", - "Import YouTube playlist (.csv)": "Імпорт списку відтворення YouTube (.csv)" + "Import YouTube playlist (.csv)": "Імпорт списку відтворення YouTube (.csv)", + "channel_tab_podcasts_label": "Подкасти", + "playlist_button_add_items": "Додати відео", + "generic_button_cancel": "Скасувати", + "generic_button_rss": "RSS", + "channel_tab_releases_label": "Випуски", + "generic_button_delete": "Видалити", + "generic_button_edit": "Змінити", + "generic_button_save": "Зберегти" } From d7d95fd725f3f79d35c34a0b0219a85e3fa2ee9a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?O=C4=9Fuz=20Ersen?= Date: Sun, 16 Jul 2023 18:27:44 +0000 Subject: [PATCH 052/143] Update Turkish translation --- locales/tr.json | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/locales/tr.json b/locales/tr.json index 22732a51..7f3f2de8 100644 --- a/locales/tr.json +++ b/locales/tr.json @@ -476,5 +476,13 @@ "Song: ": "Şarkı: ", "Standard YouTube license": "Standart YouTube lisansı", "Download is disabled": "İndirme devre dışı", - "Import YouTube playlist (.csv)": "YouTube Oynatma Listesini İçe Aktar (.csv)" + "Import YouTube playlist (.csv)": "YouTube Oynatma Listesini İçe Aktar (.csv)", + "generic_button_delete": "Sil", + "generic_button_edit": "Düzenle", + "generic_button_save": "Kaydet", + "generic_button_cancel": "İptal", + "generic_button_rss": "RSS", + "channel_tab_releases_label": "Yayınlar", + "playlist_button_add_items": "Video ekle", + "channel_tab_podcasts_label": "Podcast'ler" } From b7f6c265f74b89ea5079516b1b6d756bc76f2d67 Mon Sep 17 00:00:00 2001 From: maboroshin Date: Mon, 17 Jul 2023 09:06:35 +0000 Subject: [PATCH 053/143] Update Japanese translation --- locales/ja.json | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/locales/ja.json b/locales/ja.json index b489ece0..ba3641fc 100644 --- a/locales/ja.json +++ b/locales/ja.json @@ -460,5 +460,13 @@ "Channel Sponsor": "チャンネルのスポンサー", "Standard YouTube license": "標準 Youtube ライセンス", "Download is disabled": "ダウンロード: このインスタンスでは未対応", - "Import YouTube playlist (.csv)": "YouTube 再生リストをインポート (.csv)" + "Import YouTube playlist (.csv)": "YouTube 再生リストをインポート (.csv)", + "generic_button_delete": "削除", + "generic_button_cancel": "キャンセル", + "channel_tab_podcasts_label": "ポッドキャスト", + "channel_tab_releases_label": "リリース", + "generic_button_edit": "編集", + "generic_button_save": "保存", + "generic_button_rss": "RSS", + "playlist_button_add_items": "動画を追加" } From a337150cbf21e97d848e542053e21ea83166dced Mon Sep 17 00:00:00 2001 From: xrfmkrh Date: Mon, 17 Jul 2023 13:03:36 +0000 Subject: [PATCH 054/143] Update Korean translation --- locales/ko.json | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/locales/ko.json b/locales/ko.json index 9c8db5a1..e02a8316 100644 --- a/locales/ko.json +++ b/locales/ko.json @@ -460,5 +460,13 @@ "Music in this video": "동영상 속 음악", "Artist: ": "아티스트: ", "Download is disabled": "다운로드가 비활성화 되어있음", - "Import YouTube playlist (.csv)": "유튜브 플레이리스트 가져오기 (.csv)" + "Import YouTube playlist (.csv)": "유튜브 플레이리스트 가져오기 (.csv)", + "playlist_button_add_items": "동영상 추가", + "channel_tab_podcasts_label": "팟캐스트", + "generic_button_delete": "삭제", + "generic_button_edit": "편집", + "generic_button_save": "저장", + "generic_button_cancel": "취소", + "generic_button_rss": "RSS", + "channel_tab_releases_label": "출시" } From 979168d8defd0586316f1f0c23f19b3533233f85 Mon Sep 17 00:00:00 2001 From: Nidi Date: Wed, 19 Jul 2023 18:56:49 +0200 Subject: [PATCH 055/143] Add Azerbaijani translation --- locales/az.json | 1 + 1 file changed, 1 insertion(+) create mode 100644 locales/az.json diff --git a/locales/az.json b/locales/az.json new file mode 100644 index 00000000..0967ef42 --- /dev/null +++ b/locales/az.json @@ -0,0 +1 @@ +{} From 6d0a6870cb3dc70917680da6625352f59f1e2a68 Mon Sep 17 00:00:00 2001 From: Jeff Huang Date: Thu, 20 Jul 2023 02:34:31 +0000 Subject: [PATCH 056/143] Update Chinese (Traditional) translation --- locales/zh-TW.json | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/locales/zh-TW.json b/locales/zh-TW.json index 7da2d762..da81922b 100644 --- a/locales/zh-TW.json +++ b/locales/zh-TW.json @@ -460,5 +460,13 @@ "Song: ": "歌曲: ", "Standard YouTube license": "標準 YouTube 授權條款", "Download is disabled": "已停用下載", - "Import YouTube playlist (.csv)": "匯入 YouTube 播放清單 (.csv)" + "Import YouTube playlist (.csv)": "匯入 YouTube 播放清單 (.csv)", + "generic_button_cancel": "取消", + "generic_button_edit": "編輯", + "generic_button_save": "儲存", + "generic_button_rss": "RSS", + "generic_button_delete": "刪除", + "playlist_button_add_items": "新增影片", + "channel_tab_podcasts_label": "Podcast", + "channel_tab_releases_label": "發布" } From d83f92a074e60950265c80d6c26ae1949ff17a99 Mon Sep 17 00:00:00 2001 From: VoidWalker Date: Sat, 22 Jul 2023 01:53:24 +0000 Subject: [PATCH 057/143] Update Russian translation --- locales/ru.json | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/locales/ru.json b/locales/ru.json index a93207ad..5325a9b6 100644 --- a/locales/ru.json +++ b/locales/ru.json @@ -492,5 +492,13 @@ "Standard YouTube license": "Стандартная лицензия YouTube", "Channel Sponsor": "Спонсор канала", "Download is disabled": "Загрузка отключена", - "Import YouTube playlist (.csv)": "Импорт плейлиста YouTube (.csv)" + "Import YouTube playlist (.csv)": "Импорт плейлиста YouTube (.csv)", + "channel_tab_releases_label": "Релизы", + "generic_button_delete": "Удалить", + "generic_button_edit": "Редактировать", + "generic_button_save": "Сохранить", + "generic_button_cancel": "Отменить", + "generic_button_rss": "RSS", + "playlist_button_add_items": "Добавить видео", + "channel_tab_podcasts_label": "Подкасты" } From 991d30066d91e72286e536509f3b6863b751f2a9 Mon Sep 17 00:00:00 2001 From: maboroshin Date: Fri, 21 Jul 2023 23:48:40 +0000 Subject: [PATCH 058/143] Update Japanese translation --- locales/ja.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/locales/ja.json b/locales/ja.json index ba3641fc..6fc02e2d 100644 --- a/locales/ja.json +++ b/locales/ja.json @@ -81,7 +81,7 @@ "preferences_category_subscription": "登録チャンネル設定", "preferences_annotations_subscribed_label": "最初から登録チャンネルのアノテーションを表示 ", "Redirect homepage to feed: ": "ホームからフィードにリダイレクト: ", - "preferences_max_results_label": "フィードに表示する動画の量: ", + "preferences_max_results_label": "フィードに表示する動画数: ", "preferences_sort_label": "動画を並び替え: ", "published": "投稿日", "published - reverse": "投稿日 - 逆順", From b6b364c7307c162ec06df45055f158999b9d8219 Mon Sep 17 00:00:00 2001 From: joaooliva Date: Thu, 20 Jul 2023 20:39:27 +0000 Subject: [PATCH 059/143] Update Portuguese (Brazil) translation --- locales/pt-BR.json | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/locales/pt-BR.json b/locales/pt-BR.json index 81290398..68a6e3ab 100644 --- a/locales/pt-BR.json +++ b/locales/pt-BR.json @@ -475,6 +475,14 @@ "Standard YouTube license": "Licença padrão do YouTube", "Song: ": "Música: ", "Channel Sponsor": "Patrocinador do Canal", - "Download is disabled": "Download está desativado", - "Import YouTube playlist (.csv)": "Importar lista de reprodução do YouTube (.csv)" + "Download is disabled": "Download está desabilitado", + "Import YouTube playlist (.csv)": "Importar lista de reprodução do YouTube (.csv)", + "generic_button_delete": "Apagar", + "generic_button_save": "Salvar", + "generic_button_edit": "Editar", + "playlist_button_add_items": "Adicionar vídeos", + "channel_tab_releases_label": "Lançamentos", + "channel_tab_podcasts_label": "Podcasts", + "generic_button_cancel": "Cancelar", + "generic_button_rss": "RSS" } From b41574481df3f6c29967b60ec15eb568ad6b7489 Mon Sep 17 00:00:00 2001 From: Milo Ivir Date: Thu, 20 Jul 2023 12:25:07 +0000 Subject: [PATCH 060/143] Update Croatian translation --- locales/hr.json | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/locales/hr.json b/locales/hr.json index 0549fa70..ba3dd5e5 100644 --- a/locales/hr.json +++ b/locales/hr.json @@ -492,5 +492,13 @@ "Song: ": "Pjesma: ", "Standard YouTube license": "Standardna YouTube licenca", "Download is disabled": "Preuzimanje je deaktivirano", - "Import YouTube playlist (.csv)": "Uvezi YouTube zbirku (.csv)" + "Import YouTube playlist (.csv)": "Uvezi YouTube zbirku (.csv)", + "generic_button_delete": "Izbriši", + "playlist_button_add_items": "Dodaj videa", + "channel_tab_podcasts_label": "Podcasti", + "generic_button_edit": "Uredi", + "generic_button_save": "Spremi", + "generic_button_cancel": "Odustani", + "generic_button_rss": "RSS", + "channel_tab_releases_label": "Izdanja" } From 7bf3f08daf5854a323a1807024a43cc97f7d280e Mon Sep 17 00:00:00 2001 From: Fjuro Date: Fri, 21 Jul 2023 19:24:09 +0000 Subject: [PATCH 061/143] Update Czech translation --- locales/cs.json | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/locales/cs.json b/locales/cs.json index 73ed960d..b2cce0bd 100644 --- a/locales/cs.json +++ b/locales/cs.json @@ -492,5 +492,13 @@ "Song: ": "Skladba: ", "Standard YouTube license": "Standardní licence YouTube", "Download is disabled": "Stahování je zakázáno", - "Import YouTube playlist (.csv)": "Importovat YouTube playlist (.csv)" + "Import YouTube playlist (.csv)": "Importovat YouTube playlist (.csv)", + "generic_button_save": "Uložit", + "generic_button_delete": "Odstranit", + "generic_button_cancel": "Zrušit", + "channel_tab_podcasts_label": "Podcasty", + "channel_tab_releases_label": "Vydání", + "generic_button_edit": "Upravit", + "generic_button_rss": "RSS", + "playlist_button_add_items": "Přidat videa" } From 8a88e51382f57bdc4e5b2edd11d569e97eec4321 Mon Sep 17 00:00:00 2001 From: Subham Jena Date: Mon, 24 Jul 2023 14:23:07 +0000 Subject: [PATCH 062/143] Update Odia translation --- locales/or.json | 30 +++++++++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/locales/or.json b/locales/or.json index 0967ef42..948610f1 100644 --- a/locales/or.json +++ b/locales/or.json @@ -1 +1,29 @@ -{} +{ + "preferences_quality_dash_option_720p": "୭୨୦ପି", + "preferences_quality_dash_option_4320p": "୪୩୨୦ପି", + "preferences_quality_dash_option_240p": "୨୪୦ପି", + "preferences_quality_dash_option_2160p": "୨୧୬୦ପି", + "preferences_quality_dash_option_144p": "୧୪୪ପି", + "reddit": "Reddit", + "preferences_quality_dash_option_480p": "୪୮୦ପି", + "preferences_dark_mode_label": "ଥିମ୍: ", + "dark": "ଗାଢ଼", + "published": "ପ୍ରକାଶିତ", + "generic_videos_count": "{{count}}ଟିଏ ଵିଡ଼ିଓ", + "generic_videos_count_plural": "{{count}}ଟି ଵିଡ଼ିଓ", + "generic_button_edit": "ସମ୍ପାଦନା", + "light": "ହାଲୁକା", + "last": "ଗତ", + "New password": "ନୂଆ ପାସ୍‌ୱର୍ଡ଼", + "preferences_quality_dash_option_1440p": "୧୪୪୦ପି", + "preferences_quality_dash_option_360p": "୩୬୦ପି", + "preferences_quality_option_medium": "ମଧ୍ୟମ", + "preferences_quality_dash_option_1080p": "୧୦୮୦ପି", + "youtube": "YouTube", + "preferences_quality_option_hd720": "HD୭୨୦", + "invidious": "Invidious", + "generic_playlists_count": "{{count}}ଟିଏ ଚାଳନାତାଲିକା", + "generic_playlists_count_plural": "{{count}}ଟି ଚାଳନାତାଲିକା", + "Yes": "ହଁ", + "No": "ନାହିଁ" +} From a5bcf9ba441baaa70d7b4f7ad9abb9211e76dd52 Mon Sep 17 00:00:00 2001 From: Overplant Poster Date: Wed, 26 Jul 2023 21:15:01 +0000 Subject: [PATCH 063/143] Update Sinhala translation --- locales/si.json | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/locales/si.json b/locales/si.json index 19f34fac..4637cbd2 100644 --- a/locales/si.json +++ b/locales/si.json @@ -89,7 +89,7 @@ "preferences_quality_option_hd720": "HD720", "preferences_quality_dash_option_auto": "ස්වයංක්‍රීය", "preferences_quality_option_small": "කුඩා", - "preferences_quality_dash_option_best": "උසස්", + "preferences_quality_dash_option_best": "හොඳම", "preferences_quality_dash_option_2160p": "2160p", "preferences_quality_dash_option_1440p": "1440p", "preferences_quality_dash_option_720p": "720p", @@ -119,5 +119,9 @@ "Only show latest unwatched video from channel: ": "නාලිකාවේ නවතම නැරඹන නොලද වීඩියෝව පමණක් පෙන්වන්න: ", "preferences_category_data": "දත්ත මනාප", "Clear watch history": "නැරඹුම් ඉතිහාසය මකාදැමීම", - "Subscriptions": "දායකත්ව" + "Subscriptions": "දායකත්ව", + "generic_button_rss": "RSS", + "generic_button_save": "සුරකින්න", + "generic_button_cancel": "අවලංගු කරන්න", + "preferences_quality_dash_option_worst": "නරකම" } From 2117e34e9748a928527b1fda78f6fe883cc5253a Mon Sep 17 00:00:00 2001 From: John Donne Date: Sun, 30 Jul 2023 21:47:27 +0000 Subject: [PATCH 064/143] Update French translation --- locales/fr.json | 90 +++++++++++++++++++++++++++++-------------------- 1 file changed, 54 insertions(+), 36 deletions(-) diff --git a/locales/fr.json b/locales/fr.json index 2eb4dd2b..5e0f5152 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -1,14 +1,19 @@ { - "generic_views_count": "{{count}} vue", - "generic_views_count_plural": "{{count}} vues", - "generic_videos_count": "{{count}} vidéo", - "generic_videos_count_plural": "{{count}} vidéos", - "generic_playlists_count": "{{count}} liste de lecture", - "generic_playlists_count_plural": "{{count}} listes de lecture", - "generic_subscribers_count": "{{count}} abonné", - "generic_subscribers_count_plural": "{{count}} abonnés", - "generic_subscriptions_count": "{{count}} abonnement", - "generic_subscriptions_count_plural": "{{count}} abonnements", + "generic_views_count_0": "{{count}} vue", + "generic_views_count_1": "{{count}} vues", + "generic_views_count_2": "{{count}} vues", + "generic_videos_count_0": "{{count}} vidéo", + "generic_videos_count_1": "{{count}} vidéos", + "generic_videos_count_2": "{{count}} vidéos", + "generic_playlists_count_0": "{{count}} liste de lecture", + "generic_playlists_count_1": "{{count}} listes de lecture", + "generic_playlists_count_2": "{{count}} listes de lecture", + "generic_subscribers_count_0": "{{count}} abonné", + "generic_subscribers_count_1": "{{count}} abonnés", + "generic_subscribers_count_2": "{{count}} abonnés", + "generic_subscriptions_count_0": "{{count}} abonnement", + "generic_subscriptions_count_1": "{{count}} abonnements", + "generic_subscriptions_count_2": "{{count}} abonnements", "generic_button_delete": "Supprimer", "generic_button_edit": "Editer", "generic_button_save": "Enregistrer", @@ -55,10 +60,10 @@ "Password": "Mot de passe", "Time (h:mm:ss):": "Heure (h:mm:ss) :", "Text CAPTCHA": "CAPTCHA textuel", - "Image CAPTCHA": "CAPTCHA graphique", - "Sign In": "Se connecter", + "Image CAPTCHA": "CAPTCHA pictural", + "Sign In": "S'identifier", "Register": "S'inscrire", - "E-mail": "E-mail", + "E-mail": "Courriel", "Preferences": "Préférences", "preferences_category_player": "Préférences du lecteur", "preferences_video_loop_label": "Lire en boucle : ", @@ -128,14 +133,16 @@ "Subscription manager": "Gestionnaire d'abonnement", "Token manager": "Gestionnaire de token", "Token": "Token", - "tokens_count": "{{count}} token", - "tokens_count_plural": "{{count}} tokens", + "tokens_count_0": "{{count}} jeton", + "tokens_count_1": "{{count}} jetons", + "tokens_count_2": "{{count}} jetons", "Import/export": "Importer/Exporter", "unsubscribe": "se désabonner", "revoke": "révoquer", "Subscriptions": "Abonnements", - "subscriptions_unseen_notifs_count": "{{count}} notification non vue", - "subscriptions_unseen_notifs_count_plural": "{{count}} notifications non vues", + "subscriptions_unseen_notifs_count_0": "{{count}} notification non vue", + "subscriptions_unseen_notifs_count_1": "{{count}} notifications non vues", + "subscriptions_unseen_notifs_count_2": "{{count}} notifications non vues", "search": "rechercher", "Log out": "Se déconnecter", "Released under the AGPLv3 on Github.": "Publié sous licence AGPLv3 sur GitHub.", @@ -197,12 +204,14 @@ "This channel does not exist.": "Cette chaine n'existe pas.", "Could not get channel info.": "Impossible de charger les informations de cette chaîne.", "Could not fetch comments": "Impossible de charger les commentaires", - "comments_view_x_replies": "Voir {{count}} réponse", - "comments_view_x_replies_plural": "Voir {{count}} réponses", + "comments_view_x_replies_0": "Voir {{count}} réponse", + "comments_view_x_replies_1": "Voir {{count}} réponses", + "comments_view_x_replies_2": "Voir {{count}} réponses", "`x` ago": "il y a `x`", "Load more": "Voir plus", - "comments_points_count": "{{count}} point", - "comments_points_count_plural": "{{count}} points", + "comments_points_count_0": "{{count}} point", + "comments_points_count_1": "{{count}} points", + "comments_points_count_2": "{{count}} points", "Could not create mix.": "Impossible de charger cette liste de lecture.", "Empty playlist": "La liste de lecture est vide", "Not a playlist.": "La liste de lecture est invalide.", @@ -320,20 +329,27 @@ "Yiddish": "Yiddish", "Yoruba": "Yoruba", "Zulu": "Zoulou", - "generic_count_years": "{{count}} an", - "generic_count_years_plural": "{{count}} ans", - "generic_count_months": "{{count}} mois", - "generic_count_months_plural": "{{count}} mois", - "generic_count_weeks": "{{count}} semaine", - "generic_count_weeks_plural": "{{count}} semaines", - "generic_count_days": "{{count}} jour", - "generic_count_days_plural": "{{count}} jours", - "generic_count_hours": "{{count}} heure", - "generic_count_hours_plural": "{{count}} heures", - "generic_count_minutes": "{{count}} minute", - "generic_count_minutes_plural": "{{count}} minutes", - "generic_count_seconds": "{{count}} seconde", - "generic_count_seconds_plural": "{{count}} secondes", + "generic_count_years_0": "{{count}} an", + "generic_count_years_1": "{{count}} ans", + "generic_count_years_2": "{{count}} ans", + "generic_count_months_0": "{{count}} mois", + "generic_count_months_1": "{{count}} mois", + "generic_count_months_2": "{{count}} mois", + "generic_count_weeks_0": "{{count}} semaine", + "generic_count_weeks_1": "{{count}} semaines", + "generic_count_weeks_2": "{{count}} semaines", + "generic_count_days_0": "{{count}} jour", + "generic_count_days_1": "{{count}} jours", + "generic_count_days_2": "{{count}} jours", + "generic_count_hours_0": "{{count}} heure", + "generic_count_hours_1": "{{count}} heures", + "generic_count_hours_2": "{{count}} heures", + "generic_count_minutes_0": "{{count}} minute", + "generic_count_minutes_1": "{{count}} minutes", + "generic_count_minutes_2": "{{count}} minutes", + "generic_count_seconds_0": "{{count}} seconde", + "generic_count_seconds_1": "{{count}} secondes", + "generic_count_seconds_2": "{{count}} secondes", "Fallback comments: ": "Commentaires alternatifs : ", "Popular": "Populaire", "Search": "Rechercher", @@ -482,5 +498,7 @@ "Music in this video": "Musique dans cette vidéo", "Channel Sponsor": "Soutien de la chaîne", "Download is disabled": "Le téléchargement est désactivé", - "Import YouTube playlist (.csv)": "Importer des listes de lecture de Youtube (.csv)" + "Import YouTube playlist (.csv)": "Importer des listes de lecture de Youtube (.csv)", + "channel_tab_releases_label": "Parutions", + "channel_tab_podcasts_label": "Émissions audio" } From b4e9f173ab002ffad987593cab635638e97ecf99 Mon Sep 17 00:00:00 2001 From: atilluF <110931720+atilluF@users.noreply.github.com> Date: Fri, 28 Jul 2023 12:53:31 +0000 Subject: [PATCH 065/143] Update Italian translation --- locales/it.json | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/locales/it.json b/locales/it.json index 9d633264..29b7445a 100644 --- a/locales/it.json +++ b/locales/it.json @@ -491,5 +491,13 @@ "Song: ": "Canzone: ", "Standard YouTube license": "Licenza standard di YouTube", "Channel Sponsor": "Sponsor del canale", - "Import YouTube playlist (.csv)": "Importa playlist di YouTube (.csv)" + "Import YouTube playlist (.csv)": "Importa playlist di YouTube (.csv)", + "generic_button_edit": "Modifica", + "generic_button_cancel": "Annulla", + "generic_button_rss": "RSS", + "channel_tab_releases_label": "Pubblicazioni", + "generic_button_delete": "Elimina", + "generic_button_save": "Salva", + "playlist_button_add_items": "Aggiungi video", + "channel_tab_podcasts_label": "Podcast" } From 1e170ef7d08ad01cc241c293a1569a537c7fa84b Mon Sep 17 00:00:00 2001 From: random r Date: Sun, 30 Jul 2023 10:13:57 +0000 Subject: [PATCH 066/143] Update Italian translation --- locales/it.json | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/locales/it.json b/locales/it.json index 29b7445a..f7463ee3 100644 --- a/locales/it.json +++ b/locales/it.json @@ -16,7 +16,7 @@ "View playlist on YouTube": "Vedi playlist su YouTube", "newest": "più recente", "oldest": "più vecchio", - "popular": "Tendenze", + "popular": "popolare", "last": "ultimo", "Next page": "Pagina successiva", "Previous page": "Pagina precedente", @@ -119,8 +119,9 @@ "generic_subscriptions_count_0": "{{count}} iscrizione", "generic_subscriptions_count_1": "{{count}} iscrizioni", "generic_subscriptions_count_2": "{{count}} iscrizioni", - "tokens_count": "{{count}} gettone", - "tokens_count_plural": "{{count}} gettoni", + "tokens_count_0": "{{count}} gettone", + "tokens_count_1": "{{count}} gettoni", + "tokens_count_2": "{{count}} gettoni", "Import/export": "Importa/esporta", "unsubscribe": "disiscriviti", "revoke": "revoca", @@ -482,7 +483,7 @@ "channel_tab_shorts_label": "Short", "channel_tab_playlists_label": "Playlist", "channel_tab_channels_label": "Canali", - "channel_tab_streams_label": "Livestream", + "channel_tab_streams_label": "Trasmissioni in diretta", "channel_tab_community_label": "Comunità", "Music in this video": "Musica in questo video", "Artist: ": "Artista: ", From 9715e96adbf65300f895fc1c30d02c25704d5ea8 Mon Sep 17 00:00:00 2001 From: Eric Date: Sat, 29 Jul 2023 04:00:38 +0000 Subject: [PATCH 067/143] Update Chinese (Simplified) translation --- locales/zh-CN.json | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/locales/zh-CN.json b/locales/zh-CN.json index 58b834fa..62f45a29 100644 --- a/locales/zh-CN.json +++ b/locales/zh-CN.json @@ -460,5 +460,13 @@ "Channel Sponsor": "频道赞助者", "Standard YouTube license": "标准 YouTube 许可证", "Download is disabled": "已禁用下载", - "Import YouTube playlist (.csv)": "导入 YouTube 播放列表(.csv)" + "Import YouTube playlist (.csv)": "导入 YouTube 播放列表(.csv)", + "generic_button_cancel": "取消", + "playlist_button_add_items": "添加视频", + "generic_button_delete": "删除", + "channel_tab_podcasts_label": "播客", + "generic_button_edit": "编辑", + "generic_button_save": "保存", + "generic_button_rss": "RSS", + "channel_tab_releases_label": "公告" } From 00ac29a2ba7640b9ef1cbae5f7147935b49fa885 Mon Sep 17 00:00:00 2001 From: Leonardo Colman Date: Sat, 29 Jul 2023 22:15:27 +0000 Subject: [PATCH 068/143] Update Portuguese (Brazil) translation --- locales/pt-BR.json | 80 +++++++++++++++++++++++++++------------------- 1 file changed, 48 insertions(+), 32 deletions(-) diff --git a/locales/pt-BR.json b/locales/pt-BR.json index 68a6e3ab..7d522ed5 100644 --- a/locales/pt-BR.json +++ b/locales/pt-BR.json @@ -112,8 +112,9 @@ "Subscription manager": "Gerenciador de inscrições", "Token manager": "Gerenciador de tokens", "Token": "Token", - "tokens_count": "{{count}} token", - "tokens_count_plural": "{{count}} tokens", + "tokens_count_0": "{{count}} token", + "tokens_count_1": "{{count}} tokens", + "tokens_count_2": "{{count}} tokens", "Import/export": "Importar/Exportar", "unsubscribe": "cancelar inscrição", "revoke": "revogar", @@ -297,20 +298,27 @@ "Yiddish": "Iídiche", "Yoruba": "Iorubá", "Zulu": "Zulu", - "generic_count_years": "{{count}} ano", - "generic_count_years_plural": "{{count}} anos", - "generic_count_months": "{{count}} mês", - "generic_count_months_plural": "{{count}} meses", - "generic_count_weeks": "{{count}} semana", - "generic_count_weeks_plural": "{{count}} semanas", - "generic_count_days": "{{count}} dia", - "generic_count_days_plural": "{{count}} dias", - "generic_count_hours": "{{count}} hora", - "generic_count_hours_plural": "{{count}} horas", - "generic_count_minutes": "{{count}} minuto", - "generic_count_minutes_plural": "{{count}} minutos", - "generic_count_seconds": "{{count}} segundo", - "generic_count_seconds_plural": "{{count}} segundos", + "generic_count_years_0": "{{count}} ano", + "generic_count_years_1": "{{count}} anos", + "generic_count_years_2": "{{count}} anos", + "generic_count_months_0": "{{count}} mês", + "generic_count_months_1": "{{count}} meses", + "generic_count_months_2": "{{count}} meses", + "generic_count_weeks_0": "{{count}} semana", + "generic_count_weeks_1": "{{count}} semanas", + "generic_count_weeks_2": "{{count}} semanas", + "generic_count_days_0": "{{count}} dia", + "generic_count_days_1": "{{count}} dias", + "generic_count_days_2": "{{count}} dias", + "generic_count_hours_0": "{{count}} hora", + "generic_count_hours_1": "{{count}} horas", + "generic_count_hours_2": "{{count}} horas", + "generic_count_minutes_0": "{{count}} minuto", + "generic_count_minutes_1": "{{count}} minutos", + "generic_count_minutes_2": "{{count}} minutos", + "generic_count_seconds_0": "{{count}} segundo", + "generic_count_seconds_1": "{{count}} segundos", + "generic_count_seconds_2": "{{count}} segundos", "Fallback comments: ": "Comentários alternativos: ", "Popular": "Populares", "Search": "Procurar", @@ -377,20 +385,27 @@ "preferences_quality_dash_label": "Qualidade de vídeo do painel preferida: ", "preferences_region_label": "País do conteúdo: ", "preferences_quality_dash_option_4320p": "4320p", - "generic_videos_count": "{{count}} vídeo", - "generic_videos_count_plural": "{{count}} vídeos", - "generic_playlists_count": "{{count}} lista de reprodução", - "generic_playlists_count_plural": "{{count}} listas de reprodução", - "generic_subscribers_count": "{{count}} inscrito", - "generic_subscribers_count_plural": "{{count}} inscritos", - "generic_subscriptions_count": "{{count}} inscrição", - "generic_subscriptions_count_plural": "{{count}} inscrições", - "subscriptions_unseen_notifs_count": "{{count}} notificação não vista", - "subscriptions_unseen_notifs_count_plural": "{{count}} notificações não vistas", - "comments_view_x_replies": "Ver {{count}} resposta", - "comments_view_x_replies_plural": "Ver {{count}} respostas", - "comments_points_count": "{{count}} ponto", - "comments_points_count_plural": "{{count}} pontos", + "generic_videos_count_0": "{{count}} vídeo", + "generic_videos_count_1": "{{count}} vídeos", + "generic_videos_count_2": "{{count}} vídeos", + "generic_playlists_count_0": "{{count}} lista de reprodução", + "generic_playlists_count_1": "{{count}} listas de reprodução", + "generic_playlists_count_2": "{{count}} listas de reprodução", + "generic_subscribers_count_0": "{{count}} inscrito", + "generic_subscribers_count_1": "{{count}} inscritos", + "generic_subscribers_count_2": "{{count}} inscritos", + "generic_subscriptions_count_0": "{{count}} inscrição", + "generic_subscriptions_count_1": "{{count}} inscrições", + "generic_subscriptions_count_2": "{{count}} inscrições", + "subscriptions_unseen_notifs_count_0": "{{count}} notificação não vista", + "subscriptions_unseen_notifs_count_1": "{{count}} notificações não vistas", + "subscriptions_unseen_notifs_count_2": "{{count}} notificações não vistas", + "comments_view_x_replies_0": "Ver {{count}} resposta", + "comments_view_x_replies_1": "Ver {{count}} respostas", + "comments_view_x_replies_2": "Ver {{count}} respostas", + "comments_points_count_0": "{{count}} ponto", + "comments_points_count_1": "{{count}} pontos", + "comments_points_count_2": "{{count}} pontos", "crash_page_you_found_a_bug": "Parece que você encontrou um erro no Invidious!", "crash_page_before_reporting": "Antes de reportar um erro, verifique se você:", "preferences_save_player_pos_label": "Salvar a posição de reprodução: ", @@ -400,8 +415,9 @@ "crash_page_search_issue": "procurou por um erro existente no GitHub", "crash_page_report_issue": "Se nenhuma opção acima ajudou, por favor abra um novo problema no Github (preferencialmente em inglês) e inclua o seguinte texto (NÃO traduza):", "crash_page_read_the_faq": "leia as Perguntas frequentes (FAQ)", - "generic_views_count": "{{count}} visualização", - "generic_views_count_plural": "{{count}} visualizações", + "generic_views_count_0": "{{count}} visualização", + "generic_views_count_1": "{{count}} visualizações", + "generic_views_count_2": "{{count}} visualizações", "preferences_quality_option_dash": "DASH (qualidade adaptável)", "preferences_quality_option_hd720": "HD720", "preferences_quality_option_small": "Pequeno", From ebb69ee4fd2f381f004bd13e3ef4bb0f1de3f11a Mon Sep 17 00:00:00 2001 From: Hoang Minh Pham Date: Fri, 28 Jul 2023 16:56:35 +0000 Subject: [PATCH 069/143] Update Vietnamese translation --- locales/vi.json | 36 ++++++++++++++++++++---------------- 1 file changed, 20 insertions(+), 16 deletions(-) diff --git a/locales/vi.json b/locales/vi.json index d79c684c..9cb87d3e 100644 --- a/locales/vi.json +++ b/locales/vi.json @@ -2,7 +2,7 @@ "generic_videos_count_0": "{{count}} video", "generic_subscribers_count_0": "{{count}} người theo dõi", "LIVE": "TRỰC TIẾP", - "Shared `x` ago": "Đã chia sẻ` x` trước", + "Shared `x` ago": "Đã chia sẻ `x` trước", "Unsubscribe": "Hủy theo dõi", "Subscribe": "Theo dõi", "View channel on YouTube": "Xem kênh trên YouTube", @@ -71,7 +71,7 @@ "Dark mode: ": "Chế độ tối: ", "preferences_dark_mode_label": "Chủ đề: ", "dark": "tối", - "light": "ánh sáng", + "light": "sáng", "preferences_thin_mode_label": "Chế độ mỏng: ", "preferences_category_misc": "Tùy chọn khác", "preferences_automatic_instance_redirect_label": "Tự động chuyển hướng phiên bản (dự phòng về redirect.invidious.io): ", @@ -120,7 +120,7 @@ "View JavaScript license information.": "Xem thông tin giấy phép JavaScript.", "View privacy policy.": "Xem chính sách bảo mật.", "Trending": "Xu hướng", - "Public": "Công cộng", + "Public": "Công khai", "Unlisted": "Không hiển thị", "Private": "Riêng tư", "View all playlists": "Xem tất cả danh sách phát", @@ -182,17 +182,17 @@ "Amharic": "Amharic", "Arabic": "Tiếng Ả Rập", "Armenian": "Tiếng Armenia", - "Azerbaijani": "Azerbaijan", - "Bangla": "Bangla", + "Azerbaijani": "Tiếng Azerbaijan", + "Bangla": "Tiếng Bengal", "Basque": "Tiếng Basque", - "Belarusian": "Người Belarus", + "Belarusian": "Tiếng Belarus", "Bosnian": "Tiếng Bosnia", "Bulgarian": "Tiếng Bungari", "Burmese": "Tiếng Miến Điện", "Catalan": "Tiếng Catalan", "Cebuano": "Cebuano", "Chinese (Simplified)": "Tiếng Trung (Giản thể)", - "Chinese (Traditional)": "Truyền thống Trung Hoa)", + "Chinese (Traditional)": "Tiếng Trung (Phồn thể)", "Corsican": "Corsican", "Croatian": "Tiếng Croatia", "Czech": "Tiếng Séc", @@ -219,22 +219,22 @@ "Igbo": "Igbo", "Indonesian": "Tiếng Indonesia", "Irish": "Tiếng Ailen", - "Italian": "Người Ý", + "Italian": "Tiếng Ý", "Japanese": "Tiếng Nhật", "Javanese": "Tiếng Java", "Kannada": "Tiếng Kannada", "Kazakh": "Tiếng Kazakh", "Khmer": "Tiếng Khmer", - "Korean": "Hàn Quốc", + "Korean": "Tiếng Hàn", "Kurdish": "Tiếng Kurd", - "Kyrgyz": "Kyrgyz", - "Lao": "Lào", - "Latin": "Latin", + "Kyrgyz": "Tiếng Kyrgyz", + "Lao": "Tiếng Lào", + "Latin": "Tiếng Latin", "Latvian": "Tiếng Latvia", "Lithuanian": "Tiếng Litva", "Luxembourgish": "Tiếng Luxembourg", - "Macedonian": "Người Macedonian", - "Malagasy": "Malagasy", + "Macedonian": "Tiếng Macedonian", + "Malagasy": "Tiếng Malagasy", "Malay": "Tiếng Mã Lai", "Malayalam": "Tiếng Malayalam", "Maltese": "Cây nho", @@ -364,7 +364,7 @@ "Import/export": "Xuất/nhập dữ liệu", "preferences_quality_dash_option_4320p": "4320p", "preferences_quality_option_dash": "DASH (tự tối ưu chất lượng)", - "generic_subscriptions_count_0": "{{count}} thuê bao", + "generic_subscriptions_count_0": "{{count}} người đăng kí", "preferences_quality_dash_option_1440p": "1440p", "preferences_quality_dash_option_480p": "480p", "preferences_quality_dash_option_2160p": "2160p", @@ -383,5 +383,9 @@ "Standard YouTube license": "Giấy phép YouTube thông thường", "Album: ": "Album: ", "preferences_save_player_pos_label": "Lưu vị trí xem cuối cùng ", - "Hi! Looks like you have JavaScript turned off. Click here to view comments, keep in mind they may take a bit longer to load.": "Xin chào! Có vẻ như bạn đã tắt JavaScript. Bấm vào đây để xem bình luận, lưu ý rằng thời gian tải có thể lâu hơn." + "Hi! Looks like you have JavaScript turned off. Click here to view comments, keep in mind they may take a bit longer to load.": "Xin chào! Có vẻ như bạn đã tắt JavaScript. Bấm vào đây để xem bình luận, lưu ý rằng thời gian tải có thể lâu hơn.", + "Chinese (China)": "Tiếng Trung (Trung Quốc)", + "generic_button_cancel": "Hủy", + "Chinese": "Tiếng Trung", + "generic_button_delete": "Xóa" } From 3123478cb2477969bf49e953c46aaaaeaddfd1bb Mon Sep 17 00:00:00 2001 From: Leonardo Colman Date: Sat, 29 Jul 2023 22:10:14 +0000 Subject: [PATCH 070/143] Update Portuguese translation --- locales/pt.json | 94 +++++++++++++++++++++++++++++++------------------ 1 file changed, 59 insertions(+), 35 deletions(-) diff --git a/locales/pt.json b/locales/pt.json index dfa411c3..df63abe6 100644 --- a/locales/pt.json +++ b/locales/pt.json @@ -19,7 +19,7 @@ "search_filters_features_option_hdr": "HDR", "search_filters_features_option_location": "Localização", "search_filters_features_option_four_k": "4K", - "search_filters_features_option_live": "Em direto", + "search_filters_features_option_live": "Ao Vivo", "search_filters_features_option_three_d": "3D", "search_filters_features_option_c_commons": "Creative Commons", "search_filters_features_option_subtitles": "Legendas", @@ -44,20 +44,27 @@ "Default": "Predefinido", "Top": "Destaques", "Search": "Pesquisar", - "generic_count_years": "{{count}} segundo", - "generic_count_years_plural": "{{count}} segundos", - "generic_count_months": "{{count}} minuto", - "generic_count_months_plural": "{{count}} minutos", - "generic_count_weeks": "{{count}} hora", - "generic_count_weeks_plural": "{{count}} horas", - "generic_count_days": "{{count}} dia", - "generic_count_days_plural": "{{count}} dias", - "generic_count_hours": "{{count}} seman", - "generic_count_hours_plural": "{{count}} semanas", - "generic_count_minutes": "{{count}} mês", - "generic_count_minutes_plural": "{{count}} meses", - "generic_count_seconds": "{{count}} ano", - "generic_count_seconds_plural": "{{count}} anos", + "generic_count_years_0": "{{count}} segundo", + "generic_count_years_1": "{{count}} segundos", + "generic_count_years_2": "{{count}} segundos", + "generic_count_months_0": "{{count}} minuto", + "generic_count_months_1": "{{count}} minutos", + "generic_count_months_2": "{{count}} minutos", + "generic_count_weeks_0": "{{count}} hora", + "generic_count_weeks_1": "{{count}} horas", + "generic_count_weeks_2": "{{count}} horas", + "generic_count_days_0": "{{count}} dia", + "generic_count_days_1": "{{count}} dias", + "generic_count_days_2": "{{count}} dias", + "generic_count_hours_0": "{{count}} seman", + "generic_count_hours_1": "{{count}} semanas", + "generic_count_hours_2": "{{count}} semanas", + "generic_count_minutes_0": "{{count}} mês", + "generic_count_minutes_1": "{{count}} meses", + "generic_count_minutes_2": "{{count}} meses", + "generic_count_seconds_0": "{{count}} ano", + "generic_count_seconds_1": "{{count}} anos", + "generic_count_seconds_2": "{{count}} anos", "Chinese (Traditional)": "Chinês (tradicional)", "Chinese (Simplified)": "Chinês (simplificado)", "Could not pull trending pages.": "Não foi possível obter as páginas de tendências.", @@ -167,8 +174,9 @@ "Log out": "Terminar sessão", "Subscriptions": "Subscrições", "revoke": "revogar", - "tokens_count": "{{count}} token", - "tokens_count_plural": "{{count}} tokens", + "tokens_count_0": "{{count}} token", + "tokens_count_1": "{{count}} tokens", + "tokens_count_2": "{{count}} tokens", "Token": "Token", "Token manager": "Gerir tokens", "Subscription manager": "Gerir subscrições", @@ -365,7 +373,7 @@ "Subscribe": "Subscrever", "Unsubscribe": "Anular subscrição", "Shared `x` ago": "Partilhado `x` atrás", - "LIVE": "Em direto", + "LIVE": "AO VIVO", "search_filters_duration_option_short": "Curto (< 4 minutos)", "search_filters_duration_option_long": "Longo (> 20 minutos)", "footer_source_code": "Código-fonte", @@ -402,24 +410,32 @@ "videoinfo_youTube_embed_link": "Incorporar", "preferences_save_player_pos_label": "Guardar a posição de reprodução atual do vídeo: ", "download_subtitles": "Legendas - `x` (.vtt)", - "generic_views_count": "{{count}} visualização", - "generic_views_count_plural": "{{count}} visualizações", + "generic_views_count_0": "{{count}} visualização", + "generic_views_count_1": "{{count}} visualizações", + "generic_views_count_2": "{{count}} visualizações", "videoinfo_started_streaming_x_ago": "Iniciou a transmissão há `x`", "user_saved_playlists": "`x` listas de reprodução guardadas", - "generic_videos_count": "{{count}} vídeo", - "generic_videos_count_plural": "{{count}} vídeos", - "generic_playlists_count": "{{count}} lista de reprodução", - "generic_playlists_count_plural": "{{count}} listas de reprodução", - "subscriptions_unseen_notifs_count": "{{count}} notificação não vista", - "subscriptions_unseen_notifs_count_plural": "{{count}} notificações não vistas", - "comments_view_x_replies": "Ver {{count}} resposta", - "comments_view_x_replies_plural": "Ver {{count}} respostas", - "generic_subscribers_count": "{{count}} inscrito", - "generic_subscribers_count_plural": "{{count}} inscritos", - "generic_subscriptions_count": "{{count}} inscrição", - "generic_subscriptions_count_plural": "{{count}} inscrições", - "comments_points_count": "{{count}} ponto", - "comments_points_count_plural": "{{count}} pontos", + "generic_videos_count_0": "{{count}} vídeo", + "generic_videos_count_1": "{{count}} vídeos", + "generic_videos_count_2": "{{count}} vídeos", + "generic_playlists_count_0": "{{count}} lista de reprodução", + "generic_playlists_count_1": "{{count}} listas de reprodução", + "generic_playlists_count_2": "{{count}} listas de reprodução", + "subscriptions_unseen_notifs_count_0": "{{count}} notificação não vista", + "subscriptions_unseen_notifs_count_1": "{{count}} notificações não vistas", + "subscriptions_unseen_notifs_count_2": "{{count}} notificações não vistas", + "comments_view_x_replies_0": "Ver {{count}} resposta", + "comments_view_x_replies_1": "Ver {{count}} respostas", + "comments_view_x_replies_2": "Ver {{count}} respostas", + "generic_subscribers_count_0": "{{count}} inscrito", + "generic_subscribers_count_1": "{{count}} inscritos", + "generic_subscribers_count_2": "{{count}} inscritos", + "generic_subscriptions_count_0": "{{count}} inscrição", + "generic_subscriptions_count_1": "{{count}} inscrições", + "generic_subscriptions_count_2": "{{count}} inscrições", + "comments_points_count_0": "{{count}} ponto", + "comments_points_count_1": "{{count}} pontos", + "comments_points_count_2": "{{count}} pontos", "crash_page_you_found_a_bug": "Parece que encontrou um erro no Invidious!", "crash_page_before_reporting": "Antes de reportar um erro, verifique se:", "crash_page_refresh": "tentou recarregar a página", @@ -476,5 +492,13 @@ "Channel Sponsor": "Patrocinador do canal", "Standard YouTube license": "Licença padrão do YouTube", "Download is disabled": "A descarga está desativada", - "Import YouTube playlist (.csv)": "Importar lista de reprodução do YouTube (.csv)" + "Import YouTube playlist (.csv)": "Importar lista de reprodução do YouTube (.csv)", + "generic_button_delete": "Deletar", + "generic_button_edit": "Editar", + "generic_button_rss": "RSS", + "channel_tab_podcasts_label": "Podcasts", + "channel_tab_releases_label": "Lançamentos", + "generic_button_save": "Salvar", + "generic_button_cancel": "Cancelar", + "playlist_button_add_items": "Adicionar vídeos" } From 709bb7281b3856421084ea9127c1504b6eb6db96 Mon Sep 17 00:00:00 2001 From: Damjan Gerl Date: Mon, 31 Jul 2023 18:55:04 +0000 Subject: [PATCH 071/143] Update Slovenian translation --- locales/sl.json | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/locales/sl.json b/locales/sl.json index 45f63c6b..de0c7812 100644 --- a/locales/sl.json +++ b/locales/sl.json @@ -508,5 +508,13 @@ "Standard YouTube license": "Standardna licenca YouTube", "Channel Sponsor": "Sponzor kanala", "Download is disabled": "Prenos je onemogočen", - "Import YouTube playlist (.csv)": "Uvoz seznama predvajanja YouTube (.csv)" + "Import YouTube playlist (.csv)": "Uvoz seznama predvajanja YouTube (.csv)", + "generic_button_delete": "Izbriši", + "generic_button_edit": "Uredi", + "generic_button_save": "Shrani", + "generic_button_cancel": "Prekliči", + "generic_button_rss": "RSS", + "playlist_button_add_items": "Dodaj videoposnetke", + "channel_tab_podcasts_label": "Poddaje", + "channel_tab_releases_label": "Izdaje" } From a81c0f329cfe0ef343c31636b74615e91e613f72 Mon Sep 17 00:00:00 2001 From: syeopite Date: Tue, 8 Aug 2023 15:13:23 -0700 Subject: [PATCH 072/143] Add workaround for storyboards on priv. instances An upstream problem with videojs-vtt-thumbnails means that URLs gets joined incorrectly on any instance where `domain`, `external_port` and `https_only` aren't set. This commit adds some logic with the 404 handler to mitigate this problem. This is however only a workaround. See: https://github.com/iv-org/invidious/issues/3117 https://github.com/chrisboustead/videojs-vtt-thumbnails/issues/31 --- src/invidious/routes/errors.cr | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/invidious/routes/errors.cr b/src/invidious/routes/errors.cr index b138b562..4d8d9ee8 100644 --- a/src/invidious/routes/errors.cr +++ b/src/invidious/routes/errors.cr @@ -1,5 +1,10 @@ module Invidious::Routes::ErrorRoutes def self.error_404(env) + # Workaround for # 3117 + if HOST_URL.empty? && env.request.path.starts_with?("/v1/storyboards/sb") + return env.redirect "#{env.request.path[15..]}?#{env.params.query}" + end + if md = env.request.path.match(/^\/(?([a-zA-Z0-9_-]{11})|(\w+))$/) item = md["id"] From 6b17bb525095a62b163489c565edb0ca29eb1a93 Mon Sep 17 00:00:00 2001 From: syeopite Date: Tue, 8 Aug 2023 15:20:48 -0700 Subject: [PATCH 073/143] Regression from #4037 | Fix storyboards PR #4037 introduced a workaround around YouTube's new integrity checks on streaming URLs. However, the usage of this workaround prevents storyboard data from being returned by InnerTube. This commit fixes that by only using the workaround when calling try_fetch_streaming_data --- src/invidious/videos/parser.cr | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/invidious/videos/parser.cr b/src/invidious/videos/parser.cr index 2a09d187..06ff96b1 100644 --- a/src/invidious/videos/parser.cr +++ b/src/invidious/videos/parser.cr @@ -55,9 +55,7 @@ def extract_video_info(video_id : String, proxy_region : String? = nil) client_config = YoutubeAPI::ClientConfig.new(proxy_region: proxy_region) # Fetch data from the player endpoint - # CgIQBg is a workaround for streaming URLs that returns a 403. - # See https://github.com/iv-org/invidious/issues/4027#issuecomment-1666944520 - player_response = YoutubeAPI.player(video_id: video_id, params: "CgIQBg", client_config: client_config) + player_response = YoutubeAPI.player(video_id: video_id, params: "", client_config: client_config) playability_status = player_response.dig?("playabilityStatus", "status").try &.as_s @@ -120,6 +118,9 @@ def extract_video_info(video_id : String, proxy_region : String? = nil) # Replace player response and reset reason if !new_player_response.nil? + # Preserve storyboard data before replacement + new_player_response["storyboards"] = player_response["storyboards"] if player_response["storyboards"]? + player_response = new_player_response params.delete("reason") end From 2b36d3b419d04fd4fc46e97e03a4c3af7285b663 Mon Sep 17 00:00:00 2001 From: syeopite <70992037+syeopite@users.noreply.github.com> Date: Thu, 10 Aug 2023 18:45:10 +0000 Subject: [PATCH 074/143] Update errors.cr --- src/invidious/routes/errors.cr | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/invidious/routes/errors.cr b/src/invidious/routes/errors.cr index 4d8d9ee8..1e9ab44e 100644 --- a/src/invidious/routes/errors.cr +++ b/src/invidious/routes/errors.cr @@ -1,6 +1,6 @@ module Invidious::Routes::ErrorRoutes def self.error_404(env) - # Workaround for # 3117 + # Workaround for #3117 if HOST_URL.empty? && env.request.path.starts_with?("/v1/storyboards/sb") return env.redirect "#{env.request.path[15..]}?#{env.params.query}" end From c089d57cdb5517ca199e2ddecc5e54906dc55a8c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Milan=20=C5=A0alka?= Date: Thu, 10 Aug 2023 10:22:06 +0000 Subject: [PATCH 075/143] Update Slovak translation --- locales/sk.json | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/locales/sk.json b/locales/sk.json index 7346dc58..86681dfa 100644 --- a/locales/sk.json +++ b/locales/sk.json @@ -99,5 +99,23 @@ "generic_subscriptions_count_1": "{{count}} odbery", "generic_subscriptions_count_2": "{{count}} odberov", "Authorize token for `x`?": "Autorizovať token pre `x`?", - "View playlist on YouTube": "Zobraziť playlist na YouTube" + "View playlist on YouTube": "Zobraziť playlist na YouTube", + "preferences_quality_dash_option_best": "Najlepšia", + "preferences_quality_dash_option_worst": "Najhoršia", + "preferences_quality_dash_option_1440p": "1440p", + "preferences_quality_dash_option_720p": "720p", + "preferences_quality_option_hd720": "HD720", + "preferences_quality_dash_label": "Preferovaná video kvalita DASH: ", + "preferences_quality_option_dash": "DASH (adaptívna kvalita)", + "preferences_quality_option_small": "Malá", + "preferences_watch_history_label": "Zapnúť históriu pozerania: ", + "preferences_quality_dash_option_240p": "240p", + "preferences_quality_dash_option_1080p": "1080p", + "preferences_quality_dash_option_480p": "480p", + "preferences_quality_dash_option_auto": "Auto", + "preferences_quality_dash_option_144p": "144p", + "preferences_quality_dash_option_2160p": "2160p", + "invidious": "Invidious", + "preferences_quality_dash_option_4320p": "4320p", + "preferences_quality_dash_option_360p": "360p" } From 37f1a6aacfe2de5f52bd754e650883361e82045e Mon Sep 17 00:00:00 2001 From: Ati Date: Thu, 10 Aug 2023 10:21:34 +0000 Subject: [PATCH 076/143] Update Slovak translation --- locales/sk.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/locales/sk.json b/locales/sk.json index 86681dfa..8add0f57 100644 --- a/locales/sk.json +++ b/locales/sk.json @@ -9,7 +9,7 @@ "last": "posledné", "Next page": "Ďalšia strana", "Previous page": "Predchádzajúca strana", - "Clear watch history?": "Vymazať históriu sledovania?", + "Clear watch history?": "Vymazať históriu pozerania?", "New password": "Nové heslo", "New passwords must match": "Nové heslá sa musia zhodovať", "Authorize token?": "Autorizovať token?", From 4b85890c6ddca8e733e44f1d5599fc7c73564fae Mon Sep 17 00:00:00 2001 From: Noa Laznik Date: Fri, 11 Aug 2023 02:52:09 +0000 Subject: [PATCH 077/143] Update Slovenian translation --- locales/sl.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/locales/sl.json b/locales/sl.json index de0c7812..fec1cb62 100644 --- a/locales/sl.json +++ b/locales/sl.json @@ -222,7 +222,7 @@ "search_filters_date_option_week": "Ta teden", "search_filters_type_label": "Vrsta", "search_filters_type_option_all": "Katerakoli vrsta", - "search_filters_type_option_playlist": "Seznami predvajanja", + "search_filters_type_option_playlist": "Seznam predvajanja", "search_filters_features_option_subtitles": "Podnapisi/CC", "search_filters_features_option_location": "Lokacija", "footer_donate_page": "Prispevaj", From de2ea478540c1237a5559c134df1839c69bda950 Mon Sep 17 00:00:00 2001 From: Petter Reinholdtsen Date: Sun, 13 Aug 2023 11:54:19 +0000 Subject: [PATCH 078/143] =?UTF-8?q?Update=20Norwegian=20Bokm=C3=A5l=20tran?= =?UTF-8?q?slation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- locales/nb-NO.json | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/locales/nb-NO.json b/locales/nb-NO.json index 1e0e9e77..216b559f 100644 --- a/locales/nb-NO.json +++ b/locales/nb-NO.json @@ -154,7 +154,7 @@ "View YouTube comments": "Vis YouTube-kommentarer", "View more comments on Reddit": "Vis flere kommenterer på Reddit", "View `x` comments": { - "([^.,0-9]|^)1([^.,0-9]|$)": "Vis `x` kommentarer", + "([^.,0-9]|^)1([^.,0-9]|$)": "Vis `x` kommentar", "": "Vis `x` kommentarer" }, "View Reddit comments": "Vis Reddit-kommentarer", @@ -476,5 +476,13 @@ "Album: ": "Album: ", "Download is disabled": "Nedlasting er avskrudd", "Channel Sponsor": "Kanalsponsor", - "Import YouTube playlist (.csv)": "Importer YouTube-spilleliste (.csv)" + "Import YouTube playlist (.csv)": "Importer YouTube-spilleliste (.csv)", + "channel_tab_podcasts_label": "Podkaster", + "channel_tab_releases_label": "Utgaver", + "generic_button_delete": "Slett", + "generic_button_edit": "Endre", + "generic_button_save": "Lagre", + "generic_button_cancel": "Avbryt", + "generic_button_rss": "RSS", + "playlist_button_add_items": "Legg til videoer" } From ce44cb942130d261ee13c37b3ac44025936d4813 Mon Sep 17 00:00:00 2001 From: Snwglb Date: Fri, 18 Aug 2023 08:16:10 +0000 Subject: [PATCH 079/143] Update Hindi translation --- locales/hi.json | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/locales/hi.json b/locales/hi.json index dcb7294d..c1662dd9 100644 --- a/locales/hi.json +++ b/locales/hi.json @@ -471,5 +471,18 @@ "channel_tab_shorts_label": "शॉर्ट्स", "channel_tab_streams_label": "लाइवस्ट्रीम्स", "channel_tab_playlists_label": "प्लेलिस्ट्स", - "channel_tab_channels_label": "चैनल्स" + "channel_tab_channels_label": "चैनल्स", + "generic_button_save": "सहेजें", + "generic_button_cancel": "रद्द करें", + "generic_button_rss": "आरएसएस", + "generic_button_edit": "संपादित करें", + "generic_button_delete": "मिटाएं", + "playlist_button_add_items": "वीडियो जोड़ें", + "Song: ": "गाना: ", + "channel_tab_podcasts_label": "पाॅडकास्ट", + "channel_tab_releases_label": "रिलीज़ेस्", + "Import YouTube playlist (.csv)": "यूट्यूब प्लेलिस्ट को आयात करें", + "Standard YouTube license": "मानक यूट्यूब लाइसेंस", + "Channel Sponsor": "चैनल प्रायोजक", + "Download is disabled": "डाउनलोड करना अक्षम है" } From 387f057a9621ac6a9d6ac2d0f27534ef1f237928 Mon Sep 17 00:00:00 2001 From: Ettore Atalan Date: Sun, 20 Aug 2023 00:48:59 +0000 Subject: [PATCH 080/143] Update German translation --- locales/de.json | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/locales/de.json b/locales/de.json index 66f2ae6f..6ceaa44b 100644 --- a/locales/de.json +++ b/locales/de.json @@ -476,5 +476,11 @@ "Standard YouTube license": "Standard YouTube-Lizenz", "Song: ": "Musik: ", "Download is disabled": "Herunterladen ist deaktiviert", - "Import YouTube playlist (.csv)": "YouTube Playlist Importieren (.csv)" + "Import YouTube playlist (.csv)": "YouTube Playlist Importieren (.csv)", + "generic_button_delete": "Löschen", + "generic_button_edit": "Bearbeiten", + "generic_button_save": "Speichern", + "generic_button_cancel": "Abbrechen", + "generic_button_rss": "RSS", + "playlist_button_add_items": "Videos hinzufügen" } From 23b19c80b31c1076cecb522a60bd13e1b5b14458 Mon Sep 17 00:00:00 2001 From: Snwglb Date: Sat, 19 Aug 2023 08:45:51 +0000 Subject: [PATCH 081/143] Update Hindi translation --- locales/hi.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/locales/hi.json b/locales/hi.json index c1662dd9..21807c50 100644 --- a/locales/hi.json +++ b/locales/hi.json @@ -481,7 +481,7 @@ "Song: ": "गाना: ", "channel_tab_podcasts_label": "पाॅडकास्ट", "channel_tab_releases_label": "रिलीज़ेस्", - "Import YouTube playlist (.csv)": "यूट्यूब प्लेलिस्ट को आयात करें", + "Import YouTube playlist (.csv)": "YouTube प्लेलिस्ट (.csv) आयात करें", "Standard YouTube license": "मानक यूट्यूब लाइसेंस", "Channel Sponsor": "चैनल प्रायोजक", "Download is disabled": "डाउनलोड करना अक्षम है" From 1f7592e599054131c689246b0dd6aad45f2d8e7a Mon Sep 17 00:00:00 2001 From: syeopite Date: Thu, 24 Aug 2023 16:00:02 -0700 Subject: [PATCH 082/143] Refactor structure of caption.cr Rename CaptionsMetadata to Metadata Nest Metadata under Captions Unnest LANGUAGES constant from Metadata to main Captions module --- src/invidious/frontend/watch_page.cr | 2 +- src/invidious/videos.cr | 6 +- src/invidious/videos/caption.cr | 166 ++++++++++++----------- src/invidious/videos/transcript.cr | 2 +- src/invidious/views/user/preferences.ecr | 2 +- 5 files changed, 90 insertions(+), 88 deletions(-) diff --git a/src/invidious/frontend/watch_page.cr b/src/invidious/frontend/watch_page.cr index b860dba7..5fd81168 100644 --- a/src/invidious/frontend/watch_page.cr +++ b/src/invidious/frontend/watch_page.cr @@ -7,7 +7,7 @@ module Invidious::Frontend::WatchPage getter full_videos : Array(Hash(String, JSON::Any)) getter video_streams : Array(Hash(String, JSON::Any)) getter audio_streams : Array(Hash(String, JSON::Any)) - getter captions : Array(Invidious::Videos::CaptionMetadata) + getter captions : Array(Invidious::Videos::Captions::Metadata) def initialize( @full_videos, diff --git a/src/invidious/videos.cr b/src/invidious/videos.cr index 2b1d2603..9fbd1374 100644 --- a/src/invidious/videos.cr +++ b/src/invidious/videos.cr @@ -24,7 +24,7 @@ struct Video property updated : Time @[DB::Field(ignore: true)] - @captions = [] of Invidious::Videos::CaptionMetadata + @captions = [] of Invidious::Videos::Captions::Metadata @[DB::Field(ignore: true)] property adaptive_fmts : Array(Hash(String, JSON::Any))? @@ -215,9 +215,9 @@ struct Video keywords.includes? "YouTube Red" end - def captions : Array(Invidious::Videos::CaptionMetadata) + def captions : Array(Invidious::Videos::Captions::Metadata) if @captions.empty? && @info.has_key?("captions") - @captions = Invidious::Videos::CaptionMetadata.from_yt_json(info["captions"]) + @captions = Invidious::Videos::Captions::Metadata.from_yt_json(info["captions"]) end return @captions diff --git a/src/invidious/videos/caption.cr b/src/invidious/videos/caption.cr index 1e2abde9..82b68dcd 100644 --- a/src/invidious/videos/caption.cr +++ b/src/invidious/videos/caption.cr @@ -1,107 +1,109 @@ require "json" module Invidious::Videos - struct CaptionMetadata - property name : String - property language_code : String - property base_url : String + module Captions + struct Metadata + property name : String + property language_code : String + property base_url : String - property auto_generated : Bool + property auto_generated : Bool - def initialize(@name, @language_code, @base_url, @auto_generated) - end - - # Parse the JSON structure from Youtube - def self.from_yt_json(container : JSON::Any) : Array(CaptionMetadata) - caption_tracks = container - .dig?("playerCaptionsTracklistRenderer", "captionTracks") - .try &.as_a - - captions_list = [] of CaptionMetadata - return captions_list if caption_tracks.nil? - - caption_tracks.each do |caption| - name = caption["name"]["simpleText"]? || caption["name"]["runs"][0]["text"] - name = name.to_s.split(" - ")[0] - - language_code = caption["languageCode"].to_s - base_url = caption["baseUrl"].to_s - - auto_generated = false - if caption["kind"]? && caption["kind"] == "asr" - auto_generated = true - end - - captions_list << CaptionMetadata.new(name, language_code, base_url, auto_generated) + def initialize(@name, @language_code, @base_url, @auto_generated) end - return captions_list - end + # Parse the JSON structure from Youtube + def self.from_yt_json(container : JSON::Any) : Array(Captions::Metadata) + caption_tracks = container + .dig?("playerCaptionsTracklistRenderer", "captionTracks") + .try &.as_a - def timedtext_to_vtt(timedtext : String, tlang = nil) : String - # In the future, we could just directly work with the url. This is more of a POC - cues = [] of XML::Node - tree = XML.parse(timedtext) - tree = tree.children.first + captions_list = [] of Captions::Metadata + return captions_list if caption_tracks.nil? - tree.children.each do |item| - if item.name == "body" - item.children.each do |cue| - if cue.name == "p" && !(cue.children.size == 1 && cue.children[0].content == "\n") - cues << cue + caption_tracks.each do |caption| + name = caption["name"]["simpleText"]? || caption["name"]["runs"][0]["text"] + name = name.to_s.split(" - ")[0] + + language_code = caption["languageCode"].to_s + base_url = caption["baseUrl"].to_s + + auto_generated = false + if caption["kind"]? && caption["kind"] == "asr" + auto_generated = true + end + + captions_list << Captions::Metadata.new(name, language_code, base_url, auto_generated) + end + + return captions_list + end + + def timedtext_to_vtt(timedtext : String, tlang = nil) : String + # In the future, we could just directly work with the url. This is more of a POC + cues = [] of XML::Node + tree = XML.parse(timedtext) + tree = tree.children.first + + tree.children.each do |item| + if item.name == "body" + item.children.each do |cue| + if cue.name == "p" && !(cue.children.size == 1 && cue.children[0].content == "\n") + cues << cue + end end + break end - break end - end - result = String.build do |result| - result << <<-END_VTT - WEBVTT - Kind: captions - Language: #{tlang || @language_code} + result = String.build do |result| + result << <<-END_VTT + WEBVTT + Kind: captions + Language: #{tlang || @language_code} - END_VTT + END_VTT - result << "\n\n" + result << "\n\n" - cues.each_with_index do |node, i| - start_time = node["t"].to_f.milliseconds + cues.each_with_index do |node, i| + start_time = node["t"].to_f.milliseconds - duration = node["d"]?.try &.to_f.milliseconds + duration = node["d"]?.try &.to_f.milliseconds - duration ||= start_time + duration ||= start_time - if cues.size > i + 1 - end_time = cues[i + 1]["t"].to_f.milliseconds - else - end_time = start_time + duration + if cues.size > i + 1 + end_time = cues[i + 1]["t"].to_f.milliseconds + else + end_time = start_time + duration + end + + # start_time + result << start_time.hours.to_s.rjust(2, '0') + result << ':' << start_time.minutes.to_s.rjust(2, '0') + result << ':' << start_time.seconds.to_s.rjust(2, '0') + result << '.' << start_time.milliseconds.to_s.rjust(3, '0') + + result << " --> " + + # end_time + result << end_time.hours.to_s.rjust(2, '0') + result << ':' << end_time.minutes.to_s.rjust(2, '0') + result << ':' << end_time.seconds.to_s.rjust(2, '0') + result << '.' << end_time.milliseconds.to_s.rjust(3, '0') + + result << "\n" + + node.children.each do |s| + result << s.content + end + result << "\n" + result << "\n" end - - # start_time - result << start_time.hours.to_s.rjust(2, '0') - result << ':' << start_time.minutes.to_s.rjust(2, '0') - result << ':' << start_time.seconds.to_s.rjust(2, '0') - result << '.' << start_time.milliseconds.to_s.rjust(3, '0') - - result << " --> " - - # end_time - result << end_time.hours.to_s.rjust(2, '0') - result << ':' << end_time.minutes.to_s.rjust(2, '0') - result << ':' << end_time.seconds.to_s.rjust(2, '0') - result << '.' << end_time.milliseconds.to_s.rjust(3, '0') - - result << "\n" - - node.children.each do |s| - result << s.content - end - result << "\n" - result << "\n" end + return result end - return result end # List of all caption languages available on Youtube. diff --git a/src/invidious/videos/transcript.cr b/src/invidious/videos/transcript.cr index ba2728cd..c86b3988 100644 --- a/src/invidious/videos/transcript.cr +++ b/src/invidious/videos/transcript.cr @@ -37,7 +37,7 @@ module Invidious::Videos # Convert into array of TranscriptLine lines = self.parse(initial_data) - # Taken from Invidious::Videos::CaptionMetadata.timedtext_to_vtt() + # Taken from Invidious::Videos::Captions::Metadata.timedtext_to_vtt() vtt = String.build do |vtt| vtt << <<-END_VTT WEBVTT diff --git a/src/invidious/views/user/preferences.ecr b/src/invidious/views/user/preferences.ecr index b1061ee8..55349c5a 100644 --- a/src/invidious/views/user/preferences.ecr +++ b/src/invidious/views/user/preferences.ecr @@ -89,7 +89,7 @@ <% preferences.captions.each_with_index do |caption, index| %> From 7d435f082bf24c1122c95ecc92efee4a39a7b539 Mon Sep 17 00:00:00 2001 From: syeopite <70992037+syeopite@users.noreply.github.com> Date: Thu, 24 Aug 2023 23:20:20 +0000 Subject: [PATCH 083/143] Update src/invidious/videos/transcript.cr Co-authored-by: Samantaz Fox --- src/invidious/videos/transcript.cr | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/invidious/videos/transcript.cr b/src/invidious/videos/transcript.cr index c86b3988..f3360a52 100644 --- a/src/invidious/videos/transcript.cr +++ b/src/invidious/videos/transcript.cr @@ -4,16 +4,13 @@ module Invidious::Videos record TranscriptLine, start_ms : Time::Span, end_ms : Time::Span, line : String def self.generate_param(video_id : String, language_code : String, auto_generated : Bool) : String - if !auto_generated - is_auto_generated = "" - elsif is_auto_generated = "asr" - end + kind = auto_generated ? "asr" : "" object = { "1:0:string" => video_id, "2:base64" => { - "1:string" => is_auto_generated, + "1:string" => kind, "2:string" => language_code, "3:string" => "", }, From 3615bb0e62209cfad4825e8c40d8e6de69aac687 Mon Sep 17 00:00:00 2001 From: syeopite Date: Thu, 24 Aug 2023 16:21:05 -0700 Subject: [PATCH 084/143] Update src/invidious/videos/caption.cr Co-authored-by: Samantaz Fox --- src/invidious/videos/caption.cr | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/invidious/videos/caption.cr b/src/invidious/videos/caption.cr index 82b68dcd..256dfcc0 100644 --- a/src/invidious/videos/caption.cr +++ b/src/invidious/videos/caption.cr @@ -28,10 +28,7 @@ module Invidious::Videos language_code = caption["languageCode"].to_s base_url = caption["baseUrl"].to_s - auto_generated = false - if caption["kind"]? && caption["kind"] == "asr" - auto_generated = true - end + auto_generated = (caption["kind"]? == "asr") captions_list << Captions::Metadata.new(name, language_code, base_url, auto_generated) end From 1377f2ce7d0a8fed716e8e285902bfbfef1a17e0 Mon Sep 17 00:00:00 2001 From: Samantaz Fox Date: Fri, 25 Aug 2023 08:24:25 +0200 Subject: [PATCH 085/143] Revert broken i18next v3 changes made by weblate --- locales/fr.json | 80 +++++++++++++++++++--------------------------- locales/it.json | 80 +++++++++++++++++++--------------------------- locales/pt-BR.json | 80 +++++++++++++++++++--------------------------- locales/pt.json | 80 +++++++++++++++++++--------------------------- 4 files changed, 128 insertions(+), 192 deletions(-) diff --git a/locales/fr.json b/locales/fr.json index 5e0f5152..286ae361 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -1,19 +1,14 @@ { - "generic_views_count_0": "{{count}} vue", - "generic_views_count_1": "{{count}} vues", - "generic_views_count_2": "{{count}} vues", - "generic_videos_count_0": "{{count}} vidéo", - "generic_videos_count_1": "{{count}} vidéos", - "generic_videos_count_2": "{{count}} vidéos", - "generic_playlists_count_0": "{{count}} liste de lecture", - "generic_playlists_count_1": "{{count}} listes de lecture", - "generic_playlists_count_2": "{{count}} listes de lecture", - "generic_subscribers_count_0": "{{count}} abonné", - "generic_subscribers_count_1": "{{count}} abonnés", - "generic_subscribers_count_2": "{{count}} abonnés", - "generic_subscriptions_count_0": "{{count}} abonnement", - "generic_subscriptions_count_1": "{{count}} abonnements", - "generic_subscriptions_count_2": "{{count}} abonnements", + "generic_views_count": "{{count}} vue", + "generic_views_count_plural": "{{count}} vues", + "generic_videos_count": "{{count}} vidéo", + "generic_videos_count_plural": "{{count}} vidéos", + "generic_playlists_count": "{{count}} liste de lecture", + "generic_playlists_count_plural": "{{count}} listes de lecture", + "generic_subscribers_count": "{{count}} abonné", + "generic_subscribers_count_plural": "{{count}} abonnés", + "generic_subscriptions_count": "{{count}} abonnement", + "generic_subscriptions_count_plural": "{{count}} abonnements", "generic_button_delete": "Supprimer", "generic_button_edit": "Editer", "generic_button_save": "Enregistrer", @@ -133,16 +128,14 @@ "Subscription manager": "Gestionnaire d'abonnement", "Token manager": "Gestionnaire de token", "Token": "Token", - "tokens_count_0": "{{count}} jeton", - "tokens_count_1": "{{count}} jetons", - "tokens_count_2": "{{count}} jetons", + "tokens_count": "{{count}} jeton", + "tokens_count_plural": "{{count}} jetons", "Import/export": "Importer/Exporter", "unsubscribe": "se désabonner", "revoke": "révoquer", "Subscriptions": "Abonnements", - "subscriptions_unseen_notifs_count_0": "{{count}} notification non vue", - "subscriptions_unseen_notifs_count_1": "{{count}} notifications non vues", - "subscriptions_unseen_notifs_count_2": "{{count}} notifications non vues", + "subscriptions_unseen_notifs_count": "{{count}} notification non vue", + "subscriptions_unseen_notifs_count_plural": "{{count}} notifications non vues", "search": "rechercher", "Log out": "Se déconnecter", "Released under the AGPLv3 on Github.": "Publié sous licence AGPLv3 sur GitHub.", @@ -204,14 +197,12 @@ "This channel does not exist.": "Cette chaine n'existe pas.", "Could not get channel info.": "Impossible de charger les informations de cette chaîne.", "Could not fetch comments": "Impossible de charger les commentaires", - "comments_view_x_replies_0": "Voir {{count}} réponse", - "comments_view_x_replies_1": "Voir {{count}} réponses", - "comments_view_x_replies_2": "Voir {{count}} réponses", + "comments_view_x_replies": "Voir {{count}} réponse", + "comments_view_x_replies_plural": "Voir {{count}} réponses", "`x` ago": "il y a `x`", "Load more": "Voir plus", - "comments_points_count_0": "{{count}} point", - "comments_points_count_1": "{{count}} points", - "comments_points_count_2": "{{count}} points", + "comments_points_count": "{{count}} point", + "comments_points_count_plural": "{{count}} points", "Could not create mix.": "Impossible de charger cette liste de lecture.", "Empty playlist": "La liste de lecture est vide", "Not a playlist.": "La liste de lecture est invalide.", @@ -329,27 +320,20 @@ "Yiddish": "Yiddish", "Yoruba": "Yoruba", "Zulu": "Zoulou", - "generic_count_years_0": "{{count}} an", - "generic_count_years_1": "{{count}} ans", - "generic_count_years_2": "{{count}} ans", - "generic_count_months_0": "{{count}} mois", - "generic_count_months_1": "{{count}} mois", - "generic_count_months_2": "{{count}} mois", - "generic_count_weeks_0": "{{count}} semaine", - "generic_count_weeks_1": "{{count}} semaines", - "generic_count_weeks_2": "{{count}} semaines", - "generic_count_days_0": "{{count}} jour", - "generic_count_days_1": "{{count}} jours", - "generic_count_days_2": "{{count}} jours", - "generic_count_hours_0": "{{count}} heure", - "generic_count_hours_1": "{{count}} heures", - "generic_count_hours_2": "{{count}} heures", - "generic_count_minutes_0": "{{count}} minute", - "generic_count_minutes_1": "{{count}} minutes", - "generic_count_minutes_2": "{{count}} minutes", - "generic_count_seconds_0": "{{count}} seconde", - "generic_count_seconds_1": "{{count}} secondes", - "generic_count_seconds_2": "{{count}} secondes", + "generic_count_years": "{{count}} an", + "generic_count_years_plural": "{{count}} ans", + "generic_count_months": "{{count}} mois", + "generic_count_months_plural": "{{count}} mois", + "generic_count_weeks": "{{count}} semaine", + "generic_count_weeks_plural": "{{count}} semaines", + "generic_count_days": "{{count}} jour", + "generic_count_days_plural": "{{count}} jours", + "generic_count_hours": "{{count}} heure", + "generic_count_hours_plural": "{{count}} heures", + "generic_count_minutes": "{{count}} minute", + "generic_count_minutes_plural": "{{count}} minutes", + "generic_count_seconds": "{{count}} seconde", + "generic_count_seconds_plural": "{{count}} secondes", "Fallback comments: ": "Commentaires alternatifs : ", "Popular": "Populaire", "Search": "Rechercher", diff --git a/locales/it.json b/locales/it.json index f7463ee3..894eb97f 100644 --- a/locales/it.json +++ b/locales/it.json @@ -1,13 +1,10 @@ { - "generic_subscribers_count_0": "{{count}} iscritto", - "generic_subscribers_count_1": "{{count}} iscritti", - "generic_subscribers_count_2": "{{count}} iscritti", - "generic_videos_count_0": "{{count}} video", - "generic_videos_count_1": "{{count}} video", - "generic_videos_count_2": "{{count}} video", - "generic_playlists_count_0": "{{count}} playlist", - "generic_playlists_count_1": "{{count}} playlist", - "generic_playlists_count_2": "{{count}} playlist", + "generic_subscribers_count": "{{count}} iscritto", + "generic_subscribers_count_plural": "{{count}} iscritti", + "generic_videos_count": "{{count}} video", + "generic_videos_count_plural": "{{count}} video", + "generic_playlists_count": "{{count}} playlist", + "generic_playlists_count_plural": "{{count}} playlist", "LIVE": "IN DIRETTA", "Shared `x` ago": "Condiviso `x` fa", "Unsubscribe": "Disiscriviti", @@ -116,19 +113,16 @@ "Subscription manager": "Gestione delle iscrizioni", "Token manager": "Gestione dei gettoni", "Token": "Gettone", - "generic_subscriptions_count_0": "{{count}} iscrizione", - "generic_subscriptions_count_1": "{{count}} iscrizioni", - "generic_subscriptions_count_2": "{{count}} iscrizioni", - "tokens_count_0": "{{count}} gettone", - "tokens_count_1": "{{count}} gettoni", - "tokens_count_2": "{{count}} gettoni", + "generic_subscriptions_count": "{{count}} iscrizione", + "generic_subscriptions_count_plural": "{{count}} iscrizioni", + "tokens_count": "{{count}} gettone", + "tokens_count_plural": "{{count}} gettoni", "Import/export": "Importa/esporta", "unsubscribe": "disiscriviti", "revoke": "revoca", "Subscriptions": "Iscrizioni", - "subscriptions_unseen_notifs_count_0": "{{count}} notifica non visualizzata", - "subscriptions_unseen_notifs_count_1": "{{count}} notifiche non visualizzate", - "subscriptions_unseen_notifs_count_2": "{{count}} notifiche non visualizzate", + "subscriptions_unseen_notifs_count": "{{count}} notifica non visualizzata", + "subscriptions_unseen_notifs_count_plural": "{{count}} notifiche non visualizzate", "search": "Cerca", "Log out": "Esci", "Source available here.": "Codice sorgente.", @@ -157,9 +151,8 @@ "Whitelisted regions: ": "Regioni in lista bianca: ", "Blacklisted regions: ": "Regioni in lista nera: ", "Shared `x`": "Condiviso `x`", - "generic_views_count_0": "{{count}} visualizzazione", - "generic_views_count_1": "{{count}} visualizzazioni", - "generic_views_count_2": "{{count}} visualizzazioni", + "generic_views_count": "{{count}} visualizzazione", + "generic_views_count_plural": "{{count}} visualizzazioni", "Premieres in `x`": "In anteprima in `x`", "Premieres `x`": "In anteprima `x`", "Hi! Looks like you have JavaScript turned off. Click here to view comments, keep in mind they may take a bit longer to load.": "Ciao, Sembra che tu abbia disattivato JavaScript. Clicca qui per visualizzare i commenti, ma considera che il caricamento potrebbe richiedere più tempo.", @@ -307,27 +300,20 @@ "Yiddish": "Yiddish", "Yoruba": "Yoruba", "Zulu": "Zulu", - "generic_count_years_0": "{{count}} anno", - "generic_count_years_1": "{{count}} anni", - "generic_count_years_2": "{{count}} anni", - "generic_count_months_0": "{{count}} mese", - "generic_count_months_1": "{{count}} mesi", - "generic_count_months_2": "{{count}} mesi", - "generic_count_weeks_0": "{{count}} settimana", - "generic_count_weeks_1": "{{count}} settimane", - "generic_count_weeks_2": "{{count}} settimane", - "generic_count_days_0": "{{count}} giorno", - "generic_count_days_1": "{{count}} giorni", - "generic_count_days_2": "{{count}} giorni", - "generic_count_hours_0": "{{count}} ora", - "generic_count_hours_1": "{{count}} ore", - "generic_count_hours_2": "{{count}} ore", - "generic_count_minutes_0": "{{count}} minuto", - "generic_count_minutes_1": "{{count}} minuti", - "generic_count_minutes_2": "{{count}} minuti", - "generic_count_seconds_0": "{{count}} secondo", - "generic_count_seconds_1": "{{count}} secondi", - "generic_count_seconds_2": "{{count}} secondi", + "generic_count_years": "{{count}} anno", + "generic_count_years_plural": "{{count}} anni", + "generic_count_months": "{{count}} mese", + "generic_count_months_plural": "{{count}} mesi", + "generic_count_weeks": "{{count}} settimana", + "generic_count_weeks_plural": "{{count}} settimane", + "generic_count_days": "{{count}} giorno", + "generic_count_days_plural": "{{count}} giorni", + "generic_count_hours": "{{count}} ora", + "generic_count_hours_plural": "{{count}} ore", + "generic_count_minutes": "{{count}} minuto", + "generic_count_minutes_plural": "{{count}} minuti", + "generic_count_seconds": "{{count}} secondo", + "generic_count_seconds_plural": "{{count}} secondi", "Fallback comments: ": "Commenti alternativi: ", "Popular": "Popolare", "Search": "Cerca", @@ -431,12 +417,10 @@ "search_filters_duration_option_short": "Corto (< 4 minuti)", "search_filters_duration_option_long": "Lungo (> 20 minuti)", "search_filters_features_option_purchased": "Acquistato", - "comments_view_x_replies_0": "Vedi {{count}} risposta", - "comments_view_x_replies_1": "Vedi {{count}} risposte", - "comments_view_x_replies_2": "Vedi {{count}} risposte", - "comments_points_count_0": "{{count}} punto", - "comments_points_count_1": "{{count}} punti", - "comments_points_count_2": "{{count}} punti", + "comments_view_x_replies": "Vedi {{count}} risposta", + "comments_view_x_replies_plural": "Vedi {{count}} risposte", + "comments_points_count": "{{count}} punto", + "comments_points_count_plural": "{{count}} punti", "Portuguese (auto-generated)": "Portoghese (generati automaticamente)", "crash_page_you_found_a_bug": "Sembra che tu abbia trovato un bug in Invidious!", "crash_page_switch_instance": "provato a usare un'altra istanza", diff --git a/locales/pt-BR.json b/locales/pt-BR.json index 7d522ed5..68a6e3ab 100644 --- a/locales/pt-BR.json +++ b/locales/pt-BR.json @@ -112,9 +112,8 @@ "Subscription manager": "Gerenciador de inscrições", "Token manager": "Gerenciador de tokens", "Token": "Token", - "tokens_count_0": "{{count}} token", - "tokens_count_1": "{{count}} tokens", - "tokens_count_2": "{{count}} tokens", + "tokens_count": "{{count}} token", + "tokens_count_plural": "{{count}} tokens", "Import/export": "Importar/Exportar", "unsubscribe": "cancelar inscrição", "revoke": "revogar", @@ -298,27 +297,20 @@ "Yiddish": "Iídiche", "Yoruba": "Iorubá", "Zulu": "Zulu", - "generic_count_years_0": "{{count}} ano", - "generic_count_years_1": "{{count}} anos", - "generic_count_years_2": "{{count}} anos", - "generic_count_months_0": "{{count}} mês", - "generic_count_months_1": "{{count}} meses", - "generic_count_months_2": "{{count}} meses", - "generic_count_weeks_0": "{{count}} semana", - "generic_count_weeks_1": "{{count}} semanas", - "generic_count_weeks_2": "{{count}} semanas", - "generic_count_days_0": "{{count}} dia", - "generic_count_days_1": "{{count}} dias", - "generic_count_days_2": "{{count}} dias", - "generic_count_hours_0": "{{count}} hora", - "generic_count_hours_1": "{{count}} horas", - "generic_count_hours_2": "{{count}} horas", - "generic_count_minutes_0": "{{count}} minuto", - "generic_count_minutes_1": "{{count}} minutos", - "generic_count_minutes_2": "{{count}} minutos", - "generic_count_seconds_0": "{{count}} segundo", - "generic_count_seconds_1": "{{count}} segundos", - "generic_count_seconds_2": "{{count}} segundos", + "generic_count_years": "{{count}} ano", + "generic_count_years_plural": "{{count}} anos", + "generic_count_months": "{{count}} mês", + "generic_count_months_plural": "{{count}} meses", + "generic_count_weeks": "{{count}} semana", + "generic_count_weeks_plural": "{{count}} semanas", + "generic_count_days": "{{count}} dia", + "generic_count_days_plural": "{{count}} dias", + "generic_count_hours": "{{count}} hora", + "generic_count_hours_plural": "{{count}} horas", + "generic_count_minutes": "{{count}} minuto", + "generic_count_minutes_plural": "{{count}} minutos", + "generic_count_seconds": "{{count}} segundo", + "generic_count_seconds_plural": "{{count}} segundos", "Fallback comments: ": "Comentários alternativos: ", "Popular": "Populares", "Search": "Procurar", @@ -385,27 +377,20 @@ "preferences_quality_dash_label": "Qualidade de vídeo do painel preferida: ", "preferences_region_label": "País do conteúdo: ", "preferences_quality_dash_option_4320p": "4320p", - "generic_videos_count_0": "{{count}} vídeo", - "generic_videos_count_1": "{{count}} vídeos", - "generic_videos_count_2": "{{count}} vídeos", - "generic_playlists_count_0": "{{count}} lista de reprodução", - "generic_playlists_count_1": "{{count}} listas de reprodução", - "generic_playlists_count_2": "{{count}} listas de reprodução", - "generic_subscribers_count_0": "{{count}} inscrito", - "generic_subscribers_count_1": "{{count}} inscritos", - "generic_subscribers_count_2": "{{count}} inscritos", - "generic_subscriptions_count_0": "{{count}} inscrição", - "generic_subscriptions_count_1": "{{count}} inscrições", - "generic_subscriptions_count_2": "{{count}} inscrições", - "subscriptions_unseen_notifs_count_0": "{{count}} notificação não vista", - "subscriptions_unseen_notifs_count_1": "{{count}} notificações não vistas", - "subscriptions_unseen_notifs_count_2": "{{count}} notificações não vistas", - "comments_view_x_replies_0": "Ver {{count}} resposta", - "comments_view_x_replies_1": "Ver {{count}} respostas", - "comments_view_x_replies_2": "Ver {{count}} respostas", - "comments_points_count_0": "{{count}} ponto", - "comments_points_count_1": "{{count}} pontos", - "comments_points_count_2": "{{count}} pontos", + "generic_videos_count": "{{count}} vídeo", + "generic_videos_count_plural": "{{count}} vídeos", + "generic_playlists_count": "{{count}} lista de reprodução", + "generic_playlists_count_plural": "{{count}} listas de reprodução", + "generic_subscribers_count": "{{count}} inscrito", + "generic_subscribers_count_plural": "{{count}} inscritos", + "generic_subscriptions_count": "{{count}} inscrição", + "generic_subscriptions_count_plural": "{{count}} inscrições", + "subscriptions_unseen_notifs_count": "{{count}} notificação não vista", + "subscriptions_unseen_notifs_count_plural": "{{count}} notificações não vistas", + "comments_view_x_replies": "Ver {{count}} resposta", + "comments_view_x_replies_plural": "Ver {{count}} respostas", + "comments_points_count": "{{count}} ponto", + "comments_points_count_plural": "{{count}} pontos", "crash_page_you_found_a_bug": "Parece que você encontrou um erro no Invidious!", "crash_page_before_reporting": "Antes de reportar um erro, verifique se você:", "preferences_save_player_pos_label": "Salvar a posição de reprodução: ", @@ -415,9 +400,8 @@ "crash_page_search_issue": "procurou por um erro existente no GitHub", "crash_page_report_issue": "Se nenhuma opção acima ajudou, por favor abra um novo problema no Github (preferencialmente em inglês) e inclua o seguinte texto (NÃO traduza):", "crash_page_read_the_faq": "leia as Perguntas frequentes (FAQ)", - "generic_views_count_0": "{{count}} visualização", - "generic_views_count_1": "{{count}} visualizações", - "generic_views_count_2": "{{count}} visualizações", + "generic_views_count": "{{count}} visualização", + "generic_views_count_plural": "{{count}} visualizações", "preferences_quality_option_dash": "DASH (qualidade adaptável)", "preferences_quality_option_hd720": "HD720", "preferences_quality_option_small": "Pequeno", diff --git a/locales/pt.json b/locales/pt.json index df63abe6..e7cc4810 100644 --- a/locales/pt.json +++ b/locales/pt.json @@ -44,27 +44,20 @@ "Default": "Predefinido", "Top": "Destaques", "Search": "Pesquisar", - "generic_count_years_0": "{{count}} segundo", - "generic_count_years_1": "{{count}} segundos", - "generic_count_years_2": "{{count}} segundos", - "generic_count_months_0": "{{count}} minuto", - "generic_count_months_1": "{{count}} minutos", - "generic_count_months_2": "{{count}} minutos", - "generic_count_weeks_0": "{{count}} hora", - "generic_count_weeks_1": "{{count}} horas", - "generic_count_weeks_2": "{{count}} horas", - "generic_count_days_0": "{{count}} dia", - "generic_count_days_1": "{{count}} dias", - "generic_count_days_2": "{{count}} dias", - "generic_count_hours_0": "{{count}} seman", - "generic_count_hours_1": "{{count}} semanas", - "generic_count_hours_2": "{{count}} semanas", - "generic_count_minutes_0": "{{count}} mês", - "generic_count_minutes_1": "{{count}} meses", - "generic_count_minutes_2": "{{count}} meses", - "generic_count_seconds_0": "{{count}} ano", - "generic_count_seconds_1": "{{count}} anos", - "generic_count_seconds_2": "{{count}} anos", + "generic_count_years": "{{count}} segundo", + "generic_count_years_plural": "{{count}} segundos", + "generic_count_months": "{{count}} minuto", + "generic_count_months_plural": "{{count}} minutos", + "generic_count_weeks": "{{count}} hora", + "generic_count_weeks_plural": "{{count}} horas", + "generic_count_days": "{{count}} dia", + "generic_count_days_plural": "{{count}} dias", + "generic_count_hours": "{{count}} seman", + "generic_count_hours_plural": "{{count}} semanas", + "generic_count_minutes": "{{count}} mês", + "generic_count_minutes_plural": "{{count}} meses", + "generic_count_seconds": "{{count}} ano", + "generic_count_seconds_plural": "{{count}} anos", "Chinese (Traditional)": "Chinês (tradicional)", "Chinese (Simplified)": "Chinês (simplificado)", "Could not pull trending pages.": "Não foi possível obter as páginas de tendências.", @@ -174,9 +167,8 @@ "Log out": "Terminar sessão", "Subscriptions": "Subscrições", "revoke": "revogar", - "tokens_count_0": "{{count}} token", - "tokens_count_1": "{{count}} tokens", - "tokens_count_2": "{{count}} tokens", + "tokens_count": "{{count}} token", + "tokens_count_plural": "{{count}} tokens", "Token": "Token", "Token manager": "Gerir tokens", "Subscription manager": "Gerir subscrições", @@ -410,32 +402,24 @@ "videoinfo_youTube_embed_link": "Incorporar", "preferences_save_player_pos_label": "Guardar a posição de reprodução atual do vídeo: ", "download_subtitles": "Legendas - `x` (.vtt)", - "generic_views_count_0": "{{count}} visualização", - "generic_views_count_1": "{{count}} visualizações", - "generic_views_count_2": "{{count}} visualizações", + "generic_views_count": "{{count}} visualização", + "generic_views_count_plural": "{{count}} visualizações", "videoinfo_started_streaming_x_ago": "Iniciou a transmissão há `x`", "user_saved_playlists": "`x` listas de reprodução guardadas", - "generic_videos_count_0": "{{count}} vídeo", - "generic_videos_count_1": "{{count}} vídeos", - "generic_videos_count_2": "{{count}} vídeos", - "generic_playlists_count_0": "{{count}} lista de reprodução", - "generic_playlists_count_1": "{{count}} listas de reprodução", - "generic_playlists_count_2": "{{count}} listas de reprodução", - "subscriptions_unseen_notifs_count_0": "{{count}} notificação não vista", - "subscriptions_unseen_notifs_count_1": "{{count}} notificações não vistas", - "subscriptions_unseen_notifs_count_2": "{{count}} notificações não vistas", - "comments_view_x_replies_0": "Ver {{count}} resposta", - "comments_view_x_replies_1": "Ver {{count}} respostas", - "comments_view_x_replies_2": "Ver {{count}} respostas", - "generic_subscribers_count_0": "{{count}} inscrito", - "generic_subscribers_count_1": "{{count}} inscritos", - "generic_subscribers_count_2": "{{count}} inscritos", - "generic_subscriptions_count_0": "{{count}} inscrição", - "generic_subscriptions_count_1": "{{count}} inscrições", - "generic_subscriptions_count_2": "{{count}} inscrições", - "comments_points_count_0": "{{count}} ponto", - "comments_points_count_1": "{{count}} pontos", - "comments_points_count_2": "{{count}} pontos", + "generic_videos_count": "{{count}} vídeo", + "generic_videos_count_plural": "{{count}} vídeos", + "generic_playlists_count": "{{count}} lista de reprodução", + "generic_playlists_count_plural": "{{count}} listas de reprodução", + "subscriptions_unseen_notifs_count": "{{count}} notificação não vista", + "subscriptions_unseen_notifs_count_plural": "{{count}} notificações não vistas", + "comments_view_x_replies": "Ver {{count}} resposta", + "comments_view_x_replies_plural": "Ver {{count}} respostas", + "generic_subscribers_count": "{{count}} inscrito", + "generic_subscribers_count_plural": "{{count}} inscritos", + "generic_subscriptions_count": "{{count}} inscrição", + "generic_subscriptions_count_plural": "{{count}} inscrições", + "comments_points_count": "{{count}} ponto", + "comments_points_count_plural": "{{count}} pontos", "crash_page_you_found_a_bug": "Parece que encontrou um erro no Invidious!", "crash_page_before_reporting": "Antes de reportar um erro, verifique se:", "crash_page_refresh": "tentou recarregar a página", From eabcea6f4a16a47555d945460ac824588ff546e5 Mon Sep 17 00:00:00 2001 From: syeopite <70992037+syeopite@users.noreply.github.com> Date: Tue, 29 Aug 2023 06:18:35 +0000 Subject: [PATCH 086/143] Remove trailing whitespace in config documentation Co-authored-by: Samantaz Fox --- config/config.example.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/config.example.yml b/config/config.example.yml index 51beab89..c6051bce 100644 --- a/config/config.example.yml +++ b/config/config.example.yml @@ -187,7 +187,7 @@ https_only: false ## ## Useful for larger instances as InnerTube is **not ratelimited**. See https://github.com/iv-org/invidious/issues/2567 ## -## Subtitle experience may differ slightly on Invidious. +## Subtitle experience may differ slightly on Invidious. ## ## Accepted values: true, false ## Default: false From d7696574f4a281d7450176097c87bca08705734a Mon Sep 17 00:00:00 2001 From: ChunkyProgrammer <78101139+ChunkyProgrammer@users.noreply.github.com> Date: Tue, 1 Aug 2023 08:55:23 -0700 Subject: [PATCH 087/143] Playlist: Use subtitle when author is missing --- src/invidious/playlists.cr | 5 +++++ src/invidious/views/playlist.ecr | 6 +++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/src/invidious/playlists.cr b/src/invidious/playlists.cr index 013be268..955e0855 100644 --- a/src/invidious/playlists.cr +++ b/src/invidious/playlists.cr @@ -89,6 +89,7 @@ struct Playlist property views : Int64 property updated : Time property thumbnail : String? + property subtitle : String? def to_json(offset, json : JSON::Builder, video_id : String? = nil) json.object do @@ -100,6 +101,7 @@ struct Playlist json.field "author", self.author json.field "authorId", self.ucid json.field "authorUrl", "/channel/#{self.ucid}" + json.field "subtitle", self.subtitle json.field "authorThumbnails" do json.array do @@ -356,6 +358,8 @@ def fetch_playlist(plid : String) updated = Time.utc video_count = 0 + subtitle = extract_text(initial_data.dig?("header", "playlistHeaderRenderer", "subtitle")) + playlist_info["stats"]?.try &.as_a.each do |stat| text = stat["runs"]?.try &.as_a.map(&.["text"].as_s).join("") || stat["simpleText"]?.try &.as_s next if !text @@ -397,6 +401,7 @@ def fetch_playlist(plid : String) views: views, updated: updated, thumbnail: thumbnail, + subtitle: subtitle, }) end diff --git a/src/invidious/views/playlist.ecr b/src/invidious/views/playlist.ecr index ee9ba87b..3bc7596e 100644 --- a/src/invidious/views/playlist.ecr +++ b/src/invidious/views/playlist.ecr @@ -70,7 +70,11 @@ <% else %> - <%= author %> | + <% if !author.empty? %> + <%= author %> | + <% elsif !playlist.subtitle.nil? %> + <%= playlist.subtitle.try &.split(" • ")[0] %> | + <% end %> <%= translate_count(locale, "generic_videos_count", playlist.video_count) %> | <%= translate(locale, "Updated `x` ago", recode_date(playlist.updated, locale)) %> From afb04c3bdaa29f19db44f6560ce7954bc656d791 Mon Sep 17 00:00:00 2001 From: ChunkyProgrammer <78101139+ChunkyProgrammer@users.noreply.github.com> Date: Mon, 7 Aug 2023 11:58:20 -0700 Subject: [PATCH 088/143] HTMLl.Escape the playlist subtitle --- src/invidious/views/playlist.ecr | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/invidious/views/playlist.ecr b/src/invidious/views/playlist.ecr index 3bc7596e..24ba437d 100644 --- a/src/invidious/views/playlist.ecr +++ b/src/invidious/views/playlist.ecr @@ -73,7 +73,8 @@ <% if !author.empty? %> <%= author %> | <% elsif !playlist.subtitle.nil? %> - <%= playlist.subtitle.try &.split(" • ")[0] %> | + <% subtitle = playlist.subtitle || "" %> + <%= HTML.escape(subtitle[0..subtitle.rindex(" • ") || subtitle.size]) %> | <% end %> <%= translate_count(locale, "generic_videos_count", playlist.video_count) %> | <%= translate(locale, "Updated `x` ago", recode_date(playlist.updated, locale)) %> From 49b9316b9f2e9ccc6921a2f293abacb37f9805f6 Mon Sep 17 00:00:00 2001 From: Samantaz Fox Date: Wed, 13 Sep 2023 23:40:20 +0200 Subject: [PATCH 089/143] Routing: Handle current and future routes more nicely --- src/invidious/routes/channels.cr | 19 +++++++++++++---- src/invidious/routing.cr | 36 +++++++++++++++++++++----------- 2 files changed, 39 insertions(+), 16 deletions(-) diff --git a/src/invidious/routes/channels.cr b/src/invidious/routes/channels.cr index 9892ae2a..5500672f 100644 --- a/src/invidious/routes/channels.cr +++ b/src/invidious/routes/channels.cr @@ -217,6 +217,11 @@ module Invidious::Routes::Channels env.redirect "/channel/#{ucid}" end + private KNOWN_TABS = { + "home", "videos", "shorts", "streams", "podcasts", + "releases", "playlists", "community", "channels", "about", + } + # Redirects brand url channels to a normal /channel/:ucid route def self.brand_redirect(env) locale = env.get("preferences").as(Preferences).locale @@ -227,7 +232,10 @@ module Invidious::Routes::Channels yt_url_params = URI::Params.encode(env.params.query.to_h.select(["a", "u", "user"])) # Retrieves URL params that only Invidious uses - invidious_url_params = URI::Params.encode(env.params.query.to_h.select!(["a", "u", "user"])) + invidious_url_params = env.params.query.dup + invidious_url_params.delete_all("a") + invidious_url_params.delete_all("u") + invidious_url_params.delete_all("user") begin resolved_url = YoutubeAPI.resolve_url("https://youtube.com#{env.request.path}#{yt_url_params.size > 0 ? "?#{yt_url_params}" : ""}") @@ -236,14 +244,17 @@ module Invidious::Routes::Channels return error_template(404, translate(locale, "This channel does not exist.")) end - selected_tab = env.request.path.split("/")[-1] - if {"home", "videos", "shorts", "streams", "playlists", "community", "channels", "about"}.includes? selected_tab + selected_tab = env.params.url["tab"]? + + if KNOWN_TABS.includes? selected_tab url = "/channel/#{ucid}/#{selected_tab}" else url = "/channel/#{ucid}" end - env.redirect url + url += "?#{invidious_url_params}" if !invidious_url_params.empty? + + return env.redirect url end # Handles redirects for the /profile endpoint diff --git a/src/invidious/routing.cr b/src/invidious/routing.cr index 9c43171c..5ec7fae3 100644 --- a/src/invidious/routing.cr +++ b/src/invidious/routing.cr @@ -124,22 +124,34 @@ module Invidious::Routing get "/channel/:ucid/community", Routes::Channels, :community get "/channel/:ucid/channels", Routes::Channels, :channels get "/channel/:ucid/about", Routes::Channels, :about + get "/channel/:ucid/live", Routes::Channels, :live get "/user/:user/live", Routes::Channels, :live get "/c/:user/live", Routes::Channels, :live - {"", "/videos", "/shorts", "/streams", "/playlists", "/community", "/about"}.each do |path| - # /c/LinusTechTips - get "/c/:user#{path}", Routes::Channels, :brand_redirect - # /user/linustechtips | Not always the same as /c/ - get "/user/:user#{path}", Routes::Channels, :brand_redirect - # /@LinusTechTips | Handle - get "/@:user#{path}", Routes::Channels, :brand_redirect - # /attribution_link?a=anything&u=/channel/UCZYTClx2T1of7BRZ86-8fow - get "/attribution_link#{path}", Routes::Channels, :brand_redirect - # /profile?user=linustechtips - get "/profile/#{path}", Routes::Channels, :profile - end + # Channel catch-all, to redirect future routes to the channel's home + # NOTE: defined last in order to be processed after the other routes + get "/channel/:ucid/*", Routes::Channels, :home + + # /c/LinusTechTips + get "/c/:user", Routes::Channels, :brand_redirect + get "/c/:user/:tab", Routes::Channels, :brand_redirect + + # /user/linustechtips (Not always the same as /c/) + get "/user/:user", Routes::Channels, :brand_redirect + get "/user/:user/:tab", Routes::Channels, :brand_redirect + + # /@LinusTechTips (Handle) + get "/@:user", Routes::Channels, :brand_redirect + get "/@:user/:tab", Routes::Channels, :brand_redirect + + # /attribution_link?a=anything&u=/channel/UCZYTClx2T1of7BRZ86-8fow + get "/attribution_link", Routes::Channels, :brand_redirect + get "/attribution_link/:tab", Routes::Channels, :brand_redirect + + # /profile?user=linustechtips + get "/profile", Routes::Channels, :profile + get "/profile/*", Routes::Channels, :profile end def register_watch_routes From 2425c47882feaa56a69f6ba842cf1cb9d5b450e0 Mon Sep 17 00:00:00 2001 From: Samantaz Fox Date: Wed, 13 Sep 2023 23:41:31 +0200 Subject: [PATCH 090/143] Routing: Add support for the '/live/' route --- src/invidious/routing.cr | 1 + 1 file changed, 1 insertion(+) diff --git a/src/invidious/routing.cr b/src/invidious/routing.cr index 5ec7fae3..f6b3aaa6 100644 --- a/src/invidious/routing.cr +++ b/src/invidious/routing.cr @@ -158,6 +158,7 @@ module Invidious::Routing get "/watch", Routes::Watch, :handle post "/watch_ajax", Routes::Watch, :mark_watched get "/watch/:id", Routes::Watch, :redirect + get "/live/:id", Routes::Watch, :redirect get "/shorts/:id", Routes::Watch, :redirect get "/clip/:clip", Routes::Watch, :clip get "/w/:id", Routes::Watch, :redirect From ebee973b2454f3e1adfab798ed8be6b9ef123069 Mon Sep 17 00:00:00 2001 From: Samantaz Fox Date: Sat, 16 Sep 2023 00:54:14 +0200 Subject: [PATCH 091/143] Routes: Redirect unknown channel tabs to channel home page --- src/invidious/routes/channels.cr | 6 ++++++ src/invidious/routing.cr | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/invidious/routes/channels.cr b/src/invidious/routes/channels.cr index 5500672f..d744712d 100644 --- a/src/invidious/routes/channels.cr +++ b/src/invidious/routes/channels.cr @@ -1,6 +1,12 @@ {% skip_file if flag?(:api_only) %} module Invidious::Routes::Channels + # Redirection for unsupported routes ("tabs") + def self.redirect_home(env) + ucid = env.params.url["ucid"] + return env.redirect "/channel/#{URI.encode_www_form(ucid)}" + end + def self.home(env) self.videos(env) end diff --git a/src/invidious/routing.cr b/src/invidious/routing.cr index f6b3aaa6..8c38aaed 100644 --- a/src/invidious/routing.cr +++ b/src/invidious/routing.cr @@ -131,7 +131,7 @@ module Invidious::Routing # Channel catch-all, to redirect future routes to the channel's home # NOTE: defined last in order to be processed after the other routes - get "/channel/:ucid/*", Routes::Channels, :home + get "/channel/:ucid/*", Routes::Channels, :redirect_home # /c/LinusTechTips get "/c/:user", Routes::Channels, :brand_redirect From cc03610325f8c49b14216f2913a8a3148b48e3a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89milien=20=28perso=29?= <4016501+unixfox@users.noreply.github.com> Date: Sat, 16 Sep 2023 09:10:48 +0000 Subject: [PATCH 092/143] Test crystal 1.8.2 --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 96903fc7..f94a943a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -42,7 +42,7 @@ jobs: - 1.5.1 - 1.6.2 - 1.7.3 - - 1.8.1 + - 1.8.2 include: - crystal: nightly stable: false From 33ce0ddf148c3537d93143afeaf458a474c6bf1b Mon Sep 17 00:00:00 2001 From: Samantaz Fox Date: Sun, 10 Sep 2023 19:50:46 +0000 Subject: [PATCH 093/143] Update crystal version matrix in ci.yml --- .github/workflows/ci.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f94a943a..2c115f13 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -38,11 +38,10 @@ jobs: matrix: stable: [true] crystal: - - 1.4.1 - - 1.5.1 - 1.6.2 - 1.7.3 - 1.8.2 + - 1.9.2 include: - crystal: nightly stable: false From bbf067ed55810bef6ee5550c66c38528d1ef8a32 Mon Sep 17 00:00:00 2001 From: Samantaz Fox Date: Sun, 10 Sep 2023 22:15:33 +0200 Subject: [PATCH 094/143] Bump crystal-install too --- .github/workflows/ci.yml | 2 +- .github/workflows/container-release.yml | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2c115f13..1ca0dc96 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -52,7 +52,7 @@ jobs: submodules: true - name: Install Crystal - uses: crystal-lang/install-crystal@v1.7.0 + uses: crystal-lang/install-crystal@v1.8.0 with: crystal: ${{ matrix.crystal }} diff --git a/.github/workflows/container-release.yml b/.github/workflows/container-release.yml index 13bbf34c..c2756fcc 100644 --- a/.github/workflows/container-release.yml +++ b/.github/workflows/container-release.yml @@ -25,9 +25,9 @@ jobs: uses: actions/checkout@v3 - name: Install Crystal - uses: crystal-lang/install-crystal@v1.6.0 + uses: crystal-lang/install-crystal@v1.8.0 with: - crystal: 1.5.0 + crystal: 1.9.2 - name: Run lint run: | @@ -77,4 +77,3 @@ jobs: tags: quay.io/invidious/invidious:${{ github.sha }}-arm64,quay.io/invidious/invidious:latest-arm64 build-args: | "release=1" - From 760bf4cfb310954eb649aa18ca9961c2c7f3ca51 Mon Sep 17 00:00:00 2001 From: syeopite <70992037+syeopite@users.noreply.github.com> Date: Sat, 16 Sep 2023 23:22:49 +0000 Subject: [PATCH 095/143] Bump stale timer for PRs --- .github/workflows/stale.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index a945da58..a7e218a2 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -14,7 +14,7 @@ jobs: with: repo-token: ${{ secrets.GITHUB_TOKEN }} days-before-stale: 365 - days-before-pr-stale: 45 # PRs should be active. Anything that hasn't had activity in more than 45 days should be considered abandoned. + days-before-pr-stale: 90 days-before-close: 30 exempt-pr-labels: blocked stale-issue-message: 'This issue has been automatically marked as stale and will be closed in 30 days because it has not had recent activity and is much likely outdated. If you think this issue is still relevant and applicable, you just have to post a comment and it will be unmarked.' From e3c365f3d68985a8e3ce35f76d334c7184a548de Mon Sep 17 00:00:00 2001 From: ChunkyProgrammer <78101139+ChunkyProgrammer@users.noreply.github.com> Date: Wed, 19 Jul 2023 12:35:22 -0700 Subject: [PATCH 096/143] Add support for post page --- assets/css/default.css | 2 +- assets/js/comments.js | 174 +++++++++++++++++++++ assets/js/post.js | 3 + assets/js/watch.js | 157 ------------------- src/invidious/channels/community.cr | 39 ++++- src/invidious/comments/youtube.cr | 51 +++++- src/invidious/frontend/comments_youtube.cr | 18 +++ src/invidious/routes/api/v1/channels.cr | 47 ++++++ src/invidious/routes/api/v1/misc.cr | 10 +- src/invidious/routes/channels.cr | 37 +++++ src/invidious/routing.cr | 6 + src/invidious/views/community.ecr | 2 +- src/invidious/views/post.ecr | 31 ++++ src/invidious/views/watch.ecr | 6 +- 14 files changed, 416 insertions(+), 167 deletions(-) create mode 100644 assets/js/comments.js create mode 100644 assets/js/post.js create mode 100644 src/invidious/views/post.ecr diff --git a/assets/css/default.css b/assets/css/default.css index c31b24e5..69fe8d5f 100644 --- a/assets/css/default.css +++ b/assets/css/default.css @@ -392,7 +392,7 @@ p.video-data { margin: 0; font-weight: bold; font-size: 80%; } * Comments & community posts */ -#comments { +.comments { max-width: 800px; margin: auto; } diff --git a/assets/js/comments.js b/assets/js/comments.js new file mode 100644 index 00000000..00a8cae9 --- /dev/null +++ b/assets/js/comments.js @@ -0,0 +1,174 @@ +var video_data = JSON.parse(document.getElementById('video_data').textContent); + +var spinnerHTML = '

'; +var spinnerHTMLwithHR = spinnerHTML + '
'; + +String.prototype.supplant = function (o) { + return this.replace(/{([^{}]*)}/g, function (a, b) { + var r = o[b]; + return typeof r === 'string' || typeof r === 'number' ? r : a; + }); +}; + +function toggle_comments(event) { + var target = event.target; + var body = target.parentNode.parentNode.parentNode.children[1]; + if (body.style.display === 'none') { + target.textContent = '[ − ]'; + body.style.display = ''; + } else { + target.textContent = '[ + ]'; + body.style.display = 'none'; + } +} + +function hide_youtube_replies(event) { + var target = event.target; + + var sub_text = target.getAttribute('data-inner-text'); + var inner_text = target.getAttribute('data-sub-text'); + + var body = target.parentNode.parentNode.children[1]; + body.style.display = 'none'; + + target.textContent = sub_text; + target.onclick = show_youtube_replies; + target.setAttribute('data-inner-text', inner_text); + target.setAttribute('data-sub-text', sub_text); +} + +function show_youtube_replies(event) { + var target = event.target; + + var sub_text = target.getAttribute('data-inner-text'); + var inner_text = target.getAttribute('data-sub-text'); + + var body = target.parentNode.parentNode.children[1]; + body.style.display = ''; + + target.textContent = sub_text; + target.onclick = hide_youtube_replies; + target.setAttribute('data-inner-text', inner_text); + target.setAttribute('data-sub-text', sub_text); +} + +function get_youtube_comments() { + var comments = document.getElementById('comments'); + + var fallback = comments.innerHTML; + comments.innerHTML = spinnerHTML; + + var baseUrl = video_data.base_url || '/api/v1/comments/'+ video_data.id + var url = baseUrl + + '?format=html' + + '&hl=' + video_data.preferences.locale + + '&thin_mode=' + video_data.preferences.thin_mode; + + if (video_data.ucid) { + url += '&ucid=' + video_data.ucid + } + + var onNon200 = function (xhr) { comments.innerHTML = fallback; }; + if (video_data.params.comments[1] === 'youtube') + onNon200 = function (xhr) {}; + + helpers.xhr('GET', url, {retries: 5, entity_name: 'comments'}, { + on200: function (response) { + var commentInnerHtml = ' \ +
\ +

\ + [ − ] \ + {commentsText} \ +

\ + \ + ' + if (video_data.support_reddit) { + commentInnerHtml += ' \ + {redditComments} \ + \ + ' + } + commentInnerHtml += ' \ +
\ +
{contentHtml}
\ +
' + commentInnerHtml = commentInnerHtml.supplant({ + contentHtml: response.contentHtml, + redditComments: video_data.reddit_comments_text, + commentsText: video_data.comments_text.supplant({ + // toLocaleString correctly splits number with local thousands separator. e.g.: + // '1,234,567.89' for user with English locale + // '1 234 567,89' for user with Russian locale + // '1.234.567,89' for user with Portuguese locale + commentCount: response.commentCount.toLocaleString() + }) + }); + comments.innerHTML = commentInnerHtml; + comments.children[0].children[0].children[0].onclick = toggle_comments; + if (video_data.support_reddit) { + comments.children[0].children[1].children[0].onclick = swap_comments; + } + }, + onNon200: onNon200, // declared above + onError: function (xhr) { + comments.innerHTML = spinnerHTML; + }, + onTimeout: function (xhr) { + comments.innerHTML = spinnerHTML; + } + }); +} + +function get_youtube_replies(target, load_more, load_replies) { + var continuation = target.getAttribute('data-continuation'); + + var body = target.parentNode.parentNode; + var fallback = body.innerHTML; + body.innerHTML = spinnerHTML; + var baseUrl = video_data.base_url || '/api/v1/comments/'+ video_data.id + var url = baseUrl + + '?format=html' + + '&hl=' + video_data.preferences.locale + + '&thin_mode=' + video_data.preferences.thin_mode + + '&continuation=' + continuation; + + if (video_data.ucid) { + url += '&ucid=' + video_data.ucid + } + if (load_replies) url += '&action=action_get_comment_replies'; + + helpers.xhr('GET', url, {}, { + on200: function (response) { + if (load_more) { + body = body.parentNode.parentNode; + body.removeChild(body.lastElementChild); + body.insertAdjacentHTML('beforeend', response.contentHtml); + } else { + body.removeChild(body.lastElementChild); + + var p = document.createElement('p'); + var a = document.createElement('a'); + p.appendChild(a); + + a.href = 'javascript:void(0)'; + a.onclick = hide_youtube_replies; + a.setAttribute('data-sub-text', video_data.hide_replies_text); + a.setAttribute('data-inner-text', video_data.show_replies_text); + a.textContent = video_data.hide_replies_text; + + var div = document.createElement('div'); + div.innerHTML = response.contentHtml; + + body.appendChild(p); + body.appendChild(div); + } + }, + onNon200: function (xhr) { + body.innerHTML = fallback; + }, + onTimeout: function (xhr) { + console.warn('Pulling comments failed'); + body.innerHTML = fallback; + } + }); +} \ No newline at end of file diff --git a/assets/js/post.js b/assets/js/post.js new file mode 100644 index 00000000..fcbc9155 --- /dev/null +++ b/assets/js/post.js @@ -0,0 +1,3 @@ +addEventListener('load', function (e) { + get_youtube_comments(); +}); diff --git a/assets/js/watch.js b/assets/js/watch.js index 36506abd..26ad138f 100644 --- a/assets/js/watch.js +++ b/assets/js/watch.js @@ -1,14 +1,4 @@ 'use strict'; -var video_data = JSON.parse(document.getElementById('video_data').textContent); -var spinnerHTML = '

'; -var spinnerHTMLwithHR = spinnerHTML + '
'; - -String.prototype.supplant = function (o) { - return this.replace(/{([^{}]*)}/g, function (a, b) { - var r = o[b]; - return typeof r === 'string' || typeof r === 'number' ? r : a; - }); -}; function toggle_parent(target) { var body = target.parentNode.parentNode.children[1]; @@ -21,18 +11,6 @@ function toggle_parent(target) { } } -function toggle_comments(event) { - var target = event.target; - var body = target.parentNode.parentNode.parentNode.children[1]; - if (body.style.display === 'none') { - target.textContent = '[ − ]'; - body.style.display = ''; - } else { - target.textContent = '[ + ]'; - body.style.display = 'none'; - } -} - function swap_comments(event) { var source = event.target.getAttribute('data-comments'); @@ -43,36 +21,6 @@ function swap_comments(event) { } } -function hide_youtube_replies(event) { - var target = event.target; - - var sub_text = target.getAttribute('data-inner-text'); - var inner_text = target.getAttribute('data-sub-text'); - - var body = target.parentNode.parentNode.children[1]; - body.style.display = 'none'; - - target.textContent = sub_text; - target.onclick = show_youtube_replies; - target.setAttribute('data-inner-text', inner_text); - target.setAttribute('data-sub-text', sub_text); -} - -function show_youtube_replies(event) { - var target = event.target; - - var sub_text = target.getAttribute('data-inner-text'); - var inner_text = target.getAttribute('data-sub-text'); - - var body = target.parentNode.parentNode.children[1]; - body.style.display = ''; - - target.textContent = sub_text; - target.onclick = hide_youtube_replies; - target.setAttribute('data-inner-text', inner_text); - target.setAttribute('data-sub-text', sub_text); -} - var continue_button = document.getElementById('continue'); if (continue_button) { continue_button.onclick = continue_autoplay; @@ -208,111 +156,6 @@ function get_reddit_comments() { }); } -function get_youtube_comments() { - var comments = document.getElementById('comments'); - - var fallback = comments.innerHTML; - comments.innerHTML = spinnerHTML; - - var url = '/api/v1/comments/' + video_data.id + - '?format=html' + - '&hl=' + video_data.preferences.locale + - '&thin_mode=' + video_data.preferences.thin_mode; - - var onNon200 = function (xhr) { comments.innerHTML = fallback; }; - if (video_data.params.comments[1] === 'youtube') - onNon200 = function (xhr) {}; - - helpers.xhr('GET', url, {retries: 5, entity_name: 'comments'}, { - on200: function (response) { - comments.innerHTML = ' \ -
\ -

\ - [ − ] \ - {commentsText} \ -

\ - \ - \ - {redditComments} \ - \ - \ -
\ -
{contentHtml}
\ -
'.supplant({ - contentHtml: response.contentHtml, - redditComments: video_data.reddit_comments_text, - commentsText: video_data.comments_text.supplant({ - // toLocaleString correctly splits number with local thousands separator. e.g.: - // '1,234,567.89' for user with English locale - // '1 234 567,89' for user with Russian locale - // '1.234.567,89' for user with Portuguese locale - commentCount: response.commentCount.toLocaleString() - }) - }); - - comments.children[0].children[0].children[0].onclick = toggle_comments; - comments.children[0].children[1].children[0].onclick = swap_comments; - }, - onNon200: onNon200, // declared above - onError: function (xhr) { - comments.innerHTML = spinnerHTML; - }, - onTimeout: function (xhr) { - comments.innerHTML = spinnerHTML; - } - }); -} - -function get_youtube_replies(target, load_more, load_replies) { - var continuation = target.getAttribute('data-continuation'); - - var body = target.parentNode.parentNode; - var fallback = body.innerHTML; - body.innerHTML = spinnerHTML; - - var url = '/api/v1/comments/' + video_data.id + - '?format=html' + - '&hl=' + video_data.preferences.locale + - '&thin_mode=' + video_data.preferences.thin_mode + - '&continuation=' + continuation; - if (load_replies) url += '&action=action_get_comment_replies'; - - helpers.xhr('GET', url, {}, { - on200: function (response) { - if (load_more) { - body = body.parentNode.parentNode; - body.removeChild(body.lastElementChild); - body.insertAdjacentHTML('beforeend', response.contentHtml); - } else { - body.removeChild(body.lastElementChild); - - var p = document.createElement('p'); - var a = document.createElement('a'); - p.appendChild(a); - - a.href = 'javascript:void(0)'; - a.onclick = hide_youtube_replies; - a.setAttribute('data-sub-text', video_data.hide_replies_text); - a.setAttribute('data-inner-text', video_data.show_replies_text); - a.textContent = video_data.hide_replies_text; - - var div = document.createElement('div'); - div.innerHTML = response.contentHtml; - - body.appendChild(p); - body.appendChild(div); - } - }, - onNon200: function (xhr) { - body.innerHTML = fallback; - }, - onTimeout: function (xhr) { - console.warn('Pulling comments failed'); - body.innerHTML = fallback; - } - }); -} - if (video_data.play_next) { player.on('ended', function () { var url = new URL('https://example.com/watch?v=' + video_data.next_video); diff --git a/src/invidious/channels/community.cr b/src/invidious/channels/community.cr index 791f1641..85ddff35 100644 --- a/src/invidious/channels/community.cr +++ b/src/invidious/channels/community.cr @@ -24,7 +24,35 @@ def fetch_channel_community(ucid, cursor, locale, format, thin_mode) return extract_channel_community(items, ucid: ucid, locale: locale, format: format, thin_mode: thin_mode) end -def extract_channel_community(items, *, ucid, locale, format, thin_mode) +def fetch_channel_community_post(ucid, postId, locale, format, thin_mode, params : String | Nil = nil) + if params.nil? + object = { + "2:string" => "community", + "25:embedded" => { + "22:string" => postId.to_s, + }, + "45:embedded" => { + "2:varint" => 1_i64, + "3:varint" => 1_i64, + }, + } + params = object.try { |i| Protodec::Any.cast_json(i) } + .try { |i| Protodec::Any.from_json(i) } + .try { |i| Base64.urlsafe_encode(i) } + .try { |i| URI.encode_www_form(i) } + end + + initial_data = YoutubeAPI.browse(ucid, params: params) + + items = [] of JSON::Any + extract_items(initial_data) do |item| + items << item + end + + return extract_channel_community(items, ucid: ucid, locale: locale, format: format, thin_mode: thin_mode, is_single_post: true) +end + +def extract_channel_community(items, *, ucid, locale, format, thin_mode, is_single_post : Bool = false) if message = items[0]["messageRenderer"]? error_message = (message["text"]["simpleText"]? || message["text"]["runs"]?.try &.[0]?.try &.["text"]?) @@ -39,6 +67,9 @@ def extract_channel_community(items, *, ucid, locale, format, thin_mode) response = JSON.build do |json| json.object do json.field "authorId", ucid + if is_single_post + json.field "singlePost", true + end json.field "comments" do json.array do items.each do |post| @@ -240,8 +271,10 @@ def extract_channel_community(items, *, ucid, locale, format, thin_mode) end end end - if cont = items.dig?(-1, "continuationItemRenderer", "continuationEndpoint", "continuationCommand", "token") - json.field "continuation", extract_channel_community_cursor(cont.as_s) + if !is_single_post + if cont = items.dig?(-1, "continuationItemRenderer", "continuationEndpoint", "continuationCommand", "token") + json.field "continuation", extract_channel_community_cursor(cont.as_s) + end end end end diff --git a/src/invidious/comments/youtube.cr b/src/invidious/comments/youtube.cr index 1ba1b534..da7f0543 100644 --- a/src/invidious/comments/youtube.cr +++ b/src/invidious/comments/youtube.cr @@ -13,6 +13,51 @@ module Invidious::Comments client_config = YoutubeAPI::ClientConfig.new(region: region) response = YoutubeAPI.next(continuation: ctoken, client_config: client_config) + return parse_youtube(id, response, format, locale, thin_mode, sort_by) + end + + def fetch_community_post_comments(ucid, postId) + object = { + "2:string" => "community", + "25:embedded" => { + "22:string" => postId, + }, + "45:embedded" => { + "2:varint" => 1_i64, + "3:varint" => 1_i64, + }, + "53:embedded" => { + "4:embedded" => { + "6:varint" => 0_i64, + "27:varint" => 1_i64, + "29:string" => postId, + "30:string" => ucid, + }, + "8:string" => "comments-section", + }, + } + + objectParsed = object.try { |i| Protodec::Any.cast_json(i) } + .try { |i| Protodec::Any.from_json(i) } + .try { |i| Base64.urlsafe_encode(i) } + + object2 = { + "80226972:embedded" => { + "2:string" => ucid, + "3:string" => objectParsed, + }, + } + + continuation = object2.try { |i| Protodec::Any.cast_json(i) } + .try { |i| Protodec::Any.from_json(i) } + .try { |i| Base64.urlsafe_encode(i) } + .try { |i| URI.encode_www_form(i) } + + initial_data = YoutubeAPI.browse(continuation: continuation) + return initial_data + end + + def parse_youtube(id, response, format, locale, thin_mode, sort_by = "top", isPost = false) contents = nil if on_response_received_endpoints = response["onResponseReceivedEndpoints"]? @@ -68,7 +113,11 @@ module Invidious::Comments json.field "commentCount", comment_count end - json.field "videoId", id + if isPost + json.field "postId", id + else + json.field "videoId", id + end json.field "comments" do json.array do diff --git a/src/invidious/frontend/comments_youtube.cr b/src/invidious/frontend/comments_youtube.cr index 41f43f04..ecc0bc1b 100644 --- a/src/invidious/frontend/comments_youtube.cr +++ b/src/invidious/frontend/comments_youtube.cr @@ -23,6 +23,24 @@ module Invidious::Frontend::Comments
END_HTML + elsif comments["authorId"]? && !comments["singlePost"]? + # for posts we should display a link to the post + replies_count_text = translate_count(locale, + "comments_view_x_replies", + child["replyCount"].as_i64 || 0, + NumberFormatting::Separator + ) + + replies_html = <<-END_HTML +
+
+ +
+ END_HTML end if !thin_mode diff --git a/src/invidious/routes/api/v1/channels.cr b/src/invidious/routes/api/v1/channels.cr index adf05d30..0d2d2eb1 100644 --- a/src/invidious/routes/api/v1/channels.cr +++ b/src/invidious/routes/api/v1/channels.cr @@ -343,6 +343,53 @@ module Invidious::Routes::API::V1::Channels end end + def self.post(env) + locale = env.get("preferences").as(Preferences).locale + + env.response.content_type = "application/json" + + id = env.params.url["id"].to_s + ucid = env.params.query["ucid"] + + thin_mode = env.params.query["thin_mode"]? + thin_mode = thin_mode == "true" + + format = env.params.query["format"]? + format ||= "json" + + begin + fetch_channel_community_post(ucid, id, locale, format, thin_mode) + rescue ex + return error_json(500, ex) + end + end + + def self.post_comments(env) + locale = env.get("preferences").as(Preferences).locale + region = env.params.query["region"]? + + env.response.content_type = "application/json" + + id = env.params.url["id"] + + thin_mode = env.params.query["thin_mode"]? + thin_mode = thin_mode == "true" + + format = env.params.query["format"]? + format ||= "json" + + continuation = env.params.query["continuation"]? + + case continuation + when nil, "" + ucid = env.params.query["ucid"] + comments = Comments.fetch_community_post_comments(ucid, id) + else + comments = YoutubeAPI.browse(continuation: continuation) + end + return Comments.parse_youtube(id, comments, format, locale, thin_mode, isPost: true) + end + def self.channels(env) locale = env.get("preferences").as(Preferences).locale ucid = env.params.url["ucid"] diff --git a/src/invidious/routes/api/v1/misc.cr b/src/invidious/routes/api/v1/misc.cr index e499f4d6..91a62fa3 100644 --- a/src/invidious/routes/api/v1/misc.cr +++ b/src/invidious/routes/api/v1/misc.cr @@ -162,17 +162,23 @@ module Invidious::Routes::API::V1::Misc resolved_url = YoutubeAPI.resolve_url(url.as(String)) endpoint = resolved_url["endpoint"] pageType = endpoint.dig?("commandMetadata", "webCommandMetadata", "webPageType").try &.as_s || "" - if resolved_ucid = endpoint.dig?("watchEndpoint", "videoId") - elsif resolved_ucid = endpoint.dig?("browseEndpoint", "browseId") + if sub_endpoint = endpoint.dig?("watchEndpoint") + resolved_ucid = sub_endpoint.dig?("videoId") + elsif sub_endpoint = endpoint.dig?("browseEndpoint") + resolved_ucid = sub_endpoint.dig?("browseId") elsif pageType == "WEB_PAGE_TYPE_UNKNOWN" return error_json(400, "Unknown url") end + if !sub_endpoint.nil? + params = sub_endpoint.dig?("params") + end rescue ex return error_json(500, ex) end JSON.build do |json| json.object do json.field "ucid", resolved_ucid.try &.as_s || "" + json.field "params", params.try &.as_s json.field "pageType", pageType end end diff --git a/src/invidious/routes/channels.cr b/src/invidious/routes/channels.cr index 9892ae2a..1d02ee08 100644 --- a/src/invidious/routes/channels.cr +++ b/src/invidious/routes/channels.cr @@ -159,6 +159,11 @@ module Invidious::Routes::Channels end locale, user, subscriptions, continuation, ucid, channel = data + # redirect to post page + if lb = env.params.query["lb"]? + env.redirect "/post/#{lb}?ucid=#{ucid}" + end + thin_mode = env.params.query["thin_mode"]? || env.get("preferences").as(Preferences).thin_mode thin_mode = thin_mode == "true" @@ -187,6 +192,38 @@ module Invidious::Routes::Channels templated "community" end + def self.post(env) + # /post/{postId} + id = env.params.url["id"] + ucid = env.params.query["ucid"]? + + prefs = env.get("preferences").as(Preferences) + + locale = prefs.locale + region = env.params.query["region"]? || prefs.region + + thin_mode = env.params.query["thin_mode"]? || prefs.thin_mode + thin_mode = thin_mode == "true" + + client_config = YoutubeAPI::ClientConfig.new(region: region) + + if !ucid.nil? + ucid = ucid.to_s + post_response = fetch_channel_community_post(ucid, id, locale, "json", thin_mode) + else + # resolve the url to get the author's UCID + response = YoutubeAPI.resolve_url("https://www.youtube.com/post/#{id}") + return error_template(400, "Invalid post ID") if response["error"]? + + ucid = response.dig("endpoint", "browseEndpoint", "browseId").as_s + params = response.dig("endpoint", "browseEndpoint", "params").as_s + post_response = fetch_channel_community_post(ucid, id, locale, "json", thin_mode, params: params) + end + + post_response = JSON.parse(post_response) + templated "post" + end + def self.channels(env) data = self.fetch_basic_information(env) return data if !data.is_a?(Tuple) diff --git a/src/invidious/routing.cr b/src/invidious/routing.cr index 9c43171c..8cb49249 100644 --- a/src/invidious/routing.cr +++ b/src/invidious/routing.cr @@ -127,6 +127,7 @@ module Invidious::Routing get "/channel/:ucid/live", Routes::Channels, :live get "/user/:user/live", Routes::Channels, :live get "/c/:user/live", Routes::Channels, :live + get "/post/:id", Routes::Channels, :post {"", "/videos", "/shorts", "/streams", "/playlists", "/community", "/about"}.each do |path| # /c/LinusTechTips @@ -240,6 +241,10 @@ module Invidious::Routing get "/api/v1/channels/:ucid/#{{{route}}}", {{namespace}}::Channels, :{{route}} {% end %} + # Posts + get "/api/v1/post/:id", {{namespace}}::Channels, :post + get "/api/v1/post/:id/comments", {{namespace}}::Channels, :post_comments + # 301 redirects to new /api/v1/channels/community/:ucid and /:ucid/community get "/api/v1/channels/comments/:ucid", {{namespace}}::Channels, :channel_comments_redirect get "/api/v1/channels/:ucid/comments", {{namespace}}::Channels, :channel_comments_redirect @@ -249,6 +254,7 @@ module Invidious::Routing get "/api/v1/search/suggestions", {{namespace}}::Search, :search_suggestions get "/api/v1/hashtag/:hashtag", {{namespace}}::Search, :hashtag + # Authenticated # The notification APIs cannot be extracted yet! They require the *local* notifications constant defined in invidious.cr diff --git a/src/invidious/views/community.ecr b/src/invidious/views/community.ecr index 24efc34e..d2a305d3 100644 --- a/src/invidious/views/community.ecr +++ b/src/invidious/views/community.ecr @@ -26,7 +26,7 @@

<%= error_message %>

<% else %> -
+
<%= IV::Frontend::Comments.template_youtube(items.not_nil!, locale, thin_mode) %>
<% end %> diff --git a/src/invidious/views/post.ecr b/src/invidious/views/post.ecr new file mode 100644 index 00000000..b2cd778c --- /dev/null +++ b/src/invidious/views/post.ecr @@ -0,0 +1,31 @@ +<% content_for "header" do %> +Invidious +<% end %> + +
+ <%= IV::Frontend::Comments.template_youtube(post_response.not_nil!, locale, thin_mode) %> +
+
+
+ + + + \ No newline at end of file diff --git a/src/invidious/views/watch.ecr b/src/invidious/views/watch.ecr index 498d57a1..62a154a4 100644 --- a/src/invidious/views/watch.ecr +++ b/src/invidious/views/watch.ecr @@ -64,7 +64,8 @@ we're going to need to do it here in order to allow for translations. "premiere_timestamp" => video.premiere_timestamp.try &.to_unix, "vr" => video.is_vr, "projection_type" => video.projection_type, - "local_disabled" => CONFIG.disabled?("local") + "local_disabled" => CONFIG.disabled?("local"), + "support_reddit" => true }.to_pretty_json %> @@ -270,7 +271,7 @@ we're going to need to do it here in order to allow for translations.
<% end %> -
+
<% if nojs %> <%= comment_html %> <% else %> @@ -352,4 +353,5 @@ we're going to need to do it here in order to allow for translations.
<% end %>
+ From 734f1b7764598bd5ff24acd11ab833f831d0f4a7 Mon Sep 17 00:00:00 2001 From: ChunkyProgrammer <78101139+ChunkyProgrammer@users.noreply.github.com> Date: Thu, 27 Jul 2023 19:14:34 -0400 Subject: [PATCH 097/143] Simplify resolveUrl api call Co-Authored-By: Samantaz Fox --- src/invidious/channels/community.cr | 4 ++-- src/invidious/comments/youtube.cr | 6 +++--- src/invidious/routes/api/v1/misc.cr | 13 ++++++------- src/invidious/routes/channels.cr | 2 +- 4 files changed, 12 insertions(+), 13 deletions(-) diff --git a/src/invidious/channels/community.cr b/src/invidious/channels/community.cr index 85ddff35..76dab361 100644 --- a/src/invidious/channels/community.cr +++ b/src/invidious/channels/community.cr @@ -24,12 +24,12 @@ def fetch_channel_community(ucid, cursor, locale, format, thin_mode) return extract_channel_community(items, ucid: ucid, locale: locale, format: format, thin_mode: thin_mode) end -def fetch_channel_community_post(ucid, postId, locale, format, thin_mode, params : String | Nil = nil) +def fetch_channel_community_post(ucid, post_id, locale, format, thin_mode, params : String | Nil = nil) if params.nil? object = { "2:string" => "community", "25:embedded" => { - "22:string" => postId.to_s, + "22:string" => post_id.to_s, }, "45:embedded" => { "2:varint" => 1_i64, diff --git a/src/invidious/comments/youtube.cr b/src/invidious/comments/youtube.cr index da7f0543..01c2564f 100644 --- a/src/invidious/comments/youtube.cr +++ b/src/invidious/comments/youtube.cr @@ -16,11 +16,11 @@ module Invidious::Comments return parse_youtube(id, response, format, locale, thin_mode, sort_by) end - def fetch_community_post_comments(ucid, postId) + def fetch_community_post_comments(ucid, post_id) object = { "2:string" => "community", "25:embedded" => { - "22:string" => postId, + "22:string" => post_id, }, "45:embedded" => { "2:varint" => 1_i64, @@ -30,7 +30,7 @@ module Invidious::Comments "4:embedded" => { "6:varint" => 0_i64, "27:varint" => 1_i64, - "29:string" => postId, + "29:string" => post_id, "30:string" => ucid, }, "8:string" => "comments-section", diff --git a/src/invidious/routes/api/v1/misc.cr b/src/invidious/routes/api/v1/misc.cr index 91a62fa3..6118a0d1 100644 --- a/src/invidious/routes/api/v1/misc.cr +++ b/src/invidious/routes/api/v1/misc.cr @@ -162,16 +162,15 @@ module Invidious::Routes::API::V1::Misc resolved_url = YoutubeAPI.resolve_url(url.as(String)) endpoint = resolved_url["endpoint"] pageType = endpoint.dig?("commandMetadata", "webCommandMetadata", "webPageType").try &.as_s || "" - if sub_endpoint = endpoint.dig?("watchEndpoint") - resolved_ucid = sub_endpoint.dig?("videoId") - elsif sub_endpoint = endpoint.dig?("browseEndpoint") - resolved_ucid = sub_endpoint.dig?("browseId") + if sub_endpoint = endpoint["watchEndpoint"]? + resolved_ucid = sub_endpoint["videoId"]? + elsif sub_endpoint = endpoint["browseEndpoint"]? + resolved_ucid = sub_endpoint["browseId"]? elsif pageType == "WEB_PAGE_TYPE_UNKNOWN" return error_json(400, "Unknown url") end - if !sub_endpoint.nil? - params = sub_endpoint.dig?("params") - end + + params = sub_endpoint.try &.dig?("params") rescue ex return error_json(500, ex) end diff --git a/src/invidious/routes/channels.cr b/src/invidious/routes/channels.cr index 1d02ee08..8515b910 100644 --- a/src/invidious/routes/channels.cr +++ b/src/invidious/routes/channels.cr @@ -161,7 +161,7 @@ module Invidious::Routes::Channels # redirect to post page if lb = env.params.query["lb"]? - env.redirect "/post/#{lb}?ucid=#{ucid}" + env.redirect "/post/#{URI.encode_www_form(lb)}?ucid=#{URI.encode_www_form(ucid)}" end thin_mode = env.params.query["thin_mode"]? || env.get("preferences").as(Preferences).thin_mode From f55b96a53bde8d8c6a24d4db4e9d10f14ffee585 Mon Sep 17 00:00:00 2001 From: ChunkyProgrammer <78101139+ChunkyProgrammer@users.noreply.github.com> Date: Mon, 7 Aug 2023 12:46:19 -0700 Subject: [PATCH 098/143] Always craft Community Post params --- src/invidious/channels/community.cr | 32 ++++++++++++++--------------- src/invidious/routes/channels.cr | 3 +-- 2 files changed, 16 insertions(+), 19 deletions(-) diff --git a/src/invidious/channels/community.cr b/src/invidious/channels/community.cr index 76dab361..49ffd990 100644 --- a/src/invidious/channels/community.cr +++ b/src/invidious/channels/community.cr @@ -24,23 +24,21 @@ def fetch_channel_community(ucid, cursor, locale, format, thin_mode) return extract_channel_community(items, ucid: ucid, locale: locale, format: format, thin_mode: thin_mode) end -def fetch_channel_community_post(ucid, post_id, locale, format, thin_mode, params : String | Nil = nil) - if params.nil? - object = { - "2:string" => "community", - "25:embedded" => { - "22:string" => post_id.to_s, - }, - "45:embedded" => { - "2:varint" => 1_i64, - "3:varint" => 1_i64, - }, - } - params = object.try { |i| Protodec::Any.cast_json(i) } - .try { |i| Protodec::Any.from_json(i) } - .try { |i| Base64.urlsafe_encode(i) } - .try { |i| URI.encode_www_form(i) } - end +def fetch_channel_community_post(ucid, post_id, locale, format, thin_mode) + object = { + "2:string" => "community", + "25:embedded" => { + "22:string" => post_id.to_s, + }, + "45:embedded" => { + "2:varint" => 1_i64, + "3:varint" => 1_i64, + }, + } + params = object.try { |i| Protodec::Any.cast_json(i) } + .try { |i| Protodec::Any.from_json(i) } + .try { |i| Base64.urlsafe_encode(i) } + .try { |i| URI.encode_www_form(i) } initial_data = YoutubeAPI.browse(ucid, params: params) diff --git a/src/invidious/routes/channels.cr b/src/invidious/routes/channels.cr index 8515b910..20b02dc1 100644 --- a/src/invidious/routes/channels.cr +++ b/src/invidious/routes/channels.cr @@ -216,8 +216,7 @@ module Invidious::Routes::Channels return error_template(400, "Invalid post ID") if response["error"]? ucid = response.dig("endpoint", "browseEndpoint", "browseId").as_s - params = response.dig("endpoint", "browseEndpoint", "params").as_s - post_response = fetch_channel_community_post(ucid, id, locale, "json", thin_mode, params: params) + post_response = fetch_channel_community_post(ucid, id, locale, "json", thin_mode) end post_response = JSON.parse(post_response) From bb04bcc42c1b135aaf50de8799264f86bc42f4db Mon Sep 17 00:00:00 2001 From: ChunkyProgrammer <78101139+ChunkyProgrammer@users.noreply.github.com> Date: Tue, 29 Aug 2023 19:10:01 -0700 Subject: [PATCH 099/143] Apply suggestions from code review add videoId to resolve_url function Co-Authored-By: Samantaz Fox --- src/invidious/comments/youtube.cr | 4 ++-- src/invidious/routes/api/v1/channels.cr | 11 +++++++++-- src/invidious/routes/api/v1/misc.cr | 10 ++++------ src/invidious/routes/channels.cr | 2 -- src/invidious/views/post.ecr | 2 +- 5 files changed, 16 insertions(+), 13 deletions(-) diff --git a/src/invidious/comments/youtube.cr b/src/invidious/comments/youtube.cr index 01c2564f..185d8e43 100644 --- a/src/invidious/comments/youtube.cr +++ b/src/invidious/comments/youtube.cr @@ -37,14 +37,14 @@ module Invidious::Comments }, } - objectParsed = object.try { |i| Protodec::Any.cast_json(i) } + object_parsed = object.try { |i| Protodec::Any.cast_json(i) } .try { |i| Protodec::Any.from_json(i) } .try { |i| Base64.urlsafe_encode(i) } object2 = { "80226972:embedded" => { "2:string" => ucid, - "3:string" => objectParsed, + "3:string" => object_parsed, }, } diff --git a/src/invidious/routes/api/v1/channels.cr b/src/invidious/routes/api/v1/channels.cr index 0d2d2eb1..a5ae16a8 100644 --- a/src/invidious/routes/api/v1/channels.cr +++ b/src/invidious/routes/api/v1/channels.cr @@ -347,9 +347,8 @@ module Invidious::Routes::API::V1::Channels locale = env.get("preferences").as(Preferences).locale env.response.content_type = "application/json" - id = env.params.url["id"].to_s - ucid = env.params.query["ucid"] + ucid = env.params.query["ucid"]? thin_mode = env.params.query["thin_mode"]? thin_mode = thin_mode == "true" @@ -357,6 +356,14 @@ module Invidious::Routes::API::V1::Channels format = env.params.query["format"]? format ||= "json" + if ucid.nil? + response = YoutubeAPI.resolve_url("https://www.youtube.com/post/#{id}") + return error_json(400, "Invalid post ID") if response["error"]? + ucid = response.dig("endpoint", "browseEndpoint", "browseId").as_s + else + ucid = ucid.to_s + end + begin fetch_channel_community_post(ucid, id, locale, format, thin_mode) rescue ex diff --git a/src/invidious/routes/api/v1/misc.cr b/src/invidious/routes/api/v1/misc.cr index 6118a0d1..5dfc4afa 100644 --- a/src/invidious/routes/api/v1/misc.cr +++ b/src/invidious/routes/api/v1/misc.cr @@ -162,21 +162,19 @@ module Invidious::Routes::API::V1::Misc resolved_url = YoutubeAPI.resolve_url(url.as(String)) endpoint = resolved_url["endpoint"] pageType = endpoint.dig?("commandMetadata", "webCommandMetadata", "webPageType").try &.as_s || "" - if sub_endpoint = endpoint["watchEndpoint"]? - resolved_ucid = sub_endpoint["videoId"]? - elsif sub_endpoint = endpoint["browseEndpoint"]? - resolved_ucid = sub_endpoint["browseId"]? - elsif pageType == "WEB_PAGE_TYPE_UNKNOWN" + if pageType == "WEB_PAGE_TYPE_UNKNOWN" return error_json(400, "Unknown url") end + sub_endpoint = endpoint["watchEndpoint"]? || endpoint["browseEndpoint"]? || endpoint params = sub_endpoint.try &.dig?("params") rescue ex return error_json(500, ex) end JSON.build do |json| json.object do - json.field "ucid", resolved_ucid.try &.as_s || "" + json.field "ucid", sub_endpoint["browseId"].try &.as_s if sub_endpoint["browseId"]? + json.field "videoId", sub_endpoint["videoId"].try &.as_s if sub_endpoint["videoId"]? json.field "params", params.try &.as_s json.field "pageType", pageType end diff --git a/src/invidious/routes/channels.cr b/src/invidious/routes/channels.cr index 20b02dc1..29995bf6 100644 --- a/src/invidious/routes/channels.cr +++ b/src/invidious/routes/channels.cr @@ -205,8 +205,6 @@ module Invidious::Routes::Channels thin_mode = env.params.query["thin_mode"]? || prefs.thin_mode thin_mode = thin_mode == "true" - client_config = YoutubeAPI::ClientConfig.new(region: region) - if !ucid.nil? ucid = ucid.to_s post_response = fetch_channel_community_post(ucid, id, locale, "json", thin_mode) diff --git a/src/invidious/views/post.ecr b/src/invidious/views/post.ecr index b2cd778c..071d1c88 100644 --- a/src/invidious/views/post.ecr +++ b/src/invidious/views/post.ecr @@ -22,7 +22,7 @@ "comments": ["youtube"] }, "preferences" => prefs, - "base_url" => "/api/v1/post/" + id + "/comments", + "base_url" => "/api/v1/post/#{URI.encode_www_form(id)}/comments", "ucid" => ucid }.to_pretty_json %> From 8781520b8af221e5ab202775a1b58dd5e0e3fd83 Mon Sep 17 00:00:00 2001 From: ChunkyProgrammer <78101139+ChunkyProgrammer@users.noreply.github.com> Date: Tue, 18 Jul 2023 08:06:50 -0700 Subject: [PATCH 100/143] Search: Parse channel handle and hide video count when channel handle exists Co-Authored-By: Samantaz Fox --- src/invidious/helpers/serialized_yt_data.cr | 2 ++ src/invidious/views/components/item.ecr | 3 ++- src/invidious/yt_backend/extractors.cr | 10 ++++++---- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/src/invidious/helpers/serialized_yt_data.cr b/src/invidious/helpers/serialized_yt_data.cr index e0bd7279..31a3cf44 100644 --- a/src/invidious/helpers/serialized_yt_data.cr +++ b/src/invidious/helpers/serialized_yt_data.cr @@ -186,6 +186,7 @@ struct SearchChannel property author_thumbnail : String property subscriber_count : Int32 property video_count : Int32 + property channel_handle : String? property description_html : String property auto_generated : Bool property author_verified : Bool @@ -214,6 +215,7 @@ struct SearchChannel json.field "autoGenerated", self.auto_generated json.field "subCount", self.subscriber_count json.field "videoCount", self.video_count + json.field "channelHandle", self.channel_handle json.field "description", html_to_content(self.description_html) json.field "descriptionHtml", self.description_html diff --git a/src/invidious/views/components/item.ecr b/src/invidious/views/components/item.ecr index c29ec47b..031b46da 100644 --- a/src/invidious/views/components/item.ecr +++ b/src/invidious/views/components/item.ecr @@ -26,8 +26,9 @@
+ <% if !item.channel_handle.nil? %>

<%= item.channel_handle %>

<% end %>

<%= translate_count(locale, "generic_subscribers_count", item.subscriber_count, NumberFormatting::Separator) %>

- <% if !item.auto_generated %>

<%= translate_count(locale, "generic_videos_count", item.video_count, NumberFormatting::Separator) %>

<% end %> + <% if !item.auto_generated && item.channel_handle.nil? %>

<%= translate_count(locale, "generic_videos_count", item.video_count, NumberFormatting::Separator) %>

<% end %>
<%= item.description_html %>
<% when SearchHashtag %> <% if !thin_mode %> diff --git a/src/invidious/yt_backend/extractors.cr b/src/invidious/yt_backend/extractors.cr index aaf7772e..56325cf7 100644 --- a/src/invidious/yt_backend/extractors.cr +++ b/src/invidious/yt_backend/extractors.cr @@ -175,17 +175,18 @@ private module Parsers # Always simpleText # TODO change default value to nil - subscriber_count = item_contents.dig?("subscriberCountText", "simpleText") + subscriber_count = item_contents.dig?("subscriberCountText", "simpleText").try &.as_s + channel_handle = subscriber_count if (subscriber_count.try &.starts_with? "@") # Since youtube added channel handles, `VideoCountText` holds the number of # subscribers and `subscriberCountText` holds the handle, except when the # channel doesn't have a handle (e.g: some topic music channels). # See https://github.com/iv-org/invidious/issues/3394#issuecomment-1321261688 - if !subscriber_count || !subscriber_count.as_s.includes? " subscriber" - subscriber_count = item_contents.dig?("videoCountText", "simpleText") + if !subscriber_count || !subscriber_count.includes? " subscriber" + subscriber_count = item_contents.dig?("videoCountText", "simpleText").try &.as_s end subscriber_count = subscriber_count - .try { |s| short_text_to_number(s.as_s.split(" ")[0]).to_i32 } || 0 + .try { |s| short_text_to_number(s.split(" ")[0]).to_i32 } || 0 # Auto-generated channels doesn't have videoCountText # Taken from: https://github.com/iv-org/invidious/pull/2228#discussion_r717620922 @@ -200,6 +201,7 @@ private module Parsers author_thumbnail: author_thumbnail, subscriber_count: subscriber_count, video_count: video_count, + channel_handle: channel_handle, description_html: description_html, auto_generated: auto_generated, author_verified: author_verified, From 54fa59cbb0ae90a54136522c944410e2d18c234b Mon Sep 17 00:00:00 2001 From: syeopite Date: Thu, 24 Aug 2023 14:58:50 -0700 Subject: [PATCH 101/143] Add method to construct WebVTT files Similar to JSON.Build --- spec/helpers/vtt/builder_spec.cr | 64 ++++++++++++++++++++++++++++++ src/invidious/helpers/webvtt.cr | 67 ++++++++++++++++++++++++++++++++ 2 files changed, 131 insertions(+) create mode 100644 spec/helpers/vtt/builder_spec.cr create mode 100644 src/invidious/helpers/webvtt.cr diff --git a/spec/helpers/vtt/builder_spec.cr b/spec/helpers/vtt/builder_spec.cr new file mode 100644 index 00000000..69303bab --- /dev/null +++ b/spec/helpers/vtt/builder_spec.cr @@ -0,0 +1,64 @@ +require "../../spec_helper.cr" + +MockLines = [ + { + "start_time": Time::Span.new(seconds: 1), + "end_time": Time::Span.new(seconds: 2), + "text": "Line 1", + }, + + { + "start_time": Time::Span.new(seconds: 2), + "end_time": Time::Span.new(seconds: 3), + "text": "Line 2", + }, +] + +Spectator.describe "WebVTT::Builder" do + it "correctly builds a vtt file" do + result = WebVTT.build do |vtt| + MockLines.each do |line| + vtt.line(line["start_time"], line["end_time"], line["text"]) + end + end + + expect(result).to eq([ + "WEBVTT", + "", + "00:00:01.000 --> 00:00:02.000", + "Line 1", + "", + "00:00:02.000 --> 00:00:03.000", + "Line 2", + "", + "", + ].join('\n')) + end + + it "correctly builds a vtt file with setting fields" do + setting_fields = { + "Kind" => "captions", + "Language" => "en", + } + + result = WebVTT.build(setting_fields) do |vtt| + MockLines.each do |line| + vtt.line(line["start_time"], line["end_time"], line["text"]) + end + end + + expect(result).to eq([ + "WEBVTT", + "Kind: captions", + "Language: en", + "", + "00:00:01.000 --> 00:00:02.000", + "Line 1", + "", + "00:00:02.000 --> 00:00:03.000", + "Line 2", + "", + "", + ].join('\n')) + end +end diff --git a/src/invidious/helpers/webvtt.cr b/src/invidious/helpers/webvtt.cr new file mode 100644 index 00000000..7d9d5f1f --- /dev/null +++ b/src/invidious/helpers/webvtt.cr @@ -0,0 +1,67 @@ +# Namespace for logic relating to generating WebVTT files +# +# Probably not compliant to WebVTT's specs but it is enough for Invidious. +module WebVTT + # A WebVTT builder generates WebVTT files + private class Builder + def initialize(@io : IO) + end + + # Writes an vtt line with the specified time stamp and contents + def line(start_time : Time::Span, end_time : Time::Span, text : String) + timestamp(start_time, end_time) + @io << text + @io << "\n\n" + end + + private def timestamp(start_time : Time::Span, end_time : Time::Span) + add_timestamp_component(start_time) + @io << " --> " + add_timestamp_component(end_time) + + @io << '\n' + end + + private def add_timestamp_component(timestamp : Time::Span) + @io << timestamp.hours.to_s.rjust(2, '0') + @io << ':' << timestamp.minutes.to_s.rjust(2, '0') + @io << ':' << timestamp.seconds.to_s.rjust(2, '0') + @io << '.' << timestamp.milliseconds.to_s.rjust(3, '0') + end + + def document(setting_fields : Hash(String, String)? = nil, &) + @io << "WEBVTT\n" + + if setting_fields + setting_fields.each do |name, value| + @io << "#{name}: #{value}\n" + end + end + + @io << '\n' + + yield + end + end + + # Returns the resulting `String` of writing WebVTT to the yielded WebVTT::Builder + # + # ``` + # string = WebVTT.build do |io| + # vtt.line(Time::Span.new(seconds: 1), Time::Span.new(seconds: 2), "Line 1") + # vtt.line(Time::Span.new(seconds: 2), Time::Span.new(seconds: 3), "Line 2") + # end + # + # string # => "WEBVTT\n\n00:00:01.000 --> 00:00:02.000\nLine 1\n\n00:00:02.000 --> 00:00:03.000\nLine 2\n\n" + # ``` + # + # Accepts an optional settings fields hash to add settings attribute to the resulting vtt file. + def self.build(setting_fields : Hash(String, String)? = nil, &) + String.build do |str| + builder = Builder.new(str) + builder.document(setting_fields) do + yield builder + end + end + end +end From 0cb7d0b44137c2cee9b6352969a28dac4e3576c5 Mon Sep 17 00:00:00 2001 From: syeopite Date: Thu, 24 Aug 2023 15:10:50 -0700 Subject: [PATCH 102/143] Refactor Invidious's VTT logic to use WebVtt.build --- src/invidious/routes/api/v1/videos.cr | 39 +++++++------------------ src/invidious/videos/caption.cr | 41 ++++++++------------------- src/invidious/videos/transcript.cr | 40 +++++--------------------- 3 files changed, 29 insertions(+), 91 deletions(-) diff --git a/src/invidious/routes/api/v1/videos.cr b/src/invidious/routes/api/v1/videos.cr index 25e766d2..5c50a804 100644 --- a/src/invidious/routes/api/v1/videos.cr +++ b/src/invidious/routes/api/v1/videos.cr @@ -101,20 +101,17 @@ module Invidious::Routes::API::V1::Videos if caption.name.includes? "auto-generated" caption_xml = YT_POOL.client &.get(url).body + settings_field = { + "Kind" => "captions", + "Language" => "#{tlang || caption.language_code}", + } + if caption_xml.starts_with?("/, "") text = text.gsub(/<\/font>/, "") @@ -137,12 +131,7 @@ module Invidious::Routes::API::V1::Videos text = "#{md["text"]}" end - str << <<-END_CUE - #{start_time} --> #{end_time} - #{text} - - - END_CUE + webvtt.line(start_time, end_time, text) end end end @@ -215,11 +204,7 @@ module Invidious::Routes::API::V1::Videos storyboard = storyboard[0] end - String.build do |str| - str << <<-END_VTT - WEBVTT - END_VTT - + WebVTT.build do |vtt| start_time = 0.milliseconds end_time = storyboard[:interval].milliseconds @@ -231,12 +216,8 @@ module Invidious::Routes::API::V1::Videos storyboard[:storyboard_height].times do |j| storyboard[:storyboard_width].times do |k| - str << <<-END_CUE - #{start_time}.000 --> #{end_time}.000 - #{url}#xywh=#{storyboard[:width] * k},#{storyboard[:height] * j},#{storyboard[:width] - 2},#{storyboard[:height]} - - - END_CUE + current_cue_url = "#{url}#xywh=#{storyboard[:width] * k},#{storyboard[:height] * j},#{storyboard[:width] - 2},#{storyboard[:height]}" + vtt.line(start_time, end_time, current_cue_url) start_time += storyboard[:interval].milliseconds end_time += storyboard[:interval].milliseconds diff --git a/src/invidious/videos/caption.cr b/src/invidious/videos/caption.cr index 256dfcc0..dc58f9a0 100644 --- a/src/invidious/videos/caption.cr +++ b/src/invidious/videos/caption.cr @@ -52,17 +52,13 @@ module Invidious::Videos break end end - result = String.build do |result| - result << <<-END_VTT - WEBVTT - Kind: captions - Language: #{tlang || @language_code} + settings_field = { + "Kind" => "captions", + "Language" => "#{tlang || @language_code}", + } - END_VTT - - result << "\n\n" - + result = WebVTT.build(settings_field) do |vtt| cues.each_with_index do |node, i| start_time = node["t"].to_f.milliseconds @@ -76,29 +72,16 @@ module Invidious::Videos end_time = start_time + duration end - # start_time - result << start_time.hours.to_s.rjust(2, '0') - result << ':' << start_time.minutes.to_s.rjust(2, '0') - result << ':' << start_time.seconds.to_s.rjust(2, '0') - result << '.' << start_time.milliseconds.to_s.rjust(3, '0') - - result << " --> " - - # end_time - result << end_time.hours.to_s.rjust(2, '0') - result << ':' << end_time.minutes.to_s.rjust(2, '0') - result << ':' << end_time.seconds.to_s.rjust(2, '0') - result << '.' << end_time.milliseconds.to_s.rjust(3, '0') - - result << "\n" - - node.children.each do |s| - result << s.content + text = String.build do |io| + node.children.each do |s| + io << s.content + end end - result << "\n" - result << "\n" + + vtt.line(start_time, end_time, text) end end + return result end end diff --git a/src/invidious/videos/transcript.cr b/src/invidious/videos/transcript.cr index f3360a52..cd97cfde 100644 --- a/src/invidious/videos/transcript.cr +++ b/src/invidious/videos/transcript.cr @@ -34,41 +34,15 @@ module Invidious::Videos # Convert into array of TranscriptLine lines = self.parse(initial_data) + settings_field = { + "Kind" => "captions", + "Language" => target_language + } + # Taken from Invidious::Videos::Captions::Metadata.timedtext_to_vtt() - vtt = String.build do |vtt| - vtt << <<-END_VTT - WEBVTT - Kind: captions - Language: #{target_language} - - - END_VTT - - vtt << "\n\n" - + vtt = WebVTT.build(settings_field) do |vtt| lines.each do |line| - start_time = line.start_ms - end_time = line.end_ms - - # start_time - vtt << start_time.hours.to_s.rjust(2, '0') - vtt << ':' << start_time.minutes.to_s.rjust(2, '0') - vtt << ':' << start_time.seconds.to_s.rjust(2, '0') - vtt << '.' << start_time.milliseconds.to_s.rjust(3, '0') - - vtt << " --> " - - # end_time - vtt << end_time.hours.to_s.rjust(2, '0') - vtt << ':' << end_time.minutes.to_s.rjust(2, '0') - vtt << ':' << end_time.seconds.to_s.rjust(2, '0') - vtt << '.' << end_time.milliseconds.to_s.rjust(3, '0') - - vtt << "\n" - vtt << line.line - - vtt << "\n" - vtt << "\n" + vtt.line(line.start_ms, line.end_ms, line.line) end end From d371eb50f27b9d29bc68ec883d8bee54894c79a4 Mon Sep 17 00:00:00 2001 From: syeopite Date: Thu, 24 Aug 2023 15:42:42 -0700 Subject: [PATCH 103/143] WebVTT::Builder: rename #line to #cue --- spec/helpers/vtt/builder_spec.cr | 4 ++-- src/invidious/helpers/webvtt.cr | 8 ++++---- src/invidious/routes/api/v1/videos.cr | 4 ++-- src/invidious/videos/caption.cr | 2 +- src/invidious/videos/transcript.cr | 2 +- 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/spec/helpers/vtt/builder_spec.cr b/spec/helpers/vtt/builder_spec.cr index 69303bab..7b543ddc 100644 --- a/spec/helpers/vtt/builder_spec.cr +++ b/spec/helpers/vtt/builder_spec.cr @@ -18,7 +18,7 @@ Spectator.describe "WebVTT::Builder" do it "correctly builds a vtt file" do result = WebVTT.build do |vtt| MockLines.each do |line| - vtt.line(line["start_time"], line["end_time"], line["text"]) + vtt.cue(line["start_time"], line["end_time"], line["text"]) end end @@ -43,7 +43,7 @@ Spectator.describe "WebVTT::Builder" do result = WebVTT.build(setting_fields) do |vtt| MockLines.each do |line| - vtt.line(line["start_time"], line["end_time"], line["text"]) + vtt.cue(line["start_time"], line["end_time"], line["text"]) end end diff --git a/src/invidious/helpers/webvtt.cr b/src/invidious/helpers/webvtt.cr index 7d9d5f1f..c50d7fa2 100644 --- a/src/invidious/helpers/webvtt.cr +++ b/src/invidious/helpers/webvtt.cr @@ -7,8 +7,8 @@ module WebVTT def initialize(@io : IO) end - # Writes an vtt line with the specified time stamp and contents - def line(start_time : Time::Span, end_time : Time::Span, text : String) + # Writes an vtt cue with the specified time stamp and contents + def cue(start_time : Time::Span, end_time : Time::Span, text : String) timestamp(start_time, end_time) @io << text @io << "\n\n" @@ -48,8 +48,8 @@ module WebVTT # # ``` # string = WebVTT.build do |io| - # vtt.line(Time::Span.new(seconds: 1), Time::Span.new(seconds: 2), "Line 1") - # vtt.line(Time::Span.new(seconds: 2), Time::Span.new(seconds: 3), "Line 2") + # vtt.cue(Time::Span.new(seconds: 1), Time::Span.new(seconds: 2), "Line 1") + # vtt.cue(Time::Span.new(seconds: 2), Time::Span.new(seconds: 3), "Line 2") # end # # string # => "WEBVTT\n\n00:00:01.000 --> 00:00:02.000\nLine 1\n\n00:00:02.000 --> 00:00:03.000\nLine 2\n\n" diff --git a/src/invidious/routes/api/v1/videos.cr b/src/invidious/routes/api/v1/videos.cr index 5c50a804..449c9f9b 100644 --- a/src/invidious/routes/api/v1/videos.cr +++ b/src/invidious/routes/api/v1/videos.cr @@ -131,7 +131,7 @@ module Invidious::Routes::API::V1::Videos text = "#{md["text"]}" end - webvtt.line(start_time, end_time, text) + webvtt.cue(start_time, end_time, text) end end end @@ -217,7 +217,7 @@ module Invidious::Routes::API::V1::Videos storyboard[:storyboard_height].times do |j| storyboard[:storyboard_width].times do |k| current_cue_url = "#{url}#xywh=#{storyboard[:width] * k},#{storyboard[:height] * j},#{storyboard[:width] - 2},#{storyboard[:height]}" - vtt.line(start_time, end_time, current_cue_url) + vtt.cue(start_time, end_time, current_cue_url) start_time += storyboard[:interval].milliseconds end_time += storyboard[:interval].milliseconds diff --git a/src/invidious/videos/caption.cr b/src/invidious/videos/caption.cr index dc58f9a0..484e61d2 100644 --- a/src/invidious/videos/caption.cr +++ b/src/invidious/videos/caption.cr @@ -78,7 +78,7 @@ module Invidious::Videos end end - vtt.line(start_time, end_time, text) + vtt.cue(start_time, end_time, text) end end diff --git a/src/invidious/videos/transcript.cr b/src/invidious/videos/transcript.cr index cd97cfde..055d96fb 100644 --- a/src/invidious/videos/transcript.cr +++ b/src/invidious/videos/transcript.cr @@ -42,7 +42,7 @@ module Invidious::Videos # Taken from Invidious::Videos::Captions::Metadata.timedtext_to_vtt() vtt = WebVTT.build(settings_field) do |vtt| lines.each do |line| - vtt.line(line.start_ms, line.end_ms, line.line) + vtt.cue(line.start_ms, line.end_ms, line.line) end end From 4e97d8ad0942bd64a23ed4a2ba89e48a97c520aa Mon Sep 17 00:00:00 2001 From: syeopite Date: Thu, 24 Aug 2023 16:27:06 -0700 Subject: [PATCH 104/143] Update documentation for `WebVTT.build` --- src/invidious/helpers/webvtt.cr | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/invidious/helpers/webvtt.cr b/src/invidious/helpers/webvtt.cr index c50d7fa2..52138854 100644 --- a/src/invidious/helpers/webvtt.cr +++ b/src/invidious/helpers/webvtt.cr @@ -44,10 +44,10 @@ module WebVTT end end - # Returns the resulting `String` of writing WebVTT to the yielded WebVTT::Builder + # Returns the resulting `String` of writing WebVTT to the yielded `WebVTT::Builder` # # ``` - # string = WebVTT.build do |io| + # string = WebVTT.build do |vtt| # vtt.cue(Time::Span.new(seconds: 1), Time::Span.new(seconds: 2), "Line 1") # vtt.cue(Time::Span.new(seconds: 2), Time::Span.new(seconds: 3), "Line 2") # end From e9d59a6dfd14fd115f3bfc59ca6f33182a631575 Mon Sep 17 00:00:00 2001 From: syeopite <70992037+syeopite@users.noreply.github.com> Date: Tue, 29 Aug 2023 05:59:08 +0000 Subject: [PATCH 105/143] Update src/invidious/helpers/webvtt.cr Co-authored-by: Samantaz Fox --- src/invidious/helpers/webvtt.cr | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/invidious/helpers/webvtt.cr b/src/invidious/helpers/webvtt.cr index 52138854..aace6bb8 100644 --- a/src/invidious/helpers/webvtt.cr +++ b/src/invidious/helpers/webvtt.cr @@ -34,7 +34,7 @@ module WebVTT if setting_fields setting_fields.each do |name, value| - @io << "#{name}: #{value}\n" + @io << name << ": " << value << '\n' end end From a999438ae46739477a6ca5f8515fa70b6b492443 Mon Sep 17 00:00:00 2001 From: syeopite Date: Mon, 28 Aug 2023 23:14:25 -0700 Subject: [PATCH 106/143] Consistency: rename #add_timestamp_component Removes the add_ prefix for consistency with the other methods in WebVTT::Builder --- src/invidious/helpers/webvtt.cr | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/invidious/helpers/webvtt.cr b/src/invidious/helpers/webvtt.cr index aace6bb8..56f761ed 100644 --- a/src/invidious/helpers/webvtt.cr +++ b/src/invidious/helpers/webvtt.cr @@ -15,14 +15,14 @@ module WebVTT end private def timestamp(start_time : Time::Span, end_time : Time::Span) - add_timestamp_component(start_time) + timestamp_component(start_time) @io << " --> " - add_timestamp_component(end_time) + timestamp_component(end_time) @io << '\n' end - private def add_timestamp_component(timestamp : Time::Span) + private def timestamp_component(timestamp : Time::Span) @io << timestamp.hours.to_s.rjust(2, '0') @io << ':' << timestamp.minutes.to_s.rjust(2, '0') @io << ':' << timestamp.seconds.to_s.rjust(2, '0') From be2feba17c2f3b9d8e043825beff57568df46f2e Mon Sep 17 00:00:00 2001 From: syeopite Date: Sat, 23 Sep 2023 09:57:26 -0400 Subject: [PATCH 107/143] Lint --- src/invidious/videos/transcript.cr | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/invidious/videos/transcript.cr b/src/invidious/videos/transcript.cr index 055d96fb..dac00eea 100644 --- a/src/invidious/videos/transcript.cr +++ b/src/invidious/videos/transcript.cr @@ -35,8 +35,8 @@ module Invidious::Videos lines = self.parse(initial_data) settings_field = { - "Kind" => "captions", - "Language" => target_language + "Kind" => "captions", + "Language" => target_language, } # Taken from Invidious::Videos::Captions::Metadata.timedtext_to_vtt() From 4f25069f55b5ee87bb214a975d97522155bffc2c Mon Sep 17 00:00:00 2001 From: ChunkyProgrammer <78101139+ChunkyProgrammer@users.noreply.github.com> Date: Tue, 26 Sep 2023 19:05:37 -0400 Subject: [PATCH 108/143] remove unused variable simplify resolve url remove trailing spaces Co-Authored-By: Samantaz Fox --- assets/js/comments.js | 2 +- src/invidious/routes/api/v1/channels.cr | 1 - src/invidious/routes/api/v1/misc.cr | 4 ++-- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/assets/js/comments.js b/assets/js/comments.js index 00a8cae9..35ffa96e 100644 --- a/assets/js/comments.js +++ b/assets/js/comments.js @@ -131,7 +131,7 @@ function get_youtube_replies(target, load_more, load_replies) { '&hl=' + video_data.preferences.locale + '&thin_mode=' + video_data.preferences.thin_mode + '&continuation=' + continuation; - + if (video_data.ucid) { url += '&ucid=' + video_data.ucid } diff --git a/src/invidious/routes/api/v1/channels.cr b/src/invidious/routes/api/v1/channels.cr index a5ae16a8..67018660 100644 --- a/src/invidious/routes/api/v1/channels.cr +++ b/src/invidious/routes/api/v1/channels.cr @@ -373,7 +373,6 @@ module Invidious::Routes::API::V1::Channels def self.post_comments(env) locale = env.get("preferences").as(Preferences).locale - region = env.params.query["region"]? env.response.content_type = "application/json" diff --git a/src/invidious/routes/api/v1/misc.cr b/src/invidious/routes/api/v1/misc.cr index 5dfc4afa..8a92e160 100644 --- a/src/invidious/routes/api/v1/misc.cr +++ b/src/invidious/routes/api/v1/misc.cr @@ -173,8 +173,8 @@ module Invidious::Routes::API::V1::Misc end JSON.build do |json| json.object do - json.field "ucid", sub_endpoint["browseId"].try &.as_s if sub_endpoint["browseId"]? - json.field "videoId", sub_endpoint["videoId"].try &.as_s if sub_endpoint["videoId"]? + json.field "ucid", sub_endpoint["browseId"].as_s if sub_endpoint["browseId"]? + json.field "videoId", sub_endpoint["videoId"].as_s if sub_endpoint["videoId"]? json.field "params", params.try &.as_s json.field "pageType", pageType end From f77e4378fe1ee69d0cf8adced1c8eef8140896ee Mon Sep 17 00:00:00 2001 From: ChunkyProgrammer <78101139+ChunkyProgrammer@users.noreply.github.com> Date: Wed, 27 Sep 2023 18:40:50 -0400 Subject: [PATCH 109/143] Add support for viewing comments without js Improve stylings --- assets/css/default.css | 8 ++++++++ src/invidious/routes/channels.cr | 11 ++++++++++- src/invidious/views/post.ecr | 25 +++++++++++++++++++++---- 3 files changed, 39 insertions(+), 5 deletions(-) diff --git a/assets/css/default.css b/assets/css/default.css index 69fe8d5f..b4053b5c 100644 --- a/assets/css/default.css +++ b/assets/css/default.css @@ -397,6 +397,14 @@ p.video-data { margin: 0; font-weight: bold; font-size: 80%; } margin: auto; } +/* + * We don't want the top and bottom margin on the post page. + */ +.comments.post-comments { + margin-bottom: 0; + margin-top: 0; +} + .video-iframe-wrapper { position: relative; height: 0; diff --git a/src/invidious/routes/channels.cr b/src/invidious/routes/channels.cr index 29995bf6..62b3884e 100644 --- a/src/invidious/routes/channels.cr +++ b/src/invidious/routes/channels.cr @@ -200,11 +200,15 @@ module Invidious::Routes::Channels prefs = env.get("preferences").as(Preferences) locale = prefs.locale - region = env.params.query["region"]? || prefs.region thin_mode = env.params.query["thin_mode"]? || prefs.thin_mode thin_mode = thin_mode == "true" + nojs = env.params.query["nojs"]? + + nojs ||= "0" + nojs = nojs == "1" + if !ucid.nil? ucid = ucid.to_s post_response = fetch_channel_community_post(ucid, id, locale, "json", thin_mode) @@ -218,6 +222,11 @@ module Invidious::Routes::Channels end post_response = JSON.parse(post_response) + + if nojs + comments = Comments.fetch_community_post_comments(ucid, id) + comment_html = JSON.parse(Comments.parse_youtube(id, comments, "html", locale, thin_mode, isPost: true))["contentHtml"] + end templated "post" end diff --git a/src/invidious/views/post.ecr b/src/invidious/views/post.ecr index 071d1c88..fb03a44c 100644 --- a/src/invidious/views/post.ecr +++ b/src/invidious/views/post.ecr @@ -2,10 +2,27 @@ Invidious <% end %> -
- <%= IV::Frontend::Comments.template_youtube(post_response.not_nil!, locale, thin_mode) %> -
-
+
+
+ <%= IV::Frontend::Comments.template_youtube(post_response.not_nil!, locale, thin_mode) %> +
+ + <% if nojs %> +
+ <% end %> +
+ +
+ <% if nojs %> + <%= comment_html %> + <% else %> + + <% end %> +