Compare commits

..

No commits in common. "5e38ef59da10cc0bfc16bb30e4006cdb70171deb" and "90e94d4e6cc126a8b7a091d12d7a5556bfe369d5" have entirely different histories.

18 changed files with 49 additions and 29 deletions

View File

@ -301,6 +301,7 @@ Spectator.describe Invidious::Search::Filters do
it "Encodes features filter (single)" do it "Encodes features filter (single)" do
Invidious::Search::Filters::Features.each do |value| Invidious::Search::Filters::Features.each do |value|
string = described_class.format_features(value)
filters = described_class.new(features: value) filters = described_class.new(features: value)
expect("#{filters.to_iv_params}") expect("#{filters.to_iv_params}")

View File

@ -232,7 +232,7 @@ def fetch_channel(ucid, pull_all_videos : Bool)
id: video_id, id: video_id,
title: title, title: title,
published: published, published: published,
updated: updated, updated: Time.utc,
ucid: ucid, ucid: ucid,
author: author, author: author,
length_seconds: length_seconds, length_seconds: length_seconds,

View File

@ -149,12 +149,12 @@ module Invidious::Frontend::Comments
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="#{translate(locale, "YouTube comment permalink")}">[YT]</a> <a 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="#{translate(locale, "YouTube comment permalink")}">[YT]</a> <a 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

View File

@ -6,9 +6,9 @@ module Invidious::Frontend::Misc
if prefs.automatic_instance_redirect if prefs.automatic_instance_redirect
current_page = env.get?("current_page").as(String) current_page = env.get?("current_page").as(String)
return "/redirect?referer=#{current_page}" redirect_url = "/redirect?referer=#{current_page}"
else else
return "https://redirect.invidious.io#{env.request.resource}" redirect_url = "https://redirect.invidious.io#{env.request.resource}"
end end
end end
end end

View File

@ -190,7 +190,7 @@ def error_redirect_helper(env : HTTP::Server::Context)
<a href="/redirect?referer=#{env.get("current_page")}">#{switch_instance}</a> <a href="/redirect?referer=#{env.get("current_page")}">#{switch_instance}</a>
</li> </li>
<li> <li>
<a rel="noreferrer noopener" href="https://youtube.com#{env.request.resource}">#{go_to_youtube}</a> <a href="https://youtube.com#{env.request.resource}">#{go_to_youtube}</a>
</li> </li>
</ul> </ul>
END_HTML END_HTML

View File

@ -97,7 +97,7 @@ class AuthHandler < Kemal::Handler
if token = env.request.headers["Authorization"]? if token = env.request.headers["Authorization"]?
token = JSON.parse(URI.decode_www_form(token.lchop("Bearer "))) token = JSON.parse(URI.decode_www_form(token.lchop("Bearer ")))
session = URI.decode_www_form(token["session"].as_s) session = URI.decode_www_form(token["session"].as_s)
scopes, _, _ = validate_request(token, session, env.request, HMAC_KEY, nil) scopes, expire, signature = validate_request(token, session, env.request, HMAC_KEY, nil)
if email = Invidious::Database::SessionIDs.select_email(session) if email = Invidious::Database::SessionIDs.select_email(session)
user = Invidious::Database::Users.select!(email: email) user = Invidious::Database::Users.select!(email: email)

View File

