revert module encapsulation, remove Log.forf macro

This commit is contained in:
Fijxu 2025-08-31 16:30:29 -04:00
parent d0cd940893
commit b6f164bef3
No known key found for this signature in database
GPG Key ID: 32C1DDF333EDA6A4
70 changed files with 1116 additions and 1133 deletions

View File

@ -24,7 +24,7 @@ def create_licence_tr(path, file_name, licence_name, licence_link, source_locati
"<tr> "<tr>
<td><a href=\\"/#{path}\\">#{file_name}</a></td> <td><a href=\\"/#{path}\\">#{file_name}</a></td>
<td><a href=\\"#{licence_link}\\">#{licence_name}</a></td> <td><a href=\\"#{licence_link}\\">#{licence_name}</a></td>
<td><a href=\\"#{source_location}\\">\#{I18n.translate(locale, "source")}</a></td> <td><a href=\\"#{source_location}\\">\#{translate(locale, "source")}</a></td>
</tr>" </tr>"
HTML HTML

View File

@ -4,7 +4,7 @@ Spectator.describe Invidious::Hashtag do
it "parses richItemRenderer containers (test 1)" do it "parses richItemRenderer containers (test 1)" do
# Enable mock # Enable mock
test_content = load_mock("hashtag/martingarrix_page1") test_content = load_mock("hashtag/martingarrix_page1")
videos, _ = YoutubeJSONParser.extract_items(test_content) videos, _ = extract_items(test_content)
expect(typeof(videos)).to eq(Array(SearchItem)) expect(typeof(videos)).to eq(Array(SearchItem))
expect(videos.size).to eq(60) expect(videos.size).to eq(60)
@ -57,7 +57,7 @@ Spectator.describe Invidious::Hashtag do
it "parses richItemRenderer containers (test 2)" do it "parses richItemRenderer containers (test 2)" do
# Enable mock # Enable mock
test_content = load_mock("hashtag/martingarrix_page2") test_content = load_mock("hashtag/martingarrix_page2")
videos, _ = YoutubeJSONParser.extract_items(test_content) videos, _ = extract_items(test_content)
expect(typeof(videos)).to eq(Array(SearchItem)) expect(typeof(videos)).to eq(Array(SearchItem))
expect(videos.size).to eq(60) expect(videos.size).to eq(60)

View File

@ -7,7 +7,7 @@ Spectator.describe "parse_video_info" do
_next = load_mock("video/regular_mrbeast.next") _next = load_mock("video/regular_mrbeast.next")
raw_data = _player.merge!(_next) raw_data = _player.merge!(_next)
info = Parser.parse_video_info("2isYuQZMbdU", raw_data) info = parse_video_info("2isYuQZMbdU", raw_data)
# Some basic verifications # Some basic verifications
expect(typeof(info)).to eq(Hash(String, JSON::Any)) expect(typeof(info)).to eq(Hash(String, JSON::Any))
@ -89,7 +89,7 @@ Spectator.describe "parse_video_info" do
_next = load_mock("video/regular_no-description.next") _next = load_mock("video/regular_no-description.next")
raw_data = _player.merge!(_next) raw_data = _player.merge!(_next)
info = Parser.parse_video_info("iuevw6218F0", raw_data) info = parse_video_info("iuevw6218F0", raw_data)
# Some basic verifications # Some basic verifications
expect(typeof(info)).to eq(Hash(String, JSON::Any)) expect(typeof(info)).to eq(Hash(String, JSON::Any))

View File

@ -7,7 +7,7 @@ Spectator.describe "parse_video_info" do
_next = load_mock("video/scheduled_live_PBD-Podcast.next") _next = load_mock("video/scheduled_live_PBD-Podcast.next")
raw_data = _player.merge!(_next) raw_data = _player.merge!(_next)
info = Parser.parse_video_info("N-yVic7BbY0", raw_data) info = parse_video_info("N-yVic7BbY0", raw_data)
# Some basic verifications # Some basic verifications
expect(typeof(info)).to eq(Hash(String, JSON::Any)) expect(typeof(info)).to eq(Hash(String, JSON::Any))

View File

@ -200,7 +200,7 @@ def fetch_related_channels(about_channel : AboutChannel, continuation : String?
initial_data = YoutubeAPI.browse(continuation) initial_data = YoutubeAPI.browse(continuation)
end end
items, continuation = YoutubeJSONParser.extract_items(initial_data) items, continuation = extract_items(initial_data)
return items.select(SearchChannel), continuation return items.select(SearchChannel), continuation
end end

View File

@ -38,7 +38,7 @@ struct ChannelVideo
json.field "authorId", self.ucid json.field "authorId", self.ucid
json.field "authorUrl", "/channel/#{self.ucid}" json.field "authorUrl", "/channel/#{self.ucid}"
json.field "published", self.published.to_unix json.field "published", self.published.to_unix
json.field "publishedText", I18n.translate(locale, "`x` ago", recode_date(self.published, locale)) json.field "publishedText", translate(locale, "`x` ago", recode_date(self.published, locale))
json.field "viewCount", self.views json.field "viewCount", self.views
end end
@ -156,8 +156,8 @@ def get_channel(id) : InvidiousChannel
end end
def fetch_channel(ucid, pull_all_videos : Bool) def fetch_channel(ucid, pull_all_videos : Bool)
::Log.forf.debug { "#{ucid}" } Log.debug { "fetch_channel: #{ucid}" }
::Log.forf.trace { "#{ucid} : pull_all_videos = #{pull_all_videos}" } Log.trace { "fetch_channel: #{ucid} : pull_all_videos = #{pull_all_videos}" }
namespaces = { namespaces = {
"yt" => "http://www.youtube.com/xml/schemas/2015", "yt" => "http://www.youtube.com/xml/schemas/2015",
@ -165,9 +165,9 @@ def fetch_channel(ucid, pull_all_videos : Bool)
"default" => "http://www.w3.org/2005/Atom", "default" => "http://www.w3.org/2005/Atom",
} }
::Log.forf.trace { "#{ucid} : Downloading RSS feed" } Log.trace { "fetch_channel: #{ucid} : Downloading RSS feed" }
rss = YT_POOL.client &.get("/feeds/videos.xml?channel_id=#{ucid}").body rss = YT_POOL.client &.get("/feeds/videos.xml?channel_id=#{ucid}").body
::Log.forf.trace { "#{ucid} : Parsing RSS feed" } Log.trace { "fetch_channel: #{ucid} : Parsing RSS feed" }
rss = XML.parse(rss) rss = XML.parse(rss)
author = rss.xpath_node("//default:feed/default:title", namespaces) author = rss.xpath_node("//default:feed/default:title", namespaces)
@ -184,7 +184,7 @@ def fetch_channel(ucid, pull_all_videos : Bool)
auto_generated = true auto_generated = true
end end
::Log.forf.trace { "#{ucid} : author = #{author}, auto_generated = #{auto_generated}" } Log.trace { "fetch_channel: #{ucid} : author = #{author}, auto_generated = #{auto_generated}" }
channel = InvidiousChannel.new({ channel = InvidiousChannel.new({
id: ucid, id: ucid,
@ -194,10 +194,10 @@ def fetch_channel(ucid, pull_all_videos : Bool)
subscribed: nil, subscribed: nil,
}) })
::Log.forf.trace { "#{ucid} : Downloading channel videos page" } Log.trace { "fetch_channel: #{ucid} : Downloading channel videos page" }
videos, continuation = IV::Channel::Tabs.get_videos(channel) videos, continuation = IV::Channel::Tabs.get_videos(channel)
::Log.forf.trace { "#{ucid} : Extracting videos from channel RSS feed" } Log.trace { "fetch_channel: #{ucid} : Extracting videos from channel RSS feed" }
rss.xpath_nodes("//default:feed/default:entry", namespaces).each do |entry| rss.xpath_nodes("//default:feed/default:entry", namespaces).each do |entry|
video_id = entry.xpath_node("yt:videoId", namespaces).not_nil!.content video_id = entry.xpath_node("yt:videoId", namespaces).not_nil!.content
title = entry.xpath_node("default:title", namespaces).not_nil!.content title = entry.xpath_node("default:title", namespaces).not_nil!.content
@ -241,17 +241,17 @@ def fetch_channel(ucid, pull_all_videos : Bool)
views: views, views: views,
}) })
::Log.forf.trace { "#{ucid} : video #{video_id} : Updating or inserting video" } Log.trace { "fetch_channel: #{ucid} : video #{video_id} : Updating or inserting video" }
# We don't include the 'premiere_timestamp' here because channel pages don't include them, # We don't include the 'premiere_timestamp' here because channel pages don't include them,
# meaning the above timestamp is always null # meaning the above timestamp is always null
was_insert = Invidious::Database::ChannelVideos.insert(video) was_insert = Invidious::Database::ChannelVideos.insert(video)
if was_insert if was_insert
::Log.forf.trace { "#{ucid} : video #{video_id} : Inserted, updating subscriptions" } Log.trace { "fetch_channel: #{ucid} : video #{video_id} : Inserted, updating subscriptions" }
NOTIFICATION_CHANNEL.send(VideoNotification.from_video(video)) NOTIFICATION_CHANNEL.send(VideoNotification.from_video(video))
else else
::Log.forf.trace { "#{ucid} : video #{video_id} : Updated" } Log.trace { "fetch_channel: #{ucid} : video #{video_id} : Updated" }
end end
end end

View File

