diff --git a/config/config.example.yml b/config/config.example.yml
index 8d3e6212..ecd24a1e 100644
--- a/config/config.example.yml
+++ b/config/config.example.yml
@@ -289,7 +289,7 @@ https_only: false
## Logging Verbosity. This is overridden if "-l LEVEL" or
## "--log-level=LEVEL" are passed on the command line.
##
-## Accepted values: All, Trace, Debug, Info, Warn, Error, Fatal, Off
+## Accepted values: Trace, Debug, Info, Notice, Warn, Error, Fatal, None
## Default: Info
##
#log_level: Info
diff --git a/scripts/generate_js_licenses.cr b/scripts/generate_js_licenses.cr
index 1f4ffa62..7df70bf2 100644
--- a/scripts/generate_js_licenses.cr
+++ b/scripts/generate_js_licenses.cr
@@ -24,7 +24,7 @@ def create_licence_tr(path, file_name, licence_name, licence_link, source_locati
"
-
#{translate(locale, "crash_page_you_found_a_bug")}
+
#{I18n.translate(locale, "crash_page_you_found_a_bug")}
-
#{translate(locale, "crash_page_before_reporting")}
+
#{I18n.translate(locale, "crash_page_before_reporting")}
- - #{translate(locale, "crash_page_refresh", env.request.resource)}
- - #{translate(locale, "crash_page_switch_instance", url_switch)}
- - #{translate(locale, "crash_page_read_the_faq", url_faq)}
- - #{translate(locale, "crash_page_search_issue", url_search_issues)}
+ - #{I18n.translate(locale, "crash_page_refresh", env.request.resource)}
+ - #{I18n.translate(locale, "crash_page_switch_instance", url_switch)}
+ - #{I18n.translate(locale, "crash_page_read_the_faq", url_faq)}
+ - #{I18n.translate(locale, "crash_page_search_issue", url_search_issues)}
-
#{translate(locale, "crash_page_report_issue", url_new_issue)}
+
#{I18n.translate(locale, "crash_page_report_issue", url_new_issue)}
#{issue_template}
@@ -95,7 +95,7 @@ def error_template_helper(env : HTTP::Server::Context, status_code : Int32, mess
locale = env.get("preferences").as(Preferences).locale
- error_message = translate(locale, message)
+ error_message = I18n.translate(locale, message)
next_steps = error_redirect_helper(env)
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") ||
request_path.starts_with?("/channel") || request_path.starts_with?("/playlist?list=PL")
- next_steps_text = translate(locale, "next_steps_error_message")
- refresh = translate(locale, "next_steps_error_message_refresh")
- go_to_youtube = translate(locale, "next_steps_error_message_go_to_youtube")
- switch_instance = translate(locale, "Switch Invidious Instance")
+ next_steps_text = I18n.translate(locale, "next_steps_error_message")
+ refresh = I18n.translate(locale, "next_steps_error_message_refresh")
+ go_to_youtube = I18n.translate(locale, "next_steps_error_message_go_to_youtube")
+ switch_instance = I18n.translate(locale, "Switch Invidious Instance")
return <<-END_HTML
#{next_steps_text}
diff --git a/src/invidious/helpers/i18n.cr b/src/invidious/helpers/i18n.cr
index bca2edda..944feb51 100644
--- a/src/invidious/helpers/i18n.cr
+++ b/src/invidious/helpers/i18n.cr
@@ -1,199 +1,202 @@
-# Languages requiring a better level of translation (at least 20%)
-# to be added to the list below:
-#
-# "af" => "", # Afrikaans
-# "az" => "", # Azerbaijani
-# "be" => "", # Belarusian
-# "bn_BD" => "", # Bengali (Bangladesh)
-# "ia" => "", # Interlingua
-# "or" => "", # Odia
-# "tk" => "", # Turkmen
-# "tok => "", # Toki Pona
-#
-LOCALES_LIST = {
- "ar" => "العربية", # Arabic
- "bg" => "български", # Bulgarian
- "bn" => "বাংলা", # Bengali
- "ca" => "Català", # Catalan
- "cs" => "Čeština", # Czech
- "cy" => "Cymraeg", # Welsh
- "da" => "Dansk", # Danish
- "de" => "Deutsch", # German
- "el" => "Ελληνικά", # Greek
- "en-US" => "English", # English
- "eo" => "Esperanto", # Esperanto
- "es" => "Español", # Spanish
- "et" => "Eesti keel", # Estonian
- "eu" => "Euskara", # Basque
- "fa" => "فارسی", # Persian
- "fi" => "Suomi", # Finnish
- "fr" => "Français", # French
- "he" => "עברית", # Hebrew
- "hi" => "हिन्दी", # Hindi
- "hr" => "Hrvatski", # Croatian
- "hu-HU" => "Magyar Nyelv", # Hungarian
- "id" => "Bahasa Indonesia", # Indonesian
- "is" => "Íslenska", # Icelandic
- "it" => "Italiano", # Italian
- "ja" => "日本語", # Japanese
- "ko" => "한국어", # Korean
- "lmo" => "Lombard", # Lombard
- "lt" => "Lietuvių", # Lithuanian
- "nb-NO" => "Norsk bokmål", # Norwegian Bokmål
- "nl" => "Nederlands", # Dutch
- "pl" => "Polski", # Polish
- "pt" => "Português", # Portuguese
- "pt-BR" => "Português Brasileiro", # Portuguese (Brazil)
- "pt-PT" => "Português de Portugal", # Portuguese (Portugal)
- "ro" => "Română", # Romanian
- "ru" => "Русский", # Russian
- "si" => "සිංහල", # Sinhala
- "sk" => "Slovenčina", # Slovak
- "sl" => "Slovenščina", # Slovenian
- "sq" => "Shqip", # Albanian
- "sr" => "Srpski (latinica)", # Serbian (Latin)
- "sr_Cyrl" => "Српски (ћирилица)", # Serbian (Cyrillic)
- "sv-SE" => "Svenska", # Swedish
- "ta" => "தமிழ்", # Tamil
- "tr" => "Türkçe", # Turkish
- "uk" => "Українська", # Ukrainian
- "vi" => "Tiếng Việt", # Vietnamese
- "zh-CN" => "汉语", # Chinese (Simplified)
- "zh-TW" => "漢語", # Chinese (Traditional)
-}
+module I18n
+ extend self
+ # Languages requiring a better level of translation (at least 20%)
+ # to be added to the list below:
+ #
+ # "af" => "", # Afrikaans
+ # "az" => "", # Azerbaijani
+ # "be" => "", # Belarusian
+ # "bn_BD" => "", # Bengali (Bangladesh)
+ # "ia" => "", # Interlingua
+ # "or" => "", # Odia
+ # "tk" => "", # Turkmen
+ # "tok => "", # Toki Pona
+ #
+ LOCALES_LIST = {
+ "ar" => "العربية", # Arabic
+ "bg" => "български", # Bulgarian
+ "bn" => "বাংলা", # Bengali
+ "ca" => "Català", # Catalan
+ "cs" => "Čeština", # Czech
+ "cy" => "Cymraeg", # Welsh
+ "da" => "Dansk", # Danish
+ "de" => "Deutsch", # German
+ "el" => "Ελληνικά", # Greek
+ "en-US" => "English", # English
+ "eo" => "Esperanto", # Esperanto
+ "es" => "Español", # Spanish
+ "et" => "Eesti keel", # Estonian
+ "eu" => "Euskara", # Basque
+ "fa" => "فارسی", # Persian
+ "fi" => "Suomi", # Finnish
+ "fr" => "Français", # French
+ "he" => "עברית", # Hebrew
+ "hi" => "हिन्दी", # Hindi
+ "hr" => "Hrvatski", # Croatian
+ "hu-HU" => "Magyar Nyelv", # Hungarian
+ "id" => "Bahasa Indonesia", # Indonesian
+ "is" => "Íslenska", # Icelandic
+ "it" => "Italiano", # Italian
+ "ja" => "日本語", # Japanese
+ "ko" => "한국어", # Korean
+ "lmo" => "Lombard", # Lombard
+ "lt" => "Lietuvių", # Lithuanian
+ "nb-NO" => "Norsk bokmål", # Norwegian Bokmål
+ "nl" => "Nederlands", # Dutch
+ "pl" => "Polski", # Polish
+ "pt" => "Português", # Portuguese
+ "pt-BR" => "Português Brasileiro", # Portuguese (Brazil)
+ "pt-PT" => "Português de Portugal", # Portuguese (Portugal)
+ "ro" => "Română", # Romanian
+ "ru" => "Русский", # Russian
+ "si" => "සිංහල", # Sinhala
+ "sk" => "Slovenčina", # Slovak
+ "sl" => "Slovenščina", # Slovenian
+ "sq" => "Shqip", # Albanian
+ "sr" => "Srpski (latinica)", # Serbian (Latin)
+ "sr_Cyrl" => "Српски (ћирилица)", # Serbian (Cyrillic)
+ "sv-SE" => "Svenska", # Swedish
+ "ta" => "தமிழ்", # Tamil
+ "tr" => "Türkçe", # Turkish
+ "uk" => "Українська", # Ukrainian
+ "vi" => "Tiếng Việt", # Vietnamese
+ "zh-CN" => "汉语", # Chinese (Simplified)
+ "zh-TW" => "漢語", # Chinese (Traditional)
+ }
-LOCALES = load_all_locales()
+ LOCALES = load_all_locales()
-CONTENT_REGIONS = {
- "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",
- "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",
- "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",
- "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",
- "YE", "ZA", "ZW",
-}
+ CONTENT_REGIONS = {
+ "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",
+ "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",
+ "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",
+ "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",
+ "YE", "ZA", "ZW",
+ }
-# Enum for the different types of number formats
-enum NumberFormatting
- None # Print the number as-is
- Separator # Use a separator for thousands
- Short # Use short notation (k/M/B)
- HtmlSpan # Surround with
-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
+ # Enum for the different types of number formats
+ enum NumberFormatting
+ None # Print the number as-is
+ Separator # Use a separator for thousands
+ Short # Use short notation (k/M/B)
+ HtmlSpan # Surround with
end
- return locales
-end
+ def load_all_locales
+ locales = {} of String => Hash(String, JSON::Any)
-def translate(locale : String?, key : String, text : String | Hash(String, String) | Nil = nil) : String
- # Log a warning if "key" doesn't exist in en-US locale and return
- # that key as the text, so this is more or less transparent to the user.
- if !LOCALES["en-US"].has_key?(key)
- LOGGER.warn("i18n: Missing translation key \"#{key}\"")
- return key
+ LOCALES_LIST.each_key do |name|
+ locales[name] = JSON.parse(File.read("locales/#{name}.json")).as_h
+ end
+
+ return locales
end
- # Default to english, whenever the locale doesn't exist,
- # or the key requested has not been translated
- if locale && LOCALES.has_key?(locale) && LOCALES[locale].has_key?(key)
- raw_data = LOCALES[locale][key]
- else
- raw_data = LOCALES["en-US"][key]
- end
+ def translate(locale : String?, key : String, text : String | Hash(String, String) | Nil = nil) : String
+ # Log a warning if "key" doesn't exist in en-US locale and return
+ # that key as the text, so this is more or less transparent to the user.
+ if !LOCALES["en-US"].has_key?(key)
+ Log.warn { "Missing translation key \"#{key}\"" }
+ return key
+ end
- case raw_data
- when .as_h?
- # Init
- translation = ""
- match_length = 0
+ # Default to english, whenever the locale doesn't exist,
+ # or the key requested has not been translated
+ if locale && LOCALES.has_key?(locale) && LOCALES[locale].has_key?(key)
+ raw_data = LOCALES[locale][key]
+ else
+ raw_data = LOCALES["en-US"][key]
+ end
- 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
+ case raw_data
+ when .as_h?
+ # Init
+ translation = ""
+ match_length = 0
+
+ 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
- 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)
+ when .as_s?
+ translation = raw_data.as_s
else
- # Return key if we're already in english, as the translation is missing
- LOGGER.warn("i18n: Missing translation key \"#{key}\"")
- return key
+ 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
+ # 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 = "
" + count.to_s + ""
+ 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
- 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 = "
" + count.to_s + ""
- else count_txt = count.to_s
- end
+ def locale_is_rtl?(locale : String?)
+ # Fallback to en-US
+ return false if locale.nil?
- 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")
+ # Arabic, Persian, Hebrew
+ # See https://en.wikipedia.org/wiki/Right-to-left_script#List_of_RTL_scripts
+ return {"ar", "fa", "he"}.includes? locale
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
diff --git a/src/invidious/helpers/i18next.cr b/src/invidious/helpers/i18next.cr
index 684e6d14..023fae97 100644
--- a/src/invidious/helpers/i18next.cr
+++ b/src/invidious/helpers/i18next.cr
@@ -145,7 +145,7 @@ module I18next::Plurals
if version > 4 || version == 0
raise "Invalid i18next version: v#{version}."
elsif version == 4
- # Logger.error("Unsupported i18next version: v4. Falling back to v3")
+ # Log.error { "Unsupported i18next version: v4. Falling back to v3" }
@version = 3_u8
else
@version = version.to_u8
diff --git a/src/invidious/helpers/logger.cr b/src/invidious/helpers/logger.cr
index 03349595..a8043d51 100644
--- a/src/invidious/helpers/logger.cr
+++ b/src/invidious/helpers/logger.cr
@@ -1,72 +1,46 @@
require "colorize"
-enum LogLevel
- All = 0
- Trace = 1
- Debug = 2
- Info = 3
- Warn = 4
- Error = 5
- Fatal = 6
- Off = 7
-end
+module Invidious::Logger
+ extend self
-class Invidious::LogHandler < Kemal::BaseLogHandler
- def initialize(@io : IO = STDOUT, @level = LogLevel::Debug, use_color : Bool = true)
+ def formatter(use_color : Bool = true)
Colorize.enabled = use_color
Colorize.on_tty_only!
- end
- def call(context : HTTP::Server::Context)
- elapsed_time = Time.measure { call_next(context) }
- elapsed_text = elapsed_text(elapsed_time)
+ formatter = ::Log::Formatter.new do |entry, io|
+ message = entry.message
+ severity = entry.severity
+ data = entry.data
+ source = entry.source
+ timestamp = entry.timestamp
- # Default: full path with parameters
- requested_url = context.request.resource
-
- # Try not to log search queries passed as GET parameters during normal use
- # (They will still be logged if log level is 'Debug' or 'Trace')
- if @level > LogLevel::Debug && (
- requested_url.downcase.includes?("search") || requested_url.downcase.includes?("q=")
- )
- # Log only the path
- requested_url = context.request.path
- end
-
- info("#{context.response.status_code} #{context.request.method} #{requested_url} #{elapsed_text}")
-
- context
- end
-
- def write(message : String)
- @io << message
- @io.flush
- end
-
- def color(level)
- case level
- when LogLevel::Trace then :cyan
- when LogLevel::Debug then :green
- when LogLevel::Info then :white
- when LogLevel::Warn then :yellow
- when LogLevel::Error then :red
- when LogLevel::Fatal then :magenta
- else :default
- end
- end
-
- {% for level in %w(trace debug info warn error fatal) %}
- def {{level.id}}(message : String)
- if LogLevel::{{level.id.capitalize}} >= @level
- puts("#{Time.utc} [{{level.id}}] #{message}".colorize(color(LogLevel::{{level.id.capitalize}})))
+ io << (use_color ? timestamp.colorize(:dark_gray) : timestamp) << " "
+ io << (use_color ? colorize_severity(severity) : severity.label) << " "
+ io << (use_color ? source.colorize(:dark_gray) : source) << ": " if !source.empty?
+ io << message
+ if !data.empty?
+ io << " "
+ data.each do |dat|
+ io << (use_color ? dat[0].to_s.colorize(:light_cyan) : dat[0].to_s)
+ io << "="
+ io << dat[1].to_s
+ end
end
end
- {% end %}
- private def elapsed_text(elapsed)
- millis = elapsed.total_milliseconds
- return "#{millis.round(2)}ms" if millis >= 1
+ return formatter
+ end
- "#{(millis * 1000).round(2)}µs"
+ private def colorize_severity(severity : Log::Severity)
+ case severity
+ in Log::Severity::Trace then severity.label.colorize(:cyan)
+ in Log::Severity::Info then severity.label.colorize(:green)
+ in Log::Severity::Notice then severity.label.colorize(:light_yellow)
+ in Log::Severity::Warn then severity.label.colorize(:yellow)
+ in Log::Severity::Error then severity.label.colorize(:red)
+ in Log::Severity::Fatal then severity.label.colorize(:red)
+ in Log::Severity::Debug then severity.label
+ in Log::Severity::None then severity.label
+ end
end
end
diff --git a/src/invidious/helpers/macros.cr b/src/invidious/helpers/macros.cr
index 84847321..0278ab2a 100644
--- a/src/invidious/helpers/macros.cr
+++ b/src/invidious/helpers/macros.cr
@@ -70,3 +70,9 @@ macro haltf(env, status_code = 200, response = "")
{{env}}.response.close
return
end
+
+class Log
+ macro forf
+ Log.for({{@def.name.stringify}})
+ end
+end
diff --git a/src/invidious/helpers/serialized_yt_data.cr b/src/invidious/helpers/serialized_yt_data.cr
index 2796a8dc..df040d06 100644
--- a/src/invidious/helpers/serialized_yt_data.cr
+++ b/src/invidious/helpers/serialized_yt_data.cr
@@ -115,9 +115,9 @@ struct SearchVideo
json.field "descriptionHtml", self.description_html
json.field "viewCount", self.views
- json.field "viewCountText", translate_count(locale, "generic_views_count", self.views, NumberFormatting::Short)
+ json.field "viewCountText", I18n.translate_count(locale, "generic_views_count", self.views, I18n::NumberFormatting::Short)
json.field "published", self.published.to_unix
- json.field "publishedText", translate(locale, "`x` ago", recode_date(self.published, locale))
+ json.field "publishedText", I18n.translate(locale, "`x` ago", recode_date(self.published, locale))
json.field "lengthSeconds", self.length_seconds
json.field "liveNow", self.badges.live_now?
json.field "premium", self.badges.premium?
@@ -327,8 +327,8 @@ struct ProblematicTimelineItem
xml.element("content", type: "xhtml") do
xml.element("div", xmlns: "http://www.w3.org/1999/xhtml") do
xml.element("div") do
- xml.element("h4") { translate(locale, "timeline_parse_error_placeholder_heading") }
- xml.element("p") { translate(locale, "timeline_parse_error_placeholder_message") }
+ xml.element("h4") { I18n.translate(locale, "timeline_parse_error_placeholder_heading") }
+ xml.element("p") { I18n.translate(locale, "timeline_parse_error_placeholder_message") }
end
xml.element("pre") do
diff --git a/src/invidious/helpers/sig_helper.cr b/src/invidious/helpers/sig_helper.cr
index 6d198a42..65c8618f 100644
--- a/src/invidious/helpers/sig_helper.cr
+++ b/src/invidious/helpers/sig_helper.cr
@@ -73,6 +73,8 @@ module Invidious::SigHelper
# ----------------------
class Client
+ Log = ::Log.for(self)
+
@mux : Multiplexor
def initialize(uri_or_path)
@@ -156,8 +158,8 @@ module Invidious::SigHelper
slice = channel.receive
return yield slice
rescue ex
- LOGGER.debug("SigHelper: Error when sending a request")
- LOGGER.trace(ex.inspect_with_backtrace)
+ Log.debug { "Error when sending a request" }
+ Log.trace { ex.inspect_with_backtrace }
return nil
end
end
@@ -167,6 +169,8 @@ module Invidious::SigHelper
# ---------------------
class Multiplexor
+ Log = ::Log.for(self)
+
alias TransactionID = UInt32
record Transaction, channel = ::Channel(Bytes).new
@@ -185,22 +189,22 @@ module Invidious::SigHelper
def listen : Nil
raise "Socket is closed" if @conn.closed?
- LOGGER.debug("SigHelper: Multiplexor listening")
+ Log.debug { "Multiplexor listening" }
spawn do
loop do
begin
receive_data
rescue ex
- LOGGER.info("SigHelper: Connection to helper died with '#{ex.message}' trying to reconnect...")
+ Log.info { "Connection to helper died with '#{ex.message}' trying to reconnect..." }
# We close the socket because for some reason is not closed.
@conn.close
loop do
begin
@conn = Connection.new(@uri_or_path)
- LOGGER.info("SigHelper: Reconnected to SigHelper!")
+ Log.info { "Reconnected to SigHelper!" }
rescue ex
- LOGGER.debug("SigHelper: Reconnection to helper unsuccessful with error '#{ex.message}'. Retrying")
+ Log.debug { "Reconnection to helper unsuccessful with error '#{ex.message}'. Retrying" }
sleep 500.milliseconds
next
end
@@ -238,7 +242,7 @@ module Invidious::SigHelper
if transaction = @queue.delete(transaction_id)
# Remove transaction from queue and send data to the channel
transaction.channel.send(slice)
- LOGGER.trace("SigHelper: Transaction unqueued and data sent to channel")
+ Log.trace { "Transaction unqueued and data sent to channel" }
else
raise Exception.new("SigHelper: Received transaction was not in queue")
end
@@ -251,7 +255,7 @@ module Invidious::SigHelper
transaction_id = @conn.read_bytes(UInt32, NetworkEndian)
length = @conn.read_bytes(UInt32, NetworkEndian)
- LOGGER.trace("SigHelper: Recv transaction 0x#{transaction_id.to_s(base: 16)} / length #{length}")
+ Log.trace { "Recv transaction 0x#{transaction_id.to_s(base: 16)} / length #{length}" }
if length > 67_000
raise Exception.new("SigHelper: Packet longer than expected (#{length})")
@@ -261,15 +265,15 @@ module Invidious::SigHelper
slice = Bytes.new(length)
@conn.read(slice) if length > 0
- LOGGER.trace("SigHelper: payload = #{slice}")
- LOGGER.trace("SigHelper: Recv transaction 0x#{transaction_id.to_s(base: 16)} - Done")
+ Log.trace { "payload = #{slice}" }
+ Log.trace { "Recv transaction 0x#{transaction_id.to_s(base: 16)} - Done" }
return transaction_id, slice
end
# Write a single packet to the socket
private def write_packet(transaction_id : TransactionID, request : Request)
- LOGGER.trace("SigHelper: Send transaction 0x#{transaction_id.to_s(base: 16)} / opcode #{request.opcode}")
+ Log.trace { "Send transaction 0x#{transaction_id.to_s(base: 16)} / opcode #{request.opcode}" }
io = IO::Memory.new(1024)
io.write_bytes(request.opcode.to_u8, NetworkEndian)
@@ -282,11 +286,13 @@ module Invidious::SigHelper
@conn.send(io)
@conn.flush
- LOGGER.trace("SigHelper: Send transaction 0x#{transaction_id.to_s(base: 16)} - Done")
+ Log.trace { "Send transaction 0x#{transaction_id.to_s(base: 16)} - Done" }
end
end
class Connection
+ Log = ::Log.for(self)
+
@socket : UNIXSocket | TCPSocket
{% if flag?(:advanced_debug) %}
@@ -309,7 +315,7 @@ module Invidious::SigHelper
uri = URI.parse("tcp://#{host_or_path}")
@socket = TCPSocket.new(uri.host.not_nil!, uri.port.not_nil!)
end
- LOGGER.info("SigHelper: Using helper at '#{host_or_path}'")
+ Log.info { "Using helper at '#{host_or_path}'" }
{% if flag?(:advanced_debug) %}
@io = IO::Hexdump.new(@socket, output: STDERR, read: true, write: true)
diff --git a/src/invidious/helpers/signatures.cr b/src/invidious/helpers/signatures.cr
index 82a28fc0..532618a4 100644
--- a/src/invidious/helpers/signatures.cr
+++ b/src/invidious/helpers/signatures.cr
@@ -2,6 +2,8 @@ require "http/params"
require "./sig_helper"
class Invidious::DecryptFunction
+ Log = ::Log.for(self).for("Signature")
+
@last_update : Time = Time.utc - 42.days
def initialize(uri_or_path)
@@ -18,7 +20,7 @@ class Invidious::DecryptFunction
update_time_elapsed = (@client.get_player_timestamp || 301).seconds
if update_time_elapsed > 5.minutes
- LOGGER.debug("Signature: Player might be outdated, updating")
+ Log.debug { "Player might be outdated, updating" }
@client.force_update
@last_update = Time.utc
end
@@ -28,8 +30,8 @@ class Invidious::DecryptFunction
self.check_update
return @client.decrypt_n_param(n)
rescue ex
- LOGGER.debug(ex.message || "Signature: Unknown error")
- LOGGER.trace(ex.inspect_with_backtrace)
+ Log.debug { ex.message || "Unknown error" }
+ Log.trace { ex.inspect_with_backtrace }
return nil
end
@@ -37,8 +39,8 @@ class Invidious::DecryptFunction
self.check_update
return @client.decrypt_sig(str)
rescue ex
- LOGGER.debug(ex.message || "Signature: Unknown error")
- LOGGER.trace(ex.inspect_with_backtrace)
+ Log.debug { ex.message || "Unknown error" }
+ Log.trace { ex.inspect_with_backtrace }
return nil
end
@@ -46,8 +48,8 @@ class Invidious::DecryptFunction
self.check_update
return @client.get_signature_timestamp
rescue ex
- LOGGER.debug(ex.message || "Signature: Unknown error")
- LOGGER.trace(ex.inspect_with_backtrace)
+ Log.debug { ex.message || "Unknown error" }
+ Log.trace { ex.inspect_with_backtrace }
return nil
end
end
diff --git a/src/invidious/helpers/utils.cr b/src/invidious/helpers/utils.cr
index 5637e533..ae160530 100644
--- a/src/invidious/helpers/utils.cr
+++ b/src/invidious/helpers/utils.cr
@@ -144,19 +144,19 @@ def recode_date(time : Time, locale)
span = Time.utc - time
if span.total_days > 365.0
- return translate_count(locale, "generic_count_years", span.total_days.to_i // 365)
+ return I18n.translate_count(locale, "generic_count_years", span.total_days.to_i // 365)
elsif span.total_days > 30.0
- return translate_count(locale, "generic_count_months", span.total_days.to_i // 30)
+ return I18n.translate_count(locale, "generic_count_months", span.total_days.to_i // 30)
elsif span.total_days > 7.0
- return translate_count(locale, "generic_count_weeks", span.total_days.to_i // 7)
+ return I18n.translate_count(locale, "generic_count_weeks", span.total_days.to_i // 7)
elsif span.total_hours > 24.0
- return translate_count(locale, "generic_count_days", span.total_days.to_i)
+ return I18n.translate_count(locale, "generic_count_days", span.total_days.to_i)
elsif span.total_minutes > 60.0
- return translate_count(locale, "generic_count_hours", span.total_hours.to_i)
+ return I18n.translate_count(locale, "generic_count_hours", span.total_hours.to_i)
elsif span.total_seconds > 60.0
- return translate_count(locale, "generic_count_minutes", span.total_minutes.to_i)
+ return I18n.translate_count(locale, "generic_count_minutes", span.total_minutes.to_i)
else
- return translate_count(locale, "generic_count_seconds", span.total_seconds.to_i)
+ return I18n.translate_count(locale, "generic_count_seconds", span.total_seconds.to_i)
end
end
diff --git a/src/invidious/jobs/clear_expired_items_job.cr b/src/invidious/jobs/clear_expired_items_job.cr
index 17191aac..e8c87768 100644
--- a/src/invidious/jobs/clear_expired_items_job.cr
+++ b/src/invidious/jobs/clear_expired_items_job.cr
@@ -1,11 +1,13 @@
class Invidious::Jobs::ClearExpiredItemsJob < Invidious::Jobs::BaseJob
+ Log = ::Log.for(self)
+
# Remove items (videos, nonces, etc..) whose cache is outdated every hour.
# Removes the need for a cron job.
def begin
loop do
failed = false
- LOGGER.info("jobs: running ClearExpiredItems job")
+ Log.info { "running ClearExpiredItemsJob job" }
begin
Invidious::Database::Videos.delete_expired
@@ -16,10 +18,10 @@ class Invidious::Jobs::ClearExpiredItemsJob < Invidious::Jobs::BaseJob
# Retry earlier than scheduled on DB error
if failed
- LOGGER.info("jobs: ClearExpiredItems failed. Retrying in 10 minutes.")
+ Log.info { "ClearExpiredItems failed. Retrying in 10 minutes." }
sleep 10.minutes
else
- LOGGER.info("jobs: ClearExpiredItems done.")
+ Log.info { "ClearExpiredItems done." }
sleep 1.hour
end
end
diff --git a/src/invidious/jobs/instance_refresh_job.cr b/src/invidious/jobs/instance_refresh_job.cr
index cb4280b9..642dca26 100644
--- a/src/invidious/jobs/instance_refresh_job.cr
+++ b/src/invidious/jobs/instance_refresh_job.cr
@@ -1,4 +1,6 @@
class Invidious::Jobs::InstanceListRefreshJob < Invidious::Jobs::BaseJob
+ Log = ::Log.for(self)
+
# We update the internals of a constant as so it can be accessed from anywhere
# within the codebase
#
@@ -12,7 +14,7 @@ class Invidious::Jobs::InstanceListRefreshJob < Invidious::Jobs::BaseJob
def begin
loop do
refresh_instances
- LOGGER.info("InstanceListRefreshJob: Done, sleeping for 30 minutes")
+ Log.info { "Done, sleeping for 30 minutes" }
sleep 30.minute
Fiber.yield
end
@@ -43,9 +45,9 @@ class Invidious::Jobs::InstanceListRefreshJob < Invidious::Jobs::BaseJob
filtered_instance_list << {info["region"].as_s, domain.as_s}
rescue ex
if domain
- LOGGER.info("InstanceListRefreshJob: failed to parse information from '#{domain}' because \"#{ex}\"\n\"#{ex.backtrace.join('\n')}\" ")
+ Log.info { "failed to parse information from '#{domain}' because \"#{ex}\"\n\"#{ex.backtrace.join('\n')}\" " }
else
- LOGGER.info("InstanceListRefreshJob: failed to parse information from an instance because \"#{ex}\"\n\"#{ex.backtrace.join('\n')}\" ")
+ Log.info { "failed to parse information from an instance because \"#{ex}\"\n\"#{ex.backtrace.join('\n')}\" " }
end
end
end
diff --git a/src/invidious/jobs/notification_job.cr b/src/invidious/jobs/notification_job.cr
index 968ee47f..ede5ea75 100644
--- a/src/invidious/jobs/notification_job.cr
+++ b/src/invidious/jobs/notification_job.cr
@@ -22,6 +22,8 @@ struct VideoNotification
end
class Invidious::Jobs::NotificationJob < Invidious::Jobs::BaseJob
+ Log = ::Log.for(self)
+
private getter notification_channel : ::Channel(VideoNotification)
private getter connection_channel : ::Channel({Bool, ::Channel(PQ::Notification)})
private getter pg_url : URI
@@ -57,7 +59,7 @@ class Invidious::Jobs::NotificationJob < Invidious::Jobs::BaseJob
spawn do
loop do
begin
- LOGGER.debug("NotificationJob: waking up")
+ Log.debug { "waking up" }
cloned = {} of String => Set(VideoNotification)
notify_mutex.synchronize do
cloned = to_notify.clone
@@ -69,7 +71,7 @@ class Invidious::Jobs::NotificationJob < Invidious::Jobs::BaseJob
next
end
- LOGGER.info("NotificationJob: updating channel #{channel_id} with #{notifications.size} notifications")
+ Log.info { "updating channel #{channel_id} with #{notifications.size} notifications" }
if CONFIG.enable_user_notifications
video_ids = notifications.map(&.video_id)
Invidious::Database::Users.add_multiple_notifications(channel_id, video_ids)
@@ -89,9 +91,9 @@ class Invidious::Jobs::NotificationJob < Invidious::Jobs::BaseJob
end
end
- LOGGER.trace("NotificationJob: Done, sleeping")
+ Log.trace { "Done, sleeping" }
rescue ex
- LOGGER.error("NotificationJob: #{ex.message}")
+ Log.error { ex.message }
end
sleep 1.minute
Fiber.yield
diff --git a/src/invidious/jobs/refresh_channels_job.cr b/src/invidious/jobs/refresh_channels_job.cr
index 80812a63..ef29402d 100644
--- a/src/invidious/jobs/refresh_channels_job.cr
+++ b/src/invidious/jobs/refresh_channels_job.cr
@@ -1,4 +1,6 @@
class Invidious::Jobs::RefreshChannelsJob < Invidious::Jobs::BaseJob
+ Log = ::Log.for(self)
+
private getter db : DB::Database
def initialize(@db)
@@ -12,37 +14,37 @@ class Invidious::Jobs::RefreshChannelsJob < Invidious::Jobs::BaseJob
backoff = 2.minutes
loop do
- LOGGER.debug("RefreshChannelsJob: Refreshing all channels")
+ Log.debug { "Refreshing all channels" }
PG_DB.query("SELECT id FROM channels ORDER BY updated") do |rs|
rs.each do
id = rs.read(String)
if active_fibers >= lim_fibers
- LOGGER.trace("RefreshChannelsJob: Fiber limit reached, waiting...")
+ Log.trace { "Fiber limit reached, waiting..." }
if active_channel.receive
- LOGGER.trace("RefreshChannelsJob: Fiber limit ok, continuing")
+ Log.trace { "Fiber limit ok, continuing" }
active_fibers -= 1
end
end
- LOGGER.debug("RefreshChannelsJob: #{id} : Spawning fiber")
+ Log.debug { "#{id} : Spawning fiber" }
active_fibers += 1
spawn do
begin
- LOGGER.trace("RefreshChannelsJob: #{id} fiber : Fetching channel")
+ Log.trace { "#{id} fiber : Fetching channel" }
channel = fetch_channel(id, pull_all_videos: CONFIG.full_refresh)
lim_fibers = max_fibers
- LOGGER.trace("RefreshChannelsJob: #{id} fiber : Updating DB")
+ Log.trace { "#{id} fiber : Updating DB" }
Invidious::Database::Channels.update_author(id, channel.author)
rescue ex
- LOGGER.error("RefreshChannelsJob: #{id} : #{ex.message}")
+ Log.error { "#{id} : #{ex.message}" }
if ex.message == "Deleted or invalid channel"
Invidious::Database::Channels.update_mark_deleted(id)
else
lim_fibers = 1
- LOGGER.error("RefreshChannelsJob: #{id} fiber : backing off for #{backoff}s")
+ Log.error { "#{id} fiber : backing off for #{backoff}s" }
sleep backoff
if backoff < 1.days
backoff += backoff
@@ -51,14 +53,14 @@ class Invidious::Jobs::RefreshChannelsJob < Invidious::Jobs::BaseJob
end
end
ensure
- LOGGER.debug("RefreshChannelsJob: #{id} fiber : Done")
+ Log.debug { "#{id} fiber : Done" }
active_channel.send(true)
end
end
end
end
- LOGGER.debug("RefreshChannelsJob: Done, sleeping for #{CONFIG.channel_refresh_interval}")
+ Log.debug { "Done, sleeping for #{CONFIG.channel_refresh_interval}" }
sleep CONFIG.channel_refresh_interval
Fiber.yield
end
diff --git a/src/invidious/jobs/refresh_feeds_job.cr b/src/invidious/jobs/refresh_feeds_job.cr
index 4f8130df..37508c3c 100644
--- a/src/invidious/jobs/refresh_feeds_job.cr
+++ b/src/invidious/jobs/refresh_feeds_job.cr
@@ -1,4 +1,6 @@
class Invidious::Jobs::RefreshFeedsJob < Invidious::Jobs::BaseJob
+ Log = ::Log.for(self)
+
private getter db : DB::Database
def initialize(@db)
@@ -28,14 +30,14 @@ class Invidious::Jobs::RefreshFeedsJob < Invidious::Jobs::BaseJob
column_array = Invidious::Database.get_column_array(db, view_name)
ChannelVideo.type_array.each_with_index do |name, i|
if name != column_array[i]?
- LOGGER.info("RefreshFeedsJob: DROP MATERIALIZED VIEW #{view_name}")
+ Log.info { "DROP MATERIALIZED VIEW #{view_name}" }
db.exec("DROP MATERIALIZED VIEW #{view_name}")
raise "view does not exist"
end
end
if !db.query_one("SELECT pg_get_viewdef('#{view_name}')", as: String).includes? "WHERE ((cv.ucid = ANY (u.subscriptions))"
- LOGGER.info("RefreshFeedsJob: Materialized view #{view_name} is out-of-date, recreating...")
+ Log.info { "Materialized view #{view_name} is out-of-date, recreating..." }
db.exec("DROP MATERIALIZED VIEW #{view_name}")
end
@@ -47,18 +49,18 @@ class Invidious::Jobs::RefreshFeedsJob < Invidious::Jobs::BaseJob
legacy_view_name = "subscriptions_#{sha256(email)[0..7]}"
db.exec("SELECT * FROM #{legacy_view_name} LIMIT 0")
- LOGGER.info("RefreshFeedsJob: RENAME MATERIALIZED VIEW #{legacy_view_name}")
+ Log.info { "RENAME MATERIALIZED VIEW #{legacy_view_name}" }
db.exec("ALTER MATERIALIZED VIEW #{legacy_view_name} RENAME TO #{view_name}")
rescue ex
begin
# While iterating through, we may have an email stored from a deleted account
if db.query_one?("SELECT true FROM users WHERE email = $1", email, as: Bool)
- LOGGER.info("RefreshFeedsJob: CREATE #{view_name}")
+ Log.info { "CREATE #{view_name}" }
db.exec("CREATE MATERIALIZED VIEW #{view_name} AS #{MATERIALIZED_VIEW_SQL.call(email)}")
db.exec("UPDATE users SET feed_needs_update = false WHERE email = $1", email)
end
rescue ex
- LOGGER.error("RefreshFeedJobs: REFRESH #{email} : #{ex.message}")
+ Log.error { "REFRESH #{email} : #{ex.message}" }
end
end
end
diff --git a/src/invidious/jobs/subscribe_to_feeds_job.cr b/src/invidious/jobs/subscribe_to_feeds_job.cr
index 8584fb9c..babd22d4 100644
--- a/src/invidious/jobs/subscribe_to_feeds_job.cr
+++ b/src/invidious/jobs/subscribe_to_feeds_job.cr
@@ -1,4 +1,6 @@
class Invidious::Jobs::SubscribeToFeedsJob < Invidious::Jobs::BaseJob
+ Log = ::Log.for(self)
+
private getter db : DB::Database
private getter hmac_key : String
@@ -32,10 +34,10 @@ class Invidious::Jobs::SubscribeToFeedsJob < Invidious::Jobs::BaseJob
response = subscribe_pubsub(ucid, hmac_key)
if response.status_code >= 400
- LOGGER.error("SubscribeToFeedsJob: #{ucid} : #{response.body}")
+ Log.error { "#{ucid} : #{response.body}" }
end
rescue ex
- LOGGER.error("SubscribeToFeedsJob: #{ucid} : #{ex.message}")
+ Log.error { "#{ucid} : #{ex.message}" }
end
active_channel.send(true)
diff --git a/src/invidious/jsonify/api_v1/video_json.cr b/src/invidious/jsonify/api_v1/video_json.cr
index 58805af2..1efce5db 100644
--- a/src/invidious/jsonify/api_v1/video_json.cr
+++ b/src/invidious/jsonify/api_v1/video_json.cr
@@ -22,7 +22,7 @@ module Invidious::JSONify::APIv1
json.field "description", video.description
json.field "descriptionHtml", video.description_html
json.field "published", video.published.to_unix
- json.field "publishedText", translate(locale, "`x` ago", recode_date(video.published, locale))
+ json.field "publishedText", I18n.translate(locale, "`x` ago", recode_date(video.published, locale))
json.field "keywords", video.keywords
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 "published", rv["published"]?
if rv["published"]?.try &.presence
- json.field "publishedText", translate(locale, "`x` ago", recode_date(Time.parse_rfc3339(rv["published"].to_s), locale))
+ json.field "publishedText", I18n.translate(locale, "`x` ago", recode_date(Time.parse_rfc3339(rv["published"].to_s), locale))
else
json.field "publishedText", ""
end
diff --git a/src/invidious/routes/before_all.cr b/src/invidious/routes/before_all.cr
index b5269668..1cdb6403 100644
--- a/src/invidious/routes/before_all.cr
+++ b/src/invidious/routes/before_all.cr
@@ -7,7 +7,7 @@ module Invidious::Routes::BeforeAll
preferences = Preferences.from_json(URI.decode_www_form(prefs_cookie.value))
else
if language_header = env.request.headers["Accept-Language"]?
- if language = ANG.language_negotiator.best(language_header, LOCALES.keys)
+ if language = ANG.language_negotiator.best(language_header, I18n::LOCALES.keys)
preferences.locale = language.header
end
end
diff --git a/src/invidious/routes/channels.cr b/src/invidious/routes/channels.cr
index 508aa3e4..5e4a5e5f 100644
--- a/src/invidious/routes/channels.cr
+++ b/src/invidious/routes/channels.cr
@@ -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}" : ""}")
ucid = resolved_url["endpoint"]["browseEndpoint"]["browseId"]
rescue ex : InfoException | KeyError
- return error_template(404, translate(locale, "This channel does not exist."))
+ return error_template(404, I18n.translate(locale, "This channel does not exist."))
end
selected_tab = env.params.url["tab"]?
diff --git a/src/invidious/routes/embed.cr b/src/invidious/routes/embed.cr
index 930e4915..19abf317 100644
--- a/src/invidious/routes/embed.cr
+++ b/src/invidious/routes/embed.cr
@@ -10,7 +10,7 @@ module Invidious::Routes::Embed
videos = get_playlist_videos(playlist, offset: offset)
if videos.empty?
url = "/playlist?list=#{plid}"
- raise NotFoundException.new(translate(locale, "error_video_not_in_playlist", url))
+ raise NotFoundException.new(I18n.translate(locale, "error_video_not_in_playlist", url))
end
first_playlist_video = videos[0].as(PlaylistVideo)
@@ -72,7 +72,7 @@ module Invidious::Routes::Embed
videos = get_playlist_videos(playlist, offset: offset)
if videos.empty?
url = "/playlist?list=#{plid}"
- raise NotFoundException.new(translate(locale, "error_video_not_in_playlist", url))
+ raise NotFoundException.new(I18n.translate(locale, "error_video_not_in_playlist", url))
end
first_playlist_video = videos[0].as(PlaylistVideo)
diff --git a/src/invidious/routes/feeds.cr b/src/invidious/routes/feeds.cr
index 070c96eb..bc5e1e85 100644
--- a/src/invidious/routes/feeds.cr
+++ b/src/invidious/routes/feeds.cr
@@ -37,7 +37,7 @@ module Invidious::Routes::Feeds
if CONFIG.popular_enabled
templated "feeds/popular"
else
- message = translate(locale, "The Popular feed has been disabled by the administrator.")
+ message = I18n.translate(locale, "The Popular feed has been disabled by the administrator.")
templated "message"
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": "application/atom+xml", rel: "self",
href: "#{HOST_URL}#{env.request.resource}")
- xml.element("title") { xml.text translate(locale, "Invidious Private Feed for `x`", user.email) }
+ xml.element("title") { xml.text I18n.translate(locale, "Invidious Private Feed for `x`", user.email) }
(notifications + videos).each do |video|
video.to_xml(locale, params, xml)
@@ -403,7 +403,7 @@ module Invidious::Routes::Feeds
signature = env.request.headers["X-Hub-Signature"].lchop("sha1=")
if signature != OpenSSL::HMAC.hexdigest(:sha1, HMAC_KEY, body)
- LOGGER.error("/feed/webhook/#{token} : Invalid signature")
+ Log.error { "/feed/webhook/#{token} : Invalid signature" }
haltf env, status_code: 200
end
diff --git a/src/invidious/routes/login.cr b/src/invidious/routes/login.cr
index e7de5018..0abfd044 100644
--- a/src/invidious/routes/login.cr
+++ b/src/invidious/routes/login.cr
@@ -110,7 +110,7 @@ module Invidious::Routes::Login
user, sid = create_user(sid, email, password)
if language_header = env.request.headers["Accept-Language"]?
- if language = ANG.language_negotiator.best(language_header, LOCALES.keys)
+ if language = ANG.language_negotiator.best(language_header, I18n::LOCALES.keys)
user.preferences.locale = language.header
end
end
diff --git a/src/invidious/routes/watch.cr b/src/invidious/routes/watch.cr
index e777b3f1..b573567e 100644
--- a/src/invidious/routes/watch.cr
+++ b/src/invidious/routes/watch.cr
@@ -54,10 +54,10 @@ module Invidious::Routes::Watch
begin
video = get_video(id, region: params.region)
rescue ex : NotFoundException
- LOGGER.error("get_video not found: #{id} : #{ex.message}")
+ Log.error { "get_video not found: #{id} : #{ex.message}" }
return error_template(404, ex)
rescue ex
- LOGGER.error("get_video: #{id} : #{ex.message}")
+ Log.error { "get_video: #{id} : #{ex.message}" }
return error_template(500, ex)
end
diff --git a/src/invidious/search/processors.cr b/src/invidious/search/processors.cr
index 25edb936..e4c2c613 100644
--- a/src/invidious/search/processors.cr
+++ b/src/invidious/search/processors.cr
@@ -9,7 +9,7 @@ module Invidious::Search
client_config = YoutubeAPI::ClientConfig.new(region: query.region)
initial_data = YoutubeAPI.search(query.text, search_params, client_config: client_config)
- items, _ = extract_items(initial_data)
+ items, _ = YoutubeJSONParser.extract_items(initial_data)
return items.reject!(Category)
end
@@ -31,7 +31,7 @@ module Invidious::Search
continuation = produce_channel_search_continuation(ucid, query.text, query.page)
response_json = YoutubeAPI.browse(continuation)
- items, _ = extract_items(response_json, "", ucid)
+ items, _ = YoutubeJSONParser.extract_items(response_json, "", ucid)
return items.reject!(Category)
end
diff --git a/src/invidious/trending.cr b/src/invidious/trending.cr
index d14cde5d..538cf5a3 100644
--- a/src/invidious/trending.cr
+++ b/src/invidious/trending.cr
@@ -18,7 +18,7 @@ def fetch_trending(trending_type, region, locale)
client_config = YoutubeAPI::ClientConfig.new(region: region)
initial_data = YoutubeAPI.browse("FEtrending", params: params, client_config: client_config)
- items, _ = extract_items(initial_data)
+ items, _ = YoutubeJSONParser.extract_items(initial_data)
extracted = [] of SearchItem
diff --git a/src/invidious/videos.cr b/src/invidious/videos.cr
index 348a0a66..cf5802b0 100644
--- a/src/invidious/videos.cr
+++ b/src/invidious/videos.cr
@@ -324,7 +324,7 @@ rescue DB::Error
end
def fetch_video(id, region)
- info = extract_video_info(video_id: id)
+ info = Parser.extract_video_info(video_id: id)
if reason = info["reason"]?
if reason == "Video unavailable"
diff --git a/src/invidious/videos/parser.cr b/src/invidious/videos/parser.cr
index feb58440..7a15bb3f 100644
--- a/src/invidious/videos/parser.cr
+++ b/src/invidious/videos/parser.cr
@@ -6,499 +6,504 @@ require "json"
#
# TODO: "compactRadioRenderer" (Mix) and
# TODO: Use a proper struct/class instead of a hacky JSON object
-def parse_related_video(related : JSON::Any) : Hash(String, JSON::Any)?
- return nil if !related["videoId"]?
+module Parser
+ extend self
+ Log = ::Log.for(self)
- # The compact renderer has video length in seconds, where the end
- # screen rendered has a full text version ("42:40")
- length = related["lengthInSeconds"]?.try &.as_i.to_s
- length ||= related.dig?("lengthText", "simpleText").try do |box|
- decode_length_seconds(box.as_s).to_s
+ private def parse_related_video(related : JSON::Any) : Hash(String, JSON::Any)?
+ return nil if !related["videoId"]?
+
+ # The compact renderer has video length in seconds, where the end
+ # screen rendered has a full text version ("42:40")
+ 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
- # Both have "short", so the "long" option shouldn't be required
- channel_info = (related["shortBylineText"]? || related["longBylineText"]?)
- .try &.dig?("runs", 0)
+ def extract_video_info(video_id : String)
+ # Init client config for the API
+ client_config = YoutubeAPI::ClientConfig.new
- author = channel_info.try &.dig?("text")
- author_verified = has_verified_badge?(related["ownerBadges"]?).to_s
+ # Fetch data from the player endpoint
+ player_response = YoutubeAPI.player(video_id: video_id, params: "2AMB", client_config: client_config)
- ucid = channel_info.try { |ci| HelperExtractors.get_browse_id(ci) }
+ playability_status = player_response.dig?("playabilityStatus", "status").try &.as_s
- # "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/, "")
+ 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
- short_view_count = related.try do |r|
- HelperExtractors.get_short_view_count(r).to_s
- end
+ # 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 {
+ "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.
- LOGGER.trace("parse_related_video: Found \"watchNextEndScreenRenderer\" container")
+ # Although technically not a call to /videoplayback the fact that YouTube is returning the
+ # 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 {
"version" => JSON::Any.new(Video::SCHEMA_VERSION.to_i64),
- "reason" => JSON::Any.new(reason),
+ "reason" => JSON::Any.new("Can't load the video on this Invidious instance. YouTube is currently trying to block Invidious instances.
Click here for more info about the issue."),
}
+ else
+ reason = nil
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
- # wrong video means that we should count it as a failure.
- get_playback_statistic()["totalRequests"] += 1
+ # Don't fetch the next endpoint if the video is unavailable.
+ if {"OK", "LIVE_STREAM_OFFLINE", "LOGIN_REQUIRED"}.any?(playability_status)
+ next_response = YoutubeAPI.next({"videoId": video_id, "params": ""})
+ player_response = player_response.merge(next_response)
+ end
- return {
- "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.
Click here for more info about the issue."),
- }
- else
- reason = nil
- end
+ params = parse_video_info(video_id, player_response)
+ params["reason"] = JSON::Any.new(reason) if reason
- # Don't fetch the next endpoint if the video is unavailable.
- if {"OK", "LIVE_STREAM_OFFLINE", "LOGIN_REQUIRED"}.any?(playability_status)
- next_response = YoutubeAPI.next({"videoId": video_id, "params": ""})
- player_response = player_response.merge(next_response)
- end
+ 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::TvHtml5, YoutubeAPI::ClientType::WebMobile}
- params = parse_video_info(video_id, player_response)
- params["reason"] = JSON::Any.new(reason) if reason
+ players_fallback.each do |player_fallback|
+ client_config.client_type = player_fallback
- if !CONFIG.invidious_companion.present?
- if player_response.dig?("streamingData", "adaptiveFormats", 0, "url").nil?
- LOGGER.warn("Missing URLs for adaptive formats, falling back to other YT clients.")
- players_fallback = {YoutubeAPI::ClientType::TvHtml5, YoutubeAPI::ClientType::WebMobile}
+ next if !(player_fallback_response = try_fetch_streaming_data(video_id, client_config))
- players_fallback.each do |player_fallback|
- client_config.client_type = player_fallback
-
- next if !(player_fallback_response = try_fetch_streaming_data(video_id, client_config))
-
- if player_fallback_response.dig?("streamingData", "adaptiveFormats", 0, "url")
- streaming_data = player_response["streamingData"].as_h
- streaming_data["adaptiveFormats"] = player_fallback_response["streamingData"]["adaptiveFormats"]
- player_response["streamingData"] = JSON::Any.new(streaming_data)
- break
+ if player_fallback_response.dig?("streamingData", "adaptiveFormats", 0, "url")
+ streaming_data = player_response["streamingData"].as_h
+ streaming_data["adaptiveFormats"] = player_fallback_response["streamingData"]["adaptiveFormats"]
+ player_response["streamingData"] = JSON::Any.new(streaming_data)
+ break
+ end
+ rescue InfoException
+ next Log.warn { "Failed to fetch streams with #{player_fallback}" }
end
- rescue InfoException
- next LOGGER.warn("Failed to fetch streams with #{player_fallback}")
end
+
+ # 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
- # 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
- {"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.as_h["url"] = JSON::Any.new(convert_url(format))
+ # 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.as_h["url"] = JSON::Any.new(convert_url(format))
+ end
end
+
+ params["streamingData"] = streaming_data
end
- params["streamingData"] = streaming_data
+ # Data structure version, for cache control
+ params["version"] = JSON::Any.new(Video::SCHEMA_VERSION.to_i64)
+
+ return params
end
- # Data structure version, for cache control
- params["version"] = JSON::Any.new(Video::SCHEMA_VERSION.to_i64)
+ def try_fetch_streaming_data(id : String, client_config : YoutubeAPI::ClientConfig) : Hash(String, JSON::Any)?
+ ::Log.forf.debug { "[#{id}] Using #{client_config.client_type} client." }
+ response = YoutubeAPI.player(video_id: id, params: "2AMB", client_config: client_config)
- return params
-end
+ playability_status = response["playabilityStatus"]["status"]
+ ::Log.forf.debug { "[#{id}] Got playabilityStatus == #{playability_status}." }
-def try_fetch_streaming_data(id : String, client_config : YoutubeAPI::ClientConfig) : Hash(String, JSON::Any)?
- LOGGER.debug("try_fetch_streaming_data: [#{id}] Using #{client_config.client_type} client.")
- response = YoutubeAPI.player(video_id: id, params: "2AMB", client_config: client_config)
-
- playability_status = response["playabilityStatus"]["status"]
- LOGGER.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
-
- LOGGER.debug("extract_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
+ 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
- # If nothing was found previously, fall back to end screen renderer
- if related.empty?
- # Container for "endScreenVideoRenderer" items
- player_overlays = player_response.dig?(
- "playerOverlays", "playerOverlayRenderer",
- "endScreen", "watchNextEndScreenRenderer", "results"
- )
+ def parse_video_info(video_id : String, player_response : Hash(String, JSON::Any)) : Hash(String, JSON::Any)
+ # Top level elements
- player_overlays.try &.as_a.each do |element|
- if item = element["endScreenVideoRenderer"]?
+ 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.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 << JSON::Any.new(related_video) if related_video
end
end
- end
- # Likes
-
- toplevel_buttons = video_primary_renderer
- .try &.dig?("videoActions", "menuRenderer", "topLevelButtons")
-
- if toplevel_buttons
- # New Format as of december 2023
- likes_button = toplevel_buttons.dig?(0,
- "segmentedLikeDislikeButtonViewModel",
- "likeButtonViewModel",
- "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 nothing was found previously, fall back to end screen renderer
+ if related.empty?
+ # Container for "endScreenVideoRenderer" items
+ player_overlays = player_response.dig?(
+ "playerOverlays", "playerOverlayRenderer",
+ "endScreen", "watchNextEndScreenRenderer", "results"
)
- 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
-
- LOGGER.trace("extract_video_info: Found \"likes\" button. Button text is \"#{likes_txt}\"")
- LOGGER.debug("extract_video_info: 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|
- 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
-
- 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"))
+ player_overlays.try &.as_a.each do |element|
+ if item = element["endScreenVideoRenderer"]?
+ related_video = parse_related_video(item)
+ related << JSON::Any.new(related_video) if related_video
+ end
end
end
- music_list << VideoMusic.new(song.to_s, album.to_s, artist.to_s, music_license.to_s)
- end
- # Author infos
+ # Likes
- author = video_details["author"]?.try &.as_s
- ucid = video_details["channelId"]?.try &.as_s
+ toplevel_buttons = video_primary_renderer
+ .try &.dig?("videoActions", "menuRenderer", "topLevelButtons")
- 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"]?)
+ if toplevel_buttons
+ # New Format as of december 2023
+ likes_button = toplevel_buttons.dig?(0,
+ "segmentedLikeDislikeButtonViewModel",
+ "likeButtonViewModel",
+ "likeButtonViewModel",
+ "toggleButtonViewModel",
+ "toggleButtonViewModel",
+ "defaultButtonViewModel",
+ "buttonViewModel"
+ )
- subs_text = author_info["subscriberCountText"]?
- .try { |t| t["simpleText"]? || t.dig?("runs", 0, "text") }
- .try &.as_s.split(" ", 2)[0]
- end
+ likes_button ||= toplevel_buttons.try &.as_a
+ .find(&.dig?("toggleButtonRenderer", "defaultIcon", "iconType").=== "LIKE")
+ .try &.["toggleButtonRenderer"]
- # Return data
+ # New format as of september 2022
+ likes_button ||= toplevel_buttons.try &.as_a
+ .find(&.["segmentedLikeDislikeButtonRenderer"]?)
+ .try &.dig?(
+ "segmentedLikeDislikeButtonRenderer",
+ "likeButton", "toggleButtonRenderer"
+ )
- 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
+ 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
- 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 || "
"),
- "shortDescription" => JSON::Any.new(short_description.try &.as_s || nil),
+
+ 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
- "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 || ""),
+
+ 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" => JSON.parse(music_list.to_json),
+
+ 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
+
+ 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" => 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
+ author = video_details["author"]?.try &.as_s
+ ucid = video_details["channelId"]?.try &.as_s
-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
+ 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"]?)
- LOGGER.debug("convert_url: Decoding '#{cfr}'")
+ subs_text = author_info["subscriberCountText"]?
+ .try { |t| t["simpleText"]? || t.dig?("runs", 0, "text") }
+ .try &.as_s.split(" ", 2)[0]
+ end
- 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
+ # 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 || "
"),
+ "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
- n = DECRYPT_FUNCTION.try &.decrypt_nsig(params["n"])
- params["n"] = n if n
+ 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
- if token = CONFIG.po_token
- params["pot"] = token
+ ::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
-
- url.query_params = params
- LOGGER.trace("convert_url: new url is '#{url}'")
-
- return url.to_s
-rescue ex
- LOGGER.debug("convert_url: Error when parsing video URL")
- LOGGER.trace(ex.inspect_with_backtrace)
- return ""
end
diff --git a/src/invidious/views/add_playlist_items.ecr b/src/invidious/views/add_playlist_items.ecr
index 6aea82ae..4d73bdc4 100644
--- a/src/invidious/views/add_playlist_items.ecr
+++ b/src/invidious/views/add_playlist_items.ecr
@@ -8,12 +8,12 @@
diff --git a/src/invidious/views/community.ecr b/src/invidious/views/community.ecr
index 132e636c..a0fc47f5 100644
--- a/src/invidious/views/community.ecr
+++ b/src/invidious/views/community.ecr
@@ -35,10 +35,10 @@
<%=
{
"ucid" => ucid,
- "youtube_comments_text" => HTML.escape(translate(locale, "View YouTube comments")),
- "comments_text" => HTML.escape(translate(locale, "View `x` comments", "{commentCount}")),
- "hide_replies_text" => HTML.escape(translate(locale, "Hide replies")),
- "show_replies_text" => HTML.escape(translate(locale, "Show replies")),
+ "youtube_comments_text" => HTML.escape(I18n.translate(locale, "View YouTube comments")),
+ "comments_text" => HTML.escape(I18n.translate(locale, "View `x` comments", "{commentCount}")),
+ "hide_replies_text" => HTML.escape(I18n.translate(locale, "Hide replies")),
+ "show_replies_text" => HTML.escape(I18n.translate(locale, "Show replies")),
"preferences" => env.get("preferences").as(Preferences)
}.to_pretty_json
%>
diff --git a/src/invidious/views/components/channel_info.ecr b/src/invidious/views/components/channel_info.ecr
index f4164f31..1288ff58 100644
--- a/src/invidious/views/components/channel_info.ecr
+++ b/src/invidious/views/components/channel_info.ecr
@@ -24,7 +24,7 @@
@@ -37,10 +37,10 @@
<%= Invidious::Frontend::ChannelPage.generate_tabs_links(locale, channel, selected_tab) %>
@@ -50,9 +50,9 @@
<% sort_options.each do |sort| %>
<% end %>
diff --git a/src/invidious/views/components/feed_menu.ecr b/src/invidious/views/components/feed_menu.ecr
index 3dbeaf37..aeaf183b 100644
--- a/src/invidious/views/components/feed_menu.ecr
+++ b/src/invidious/views/components/feed_menu.ecr
@@ -5,7 +5,7 @@
<% end %>
<% feed_menu.each do |feed| %>
<% end %>
diff --git a/src/invidious/views/components/item.ecr b/src/invidious/views/components/item.ecr
index a24423df..ece2efe8 100644
--- a/src/invidious/views/components/item.ecr
+++ b/src/invidious/views/components/item.ecr
@@ -27,8 +27,8 @@
<% if !item.channel_handle.nil? %>
<%= item.channel_handle %>
<% end %>
-
<%= translate_count(locale, "generic_subscribers_count", item.subscriber_count, NumberFormatting::Separator) %>
- <% if !item.auto_generated && item.channel_handle.nil? %>
<%= translate_count(locale, "generic_videos_count", item.video_count, NumberFormatting::Separator) %>
<% end %>
+
<%= I18n.translate_count(locale, "generic_subscribers_count", item.subscriber_count, I18n::NumberFormatting::Separator) %>
+ <% if !item.auto_generated && item.channel_handle.nil? %>
<%= I18n.translate_count(locale, "generic_videos_count", item.video_count, I18n::NumberFormatting::Separator) %>
<% end %>
<%= item.description_html %>
<% when SearchHashtag %>
<% if !thin_mode %>
@@ -45,13 +45,13 @@
<%- if item.video_count != 0 -%>
-
<%= translate_count(locale, "generic_videos_count", item.video_count, NumberFormatting::Separator) %>
+
<%= I18n.translate_count(locale, "generic_videos_count", item.video_count, I18n::NumberFormatting::Separator) %>
<%- end -%>
<%- if item.channel_count != 0 -%>
-
<%= translate_count(locale, "generic_channels_count", item.channel_count, NumberFormatting::Separator) %>
+
<%= I18n.translate_count(locale, "generic_channels_count", item.channel_count, I18n::NumberFormatting::Separator) %>
<%- end -%>
<% when SearchPlaylist, InvidiousPlaylist %>
@@ -73,7 +73,7 @@
<%- end -%>
-
<%= translate_count(locale, "generic_videos_count", item.video_count, NumberFormatting::Separator) %>
+
<%= I18n.translate_count(locale, "generic_videos_count", item.video_count, I18n::NumberFormatting::Separator) %>
@@ -101,11 +101,11 @@
-
<%=translate(locale, "timeline_parse_error_placeholder_heading")%>
-
<%=translate(locale, "timeline_parse_error_placeholder_message")%>
+
<%=I18n.translate(locale, "timeline_parse_error_placeholder_heading")%>
+
<%=I18n.translate(locale, "timeline_parse_error_placeholder_message")%>
- <%=translate(locale, "timeline_parse_error_show_technical_details")%>
+ <%=I18n.translate(locale, "timeline_parse_error_show_technical_details")%>
<%=get_issue_template(env, item.parse_exception)[1]%>
@@ -168,7 +168,7 @@
<%- if item.responds_to?(:live_now) && item.live_now -%>
-
<%= translate(locale, "LIVE") %>
+
<%= I18n.translate(locale, "LIVE") %>
<%- elsif item.length_seconds != 0 -%>
<%= recode_length_seconds(item.length_seconds) %>
<%- end -%>
@@ -200,15 +200,15 @@
<% if item.responds_to?(:premiere_timestamp) && item.premiere_timestamp.try &.> Time.utc %>
-
<%= translate(locale, "Premieres in `x`", recode_date((item.premiere_timestamp.as(Time) - Time.utc).ago, locale)) %>
+
<%= I18n.translate(locale, "Premieres in `x`", recode_date((item.premiere_timestamp.as(Time) - Time.utc).ago, locale)) %>
<% elsif item.responds_to?(:published) && (Time.utc - item.published) > 1.minute %>
-
<%= translate(locale, "Shared `x` ago", recode_date(item.published, locale)) %>
+
<%= I18n.translate(locale, "Shared `x` ago", recode_date(item.published, locale)) %>
<% end %>
<% if item.responds_to?(:views) && item.views %>
-
<%= translate_count(locale, "generic_views_count", item.views || 0, NumberFormatting::Short) %>
+
<%= I18n.translate_count(locale, "generic_views_count", item.views || 0, I18n::NumberFormatting::Short) %>
<% end %>
diff --git a/src/invidious/views/components/items_paginated.ecr b/src/invidious/views/components/items_paginated.ecr
index f69df3fe..bb630d62 100644
--- a/src/invidious/views/components/items_paginated.ecr
+++ b/src/invidious/views/components/items_paginated.ecr
@@ -11,9 +11,9 @@
diff --git a/src/invidious/views/components/search_box.ecr b/src/invidious/views/components/search_box.ecr
index 29da2c52..f957c25c 100644
--- a/src/invidious/views/components/search_box.ecr
+++ b/src/invidious/views/components/search_box.ecr
@@ -2,11 +2,11 @@
-