@ -95,6 +95,7 @@ module I18next::Plurals
"hr" => PluralForms::Special_Hungarian_Serbian, "hr" => PluralForms::Special_Hungarian_Serbian,
"it" => PluralForms::Special_Spanish_Italian, "it" => PluralForms::Special_Spanish_Italian,
"pt" => PluralForms::Special_French_Portuguese, "pt" => PluralForms::Special_French_Portuguese,
"pt" => PluralForms::Special_French_Portuguese,
"sr" => PluralForms::Special_Hungarian_Serbian, "sr" => PluralForms::Special_Hungarian_Serbian,
} }
@ -188,7 +189,7 @@ module I18next::Plurals
# Emulate the `rule.numbers.size == 2 && rule.numbers[0] == 1` check # Emulate the `rule.numbers.size == 2 && rule.numbers[0] == 1` check
# from original i18next code # from original i18next code
private def simple_plural?(form : PluralForms) : Bool private def is_simple_plural(form : PluralForms) : Bool
case form case form
when .single_gt_one? then return true when .single_gt_one? then return true
when .single_not_one? then return true when .single_not_one? then return true
@ -210,7 +211,7 @@ module I18next::Plurals
idx = SuffixIndex.get_index(plural_form, count) idx = SuffixIndex.get_index(plural_form, count)
# Simple plurals are handled differently in all versions (but v4) # Simple plurals are handled differently in all versions (but v4)
if @simplify_plural_suffix && simple_plural?(plural_form) if @simplify_plural_suffix && is_simple_plural(plural_form)
return (idx == 1) ? "_plural" : "" return (idx == 1) ? "_plural" : ""
end end
@ -261,9 +262,9 @@ module I18next::Plurals
when .special_hebrew? then return special_hebrew(count) when .special_hebrew? then return special_hebrew(count)
when .special_odia? then return special_odia(count) when .special_odia? then return special_odia(count)
# Mixed v3/v4 forms # Mixed v3/v4 forms
when .special_spanish_italian? then return special_cldr_spanish_italian(count) when .special_spanish_italian? then return special_cldr_Spanish_Italian(count)
when .special_french_portuguese? then return special_cldr_french_portuguese(count) when .special_french_portuguese? then return special_cldr_French_Portuguese(count)
when .special_hungarian_serbian? then return special_cldr_hungarian_serbian(count) when .special_hungarian_serbian? then return special_cldr_Hungarian_Serbian(count)
else else
# default, if nothing matched above # default, if nothing matched above
return 0_u8 return 0_u8
@ -534,7 +535,7 @@ module I18next::Plurals
# #
# This rule is mostly compliant to CLDR v42 # This rule is mostly compliant to CLDR v42
# #
def self.special_cldr_spanish_italian(count : Int) : UInt8 def self.special_cldr_Spanish_Italian(count : Int) : UInt8
return 0_u8 if (count == 1) # one return 0_u8 if (count == 1) # one
return 1_u8 if (count != 0 && count % 1_000_000 == 0) # many return 1_u8 if (count != 0 && count % 1_000_000 == 0) # many
return 2_u8 # other return 2_u8 # other
@ -544,7 +545,7 @@ module I18next::Plurals
# #
# This rule is mostly compliant to CLDR v42 # This rule is mostly compliant to CLDR v42
# #
def self.special_cldr_french_portuguese(count : Int) : UInt8 def self.special_cldr_French_Portuguese(count : Int) : UInt8
return 0_u8 if (count == 0 || count == 1) # one return 0_u8 if (count == 0 || count == 1) # one
return 1_u8 if (count % 1_000_000 == 0) # many return 1_u8 if (count % 1_000_000 == 0) # many
return 2_u8 # other return 2_u8 # other
@ -554,7 +555,7 @@ module I18next::Plurals
# #
# This rule is mostly compliant to CLDR v42 # This rule is mostly compliant to CLDR v42
# #
def self.special_cldr_hungarian_serbian(count : Int) : UInt8 def self.special_cldr_Hungarian_Serbian(count : Int) : UInt8
n_mod_10 = count % 10 n_mod_10 = count % 10
n_mod_100 = count % 100 n_mod_100 = count % 100

View File

@ -34,11 +34,24 @@ class Invidious::LogHandler < Kemal::BaseLogHandler
context context
end end
def puts(message : String)
@io << message << '\n'
@io.flush
end
def write(message : String) def write(message : String)
@io << message @io << message
@io.flush @io.flush
end end
def set_log_level(level : String)
@level = LogLevel.parse(level)
end
def set_log_level(level : LogLevel)
@level = level
end
{% for level in %w(trace debug info warn error fatal) %} {% for level in %w(trace debug info warn error fatal) %}
def {{level.id}}(message : String) def {{level.id}}(message : String)
if LogLevel::{{level.id.capitalize}} >= @level if LogLevel::{{level.id.capitalize}} >= @level