@ -7,7 +7,7 @@ def fetch_channel_community(ucid, cursor, locale, format, thin_mode)
initial_data = YoutubeAPI.browse(ucid, params: "EgVwb3N0c_IGBAoCSgA%3D") initial_data = YoutubeAPI.browse(ucid, params: "EgVwb3N0c_IGBAoCSgA%3D")
items = [] of JSON::Any items = [] of JSON::Any
YoutubeJSONParser.extract_items(initial_data) do |item| extract_items(initial_data) do |item|
items << item items << item
end end
else else
@ -49,7 +49,7 @@ def fetch_channel_community_post(ucid, post_id, locale, format, thin_mode)
initial_data = YoutubeAPI.browse("FEpost_detail", params: params) initial_data = YoutubeAPI.browse("FEpost_detail", params: params)
items = [] of JSON::Any items = [] of JSON::Any
YoutubeJSONParser.extract_items(initial_data) do |item| extract_items(initial_data) do |item|
items << item items << item
end end
@ -131,7 +131,7 @@ def extract_channel_community(items, *, ucid, locale, format, thin_mode, is_sing
json.field "contentHtml", content_html json.field "contentHtml", content_html
json.field "published", published.to_unix json.field "published", published.to_unix
json.field "publishedText", I18n.translate(locale, "`x` ago", recode_date(published, locale)) json.field "publishedText", translate(locale, "`x` ago", recode_date(published, locale))
json.field "likeCount", like_count json.field "likeCount", like_count
json.field "replyCount", reply_count json.field "replyCount", reply_count
@ -142,7 +142,7 @@ def extract_channel_community(items, *, ucid, locale, format, thin_mode, is_sing
json.field "attachment" do json.field "attachment" do
case attachment.as_h case attachment.as_h
when .has_key?("videoRenderer") when .has_key?("videoRenderer")
YoutubeJSONParser.parse_item(attachment) parse_item(attachment)
.as(SearchVideo) .as(SearchVideo)
.to_json(locale, json) .to_json(locale, json)
when .has_key?("backstageImageRenderer") when .has_key?("backstageImageRenderer")
@ -230,7 +230,7 @@ def extract_channel_community(items, *, ucid, locale, format, thin_mode, is_sing
end end
end end
when .has_key?("playlistRenderer") when .has_key?("playlistRenderer")
YoutubeJSONParser.parse_item(attachment) parse_item(attachment)
.as(SearchPlaylist) .as(SearchPlaylist)
.to_json(locale, json) .to_json(locale, json)
when .has_key?("quizRenderer") when .has_key?("quizRenderer")

View File

@ -24,7 +24,7 @@ def fetch_channel_playlists(ucid, author, continuation, sort_by)
initial_data = YoutubeAPI.browse(ucid, params: params || "") initial_data = YoutubeAPI.browse(ucid, params: params || "")
end end
return YoutubeJSONParser.extract_items(initial_data, author, ucid) return extract_items(initial_data, author, ucid)
end end
def fetch_channel_podcasts(ucid, author, continuation) def fetch_channel_podcasts(ucid, author, continuation)
@ -33,7 +33,7 @@ def fetch_channel_podcasts(ucid, author, continuation)
else else
initial_data = YoutubeAPI.browse(ucid, params: "Eghwb2RjYXN0c_IGBQoDugEA") initial_data = YoutubeAPI.browse(ucid, params: "Eghwb2RjYXN0c_IGBQoDugEA")
end end
return YoutubeJSONParser.extract_items(initial_data, author, ucid) return extract_items(initial_data, author, ucid)
end end
def fetch_channel_releases(ucid, author, continuation) def fetch_channel_releases(ucid, author, continuation)
@ -42,7 +42,7 @@ def fetch_channel_releases(ucid, author, continuation)
else else
initial_data = YoutubeAPI.browse(ucid, params: "EghyZWxlYXNlc_IGBQoDsgEA") initial_data = YoutubeAPI.browse(ucid, params: "EghyZWxlYXNlc_IGBQoDsgEA")
end end
return YoutubeJSONParser.extract_items(initial_data, author, ucid) return extract_items(initial_data, author, ucid)
end end
def fetch_channel_courses(ucid, author, continuation) def fetch_channel_courses(ucid, author, continuation)
@ -51,5 +51,5 @@ def fetch_channel_courses(ucid, author, continuation)
else else
initial_data = YoutubeAPI.browse(ucid, params: "Egdjb3Vyc2Vz8gYFCgPCAQA%3D") initial_data = YoutubeAPI.browse(ucid, params: "Egdjb3Vyc2Vz8gYFCgPCAQA%3D")
end end
return YoutubeJSONParser.extract_items(initial_data, author, ucid) return extract_items(initial_data, author, ucid)
end end

View File

@ -29,7 +29,7 @@ module Invidious::Channel::Tabs
continuation ||= make_initial_videos_ctoken(ucid, sort_by) continuation ||= make_initial_videos_ctoken(ucid, sort_by)
initial_data = YoutubeAPI.browse(continuation: continuation) initial_data = YoutubeAPI.browse(continuation: continuation)
return YoutubeJSONParser.extract_items(initial_data, author, ucid) return extract_items(initial_data, author, ucid)
end end
def get_60_videos(channel : AboutChannel, *, continuation : String? = nil, sort_by = "newest") def get_60_videos(channel : AboutChannel, *, continuation : String? = nil, sort_by = "newest")
@ -59,7 +59,7 @@ module Invidious::Channel::Tabs
continuation ||= make_initial_shorts_ctoken(channel.ucid, sort_by) continuation ||= make_initial_shorts_ctoken(channel.ucid, sort_by)
initial_data = YoutubeAPI.browse(continuation: continuation) initial_data = YoutubeAPI.browse(continuation: continuation)
return YoutubeJSONParser.extract_items(initial_data, channel.author, channel.ucid) return extract_items(initial_data, channel.author, channel.ucid)
end end
# ------------------- # -------------------
@ -70,7 +70,7 @@ module Invidious::Channel::Tabs
continuation ||= make_initial_livestreams_ctoken(channel.ucid, sort_by) continuation ||= make_initial_livestreams_ctoken(channel.ucid, sort_by)
initial_data = YoutubeAPI.browse(continuation: continuation) initial_data = YoutubeAPI.browse(continuation: continuation)
return YoutubeJSONParser.extract_items(initial_data, channel.author, channel.ucid) return extract_items(initial_data, channel.author, channel.ucid)
end end
def get_60_livestreams(channel : AboutChannel, *, continuation : String? = nil, sort_by = "newest") def get_60_livestreams(channel : AboutChannel, *, continuation : String? = nil, sort_by = "newest")

View File

@ -268,7 +268,7 @@ module Invidious::Comments
end end
json.field "published", published.to_unix json.field "published", published.to_unix
json.field "publishedText", I18n.translate(locale, "`x` ago", recode_date(published, locale)) json.field "publishedText", translate(locale, "`x` ago", recode_date(published, locale))
end end
if node_replies && !response["commentRepliesContinuation"]? if node_replies && !response["commentRepliesContinuation"]?

View File

@ -34,7 +34,7 @@ module Invidious::Database
return # TODO return # TODO
if !PG_DB.query_one?("SELECT true FROM pg_type WHERE typname = $1", enum_name, as: Bool) if !PG_DB.query_one?("SELECT true FROM pg_type WHERE typname = $1", enum_name, as: Bool)
::Log.forf.info { "CREATE TYPE #{enum_name}" } Log.info { "check_enum: CREATE TYPE #{enum_name}" }
PG_DB.using_connection do |conn| PG_DB.using_connection do |conn|
conn.as(PG::Connection).exec_all(File.read("config/sql/#{enum_name}.sql")) conn.as(PG::Connection).exec_all(File.read("config/sql/#{enum_name}.sql"))
@ -47,7 +47,7 @@ module Invidious::Database
begin begin
PG_DB.exec("SELECT * FROM #{table_name} LIMIT 0") PG_DB.exec("SELECT * FROM #{table_name} LIMIT 0")
rescue ex rescue ex
::Log.forf.info { "CREATE TABLE #{table_name}" } Log.info { "check_table: CREATE TABLE #{table_name}" }
PG_DB.using_connection do |conn| PG_DB.using_connection do |conn|
conn.as(PG::Connection).exec_all(File.read("config/sql/#{table_name}.sql")) conn.as(PG::Connection).exec_all(File.read("config/sql/#{table_name}.sql"))
@ -67,7 +67,7 @@ module Invidious::Database
if name != column_array[i]? if name != column_array[i]?
if !column_array[i]? if !column_array[i]?
new_column = column_types.select(&.starts_with?(name))[0] new_column = column_types.select(&.starts_with?(name))[0]
::Log.forf.info { "ALTER TABLE #{table_name} ADD COLUMN #{new_column}" } Log.info { "check_table: ALTER TABLE #{table_name} ADD COLUMN #{new_column}" }
PG_DB.exec("ALTER TABLE #{table_name} ADD COLUMN #{new_column}") PG_DB.exec("ALTER TABLE #{table_name} ADD COLUMN #{new_column}")
next next
end end
@ -85,29 +85,29 @@ module Invidious::Database
# There's a column we didn't expect # There's a column we didn't expect
if !new_column if !new_column
::Log.forf.info { "ALTER TABLE #{table_name} DROP COLUMN #{column_array[i]}" } Log.info { "check_table: ALTER TABLE #{table_name} DROP COLUMN #{column_array[i]}" }
PG_DB.exec("ALTER TABLE #{table_name} DROP COLUMN #{column_array[i]} CASCADE") PG_DB.exec("ALTER TABLE #{table_name} DROP COLUMN #{column_array[i]} CASCADE")
column_array = get_column_array(PG_DB, table_name) column_array = get_column_array(PG_DB, table_name)
next next
end end
::Log.forf.info { "ALTER TABLE #{table_name} ADD COLUMN #{new_column}" } Log.info { "check_table: ALTER TABLE #{table_name} ADD COLUMN #{new_column}" }
PG_DB.exec("ALTER TABLE #{table_name} ADD COLUMN #{new_column}") PG_DB.exec("ALTER TABLE #{table_name} ADD COLUMN #{new_column}")
::Log.forf.info { "UPDATE #{table_name} SET #{column_array[i]}_new=#{column_array[i]}" } Log.info { "check_table: UPDATE #{table_name} SET #{column_array[i]}_new=#{column_array[i]}" }
PG_DB.exec("UPDATE #{table_name} SET #{column_array[i]}_new=#{column_array[i]}") PG_DB.exec("UPDATE #{table_name} SET #{column_array[i]}_new=#{column_array[i]}")
::Log.forf.info { "ALTER TABLE #{table_name} DROP COLUMN #{column_array[i]} CASCADE" } Log.info { "check_table: ALTER TABLE #{table_name} DROP COLUMN #{column_array[i]} CASCADE" }
PG_DB.exec("ALTER TABLE #{table_name} DROP COLUMN #{column_array[i]} CASCADE") PG_DB.exec("ALTER TABLE #{table_name} DROP COLUMN #{column_array[i]} CASCADE")
::Log.forf.info { "ALTER TABLE #{table_name} RENAME COLUMN #{column_array[i]}_new TO #{column_array[i]}" } Log.info { "check_table: ALTER TABLE #{table_name} RENAME COLUMN #{column_array[i]}_new TO #{column_array[i]}" }
PG_DB.exec("ALTER TABLE #{table_name} RENAME COLUMN #{column_array[i]}_new TO #{column_array[i]}") PG_DB.exec("ALTER TABLE #{table_name} RENAME COLUMN #{column_array[i]}_new TO #{column_array[i]}")
column_array = get_column_array(PG_DB, table_name) column_array = get_column_array(PG_DB, table_name)
end end
else else
::Log.forf.info { "ALTER TABLE #{table_name} DROP COLUMN #{column_array[i]} CASCADE" } Log.info { "check_table: ALTER TABLE #{table_name} DROP COLUMN #{column_array[i]} CASCADE" }
PG_DB.exec("ALTER TABLE #{table_name} DROP COLUMN #{column_array[i]} CASCADE") PG_DB.exec("ALTER TABLE #{table_name} DROP COLUMN #{column_array[i]} CASCADE")
end end
end end
@ -117,7 +117,7 @@ module Invidious::Database
column_array.each do |column| column_array.each do |column|
if !struct_array.includes? column if !struct_array.includes? column
::Log.forf.info { "ALTER TABLE #{table_name} DROP COLUMN #{column} CASCADE" } Log.info { "check_table: ALTER TABLE #{table_name} DROP COLUMN #{column} CASCADE" }
PG_DB.exec("ALTER TABLE #{table_name} DROP COLUMN #{column} CASCADE") PG_DB.exec("ALTER TABLE #{table_name} DROP COLUMN #{column} CASCADE")
end end
end end

View File

@ -28,14 +28,14 @@ module Invidious::Frontend::ChannelPage
if tab == selected_tab if tab == selected_tab
str << "\t<b>" str << "\t<b>"
str << I18n.translate(locale, "channel_tab_#{tab_name}_label") str << translate(locale, "channel_tab_#{tab_name}_label")
str << "</b>\n" str << "</b>\n"
else else
# Video tab doesn't have the last path component # Video tab doesn't have the last path component
url = tab.videos? ? base_url : "#{base_url}/#{tab_name}" url = tab.videos? ? base_url : "#{base_url}/#{tab_name}"
str << %(\t<a href=") << url << %(">) str << %(\t<a href=") << url << %(">)
str << I18n.translate(locale, "channel_tab_#{tab_name}_label") str << translate(locale, "channel_tab_#{tab_name}_label")
str << "</a>\n" str << "</a>\n"
end end

View File

@ -32,9 +32,9 @@ module Invidious::Frontend::Comments
<p> <p>
<a href="javascript:void(0)" data-onclick="toggle_parent">[ ]</a> <a href="javascript:void(0)" data-onclick="toggle_parent">[ ]</a>
<b><a href="https://www.reddit.com/user/#{child.author}">#{child.author}</a></b> <b><a href="https://www.reddit.com/user/#{child.author}">#{child.author}</a></b>
#{I18n.translate_count(locale, "comments_points_count", child.score, I18n::NumberFormatting::Separator)} #{translate_count(locale, "comments_points_count", child.score, NumberFormatting::Separator)}
<span title="#{child.created_utc.to_s("%a %B %-d %T %Y UTC")}">#{I18n.translate(locale, "`x` ago", recode_date(child.created_utc, locale))}</span> <span title="#{child.created_utc.to_s("%a %B %-d %T %Y UTC")}">#{translate(locale, "`x` ago", recode_date(child.created_utc, locale))}</span>
<a href="https://www.reddit.com#{child.permalink}" title="#{I18n.translate(locale, "permalink")}">#{I18n.translate(locale, "permalink")}</a> <a href="https://www.reddit.com#{child.permalink}" title="#{translate(locale, "permalink")}">#{translate(locale, "permalink")}</a>
</p> </p>
<div> <div>
#{body_html} #{body_html}

View File

@ -6,10 +6,10 @@ module Invidious::Frontend::Comments
root = comments["comments"].as_a root = comments["comments"].as_a
root.each do |child| root.each do |child|
if child["replies"]? if child["replies"]?
replies_count_text = I18n.translate_count(locale, replies_count_text = translate_count(locale,
"comments_view_x_replies", "comments_view_x_replies",
child["replies"]["replyCount"].as_i64 || 0, child["replies"]["replyCount"].as_i64 || 0,
I18n::NumberFormatting::Separator NumberFormatting::Separator
) )
replies_html = <<-END_HTML replies_html = <<-END_HTML
@ -25,10 +25,10 @@ module Invidious::Frontend::Comments
END_HTML END_HTML
elsif comments["authorId"]? && !comments["singlePost"]? elsif comments["authorId"]? && !comments["singlePost"]?
# for posts we should display a link to the post # for posts we should display a link to the post
replies_count_text = I18n.translate_count(locale, replies_count_text = translate_count(locale,
"comments_view_x_replies", "comments_view_x_replies",
child["replyCount"].as_i64 || 0, child["replyCount"].as_i64 || 0,
I18n::NumberFormatting::Separator NumberFormatting::Separator
) )
replies_html = <<-END_HTML replies_html = <<-END_HTML
@ -61,7 +61,7 @@ module Invidious::Frontend::Comments
sponsor_icon = String.build do |str| sponsor_icon = String.build do |str|
str << %(<img alt="" ) str << %(<img alt="" )
str << %(src="/ggpht) << URI.parse(child["sponsorIconUrl"].as_s).request_target << "\" " str << %(src="/ggpht) << URI.parse(child["sponsorIconUrl"].as_s).request_target << "\" "
str << %(title=") << I18n.translate(locale, "Channel Sponsor") << "\" " str << %(title=") << translate(locale, "Channel Sponsor") << "\" "
str << %(width="16" height="16" />) str << %(width="16" height="16" />)
end end
end end
@ -110,14 +110,14 @@ module Invidious::Frontend::Comments
when "multiImage" when "multiImage"
html << <<-END_HTML html << <<-END_HTML
<section class="carousel"> <section class="carousel">
<a class="skip-link" href="#skip-#{child["commentId"]}">#{I18n.translate(locale, "carousel_skip")}</a> <a class="skip-link" href="#skip-#{child["commentId"]}">#{translate(locale, "carousel_skip")}</a>
<div class="slides"> <div class="slides">
END_HTML END_HTML
image_array = attachment["images"].as_a image_array = attachment["images"].as_a
image_array.each_index do |i| image_array.each_index do |i|
html << <<-END_HTML html << <<-END_HTML
<div class="slides-item slide-#{i + 1}" id="#{child["commentId"]}-slide-#{i + 1}" aria-label="#{I18n.translate(locale, "carousel_slide", {"current" => (i + 1).to_s, "total" => image_array.size.to_s})}" tabindex="0"> <div class="slides-item slide-#{i + 1}" id="#{child["commentId"]}-slide-#{i + 1}" aria-label="#{translate(locale, "carousel_slide", {"current" => (i + 1).to_s, "total" => image_array.size.to_s})}" tabindex="0">
<img loading="lazy" src="/ggpht#{URI.parse(image_array[i][1]["url"].as_s).request_target}" alt="" /> <img loading="lazy" src="/ggpht#{URI.parse(image_array[i][1]["url"].as_s).request_target}" alt="" />
</div> </div>
END_HTML END_HTML
@ -129,7 +129,7 @@ module Invidious::Frontend::Comments
END_HTML END_HTML
attachment["images"].as_a.each_index do |i| attachment["images"].as_a.each_index do |i|
html << <<-END_HTML html << <<-END_HTML
<a class="slider-nav" href="##{child["commentId"]}-slide-#{i + 1}" aria-label="#{I18n.translate(locale, "carousel_go_to", (i + 1).to_s)}" tabindex="-1" aria-hidden="true">#{i + 1}</a> <a class="slider-nav" href="##{child["commentId"]}-slide-#{i + 1}" aria-label="#{translate(locale, "carousel_go_to", (i + 1).to_s)}" tabindex="-1" aria-hidden="true">#{i + 1}</a>
END_HTML END_HTML
end end
html << <<-END_HTML html << <<-END_HTML
@ -143,18 +143,18 @@ module Invidious::Frontend::Comments
html << <<-END_HTML html << <<-END_HTML
<p> <p>
<span title="#{Time.unix(child["published"].as_i64).to_s(I18n.translate(locale, "%A %B %-d, %Y"))}">#{I18n.translate(locale, "`x` ago", recode_date(Time.unix(child["published"].as_i64), locale))} #{child["isEdited"] == true ? I18n.translate(locale, "(edited)") : ""}</span> <span title="#{Time.unix(child["published"].as_i64).to_s(translate(locale, "%A %B %-d, %Y"))}">#{translate(locale, "`x` ago", recode_date(Time.unix(child["published"].as_i64), locale))} #{child["isEdited"] == true ? translate(locale, "(edited)") : ""}</span>
| |
END_HTML END_HTML
if comments["videoId"]? if comments["videoId"]?
html << <<-END_HTML html << <<-END_HTML
<a rel="noreferrer noopener" href="https://www.youtube.com/watch?v=#{comments["videoId"]}&lc=#{child["commentId"]}" title="#{I18n.translate(locale, "YouTube comment permalink")}">[YT]</a> <a rel="noreferrer noopener" href="https://www.youtube.com/watch?v=#{comments["videoId"]}&lc=#{child["commentId"]}" title="#{translate(locale, "YouTube comment permalink")}">[YT]</a>
| |
END_HTML END_HTML
elsif comments["authorId"]? elsif comments["authorId"]?
html << <<-END_HTML html << <<-END_HTML
<a rel="noreferrer noopener" href="https://www.youtube.com/channel/#{comments["authorId"]}/community?lb=#{child["commentId"]}" title="#{I18n.translate(locale, "YouTube comment permalink")}">[YT]</a> <a rel="noreferrer noopener" href="https://www.youtube.com/channel/#{comments["authorId"]}/community?lb=#{child["commentId"]}" title="#{translate(locale, "YouTube comment permalink")}">[YT]</a>
| |
END_HTML END_HTML
end end
@ -172,7 +172,7 @@ module Invidious::Frontend::Comments
html << <<-END_HTML html << <<-END_HTML
&nbsp; &nbsp;
<span class="creator-heart-container" title="#{I18n.translate(locale, "`x` marked it with a ❤", child["creatorHeart"]["creatorName"].as_s)}"> <span class="creator-heart-container" title="#{translate(locale, "`x` marked it with a ❤", child["creatorHeart"]["creatorName"].as_s)}">
<span class="creator-heart"> <span class="creator-heart">
<img loading="lazy" class="creator-heart-background-hearted" src="#{creator_thumbnail}" alt="" /> <img loading="lazy" class="creator-heart-background-hearted" src="#{creator_thumbnail}" alt="" />
<span class="creator-heart-small-hearted"> <span class="creator-heart-small-hearted">
@ -197,7 +197,7 @@ module Invidious::Frontend::Comments
<div class="pure-u-1"> <div class="pure-u-1">
<p> <p>
<a href="javascript:void(0)" data-continuation="#{comments["continuation"]}" <a href="javascript:void(0)" data-continuation="#{comments["continuation"]}"
data-onclick="get_youtube_replies" data-load-more #{"data-load-replies" if is_replies}>#{I18n.translate(locale, "Load more")}</a> data-onclick="get_youtube_replies" data-load-more #{"data-load-replies" if is_replies}>#{translate(locale, "Load more")}</a>
</p> </p>
</div> </div>
</div> </div>

View File

@ -6,16 +6,16 @@ module Invidious::Frontend::Pagination
private def first_page(str : String::Builder, locale : String?, url : String) private def first_page(str : String::Builder, locale : String?, url : String)
str << %(<a href=") << url << %(" class="pure-button pure-button-secondary">) str << %(<a href=") << url << %(" class="pure-button pure-button-secondary">)
if I18n.locale_is_rtl?(locale) if locale_is_rtl?(locale)
# Inverted arrow ("first" points to the right) # Inverted arrow ("first" points to the right)
str << I18n.translate(locale, "First page") str << translate(locale, "First page")
str << "&nbsp;&nbsp;" str << "&nbsp;&nbsp;"
str << %(<i class="icon ion-ios-arrow-forward"></i>) str << %(<i class="icon ion-ios-arrow-forward"></i>)
else else
# Regular arrow ("first" points to the left) # Regular arrow ("first" points to the left)
str << %(<i class="icon ion-ios-arrow-back"></i>) str << %(<i class="icon ion-ios-arrow-back"></i>)
str << "&nbsp;&nbsp;" str << "&nbsp;&nbsp;"
str << I18n.translate(locale, "First page") str << translate(locale, "First page")
end end
str << "</a>" str << "</a>"
@ -25,16 +25,16 @@ module Invidious::Frontend::Pagination
# Link # Link
str << %(<a href=") << url << %(" class="pure-button pure-button-secondary">) str << %(<a href=") << url << %(" class="pure-button pure-button-secondary">)
if I18n.locale_is_rtl?(locale) if locale_is_rtl?(locale)
# Inverted arrow ("previous" points to the right) # Inverted arrow ("previous" points to the right)
str << I18n.translate(locale, "Previous page") str << translate(locale, "Previous page")
str << "&nbsp;&nbsp;" str << "&nbsp;&nbsp;"
str << %(<i class="icon ion-ios-arrow-forward"></i>) str << %(<i class="icon ion-ios-arrow-forward"></i>)
else else
# Regular arrow ("previous" points to the left) # Regular arrow ("previous" points to the left)
str << %(<i class="icon ion-ios-arrow-back"></i>) str << %(<i class="icon ion-ios-arrow-back"></i>)
str << "&nbsp;&nbsp;" str << "&nbsp;&nbsp;"
str << I18n.translate(locale, "Previous page") str << translate(locale, "Previous page")
end end
str << "</a>" str << "</a>"
@ -44,14 +44,14 @@ module Invidious::Frontend::Pagination
# Link # Link
str << %(<a href=") << url << %(" class="pure-button pure-button-secondary">) str << %(<a href=") << url << %(" class="pure-button pure-button-secondary">)
if I18n.locale_is_rtl?(locale) if locale_is_rtl?(locale)
# Inverted arrow ("next" points to the left) # Inverted arrow ("next" points to the left)
str << %(<i class="icon ion-ios-arrow-back"></i>) str << %(<i class="icon ion-ios-arrow-back"></i>)
str << "&nbsp;&nbsp;" str << "&nbsp;&nbsp;"
str << I18n.translate(locale, "Next page") str << translate(locale, "Next page")
else else
# Regular arrow ("next" points to the right) # Regular arrow ("next" points to the right)
str << I18n.translate(locale, "Next page") str << translate(locale, "Next page")
str << "&nbsp;&nbsp;" str << "&nbsp;&nbsp;"
str << %(<i class="icon ion-ios-arrow-forward"></i>) str << %(<i class="icon ion-ios-arrow-forward"></i>)
end end

View File

@ -6,7 +6,7 @@ module Invidious::Frontend::SearchFilters
return String.build(8000) do |str| return String.build(8000) do |str|
str << "<div id='filters'>\n" str << "<div id='filters'>\n"
str << "\t<details id='filters-collapse'>" str << "\t<details id='filters-collapse'>"
str << "\t\t<summary>" << I18n.translate(locale, "search_filters_title") << "</summary>\n" str << "\t\t<summary>" << translate(locale, "search_filters_title") << "</summary>\n"
str << "\t\t<div id='filters-box'><form action='/search' method='get'>\n" str << "\t\t<div id='filters-box'><form action='/search' method='get'>\n"
@ -25,7 +25,7 @@ module Invidious::Frontend::SearchFilters
str << "\t\t\t<div id='filters-apply'>" str << "\t\t\t<div id='filters-apply'>"
str << "<button type='submit' class=\"pure-button pure-button-primary\">" str << "<button type='submit' class=\"pure-button pure-button-primary\">"
str << I18n.translate(locale, "search_filters_apply_button") str << translate(locale, "search_filters_apply_button")
str << "</button></div>\n" str << "</button></div>\n"
str << "\t\t</form></div>\n" str << "\t\t</form></div>\n"
@ -41,7 +41,7 @@ module Invidious::Frontend::SearchFilters
str << "\t\t\t\t<div class=\"filter-column\"><fieldset>\n" str << "\t\t\t\t<div class=\"filter-column\"><fieldset>\n"
str << "\t\t\t\t\t<legend><div class=\"filter-name underlined\">" str << "\t\t\t\t\t<legend><div class=\"filter-name underlined\">"
str << I18n.translate(locale, "search_filters_{{name}}_label") str << translate(locale, "search_filters_{{name}}_label")
str << "</div></legend>\n" str << "</div></legend>\n"
str << "\t\t\t\t\t<div class=\"filter-options\">\n" str << "\t\t\t\t\t<div class=\"filter-options\">\n"
@ -62,7 +62,7 @@ module Invidious::Frontend::SearchFilters
str << '>' str << '>'
str << "<label for='filter-date-{{date}}'>" str << "<label for='filter-date-{{date}}'>"
str << I18n.translate(locale, "search_filters_date_option_{{date}}") str << translate(locale, "search_filters_date_option_{{date}}")
str << "</label></div>\n" str << "</label></div>\n"
{% end %} {% end %}
end end
@ -78,7 +78,7 @@ module Invidious::Frontend::SearchFilters
str << '>' str << '>'
str << "<label for='filter-type-{{type}}'>" str << "<label for='filter-type-{{type}}'>"
str << I18n.translate(locale, "search_filters_type_option_{{type}}") str << translate(locale, "search_filters_type_option_{{type}}")
str << "</label></div>\n" str << "</label></div>\n"
{% end %} {% end %}
end end
@ -94,7 +94,7 @@ module Invidious::Frontend::SearchFilters
str << '>' str << '>'
str << "<label for='filter-duration-{{duration}}'>" str << "<label for='filter-duration-{{duration}}'>"
str << I18n.translate(locale, "search_filters_duration_option_{{duration}}") str << translate(locale, "search_filters_duration_option_{{duration}}")
str << "</label></div>\n" str << "</label></div>\n"
{% end %} {% end %}
end end
@ -111,7 +111,7 @@ module Invidious::Frontend::SearchFilters
str << '>' str << '>'
str << "<label for='filter-feature-{{feature}}'>" str << "<label for='filter-feature-{{feature}}'>"
str << I18n.translate(locale, "search_filters_features_option_{{feature}}") str << translate(locale, "search_filters_features_option_{{feature}}")
str << "</label></div>\n" str << "</label></div>\n"
{% end %} {% end %}
{% end %} {% end %}
@ -128,7 +128,7 @@ module Invidious::Frontend::SearchFilters
str << '>' str << '>'
str << "<label for='filter-sort-{{sort}}'>" str << "<label for='filter-sort-{{sort}}'>"
str << I18n.translate(locale, "search_filters_sort_option_{{sort}}") str << translate(locale, "search_filters_sort_option_{{sort}}")
str << "</label></div>\n" str << "</label></div>\n"
{% end %} {% end %}
end end

View File

@ -20,7 +20,7 @@ module Invidious::Frontend::WatchPage
def download_widget(locale : String, video : Video, video_assets : VideoAssets) : String def download_widget(locale : String, video : Video, video_assets : VideoAssets) : String
if CONFIG.disabled?("downloads") if CONFIG.disabled?("downloads")
return "<p id=\"download\">#{I18n.translate(locale, "Download is disabled")}</p>" return "<p id=\"download\">#{translate(locale, "Download is disabled")}</p>"
end end
url = "/download" url = "/download"
@ -45,7 +45,7 @@ module Invidious::Frontend::WatchPage
str << "\t<div class=\"pure-control-group\">\n" str << "\t<div class=\"pure-control-group\">\n"
str << "\t\t<label for='download_widget'>" str << "\t\t<label for='download_widget'>"
str << I18n.translate(locale, "Download as: ") str << translate(locale, "Download as: ")
str << "</label>\n" str << "</label>\n"
str << "\t\t<select name='download_widget' id='download_widget'>\n" str << "\t\t<select name='download_widget' id='download_widget'>\n"
@ -94,7 +94,7 @@ module Invidious::Frontend::WatchPage
value = {"label": caption.name, "ext": "#{caption.language_code}.vtt"}.to_json value = {"label": caption.name, "ext": "#{caption.language_code}.vtt"}.to_json
str << "\t\t\t<option value='" << value << "'>" str << "\t\t\t<option value='" << value << "'>"
str << I18n.translate(locale, "download_subtitles", I18n.translate(locale, caption.name)) str << translate(locale, "download_subtitles", translate(locale, caption.name))
str << "</option>\n" str << "</option>\n"
end end
@ -104,7 +104,7 @@ module Invidious::Frontend::WatchPage
str << "\t</div>\n" str << "\t</div>\n"
str << "\t<button type=\"submit\" class=\"pure-button pure-button-primary\">\n" str << "\t<button type=\"submit\" class=\"pure-button pure-button-primary\">\n"
str << "\t\t<b>" << I18n.translate(locale, "Download") << "</b>\n" str << "\t\t<b>" << translate(locale, "Download") << "</b>\n"
str << "\t</button>\n" str << "\t</button>\n"
str << "</form>\n" str << "</form>\n"

View File

@ -8,7 +8,7 @@ module Invidious::Hashtag
client_config = YoutubeAPI::ClientConfig.new(region: region) client_config = YoutubeAPI::ClientConfig.new(region: region)
response = YoutubeAPI.browse(continuation: ctoken, client_config: client_config) response = YoutubeAPI.browse(continuation: ctoken, client_config: client_config)
items, _ = YoutubeJSONParser.extract_items(response) items, _ = extract_items(response)
return items return items
end end

View File

@ -63,19 +63,19 @@ def error_template_helper(env : HTTP::Server::Context, status_code : Int32, exce
error_message = <<-END_HTML error_message = <<-END_HTML
<div class="error_message"> <div class="error_message">
<h2>#{I18n.translate(locale, "crash_page_you_found_a_bug")}</h2> <h2>#{translate(locale, "crash_page_you_found_a_bug")}</h2>
<br/><br/> <br/><br/>
<p><b>#{I18n.translate(locale, "crash_page_before_reporting")}</b></p> <p><b>#{translate(locale, "crash_page_before_reporting")}</b></p>
<ul> <ul>
<li>#{I18n.translate(locale, "crash_page_refresh", env.request.resource)}</li> <li>#{translate(locale, "crash_page_refresh", env.request.resource)}</li>
<li>#{I18n.translate(locale, "crash_page_switch_instance", url_switch)}</li> <li>#{translate(locale, "crash_page_switch_instance", url_switch)}</li>
<li>#{I18n.translate(locale, "crash_page_read_the_faq", url_faq)}</li> <li>#{translate(locale, "crash_page_read_the_faq", url_faq)}</li>
<li>#{I18n.translate(locale, "crash_page_search_issue", url_search_issues)}</li> <li>#{translate(locale, "crash_page_search_issue", url_search_issues)}</li>
</ul> </ul>
<br/> <br/>
<p>#{I18n.translate(locale, "crash_page_report_issue", url_new_issue)}</p> <p>#{translate(locale, "crash_page_report_issue", url_new_issue)}</p>
<!-- TODO: Add a "copy to clipboard" button --> <!-- TODO: Add a "copy to clipboard" button -->
<pre class="error-issue-template">#{issue_template}</pre> <pre class="error-issue-template">#{issue_template}</pre>
@ -95,7 +95,7 @@ def error_template_helper(env : HTTP::Server::Context, status_code : Int32, mess
locale = env.get("preferences").as(Preferences).locale locale = env.get("preferences").as(Preferences).locale
error_message = I18n.translate(locale, message) error_message = translate(locale, message)
next_steps = error_redirect_helper(env) next_steps = error_redirect_helper(env)
return templated "error" return templated "error"
@ -186,10 +186,10 @@ def error_redirect_helper(env : HTTP::Server::Context)
if request_path.starts_with?("/search") || request_path.starts_with?("/watch") || if request_path.starts_with?("/search") || request_path.starts_with?("/watch") ||
request_path.starts_with?("/channel") || request_path.starts_with?("/playlist?list=PL") request_path.starts_with?("/channel") || request_path.starts_with?("/playlist?list=PL")
next_steps_text = I18n.translate(locale, "next_steps_error_message") next_steps_text = translate(locale, "next_steps_error_message")
refresh = I18n.translate(locale, "next_steps_error_message_refresh") refresh = translate(locale, "next_steps_error_message_refresh")
go_to_youtube = I18n.translate(locale, "next_steps_error_message_go_to_youtube") go_to_youtube = translate(locale, "next_steps_error_message_go_to_youtube")
switch_instance = I18n.translate(locale, "Switch Invidious Instance") switch_instance = translate(locale, "Switch Invidious Instance")
return <<-END_HTML return <<-END_HTML
<p style="margin-bottom: 4px;">#{next_steps_text}</p> <p style="margin-bottom: 4px;">#{next_steps_text}</p>

View File

@ -1,202 +1,199 @@
module I18n # Languages requiring a better level of translation (at least 20%)
extend self # to be added to the list below:
# Languages requiring a better level of translation (at least 20%) #
# to be added to the list below: # "af" => "", # Afrikaans
# # "az" => "", # Azerbaijani
# "af" => "", # Afrikaans # "be" => "", # Belarusian
# "az" => "", # Azerbaijani # "bn_BD" => "", # Bengali (Bangladesh)
# "be" => "", # Belarusian # "ia" => "", # Interlingua
# "bn_BD" => "", # Bengali (Bangladesh) # "or" => "", # Odia
# "ia" => "", # Interlingua # "tk" => "", # Turkmen
# "or" => "", # Odia # "tok => "", # Toki Pona
# "tk" => "", # Turkmen #
# "tok => "", # Toki Pona LOCALES_LIST = {
# "ar" => "العربية", # Arabic
LOCALES_LIST = { "bg" => "български", # Bulgarian
"ar" => "العربية", # Arabic "bn" => "বাংলা", # Bengali
"bg" => "български", # Bulgarian "ca" => "Català", # Catalan
"bn" => "বাংলা", # Bengali "cs" => "Čeština", # Czech
"ca" => "Català", # Catalan "cy" => "Cymraeg", # Welsh
"cs" => "Čeština", # Czech "da" => "Dansk", # Danish
"cy" => "Cymraeg", # Welsh "de" => "Deutsch", # German
"da" => "Dansk", # Danish "el" => "Ελληνικά", # Greek
"de" => "Deutsch", # German "en-US" => "English", # English
"el" => "Ελληνικά", # Greek "eo" => "Esperanto", # Esperanto
"en-US" => "English", # English "es" => "Español", # Spanish
"eo" => "Esperanto", # Esperanto "et" => "Eesti keel", # Estonian
"es" => "Español", # Spanish "eu" => "Euskara", # Basque
"et" => "Eesti keel", # Estonian "fa" => "فارسی", # Persian
"eu" => "Euskara", # Basque "fi" => "Suomi", # Finnish
"fa" => "فارسی", # Persian "fr" => "Français", # French
"fi" => "Suomi", # Finnish "he" => "עברית", # Hebrew
"fr" => "Français", # French "hi" => "हिन्दी", # Hindi
"he" => "עברית", # Hebrew "hr" => "Hrvatski", # Croatian
"hi" => "हिन्दी", # Hindi "hu-HU" => "Magyar Nyelv", # Hungarian
"hr" => "Hrvatski", # Croatian "id" => "Bahasa Indonesia", # Indonesian
"hu-HU" => "Magyar Nyelv", # Hungarian "is" => "Íslenska", # Icelandic
"id" => "Bahasa Indonesia", # Indonesian "it" => "Italiano", # Italian
"is" => "Íslenska", # Icelandic "ja" => "日本語", # Japanese
"it" => "Italiano", # Italian "ko" => "한국어", # Korean
"ja" => "日本語", # Japanese "lmo" => "Lombard", # Lombard
"ko" => "한국어", # Korean "lt" => "Lietuvių", # Lithuanian
"lmo" => "Lombard", # Lombard "nb-NO" => "Norsk bokmål", # Norwegian Bokmål
"lt" => "Lietuvių", # Lithuanian "nl" => "Nederlands", # Dutch
"nb-NO" => "Norsk bokmål", # Norwegian Bokmål "pl" => "Polski", # Polish
"nl" => "Nederlands", # Dutch "pt" => "Português", # Portuguese
"pl" => "Polski", # Polish "pt-BR" => "Português Brasileiro", # Portuguese (Brazil)
"pt" => "Português", # Portuguese "pt-PT" => "Português de Portugal", # Portuguese (Portugal)
"pt-BR" => "Português Brasileiro", # Portuguese (Brazil) "ro" => "Română", # Romanian
"pt-PT" => "Português de Portugal", # Portuguese (Portugal) "ru" => "Русский", # Russian
"ro" => "Română", # Romanian "si" => "සිංහල", # Sinhala
"ru" => "Русский", # Russian "sk" => "Slovenčina", # Slovak
"si" => "සිංහල", # Sinhala "sl" => "Slovenščina", # Slovenian
"sk" => "Slovenčina", # Slovak "sq" => "Shqip", # Albanian
"sl" => "Slovenščina", # Slovenian "sr" => "Srpski (latinica)", # Serbian (Latin)
"sq" => "Shqip", # Albanian "sr_Cyrl" => "Српски (ћирилица)", # Serbian (Cyrillic)
"sr" => "Srpski (latinica)", # Serbian (Latin) "sv-SE" => "Svenska", # Swedish
"sr_Cyrl" => "Српски (ћирилица)", # Serbian (Cyrillic) "ta" => "தமிழ்", # Tamil
"sv-SE" => "Svenska", # Swedish "tr" => "Türkçe", # Turkish
"ta" => "தமிழ்", # Tamil "uk" => "Українська", # Ukrainian
"tr" => "Türkçe", # Turkish "vi" => "Tiếng Việt", # Vietnamese
"uk" => "Українська", # Ukrainian "zh-CN" => "汉语", # Chinese (Simplified)
"vi" => "Tiếng Việt", # Vietnamese "zh-TW" => "漢語", # Chinese (Traditional)
"zh-CN" => "汉语", # Chinese (Simplified) }
"zh-TW" => "漢語", # Chinese (Traditional)
}
LOCALES = load_all_locales() LOCALES = load_all_locales()
CONTENT_REGIONS = { CONTENT_REGIONS = {
"AE", "AR", "AT", "AU", "AZ", "BA", "BD", "BE", "BG", "BH", "BO", "BR", "BY", "AE", "AR", "AT", "AU", "AZ", "BA", "BD", "BE", "BG", "BH", "BO", "BR", "BY",
"CA", "CH", "CL", "CO", "CR", "CY", "CZ", "DE", "DK", "DO", "DZ", "EC", "EE", "CA", "CH", "CL", "CO", "CR", "CY", "CZ", "DE", "DK", "DO", "DZ", "EC", "EE",
"EG", "ES", "FI", "FR", "GB", "GE", "GH", "GR", "GT", "HK", "HN", "HR", "HU", "EG", "ES", "FI", "FR", "GB", "GE", "GH", "GR", "GT", "HK", "HN", "HR", "HU",
"ID", "IE", "IL", "IN", "IQ", "IS", "IT", "JM", "JO", "JP", "KE", "KR", "KW", "ID", "IE", "IL", "IN", "IQ", "IS", "IT", "JM", "JO", "JP", "KE", "KR", "KW",
"KZ", "LB", "LI", "LK", "LT", "LU", "LV", "LY", "MA", "ME", "MK", "MT", "MX", "KZ", "LB", "LI", "LK", "LT", "LU", "LV", "LY", "MA", "ME", "MK", "MT", "MX",
"MY", "NG", "NI", "NL", "NO", "NP", "NZ", "OM", "PA", "PE", "PG", "PH", "PK", "MY", "NG", "NI", "NL", "NO", "NP", "NZ", "OM", "PA", "PE", "PG", "PH", "PK",
"PL", "PR", "PT", "PY", "QA", "RO", "RS", "RU", "SA", "SE", "SG", "SI", "SK", "PL", "PR", "PT", "PY", "QA", "RO", "RS", "RU", "SA", "SE", "SG", "SI", "SK",
"SN", "SV", "TH", "TN", "TR", "TW", "TZ", "UA", "UG", "US", "UY", "VE", "VN", "SN", "SV", "TH", "TN", "TR", "TW", "TZ", "UA", "UG", "US", "UY", "VE", "VN",
"YE", "ZA", "ZW", "YE", "ZA", "ZW",
} }
# Enum for the different types of number formats # Enum for the different types of number formats
enum NumberFormatting enum NumberFormatting
None # Print the number as-is None # Print the number as-is
Separator # Use a separator for thousands Separator # Use a separator for thousands
Short # Use short notation (k/M/B) Short # Use short notation (k/M/B)
HtmlSpan # Surround with <span id="count"></span> HtmlSpan # Surround with <span id="count"></span>
end
def load_all_locales
locales = {} of String => Hash(String, JSON::Any)
LOCALES_LIST.each_key do |name|
locales[name] = JSON.parse(File.read("locales/#{name}.json")).as_h
end end
def load_all_locales return locales
locales = {} of String => Hash(String, JSON::Any) end
LOCALES_LIST.each_key do |name| def translate(locale : String?, key : String, text : String | Hash(String, String) | Nil = nil) : String
locales[name] = JSON.parse(File.read("locales/#{name}.json")).as_h # Log a warning if "key" doesn't exist in en-US locale and return
end # that key as the text, so this is more or less transparent to the user.
if !LOCALES["en-US"].has_key?(key)
return locales Log.warn { "Missing translation key \"#{key}\"" }
return key
end end
def translate(locale : String?, key : String, text : String | Hash(String, String) | Nil = nil) : String # Default to english, whenever the locale doesn't exist,
# Log a warning if "key" doesn't exist in en-US locale and return # or the key requested has not been translated
# that key as the text, so this is more or less transparent to the user. if locale && LOCALES.has_key?(locale) && LOCALES[locale].has_key?(key)
if !LOCALES["en-US"].has_key?(key) raw_data = LOCALES[locale][key]
Log.warn { "Missing translation key \"#{key}\"" } else
return key raw_data = LOCALES["en-US"][key]
end end
# Default to english, whenever the locale doesn't exist, case raw_data
# or the key requested has not been translated when .as_h?
if locale && LOCALES.has_key?(locale) && LOCALES[locale].has_key?(key) # Init
raw_data = LOCALES[locale][key] translation = ""
else match_length = 0
raw_data = LOCALES["en-US"][key]
end
case raw_data raw_data.as_h.each do |hash_key, value|
when .as_h? if text.is_a?(String)
# Init if md = text.try &.match(/#{hash_key}/)
translation = "" if md[0].size >= match_length
match_length = 0 translation = value.as_s
match_length = md[0].size
raw_data.as_h.each do |hash_key, value|
if text.is_a?(String)
if md = text.try &.match(/#{hash_key}/)
if md[0].size >= match_length
translation = value.as_s
match_length = md[0].size
end
end end
end end
end end
when .as_s? end
translation = raw_data.as_s when .as_s?
translation = raw_data.as_s
else
raise "Invalid translation \"#{raw_data}\""
end
if text.is_a?(String)
translation = translation.gsub("`x`", text)
elsif text.is_a?(Hash(String, String))
# adds support for multi string interpolation. Based on i18next https://www.i18next.com/translation-function/interpolation#basic
text.each_key do |hash_key|
translation = translation.gsub("{{#{hash_key}}}", text[hash_key])
end
end
return translation
end
def translate_count(locale : String, key : String, count : Int, format = NumberFormatting::None) : String
# Fallback on english if locale doesn't exist
locale = "en-US" if !LOCALES.has_key?(locale)
# Retrieve suffix
suffix = I18next::Plurals::RESOLVER.get_suffix(locale, count)
plural_key = key + suffix
if LOCALES[locale].has_key?(plural_key)
translation = LOCALES[locale][plural_key].as_s
else
# Try #1: Fallback to singular in the same locale
singular_suffix = I18next::Plurals::RESOLVER.get_suffix(locale, 1)
if LOCALES[locale].has_key?(key + singular_suffix)
translation = LOCALES[locale][key + singular_suffix].as_s
elsif locale != "en-US"
# Try #2: Fallback to english
translation = translate_count("en-US", key, count)
else else
raise "Invalid translation \"#{raw_data}\"" # Return key if we're already in english, as the translation is missing
end Log.warn { "Missing translation key \"#{key}\"" }
return key
if text.is_a?(String)
translation = translation.gsub("`x`", text)
elsif text.is_a?(Hash(String, String))
# adds support for multi string interpolation. Based on i18next https://www.i18next.com/translation-function/interpolation#basic
text.each_key do |hash_key|
translation = translation.gsub("{{#{hash_key}}}", text[hash_key])
end
end
return translation
end
def translate_count(locale : String, key : String, count : Int, format = NumberFormatting::None) : String
# Fallback on english if locale doesn't exist
locale = "en-US" if !LOCALES.has_key?(locale)
# Retrieve suffix
suffix = I18next::Plurals::RESOLVER.get_suffix(locale, count)
plural_key = key + suffix
if LOCALES[locale].has_key?(plural_key)
translation = LOCALES[locale][plural_key].as_s
else
# Try #1: Fallback to singular in the same locale
singular_suffix = I18next::Plurals::RESOLVER.get_suffix(locale, 1)
if LOCALES[locale].has_key?(key + singular_suffix)
translation = LOCALES[locale][key + singular_suffix].as_s
elsif locale != "en-US"
# Try #2: Fallback to english
translation = translate_count("en-US", key, count)
else
# Return key if we're already in english, as the translation is missing
Log.warn { "Missing translation key \"#{key}\"" }
return key
end
end
case format
when .separator? then count_txt = number_with_separator(count)
when .short? then count_txt = number_to_short_text(count)
when .html_span? then count_txt = "<span id=\"count\">" + count.to_s + "</span>"
else count_txt = count.to_s
end
return translation.gsub("{{count}}", count_txt)
end
def translate_bool(locale : String?, translation : Bool)
case translation
when true
return translate(locale, "Yes")
when false
return translate(locale, "No")
end end
end end
def locale_is_rtl?(locale : String?) case format
# Fallback to en-US when .separator? then count_txt = number_with_separator(count)
return false if locale.nil? when .short? then count_txt = number_to_short_text(count)
when .html_span? then count_txt = "<span id=\"count\">" + count.to_s + "</span>"
else count_txt = count.to_s
end
# Arabic, Persian, Hebrew return translation.gsub("{{count}}", count_txt)
# See https://en.wikipedia.org/wiki/Right-to-left_script#List_of_RTL_scripts end
return {"ar", "fa", "he"}.includes? locale
def translate_bool(locale : String?, translation : Bool)
case translation
when true
return translate(locale, "Yes")
when false
return translate(locale, "No")
end end
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

View File

@ -70,9 +70,3 @@ macro haltf(env, status_code = 200, response = "")
{{env}}.response.close {{env}}.response.close
return return
end end
class Log
macro forf
Log.for({{@def.name.stringify}})
end
end

View File

@ -115,9 +115,9 @@ struct SearchVideo
json.field "descriptionHtml", self.description_html json.field "descriptionHtml", self.description_html
json.field "viewCount", self.views json.field "viewCount", self.views
json.field "viewCountText", I18n.translate_count(locale, "generic_views_count", self.views, I18n::NumberFormatting::Short) json.field "viewCountText", translate_count(locale, "generic_views_count", self.views, NumberFormatting::Short)
json.field "published", self.published.to_unix json.field "published", self.published.to_unix
json.field "publishedText", I18n.translate(locale, "`x` ago", recode_date(self.published, locale)) json.field "publishedText", translate(locale, "`x` ago", recode_date(self.published, locale))
json.field "lengthSeconds", self.length_seconds json.field "lengthSeconds", self.length_seconds
json.field "liveNow", self.badges.live_now? json.field "liveNow", self.badges.live_now?
json.field "premium", self.badges.premium? json.field "premium", self.badges.premium?
@ -327,8 +327,8 @@ struct ProblematicTimelineItem
xml.element("content", type: "xhtml") do xml.element("content", type: "xhtml") do
xml.element("div", xmlns: "http://www.w3.org/1999/xhtml") do xml.element("div", xmlns: "http://www.w3.org/1999/xhtml") do
xml.element("div") do xml.element("div") do
xml.element("h4") { I18n.translate(locale, "timeline_parse_error_placeholder_heading") } xml.element("h4") { translate(locale, "timeline_parse_error_placeholder_heading") }
xml.element("p") { I18n.translate(locale, "timeline_parse_error_placeholder_message") } xml.element("p") { translate(locale, "timeline_parse_error_placeholder_message") }
end end
xml.element("pre") do xml.element("pre") do

View File

@ -144,19 +144,19 @@ def recode_date(time : Time, locale)
span = Time.utc - time span = Time.utc - time
if span.total_days > 365.0 if span.total_days > 365.0
return I18n.translate_count(locale, "generic_count_years", span.total_days.to_i // 365) return translate_count(locale, "generic_count_years", span.total_days.to_i // 365)
elsif span.total_days > 30.0 elsif span.total_days > 30.0
return I18n.translate_count(locale, "generic_count_months", span.total_days.to_i // 30) return translate_count(locale, "generic_count_months", span.total_days.to_i // 30)
elsif span.total_days > 7.0 elsif span.total_days > 7.0
return I18n.translate_count(locale, "generic_count_weeks", span.total_days.to_i // 7) return translate_count(locale, "generic_count_weeks", span.total_days.to_i // 7)
elsif span.total_hours > 24.0 elsif span.total_hours > 24.0
return I18n.translate_count(locale, "generic_count_days", span.total_days.to_i) return translate_count(locale, "generic_count_days", span.total_days.to_i)
elsif span.total_minutes > 60.0 elsif span.total_minutes > 60.0
return I18n.translate_count(locale, "generic_count_hours", span.total_hours.to_i) return translate_count(locale, "generic_count_hours", span.total_hours.to_i)
elsif span.total_seconds > 60.0 elsif span.total_seconds > 60.0
return I18n.translate_count(locale, "generic_count_minutes", span.total_minutes.to_i) return translate_count(locale, "generic_count_minutes", span.total_minutes.to_i)
else else
return I18n.translate_count(locale, "generic_count_seconds", span.total_seconds.to_i) return translate_count(locale, "generic_count_seconds", span.total_seconds.to_i)
end end
end end

View File

@ -22,7 +22,7 @@ module Invidious::JSONify::APIv1
json.field "description", video.description json.field "description", video.description
json.field "descriptionHtml", video.description_html json.field "descriptionHtml", video.description_html
json.field "published", video.published.to_unix json.field "published", video.published.to_unix
json.field "publishedText", I18n.translate(locale, "`x` ago", recode_date(video.published, locale)) json.field "publishedText", translate(locale, "`x` ago", recode_date(video.published, locale))
json.field "keywords", video.keywords json.field "keywords", video.keywords
json.field "viewCount", video.views json.field "viewCount", video.views
@ -269,7 +269,7 @@ module Invidious::JSONify::APIv1
json.field "viewCount", rv["view_count"]?.try &.empty? ? nil : rv["view_count"].to_i64 json.field "viewCount", rv["view_count"]?.try &.empty? ? nil : rv["view_count"].to_i64
json.field "published", rv["published"]? json.field "published", rv["published"]?
if rv["published"]?.try &.presence if rv["published"]?.try &.presence
json.field "publishedText", I18n.translate(locale, "`x` ago", recode_date(Time.parse_rfc3339(rv["published"].to_s), locale)) json.field "publishedText", translate(locale, "`x` ago", recode_date(Time.parse_rfc3339(rv["published"].to_s), locale))
else else
json.field "publishedText", "" json.field "publishedText", ""
end end

View File

@ -7,7 +7,7 @@ module Invidious::Routes::BeforeAll
preferences = Preferences.from_json(URI.decode_www_form(prefs_cookie.value)) preferences = Preferences.from_json(URI.decode_www_form(prefs_cookie.value))
else else
if language_header = env.request.headers["Accept-Language"]? if language_header = env.request.headers["Accept-Language"]?
if language = ANG.language_negotiator.best(language_header, I18n::LOCALES.keys) if language = ANG.language_negotiator.best(language_header, LOCALES.keys)
preferences.locale = language.header preferences.locale = language.header
end end
end end

View File

@ -352,7 +352,7 @@ module Invidious::Routes::Channels
resolved_url = YoutubeAPI.resolve_url("https://youtube.com#{env.request.path}#{yt_url_params.size > 0 ? "?#{yt_url_params}" : ""}") resolved_url = YoutubeAPI.resolve_url("https://youtube.com#{env.request.path}#{yt_url_params.size > 0 ? "?#{yt_url_params}" : ""}")
ucid = resolved_url["endpoint"]["browseEndpoint"]["browseId"] ucid = resolved_url["endpoint"]["browseEndpoint"]["browseId"]
rescue ex : InfoException | KeyError rescue ex : InfoException | KeyError
return error_template(404, I18n.translate(locale, "This channel does not exist.")) return error_template(404, translate(locale, "This channel does not exist."))
end end
selected_tab = env.params.url["tab"]? selected_tab = env.params.url["tab"]?

View File

@ -10,7 +10,7 @@ module Invidious::Routes::Embed
videos = get_playlist_videos(playlist, offset: offset) videos = get_playlist_videos(playlist, offset: offset)
if videos.empty? if videos.empty?
url = "/playlist?list=#{plid}" url = "/playlist?list=#{plid}"
raise NotFoundException.new(I18n.translate(locale, "error_video_not_in_playlist", url)) raise NotFoundException.new(translate(locale, "error_video_not_in_playlist", url))
end end
first_playlist_video = videos[0].as(PlaylistVideo) first_playlist_video = videos[0].as(PlaylistVideo)
@ -72,7 +72,7 @@ module Invidious::Routes::Embed
videos = get_playlist_videos(playlist, offset: offset) videos = get_playlist_videos(playlist, offset: offset)
if videos.empty? if videos.empty?
url = "/playlist?list=#{plid}" url = "/playlist?list=#{plid}"
raise NotFoundException.new(I18n.translate(locale, "error_video_not_in_playlist", url)) raise NotFoundException.new(translate(locale, "error_video_not_in_playlist", url))
end end
first_playlist_video = videos[0].as(PlaylistVideo) first_playlist_video = videos[0].as(PlaylistVideo)

View File

@ -37,7 +37,7 @@ module Invidious::Routes::Feeds
if CONFIG.popular_enabled if CONFIG.popular_enabled
templated "feeds/popular" templated "feeds/popular"
else else
message = I18n.translate(locale, "The Popular feed has been disabled by the administrator.") message = translate(locale, "The Popular feed has been disabled by the administrator.")
templated "message" templated "message"
end end
end end
@ -258,7 +258,7 @@ module Invidious::Routes::Feeds
xml.element("link", "type": "text/html", rel: "alternate", href: "#{HOST_URL}/feed/subscriptions") xml.element("link", "type": "text/html", rel: "alternate", href: "#{HOST_URL}/feed/subscriptions")
xml.element("link", "type": "application/atom+xml", rel: "self", xml.element("link", "type": "application/atom+xml", rel: "self",
href: "#{HOST_URL}#{env.request.resource}") href: "#{HOST_URL}#{env.request.resource}")
xml.element("title") { xml.text I18n.translate(locale, "Invidious Private Feed for `x`", user.email) } xml.element("title") { xml.text translate(locale, "Invidious Private Feed for `x`", user.email) }
(notifications + videos).each do |video| (notifications + videos).each do |video|
video.to_xml(locale, params, xml) video.to_xml(locale, params, xml)

View File

@ -110,7 +110,7 @@ module Invidious::Routes::Login
user, sid = create_user(sid, email, password) user, sid = create_user(sid, email, password)
if language_header = env.request.headers["Accept-Language"]? if language_header = env.request.headers["Accept-Language"]?
if language = ANG.language_negotiator.best(language_header, I18n::LOCALES.keys) if language = ANG.language_negotiator.best(language_header, LOCALES.keys)
user.preferences.locale = language.header user.preferences.locale = language.header
end end
end end

View File

@ -9,7 +9,7 @@ module Invidious::Search
client_config = YoutubeAPI::ClientConfig.new(region: query.region) client_config = YoutubeAPI::ClientConfig.new(region: query.region)
initial_data = YoutubeAPI.search(query.text, search_params, client_config: client_config) initial_data = YoutubeAPI.search(query.text, search_params, client_config: client_config)
items, _ = YoutubeJSONParser.extract_items(initial_data) items, _ = extract_items(initial_data)
return items.reject!(Category) return items.reject!(Category)
end end
@ -31,7 +31,7 @@ module Invidious::Search
continuation = produce_channel_search_continuation(ucid, query.text, query.page) continuation = produce_channel_search_continuation(ucid, query.text, query.page)
response_json = YoutubeAPI.browse(continuation) response_json = YoutubeAPI.browse(continuation)
items, _ = YoutubeJSONParser.extract_items(response_json, "", ucid) items, _ = extract_items(response_json, "", ucid)
return items.reject!(Category) return items.reject!(Category)
end end

View File

@ -18,7 +18,7 @@ def fetch_trending(trending_type, region, locale)
client_config = YoutubeAPI::ClientConfig.new(region: region) client_config = YoutubeAPI::ClientConfig.new(region: region)
initial_data = YoutubeAPI.browse("FEtrending", params: params, client_config: client_config) initial_data = YoutubeAPI.browse("FEtrending", params: params, client_config: client_config)
items, _ = YoutubeJSONParser.extract_items(initial_data) items, _ = extract_items(initial_data)
extracted = [] of SearchItem extracted = [] of SearchItem

View File

@ -324,7 +324,7 @@ rescue DB::Error
end end
def fetch_video(id, region) def fetch_video(id, region)
info = Parser.extract_video_info(video_id: id) info = extract_video_info(video_id: id)
if reason = info["reason"]? if reason = info["reason"]?
if reason == "Video unavailable" if reason == "Video unavailable"

View File

@ -6,507 +6,504 @@ require "json"
# #
# TODO: "compactRadioRenderer" (Mix) and # TODO: "compactRadioRenderer" (Mix) and
# TODO: Use a proper struct/class instead of a hacky JSON object # TODO: Use a proper struct/class instead of a hacky JSON object
module Parser private def parse_related_video(related : JSON::Any) : Hash(String, JSON::Any)?
extend self return nil if !related["videoId"]?
Log = ::Log.for(self)
private def parse_related_video(related : JSON::Any) : Hash(String, JSON::Any)? # The compact renderer has video length in seconds, where the end
return nil if !related["videoId"]? # screen rendered has a full text version ("42:40")
length = related["lengthInSeconds"]?.try &.as_i.to_s
# The compact renderer has video length in seconds, where the end length ||= related.dig?("lengthText", "simpleText").try do |box|
# screen rendered has a full text version ("42:40") decode_length_seconds(box.as_s).to_s
length = related["lengthInSeconds"]?.try &.as_i.to_s
length ||= related.dig?("lengthText", "simpleText").try do |box|
decode_length_seconds(box.as_s).to_s
end
# Both have "short", so the "long" option shouldn't be required
channel_info = (related["shortBylineText"]? || related["longBylineText"]?)
.try &.dig?("runs", 0)
author = channel_info.try &.dig?("text")
author_verified = has_verified_badge?(related["ownerBadges"]?).to_s
ucid = channel_info.try { |ci| HelperExtractors.get_browse_id(ci) }
# "4,088,033 views", only available on compact renderer
# and when video is not a livestream
view_count = related.dig?("viewCountText", "simpleText")
.try &.as_s.gsub(/\D/, "")
short_view_count = related.try do |r|
HelperExtractors.get_short_view_count(r).to_s
end
::Log.forf.trace { "Found \"watchNextEndScreenRenderer\" container" }
if published_time_text = related["publishedTimeText"]?
decoded_time = decode_date(published_time_text["simpleText"].to_s)
published = decoded_time.to_rfc3339.to_s
else
published = nil
end
# TODO: when refactoring video types, make a struct for related videos
# or reuse an existing type, if that fits.
return {
"id" => related["videoId"],
"title" => related["title"]["simpleText"],
"author" => author || JSON::Any.new(""),
"ucid" => JSON::Any.new(ucid || ""),
"length_seconds" => JSON::Any.new(length || "0"),
"view_count" => JSON::Any.new(view_count || "0"),
"short_view_count" => JSON::Any.new(short_view_count || "0"),
"author_verified" => JSON::Any.new(author_verified),
"published" => JSON::Any.new(published || ""),
}
end end
def extract_video_info(video_id : String) # Both have "short", so the "long" option shouldn't be required
# Init client config for the API channel_info = (related["shortBylineText"]? || related["longBylineText"]?)
client_config = YoutubeAPI::ClientConfig.new .try &.dig?("runs", 0)
# Fetch data from the player endpoint author = channel_info.try &.dig?("text")
player_response = YoutubeAPI.player(video_id: video_id, params: "2AMB", client_config: client_config) author_verified = has_verified_badge?(related["ownerBadges"]?).to_s
playability_status = player_response.dig?("playabilityStatus", "status").try &.as_s ucid = channel_info.try { |ci| HelperExtractors.get_browse_id(ci) }
if playability_status != "OK" # "4,088,033 views", only available on compact renderer
subreason = player_response.dig?("playabilityStatus", "errorScreen", "playerErrorMessageRenderer", "subreason") # and when video is not a livestream
reason = subreason.try &.[]?("simpleText").try &.as_s view_count = related.dig?("viewCountText", "simpleText")
reason ||= subreason.try &.[]("runs").as_a.map(&.[]("text")).join("") .try &.as_s.gsub(/\D/, "")
reason ||= player_response.dig("playabilityStatus", "reason").as_s
# Stop here if video is not a scheduled livestream or short_view_count = related.try do |r|
# for LOGIN_REQUIRED when videoDetails element is not found because retrying won't help HelperExtractors.get_short_view_count(r).to_s
if !{"LIVE_STREAM_OFFLINE", "LOGIN_REQUIRED"}.any?(playability_status) || end
playability_status == "LOGIN_REQUIRED" && !player_response.dig?("videoDetails")
return {
"version" => JSON::Any.new(Video::SCHEMA_VERSION.to_i64),
"reason" => JSON::Any.new(reason),
}
end
elsif video_id != player_response.dig?("videoDetails", "videoId")
# YouTube may return a different video player response than expected.
# See: https://github.com/TeamNewPipe/NewPipe/issues/8713
# Line to be reverted if one day we solve the video not available issue.
# Although technically not a call to /videoplayback the fact that YouTube is returning the Log.trace { "parse_related_video: Found \"watchNextEndScreenRenderer\" container" }
# wrong video means that we should count it as a failure.
get_playback_statistic()["totalRequests"] += 1
if published_time_text = related["publishedTimeText"]?
decoded_time = decode_date(published_time_text["simpleText"].to_s)
published = decoded_time.to_rfc3339.to_s
else
published = nil
end
# TODO: when refactoring video types, make a struct for related videos
# or reuse an existing type, if that fits.
return {
"id" => related["videoId"],
"title" => related["title"]["simpleText"],
"author" => author || JSON::Any.new(""),
"ucid" => JSON::Any.new(ucid || ""),
"length_seconds" => JSON::Any.new(length || "0"),
"view_count" => JSON::Any.new(view_count || "0"),
"short_view_count" => JSON::Any.new(short_view_count || "0"),
"author_verified" => JSON::Any.new(author_verified),
"published" => JSON::Any.new(published || ""),
}
end
def extract_video_info(video_id : String)
# Init client config for the API
client_config = YoutubeAPI::ClientConfig.new
# Fetch data from the player endpoint
player_response = YoutubeAPI.player(video_id: video_id, params: "2AMB", client_config: client_config)
playability_status = player_response.dig?("playabilityStatus", "status").try &.as_s
if playability_status != "OK"
subreason = player_response.dig?("playabilityStatus", "errorScreen", "playerErrorMessageRenderer", "subreason")
reason = subreason.try &.[]?("simpleText").try &.as_s
reason ||= subreason.try &.[]("runs").as_a.map(&.[]("text")).join("")
reason ||= player_response.dig("playabilityStatus", "reason").as_s
# Stop here if video is not a scheduled livestream or
# for LOGIN_REQUIRED when videoDetails element is not found because retrying won't help
if !{"LIVE_STREAM_OFFLINE", "LOGIN_REQUIRED"}.any?(playability_status) ||
playability_status == "LOGIN_REQUIRED" && !player_response.dig?("videoDetails")
return { return {
"version" => JSON::Any.new(Video::SCHEMA_VERSION.to_i64), "version" => JSON::Any.new(Video::SCHEMA_VERSION.to_i64),
"reason" => JSON::Any.new("Can't load the video on this Invidious instance. YouTube is currently trying to block Invidious instances. <a href=\"https://github.com/iv-org/invidious/issues/3822\">Click here for more info about the issue.</a>"), "reason" => JSON::Any.new(reason),
} }
else
reason = nil
end end
elsif video_id != player_response.dig?("videoDetails", "videoId")
# YouTube may return a different video player response than expected.
# See: https://github.com/TeamNewPipe/NewPipe/issues/8713
# Line to be reverted if one day we solve the video not available issue.
# Don't fetch the next endpoint if the video is unavailable. # Although technically not a call to /videoplayback the fact that YouTube is returning the
if {"OK", "LIVE_STREAM_OFFLINE", "LOGIN_REQUIRED"}.any?(playability_status) # wrong video means that we should count it as a failure.
next_response = YoutubeAPI.next({"videoId": video_id, "params": ""}) get_playback_statistic()["totalRequests"] += 1
player_response = player_response.merge(next_response)
end
params = parse_video_info(video_id, player_response) return {
params["reason"] = JSON::Any.new(reason) if reason "version" => JSON::Any.new(Video::SCHEMA_VERSION.to_i64),
"reason" => JSON::Any.new("Can't load the video on this Invidious instance. YouTube is currently trying to block Invidious instances. <a href=\"https://github.com/iv-org/invidious/issues/3822\">Click here for more info about the issue.</a>"),
}
else
reason = nil
end
if !CONFIG.invidious_companion.present? # Don't fetch the next endpoint if the video is unavailable.
if player_response.dig?("streamingData", "adaptiveFormats", 0, "url").nil? if {"OK", "LIVE_STREAM_OFFLINE", "LOGIN_REQUIRED"}.any?(playability_status)
Log.warn { "Missing URLs for adaptive formats, falling back to other YT clients." } next_response = YoutubeAPI.next({"videoId": video_id, "params": ""})
players_fallback = {YoutubeAPI::ClientType::TvSimply, YoutubeAPI::ClientType::WebMobile} player_response = player_response.merge(next_response)
end
players_fallback.each do |player_fallback| params = parse_video_info(video_id, player_response)
client_config.client_type = player_fallback params["reason"] = JSON::Any.new(reason) if reason
next if !(player_fallback_response = try_fetch_streaming_data(video_id, client_config)) if !CONFIG.invidious_companion.present?
if player_response.dig?("streamingData", "adaptiveFormats", 0, "url").nil?
Log.warn { "Missing URLs for adaptive formats, falling back to other YT clients." }
players_fallback = {YoutubeAPI::ClientType::TvSimply, YoutubeAPI::ClientType::WebMobile}
adaptive_formats = player_fallback_response.dig?("streamingData", "adaptiveFormats") players_fallback.each do |player_fallback|
if adaptive_formats && (adaptive_formats.dig?(0, "url") || adaptive_formats.dig?(0, "signatureCipher")) client_config.client_type = player_fallback
streaming_data = player_response["streamingData"].as_h
streaming_data["adaptiveFormats"] = adaptive_formats next if !(player_fallback_response = try_fetch_streaming_data(video_id, client_config))
player_response["streamingData"] = JSON::Any.new(streaming_data)
break adaptive_formats = player_fallback_response.dig?("streamingData", "adaptiveFormats")
end if adaptive_formats && (adaptive_formats.dig?(0, "url") || adaptive_formats.dig?(0, "signatureCipher"))
streaming_data = player_response["streamingData"].as_h
streaming_data["adaptiveFormats"] = adaptive_formats
player_response["streamingData"] = JSON::Any.new(streaming_data)
break
end end
end rescue InfoException
next Log.warn { "Failed to fetch streams with #{player_fallback}" }
# Seems like video page can still render even without playable streams.
# its better than nothing.
#
# # Were we able to find playable video streams?
# if player_response.dig?("streamingData", "adaptiveFormats", 0, "url").nil?
# # No :(
# end
end
{"captions", "playabilityStatus", "playerConfig", "storyboards"}.each do |f|
params[f] = player_response[f] if player_response[f]?
end
# Convert URLs, if those are present
if streaming_data = player_response["streamingData"]?
%w[formats adaptiveFormats].each do |key|
streaming_data.as_h[key]?.try &.as_a.each do |format|
format = format.as_h
if format["url"]?.nil?
format["url"] = format["signatureCipher"]
end
format["url"] = JSON::Any.new(convert_url(format))
end
params["streamingData"] = streaming_data
end end
end end
# Data structure version, for cache control # Seems like video page can still render even without playable streams.
params["version"] = JSON::Any.new(Video::SCHEMA_VERSION.to_i64) # its better than nothing.
#
return params # # Were we able to find playable video streams?
# if player_response.dig?("streamingData", "adaptiveFormats", 0, "url").nil?
# # No :(
# end
end end
def try_fetch_streaming_data(id : String, client_config : YoutubeAPI::ClientConfig) : Hash(String, JSON::Any)? {"captions", "playabilityStatus", "playerConfig", "storyboards"}.each do |f|
::Log.forf.debug { "[#{id}] Using #{client_config.client_type} client." } params[f] = player_response[f] if player_response[f]?
response = YoutubeAPI.player(video_id: id, params: "2AMB", client_config: client_config) end
playability_status = response["playabilityStatus"]["status"] # Convert URLs, if those are present
::Log.forf.debug { "[#{id}] Got playabilityStatus == #{playability_status}." } if streaming_data = player_response["streamingData"]?
%w[formats adaptiveFormats].each do |key|
streaming_data.as_h[key]?.try &.as_a.each do |format|
format = format.as_h
if format["url"]?.nil?
format["url"] = format["signatureCipher"]
end
format["url"] = JSON::Any.new(convert_url(format))
end
end
if id != response.dig?("videoDetails", "videoId") params["streamingData"] = streaming_data
# YouTube may return a different video player response than expected. end
# See: https://github.com/TeamNewPipe/NewPipe/issues/8713
raise InfoException.new( # Data structure version, for cache control
"The video returned by YouTube isn't the requested one. (#{client_config.client_type} client)" params["version"] = JSON::Any.new(Video::SCHEMA_VERSION.to_i64)
)
elsif playability_status == "OK" return params
return response end
else
return nil def try_fetch_streaming_data(id : String, client_config : YoutubeAPI::ClientConfig) : Hash(String, JSON::Any)?
Log.debug { "try_fetch_streaming_data: [#{id}] Using #{client_config.client_type} client." }
response = YoutubeAPI.player(video_id: id, params: "2AMB", client_config: client_config)
playability_status = response["playabilityStatus"]["status"]
Log.debug { "try_fetch_streaming_data: [#{id}] Got playabilityStatus == #{playability_status}." }
if id != response.dig?("videoDetails", "videoId")
# YouTube may return a different video player response than expected.
# See: https://github.com/TeamNewPipe/NewPipe/issues/8713
raise InfoException.new(
"The video returned by YouTube isn't the requested one. (#{client_config.client_type} client)"
)
elsif playability_status == "OK"
return response
else
return nil
end
end
def parse_video_info(video_id : String, player_response : Hash(String, JSON::Any)) : Hash(String, JSON::Any)
# Top level elements
main_results = player_response.dig?("contents", "twoColumnWatchNextResults")
raise BrokenTubeException.new("twoColumnWatchNextResults") if !main_results
# Primary results are not available on Music videos
# See: https://github.com/iv-org/invidious/pull/3238#issuecomment-1207193725
if primary_results = main_results.dig?("results", "results", "contents")
video_primary_renderer = primary_results
.as_a.find(&.["videoPrimaryInfoRenderer"]?)
.try &.["videoPrimaryInfoRenderer"]
video_secondary_renderer = primary_results
.as_a.find(&.["videoSecondaryInfoRenderer"]?)
.try &.["videoSecondaryInfoRenderer"]
raise BrokenTubeException.new("videoPrimaryInfoRenderer") if !video_primary_renderer
raise BrokenTubeException.new("videoSecondaryInfoRenderer") if !video_secondary_renderer
end
video_details = player_response.dig?("videoDetails")
if !(microformat = player_response.dig?("microformat", "playerMicroformatRenderer"))
microformat = {} of String => JSON::Any
end
raise BrokenTubeException.new("videoDetails") if !video_details
# Basic video infos
title = video_details["title"]?.try &.as_s
# We have to try to extract viewCount from videoPrimaryInfoRenderer first,
# then from videoDetails, as the latter is "0" for livestreams (we want
# to get the amount of viewers watching).
views_txt = extract_text(
video_primary_renderer
.try &.dig?("viewCount", "videoViewCountRenderer", "viewCount")
)
views_txt ||= video_details["viewCount"]?.try &.as_s || ""
views = views_txt.gsub(/\D/, "").to_i64?
length_txt = (microformat["lengthSeconds"]? || video_details["lengthSeconds"])
.try &.as_s.to_i64
published = microformat["publishDate"]?
.try { |t| Time.parse(t.as_s, "%Y-%m-%d", Time::Location::UTC) } || Time.utc
premiere_timestamp = microformat.dig?("liveBroadcastDetails", "startTimestamp")
.try { |t| Time.parse_rfc3339(t.as_s) }
premiere_timestamp ||= player_response.dig?(
"playabilityStatus", "liveStreamability",
"liveStreamabilityRenderer", "offlineSlate",
"liveStreamOfflineSlateRenderer", "scheduledStartTime"
)
.try &.as_s.to_i64
.try { |t| Time.unix(t) }
live_now = microformat.dig?("liveBroadcastDetails", "isLiveNow")
.try &.as_bool
live_now ||= video_details.dig?("isLive").try &.as_bool || false
post_live_dvr = video_details.dig?("isPostLiveDvr")
.try &.as_bool || false
# Extra video infos
allowed_regions = microformat["availableCountries"]?
.try &.as_a.map &.as_s || [] of String
allow_ratings = video_details["allowRatings"]?.try &.as_bool
family_friendly = microformat["isFamilySafe"]?.try &.as_bool
is_listed = video_details["isCrawlable"]?.try &.as_bool
is_upcoming = video_details["isUpcoming"]?.try &.as_bool
keywords = video_details["keywords"]?
.try &.as_a.map &.as_s || [] of String
# Related videos
Log.debug { "parse_video_info: parsing related videos..." }
related = [] of JSON::Any
# Parse "compactVideoRenderer" items (under secondary results)
secondary_results = main_results
.dig?("secondaryResults", "secondaryResults", "results")
secondary_results.try &.as_a.each do |element|
if item = element["compactVideoRenderer"]?
related_video = parse_related_video(item)
related << JSON::Any.new(related_video) if related_video
end end
end end
def parse_video_info(video_id : String, player_response : Hash(String, JSON::Any)) : Hash(String, JSON::Any) # If nothing was found previously, fall back to end screen renderer
# Top level elements if related.empty?
# Container for "endScreenVideoRenderer" items
main_results = player_response.dig?("contents", "twoColumnWatchNextResults") player_overlays = player_response.dig?(
"playerOverlays", "playerOverlayRenderer",
raise BrokenTubeException.new("twoColumnWatchNextResults") if !main_results "endScreen", "watchNextEndScreenRenderer", "results"
# Primary results are not available on Music videos
# See: https://github.com/iv-org/invidious/pull/3238#issuecomment-1207193725
if primary_results = main_results.dig?("results", "results", "contents")
video_primary_renderer = primary_results
.as_a.find(&.["videoPrimaryInfoRenderer"]?)
.try &.["videoPrimaryInfoRenderer"]
video_secondary_renderer = primary_results
.as_a.find(&.["videoSecondaryInfoRenderer"]?)
.try &.["videoSecondaryInfoRenderer"]
raise BrokenTubeException.new("videoPrimaryInfoRenderer") if !video_primary_renderer
raise BrokenTubeException.new("videoSecondaryInfoRenderer") if !video_secondary_renderer
end
video_details = player_response.dig?("videoDetails")
if !(microformat = player_response.dig?("microformat", "playerMicroformatRenderer"))
microformat = {} of String => JSON::Any
end
raise BrokenTubeException.new("videoDetails") if !video_details
# Basic video infos
title = video_details["title"]?.try &.as_s
# We have to try to extract viewCount from videoPrimaryInfoRenderer first,
# then from videoDetails, as the latter is "0" for livestreams (we want
# to get the amount of viewers watching).
views_txt = extract_text(
video_primary_renderer
.try &.dig?("viewCount", "videoViewCountRenderer", "viewCount")
) )
views_txt ||= video_details["viewCount"]?.try &.as_s || ""
views = views_txt.gsub(/\D/, "").to_i64?
length_txt = (microformat["lengthSeconds"]? || video_details["lengthSeconds"]) player_overlays.try &.as_a.each do |element|
.try &.as_s.to_i64 if item = element["endScreenVideoRenderer"]?
published = microformat["publishDate"]?
.try { |t| Time.parse(t.as_s, "%Y-%m-%d", Time::Location::UTC) } || Time.utc
premiere_timestamp = microformat.dig?("liveBroadcastDetails", "startTimestamp")
.try { |t| Time.parse_rfc3339(t.as_s) }
premiere_timestamp ||= player_response.dig?(
"playabilityStatus", "liveStreamability",
"liveStreamabilityRenderer", "offlineSlate",
"liveStreamOfflineSlateRenderer", "scheduledStartTime"
)
.try &.as_s.to_i64
.try { |t| Time.unix(t) }
live_now = microformat.dig?("liveBroadcastDetails", "isLiveNow")
.try &.as_bool
live_now ||= video_details.dig?("isLive").try &.as_bool || false
post_live_dvr = video_details.dig?("isPostLiveDvr")
.try &.as_bool || false
# Extra video infos
allowed_regions = microformat["availableCountries"]?
.try &.as_a.map &.as_s || [] of String
allow_ratings = video_details["allowRatings"]?.try &.as_bool
family_friendly = microformat["isFamilySafe"]?.try &.as_bool
is_listed = video_details["isCrawlable"]?.try &.as_bool
is_upcoming = video_details["isUpcoming"]?.try &.as_bool
keywords = video_details["keywords"]?
.try &.as_a.map &.as_s || [] of String
# Related videos
::Log.forf.debug { "parsing related videos..." }
related = [] of JSON::Any
# Parse "compactVideoRenderer" items (under secondary results)
secondary_results = main_results
.dig?("secondaryResults", "secondaryResults", "results")
secondary_results.try &.as_a.each do |element|
if item = element["compactVideoRenderer"]?
related_video = parse_related_video(item) related_video = parse_related_video(item)
related << JSON::Any.new(related_video) if related_video related << JSON::Any.new(related_video) if related_video
end end
end end
end
# If nothing was found previously, fall back to end screen renderer # Likes
if related.empty?
# Container for "endScreenVideoRenderer" items
player_overlays = player_response.dig?(
"playerOverlays", "playerOverlayRenderer",
"endScreen", "watchNextEndScreenRenderer", "results"
)
player_overlays.try &.as_a.each do |element| toplevel_buttons = video_primary_renderer
if item = element["endScreenVideoRenderer"]? .try &.dig?("videoActions", "menuRenderer", "topLevelButtons")
related_video = parse_related_video(item)
related << JSON::Any.new(related_video) if related_video
end
end
end
# Likes if toplevel_buttons
# New Format as of december 2023
toplevel_buttons = video_primary_renderer likes_button = toplevel_buttons.dig?(0,
.try &.dig?("videoActions", "menuRenderer", "topLevelButtons") "segmentedLikeDislikeButtonViewModel",
"likeButtonViewModel",
if toplevel_buttons "likeButtonViewModel",
# New Format as of december 2023 "toggleButtonViewModel",
likes_button = toplevel_buttons.dig?(0, "toggleButtonViewModel",
"segmentedLikeDislikeButtonViewModel", "defaultButtonViewModel",
"likeButtonViewModel", "buttonViewModel"
"likeButtonViewModel",
"toggleButtonViewModel",
"toggleButtonViewModel",
"defaultButtonViewModel",
"buttonViewModel"
)
likes_button ||= toplevel_buttons.try &.as_a
.find(&.dig?("toggleButtonRenderer", "defaultIcon", "iconType").=== "LIKE")
.try &.["toggleButtonRenderer"]
# New format as of september 2022
likes_button ||= toplevel_buttons.try &.as_a
.find(&.["segmentedLikeDislikeButtonRenderer"]?)
.try &.dig?(
"segmentedLikeDislikeButtonRenderer",
"likeButton", "toggleButtonRenderer"
)
if likes_button
likes_txt = likes_button.dig?("accessibilityText")
# Note: The like count from `toggledText` is off by one, as it would
# represent the new like count in the event where the user clicks on "like".
likes_txt ||= (likes_button["defaultText"]? || likes_button["toggledText"]?)
.try &.dig?("accessibility", "accessibilityData", "label")
likes = likes_txt.as_s.gsub(/\D/, "").to_i64? if likes_txt
::Log.forf.trace { "Found \"likes\" button. Button text is \"#{likes_txt}\"" }
::Log.forf.debug { "Likes count is #{likes}" } if likes
end
end
# Description
description = microformat.dig?("description", "simpleText").try &.as_s || ""
short_description = player_response.dig?("videoDetails", "shortDescription")
# description_html = video_secondary_renderer.try &.dig?("description", "runs")
# .try &.as_a.try { |t| content_to_comment_html(t, video_id) }
description_html = parse_description(video_secondary_renderer.try &.dig?("attributedDescription"), video_id)
# Video metadata
metadata = video_secondary_renderer
.try &.dig?("metadataRowContainer", "metadataRowContainerRenderer", "rows")
.try &.as_a
genre = microformat["category"]?
genre_ucid = nil
license = nil
metadata.try &.each do |row|
metadata_title = extract_text(row.dig?("metadataRowRenderer", "title"))
contents = row.dig?("metadataRowRenderer", "contents", 0)
if metadata_title == "Category"
contents = contents.try &.dig?("runs", 0)
genre = contents.try &.["text"]?
genre_ucid = contents.try &.dig?("navigationEndpoint", "browseEndpoint", "browseId")
elsif metadata_title == "License"
license = contents.try &.dig?("runs", 0, "text")
elsif metadata_title == "Licensed to YouTube by"
license = contents.try &.["simpleText"]?
end
end
# Music section
music_list = [] of VideoMusic
music_desclist = player_response.dig?(
"engagementPanels", 1, "engagementPanelSectionListRenderer",
"content", "structuredDescriptionContentRenderer", "items", 2,
"videoDescriptionMusicSectionRenderer", "carouselLockups"
) )
music_desclist.try &.as_a.each do |music_desc| likes_button ||= toplevel_buttons.try &.as_a
artist = nil .find(&.dig?("toggleButtonRenderer", "defaultIcon", "iconType").=== "LIKE")
album = nil .try &.["toggleButtonRenderer"]
music_license = nil
# Used when the video has multiple songs # New format as of september 2022
if song_title = music_desc.dig?("carouselLockupRenderer", "videoLockup", "compactVideoRenderer", "title") likes_button ||= toplevel_buttons.try &.as_a
# "simpleText" for plain text / "runs" when song has a link .find(&.["segmentedLikeDislikeButtonRenderer"]?)
song = song_title["simpleText"]? || song_title.dig?("runs", 0, "text") .try &.dig?(
"segmentedLikeDislikeButtonRenderer",
"likeButton", "toggleButtonRenderer"
)
# some videos can have empty tracks. See: https://www.youtube.com/watch?v=eBGIQ7ZuuiU if likes_button
next if !song likes_txt = likes_button.dig?("accessibilityText")
end # Note: The like count from `toggledText` is off by one, as it would
# represent the new like count in the event where the user clicks on "like".
likes_txt ||= (likes_button["defaultText"]? || likes_button["toggledText"]?)
.try &.dig?("accessibility", "accessibilityData", "label")
likes = likes_txt.as_s.gsub(/\D/, "").to_i64? if likes_txt
music_desc.dig?("carouselLockupRenderer", "infoRows").try &.as_a.each do |desc| Log.trace { "parse_video_info: Found \"likes\" button. Button text is \"#{likes_txt}\"" }
desc_title = extract_text(desc.dig?("infoRowRenderer", "title")) Log.debug { "parse_video_info: Likes count is #{likes}" } if likes
if desc_title == "ARTIST" end
artist = extract_text(desc.dig?("infoRowRenderer", "defaultMetadata")) end
elsif desc_title == "SONG"
song = extract_text(desc.dig?("infoRowRenderer", "defaultMetadata")) # Description
elsif desc_title == "ALBUM"
album = extract_text(desc.dig?("infoRowRenderer", "defaultMetadata")) description = microformat.dig?("description", "simpleText").try &.as_s || ""
elsif desc_title == "LICENSES" short_description = player_response.dig?("videoDetails", "shortDescription")
music_license = extract_text(desc.dig?("infoRowRenderer", "expandedMetadata"))
end # description_html = video_secondary_renderer.try &.dig?("description", "runs")
end # .try &.as_a.try { |t| content_to_comment_html(t, video_id) }
music_list << VideoMusic.new(song.to_s, album.to_s, artist.to_s, music_license.to_s)
description_html = parse_description(video_secondary_renderer.try &.dig?("attributedDescription"), video_id)
# Video metadata
metadata = video_secondary_renderer
.try &.dig?("metadataRowContainer", "metadataRowContainerRenderer", "rows")
.try &.as_a
genre = microformat["category"]?
genre_ucid = nil
license = nil
metadata.try &.each do |row|
metadata_title = extract_text(row.dig?("metadataRowRenderer", "title"))
contents = row.dig?("metadataRowRenderer", "contents", 0)
if metadata_title == "Category"
contents = contents.try &.dig?("runs", 0)
genre = contents.try &.["text"]?
genre_ucid = contents.try &.dig?("navigationEndpoint", "browseEndpoint", "browseId")
elsif metadata_title == "License"
license = contents.try &.dig?("runs", 0, "text")
elsif metadata_title == "Licensed to YouTube by"
license = contents.try &.["simpleText"]?
end
end
# Music section
music_list = [] of VideoMusic
music_desclist = player_response.dig?(
"engagementPanels", 1, "engagementPanelSectionListRenderer",
"content", "structuredDescriptionContentRenderer", "items", 2,
"videoDescriptionMusicSectionRenderer", "carouselLockups"
)
music_desclist.try &.as_a.each do |music_desc|
artist = nil
album = nil
music_license = nil
# Used when the video has multiple songs
if song_title = music_desc.dig?("carouselLockupRenderer", "videoLockup", "compactVideoRenderer", "title")
# "simpleText" for plain text / "runs" when song has a link
song = song_title["simpleText"]? || song_title.dig?("runs", 0, "text")
# some videos can have empty tracks. See: https://www.youtube.com/watch?v=eBGIQ7ZuuiU
next if !song
end end
music_desc.dig?("carouselLockupRenderer", "infoRows").try &.as_a.each do |desc|
desc_title = extract_text(desc.dig?("infoRowRenderer", "title"))
if desc_title == "ARTIST"
artist = extract_text(desc.dig?("infoRowRenderer", "defaultMetadata"))
elsif desc_title == "SONG"
song = extract_text(desc.dig?("infoRowRenderer", "defaultMetadata"))
elsif desc_title == "ALBUM"
album = extract_text(desc.dig?("infoRowRenderer", "defaultMetadata"))
elsif desc_title == "LICENSES"
music_license = extract_text(desc.dig?("infoRowRenderer", "expandedMetadata"))
end
end
music_list << VideoMusic.new(song.to_s, album.to_s, artist.to_s, music_license.to_s)
end
# Author infos
author = video_details["author"]?.try &.as_s
ucid = video_details["channelId"]?.try &.as_s
if author_info = video_secondary_renderer.try &.dig?("owner", "videoOwnerRenderer")
author_thumbnail = author_info.dig?("thumbnail", "thumbnails", 0, "url")
author_verified = has_verified_badge?(author_info["badges"]?)
subs_text = author_info["subscriberCountText"]?
.try { |t| t["simpleText"]? || t.dig?("runs", 0, "text") }
.try &.as_s.split(" ", 2)[0]
end
# Return data
if live_now
video_type = VideoType::Livestream
elsif !premiere_timestamp.nil?
video_type = VideoType::Scheduled
published = premiere_timestamp || Time.utc
else
video_type = VideoType::Video
end
params = {
"videoType" => JSON::Any.new(video_type.to_s),
# Basic video infos
"title" => JSON::Any.new(title || ""),
"views" => JSON::Any.new(views || 0_i64),
"likes" => JSON::Any.new(likes || 0_i64),
"lengthSeconds" => JSON::Any.new(length_txt || 0_i64),
"published" => JSON::Any.new(published.to_rfc3339),
# Extra video infos
"allowedRegions" => JSON::Any.new(allowed_regions.map { |v| JSON::Any.new(v) }),
"allowRatings" => JSON::Any.new(allow_ratings || false),
"isFamilyFriendly" => JSON::Any.new(family_friendly || false),
"isListed" => JSON::Any.new(is_listed || false),
"isUpcoming" => JSON::Any.new(is_upcoming || false),
"keywords" => JSON::Any.new(keywords.map { |v| JSON::Any.new(v) }),
"isPostLiveDvr" => JSON::Any.new(post_live_dvr),
# Related videos
"relatedVideos" => JSON::Any.new(related),
# Description
"description" => JSON::Any.new(description || ""),
"descriptionHtml" => JSON::Any.new(description_html || "<p></p>"),
"shortDescription" => JSON::Any.new(short_description.try &.as_s || nil),
# Video metadata
"genre" => JSON::Any.new(genre.try &.as_s || ""),
"genreUcid" => JSON::Any.new(genre_ucid.try &.as_s?),
"license" => JSON::Any.new(license.try &.as_s || ""),
# Music section
"music" => JSON.parse(music_list.to_json),
# Author infos # Author infos
"author" => JSON::Any.new(author || ""),
"ucid" => JSON::Any.new(ucid || ""),
"authorThumbnail" => JSON::Any.new(author_thumbnail.try &.as_s || ""),
"authorVerified" => JSON::Any.new(author_verified || false),
"subCountText" => JSON::Any.new(subs_text || "-"),
}
author = video_details["author"]?.try &.as_s return params
ucid = video_details["channelId"]?.try &.as_s end
if author_info = video_secondary_renderer.try &.dig?("owner", "videoOwnerRenderer") private def convert_url(fmt)
author_thumbnail = author_info.dig?("thumbnail", "thumbnails", 0, "url") if cfr = fmt["signatureCipher"]?.try { |json| HTTP::Params.parse(json.as_s) }
author_verified = has_verified_badge?(author_info["badges"]?) sp = cfr["sp"]
url = URI.parse(cfr["url"])
subs_text = author_info["subscriberCountText"]? params = url.query_params
.try { |t| t["simpleText"]? || t.dig?("runs", 0, "text") }
.try &.as_s.split(" ", 2)[0] Log.debug { "convert_url: Decoding '#{cfr}'" }
end
unsig = DECRYPT_FUNCTION.try &.decrypt_signature(cfr["s"])
# Return data params[sp] = unsig if unsig
else
if live_now url = URI.parse(fmt["url"].as_s)
video_type = VideoType::Livestream params = url.query_params
elsif !premiere_timestamp.nil? end
video_type = VideoType::Scheduled
published = premiere_timestamp || Time.utc n = DECRYPT_FUNCTION.try &.decrypt_nsig(params["n"])
else params["n"] = n if n
video_type = VideoType::Video
end if token = CONFIG.po_token
params["pot"] = token
params = { end
"videoType" => JSON::Any.new(video_type.to_s),
# Basic video infos url.query_params = params
"title" => JSON::Any.new(title || ""), Log.trace { "convert_url: new url is '#{url}'" }
"views" => JSON::Any.new(views || 0_i64),
"likes" => JSON::Any.new(likes || 0_i64), return url.to_s
"lengthSeconds" => JSON::Any.new(length_txt || 0_i64), rescue ex
"published" => JSON::Any.new(published.to_rfc3339), Log.debug { "convert_url: Error when parsing video URL" }
# Extra video infos Log.trace { ex.inspect_with_backtrace }
"allowedRegions" => JSON::Any.new(allowed_regions.map { |v| JSON::Any.new(v) }), return ""
"allowRatings" => JSON::Any.new(allow_ratings || false),
"isFamilyFriendly" => JSON::Any.new(family_friendly || false),
"isListed" => JSON::Any.new(is_listed || false),
"isUpcoming" => JSON::Any.new(is_upcoming || false),
"keywords" => JSON::Any.new(keywords.map { |v| JSON::Any.new(v) }),
"isPostLiveDvr" => JSON::Any.new(post_live_dvr),
# Related videos
"relatedVideos" => JSON::Any.new(related),
# Description
"description" => JSON::Any.new(description || ""),
"descriptionHtml" => JSON::Any.new(description_html || "<p></p>"),
"shortDescription" => JSON::Any.new(short_description.try &.as_s || nil),
# Video metadata
"genre" => JSON::Any.new(genre.try &.as_s || ""),
"genreUcid" => JSON::Any.new(genre_ucid.try &.as_s?),
"license" => JSON::Any.new(license.try &.as_s || ""),
# Music section
"music" => JSON.parse(music_list.to_json),
# Author infos
"author" => JSON::Any.new(author || ""),
"ucid" => JSON::Any.new(ucid || ""),
"authorThumbnail" => JSON::Any.new(author_thumbnail.try &.as_s || ""),
"authorVerified" => JSON::Any.new(author_verified || false),
"subCountText" => JSON::Any.new(subs_text || "-"),
}
return params
end
private def convert_url(fmt)
if cfr = fmt["signatureCipher"]?.try { |json| HTTP::Params.parse(json.as_s) }
sp = cfr["sp"]
url = URI.parse(cfr["url"])
params = url.query_params
::Log.forf.debug { "Decoding '#{cfr}'" }
unsig = DECRYPT_FUNCTION.try &.decrypt_signature(cfr["s"])
params[sp] = unsig if unsig
else
url = URI.parse(fmt["url"].as_s)
params = url.query_params
end
n = DECRYPT_FUNCTION.try &.decrypt_nsig(params["n"])
params["n"] = n if n
if token = CONFIG.po_token
params["pot"] = token
end
url.query_params = params
::Log.forf.trace { "new url is '#{url}'" }
return url.to_s
rescue ex
::Log.forf.debug { "Error when parsing video URL" }
::Log.forf.trace { ex.inspect_with_backtrace }
return ""
end
end end

View File

@ -8,12 +8,12 @@
<div class="pure-u-1 pure-u-lg-3-5"> <div class="pure-u-1 pure-u-lg-3-5">
<div class="h-box"> <div class="h-box">
<form class="pure-form pure-form-aligned" action="/add_playlist_items" method="get"> <form class="pure-form pure-form-aligned" action="/add_playlist_items" method="get">
<legend><a href="/playlist?list=<%= playlist.id %>"><%= I18n.translate(locale, "Editing playlist `x`", %|"#{HTML.escape(playlist.title)}"|) %></a></legend> <legend><a href="/playlist?list=<%= playlist.id %>"><%= translate(locale, "Editing playlist `x`", %|"#{HTML.escape(playlist.title)}"|) %></a></legend>
<fieldset> <fieldset>
<input class="pure-input-1" type="search" name="q" <input class="pure-input-1" type="search" name="q"
<% if query %>value="<%= HTML.escape(query.text) %>"<% end %> <% if query %>value="<%= HTML.escape(query.text) %>"<% end %>
placeholder="<%= I18n.translate(locale, "Search for videos") %>"> placeholder="<%= translate(locale, "Search for videos") %>">
<input type="hidden" name="list" value="<%= plid %>"> <input type="hidden" name="list" value="<%= plid %>">
</fieldset> </fieldset>
</form> </form>

View File

@ -35,10 +35,10 @@
<%= <%=
{ {
"ucid" => ucid, "ucid" => ucid,
"youtube_comments_text" => HTML.escape(I18n.translate(locale, "View YouTube comments")), "youtube_comments_text" => HTML.escape(translate(locale, "View YouTube comments")),
"comments_text" => HTML.escape(I18n.translate(locale, "View `x` comments", "{commentCount}")), "comments_text" => HTML.escape(translate(locale, "View `x` comments", "{commentCount}")),
"hide_replies_text" => HTML.escape(I18n.translate(locale, "Hide replies")), "hide_replies_text" => HTML.escape(translate(locale, "Hide replies")),
"show_replies_text" => HTML.escape(I18n.translate(locale, "Show replies")), "show_replies_text" => HTML.escape(translate(locale, "Show replies")),
"preferences" => env.get("preferences").as(Preferences) "preferences" => env.get("preferences").as(Preferences)
}.to_pretty_json }.to_pretty_json
%> %>

View File

@ -24,7 +24,7 @@
<div class="pure-u"> <div class="pure-u">
<a class="pure-button pure-button-secondary" dir="auto" href="/feed/channel/<%= ucid %>"> <a class="pure-button pure-button-secondary" dir="auto" href="/feed/channel/<%= ucid %>">
<i class="icon ion-logo-rss"></i>&nbsp;<%= I18n.translate(locale, "generic_button_rss") %> <i class="icon ion-logo-rss"></i>&nbsp;<%= translate(locale, "generic_button_rss") %>
</a> </a>
</div> </div>
</div> </div>
@ -37,10 +37,10 @@
<div class="pure-g h-box"> <div class="pure-g h-box">
<div class="pure-u-1-2"> <div class="pure-u-1-2">
<div class="pure-u-1 pure-md-1-3"> <div class="pure-u-1 pure-md-1-3">
<a href="<%= youtube_url %>"><%= I18n.translate(locale, "View channel on YouTube") %></a> <a href="<%= youtube_url %>"><%= translate(locale, "View channel on YouTube") %></a>
</div> </div>
<div class="pure-u-1 pure-md-1-3"> <div class="pure-u-1 pure-md-1-3">
<a href="<%= redirect_url %>"><%= I18n.translate(locale, "Switch Invidious Instance") %></a> <a href="<%= redirect_url %>"><%= translate(locale, "Switch Invidious Instance") %></a>
</div> </div>
<%= Invidious::Frontend::ChannelPage.generate_tabs_links(locale, channel, selected_tab) %> <%= Invidious::Frontend::ChannelPage.generate_tabs_links(locale, channel, selected_tab) %>
@ -50,9 +50,9 @@
<% sort_options.each do |sort| %> <% sort_options.each do |sort| %>
<div class="pure-u-1 pure-md-1-3"> <div class="pure-u-1 pure-md-1-3">
<% if sort_by == sort %> <% if sort_by == sort %>
<b><%= I18n.translate(locale, sort) %></b> <b><%= translate(locale, sort) %></b>
<% else %> <% else %>
<a href="<%= relative_url %>?sort_by=<%= sort %>"><%= I18n.translate(locale, sort) %></a> <a href="<%= relative_url %>?sort_by=<%= sort %>"><%= translate(locale, sort) %></a>
<% end %> <% end %>
</div> </div>
<% end %> <% end %>

View File

@ -5,7 +5,7 @@
<% end %> <% end %>
<% feed_menu.each do |feed| %> <% feed_menu.each do |feed| %>
<a href="/feed/<%= feed.downcase %>" class="feed-menu-item pure-menu-heading"> <a href="/feed/<%= feed.downcase %>" class="feed-menu-item pure-menu-heading">
<%= I18n.translate(locale, feed) %> <%= translate(locale, feed) %>
</a> </a>
<% end %> <% end %>
</div> </div>

View File

@ -27,8 +27,8 @@
</div> </div>
<% if !item.channel_handle.nil? %><p class="channel-name" dir="auto"><%= item.channel_handle %></p><% end %> <% if !item.channel_handle.nil? %><p class="channel-name" dir="auto"><%= item.channel_handle %></p><% end %>
<p><%= I18n.translate_count(locale, "generic_subscribers_count", item.subscriber_count, I18n::NumberFormatting::Separator) %></p> <p><%= translate_count(locale, "generic_subscribers_count", item.subscriber_count, NumberFormatting::Separator) %></p>
<% if !item.auto_generated && item.channel_handle.nil? %><p><%= I18n.translate_count(locale, "generic_videos_count", item.video_count, I18n::NumberFormatting::Separator) %></p><% end %> <% if !item.auto_generated && item.channel_handle.nil? %><p><%= translate_count(locale, "generic_videos_count", item.video_count, NumberFormatting::Separator) %></p><% end %>
<h5><%= item.description_html %></h5> <h5><%= item.description_html %></h5>
<% when SearchHashtag %> <% when SearchHashtag %>
<% if !thin_mode %> <% if !thin_mode %>
@ -45,13 +45,13 @@
<div class="video-card-row"> <div class="video-card-row">
<%- if item.video_count != 0 -%> <%- if item.video_count != 0 -%>
<p><%= I18n.translate_count(locale, "generic_videos_count", item.video_count, I18n::NumberFormatting::Separator) %></p> <p><%= translate_count(locale, "generic_videos_count", item.video_count, NumberFormatting::Separator) %></p>
<%- end -%> <%- end -%>
</div> </div>
<div class="video-card-row"> <div class="video-card-row">
<%- if item.channel_count != 0 -%> <%- if item.channel_count != 0 -%>
<p><%= I18n.translate_count(locale, "generic_channels_count", item.channel_count, I18n::NumberFormatting::Separator) %></p> <p><%= translate_count(locale, "generic_channels_count", item.channel_count, NumberFormatting::Separator) %></p>
<%- end -%> <%- end -%>
</div> </div>
<% when SearchPlaylist, InvidiousPlaylist %> <% when SearchPlaylist, InvidiousPlaylist %>
@ -73,7 +73,7 @@
<%- end -%> <%- end -%>
<div class="bottom-right-overlay"> <div class="bottom-right-overlay">
<p class="length"><%= I18n.translate_count(locale, "generic_videos_count", item.video_count, I18n::NumberFormatting::Separator) %></p> <p class="length"><%= translate_count(locale, "generic_videos_count", item.video_count, NumberFormatting::Separator) %></p>
</div> </div>
</div> </div>
@ -101,11 +101,11 @@
<div class="error-card"> <div class="error-card">
<div class="explanation"> <div class="explanation">
<i class="icon ion-ios-alert"></i> <i class="icon ion-ios-alert"></i>
<h4><%=I18n.translate(locale, "timeline_parse_error_placeholder_heading")%></h4> <h4><%=translate(locale, "timeline_parse_error_placeholder_heading")%></h4>
<p><%=I18n.translate(locale, "timeline_parse_error_placeholder_message")%></p> <p><%=translate(locale, "timeline_parse_error_placeholder_message")%></p>
</div> </div>
<details> <details>
<summary class="pure-button pure-button-secondary"><%=I18n.translate(locale, "timeline_parse_error_show_technical_details")%></summary> <summary class="pure-button pure-button-secondary"><%=translate(locale, "timeline_parse_error_show_technical_details")%></summary>
<pre class="error-issue-template"><%=get_issue_template(env, item.parse_exception)[1]%></pre> <pre class="error-issue-template"><%=get_issue_template(env, item.parse_exception)[1]%></pre>
</details> </details>
</div> </div>
@ -168,7 +168,7 @@
<div class="bottom-right-overlay"> <div class="bottom-right-overlay">
<%- if item.responds_to?(:live_now) && item.live_now -%> <%- if item.responds_to?(:live_now) && item.live_now -%>
<p class="length" dir="auto"><i class="icon ion-ios-play-circle"></i>&nbsp;<%= I18n.translate(locale, "LIVE") %></p> <p class="length" dir="auto"><i class="icon ion-ios-play-circle"></i>&nbsp;<%= translate(locale, "LIVE") %></p>
<%- elsif item.length_seconds != 0 -%> <%- elsif item.length_seconds != 0 -%>
<p class="length"><%= recode_length_seconds(item.length_seconds) %></p> <p class="length"><%= recode_length_seconds(item.length_seconds) %></p>
<%- end -%> <%- end -%>
@ -200,15 +200,15 @@
<div class="video-card-row flexible"> <div class="video-card-row flexible">
<div class="flex-left"> <div class="flex-left">
<% if item.responds_to?(:premiere_timestamp) && item.premiere_timestamp.try &.> Time.utc %> <% if item.responds_to?(:premiere_timestamp) && item.premiere_timestamp.try &.> Time.utc %>
<p class="video-data" dir="auto"><%= I18n.translate(locale, "Premieres in `x`", recode_date((item.premiere_timestamp.as(Time) - Time.utc).ago, locale)) %></p> <p class="video-data" dir="auto"><%= translate(locale, "Premieres in `x`", recode_date((item.premiere_timestamp.as(Time) - Time.utc).ago, locale)) %></p>
<% elsif item.responds_to?(:published) && (Time.utc - item.published) > 1.minute %> <% elsif item.responds_to?(:published) && (Time.utc - item.published) > 1.minute %>
<p class="video-data" dir="auto"><%= I18n.translate(locale, "Shared `x` ago", recode_date(item.published, locale)) %></p> <p class="video-data" dir="auto"><%= translate(locale, "Shared `x` ago", recode_date(item.published, locale)) %></p>
<% end %> <% end %>
</div> </div>
<% if item.responds_to?(:views) && item.views %> <% if item.responds_to?(:views) && item.views %>
<div class="flex-right"> <div class="flex-right">
<p class="video-data" dir="auto"><%= I18n.translate_count(locale, "generic_views_count", item.views || 0, I18n::NumberFormatting::Short) %></p> <p class="video-data" dir="auto"><%= translate_count(locale, "generic_views_count", item.views || 0, NumberFormatting::Short) %></p>
</div> </div>
<% end %> <% end %>
</div> </div>

View File

@ -11,9 +11,9 @@
<script id="pagination-data" type="application/json"> <script id="pagination-data" type="application/json">
<%= <%=
{ {
"next_page" => I18n.translate(locale, "Next page"), "next_page" => translate(locale, "Next page"),
"prev_page" => I18n.translate(locale, "Previous page"), "prev_page" => translate(locale, "Previous page"),
"is_rtl" => I18n.locale_is_rtl?(locale) "is_rtl" => locale_is_rtl?(locale)
}.to_pretty_json }.to_pretty_json
%> %>
</script> </script>

View File

@ -2,11 +2,11 @@
<fieldset> <fieldset>
<input type="search" id="searchbox" autocorrect="off" <input type="search" id="searchbox" autocorrect="off"
autocapitalize="none" spellcheck="false" <% if autofocus %>autofocus<% end %> autocapitalize="none" spellcheck="false" <% if autofocus %>autofocus<% end %>
name="q" placeholder="<%= I18n.translate(locale, "search") %>" name="q" placeholder="<%= translate(locale, "search") %>"
title="<%= I18n.translate(locale, "search") %>" title="<%= translate(locale, "search") %>"
value="<%= env.get?("search").try {|x| HTML.escape(x.as(String)) } %>"> value="<%= env.get?("search").try {|x| HTML.escape(x.as(String)) } %>">
</fieldset> </fieldset>
<button type="submit" id="searchbutton" aria-label="<%= I18n.translate(locale, "search") %>"> <button type="submit" id="searchbutton" aria-label="<%= translate(locale, "search") %>">
<i class="icon ion-ios-search"></i> <i class="icon ion-ios-search"></i>
</button> </button>
</form> </form>

View File

@ -3,14 +3,14 @@
<form action="/subscription_ajax?action=remove_subscriptions&c=<%= ucid %>&referer=<%= env.get("current_page") %>" method="post"> <form action="/subscription_ajax?action=remove_subscriptions&c=<%= ucid %>&referer=<%= env.get("current_page") %>" method="post">
<input type="hidden" name="csrf_token" value="<%= HTML.escape(env.get?("csrf_token").try &.as(String) || "") %>"> <input type="hidden" name="csrf_token" value="<%= HTML.escape(env.get?("csrf_token").try &.as(String) || "") %>">
<button data-type="unsubscribe" id="subscribe" class="pure-button pure-button-primary"> <button data-type="unsubscribe" id="subscribe" class="pure-button pure-button-primary">
<b><input style="all:unset" type="submit" value="<%= I18n.translate(locale, "Unsubscribe") %> | <%= sub_count_text %>"></b> <b><input style="all:unset" type="submit" value="<%= translate(locale, "Unsubscribe") %> | <%= sub_count_text %>"></b>
</button> </button>
</form> </form>
<% else %> <% else %>
<form action="/subscription_ajax?action=create_subscription_to_channel&c=<%= ucid %>&referer=<%= env.get("current_page") %>" method="post"> <form action="/subscription_ajax?action=create_subscription_to_channel&c=<%= ucid %>&referer=<%= env.get("current_page") %>" method="post">
<input type="hidden" name="csrf_token" value="<%= HTML.escape(env.get?("csrf_token").try &.as(String) || "") %>"> <input type="hidden" name="csrf_token" value="<%= HTML.escape(env.get?("csrf_token").try &.as(String) || "") %>">
<button data-type="subscribe" id="subscribe" class="pure-button pure-button-primary"> <button data-type="subscribe" id="subscribe" class="pure-button pure-button-primary">
<b><input style="all:unset" type="submit" value="<%= I18n.translate(locale, "Subscribe") %> | <%= sub_count_text %>"></b> <b><input style="all:unset" type="submit" value="<%= translate(locale, "Subscribe") %> | <%= sub_count_text %>"></b>
</button> </button>
</form> </form>
<% end %> <% end %>
@ -22,8 +22,8 @@
"author" => HTML.escape(author), "author" => HTML.escape(author),
"sub_count_text" => HTML.escape(sub_count_text), "sub_count_text" => HTML.escape(sub_count_text),
"csrf_token" => URI.encode_www_form(env.get?("csrf_token").try &.as(String) || ""), "csrf_token" => URI.encode_www_form(env.get?("csrf_token").try &.as(String) || ""),
"subscribe_text" => HTML.escape(I18n.translate(locale, "Subscribe")), "subscribe_text" => HTML.escape(translate(locale, "Subscribe")),
"unsubscribe_text" => HTML.escape(I18n.translate(locale, "Unsubscribe")) "unsubscribe_text" => HTML.escape(translate(locale, "Unsubscribe"))
}.to_pretty_json }.to_pretty_json
%> %>
</script> </script>
@ -31,6 +31,6 @@
<% else %> <% else %>
<a id="subscribe" class="pure-button pure-button-primary" <a id="subscribe" class="pure-button pure-button-primary"
href="/login?referer=<%= env.get("current_page") %>"> href="/login?referer=<%= env.get("current_page") %>">
<b><%= I18n.translate(locale, "Subscribe") %> | <%= sub_count_text %></b> <b><%= translate(locale, "Subscribe") %> | <%= sub_count_text %></b>
</a> </a>
<% end %> <% end %>

View File

@ -1,18 +1,18 @@
<div class="flex-right flexible"> <div class="flex-right flexible">
<div class="icon-buttons"> <div class="icon-buttons">
<a title="<%=I18n.translate(locale, "videoinfo_watch_on_youTube")%>" rel="noreferrer noopener" href="https://www.youtube.com/watch<%=endpoint_params%>"> <a title="<%=translate(locale, "videoinfo_watch_on_youTube")%>" rel="noreferrer noopener" href="https://www.youtube.com/watch<%=endpoint_params%>">
<i class="icon ion-logo-youtube"></i> <i class="icon ion-logo-youtube"></i>
</a> </a>
<a title="<%=I18n.translate(locale, "Audio mode")%>" href="/watch<%=endpoint_params%>&listen=1"> <a title="<%=translate(locale, "Audio mode")%>" href="/watch<%=endpoint_params%>&listen=1">
<i class="icon ion-md-headset"></i> <i class="icon ion-md-headset"></i>
</a> </a>
<% if env.get("preferences").as(Preferences).automatic_instance_redirect%> <% if env.get("preferences").as(Preferences).automatic_instance_redirect%>
<a title="<%=I18n.translate(locale, "Switch Invidious Instance")%>" href="/redirect?referer=%2Fwatch<%=URI.encode_www_form(endpoint_params)%>"> <a title="<%=translate(locale, "Switch Invidious Instance")%>" href="/redirect?referer=%2Fwatch<%=URI.encode_www_form(endpoint_params)%>">
<i class="icon ion-md-jet"></i> <i class="icon ion-md-jet"></i>
</a> </a>
<% else %> <% else %>
<a title="<%=I18n.translate(locale, "Switch Invidious Instance")%>" href="https://redirect.invidious.io/watch<%=endpoint_params%>"> <a title="<%=translate(locale, "Switch Invidious Instance")%>" href="https://redirect.invidious.io/watch<%=endpoint_params%>">
<i class="icon ion-md-jet"></i> <i class="icon ion-md-jet"></i>
</a> </a>
<% end %> <% end %>

View File

@ -1,5 +1,5 @@
<% content_for "header" do %> <% content_for "header" do %>
<title><%= I18n.translate(locale, "Create playlist") %> - Invidious</title> <title><%= translate(locale, "Create playlist") %> - Invidious</title>
<% end %> <% end %>
<div class="pure-g"> <div class="pure-g">
@ -8,25 +8,25 @@
<div class="h-box"> <div class="h-box">
<form class="pure-form pure-form-aligned" action="/create_playlist?referer=<%= URI.encode_www_form(referer) %>" method="post"> <form class="pure-form pure-form-aligned" action="/create_playlist?referer=<%= URI.encode_www_form(referer) %>" method="post">
<fieldset> <fieldset>
<legend><%= I18n.translate(locale, "Create playlist") %></legend> <legend><%= translate(locale, "Create playlist") %></legend>
<div class="pure-control-group"> <div class="pure-control-group">
<label for="title"><%= I18n.translate(locale, "Title") %> :</label> <label for="title"><%= translate(locale, "Title") %> :</label>
<input required name="title" type="text" placeholder="<%= I18n.translate(locale, "Title") %>"> <input required name="title" type="text" placeholder="<%= translate(locale, "Title") %>">
</div> </div>
<div class="pure-control-group"> <div class="pure-control-group">
<label for="privacy"><%= I18n.translate(locale, "Playlist privacy") %> :</label> <label for="privacy"><%= translate(locale, "Playlist privacy") %> :</label>
<select name="privacy" id="privacy"> <select name="privacy" id="privacy">
<% PlaylistPrivacy.names.each do |option| %> <% PlaylistPrivacy.names.each do |option| %>
<option value="<%= option %>" <% if option == "Public" %> selected <% end %>><%= I18n.translate(locale, option) %></option> <option value="<%= option %>" <% if option == "Public" %> selected <% end %>><%= translate(locale, option) %></option>
<% end %> <% end %>
</select> </select>
</div> </div>
<div class="pure-controls"> <div class="pure-controls">
<button type="submit" name="action" value="create_playlist" class="pure-button pure-button-primary"> <button type="submit" name="action" value="create_playlist" class="pure-button pure-button-primary">
<%= I18n.translate(locale, "Create playlist") %> <%= translate(locale, "Create playlist") %>
</button> </button>
</div> </div>

View File

@ -1,20 +1,20 @@
<% content_for "header" do %> <% content_for "header" do %>
<title><%= I18n.translate(locale, "Delete playlist") %> - Invidious</title> <title><%= translate(locale, "Delete playlist") %> - Invidious</title>
<% end %> <% end %>
<div class="h-box"> <div class="h-box">
<form class="pure-form pure-form-aligned" action="/delete_playlist?list=<%= plid %>&referer=<%= URI.encode_www_form(referer) %>" method="post"> <form class="pure-form pure-form-aligned" action="/delete_playlist?list=<%= plid %>&referer=<%= URI.encode_www_form(referer) %>" method="post">
<legend><%= I18n.translate(locale, "Delete playlist `x`?", %|"#{HTML.escape(playlist.title)}"|) %></legend> <legend><%= translate(locale, "Delete playlist `x`?", %|"#{HTML.escape(playlist.title)}"|) %></legend>
<div class="pure-g"> <div class="pure-g">
<div class="pure-u-1-2"> <div class="pure-u-1-2">
<button type="submit" name="submit" value="delete_playlist" class="pure-button pure-button-primary"> <button type="submit" name="submit" value="delete_playlist" class="pure-button pure-button-primary">
<%= I18n.translate(locale, "Yes") %> <%= translate(locale, "Yes") %>
</button> </button>
</div> </div>
<div class="pure-u-1-2"> <div class="pure-u-1-2">
<a class="pure-button" href="/playlist?list=<%= plid %>"> <a class="pure-button" href="/playlist?list=<%= plid %>">
<%= I18n.translate(locale, "No") %> <%= translate(locale, "No") %>
</a> </a>
</div> </div>
</div> </div>

View File

@ -10,17 +10,17 @@
<div class="flex-right button-container"> <div class="flex-right button-container">
<div class="pure-u"> <div class="pure-u">
<a class="pure-button pure-button-secondary low-profile" dir="auto" href="/playlist?list=<%= plid %>"> <a class="pure-button pure-button-secondary low-profile" dir="auto" href="/playlist?list=<%= plid %>">
<i class="icon ion-md-close"></i>&nbsp;<%= I18n.translate(locale, "generic_button_cancel") %> <i class="icon ion-md-close"></i>&nbsp;<%= translate(locale, "generic_button_cancel") %>
</a> </a>
</div> </div>
<div class="pure-u"> <div class="pure-u">
<button class="pure-button pure-button-secondary low-profile" dir="auto" type="submit"> <button class="pure-button pure-button-secondary low-profile" dir="auto" type="submit">
<i class="icon ion-md-save"></i>&nbsp;<%= I18n.translate(locale, "generic_button_save") %> <i class="icon ion-md-save"></i>&nbsp;<%= translate(locale, "generic_button_save") %>
</button> </button>
</div> </div>
<div class="pure-u"> <div class="pure-u">
<a class="pure-button pure-button-secondary low-profile" dir="auto" href="/delete_playlist?list=<%= plid %>"> <a class="pure-button pure-button-secondary low-profile" dir="auto" href="/delete_playlist?list=<%= plid %>">
<i class="icon ion-md-trash"></i>&nbsp;<%= I18n.translate(locale, "generic_button_delete") %> <i class="icon ion-md-trash"></i>&nbsp;<%= translate(locale, "generic_button_delete") %>
</a> </a>
</div> </div>
</div> </div>
@ -36,11 +36,11 @@
<div class="pure-u-1-1"> <div class="pure-u-1-1">
<b> <b>
<%= HTML.escape(playlist.author) %> | <%= HTML.escape(playlist.author) %> |
<%= I18n.translate_count(locale, "generic_videos_count", playlist.video_count) %> | <%= translate_count(locale, "generic_videos_count", playlist.video_count) %> |
</b> </b>
<select name="privacy"> <select name="privacy">
<%- {"Public", "Unlisted", "Private"}.each do |option| -%> <%- {"Public", "Unlisted", "Private"}.each do |option| -%>
<option value="<%= option %>" <% if option == playlist.privacy.to_s %>selected<% end %>><%= I18n.translate(locale, option) %></option> <option value="<%= option %>" <% if option == playlist.privacy.to_s %>selected<% end %>><%= translate(locale, option) %></option>
<%- end -%> <%- end -%>
</select> </select>
</div> </div>

View File

@ -1,19 +1,19 @@
<% content_for "header" do %> <% content_for "header" do %>
<title><%= I18n.translate(locale, "History") %> - Invidious</title> <title><%= translate(locale, "History") %> - Invidious</title>
<% end %> <% end %>
<div class="pure-g h-box"> <div class="pure-g h-box">
<div class="pure-u-1-3"> <div class="pure-u-1-3">
<h3><%= I18n.translate_count(locale, "generic_videos_count", user.watched.size, I18n::NumberFormatting::HtmlSpan) %></h3> <h3><%= translate_count(locale, "generic_videos_count", user.watched.size, NumberFormatting::HtmlSpan) %></h3>
</div> </div>
<div class="pure-u-1-3"> <div class="pure-u-1-3">
<h3 style="text-align:center"> <h3 style="text-align:center">
<a href="/feed/subscriptions"><%= I18n.translate_count(locale, "generic_subscriptions_count", user.subscriptions.size, I18n::NumberFormatting::HtmlSpan) %></a> <a href="/feed/subscriptions"><%= translate_count(locale, "generic_subscriptions_count", user.subscriptions.size, NumberFormatting::HtmlSpan) %></a>
</h3> </h3>
</div> </div>
<div class="pure-u-1-3"> <div class="pure-u-1-3">
<h3 style="text-align:right"> <h3 style="text-align:right">
<a href="/clear_watch_history"><%= I18n.translate(locale, "Clear watch history") %></a> <a href="/clear_watch_history"><%= translate(locale, "Clear watch history") %></a>
</h3> </h3>
</div> </div>
</div> </div>

View File

@ -1,22 +1,22 @@
<% content_for "header" do %> <% content_for "header" do %>
<title><%= I18n.translate(locale, "Playlists") %> - Invidious</title> <title><%= translate(locale, "Playlists") %> - Invidious</title>
<% end %> <% end %>
<%= rendered "components/feed_menu" %> <%= rendered "components/feed_menu" %>
<div class="pure-g h-box"> <div class="pure-g h-box">
<div class="pure-u-1-3"> <div class="pure-u-1-3">
<h3><%= I18n.translate(locale, "user_created_playlists", %(<span id="count">#{items_created.size}</span>)) %></h3> <h3><%= translate(locale, "user_created_playlists", %(<span id="count">#{items_created.size}</span>)) %></h3>
</div> </div>
<div class="pure-u-1-3"> <div class="pure-u-1-3">
<h3 style="text-align:center"> <h3 style="text-align:center">
<a href="/create_playlist?referer=<%= URI.encode_www_form("/feed/playlists") %>"><%= I18n.translate(locale, "Create playlist") %></a> <a href="/create_playlist?referer=<%= URI.encode_www_form("/feed/playlists") %>"><%= translate(locale, "Create playlist") %></a>
</h3> </h3>
</div> </div>
<div class="pure-u-1-3"> <div class="pure-u-1-3">
<h3 style="text-align:right"> <h3 style="text-align:right">
<a href="/data_control?referer=<%= URI.encode_www_form("/feed/playlists") %>"> <a href="/data_control?referer=<%= URI.encode_www_form("/feed/playlists") %>">
<%= I18n.translate(locale, "Import/export") %> <%= translate(locale, "Import/export") %>
</a> </a>
</h3> </h3>
</div> </div>
@ -30,7 +30,7 @@
<div class="pure-g h-box"> <div class="pure-g h-box">
<div class="pure-u-1"> <div class="pure-u-1">
<h3><%= I18n.translate(locale, "user_saved_playlists", %(<span id="count">#{items_saved.size}</span>)) %></h3> <h3><%= translate(locale, "user_saved_playlists", %(<span id="count">#{items_saved.size}</span>)) %></h3>
</div> </div>
</div> </div>

View File

@ -1,8 +1,8 @@
<% content_for "header" do %> <% content_for "header" do %>
<meta name="description" content="<%= I18n.translate(locale, "An alternative front-end to YouTube") %>"> <meta name="description" content="<%= translate(locale, "An alternative front-end to YouTube") %>">
<title> <title>
<% if env.get("preferences").as(Preferences).default_home != "Popular" %> <% if env.get("preferences").as(Preferences).default_home != "Popular" %>
<%= I18n.translate(locale, "Popular") %> - Invidious <%= translate(locale, "Popular") %> - Invidious
<% else %> <% else %>
Invidious Invidious
<% end %> <% end %>

View File

@ -1,5 +1,5 @@
<% content_for "header" do %> <% content_for "header" do %>
<title><%= I18n.translate(locale, "Subscriptions") %> - Invidious</title> <title><%= translate(locale, "Subscriptions") %> - Invidious</title>
<link rel="alternate" type="application/rss+xml" title="RSS" href="/feed/private?token=<%= token %>" /> <link rel="alternate" type="application/rss+xml" title="RSS" href="/feed/private?token=<%= token %>" />
<% end %> <% end %>
@ -8,12 +8,12 @@
<div class="pure-g h-box"> <div class="pure-g h-box">
<div class="pure-u-1-3"> <div class="pure-u-1-3">
<h3> <h3>
<a href="/subscription_manager"><%= I18n.translate(locale, "Manage subscriptions") %></a> <a href="/subscription_manager"><%= translate(locale, "Manage subscriptions") %></a>
</h3> </h3>
</div> </div>
<div class="pure-u-1-3"> <div class="pure-u-1-3">
<h3 style="text-align:center"> <h3 style="text-align:center">
<a href="/feed/history"><%= I18n.translate(locale, "Watch history") %></a> <a href="/feed/history"><%= translate(locale, "Watch history") %></a>
</h3> </h3>
</div> </div>
<div class="pure-u-1-3"> <div class="pure-u-1-3">
@ -26,7 +26,7 @@
<% if CONFIG.enable_user_notifications %> <% if CONFIG.enable_user_notifications %>
<center> <center>
<%= I18n.translate_count(locale, "subscriptions_unseen_notifs_count", notifications.size) %> <%= translate_count(locale, "subscriptions_unseen_notifs_count", notifications.size) %>
</center> </center>
<% if !notifications.empty? %> <% if !notifications.empty? %>

View File

@ -1,8 +1,8 @@
<% content_for "header" do %> <% content_for "header" do %>
<meta name="description" content="<%= I18n.translate(locale, "An alternative front-end to YouTube") %>"> <meta name="description" content="<%= translate(locale, "An alternative front-end to YouTube") %>">
<title> <title>
<% if env.get("preferences").as(Preferences).default_home != "Trending" %> <% if env.get("preferences").as(Preferences).default_home != "Trending" %>
<%= I18n.translate(locale, "Trending") %> - Invidious <%= translate(locale, "Trending") %> - Invidious
<% else %> <% else %>
Invidious Invidious
<% end %> <% end %>
@ -15,7 +15,7 @@
<div style="align-self:flex-end" class="pure-u-2-3"> <div style="align-self:flex-end" class="pure-u-2-3">
<% if plid %> <% if plid %>
<a href="/playlist?list=<%= plid %>"> <a href="/playlist?list=<%= plid %>">
<%= I18n.translate(locale, "View as playlist") %> <%= translate(locale, "View as playlist") %>
</a> </a>
<% end %> <% end %>
</div> </div>
@ -24,10 +24,10 @@
<% {"Default", "Music", "Gaming", "Movies"}.each do |option| %> <% {"Default", "Music", "Gaming", "Movies"}.each do |option| %>
<div class="pure-u-1 pure-md-1-3"> <div class="pure-u-1 pure-md-1-3">
<% if trending_type == option %> <% if trending_type == option %>
<b><%= I18n.translate(locale, option) %></b> <b><%= translate(locale, option) %></b>
<% else %> <% else %>
<a href="/feed/trending?type=<%= option %>&region=<%= region %>"> <a href="/feed/trending?type=<%= option %>&region=<%= region %>">
<%= I18n.translate(locale, option) %> <%= translate(locale, option) %>
</a> </a>
<% end %> <% end %>
</div> </div>

View File

@ -7,7 +7,7 @@
</head> </head>
<body> <body>
<h1><%= I18n.translate(locale, "JavaScript license information") %></h1> <h1><%= translate(locale, "JavaScript license information") %></h1>
<table id="jslicense-labels1"> <table id="jslicense-labels1">
<tr> <tr>
<td> <td>
@ -19,7 +19,7 @@
</td> </td>
<td> <td>
<a href="https://github.com/iv-org/videojs-quality-selector"><%= I18n.translate(locale, "source") %></a> <a href="https://github.com/iv-org/videojs-quality-selector"><%= translate(locale, "source") %></a>
</td> </td>
</tr> </tr>
@ -33,7 +33,7 @@
</td> </td>
<td> <td>
<a href="https://github.com/mpetazzoni/sse.js"><%= I18n.translate(locale, "source") %></a> <a href="https://github.com/mpetazzoni/sse.js"><%= translate(locale, "source") %></a>
</td> </td>
</tr> </tr>
@ -47,7 +47,7 @@
</td> </td>
<td> <td>
<a href="https://github.com/videojs/videojs-contrib-quality-levels"><%= I18n.translate(locale, "source") %></a> <a href="https://github.com/videojs/videojs-contrib-quality-levels"><%= translate(locale, "source") %></a>
</td> </td>
</tr> </tr>
@ -61,7 +61,7 @@
</td> </td>
<td> <td>
<a href="https://github.com/jfujita/videojs-http-source-selector"><%= I18n.translate(locale, "source") %></a> <a href="https://github.com/jfujita/videojs-http-source-selector"><%= translate(locale, "source") %></a>
</td> </td>
</tr> </tr>
@ -75,7 +75,7 @@
</td> </td>
<td> <td>
<a href="https://github.com/mister-ben/videojs-mobile-ui"><%= I18n.translate(locale, "source") %></a> <a href="https://github.com/mister-ben/videojs-mobile-ui"><%= translate(locale, "source") %></a>
</td> </td>
</tr> </tr>
@ -89,7 +89,7 @@
</td> </td>
<td> <td>
<a href="https://github.com/spchuang/videojs-markers"><%= I18n.translate(locale, "source") %></a> <a href="https://github.com/spchuang/videojs-markers"><%= translate(locale, "source") %></a>
</td> </td>
</tr> </tr>
@ -103,7 +103,7 @@
</td> </td>
<td> <td>
<a href="https://github.com/brightcove/videojs-overlay"><%= I18n.translate(locale, "source") %></a> <a href="https://github.com/brightcove/videojs-overlay"><%= translate(locale, "source") %></a>
</td> </td>
</tr> </tr>
@ -117,7 +117,7 @@
</td> </td>
<td> <td>
<a href="https://github.com/mkhazov/videojs-share"><%= I18n.translate(locale, "source") %></a> <a href="https://github.com/mkhazov/videojs-share"><%= translate(locale, "source") %></a>
</td> </td>
</tr> </tr>
@ -131,7 +131,7 @@
</td> </td>
<td> <td>
<a href="https://github.com/chrisboustead/videojs-vtt-thumbnails"><%= I18n.translate(locale, "source") %></a> <a href="https://github.com/chrisboustead/videojs-vtt-thumbnails"><%= translate(locale, "source") %></a>
</td> </td>
</tr> </tr>
@ -145,7 +145,7 @@
</td> </td>
<td> <td>
<a href="https://github.com/afrmtbl/videojs-youtube-annotations"><%= I18n.translate(locale, "source") %></a> <a href="https://github.com/afrmtbl/videojs-youtube-annotations"><%= translate(locale, "source") %></a>
</td> </td>
</tr> </tr>
@ -159,7 +159,7 @@
</td> </td>
<td> <td>
<a href="https://github.com/videojs/videojs-vr"><%= I18n.translate(locale, "source") %></a> <a href="https://github.com/videojs/videojs-vr"><%= translate(locale, "source") %></a>
</td> </td>
</tr> </tr>
@ -173,7 +173,7 @@
</td> </td>
<td> <td>
<a href="https://github.com/videojs/video.js"><%= I18n.translate(locale, "source") %></a> <a href="https://github.com/videojs/video.js"><%= translate(locale, "source") %></a>
</td> </td>
</tr> </tr>

View File

@ -1,5 +1,5 @@
<% content_for "header" do %> <% content_for "header" do %>
<meta name="description" content="<%= I18n.translate(locale, "An alternative front-end to YouTube") %>"> <meta name="description" content="<%= translate(locale, "An alternative front-end to YouTube") %>">
<title> <title>
Invidious Invidious
</title> </title>

View File

@ -13,28 +13,28 @@
<%- if playlist.is_a?(InvidiousPlaylist) && playlist.author == user.try &.email -%> <%- if playlist.is_a?(InvidiousPlaylist) && playlist.author == user.try &.email -%>
<div class="pure-u"> <div class="pure-u">
<a class="pure-button pure-button-secondary low-profile" dir="auto" href="/add_playlist_items?list=<%= plid %>"> <a class="pure-button pure-button-secondary low-profile" dir="auto" href="/add_playlist_items?list=<%= plid %>">
<i class="icon ion-md-add"></i>&nbsp;<%= I18n.translate(locale, "playlist_button_add_items") %> <i class="icon ion-md-add"></i>&nbsp;<%= translate(locale, "playlist_button_add_items") %>
</a> </a>
</div> </div>
<div class="pure-u"> <div class="pure-u">
<a class="pure-button pure-button-secondary low-profile" dir="auto" href="/edit_playlist?list=<%= plid %>"> <a class="pure-button pure-button-secondary low-profile" dir="auto" href="/edit_playlist?list=<%= plid %>">
<i class="icon ion-md-create"></i>&nbsp;<%= I18n.translate(locale, "generic_button_edit") %> <i class="icon ion-md-create"></i>&nbsp;<%= translate(locale, "generic_button_edit") %>
</a> </a>
</div> </div>
<div class="pure-u"> <div class="pure-u">
<a class="pure-button pure-button-secondary low-profile" dir="auto" href="/delete_playlist?list=<%= plid %>"> <a class="pure-button pure-button-secondary low-profile" dir="auto" href="/delete_playlist?list=<%= plid %>">
<i class="icon ion-md-trash"></i>&nbsp;<%= I18n.translate(locale, "generic_button_delete") %> <i class="icon ion-md-trash"></i>&nbsp;<%= translate(locale, "generic_button_delete") %>
</a> </a>
</div> </div>
<%- else -%> <%- else -%>
<div class="pure-u"> <div class="pure-u">
<%- if IV::Database::Playlists.exists?(playlist.id) -%> <%- if IV::Database::Playlists.exists?(playlist.id) -%>
<a class="pure-button pure-button-secondary low-profile" dir="auto" href="/subscribe_playlist?list=<%= plid %>"> <a class="pure-button pure-button-secondary low-profile" dir="auto" href="/subscribe_playlist?list=<%= plid %>">
<i class="icon ion-md-add"></i>&nbsp;<%= I18n.translate(locale, "Subscribe") %> <i class="icon ion-md-add"></i>&nbsp;<%= translate(locale, "Subscribe") %>
</a> </a>
<%- else -%> <%- else -%>
<a class="pure-button pure-button-secondary low-profile" dir="auto" href="/delete_playlist?list=<%= plid %>"> <a class="pure-button pure-button-secondary low-profile" dir="auto" href="/delete_playlist?list=<%= plid %>">
<i class="icon ion-md-trash"></i>&nbsp;<%= I18n.translate(locale, "Unsubscribe") %> <i class="icon ion-md-trash"></i>&nbsp;<%= translate(locale, "Unsubscribe") %>
</a> </a>
<%- end -%> <%- end -%>
</div> </div>
@ -42,7 +42,7 @@
<div class="pure-u"> <div class="pure-u">
<a class="pure-button pure-button-secondary low-profile" dir="auto" href="/feed/playlist/<%= plid %>"> <a class="pure-button pure-button-secondary low-profile" dir="auto" href="/feed/playlist/<%= plid %>">
<i class="icon ion-logo-rss"></i>&nbsp;<%= I18n.translate(locale, "generic_button_rss") %> <i class="icon ion-logo-rss"></i>&nbsp;<%= translate(locale, "generic_button_rss") %>
</a> </a>
</div> </div>
</div> </div>
@ -57,15 +57,15 @@
<% else %> <% else %>
<%= author %> | <%= author %> |
<% end %> <% end %>
<%= I18n.translate_count(locale, "generic_videos_count", playlist.video_count) %> | <%= translate_count(locale, "generic_videos_count", playlist.video_count) %> |
<%= I18n.translate(locale, "Updated `x` ago", recode_date(playlist.updated, locale)) %> | <%= translate(locale, "Updated `x` ago", recode_date(playlist.updated, locale)) %> |
<% case playlist.as(InvidiousPlaylist).privacy when %> <% case playlist.as(InvidiousPlaylist).privacy when %>
<% when PlaylistPrivacy::Public %> <% when PlaylistPrivacy::Public %>
<i class="icon ion-md-globe"></i> <%= I18n.translate(locale, "Public") %> <i class="icon ion-md-globe"></i> <%= translate(locale, "Public") %>
<% when PlaylistPrivacy::Unlisted %> <% when PlaylistPrivacy::Unlisted %>
<i class="icon ion-ios-unlock"></i> <%= I18n.translate(locale, "Unlisted") %> <i class="icon ion-ios-unlock"></i> <%= translate(locale, "Unlisted") %>
<% when PlaylistPrivacy::Private %> <% when PlaylistPrivacy::Private %>
<i class="icon ion-ios-lock"></i> <%= I18n.translate(locale, "Private") %> <i class="icon ion-ios-lock"></i> <%= translate(locale, "Private") %>
<% end %> <% end %>
</b> </b>
<% else %> <% else %>
@ -76,25 +76,25 @@
<% subtitle = playlist.subtitle || "" %> <% subtitle = playlist.subtitle || "" %>
<span><%= HTML.escape(subtitle[0..subtitle.rindex(" • ") || subtitle.size]) %></span> | <span><%= HTML.escape(subtitle[0..subtitle.rindex(" • ") || subtitle.size]) %></span> |
<% end %> <% end %>
<%= I18n.translate_count(locale, "generic_videos_count", playlist.video_count) %> | <%= translate_count(locale, "generic_videos_count", playlist.video_count) %> |
<%= I18n.translate(locale, "Updated `x` ago", recode_date(playlist.updated, locale)) %> <%= translate(locale, "Updated `x` ago", recode_date(playlist.updated, locale)) %>
</b> </b>
<% end %> <% end %>
<% if !playlist.is_a? InvidiousPlaylist %> <% if !playlist.is_a? InvidiousPlaylist %>
<div class="pure-u-2-3"> <div class="pure-u-2-3">
<a rel="noreferrer noopener" href="https://www.youtube.com/playlist?list=<%= playlist.id %>"> <a rel="noreferrer noopener" href="https://www.youtube.com/playlist?list=<%= playlist.id %>">
<%= I18n.translate(locale, "View playlist on YouTube") %> <%= translate(locale, "View playlist on YouTube") %>
</a> </a>
<span> | </span> <span> | </span>
<% if env.get("preferences").as(Preferences).automatic_instance_redirect%> <% if env.get("preferences").as(Preferences).automatic_instance_redirect%>
<a href="/redirect?referer=<%= env.get?("current_page") %>"> <a href="/redirect?referer=<%= env.get?("current_page") %>">
<%= I18n.translate(locale, "Switch Invidious Instance") %> <%= translate(locale, "Switch Invidious Instance") %>
</a> </a>
<% else %> <% else %>
<a href="https://redirect.invidious.io/playlist?list=<%= playlist.id %>"> <a href="https://redirect.invidious.io/playlist?list=<%= playlist.id %>">
<%= I18n.translate(locale, "Switch Invidious Instance") %> <%= translate(locale, "Switch Invidious Instance") %>
</a> </a>
<% end %> <% end %>
</div> </div>

View File

@ -18,7 +18,7 @@
<% else %> <% else %>
<noscript> <noscript>
<a href="/post/<%= id %>?ucid=<%= ucid %>&nojs=1"> <a href="/post/<%= id %>?ucid=<%= ucid %>&nojs=1">
<%= I18n.translate(locale, "Hi! Looks like you have JavaScript turned off. Click here to view comments, keep in mind they may take a bit longer to load.") %> <%= translate(locale, "Hi! Looks like you have JavaScript turned off. Click here to view comments, keep in mind they may take a bit longer to load.") %>
</a> </a>
</noscript> </noscript>
<% end %> <% end %>
@ -29,12 +29,12 @@
<%= <%=
{ {
"id" => id, "id" => id,
"youtube_comments_text" => HTML.escape(I18n.translate(locale, "View YouTube comments")), "youtube_comments_text" => HTML.escape(translate(locale, "View YouTube comments")),
"reddit_comments_text" => "", "reddit_comments_text" => "",
"reddit_permalink_text" => "", "reddit_permalink_text" => "",
"comments_text" => HTML.escape(I18n.translate(locale, "View `x` comments", "{commentCount}")), "comments_text" => HTML.escape(translate(locale, "View `x` comments", "{commentCount}")),
"hide_replies_text" => HTML.escape(I18n.translate(locale, "Hide replies")), "hide_replies_text" => HTML.escape(translate(locale, "Hide replies")),
"show_replies_text" => HTML.escape(I18n.translate(locale, "Show replies")), "show_replies_text" => HTML.escape(translate(locale, "Show replies")),
"params" => { "params" => {
"comments": ["youtube"] "comments": ["youtube"]
}, },

View File

@ -11,9 +11,9 @@
<%- if items.empty? -%> <%- if items.empty? -%>
<div class="h-box no-results-error"> <div class="h-box no-results-error">
<div> <div>
<%= I18n.translate(locale, "search_message_no_results") %><br/><br/> <%= translate(locale, "search_message_no_results") %><br/><br/>
<%= I18n.translate(locale, "search_message_change_filters_or_query") %><br/><br/> <%= translate(locale, "search_message_change_filters_or_query") %><br/><br/>
<%= I18n.translate(locale, "search_message_use_another_instance", redirect_url) %> <%= translate(locale, "search_message_use_another_instance", redirect_url) %>
</div> </div>
</div> </div>
<%- else -%> <%- else -%>

View File

@ -1,7 +1,7 @@
<% content_for "header" do %> <% content_for "header" do %>
<meta name="description" content="<%= I18n.translate(locale, "An alternative front-end to YouTube") %>"> <meta name="description" content="<%= translate(locale, "An alternative front-end to YouTube") %>">
<title> <title>
Invidious - <%= I18n.translate(locale, "search") %> Invidious - <%= translate(locale, "search") %>
</title> </title>
<link rel="stylesheet" href="/css/empty.css?v=<%= ASSET_COMMIT %>"> <link rel="stylesheet" href="/css/empty.css?v=<%= ASSET_COMMIT %>">
<% end %> <% end %>

View File

@ -42,7 +42,7 @@
<div class="pure-u-1 pure-u-md-8-24 user-field"> <div class="pure-u-1 pure-u-md-8-24 user-field">
<% if env.get? "user" %> <% if env.get? "user" %>
<div class="pure-u-1-4"> <div class="pure-u-1-4">
<a id="toggle_theme" href="/toggle_theme?referer=<%= env.get?("current_page") %>" class="pure-menu-heading" title="<%= I18n.translate(locale, "toggle_theme") %>"> <a id="toggle_theme" href="/toggle_theme?referer=<%= env.get?("current_page") %>" class="pure-menu-heading" title="<%= translate(locale, "toggle_theme") %>">
<% if dark_mode == "dark" %> <% if dark_mode == "dark" %>
<i class="icon ion-ios-sunny"></i> <i class="icon ion-ios-sunny"></i>
<% else %> <% else %>
@ -51,7 +51,7 @@
</a> </a>
</div> </div>
<div class="pure-u-1-4"> <div class="pure-u-1-4">
<a id="notification_ticker" title="<%= I18n.translate(locale, "Subscriptions") %>" href="/feed/subscriptions" class="pure-menu-heading"> <a id="notification_ticker" title="<%= translate(locale, "Subscriptions") %>" href="/feed/subscriptions" class="pure-menu-heading">
<% notification_count = env.get("user").as(Invidious::User).notifications.size %> <% notification_count = env.get("user").as(Invidious::User).notifications.size %>
<% if CONFIG.enable_user_notifications && notification_count > 0 %> <% if CONFIG.enable_user_notifications && notification_count > 0 %>
<span id="notification_count"><%= notification_count %></span> <i class="icon ion-ios-notifications"></i> <span id="notification_count"><%= notification_count %></span> <i class="icon ion-ios-notifications"></i>
@ -61,7 +61,7 @@
</a> </a>
</div> </div>
<div class="pure-u-1-4"> <div class="pure-u-1-4">
<a title="<%= I18n.translate(locale, "Preferences") %>" href="/preferences?referer=<%= env.get?("current_page") %>" class="pure-menu-heading"> <a title="<%= translate(locale, "Preferences") %>" href="/preferences?referer=<%= env.get?("current_page") %>" class="pure-menu-heading">
<i class="icon ion-ios-cog"></i> <i class="icon ion-ios-cog"></i>
</a> </a>
</div> </div>
@ -74,13 +74,13 @@
<form action="/signout?referer=<%= env.get?("current_page") %>" method="post"> <form action="/signout?referer=<%= env.get?("current_page") %>" method="post">
<input type="hidden" name="csrf_token" value="<%= HTML.escape(env.get?("csrf_token").try &.as(String) || "") %>"> <input type="hidden" name="csrf_token" value="<%= HTML.escape(env.get?("csrf_token").try &.as(String) || "") %>">
<a class="pure-menu-heading" href="#"> <a class="pure-menu-heading" href="#">
<input style="all:unset" type="submit" value="<%= I18n.translate(locale, "Log out") %>"> <input style="all:unset" type="submit" value="<%= translate(locale, "Log out") %>">
</a> </a>
</form> </form>
</div> </div>
<% else %> <% else %>
<div class="pure-u-1-3"> <div class="pure-u-1-3">
<a id="toggle_theme" href="/toggle_theme?referer=<%= env.get?("current_page") %>" class="pure-menu-heading" title="<%= I18n.translate(locale, "toggle_theme") %>"> <a id="toggle_theme" href="/toggle_theme?referer=<%= env.get?("current_page") %>" class="pure-menu-heading" title="<%= translate(locale, "toggle_theme") %>">
<% if dark_mode == "dark" %> <% if dark_mode == "dark" %>
<i class="icon ion-ios-sunny"></i> <i class="icon ion-ios-sunny"></i>
<% else %> <% else %>
@ -89,14 +89,14 @@
</a> </a>
</div> </div>
<div class="pure-u-1-3"> <div class="pure-u-1-3">
<a title="<%= I18n.translate(locale, "Preferences") %>" href="/preferences?referer=<%= env.get?("current_page") %>" class="pure-menu-heading"> <a title="<%= translate(locale, "Preferences") %>" href="/preferences?referer=<%= env.get?("current_page") %>" class="pure-menu-heading">
<i class="icon ion-ios-cog"></i> <i class="icon ion-ios-cog"></i>
</a> </a>
</div> </div>
<% if CONFIG.login_enabled %> <% if CONFIG.login_enabled %>
<div class="pure-u-1-3"> <div class="pure-u-1-3">
<a href="/login?referer=<%= env.get?("current_page") %>" class="pure-menu-heading"> <a href="/login?referer=<%= env.get?("current_page") %>" class="pure-menu-heading">
<%= I18n.translate(locale, "Log in") %> <%= translate(locale, "Log in") %>
</a> </a>
</div> </div>
<% end %> <% end %>
@ -118,38 +118,38 @@
<span> <span>
<i class="icon ion-logo-github"></i> <i class="icon ion-logo-github"></i>
<% if CONFIG.modified_source_code_url %> <% if CONFIG.modified_source_code_url %>
<a href="https://github.com/iv-org/invidious"><%= I18n.translate(locale, "footer_original_source_code") %></a>&nbsp;/ <a href="https://github.com/iv-org/invidious"><%= translate(locale, "footer_original_source_code") %></a>&nbsp;/
<a href="<%= CONFIG.modified_source_code_url %>"><%= I18n.translate(locale, "footer_modfied_source_code") %></a> <a href="<%= CONFIG.modified_source_code_url %>"><%= translate(locale, "footer_modfied_source_code") %></a>
<% else %> <% else %>
<a href="https://github.com/iv-org/invidious"><%= I18n.translate(locale, "footer_source_code") %></a> <a href="https://github.com/iv-org/invidious"><%= translate(locale, "footer_source_code") %></a>
<% end %> <% end %>
</span> </span>
<span> <span>
<i class="icon ion-ios-paper"></i> <i class="icon ion-ios-paper"></i>
<a href="https://github.com/iv-org/documentation"><%= I18n.translate(locale, "footer_documentation") %></a> <a href="https://github.com/iv-org/documentation"><%= translate(locale, "footer_documentation") %></a>
</span> </span>
</div> </div>
<div class="pure-u-1 pure-u-md-1-3"> <div class="pure-u-1 pure-u-md-1-3">
<span> <span>
<a href="https://github.com/iv-org/invidious/blob/master/LICENSE"><%= I18n.translate(locale, "Released under the AGPLv3 on Github.") %></a> <a href="https://github.com/iv-org/invidious/blob/master/LICENSE"><%= translate(locale, "Released under the AGPLv3 on Github.") %></a>
</span> </span>
<span> <span>
<i class="icon ion-logo-javascript"></i> <i class="icon ion-logo-javascript"></i>
<a rel="jslicense" href="/licenses"><%= I18n.translate(locale, "View JavaScript license information.") %></a> <a rel="jslicense" href="/licenses"><%= translate(locale, "View JavaScript license information.") %></a>
</span> </span>
<span> <span>
<i class="icon ion-ios-paper"></i> <i class="icon ion-ios-paper"></i>
<a href="/privacy"><%= I18n.translate(locale, "View privacy policy.") %></a> <a href="/privacy"><%= translate(locale, "View privacy policy.") %></a>
</span> </span>
</div> </div>
<div class="pure-u-1 pure-u-md-1-3"> <div class="pure-u-1 pure-u-md-1-3">
<span> <span>
<i class="icon ion-ios-wallet"></i> <i class="icon ion-ios-wallet"></i>
<a href="https://invidious.io/donate/"><%= I18n.translate(locale, "footer_donate_page") %></a> <a href="https://invidious.io/donate/"><%= translate(locale, "footer_donate_page") %></a>
</span> </span>
<span><%= I18n.translate(locale, "Current version: ") %> <%= CURRENT_VERSION %>-<%= CURRENT_COMMIT %> @ <%= CURRENT_BRANCH %></span> <span><%= translate(locale, "Current version: ") %> <%= CURRENT_VERSION %>-<%= CURRENT_COMMIT %> @ <%= CURRENT_BRANCH %></span>
</div> </div>
</div> </div>
</footer> </footer>
@ -163,8 +163,8 @@
<script id="notification_data" type="application/json"> <script id="notification_data" type="application/json">
<%= <%=
{ {
"upload_text" => HTML.escape(I18n.translate(locale, "`x` uploaded a video")), "upload_text" => HTML.escape(translate(locale, "`x` uploaded a video")),
"live_upload_text" => HTML.escape(I18n.translate(locale, "`x` is live")) "live_upload_text" => HTML.escape(translate(locale, "`x` is live"))
}.to_pretty_json }.to_pretty_json
%> %>
</script> </script>

View File

@ -1,22 +1,22 @@
<% content_for "header" do %> <% content_for "header" do %>
<title><%= I18n.translate(locale, "Token") %> - Invidious</title> <title><%= translate(locale, "Token") %> - Invidious</title>
<% end %> <% end %>
<% if env.get? "access_token" %> <% if env.get? "access_token" %>
<div class="pure-g h-box"> <div class="pure-g h-box">
<div class="pure-u-1-3"> <div class="pure-u-1-3">
<h3> <h3>
<%= I18n.translate(locale, "Token") %> <%= translate(locale, "Token") %>
</h3> </h3>
</div> </div>
<div class="pure-u-1-3"> <div class="pure-u-1-3">
<h3 style="text-align:center"> <h3 style="text-align:center">
<a href="/token_manager"><%= I18n.translate(locale, "Token manager") %></a> <a href="/token_manager"><%= translate(locale, "Token manager") %></a>
</h3> </h3>
</div> </div>
<div class="pure-u-1-3"> <div class="pure-u-1-3">
<h3 style="text-align:right"> <h3 style="text-align:right">
<a href="/preferences"><%= I18n.translate(locale, "Preferences") %></a> <a href="/preferences"><%= translate(locale, "Preferences") %></a>
</h3> </h3>
</div> </div>
</div> </div>
@ -30,9 +30,9 @@
<div class="h-box"> <div class="h-box">
<form class="pure-form pure-form-aligned" action="/authorize_token" method="post"> <form class="pure-form pure-form-aligned" action="/authorize_token" method="post">
<% if callback_url %> <% if callback_url %>
<legend><%= I18n.translate(locale, "Authorize token for `x`?", "#{callback_url.scheme}://#{callback_url.host}") %></legend> <legend><%= translate(locale, "Authorize token for `x`?", "#{callback_url.scheme}://#{callback_url.host}") %></legend>
<% else %> <% else %>
<legend><%= I18n.translate(locale, "Authorize token?") %></legend> <legend><%= translate(locale, "Authorize token?") %></legend>
<% end %> <% end %>
<div class="pure-g"> <div class="pure-g">
@ -48,7 +48,7 @@
<div class="pure-g"> <div class="pure-g">
<div class="pure-u-1-2"> <div class="pure-u-1-2">
<button type="submit" name="submit" value="clear_watch_history" class="pure-button pure-button-primary"> <button type="submit" name="submit" value="clear_watch_history" class="pure-button pure-button-primary">
<%= I18n.translate(locale, "Yes") %> <%= translate(locale, "Yes") %>
</button> </button>
</div> </div>
<div class="pure-u-1-2"> <div class="pure-u-1-2">
@ -57,7 +57,7 @@
<% else %> <% else %>
<a class="pure-button" href="/"> <a class="pure-button" href="/">
<% end %> <% end %>
<%= I18n.translate(locale, "No") %> <%= translate(locale, "No") %>
</a> </a>
</div> </div>
</div> </div>

View File

@ -1,5 +1,5 @@
<% content_for "header" do %> <% content_for "header" do %>
<title><%= I18n.translate(locale, "Change password") %> - Invidious</title> <title><%= translate(locale, "Change password") %> - Invidious</title>
<% end %> <% end %>
<div class="pure-g"> <div class="pure-g">
@ -7,20 +7,20 @@
<div class="pure-u-1 pure-u-lg-3-5"> <div class="pure-u-1 pure-u-lg-3-5">
<div class="h-box"> <div class="h-box">
<form class="pure-form pure-form-aligned" action="/change_password?referer=<%= URI.encode_www_form(referer) %>" method="post"> <form class="pure-form pure-form-aligned" action="/change_password?referer=<%= URI.encode_www_form(referer) %>" method="post">
<legend><%= I18n.translate(locale, "Change password") %></legend> <legend><%= translate(locale, "Change password") %></legend>
<fieldset> <fieldset>
<label for="password"><%= I18n.translate(locale, "Password") %> :</label> <label for="password"><%= translate(locale, "Password") %> :</label>
<input required class="pure-input-1" name="password" type="password" placeholder="<%= I18n.translate(locale, "Password") %>"> <input required class="pure-input-1" name="password" type="password" placeholder="<%= translate(locale, "Password") %>">
<label for="new_password[0]"><%= I18n.translate(locale, "New password") %> :</label> <label for="new_password[0]"><%= translate(locale, "New password") %> :</label>
<input required class="pure-input-1" name="new_password[0]" type="password" placeholder="<%= I18n.translate(locale, "New password") %>"> <input required class="pure-input-1" name="new_password[0]" type="password" placeholder="<%= translate(locale, "New password") %>">
<label for="new_password[1]"><%= I18n.translate(locale, "New password") %> :</label> <label for="new_password[1]"><%= translate(locale, "New password") %> :</label>
<input required class="pure-input-1" name="new_password[1]" type="password" placeholder="<%= I18n.translate(locale, "New password") %>"> <input required class="pure-input-1" name="new_password[1]" type="password" placeholder="<%= translate(locale, "New password") %>">
<button type="submit" name="action" value="change_password" class="pure-button pure-button-primary"> <button type="submit" name="action" value="change_password" class="pure-button pure-button-primary">
<%= I18n.translate(locale, "Change password") %> <%= translate(locale, "Change password") %>
</button> </button>
<input type="hidden" name="csrf_token" value="<%= HTML.escape(csrf_token) %>"> <input type="hidden" name="csrf_token" value="<%= HTML.escape(csrf_token) %>">

View File

@ -1,20 +1,20 @@
<% content_for "header" do %> <% content_for "header" do %>
<title><%= I18n.translate(locale, "Clear watch history") %> - Invidious</title> <title><%= translate(locale, "Clear watch history") %> - Invidious</title>
<% end %> <% end %>
<div class="h-box"> <div class="h-box">
<form class="pure-form pure-form-aligned" action="/clear_watch_history?referer=<%= URI.encode_www_form(referer) %>" method="post"> <form class="pure-form pure-form-aligned" action="/clear_watch_history?referer=<%= URI.encode_www_form(referer) %>" method="post">
<legend><%= I18n.translate(locale, "Clear watch history?") %></legend> <legend><%= translate(locale, "Clear watch history?") %></legend>
<div class="pure-g"> <div class="pure-g">
<div class="pure-u-1-2"> <div class="pure-u-1-2">
<button type="submit" name="submit" value="clear_watch_history" class="pure-button pure-button-primary"> <button type="submit" name="submit" value="clear_watch_history" class="pure-button pure-button-primary">
<%= I18n.translate(locale, "Yes") %> <%= translate(locale, "Yes") %>
</button> </button>
</div> </div>
<div class="pure-u-1-2"> <div class="pure-u-1-2">
<a class="pure-button" href="<%= URI.encode_www_form(referer) %>"> <a class="pure-button" href="<%= URI.encode_www_form(referer) %>">
<%= I18n.translate(locale, "No") %> <%= translate(locale, "No") %>
</a> </a>
</div> </div>
</div> </div>

View File

@ -1,67 +1,67 @@
<% content_for "header" do %> <% content_for "header" do %>
<title><%= I18n.translate(locale, "Import and Export Data") %> - Invidious</title> <title><%= translate(locale, "Import and Export Data") %> - Invidious</title>
<% end %> <% end %>
<div class="h-box"> <div class="h-box">
<form class="pure-form pure-form-aligned" enctype="multipart/form-data" action="/data_control?referer=<%= URI.encode_www_form(referer) %>" method="post"> <form class="pure-form pure-form-aligned" enctype="multipart/form-data" action="/data_control?referer=<%= URI.encode_www_form(referer) %>" method="post">
<fieldset> <fieldset>
<legend><%= I18n.translate(locale, "Import") %></legend> <legend><%= translate(locale, "Import") %></legend>
<div class="pure-control-group"> <div class="pure-control-group">
<label for="import_invidious"><%= I18n.translate(locale, "Import Invidious data") %></label> <label for="import_invidious"><%= translate(locale, "Import Invidious data") %></label>
<input type="file" id="import_invidious" name="import_invidious"> <input type="file" id="import_invidious" name="import_invidious">
</div> </div>
<div class="pure-control-group"> <div class="pure-control-group">
<label for="import_youtube"> <label for="import_youtube">
<a rel="noopener noreferrer" target="_blank" href="https://github.com/iv-org/documentation/blob/master/docs/export-youtube-subscriptions.md"> <a rel="noopener noreferrer" target="_blank" href="https://github.com/iv-org/documentation/blob/master/docs/export-youtube-subscriptions.md">
<%= I18n.translate(locale, "Import YouTube subscriptions") %> <%= translate(locale, "Import YouTube subscriptions") %>
</a> </a>
</label> </label>
<input type="file" id="import_youtube" name="import_youtube"> <input type="file" id="import_youtube" name="import_youtube">
</div> </div>
<div class="pure-control-group"> <div class="pure-control-group">
<label for="import_youtube_pl"><%= I18n.translate(locale, "Import YouTube playlist (.csv)") %></label> <label for="import_youtube_pl"><%= translate(locale, "Import YouTube playlist (.csv)") %></label>
<input type="file" id="import_youtube_pl" name="import_youtube_pl"> <input type="file" id="import_youtube_pl" name="import_youtube_pl">
</div> </div>
<div class="pure-control-group"> <div class="pure-control-group">
<label for="import_youtube_wh"><%= I18n.translate(locale, "Import YouTube watch history (.json)") %></label> <label for="import_youtube_wh"><%= translate(locale, "Import YouTube watch history (.json)") %></label>
<input type="file" id="import_youtube_wh" name="import_youtube_wh"> <input type="file" id="import_youtube_wh" name="import_youtube_wh">
</div> </div>
<div class="pure-control-group"> <div class="pure-control-group">
<label for="import_freetube"><%= I18n.translate(locale, "Import FreeTube subscriptions (.db)") %></label> <label for="import_freetube"><%= translate(locale, "Import FreeTube subscriptions (.db)") %></label>
<input type="file" id="import_freetube" name="import_freetube"> <input type="file" id="import_freetube" name="import_freetube">
</div> </div>
<div class="pure-control-group"> <div class="pure-control-group">
<label for="import_newpipe_subscriptions"><%= I18n.translate(locale, "Import NewPipe subscriptions (.json)") %></label> <label for="import_newpipe_subscriptions"><%= translate(locale, "Import NewPipe subscriptions (.json)") %></label>
<input type="file" id="import_newpipe_subscriptions" name="import_newpipe_subscriptions"> <input type="file" id="import_newpipe_subscriptions" name="import_newpipe_subscriptions">
</div> </div>
<div class="pure-control-group"> <div class="pure-control-group">
<label for="import_newpipe"><%= I18n.translate(locale, "Import NewPipe data (.zip)") %></label> <label for="import_newpipe"><%= translate(locale, "Import NewPipe data (.zip)") %></label>
<input type="file" id="import_newpipe" name="import_newpipe"> <input type="file" id="import_newpipe" name="import_newpipe">
</div> </div>
<div class="pure-controls"> <div class="pure-controls">
<button type="submit" class="pure-button pure-button-primary"><%= I18n.translate(locale, "Import") %></button> <button type="submit" class="pure-button pure-button-primary"><%= translate(locale, "Import") %></button>
</div> </div>
<legend><%= I18n.translate(locale, "Export") %></legend> <legend><%= translate(locale, "Export") %></legend>
<div class="pure-control-group"> <div class="pure-control-group">
<a href="/subscription_manager?action_takeout=1"><%= I18n.translate(locale, "Export subscriptions as OPML") %></a> <a href="/subscription_manager?action_takeout=1"><%= translate(locale, "Export subscriptions as OPML") %></a>
</div> </div>
<div class="pure-control-group"> <div class="pure-control-group">
<a href="/subscription_manager?action_takeout=1&format=newpipe"><%= I18n.translate(locale, "Export subscriptions as OPML (for NewPipe & FreeTube)") %></a> <a href="/subscription_manager?action_takeout=1&format=newpipe"><%= translate(locale, "Export subscriptions as OPML (for NewPipe & FreeTube)") %></a>
</div> </div>
<div class="pure-control-group"> <div class="pure-control-group">
<a href="/subscription_manager?action_takeout=1&format=json"><%= I18n.translate(locale, "Export data as JSON") %></a> <a href="/subscription_manager?action_takeout=1&format=json"><%= translate(locale, "Export data as JSON") %></a>
</div> </div>
</fieldset> </fieldset>
</form> </form>

View File

@ -1,20 +1,20 @@
<% content_for "header" do %> <% content_for "header" do %>
<title><%= I18n.translate(locale, "Delete account") %> - Invidious</title> <title><%= translate(locale, "Delete account") %> - Invidious</title>
<% end %> <% end %>
<div class="h-box"> <div class="h-box">
<form class="pure-form pure-form-aligned" action="/delete_account?referer=<%= URI.encode_www_form(referer) %>" method="post"> <form class="pure-form pure-form-aligned" action="/delete_account?referer=<%= URI.encode_www_form(referer) %>" method="post">
<legend><%= I18n.translate(locale, "Delete account?") %></legend> <legend><%= translate(locale, "Delete account?") %></legend>
<div class="pure-g"> <div class="pure-g">
<div class="pure-u-1-2"> <div class="pure-u-1-2">
<button type="submit" name="submit" value="delete_account" class="pure-button pure-button-primary"> <button type="submit" name="submit" value="delete_account" class="pure-button pure-button-primary">
<%= I18n.translate(locale, "Yes") %> <%= translate(locale, "Yes") %>
</button> </button>
</div> </div>
<div class="pure-u-1-2"> <div class="pure-u-1-2">
<a class="pure-button" href="<%= URI.encode_www_form(referer) %>"> <a class="pure-button" href="<%= URI.encode_www_form(referer) %>">
<%= I18n.translate(locale, "No") %> <%= translate(locale, "No") %>
</a> </a>
</div> </div>
</div> </div>

View File

@ -1,5 +1,5 @@
<% content_for "header" do %> <% content_for "header" do %>
<title><%= I18n.translate(locale, "Log in") %> - Invidious</title> <title><%= translate(locale, "Log in") %> - Invidious</title>
<% end %> <% end %>
<div class="pure-g"> <div class="pure-g">
@ -13,15 +13,15 @@
<% if email %> <% if email %>
<input name="email" type="hidden" value="<%= HTML.escape(email) %>"> <input name="email" type="hidden" value="<%= HTML.escape(email) %>">
<% else %> <% else %>
<label for="email"><%= I18n.translate(locale, "User ID") %> :</label> <label for="email"><%= translate(locale, "User ID") %> :</label>
<input required class="pure-input-1" name="email" type="text" placeholder="<%= I18n.translate(locale, "User ID") %>"> <input required class="pure-input-1" name="email" type="text" placeholder="<%= translate(locale, "User ID") %>">
<% end %> <% end %>
<% if password %> <% if password %>
<input name="password" type="hidden" value="<%= HTML.escape(password) %>"> <input name="password" type="hidden" value="<%= HTML.escape(password) %>">
<% else %> <% else %>
<label for="password"><%= I18n.translate(locale, "Password") %> :</label> <label for="password"><%= translate(locale, "Password") %> :</label>
<input required class="pure-input-1" name="password" type="password" placeholder="<%= I18n.translate(locale, "Password") %>"> <input required class="pure-input-1" name="password" type="password" placeholder="<%= translate(locale, "Password") %>">
<% end %> <% end %>
<% if captcha %> <% if captcha %>
@ -30,15 +30,15 @@
<% captcha[:tokens].each_with_index do |token, i| %> <% captcha[:tokens].each_with_index do |token, i| %>
<input type="hidden" name="token[<%= i %>]" value="<%= HTML.escape(token) %>"> <input type="hidden" name="token[<%= i %>]" value="<%= HTML.escape(token) %>">
<% end %> <% end %>
<label for="answer"><%= I18n.translate(locale, "Time (h:mm:ss):") %></label> <label for="answer"><%= translate(locale, "Time (h:mm:ss):") %></label>
<input type="text" name="answer" type="text" placeholder="h:mm:ss"> <input type="text" name="answer" type="text" placeholder="h:mm:ss">
<button type="submit" name="action" value="signin" class="pure-button pure-button-primary"> <button type="submit" name="action" value="signin" class="pure-button pure-button-primary">
<%= I18n.translate(locale, "Register") %> <%= translate(locale, "Register") %>
</button> </button>
<% else %> <% else %>
<button type="submit" name="action" value="signin" class="pure-button pure-button-primary"> <button type="submit" name="action" value="signin" class="pure-button pure-button-primary">
<%= I18n.translate(locale, "Sign In") %>/<%= I18n.translate(locale, "Register") %> <%= translate(locale, "Sign In") %>/<%= translate(locale, "Register") %>
</button> </button>
<% end %> <% end %>
</fieldset> </fieldset>

View File

@ -1,49 +1,49 @@
<% content_for "header" do %> <% content_for "header" do %>
<title><%= I18n.translate(locale, "Preferences") %> - Invidious</title> <title><%= translate(locale, "Preferences") %> - Invidious</title>
<% end %> <% end %>
<div class="h-box"> <div class="h-box">
<form class="pure-form pure-form-aligned" action="/preferences?referer=<%= URI.encode_www_form(referer) %>" method="post"> <form class="pure-form pure-form-aligned" action="/preferences?referer=<%= URI.encode_www_form(referer) %>" method="post">
<fieldset> <fieldset>
<legend><%= I18n.translate(locale, "preferences_category_player") %></legend> <legend><%= translate(locale, "preferences_category_player") %></legend>
<div class="pure-control-group"> <div class="pure-control-group">
<label for="video_loop"><%= I18n.translate(locale, "preferences_video_loop_label") %></label> <label for="video_loop"><%= translate(locale, "preferences_video_loop_label") %></label>
<input name="video_loop" id="video_loop" type="checkbox" <% if preferences.video_loop %>checked<% end %>> <input name="video_loop" id="video_loop" type="checkbox" <% if preferences.video_loop %>checked<% end %>>
</div> </div>
<div class="pure-control-group"> <div class="pure-control-group">
<label for="preload"><%= I18n.translate(locale, "preferences_preload_label") %></label> <label for="preload"><%= translate(locale, "preferences_preload_label") %></label>
<input name="preload" id="preload" type="checkbox" <% if preferences.preload %>checked<% end %>> <input name="preload" id="preload" type="checkbox" <% if preferences.preload %>checked<% end %>>
</div> </div>
<div class="pure-control-group"> <div class="pure-control-group">
<label for="autoplay"><%= I18n.translate(locale, "preferences_autoplay_label") %></label> <label for="autoplay"><%= translate(locale, "preferences_autoplay_label") %></label>
<input name="autoplay" id="autoplay" type="checkbox" <% if preferences.autoplay %>checked<% end %>> <input name="autoplay" id="autoplay" type="checkbox" <% if preferences.autoplay %>checked<% end %>>
</div> </div>
<div class="pure-control-group"> <div class="pure-control-group">
<label for="continue"><%= I18n.translate(locale, "preferences_continue_label") %></label> <label for="continue"><%= translate(locale, "preferences_continue_label") %></label>
<input name="continue" id="continue" type="checkbox" <% if preferences.continue %>checked<% end %>> <input name="continue" id="continue" type="checkbox" <% if preferences.continue %>checked<% end %>>
</div> </div>
<div class="pure-control-group"> <div class="pure-control-group">
<label for="continue_autoplay"><%= I18n.translate(locale, "preferences_continue_autoplay_label") %></label> <label for="continue_autoplay"><%= translate(locale, "preferences_continue_autoplay_label") %></label>
<input name="continue_autoplay" id="continue_autoplay" type="checkbox" <% if preferences.continue_autoplay %>checked<% end %>> <input name="continue_autoplay" id="continue_autoplay" type="checkbox" <% if preferences.continue_autoplay %>checked<% end %>>
</div> </div>
<div class="pure-control-group"> <div class="pure-control-group">
<label for="local"><%= I18n.translate(locale, "preferences_local_label") %></label> <label for="local"><%= translate(locale, "preferences_local_label") %></label>
<input name="local" id="local" type="checkbox" <% if preferences.local && !CONFIG.disabled?("local") %>checked<% end %> <% if CONFIG.disabled?("local") %>disabled<% end %>> <input name="local" id="local" type="checkbox" <% if preferences.local && !CONFIG.disabled?("local") %>checked<% end %> <% if CONFIG.disabled?("local") %>disabled<% end %>>
</div> </div>
<div class="pure-control-group"> <div class="pure-control-group">
<label for="listen"><%= I18n.translate(locale, "preferences_listen_label") %></label> <label for="listen"><%= translate(locale, "preferences_listen_label") %></label>
<input name="listen" id="listen" type="checkbox" <% if preferences.listen %>checked<% end %>> <input name="listen" id="listen" type="checkbox" <% if preferences.listen %>checked<% end %>>
</div> </div>
<div class="pure-control-group"> <div class="pure-control-group">
<label for="speed"><%= I18n.translate(locale, "preferences_speed_label") %></label> <label for="speed"><%= translate(locale, "preferences_speed_label") %></label>
<select name="speed" id="speed"> <select name="speed" id="speed">
<% {2.0, 1.75, 1.5, 1.25, 1.0, 0.75, 0.5, 0.25}.each do |option| %> <% {2.0, 1.75, 1.5, 1.25, 1.0, 0.75, 0.5, 0.25}.each do |option| %>
<option <% if preferences.speed == option %> selected <% end %>><%= option %></option> <option <% if preferences.speed == option %> selected <% end %>><%= option %></option>
@ -52,11 +52,11 @@
</div> </div>
<div class="pure-control-group"> <div class="pure-control-group">
<label for="quality"><%= I18n.translate(locale, "preferences_quality_label") %></label> <label for="quality"><%= translate(locale, "preferences_quality_label") %></label>
<select name="quality" id="quality"> <select name="quality" id="quality">
<% {"dash", "hd720", "medium", "small"}.each do |option| %> <% {"dash", "hd720", "medium", "small"}.each do |option| %>
<% if !(option == "dash" && CONFIG.disabled?("dash")) %> <% if !(option == "dash" && CONFIG.disabled?("dash")) %>
<option value="<%= option %>" <% if preferences.quality == option %> selected <% end %>><%= I18n.translate(locale, "preferences_quality_option_" + option) %></option> <option value="<%= option %>" <% if preferences.quality == option %> selected <% end %>><%= translate(locale, "preferences_quality_option_" + option) %></option>
<% end %> <% end %>
<% end %> <% end %>
</select> </select>
@ -64,108 +64,108 @@
<% if !CONFIG.disabled?("dash") %> <% if !CONFIG.disabled?("dash") %>
<div class="pure-control-group"> <div class="pure-control-group">
<label for="quality_dash"><%= I18n.translate(locale, "preferences_quality_dash_label") %></label> <label for="quality_dash"><%= translate(locale, "preferences_quality_dash_label") %></label>
<select name="quality_dash" id="quality_dash"> <select name="quality_dash" id="quality_dash">
<% {"auto", "best", "4320p", "2160p", "1440p", "1080p", "720p", "480p", "360p", "240p", "144p", "worst"}.each do |option| %> <% {"auto", "best", "4320p", "2160p", "1440p", "1080p", "720p", "480p", "360p", "240p", "144p", "worst"}.each do |option| %>
<option value="<%= option %>" <% if preferences.quality_dash == option %> selected <% end %>><%= I18n.translate(locale, "preferences_quality_dash_option_" + option) %></option> <option value="<%= option %>" <% if preferences.quality_dash == option %> selected <% end %>><%= translate(locale, "preferences_quality_dash_option_" + option) %></option>
<% end %> <% end %>
</select> </select>
</div> </div>
<% end %> <% end %>
<div class="pure-control-group"> <div class="pure-control-group">
<label for="volume"><%= I18n.translate(locale, "preferences_volume_label") %></label> <label for="volume"><%= translate(locale, "preferences_volume_label") %></label>
<input name="volume" id="volume" data-onrange="update_volume_value" type="range" min="0" max="100" step="5" value="<%= preferences.volume %>"> <input name="volume" id="volume" data-onrange="update_volume_value" type="range" min="0" max="100" step="5" value="<%= preferences.volume %>">
<span class="pure-form-message-inline" id="volume-value"><%= preferences.volume %></span> <span class="pure-form-message-inline" id="volume-value"><%= preferences.volume %></span>
</div> </div>
<div class="pure-control-group"> <div class="pure-control-group">
<label for="comments[0]"><%= I18n.translate(locale, "preferences_comments_label") %></label> <label for="comments[0]"><%= translate(locale, "preferences_comments_label") %></label>
<% preferences.comments.each_with_index do |comments, index| %> <% preferences.comments.each_with_index do |comments, index| %>
<select name="comments[<%= index %>]" id="comments[<%= index %>]"> <select name="comments[<%= index %>]" id="comments[<%= index %>]">
<% {"", "youtube", "reddit"}.each do |option| %> <% {"", "youtube", "reddit"}.each do |option| %>
<option value="<%= option %>" <% if preferences.comments[index] == option %> selected <% end %>><%= I18n.translate(locale, option.blank? ? "none" : option) %></option> <option value="<%= option %>" <% if preferences.comments[index] == option %> selected <% end %>><%= translate(locale, option.blank? ? "none" : option) %></option>
<% end %> <% end %>
</select> </select>
<% end %> <% end %>
</div> </div>
<div class="pure-control-group"> <div class="pure-control-group">
<label for="captions[0]"><%= I18n.translate(locale, "preferences_captions_label") %></label> <label for="captions[0]"><%= translate(locale, "preferences_captions_label") %></label>
<% preferences.captions.each_with_index do |caption, index| %> <% preferences.captions.each_with_index do |caption, index| %>
<select class="pure-u-1-6" name="captions[<%= index %>]" id="captions[<%= index %>]"> <select class="pure-u-1-6" name="captions[<%= index %>]" id="captions[<%= index %>]">
<% Invidious::Videos::Captions::LANGUAGES.each do |option| %> <% Invidious::Videos::Captions::LANGUAGES.each do |option| %>
<option value="<%= option %>" <% if preferences.captions[index] == option %> selected <% end %>><%= I18n.translate(locale, option.blank? ? "none" : option) %></option> <option value="<%= option %>" <% if preferences.captions[index] == option %> selected <% end %>><%= translate(locale, option.blank? ? "none" : option) %></option>
<% end %> <% end %>
</select> </select>
<% end %> <% end %>
</div> </div>
<div class="pure-control-group"> <div class="pure-control-group">
<label for="related_videos"><%= I18n.translate(locale, "preferences_related_videos_label") %></label> <label for="related_videos"><%= translate(locale, "preferences_related_videos_label") %></label>
<input name="related_videos" id="related_videos" type="checkbox" <% if preferences.related_videos %>checked<% end %>> <input name="related_videos" id="related_videos" type="checkbox" <% if preferences.related_videos %>checked<% end %>>
</div> </div>
<div class="pure-control-group"> <div class="pure-control-group">
<label for="annotations"><%= I18n.translate(locale, "preferences_annotations_label") %></label> <label for="annotations"><%= translate(locale, "preferences_annotations_label") %></label>
<input name="annotations" id="annotations" type="checkbox" <% if preferences.annotations %>checked<% end %>> <input name="annotations" id="annotations" type="checkbox" <% if preferences.annotations %>checked<% end %>>
</div> </div>
<div class="pure-control-group"> <div class="pure-control-group">
<label for="extend_desc"><%= I18n.translate(locale, "preferences_extend_desc_label") %></label> <label for="extend_desc"><%= translate(locale, "preferences_extend_desc_label") %></label>
<input name="extend_desc" id="extend_desc" type="checkbox" <% if preferences.extend_desc %>checked<% end %>> <input name="extend_desc" id="extend_desc" type="checkbox" <% if preferences.extend_desc %>checked<% end %>>
</div> </div>
<div class="pure-control-group"> <div class="pure-control-group">
<label for="vr_mode"><%= I18n.translate(locale, "preferences_vr_mode_label") %></label> <label for="vr_mode"><%= translate(locale, "preferences_vr_mode_label") %></label>
<input name="vr_mode" id="vr_mode" type="checkbox" <% if preferences.vr_mode %>checked<% end %>> <input name="vr_mode" id="vr_mode" type="checkbox" <% if preferences.vr_mode %>checked<% end %>>
</div> </div>
<div class="pure-control-group"> <div class="pure-control-group">
<label for="save_player_pos"><%= I18n.translate(locale, "preferences_save_player_pos_label") %></label> <label for="save_player_pos"><%= translate(locale, "preferences_save_player_pos_label") %></label>
<input name="save_player_pos" id="save_player_pos" type="checkbox" <% if preferences.save_player_pos %>checked<% end %>> <input name="save_player_pos" id="save_player_pos" type="checkbox" <% if preferences.save_player_pos %>checked<% end %>>
</div> </div>
<legend><%= I18n.translate(locale, "preferences_category_visual") %></legend> <legend><%= translate(locale, "preferences_category_visual") %></legend>
<div class="pure-control-group"> <div class="pure-control-group">
<label for="locale"><%= I18n.translate(locale, "preferences_locale_label") %></label> <label for="locale"><%= translate(locale, "preferences_locale_label") %></label>
<select name="locale" id="locale"> <select name="locale" id="locale">
<% I18n::LOCALES_LIST.each do |iso_name, full_name| %> <% LOCALES_LIST.each do |iso_name, full_name| %>
<option value="<%= iso_name %>" <% if preferences.locale == iso_name %> selected <% end %>><%= HTML.escape(full_name) %></option> <option value="<%= iso_name %>" <% if preferences.locale == iso_name %> selected <% end %>><%= HTML.escape(full_name) %></option>
<% end %> <% end %>
</select> </select>
</div> </div>
<div class="pure-control-group"> <div class="pure-control-group">
<label for="region"><%= I18n.translate(locale, "preferences_region_label") %></label> <label for="region"><%= translate(locale, "preferences_region_label") %></label>
<select name="region" id="region"> <select name="region" id="region">
<% I18n::CONTENT_REGIONS.each do |option| %> <% CONTENT_REGIONS.each do |option| %>
<option value="<%= option %>" <% if preferences.region == option %> selected <% end %>><%= option %></option> <option value="<%= option %>" <% if preferences.region == option %> selected <% end %>><%= option %></option>
<% end %> <% end %>
</select> </select>
</div> </div>
<div class="pure-control-group"> <div class="pure-control-group">
<label for="player_style"><%= I18n.translate(locale, "preferences_player_style_label") %></label> <label for="player_style"><%= translate(locale, "preferences_player_style_label") %></label>
<select name="player_style" id="player_style"> <select name="player_style" id="player_style">
<% {"invidious", "youtube"}.each do |option| %> <% {"invidious", "youtube"}.each do |option| %>
<option value="<%= option %>" <% if preferences.player_style == option %> selected <% end %>><%= I18n.translate(locale, option) %></option> <option value="<%= option %>" <% if preferences.player_style == option %> selected <% end %>><%= translate(locale, option) %></option>
<% end %> <% end %>
</select> </select>
</div> </div>
<div class="pure-control-group"> <div class="pure-control-group">
<label for="dark_mode"><%= I18n.translate(locale, "preferences_dark_mode_label") %></label> <label for="dark_mode"><%= translate(locale, "preferences_dark_mode_label") %></label>
<select name="dark_mode" id="dark_mode"> <select name="dark_mode" id="dark_mode">
<% {"", "light", "dark"}.each do |option| %> <% {"", "light", "dark"}.each do |option| %>
<option value="<%= option %>" <% if preferences.dark_mode == option %> selected <% end %>><%= I18n.translate(locale, option.blank? ? "auto" : option) %></option> <option value="<%= option %>" <% if preferences.dark_mode == option %> selected <% end %>><%= translate(locale, option.blank? ? "auto" : option) %></option>
<% end %> <% end %>
</select> </select>
</div> </div>
<div class="pure-control-group"> <div class="pure-control-group">
<label for="thin_mode"><%= I18n.translate(locale, "preferences_thin_mode_label") %></label> <label for="thin_mode"><%= translate(locale, "preferences_thin_mode_label") %></label>
<input name="thin_mode" id="thin_mode" type="checkbox" <% if preferences.thin_mode %>checked<% end %>> <input name="thin_mode" id="thin_mode" type="checkbox" <% if preferences.thin_mode %>checked<% end %>>
</div> </div>
@ -176,187 +176,187 @@
<% end %> <% end %>
<div class="pure-control-group"> <div class="pure-control-group">
<label for="default_home"><%= I18n.translate(locale, "preferences_default_home_label") %></label> <label for="default_home"><%= translate(locale, "preferences_default_home_label") %></label>
<select name="default_home" id="default_home"> <select name="default_home" id="default_home">
<% feed_options.each do |option| %> <% feed_options.each do |option| %>
<option value="<%= option %>" <% if preferences.default_home == option %> selected <% end %>><%= I18n.translate(locale, option.blank? ? "Search" : option) %></option> <option value="<%= option %>" <% if preferences.default_home == option %> selected <% end %>><%= translate(locale, option.blank? ? "Search" : option) %></option>
<% end %> <% end %>
</select> </select>
</div> </div>
<div class="pure-control-group"> <div class="pure-control-group">
<label for="feed_menu"><%= I18n.translate(locale, "preferences_feed_menu_label") %></label> <label for="feed_menu"><%= translate(locale, "preferences_feed_menu_label") %></label>
<% (feed_options.size - 1).times do |index| %> <% (feed_options.size - 1).times do |index| %>
<select name="feed_menu[<%= index %>]" id="feed_menu[<%= index %>]"> <select name="feed_menu[<%= index %>]" id="feed_menu[<%= index %>]">
<% feed_options.each do |option| %> <% feed_options.each do |option| %>
<option value="<%= option %>" <% if preferences.feed_menu[index]? == option %> selected <% end %>><%= I18n.translate(locale, option.blank? ? "Search" : option) %></option> <option value="<%= option %>" <% if preferences.feed_menu[index]? == option %> selected <% end %>><%= translate(locale, option.blank? ? "Search" : option) %></option>
<% end %> <% end %>
</select> </select>
<% end %> <% end %>
</div> </div>
<% if env.get? "user" %> <% if env.get? "user" %>
<div class="pure-control-group"> <div class="pure-control-group">
<label for="show_nick"><%= I18n.translate(locale, "preferences_show_nick_label") %></label> <label for="show_nick"><%= translate(locale, "preferences_show_nick_label") %></label>
<input name="show_nick" id="show_nick" type="checkbox" <% if preferences.show_nick %>checked<% end %>> <input name="show_nick" id="show_nick" type="checkbox" <% if preferences.show_nick %>checked<% end %>>
</div> </div>
<% end %> <% end %>
<legend><%= I18n.translate(locale, "preferences_category_misc") %></legend> <legend><%= translate(locale, "preferences_category_misc") %></legend>
<div class="pure-control-group"> <div class="pure-control-group">
<label for="automatic_instance_redirect"><%= I18n.translate(locale, "preferences_automatic_instance_redirect_label") %></label> <label for="automatic_instance_redirect"><%= translate(locale, "preferences_automatic_instance_redirect_label") %></label>
<input name="automatic_instance_redirect" id="automatic_instance_redirect" type="checkbox" <% if preferences.automatic_instance_redirect %>checked<% end %>> <input name="automatic_instance_redirect" id="automatic_instance_redirect" type="checkbox" <% if preferences.automatic_instance_redirect %>checked<% end %>>
</div> </div>
<% if env.get? "user" %> <% if env.get? "user" %>
<legend><%= I18n.translate(locale, "preferences_category_subscription") %></legend> <legend><%= translate(locale, "preferences_category_subscription") %></legend>
<div class="pure-control-group"> <div class="pure-control-group">
<label for="watch_history"><%= I18n.translate(locale, "preferences_watch_history_label") %></label> <label for="watch_history"><%= translate(locale, "preferences_watch_history_label") %></label>
<input name="watch_history" id="watch_history" type="checkbox" <% if preferences.watch_history %>checked<% end %>> <input name="watch_history" id="watch_history" type="checkbox" <% if preferences.watch_history %>checked<% end %>>
</div> </div>
<div class="pure-control-group"> <div class="pure-control-group">
<label for="annotations_subscribed"><%= I18n.translate(locale, "preferences_annotations_subscribed_label") %></label> <label for="annotations_subscribed"><%= translate(locale, "preferences_annotations_subscribed_label") %></label>
<input name="annotations_subscribed" id="annotations_subscribed" type="checkbox" <% if preferences.annotations_subscribed %>checked<% end %>> <input name="annotations_subscribed" id="annotations_subscribed" type="checkbox" <% if preferences.annotations_subscribed %>checked<% end %>>
</div> </div>
<div class="pure-control-group"> <div class="pure-control-group">
<label for="max_results"><%= I18n.translate(locale, "preferences_max_results_label") %></label> <label for="max_results"><%= translate(locale, "preferences_max_results_label") %></label>
<input name="max_results" id="max_results" type="number" value="<%= preferences.max_results %>"> <input name="max_results" id="max_results" type="number" value="<%= preferences.max_results %>">
</div> </div>
<div class="pure-control-group"> <div class="pure-control-group">
<label for="sort"><%= I18n.translate(locale, "preferences_sort_label") %></label> <label for="sort"><%= translate(locale, "preferences_sort_label") %></label>
<select name="sort" id="sort"> <select name="sort" id="sort">
<% {"published", "published - reverse", "alphabetically", "alphabetically - reverse", "channel name", "channel name - reverse"}.each do |option| %> <% {"published", "published - reverse", "alphabetically", "alphabetically - reverse", "channel name", "channel name - reverse"}.each do |option| %>
<option value="<%= option %>" <% if preferences.sort == option %> selected <% end %>><%= I18n.translate(locale, option) %></option> <option value="<%= option %>" <% if preferences.sort == option %> selected <% end %>><%= translate(locale, option) %></option>
<% end %> <% end %>
</select> </select>
</div> </div>
<div class="pure-control-group"> <div class="pure-control-group">
<% if preferences.unseen_only %> <% if preferences.unseen_only %>
<label for="latest_only"><%= I18n.translate(locale, "Only show latest unwatched video from channel: ") %></label> <label for="latest_only"><%= translate(locale, "Only show latest unwatched video from channel: ") %></label>
<% else %> <% else %>
<label for="latest_only"><%= I18n.translate(locale, "Only show latest video from channel: ") %></label> <label for="latest_only"><%= translate(locale, "Only show latest video from channel: ") %></label>
<% end %> <% end %>
<input name="latest_only" id="latest_only" type="checkbox" <% if preferences.latest_only %>checked<% end %>> <input name="latest_only" id="latest_only" type="checkbox" <% if preferences.latest_only %>checked<% end %>>
</div> </div>
<div class="pure-control-group"> <div class="pure-control-group">
<label for="unseen_only"><%= I18n.translate(locale, "preferences_unseen_only_label") %></label> <label for="unseen_only"><%= translate(locale, "preferences_unseen_only_label") %></label>
<input name="unseen_only" id="unseen_only" type="checkbox" <% if preferences.unseen_only %>checked<% end %>> <input name="unseen_only" id="unseen_only" type="checkbox" <% if preferences.unseen_only %>checked<% end %>>
</div> </div>
<% if CONFIG.enable_user_notifications %> <% if CONFIG.enable_user_notifications %>
<div class="pure-control-group"> <div class="pure-control-group">
<label for="notifications_only"><%= I18n.translate(locale, "preferences_notifications_only_label") %></label> <label for="notifications_only"><%= translate(locale, "preferences_notifications_only_label") %></label>
<input name="notifications_only" id="notifications_only" type="checkbox" <% if preferences.notifications_only %>checked<% end %>> <input name="notifications_only" id="notifications_only" type="checkbox" <% if preferences.notifications_only %>checked<% end %>>
</div> </div>
<% # Web notifications are only supported over HTTPS %> <% # Web notifications are only supported over HTTPS %>
<% if Kemal.config.ssl || CONFIG.https_only %> <% if Kemal.config.ssl || CONFIG.https_only %>
<div class="pure-control-group"> <div class="pure-control-group">
<a href="#" data-onclick="notification_requestPermission"><%= I18n.translate(locale, "Enable web notifications") %></a> <a href="#" data-onclick="notification_requestPermission"><%= translate(locale, "Enable web notifications") %></a>
</div> </div>
<% end %> <% end %>
<% end %> <% end %>
<% end %> <% end %>
<% if env.get?("user") && CONFIG.admins.includes? env.get?("user").as(Invidious::User).email %> <% if env.get?("user") && CONFIG.admins.includes? env.get?("user").as(Invidious::User).email %>
<legend><%= I18n.translate(locale, "preferences_category_admin") %></legend> <legend><%= translate(locale, "preferences_category_admin") %></legend>
<div class="pure-control-group"> <div class="pure-control-group">
<label for="admin_default_home"><%= I18n.translate(locale, "preferences_default_home_label") %></label> <label for="admin_default_home"><%= translate(locale, "preferences_default_home_label") %></label>
<select name="admin_default_home" id="admin_default_home"> <select name="admin_default_home" id="admin_default_home">
<% feed_options.each do |option| %> <% feed_options.each do |option| %>
<option value="<%= option %>" <% if CONFIG.default_user_preferences.default_home == option %> selected <% end %>><%= I18n.translate(locale, option.blank? ? "none" : option) %></option> <option value="<%= option %>" <% if CONFIG.default_user_preferences.default_home == option %> selected <% end %>><%= translate(locale, option.blank? ? "none" : option) %></option>
<% end %> <% end %>
</select> </select>
</div> </div>
<div class="pure-control-group"> <div class="pure-control-group">
<label for="admin_feed_menu"><%= I18n.translate(locale, "preferences_feed_menu_label") %></label> <label for="admin_feed_menu"><%= translate(locale, "preferences_feed_menu_label") %></label>
<% (feed_options.size - 1).times do |index| %> <% (feed_options.size - 1).times do |index| %>
<select name="admin_feed_menu[<%= index %>]" id="admin_feed_menu[<%= index %>]"> <select name="admin_feed_menu[<%= index %>]" id="admin_feed_menu[<%= index %>]">
<% feed_options.each do |option| %> <% feed_options.each do |option| %>
<option value="<%= option %>" <% if CONFIG.default_user_preferences.feed_menu[index]? == option %> selected <% end %>><%= I18n.translate(locale, option.blank? ? "none" : option) %></option> <option value="<%= option %>" <% if CONFIG.default_user_preferences.feed_menu[index]? == option %> selected <% end %>><%= translate(locale, option.blank? ? "none" : option) %></option>
<% end %> <% end %>
</select> </select>
<% end %> <% end %>
</div> </div>
<div class="pure-control-group"> <div class="pure-control-group">
<label for="popular_enabled"><%= I18n.translate(locale, "Popular enabled: ") %></label> <label for="popular_enabled"><%= translate(locale, "Popular enabled: ") %></label>
<input name="popular_enabled" id="popular_enabled" type="checkbox" <% if CONFIG.popular_enabled %>checked<% end %>> <input name="popular_enabled" id="popular_enabled" type="checkbox" <% if CONFIG.popular_enabled %>checked<% end %>>
</div> </div>
<div class="pure-control-group"> <div class="pure-control-group">
<label for="captcha_enabled"><%= I18n.translate(locale, "CAPTCHA enabled: ") %></label> <label for="captcha_enabled"><%= translate(locale, "CAPTCHA enabled: ") %></label>
<input name="captcha_enabled" id="captcha_enabled" type="checkbox" <% if CONFIG.captcha_enabled %>checked<% end %>> <input name="captcha_enabled" id="captcha_enabled" type="checkbox" <% if CONFIG.captcha_enabled %>checked<% end %>>
</div> </div>
<div class="pure-control-group"> <div class="pure-control-group">
<label for="login_enabled"><%= I18n.translate(locale, "Login enabled: ") %></label> <label for="login_enabled"><%= translate(locale, "Login enabled: ") %></label>
<input name="login_enabled" id="login_enabled" type="checkbox" <% if CONFIG.login_enabled %>checked<% end %>> <input name="login_enabled" id="login_enabled" type="checkbox" <% if CONFIG.login_enabled %>checked<% end %>>
</div> </div>
<div class="pure-control-group"> <div class="pure-control-group">
<label for="registration_enabled"><%= I18n.translate(locale, "Registration enabled: ") %></label> <label for="registration_enabled"><%= translate(locale, "Registration enabled: ") %></label>
<input name="registration_enabled" id="registration_enabled" type="checkbox" <% if CONFIG.registration_enabled %>checked<% end %>> <input name="registration_enabled" id="registration_enabled" type="checkbox" <% if CONFIG.registration_enabled %>checked<% end %>>
</div> </div>
<div class="pure-control-group"> <div class="pure-control-group">
<label for="statistics_enabled"><%= I18n.translate(locale, "Report statistics: ") %></label> <label for="statistics_enabled"><%= translate(locale, "Report statistics: ") %></label>
<input name="statistics_enabled" id="statistics_enabled" type="checkbox" <% if CONFIG.statistics_enabled %>checked<% end %>> <input name="statistics_enabled" id="statistics_enabled" type="checkbox" <% if CONFIG.statistics_enabled %>checked<% end %>>
</div> </div>
<div class="pure-control-group"> <div class="pure-control-group">
<label for="modified_source_code_url"><%= I18n.translate(locale, "adminprefs_modified_source_code_url_label") %></label> <label for="modified_source_code_url"><%= translate(locale, "adminprefs_modified_source_code_url_label") %></label>
<input name="modified_source_code_url" id="modified_source_code_url" type="url" value="<%= CONFIG.modified_source_code_url %>"> <input name="modified_source_code_url" id="modified_source_code_url" type="url" value="<%= CONFIG.modified_source_code_url %>">
</div> </div>
<% end %> <% end %>
<% if env.get? "user" %> <% if env.get? "user" %>
<legend><%= I18n.translate(locale, "preferences_category_data") %></legend> <legend><%= translate(locale, "preferences_category_data") %></legend>
<div class="pure-control-group"> <div class="pure-control-group">
<a href="/clear_watch_history?referer=<%= URI.encode_www_form(referer) %>"><%= I18n.translate(locale, "Clear watch history") %></a> <a href="/clear_watch_history?referer=<%= URI.encode_www_form(referer) %>"><%= translate(locale, "Clear watch history") %></a>
</div> </div>
<div class="pure-control-group"> <div class="pure-control-group">
<a href="/change_password?referer=<%= URI.encode_www_form(referer) %>"><%= I18n.translate(locale, "Change password") %></a> <a href="/change_password?referer=<%= URI.encode_www_form(referer) %>"><%= translate(locale, "Change password") %></a>
</div> </div>
<div class="pure-control-group"> <div class="pure-control-group">
<a href="/data_control?referer=<%= URI.encode_www_form(referer) %>"><%= I18n.translate(locale, "Import/export data") %></a> <a href="/data_control?referer=<%= URI.encode_www_form(referer) %>"><%= translate(locale, "Import/export data") %></a>
</div> </div>
<div class="pure-control-group"> <div class="pure-control-group">
<a href="/subscription_manager"><%= I18n.translate(locale, "Manage subscriptions") %></a> <a href="/subscription_manager"><%= translate(locale, "Manage subscriptions") %></a>
</div> </div>
<div class="pure-control-group"> <div class="pure-control-group">
<a href="/token_manager"><%= I18n.translate(locale, "Manage tokens") %></a> <a href="/token_manager"><%= translate(locale, "Manage tokens") %></a>
</div> </div>
<div class="pure-control-group"> <div class="pure-control-group">
<a href="/feed/playlists"><%= I18n.translate(locale, "View all playlists") %></a> <a href="/feed/playlists"><%= translate(locale, "View all playlists") %></a>
</div> </div>
<div class="pure-control-group"> <div class="pure-control-group">
<a href="/feed/history"><%= I18n.translate(locale, "Watch history") %></a> <a href="/feed/history"><%= translate(locale, "Watch history") %></a>
</div> </div>
<div class="pure-control-group"> <div class="pure-control-group">
<a href="/delete_account?referer=<%= URI.encode_www_form(referer) %>"><%= I18n.translate(locale, "Delete account") %></a> <a href="/delete_account?referer=<%= URI.encode_www_form(referer) %>"><%= translate(locale, "Delete account") %></a>
</div> </div>
<% end %> <% end %>
<div class="pure-controls"> <div class="pure-controls">
<button type="submit" class="pure-button pure-button-primary"><%= I18n.translate(locale, "Save preferences") %></button> <button type="submit" class="pure-button pure-button-primary"><%= translate(locale, "Save preferences") %></button>
</div> </div>
</fieldset> </fieldset>
</form> </form>

View File

@ -1,26 +1,26 @@
<% content_for "header" do %> <% content_for "header" do %>
<title><%= I18n.translate(locale, "Subscription manager") %> - Invidious</title> <title><%= translate(locale, "Subscription manager") %> - Invidious</title>
<% end %> <% end %>
<div class="pure-g h-box"> <div class="pure-g h-box">
<div class="pure-u-1-3"> <div class="pure-u-1-3">
<h3> <h3>
<a href="/feed/subscriptions"> <a href="/feed/subscriptions">
<%= I18n.translate_count(locale, "generic_subscriptions_count", subscriptions.size, I18n::NumberFormatting::HtmlSpan) %> <%= translate_count(locale, "generic_subscriptions_count", subscriptions.size, NumberFormatting::HtmlSpan) %>
</a> </a>
</h3> </h3>
</div> </div>
<div class="pure-u-1-3"> <div class="pure-u-1-3">
<h3 style="text-align:center"> <h3 style="text-align:center">
<a href="/feed/history"> <a href="/feed/history">
<%= I18n.translate(locale, "Watch history") %> <%= translate(locale, "Watch history") %>
</a> </a>
</h3> </h3>
</div> </div>
<div class="pure-u-1-3"> <div class="pure-u-1-3">
<h3 style="text-align:right"> <h3 style="text-align:right">
<a href="/data_control?referer=<%= URI.encode_www_form(referer) %>"> <a href="/data_control?referer=<%= URI.encode_www_form(referer) %>">
<%= I18n.translate(locale, "Import/export") %> <%= translate(locale, "Import/export") %>
</a> </a>
</h3> </h3>
</div> </div>
@ -39,7 +39,7 @@
<h3 style="padding-right:0.5em"> <h3 style="padding-right:0.5em">
<form data-onsubmit="return_false" action="/subscription_ajax?action=remove_subscriptions&c=<%= channel.id %>&referer=<%= env.get("current_page") %>" method="post"> <form data-onsubmit="return_false" action="/subscription_ajax?action=remove_subscriptions&c=<%= channel.id %>&referer=<%= env.get("current_page") %>" method="post">
<input type="hidden" name="csrf_token" value="<%= HTML.escape(env.get?("csrf_token").try &.as(String) || "") %>"> <input type="hidden" name="csrf_token" value="<%= HTML.escape(env.get?("csrf_token").try &.as(String) || "") %>">
<input style="all:unset" type="submit" data-onclick="remove_subscription" data-ucid="<%= channel.id %>" value="<%= I18n.translate(locale, "unsubscribe") %>"> <input style="all:unset" type="submit" data-onclick="remove_subscription" data-ucid="<%= channel.id %>" value="<%= translate(locale, "unsubscribe") %>">
</form> </form>
</h3> </h3>
</div> </div>

View File

@ -1,17 +1,17 @@
<% content_for "header" do %> <% content_for "header" do %>
<title><%= I18n.translate(locale, "Token manager") %> - Invidious</title> <title><%= translate(locale, "Token manager") %> - Invidious</title>
<% end %> <% end %>
<div class="pure-g h-box"> <div class="pure-g h-box">
<div class="pure-u-1-3"> <div class="pure-u-1-3">
<h3> <h3>
<%= I18n.translate_count(locale, "tokens_count", tokens.size, I18n::NumberFormatting::HtmlSpan) %> <%= translate_count(locale, "tokens_count", tokens.size, NumberFormatting::HtmlSpan) %>
</h3> </h3>
</div> </div>
<div class="pure-u-1-3"></div> <div class="pure-u-1-3"></div>
<div class="pure-u-1-3" style="text-align:right"> <div class="pure-u-1-3" style="text-align:right">
<h3> <h3>
<a href="/preferences?referer=<%= URI.encode_www_form(referer) %>"><%= I18n.translate(locale, "Preferences") %></a> <a href="/preferences?referer=<%= URI.encode_www_form(referer) %>"><%= translate(locale, "Preferences") %></a>
</h3> </h3>
</div> </div>
</div> </div>
@ -25,13 +25,13 @@
</h4> </h4>
</div> </div>
<div class="pure-u-1-5" style="text-align:center"> <div class="pure-u-1-5" style="text-align:center">
<h4><%= I18n.translate(locale, "`x` ago", recode_date(token[:issued], locale)) %></h4> <h4><%= translate(locale, "`x` ago", recode_date(token[:issued], locale)) %></h4>
</div> </div>
<div class="pure-u-1-5" style="text-align:right"> <div class="pure-u-1-5" style="text-align:right">
<h3 style="padding-right:0.5em"> <h3 style="padding-right:0.5em">
<form data-onsubmit="return_false" action="/token_ajax?action=revoke_token&session=<%= token[:session] %>&referer=<%= env.get("current_page") %>" method="post"> <form data-onsubmit="return_false" action="/token_ajax?action=revoke_token&session=<%= token[:session] %>&referer=<%= env.get("current_page") %>" method="post">
<input type="hidden" name="csrf_token" value="<%= HTML.escape(env.get?("csrf_token").try &.as(String) || "") %>"> <input type="hidden" name="csrf_token" value="<%= HTML.escape(env.get?("csrf_token").try &.as(String) || "") %>">
<input style="all:unset" type="submit" data-onclick="revoke_token" data-session="<%= token[:session] %>" value="<%= I18n.translate(locale, "revoke") %>"> <input style="all:unset" type="submit" data-onclick="revoke_token" data-session="<%= token[:session] %>" value="<%= translate(locale, "revoke") %>">
</form> </form>
</h3> </h3>
</div> </div>

View File

@ -35,11 +35,11 @@ we're going to need to do it here in order to allow for translations.
--> -->
<style> <style>
#descexpansionbutton ~ label > a::after { #descexpansionbutton ~ label > a::after {
content: "<%= I18n.translate(locale, "Show more") %>" content: "<%= translate(locale, "Show more") %>"
} }
#descexpansionbutton:checked ~ label > a::after { #descexpansionbutton:checked ~ label > a::after {
content: "<%= I18n.translate(locale, "Show less") %>" content: "<%= translate(locale, "Show less") %>"
} }
</style> </style>
<% end %> <% end %>
@ -53,12 +53,12 @@ we're going to need to do it here in order to allow for translations.
"length_seconds" => video.length_seconds.to_f, "length_seconds" => video.length_seconds.to_f,
"play_next" => !video.related_videos.empty? && !plid && params.continue, "play_next" => !video.related_videos.empty? && !plid && params.continue,
"next_video" => video.related_videos.select { |rv| rv["id"]? }[0]?.try &.["id"], "next_video" => video.related_videos.select { |rv| rv["id"]? }[0]?.try &.["id"],
"youtube_comments_text" => HTML.escape(I18n.translate(locale, "View YouTube comments")), "youtube_comments_text" => HTML.escape(translate(locale, "View YouTube comments")),
"reddit_comments_text" => HTML.escape(I18n.translate(locale, "View Reddit comments")), "reddit_comments_text" => HTML.escape(translate(locale, "View Reddit comments")),
"reddit_permalink_text" => HTML.escape(I18n.translate(locale, "View more comments on Reddit")), "reddit_permalink_text" => HTML.escape(translate(locale, "View more comments on Reddit")),
"comments_text" => HTML.escape(I18n.translate(locale, "View `x` comments", "{commentCount}")), "comments_text" => HTML.escape(translate(locale, "View `x` comments", "{commentCount}")),
"hide_replies_text" => HTML.escape(I18n.translate(locale, "Hide replies")), "hide_replies_text" => HTML.escape(translate(locale, "Hide replies")),
"show_replies_text" => HTML.escape(I18n.translate(locale, "Show replies")), "show_replies_text" => HTML.escape(translate(locale, "Show replies")),
"params" => params, "params" => params,
"preferences" => preferences, "preferences" => preferences,
"premiere_timestamp" => video.premiere_timestamp.try &.to_unix, "premiere_timestamp" => video.premiere_timestamp.try &.to_unix,
@ -78,11 +78,11 @@ we're going to need to do it here in order to allow for translations.
<h1> <h1>
<%= title %> <%= title %>
<% if params.listen %> <% if params.listen %>
<a title="<%=I18n.translate(locale, "Video mode")%>" href="/watch?<%= env.params.query %>&listen=0"> <a title="<%=translate(locale, "Video mode")%>" href="/watch?<%= env.params.query %>&listen=0">
<i class="icon ion-ios-videocam"></i> <i class="icon ion-ios-videocam"></i>
</a> </a>
<% else %> <% else %>
<a title="<%=I18n.translate(locale, "Audio mode")%>" href="/watch?<%= env.params.query %>&listen=1"> <a title="<%=translate(locale, "Audio mode")%>" href="/watch?<%= env.params.query %>&listen=1">
<i class="icon ion-md-headset"></i> <i class="icon ion-md-headset"></i>
</a> </a>
<% end %> <% end %>
@ -90,7 +90,7 @@ we're going to need to do it here in order to allow for translations.
<% if !video.is_listed %> <% if !video.is_listed %>
<h3> <h3>
<i class="icon ion-ios-unlock"></i> <%= I18n.translate(locale, "Unlisted") %> <i class="icon ion-ios-unlock"></i> <%= translate(locale, "Unlisted") %>
</h3> </h3>
<% end %> <% end %>
@ -100,11 +100,11 @@ we're going to need to do it here in order to allow for translations.
</h3> </h3>
<% elsif video.premiere_timestamp.try &.> Time.utc %> <% elsif video.premiere_timestamp.try &.> Time.utc %>
<h3> <h3>
<%= video.premiere_timestamp.try { |t| I18n.translate(locale, "Premieres in `x`", recode_date((t - Time.utc).ago, locale)) } %> <%= video.premiere_timestamp.try { |t| translate(locale, "Premieres in `x`", recode_date((t - Time.utc).ago, locale)) } %>
</h3> </h3>
<% elsif video.live_now %> <% elsif video.live_now %>
<h3> <h3>
<%= video.premiere_timestamp.try { |t| I18n.translate(locale, "videoinfo_started_streaming_x_ago", recode_date((Time.utc - t).ago, locale)) } %> <%= video.premiere_timestamp.try { |t| translate(locale, "videoinfo_started_streaming_x_ago", recode_date((Time.utc - t).ago, locale)) } %>
</h3> </h3>
<% end %> <% end %>
</div> </div>
@ -123,13 +123,13 @@ we're going to need to do it here in order to allow for translations.
link_yt_embed = IV::HttpServer::Utils.add_params_to_url(link_yt_embed, link_yt_param) link_yt_embed = IV::HttpServer::Utils.add_params_to_url(link_yt_embed, link_yt_param)
end end
-%> -%>
<a id="link-yt-watch" rel="noreferrer noopener" data-base-url="<%= link_yt_watch %>" href="<%= link_yt_watch %>"><%= I18n.translate(locale, "videoinfo_watch_on_youTube") %></a> <a id="link-yt-watch" rel="noreferrer noopener" data-base-url="<%= link_yt_watch %>" href="<%= link_yt_watch %>"><%= translate(locale, "videoinfo_watch_on_youTube") %></a>
(<a id="link-yt-embed" rel="noreferrer noopener" data-base-url="<%= link_yt_embed %>" href="<%= link_yt_embed %>"><%= I18n.translate(locale, "videoinfo_youTube_embed_link") %></a>) (<a id="link-yt-embed" rel="noreferrer noopener" data-base-url="<%= link_yt_embed %>" href="<%= link_yt_embed %>"><%= translate(locale, "videoinfo_youTube_embed_link") %></a>)
</span> </span>
<p id="watch-on-another-invidious-instance"> <p id="watch-on-another-invidious-instance">
<%- link_iv_other = IV::Frontend::Misc.redirect_url(env) -%> <%- link_iv_other = IV::Frontend::Misc.redirect_url(env) -%>
<a id="link-iv-other" data-base-url="<%= link_iv_other %>" href="<%= link_iv_other %>"><%= I18n.translate(locale, "Switch Invidious Instance") %></a> <a id="link-iv-other" data-base-url="<%= link_iv_other %>" href="<%= link_iv_other %>"><%= translate(locale, "Switch Invidious Instance") %></a>
</p> </p>
<p id="embed-link"> <p id="embed-link">
@ -140,17 +140,17 @@ we're going to need to do it here in order to allow for translations.
link_iv_embed = URI.new(path: "/embed/#{id}") link_iv_embed = URI.new(path: "/embed/#{id}")
link_iv_embed = IV::HttpServer::Utils.add_params_to_url(link_iv_embed, params_iv_embed) link_iv_embed = IV::HttpServer::Utils.add_params_to_url(link_iv_embed, params_iv_embed)
-%> -%>
<a id="link-iv-embed" data-base-url="<%= link_iv_embed %>" href="<%= link_iv_embed %>"><%= I18n.translate(locale, "videoinfo_invidious_embed_link") %></a> <a id="link-iv-embed" data-base-url="<%= link_iv_embed %>" href="<%= link_iv_embed %>"><%= translate(locale, "videoinfo_invidious_embed_link") %></a>
</p> </p>
<p id="annotations"> <p id="annotations">
<% if params.annotations %> <% if params.annotations %>
<a href="/watch?<%= env.params.query %>&iv_load_policy=3"> <a href="/watch?<%= env.params.query %>&iv_load_policy=3">
<%= I18n.translate(locale, "Hide annotations") %> <%= translate(locale, "Hide annotations") %>
</a> </a>
<% else %> <% else %>
<a href="/watch?<%= env.params.query %>&iv_load_policy=1"> <a href="/watch?<%= env.params.query %>&iv_load_policy=1">
<%=I18n.translate(locale, "Show annotations")%> <%=translate(locale, "Show annotations")%>
</a> </a>
<% end %> <% end %>
</p> </p>
@ -160,7 +160,7 @@ we're going to need to do it here in order to allow for translations.
<% if !playlists.empty? %> <% if !playlists.empty? %>
<form data-onsubmit="return_false" class="pure-form pure-form-stacked" action="/playlist_ajax?action=add_video" method="post" target="_blank"> <form data-onsubmit="return_false" class="pure-form pure-form-stacked" action="/playlist_ajax?action=add_video" method="post" target="_blank">
<div class="pure-control-group"> <div class="pure-control-group">
<label for="playlist_id"><%= I18n.translate(locale, "Add to playlist: ") %></label> <label for="playlist_id"><%= translate(locale, "Add to playlist: ") %></label>
<select style="width:100%" name="playlist_id" id="playlist_id"> <select style="width:100%" name="playlist_id" id="playlist_id">
<% playlists.each do |plid, playlist_title| %> <% playlists.each do |plid, playlist_title| %>
<option data-plid="<%= plid %>" value="<%= plid %>"><%= HTML.escape(playlist_title) %></option> <option data-plid="<%= plid %>" value="<%= plid %>"><%= HTML.escape(playlist_title) %></option>
@ -171,7 +171,7 @@ we're going to need to do it here in order to allow for translations.
<input type="hidden" name="csrf_token" value="<%= URI.encode_www_form(env.get?("csrf_token").try &.as(String) || "") %>"> <input type="hidden" name="csrf_token" value="<%= URI.encode_www_form(env.get?("csrf_token").try &.as(String) || "") %>">
<input type="hidden" name="video_id" value="<%= video.id %>"> <input type="hidden" name="video_id" value="<%= video.id %>">
<button data-onclick="add_playlist_video" data-id="<%= video.id %>" type="submit" class="pure-button pure-button-primary"> <button data-onclick="add_playlist_video" data-id="<%= video.id %>" type="submit" class="pure-button pure-button-primary">
<b><%= I18n.translate(locale, "Add to playlist") %></b> <b><%= translate(locale, "Add to playlist") %></b>
</button> </button>
</form> </form>
<script id="playlist_data" type="application/json"> <script id="playlist_data" type="application/json">
@ -190,7 +190,7 @@ we're going to need to do it here in order to allow for translations.
<p id="views"><i class="icon ion-ios-eye"></i> <%= number_with_separator(video.views) %></p> <p id="views"><i class="icon ion-ios-eye"></i> <%= number_with_separator(video.views) %></p>
<p id="likes"><i class="icon ion-ios-thumbs-up"></i> <%= number_with_separator(video.likes) %></p> <p id="likes"><i class="icon ion-ios-thumbs-up"></i> <%= number_with_separator(video.likes) %></p>
<p id="dislikes" style="display: none; visibility: hidden;"></p> <p id="dislikes" style="display: none; visibility: hidden;"></p>
<p id="genre"><%= I18n.translate(locale, "Genre: ") %> <p id="genre"><%= translate(locale, "Genre: ") %>
<% if !video.genre_url %> <% if !video.genre_url %>
<%= video.genre %> <%= video.genre %>
<% else %> <% else %>
@ -199,21 +199,21 @@ we're going to need to do it here in order to allow for translations.
</p> </p>
<% if video.license %> <% if video.license %>
<% if video.license.empty? %> <% if video.license.empty? %>
<p id="license"><%= I18n.translate(locale, "License: ") %><%= I18n.translate(locale, "Standard YouTube license") %></p> <p id="license"><%= translate(locale, "License: ") %><%= translate(locale, "Standard YouTube license") %></p>
<% else %> <% else %>
<p id="license"><%= I18n.translate(locale, "License: ") %><%= video.license %></p> <p id="license"><%= translate(locale, "License: ") %><%= video.license %></p>
<% end %> <% end %>
<% end %> <% end %>
<p id="family_friendly"><%= I18n.translate(locale, "Family friendly? ") %><%= I18n.translate_bool(locale, video.is_family_friendly) %></p> <p id="family_friendly"><%= translate(locale, "Family friendly? ") %><%= translate_bool(locale, video.is_family_friendly) %></p>
<p id="wilson" style="display: none; visibility: hidden;"></p> <p id="wilson" style="display: none; visibility: hidden;"></p>
<p id="rating" style="display: none; visibility: hidden;"></p> <p id="rating" style="display: none; visibility: hidden;"></p>
<p id="engagement" style="display: none; visibility: hidden;"></p> <p id="engagement" style="display: none; visibility: hidden;"></p>
<% if video.allowed_regions.size != REGIONS.size %> <% if video.allowed_regions.size != REGIONS.size %>
<p id="allowed_regions"> <p id="allowed_regions">
<% if video.allowed_regions.size < REGIONS.size // 2 %> <% if video.allowed_regions.size < REGIONS.size // 2 %>
<%= I18n.translate(locale, "Whitelisted regions: ") %><%= video.allowed_regions.join(", ") %> <%= translate(locale, "Whitelisted regions: ") %><%= video.allowed_regions.join(", ") %>
<% else %> <% else %>
<%= I18n.translate(locale, "Blacklisted regions: ") %><%= (REGIONS.to_a - video.allowed_regions).join(", ") %> <%= translate(locale, "Blacklisted regions: ") %><%= (REGIONS.to_a - video.allowed_regions).join(", ") %>
<% end %> <% end %>
</p> </p>
<% end %> <% end %>
@ -245,9 +245,9 @@ we're going to need to do it here in order to allow for translations.
<div class="h-box"> <div class="h-box">
<p id="published-date"> <p id="published-date">
<% if video.premiere_timestamp.try &.> Time.utc %> <% if video.premiere_timestamp.try &.> Time.utc %>
<b><%= video.premiere_timestamp.try { |t| I18n.translate(locale, "Premieres `x`", t.to_s("%B %-d, %R UTC")) } %></b> <b><%= video.premiere_timestamp.try { |t| translate(locale, "Premieres `x`", t.to_s("%B %-d, %R UTC")) } %></b>
<% else %> <% else %>
<b><%= I18n.translate(locale, "Shared `x`", video.published.to_s("%B %-d, %Y")) %></b> <b><%= translate(locale, "Shared `x`", video.published.to_s("%B %-d, %Y")) %></b>
<% end %> <% end %>
</p> </p>
@ -269,7 +269,7 @@ we're going to need to do it here in order to allow for translations.
<input id="music-desc-expansion" type="checkbox"/> <input id="music-desc-expansion" type="checkbox"/>
<label for="music-desc-expansion"> <label for="music-desc-expansion">
<h3 id="music-description-title"> <h3 id="music-description-title">
<%= I18n.translate(locale, "Music in this video") %> <%= translate(locale, "Music in this video") %>
<span class="icon ion-ios-arrow-up"></span> <span class="icon ion-ios-arrow-up"></span>
<span class="icon ion-ios-arrow-down"></span> <span class="icon ion-ios-arrow-down"></span>
</h3> </h3>
@ -278,9 +278,9 @@ we're going to need to do it here in order to allow for translations.
<div id="music-description-box"> <div id="music-description-box">
<% video.music.each do |music| %> <% video.music.each do |music| %>
<div class="music-item"> <div class="music-item">
<p class="music-song"><%= I18n.translate(locale, "Song: ") %><%= music.song %></p> <p class="music-song"><%= translate(locale, "Song: ") %><%= music.song %></p>
<p class="music-artist"><%= I18n.translate(locale, "Artist: ") %><%= music.artist %></p> <p class="music-artist"><%= translate(locale, "Artist: ") %><%= music.artist %></p>
<p class="music-album"><%= I18n.translate(locale, "Album: ") %><%= music.album %></p> <p class="music-album"><%= translate(locale, "Album: ") %><%= music.album %></p>
</div> </div>
<% end %> <% end %>
</div> </div>
@ -293,7 +293,7 @@ we're going to need to do it here in order to allow for translations.
<% else %> <% else %>
<noscript> <noscript>
<a href="/watch?<%= env.params.query %>&nojs=1"> <a href="/watch?<%= env.params.query %>&nojs=1">
<%= I18n.translate(locale, "Hi! Looks like you have JavaScript turned off. Click here to view comments, keep in mind they may take a bit longer to load.") %> <%= translate(locale, "Hi! Looks like you have JavaScript turned off. Click here to view comments, keep in mind they may take a bit longer to load.") %>
</a> </a>
</noscript> </noscript>
<% end %> <% end %>
@ -312,7 +312,7 @@ we're going to need to do it here in order to allow for translations.
<% if !video.related_videos.empty? %> <% if !video.related_videos.empty? %>
<div <% if plid %>style="display:none"<% end %>> <div <% if plid %>style="display:none"<% end %>>
<div class="pure-control-group"> <div class="pure-control-group">
<label for="continue"><%= I18n.translate(locale, "preferences_continue_label") %></label> <label for="continue"><%= translate(locale, "preferences_continue_label") %></label>
<input name="continue" id="continue" type="checkbox" <% if params.continue %>checked<% end %>> <input name="continue" id="continue" type="checkbox" <% if params.continue %>checked<% end %>>
</div> </div>
<hr> <hr>
@ -356,7 +356,7 @@ we're going to need to do it here in order to allow for translations.
<b class="width:100%"><%= <b class="width:100%"><%=
views = rv["view_count"]?.try &.to_i? views = rv["view_count"]?.try &.to_i?
views ||= rv["view_count_short"]?.try { |x| short_text_to_number(x) } views ||= rv["view_count_short"]?.try { |x| short_text_to_number(x) }
I18n.translate_count(locale, "generic_views_count", views || 0, I18n::NumberFormatting::Short) translate_count(locale, "generic_views_count", views || 0, NumberFormatting::Short)
%></b> %></b>
</div> </div>
</h5> </h5>

View File

@ -144,7 +144,7 @@ def get_ytimg_pool(subdomain)
if pool = YTIMG_POOLS[subdomain]? if pool = YTIMG_POOLS[subdomain]?
return pool return pool
else else
Log.forf.info { "Creating a new HTTP pool for \"https://#{subdomain}.ytimg.com\"" } Log.info { "get_ytimg_pool: Creating a new HTTP pool for \"https://#{subdomain}.ytimg.com\"" }
pool = YoutubeConnectionPool.new(URI.parse("https://#{subdomain}.ytimg.com"), capacity: CONFIG.pool_size) pool = YoutubeConnectionPool.new(URI.parse("https://#{subdomain}.ytimg.com"), capacity: CONFIG.pool_size)
YTIMG_POOLS[subdomain] = pool YTIMG_POOLS[subdomain] = pool

View File

@ -452,7 +452,7 @@ private module Parsers
end end
content_container["items"]?.try &.as_a.each do |item| content_container["items"]?.try &.as_a.each do |item|
result = YoutubeJSONParser.parse_item(item, author_fallback.name, author_fallback.id) result = parse_item(item, author_fallback.name, author_fallback.id)
contents << result if result.is_a?(SearchItem) contents << result if result.is_a?(SearchItem)
end end
@ -1017,79 +1017,74 @@ module HelperExtractors
end end
end end
module YoutubeJSONParser # Parses an item from Youtube's JSON response into a more usable structure.
extend self # The end result can either be a SearchVideo, SearchPlaylist or SearchChannel.
Log = ::Log.for(self) def parse_item(item : JSON::Any, author_fallback : String? = "", author_id_fallback : String? = "")
# We "allow" nil values but secretly use empty strings instead. This is to save us the
# hassle of modifying every author_fallback and author_id_fallback arg usage
# which is more often than not nil.
author_fallback = AuthorFallback.new(author_fallback || "", author_id_fallback || "")
# Parses an item from Youtube's JSON response into a more usable structure. # Cycles through all of the item parsers and attempt to parse the raw YT JSON data.
# The end result can either be a SearchVideo, SearchPlaylist or SearchChannel. # Each parser automatically validates the data given to see if the data is
def parse_item(item : JSON::Any, author_fallback : String? = "", author_id_fallback : String? = "") # applicable to itself. If not nil is returned and the next parser is attempted.
# We "allow" nil values but secretly use empty strings instead. This is to save us the ITEM_PARSERS.each do |parser|
# hassle of modifying every author_fallback and author_id_fallback arg usage Log.trace { "parse_item: Attempting to parse item using \"#{parser.parser_name}\" (cycling...)" }
# which is more often than not nil.
author_fallback = AuthorFallback.new(author_fallback || "", author_id_fallback || "")
# Cycles through all of the item parsers and attempt to parse the raw YT JSON data. if result = parser.process(item, author_fallback)
# Each parser automatically validates the data given to see if the data is Log.debug { "parse_item: Successfully parsed via #{parser.parser_name}" }
# applicable to itself. If not nil is returned and the next parser is attempted. return result
ITEM_PARSERS.each do |parser|
Log.trace { "Attempting to parse item using \"#{parser.parser_name}\" (cycling...)" }
if result = parser.process(item, author_fallback)
Log.debug { "Successfully parsed via #{parser.parser_name}" }
return result
else
Log.trace { "Parser \"#{parser.parser_name}\" does not apply. Cycling to the next one..." }
end
end
end
# Parses multiple items from YouTube's initial JSON response into a more usable structure.
# The end result is an array of SearchItem.
#
# This function yields the container so that items can be parsed separately.
#
def extract_items(initial_data : InitialData, &)
if unpackaged_data = initial_data["contents"]?.try &.as_h
elsif unpackaged_data = initial_data["response"]?.try &.as_h
elsif unpackaged_data = initial_data.dig?("onResponseReceivedActions", 1).try &.as_h
elsif unpackaged_data = initial_data.dig?("onResponseReceivedActions", 0).try &.as_h
else else
unpackaged_data = initial_data Log.trace { "parse_item: Parser \"#{parser.parser_name}\" does not apply. Cycling to the next one..." }
end end
# This is identical to the parser cycling of parse_item().
ITEM_CONTAINER_EXTRACTOR.each do |extractor|
Log.trace { "Attempting to extract item container using \"#{extractor.extractor_name}\" (cycling...)" }
if container = extractor.process(unpackaged_data)
Log.debug { "Successfully unpacked container with \"#{extractor.extractor_name}\"" }
# Extract items in container
container.each { |item| yield item }
else
Log.trace { "Extractor \"#{extractor.extractor_name}\" does not apply. Cycling to the next one..." }
end
end
end
# Wrapper using the block function above
def extract_items(
initial_data : InitialData,
author_fallback : String? = nil,
author_id_fallback : String? = nil,
) : {Array(SearchItem), String?}
items = [] of SearchItem
continuation = nil
extract_items(initial_data) do |item|
parsed = parse_item(item, author_fallback, author_id_fallback)
case parsed
when .is_a?(Continuation) then continuation = parsed.token
when .is_a?(SearchItem) then items << parsed
end
end
return items, continuation
end end
end end
# Parses multiple items from YouTube's initial JSON response into a more usable structure.
# The end result is an array of SearchItem.
#
# This function yields the container so that items can be parsed separately.
#
def extract_items(initial_data : InitialData, &)
if unpackaged_data = initial_data["contents"]?.try &.as_h
elsif unpackaged_data = initial_data["response"]?.try &.as_h
elsif unpackaged_data = initial_data.dig?("onResponseReceivedActions", 1).try &.as_h
elsif unpackaged_data = initial_data.dig?("onResponseReceivedActions", 0).try &.as_h
else
unpackaged_data = initial_data
end
# This is identical to the parser cycling of parse_item().
ITEM_CONTAINER_EXTRACTOR.each do |extractor|
Log.trace { "extract_items: Attempting to extract item container using \"#{extractor.extractor_name}\" (cycling...)" }
if container = extractor.process(unpackaged_data)
Log.debug { "extract_items: Successfully unpacked container with \"#{extractor.extractor_name}\"" }
# Extract items in container
container.each { |item| yield item }
else
Log.trace { "extract_items: Extractor \"#{extractor.extractor_name}\" does not apply. Cycling to the next one..." }
end
end
end
# Wrapper using the block function above
def extract_items(
initial_data : InitialData,
author_fallback : String? = nil,
author_id_fallback : String? = nil,
) : {Array(SearchItem), String?}
items = [] of SearchItem
continuation = nil
extract_items(initial_data) do |item|
parsed = parse_item(item, author_fallback, author_id_fallback)
case parsed
when .is_a?(Continuation) then continuation = parsed.token
when .is_a?(SearchItem) then items << parsed
end
end
return items, continuation
end

View File

@ -696,8 +696,8 @@ module YoutubeAPI
} }
# Logging # Logging
::Log.forf.debug { "Using endpoint: \"#{endpoint}\"" } Log.debug { "Invidious companion: Using endpoint: \"#{endpoint}\"" }
::Log.forf.trace { "POST data: #{data}" } Log.trace { "Invidious companion: POST data: #{data}" }
# Send the POST request # Send the POST request