mirror of
https://gitea.invidious.io/iv-org/invidious-copy-2022-08-14.git
synced 2024-08-15 00:53:20 +00:00
f54fbd057e
* Add prefers-color-scheme support This should fix <https://github.com/omarroth/invidious/issues/559>. The cookie storage format has been changed from boolean ("true"/"false") to tri-state ("dark"/"light"/""), so that users without a cookie set will get dark mode if they have enabled the dark theme in their operating system. The code for handling the cookie state, along with the user's operating system theme, has been factored out into a new function `update_mode`, which is called both at window load and at the "storage" event listener, because the "storage" event listener is only trigerred when a change is made to the localStorage from another tab/window (for more info - see <https://stackoverflow.com/a/4679754>).
371 lines
8 KiB
Crystal
371 lines
8 KiB
Crystal
# See http://www.evanmiller.org/how-not-to-sort-by-average-rating.html
|
|
def ci_lower_bound(pos, n)
|
|
if n == 0
|
|
return 0.0
|
|
end
|
|
|
|
# z value here represents a confidence level of 0.95
|
|
z = 1.96
|
|
phat = 1.0*pos/n
|
|
|
|
return (phat + z*z/(2*n) - z * Math.sqrt((phat*(1 - phat) + z*z/(4*n))/n))/(1 + z*z/n)
|
|
end
|
|
|
|
def elapsed_text(elapsed)
|
|
millis = elapsed.total_milliseconds
|
|
return "#{millis.round(2)}ms" if millis >= 1
|
|
|
|
"#{(millis * 1000).round(2)}µs"
|
|
end
|
|
|
|
def make_client(url : URI, region = nil)
|
|
client = HTTPClient.new(url)
|
|
client.family = CONFIG.force_resolve
|
|
client.read_timeout = 15.seconds
|
|
client.connect_timeout = 15.seconds
|
|
|
|
if region
|
|
PROXY_LIST[region]?.try &.sample(40).each do |proxy|
|
|
begin
|
|
proxy = HTTPProxy.new(proxy_host: proxy[:ip], proxy_port: proxy[:port])
|
|
client.set_proxy(proxy)
|
|
break
|
|
rescue ex
|
|
end
|
|
end
|
|
end
|
|
|
|
return client
|
|
end
|
|
|
|
def decode_length_seconds(string)
|
|
length_seconds = string.split(":").map { |a| a.to_i }
|
|
length_seconds = [0] * (3 - length_seconds.size) + length_seconds
|
|
length_seconds = Time::Span.new(length_seconds[0], length_seconds[1], length_seconds[2])
|
|
length_seconds = length_seconds.total_seconds.to_i
|
|
|
|
return length_seconds
|
|
end
|
|
|
|
def recode_length_seconds(time)
|
|
if time <= 0
|
|
return ""
|
|
else
|
|
time = time.seconds
|
|
text = "#{time.minutes.to_s.rjust(2, '0')}:#{time.seconds.to_s.rjust(2, '0')}"
|
|
|
|
if time.total_hours.to_i > 0
|
|
text = "#{time.total_hours.to_i.to_s.rjust(2, '0')}:#{text}"
|
|
end
|
|
|
|
text = text.lchop('0')
|
|
|
|
return text
|
|
end
|
|
end
|
|
|
|
def decode_time(string)
|
|
time = string.try &.to_f?
|
|
|
|
if !time
|
|
hours = /(?<hours>\d+)h/.match(string).try &.["hours"].try &.to_f
|
|
hours ||= 0
|
|
|
|
minutes = /(?<minutes>\d+)m(?!s)/.match(string).try &.["minutes"].try &.to_f
|
|
minutes ||= 0
|
|
|
|
seconds = /(?<seconds>\d+)s/.match(string).try &.["seconds"].try &.to_f
|
|
seconds ||= 0
|
|
|
|
millis = /(?<millis>\d+)ms/.match(string).try &.["millis"].try &.to_f
|
|
millis ||= 0
|
|
|
|
time = hours * 3600 + minutes * 60 + seconds + millis // 1000
|
|
end
|
|
|
|
return time
|
|
end
|
|
|
|
def decode_date(string : String)
|
|
# String matches 'YYYY'
|
|
if string.match(/^\d{4}/)
|
|
return Time.utc(string.to_i, 1, 1)
|
|
end
|
|
|
|
# Try to parse as format Jul 10, 2000
|
|
begin
|
|
return Time.parse(string, "%b %-d, %Y", Time::Location.local)
|
|
rescue ex
|
|
end
|
|
|
|
case string
|
|
when "today"
|
|
return Time.utc
|
|
when "yesterday"
|
|
return Time.utc - 1.day
|
|
end
|
|
|
|
# String matches format "20 hours ago", "4 months ago"...
|
|
date = string.split(" ")[-3, 3]
|
|
delta = date[0].to_i
|
|
|
|
case date[1]
|
|
when .includes? "second"
|
|
delta = delta.seconds
|
|
when .includes? "minute"
|
|
delta = delta.minutes
|
|
when .includes? "hour"
|
|
delta = delta.hours
|
|
when .includes? "day"
|
|
delta = delta.days
|
|
when .includes? "week"
|
|
delta = delta.weeks
|
|
when .includes? "month"
|
|
delta = delta.months
|
|
when .includes? "year"
|
|
delta = delta.years
|
|
else
|
|
raise "Could not parse #{string}"
|
|
end
|
|
|
|
return Time.utc - delta
|
|
end
|
|
|
|
def recode_date(time : Time, locale)
|
|
span = Time.utc - time
|
|
|
|
if span.total_days > 365.0
|
|
span = translate(locale, "`x` years", (span.total_days.to_i // 365).to_s)
|
|
elsif span.total_days > 30.0
|
|
span = translate(locale, "`x` months", (span.total_days.to_i // 30).to_s)
|
|
elsif span.total_days > 7.0
|
|
span = translate(locale, "`x` weeks", (span.total_days.to_i // 7).to_s)
|
|
elsif span.total_hours > 24.0
|
|
span = translate(locale, "`x` days", (span.total_days.to_i).to_s)
|
|
elsif span.total_minutes > 60.0
|
|
span = translate(locale, "`x` hours", (span.total_hours.to_i).to_s)
|
|
elsif span.total_seconds > 60.0
|
|
span = translate(locale, "`x` minutes", (span.total_minutes.to_i).to_s)
|
|
else
|
|
span = translate(locale, "`x` seconds", (span.total_seconds.to_i).to_s)
|
|
end
|
|
|
|
return span
|
|
end
|
|
|
|
def number_with_separator(number)
|
|
number.to_s.reverse.gsub(/(\d{3})(?=\d)/, "\\1,").reverse
|
|
end
|
|
|
|
def short_text_to_number(short_text)
|
|
case short_text
|
|
when .ends_with? "M"
|
|
number = short_text.rstrip(" mM").to_f
|
|
number *= 1000000
|
|
when .ends_with? "K"
|
|
number = short_text.rstrip(" kK").to_f
|
|
number *= 1000
|
|
else
|
|
number = short_text.rstrip(" ")
|
|
end
|
|
|
|
number = number.to_i
|
|
|
|
return number
|
|
end
|
|
|
|
def number_to_short_text(number)
|
|
seperated = number_with_separator(number).gsub(",", ".").split("")
|
|
text = seperated.first(2).join
|
|
|
|
if seperated[2]? && seperated[2] != "."
|
|
text += seperated[2]
|
|
end
|
|
|
|
text = text.rchop(".0")
|
|
|
|
if number // 1_000_000_000 != 0
|
|
text += "B"
|
|
elsif number // 1_000_000 != 0
|
|
text += "M"
|
|
elsif number // 1000 != 0
|
|
text += "K"
|
|
end
|
|
|
|
text
|
|
end
|
|
|
|
def arg_array(array, start = 1)
|
|
if array.size == 0
|
|
args = "NULL"
|
|
else
|
|
args = [] of String
|
|
(start..array.size + start - 1).each { |i| args << "($#{i})" }
|
|
args = args.join(",")
|
|
end
|
|
|
|
return args
|
|
end
|
|
|
|
def make_host_url(config, kemal_config)
|
|
ssl = config.https_only || kemal_config.ssl
|
|
port = config.external_port || kemal_config.port
|
|
|
|
if ssl
|
|
scheme = "https://"
|
|
else
|
|
scheme = "http://"
|
|
end
|
|
|
|
# Add if non-standard port
|
|
if port != 80 && port != 443
|
|
port = ":#{kemal_config.port}"
|
|
else
|
|
port = ""
|
|
end
|
|
|
|
if !config.domain
|
|
return ""
|
|
end
|
|
|
|
host = config.domain.not_nil!.lchop(".")
|
|
|
|
return "#{scheme}#{host}#{port}"
|
|
end
|
|
|
|
def get_referer(env, fallback = "/", unroll = true)
|
|
referer = env.params.query["referer"]?
|
|
referer ||= env.request.headers["referer"]?
|
|
referer ||= fallback
|
|
|
|
referer = URI.parse(referer)
|
|
|
|
# "Unroll" nested referrers
|
|
if unroll
|
|
loop do
|
|
if referer.query
|
|
params = HTTP::Params.parse(referer.query.not_nil!)
|
|
if params["referer"]?
|
|
referer = URI.parse(URI.unescape(params["referer"]))
|
|
else
|
|
break
|
|
end
|
|
else
|
|
break
|
|
end
|
|
end
|
|
end
|
|
|
|
referer = referer.full_path
|
|
referer = "/" + referer.lstrip("\/\\")
|
|
|
|
if referer == env.request.path
|
|
referer = fallback
|
|
end
|
|
|
|
return referer
|
|
end
|
|
|
|
struct VarInt
|
|
def self.from_io(io : IO, format = IO::ByteFormat::BigEndian) : Int32
|
|
result = 0_i32
|
|
num_read = 0
|
|
|
|
loop do
|
|
byte = io.read_byte
|
|
raise "Invalid VarInt" if !byte
|
|
value = byte & 0x7f
|
|
|
|
result |= value.to_i32 << (7 * num_read)
|
|
num_read += 1
|
|
|
|
break if byte & 0x80 == 0
|
|
raise "Invalid VarInt" if num_read > 5
|
|
end
|
|
|
|
result
|
|
end
|
|
|
|
def self.to_io(io : IO, value : Int32)
|
|
io.write_byte 0x00 if value == 0x00
|
|
|
|
while value != 0
|
|
byte = (value & 0x7f).to_u8
|
|
value >>= 7
|
|
|
|
if value != 0
|
|
byte |= 0x80
|
|
end
|
|
|
|
io.write_byte byte
|
|
end
|
|
end
|
|
end
|
|
|
|
def sha256(text)
|
|
digest = OpenSSL::Digest.new("SHA256")
|
|
digest << text
|
|
return digest.hexdigest
|
|
end
|
|
|
|
def subscribe_pubsub(topic, key, config)
|
|
case topic
|
|
when .match(/^UC[A-Za-z0-9_-]{22}$/)
|
|
topic = "channel_id=#{topic}"
|
|
when .match(/^(PL|LL|EC|UU|FL|UL|OLAK5uy_)[0-9A-Za-z-_]{10,}$/)
|
|
# There's a couple missing from the above regex, namely TL and RD, which
|
|
# don't have feeds
|
|
topic = "playlist_id=#{topic}"
|
|
else
|
|
# TODO
|
|
end
|
|
|
|
client = make_client(PUBSUB_URL)
|
|
time = Time.utc.to_unix.to_s
|
|
nonce = Random::Secure.hex(4)
|
|
signature = "#{time}:#{nonce}"
|
|
|
|
host_url = make_host_url(config, Kemal.config)
|
|
|
|
body = {
|
|
"hub.callback" => "#{host_url}/feed/webhook/v1:#{time}:#{nonce}:#{OpenSSL::HMAC.hexdigest(:sha1, key, signature)}",
|
|
"hub.topic" => "https://www.youtube.com/xml/feeds/videos.xml?#{topic}",
|
|
"hub.verify" => "async",
|
|
"hub.mode" => "subscribe",
|
|
"hub.lease_seconds" => "432000",
|
|
"hub.secret" => key.to_s,
|
|
}
|
|
|
|
return client.post("/subscribe", form: body)
|
|
end
|
|
|
|
def parse_range(range)
|
|
if !range
|
|
return 0_i64, nil
|
|
end
|
|
|
|
ranges = range.lchop("bytes=").split(',')
|
|
ranges.each do |range|
|
|
start_range, end_range = range.split('-')
|
|
|
|
start_range = start_range.to_i64? || 0_i64
|
|
end_range = end_range.to_i64?
|
|
|
|
return start_range, end_range
|
|
end
|
|
|
|
return 0_i64, nil
|
|
end
|
|
|
|
def convert_theme(theme)
|
|
case theme
|
|
when "true"
|
|
"dark"
|
|
when "false"
|
|
"light"
|
|
when "", nil
|
|
nil
|
|
else
|
|
theme
|
|
end
|
|
end
|