View File

@ -53,7 +53,7 @@ module Invidious::Routes::Account
return error_template(401, "Password is a required field") return error_template(401, "Password is a required field")
end end
new_passwords = env.params.body.select { |k, _| k.match(/^new_password\[\d+\]$/) }.map { |_, v| v } new_passwords = env.params.body.select { |k, v| k.match(/^new_password\[\d+\]$/) }.map { |k, v| v }
if new_passwords.size <= 1 || new_passwords.uniq.size != 1 if new_passwords.size <= 1 || new_passwords.uniq.size != 1
return error_template(400, "New passwords must match") return error_template(400, "New passwords must match")
@ -240,7 +240,7 @@ module Invidious::Routes::Account
return error_template(400, ex) return error_template(400, ex)
end end
scopes = env.params.body.select { |k, _| k.match(/^scopes\[\d+\]$/) }.map { |_, v| v } scopes = env.params.body.select { |k, v| k.match(/^scopes\[\d+\]$/) }.map { |k, v| v }
callback_url = env.params.body["callbackUrl"]? callback_url = env.params.body["callbackUrl"]?
expire = env.params.body["expire"]?.try &.to_i? expire = env.params.body["expire"]?.try &.to_i?

View File

@ -219,7 +219,7 @@ module Invidious::Routes::API::V1::Videos
storyboard[:storyboard_count].times do |i| storyboard[:storyboard_count].times do |i|
url = storyboard[:url] url = storyboard[:url]
authority = /(i\d?).ytimg.com/.match!(url)[1]? authority = /(i\d?).ytimg.com/.match(url).not_nil![1]?
url = url.gsub("$M", i).gsub(%r(https://i\d?.ytimg.com/sb/), "") url = url.gsub("$M", i).gsub(%r(https://i\d?.ytimg.com/sb/), "")
url = "#{HOST_URL}/sb/#{authority}/#{url}" url = "#{HOST_URL}/sb/#{authority}/#{url}"
@ -254,7 +254,7 @@ module Invidious::Routes::API::V1::Videos
if CONFIG.cache_annotations && (cached_annotation = Invidious::Database::Annotations.select(id)) if CONFIG.cache_annotations && (cached_annotation = Invidious::Database::Annotations.select(id))
annotations = cached_annotation.annotations annotations = cached_annotation.annotations
else else
index = CHARS_SAFE.index!(id[0]).to_s.rjust(2, '0') index = CHARS_SAFE.index(id[0]).not_nil!.to_s.rjust(2, '0')
# IA doesn't handle leading hyphens, # IA doesn't handle leading hyphens,
# so we use https://archive.org/details/youtubeannotations_64 # so we use https://archive.org/details/youtubeannotations_64

View File

@ -124,7 +124,7 @@ struct Invidious::User
playlist = create_playlist(title, privacy, user) playlist = create_playlist(title, privacy, user)
Invidious::Database::Playlists.update_description(playlist.id, description) Invidious::Database::Playlists.update_description(playlist.id, description)
item["videos"]?.try &.as_a?.try &.each_with_index do |video_id, idx| videos = item["videos"]?.try &.as_a?.try &.each_with_index do |video_id, idx|
if idx > CONFIG.playlist_length_limit if idx > CONFIG.playlist_length_limit
raise InfoException.new("Playlist cannot have more than #{CONFIG.playlist_length_limit} videos") raise InfoException.new("Playlist cannot have more than #{CONFIG.playlist_length_limit} videos")
end end
@ -182,7 +182,7 @@ struct Invidious::User
if is_opml?(type, extension) if is_opml?(type, extension)
subscriptions = XML.parse(body) subscriptions = XML.parse(body)
user.subscriptions += subscriptions.xpath_nodes(%q(//outline[@type="rss"])).map do |channel| user.subscriptions += subscriptions.xpath_nodes(%q(//outline[@type="rss"])).map do |channel|
channel["xmlUrl"].match!(/UC[a-zA-Z0-9_-]{22}/)[0] channel["xmlUrl"].match(/UC[a-zA-Z0-9_-]{22}/).not_nil![0]
end end
elsif extension == "json" || type == "application/json" elsif extension == "json" || type == "application/json"
subscriptions = JSON.parse(body) subscriptions = JSON.parse(body)

View File

@ -394,6 +394,10 @@ end
def fetch_video(id, region) def fetch_video(id, region)
info = extract_video_info(video_id: id) info = extract_video_info(video_id: id)
allowed_regions = info
.dig?("microformat", "playerMicroformatRenderer", "availableCountries")
.try &.as_a.map &.as_s || [] of String
if reason = info["reason"]? if reason = info["reason"]?
if reason == "Video unavailable" if reason == "Video unavailable"
raise NotFoundException.new(reason.as_s || "") raise NotFoundException.new(reason.as_s || "")

View File

@ -1,6 +1,6 @@
<div class="flex-right flexible"> <div class="flex-right flexible">
<div class="icon-buttons"> <div class="icon-buttons">
<a title="<%=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")%>" 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="<%=translate(locale, "Audio mode")%>" href="/watch<%=endpoint_params%>&listen=1"> <a title="<%=translate(locale, "Audio mode")%>" href="/watch<%=endpoint_params%>&listen=1">

View File

@ -83,7 +83,7 @@
<% 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 href="https://www.youtube.com/playlist?list=<%= playlist.id %>">
<%= translate(locale, "View playlist on YouTube") %> <%= translate(locale, "View playlist on YouTube") %>
</a> </a>
<span> | </span> <span> | </span>

View File

@ -123,8 +123,8 @@ 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 %>"><%= translate(locale, "videoinfo_watch_on_youTube") %></a> <a id="link-yt-watch" 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 %>"><%= translate(locale, "videoinfo_youTube_embed_link") %></a>) (<a id="link-yt-embed" 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">

View File

@ -24,7 +24,7 @@ struct YoutubeConnectionPool
@pool = build_pool() @pool = build_pool()
end end
def client(&) def client(&block)
conn = pool.checkout conn = pool.checkout
begin begin
response = yield conn response = yield conn
@ -69,7 +69,7 @@ def make_client(url : URI, region = nil, force_resolve : Bool = false)
return client return client
end end
def make_client(url : URI, region = nil, force_resolve : Bool = false, &) def make_client(url : URI, region = nil, force_resolve : Bool = false, &block)
client = make_client(url, region, force_resolve) client = make_client(url, region, force_resolve)
begin begin
yield client yield client

View File

@ -109,6 +109,7 @@ private module Parsers
end end
live_now = false live_now = false
paid = false
premium = false premium = false
premiere_timestamp = item_contents.dig?("upcomingEventData", "startTime").try { |t| Time.unix(t.as_s.to_i64) } premiere_timestamp = item_contents.dig?("upcomingEventData", "startTime").try { |t| Time.unix(t.as_s.to_i64) }
@ -855,7 +856,7 @@ end
# #
# This function yields the container so that items can be parsed separately. # This function yields the container so that items can be parsed separately.
# #
def extract_items(initial_data : InitialData, &) def extract_items(initial_data : InitialData, &block)
if unpackaged_data = initial_data["contents"]?.try &.as_h if unpackaged_data = initial_data["contents"]?.try &.as_h
elsif unpackaged_data = initial_data["response"]?.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", 1).try &.as_h

View File

@ -83,5 +83,5 @@ end
def extract_selected_tab(tabs) def extract_selected_tab(tabs)
# Extract the selected tab from the array of tabs Youtube returns # Extract the selected tab from the array of tabs Youtube returns
return tabs.as_a.select(&.["tabRenderer"]?.try &.["selected"]?.try &.as_bool)[0]["tabRenderer"] return selected_target = tabs.as_a.select(&.["tabRenderer"]?.try &.["selected"]?.try &.as_bool)[0]["tabRenderer"]
end end