Merge 540b65a332b869dc42b2427ee285554589e3ffd5 into d51a7a44ad91d2fa7d1330970a15a0d8f365f250

This commit is contained in:
Sijawusz Pur Rahnama 2026-01-24 12:53:00 +09:00 committed by GitHub
commit 5a4ba13228
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
109 changed files with 1284 additions and 1314 deletions

View File

@ -2,24 +2,16 @@
# Lint # Lint
# #
Lint/UnusedArgument:
Excluded:
- "**/*.ecr"
# Exclude assigns for ECR files # Exclude assigns for ECR files
Lint/UselessAssign: Lint/UselessAssign:
Excluded: Excluded:
- src/invidious.cr - "**/*.ecr"
- src/invidious/helpers/errors.cr
- src/invidious/routes/**/*.cr - src/invidious/routes/**/*.cr
# Ignore false negative (if !db.query_one?...)
Lint/UnreachableCode:
Excluded:
- src/invidious/database/base.cr
# Ignore shadowed variable `key` (it works for now, and that's
# a sensitive part of the code)
Lint/ShadowingOuterLocalVar:
Excluded:
- src/invidious/helpers/tokens.cr
Lint/NotNil: Lint/NotNil:
Enabled: false Enabled: false
@ -27,41 +19,17 @@ Lint/SpecFilename:
Excluded: Excluded:
- spec/parsers_helper.cr - spec/parsers_helper.cr
# #
# Style # Style
# #
Style/RedundantBegin:
Enabled: false
Style/RedundantReturn:
Enabled: false
Style/RedundantNext:
Enabled: false
Style/ParenthesesAroundCondition:
Enabled: false
# This requires a rewrite of most data structs (and their usage) in Invidious. # This requires a rewrite of most data structs (and their usage) in Invidious.
Naming/QueryBoolMethods: Naming/QueryBoolMethods:
Enabled: false Enabled: false
Naming/AccessorMethodName:
Enabled: false
Naming/BlockParameterName: Naming/BlockParameterName:
Enabled: false Enabled: false
# Hides TODO comment warnings.
#
# Call `bin/ameba --only Documentation/DocumentationAdmonition` to
# list them
Documentation/DocumentationAdmonition:
Enabled: false
# #
# Metrics # Metrics
# #

19
.github/workflows/ameba.yml vendored Normal file
View File

@ -0,0 +1,19 @@
name: Ameba
on:
push:
pull_request:
permissions:
contents: read
jobs:
lint:
runs-on: ubuntu-latest
steps:
- name: Download source
uses: actions/checkout@v6
- name: Run Ameba Linter
uses: crystal-ameba/github-action@master

View File

@ -26,7 +26,6 @@ on:
jobs: jobs:
build: build:
runs-on: ubuntu-latest runs-on: ubuntu-latest
name: "build - crystal: ${{ matrix.crystal }}, stable: ${{ matrix.stable }}" name: "build - crystal: ${{ matrix.crystal }}, stable: ${{ matrix.stable }}"
@ -122,7 +121,6 @@ jobs:
run: docker compose logs run: docker compose logs
lint: lint:
runs-on: ubuntu-latest runs-on: ubuntu-latest
continue-on-error: true continue-on-error: true
@ -159,6 +157,3 @@ jobs:
git diff git diff
exit 1 exit 1
fi fi
- name: Run Ameba linter
run: bin/ameba

View File

@ -2,7 +2,7 @@ version: 2.0
shards: shards:
ameba: ameba:
git: https://github.com/crystal-ameba/ameba.git git: https://github.com/crystal-ameba/ameba.git
version: 1.6.1 version: 1.7.0-dev+git.commit.9dbeb92f89d7a668940029bd7b935bef370f26c1
athena-negotiation: athena-negotiation:
git: https://github.com/athena-framework/negotiation.git git: https://github.com/athena-framework/negotiation.git

View File

@ -34,7 +34,7 @@ development_dependencies:
version: ~> 0.10.4 version: ~> 0.10.4
ameba: ameba:
github: crystal-ameba/ameba github: crystal-ameba/ameba
version: ~> 1.6.1 branch: master
crystal: ">= 1.10.0, < 2.0.0" crystal: ">= 1.10.0, < 2.0.0"

View File

@ -14,7 +14,7 @@ require "spectator"
require "../../../src/invidious/http_server/static_assets_handler.cr" require "../../../src/invidious/http_server/static_assets_handler.cr"
private def get_static_assets_handler private def get_static_assets_handler
return Invidious::HttpServer::StaticAssetsHandler.new "spec/http_server/handlers/static_assets_handler", directory_listing: false Invidious::HttpServer::StaticAssetsHandler.new "spec/http_server/handlers/static_assets_handler", directory_listing: false
end end
# Slightly modified version of `handle` function from # Slightly modified version of `handle` function from
@ -125,7 +125,7 @@ Spectator.describe StaticAssetsHandler do
gzip.gets_to_end gzip.gets_to_end
end end
return expect(decompressed) expect(decompressed)
end end
it "For full file requests" do it "For full file requests" do

View File

@ -7,7 +7,7 @@ Spectator.configure do |config|
end end
def csv_sample def csv_sample
return <<-CSV <<-CSV
Kanal-ID,Kanal-URL,Kanaltitel Kanal-ID,Kanal-URL,Kanaltitel
UC0hHW5Y08ggq-9kbrGgWj0A,http://www.youtube.com/channel/UC0hHW5Y08ggq-9kbrGgWj0A,Matias Marolla UC0hHW5Y08ggq-9kbrGgWj0A,http://www.youtube.com/channel/UC0hHW5Y08ggq-9kbrGgWj0A,Matias Marolla
UC0vBXGSyV14uvJ4hECDOl0Q,http://www.youtube.com/channel/UC0vBXGSyV14uvJ4hECDOl0Q,Techquickie UC0vBXGSyV14uvJ4hECDOl0Q,http://www.youtube.com/channel/UC0vBXGSyV14uvJ4hECDOl0Q,Techquickie

View File

@ -26,7 +26,7 @@ def load_mock(file) : Hash(String, JSON::Any)
file = File.join(__DIR__, "..", "mocks", file + ".json") file = File.join(__DIR__, "..", "mocks", file + ".json")
content = File.read(file) content = File.read(file)
return JSON.parse(content).as_h JSON.parse(content).as_h
end end
Spectator.configure do |config| Spectator.configure do |config|

View File

@ -161,7 +161,7 @@ def get_about_info(ucid, locale) : AboutChannel
sub_count = 0 sub_count = 0
if (metadata_rows = initdata.dig?("header", "pageHeaderRenderer", "content", "pageHeaderViewModel", "metadata", "contentMetadataViewModel", "metadataRows").try &.as_a) if metadata_rows = initdata.dig?("header", "pageHeaderRenderer", "content", "pageHeaderViewModel", "metadata", "contentMetadataViewModel", "metadataRows").try &.as_a
metadata_rows.each do |row| metadata_rows.each do |row|
metadata_part = row.dig?("metadataParts").try &.as_a.find { |i| i.dig?("text", "content").try &.as_s.includes?("subscribers") } metadata_part = row.dig?("metadataParts").try &.as_a.find { |i| i.dig?("text", "content").try &.as_s.includes?("subscribers") }
if !metadata_part.nil? if !metadata_part.nil?

View File

@ -26,21 +26,21 @@ struct ChannelVideo
json.object do json.object do
json.field "type", "shortVideo" json.field "type", "shortVideo"
json.field "title", self.title json.field "title", title
json.field "videoId", self.id json.field "videoId", id
json.field "videoThumbnails" do json.field "videoThumbnails" do
Invidious::JSONify::APIv1.thumbnails(json, self.id) Invidious::JSONify::APIv1.thumbnails(json, id)
end end
json.field "lengthSeconds", self.length_seconds json.field "lengthSeconds", length_seconds
json.field "author", self.author json.field "author", author
json.field "authorId", self.ucid json.field "authorId", ucid
json.field "authorUrl", "/channel/#{self.ucid}" json.field "authorUrl", "/channel/#{ucid}"
json.field "published", self.published.to_unix json.field "published", published.to_unix
json.field "publishedText", translate(locale, "`x` ago", recode_date(self.published, locale)) json.field "publishedText", translate(locale, "`x` ago", recode_date(published, locale))
json.field "viewCount", self.views json.field "viewCount", views
end end
end end
@ -51,34 +51,34 @@ struct ChannelVideo
end end
def to_xml(locale, query_params, xml : XML::Builder) def to_xml(locale, query_params, xml : XML::Builder)
query_params["v"] = self.id query_params["v"] = id
xml.element("entry") do xml.element("entry") do
xml.element("id") { xml.text "yt:video:#{self.id}" } xml.element("id") { xml.text "yt:video:#{id}" }
xml.element("yt:videoId") { xml.text self.id } xml.element("yt:videoId") { xml.text id }
xml.element("yt:channelId") { xml.text self.ucid } xml.element("yt:channelId") { xml.text ucid }
xml.element("title") { xml.text self.title } xml.element("title") { xml.text title }
xml.element("link", rel: "alternate", href: "#{HOST_URL}/watch?#{query_params}") xml.element("link", rel: "alternate", href: "#{HOST_URL}/watch?#{query_params}")
xml.element("author") do xml.element("author") do
xml.element("name") { xml.text self.author } xml.element("name") { xml.text author }
xml.element("uri") { xml.text "#{HOST_URL}/channel/#{self.ucid}" } xml.element("uri") { xml.text "#{HOST_URL}/channel/#{ucid}" }
end end
xml.element("content", type: "xhtml") do xml.element("content", type: "xhtml") do
xml.element("div", xmlns: "http://www.w3.org/1999/xhtml") do xml.element("div", xmlns: "http://www.w3.org/1999/xhtml") do
xml.element("a", href: "#{HOST_URL}/watch?#{query_params}") do xml.element("a", href: "#{HOST_URL}/watch?#{query_params}") do
xml.element("img", src: "#{HOST_URL}/vi/#{self.id}/mqdefault.jpg") xml.element("img", src: "#{HOST_URL}/vi/#{id}/mqdefault.jpg")
end end
end end
end end
xml.element("published") { xml.text self.published.to_s("%Y-%m-%dT%H:%M:%S%:z") } xml.element("published") { xml.text published.to_s("%Y-%m-%dT%H:%M:%S%:z") }
xml.element("updated") { xml.text self.updated.to_s("%Y-%m-%dT%H:%M:%S%:z") } xml.element("updated") { xml.text updated.to_s("%Y-%m-%dT%H:%M:%S%:z") }
xml.element("media:group") do xml.element("media:group") do
xml.element("media:title") { xml.text self.title } xml.element("media:title") { xml.text title }
xml.element("media:thumbnail", url: "#{HOST_URL}/vi/#{self.id}/mqdefault.jpg", xml.element("media:thumbnail", url: "#{HOST_URL}/vi/#{id}/mqdefault.jpg",
width: "320", height: "180") width: "320", height: "180")
end end
end end
@ -107,7 +107,7 @@ class ChannelRedirect < Exception
end end
def get_batch_channels(channels) def get_batch_channels(channels)
finished_channel = Channel(String | Nil).new finished_channel = Channel(String?).new
max_threads = 10 max_threads = 10
spawn do spawn do
@ -141,7 +141,7 @@ def get_batch_channels(channels)
end end
end end
return final final
end end
def get_channel(id) : InvidiousChannel def get_channel(id) : InvidiousChannel
@ -152,7 +152,7 @@ def get_channel(id) : InvidiousChannel
Invidious::Database::Channels.insert(channel, update_on_conflict: true) Invidious::Database::Channels.insert(channel, update_on_conflict: true)
end end
return channel channel
end end
def fetch_channel(ucid, pull_all_videos : Bool) def fetch_channel(ucid, pull_all_videos : Bool)
@ -292,5 +292,5 @@ def fetch_channel(ucid, pull_all_videos : Bool)
end end
channel.updated = Time.utc channel.updated = Time.utc
return channel channel
end end

View File

@ -21,7 +21,7 @@ def fetch_channel_community(ucid, cursor, locale, format, thin_mode)
items = container.as_a items = container.as_a
end end
return extract_channel_community(items, ucid: ucid, locale: locale, format: format, thin_mode: thin_mode) extract_channel_community(items, ucid: ucid, locale: locale, format: format, thin_mode: thin_mode)
end end
def decode_ucid_from_post_protobuf(params) def decode_ucid_from_post_protobuf(params)
@ -30,7 +30,7 @@ def decode_ucid_from_post_protobuf(params)
.try { |i| IO::Memory.new(i) } .try { |i| IO::Memory.new(i) }
.try { |i| Protodec::Any.parse(i) } .try { |i| Protodec::Any.parse(i) }
return decoded_protobuf.try(&.["56:0:embedded"]["2:0:string"].as_s) decoded_protobuf.try(&.["56:0:embedded"]["2:0:string"].as_s)
end end
def fetch_channel_community_post(ucid, post_id, locale, format, thin_mode) def fetch_channel_community_post(ucid, post_id, locale, format, thin_mode)
@ -53,7 +53,7 @@ def fetch_channel_community_post(ucid, post_id, locale, format, thin_mode)
items << item items << item
end end
return extract_channel_community(items, ucid: ucid, locale: locale, format: format, thin_mode: thin_mode, is_single_post: true) extract_channel_community(items, ucid: ucid, locale: locale, format: format, thin_mode: thin_mode, is_single_post: true)
end end
def extract_channel_community(items, *, ucid, locale, format, thin_mode, is_single_post : Bool = false) def extract_channel_community(items, *, ucid, locale, format, thin_mode, is_single_post : Bool = false)
@ -294,7 +294,7 @@ def extract_channel_community(items, *, ucid, locale, format, thin_mode, is_sing
end end
end end
return response response
end end
def produce_channel_community_continuation(ucid, cursor) def produce_channel_community_continuation(ucid, cursor)
@ -310,7 +310,7 @@ def produce_channel_community_continuation(ucid, cursor)
.try { |i| Base64.urlsafe_encode(i) } .try { |i| Base64.urlsafe_encode(i) }
.try { |i| URI.encode_www_form(i) } .try { |i| URI.encode_www_form(i) }
return continuation continuation
end end
def extract_channel_community_cursor(continuation) def extract_channel_community_cursor(continuation)

View File

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

View File

@ -9,7 +9,7 @@ module Invidious::Channel::Tabs
# an author name and ucid directly (e.g in RSS feeds). # an author name and ucid directly (e.g in RSS feeds).
# TODO: figure out how to get rid of that # TODO: figure out how to get rid of that
def get_videos(channel : AboutChannel, *, continuation : String? = nil, sort_by = "newest") def get_videos(channel : AboutChannel, *, continuation : String? = nil, sort_by = "newest")
return get_videos( get_videos(
channel.author, channel.ucid, channel.author, channel.ucid,
continuation: continuation, sort_by: sort_by continuation: continuation, sort_by: sort_by
) )
@ -19,7 +19,7 @@ module Invidious::Channel::Tabs
# an author name and ucid directly (e.g in RSS feeds). # an author name and ucid directly (e.g in RSS feeds).
# TODO: figure out how to get rid of that # TODO: figure out how to get rid of that
def get_videos(channel : InvidiousChannel, *, continuation : String? = nil, sort_by = "newest") def get_videos(channel : InvidiousChannel, *, continuation : String? = nil, sort_by = "newest")
return get_videos( get_videos(
channel.author, channel.id, channel.author, channel.id,
continuation: continuation, sort_by: sort_by continuation: continuation, sort_by: sort_by
) )
@ -29,7 +29,7 @@ module Invidious::Channel::Tabs
continuation ||= make_initial_videos_ctoken(ucid, sort_by) continuation ||= make_initial_videos_ctoken(ucid, sort_by)
initial_data = YoutubeAPI.browse(continuation: continuation) initial_data = YoutubeAPI.browse(continuation: continuation)
return extract_items(initial_data, author, ucid) extract_items(initial_data, author, ucid)
end end
def get_60_videos(channel : AboutChannel, *, continuation : String? = nil, sort_by = "newest") def get_60_videos(channel : AboutChannel, *, continuation : String? = nil, sort_by = "newest")
@ -59,7 +59,7 @@ module Invidious::Channel::Tabs
continuation ||= make_initial_shorts_ctoken(channel.ucid, sort_by) continuation ||= make_initial_shorts_ctoken(channel.ucid, sort_by)
initial_data = YoutubeAPI.browse(continuation: continuation) initial_data = YoutubeAPI.browse(continuation: continuation)
return extract_items(initial_data, channel.author, channel.ucid) extract_items(initial_data, channel.author, channel.ucid)
end end
# ------------------- # -------------------
@ -70,7 +70,7 @@ module Invidious::Channel::Tabs
continuation ||= make_initial_livestreams_ctoken(channel.ucid, sort_by) continuation ||= make_initial_livestreams_ctoken(channel.ucid, sort_by)
initial_data = YoutubeAPI.browse(continuation: continuation) initial_data = YoutubeAPI.browse(continuation: continuation)
return extract_items(initial_data, channel.author, channel.ucid) extract_items(initial_data, channel.author, channel.ucid)
end end
def get_60_livestreams(channel : AboutChannel, *, continuation : String? = nil, sort_by = "newest") def get_60_livestreams(channel : AboutChannel, *, continuation : String? = nil, sort_by = "newest")
@ -98,10 +98,10 @@ module Invidious::Channel::Tabs
private def sort_options_videos_short(sort_by : String) private def sort_options_videos_short(sort_by : String)
case sort_by case sort_by
when "newest" then return 4_i64 when "newest" then 4_i64
when "popular" then return 2_i64 when "popular" then 2_i64
when "oldest" then return 5_i64 when "oldest" then 5_i64
else return 4_i64 # Fallback to "newest" else 4_i64 # Fallback to "newest"
end end
end end
@ -118,7 +118,7 @@ module Invidious::Channel::Tabs
}, },
} }
return channel_ctoken_wrap(ucid, object) channel_ctoken_wrap(ucid, object)
end end
# Generate the initial "continuation token" to get the first page of the # Generate the initial "continuation token" to get the first page of the
@ -134,7 +134,7 @@ module Invidious::Channel::Tabs
}, },
} }
return channel_ctoken_wrap(ucid, object) channel_ctoken_wrap(ucid, object)
end end
# Generate the initial "continuation token" to get the first page of the # Generate the initial "continuation token" to get the first page of the
@ -158,7 +158,7 @@ module Invidious::Channel::Tabs
}, },
} }
return channel_ctoken_wrap(ucid, object) channel_ctoken_wrap(ucid, object)
end end
# The protobuf structure common between videos/shorts/livestreams # The protobuf structure common between videos/shorts/livestreams
@ -187,6 +187,6 @@ module Invidious::Channel::Tabs
.try { |i| Base64.urlsafe_encode(i) } .try { |i| Base64.urlsafe_encode(i) }
.try { |i| URI.encode_www_form(i) } .try { |i| URI.encode_www_form(i) }
return continuation continuation
end end
end end

View File

@ -37,7 +37,7 @@ def text_to_parsed_content(text : String) : JSON::Any
nodes << (node) nodes << (node)
end end
end end
return JSON.parse({"runs" => nodes}.to_json) JSON.parse({"runs" => nodes}.to_json)
end end
def parse_content(content : JSON::Any, video_id : String? = "") : String def parse_content(content : JSON::Any, video_id : String? = "") : String
@ -85,5 +85,5 @@ def content_to_comment_html(content, video_id : String? = "")
text text
end end
return html_array.join("").delete('\ufeff') html_array.join("").delete('\ufeff')
end end

View File

@ -45,7 +45,7 @@ module Invidious::Comments
html = node html = node
end end
return html.to_xml(options: XML::SaveOptions::NO_DECL) html.to_xml(options: XML::SaveOptions::NO_DECL)
end end
def fill_links(html, scheme, host) def fill_links(html, scheme, host)
@ -71,6 +71,6 @@ module Invidious::Comments
html = html.xpath_node(%q(//body/p)).not_nil! html = html.xpath_node(%q(//body/p)).not_nil!
end end
return html.to_xml(options: XML::SaveOptions::NO_DECL) html.to_xml(options: XML::SaveOptions::NO_DECL)
end end
end end

View File

@ -13,7 +13,7 @@ module Invidious::Comments
client_config = YoutubeAPI::ClientConfig.new(region: region) client_config = YoutubeAPI::ClientConfig.new(region: region)
response = YoutubeAPI.next(continuation: ctoken, client_config: client_config) response = YoutubeAPI.next(continuation: ctoken, client_config: client_config)
return parse_youtube(id, response, format, locale, thin_mode, sort_by) parse_youtube(id, response, format, locale, thin_mode, sort_by)
end end
def fetch_community_post_comments(ucid, post_id, sort_by = "top") def fetch_community_post_comments(ucid, post_id, sort_by = "top")
@ -58,7 +58,7 @@ module Invidious::Comments
.try { |i| URI.encode_www_form(i) } .try { |i| URI.encode_www_form(i) }
initial_data = YoutubeAPI.browse(continuation: continuation) initial_data = YoutubeAPI.browse(continuation: continuation)
return initial_data initial_data
end end
def parse_youtube(id, response, format, locale, thin_mode, sort_by = "top", is_post = false) def parse_youtube(id, response, format, locale, thin_mode, sort_by = "top", is_post = false)
@ -320,7 +320,7 @@ module Invidious::Comments
end end
end end
return response response
end end
def produce_continuation(video_id, cursor = "", sort_by = "top") def produce_continuation(video_id, cursor = "", sort_by = "top")
@ -364,6 +364,6 @@ module Invidious::Comments
.try { |i| Base64.urlsafe_encode(i) } .try { |i| Base64.urlsafe_encode(i) }
.try { |i| URI.encode_www_form(i) } .try { |i| URI.encode_www_form(i) }
return continuation continuation
end end
end end

View File

@ -183,15 +183,15 @@ class Config
def disabled?(option) def disabled?(option)
case disabled = CONFIG.disable_proxy case disabled = CONFIG.disable_proxy
when Bool when Bool
return disabled disabled
when Array when Array
if disabled.includes? option if disabled.includes? option
return true true
else else
return false false
end end
else else
return false false
end end
end end
@ -318,6 +318,6 @@ class Config
end end
end end
return config config
end end
end end

View File

@ -19,6 +19,6 @@ module Invidious::Database::Annotations
WHERE id = $1 WHERE id = $1
SQL SQL
return PG_DB.query_one?(request, id, as: Annotation) PG_DB.query_one?(request, id, as: Annotation)
end end
end end

View File

@ -32,6 +32,7 @@ module Invidious::Database
def check_enum(enum_name, struct_type = nil) def check_enum(enum_name, struct_type = nil)
return # TODO return # TODO
# ameba:disable Lint/UnreachableCode
if !PG_DB.query_one?("SELECT true FROM pg_type WHERE typname = $1", enum_name, as: Bool) if !PG_DB.query_one?("SELECT true FROM pg_type WHERE typname = $1", enum_name, as: Bool)
LOGGER.info("check_enum: CREATE TYPE #{enum_name}") LOGGER.info("check_enum: CREATE TYPE #{enum_name}")
@ -131,6 +132,6 @@ module Invidious::Database
end end
end end
return column_array column_array
end end
end end

View File

@ -72,7 +72,7 @@ module Invidious::Database::Channels
WHERE id = $1 WHERE id = $1
SQL SQL
return PG_DB.query_one?(request, id, as: InvidiousChannel) PG_DB.query_one?(request, id, as: InvidiousChannel)
end end
def select(ids : Array(String)) : Array(InvidiousChannel)? def select(ids : Array(String)) : Array(InvidiousChannel)?
@ -83,7 +83,7 @@ module Invidious::Database::Channels
WHERE id = ANY($1) WHERE id = ANY($1)
SQL SQL
return PG_DB.query_all(request, ids, as: InvidiousChannel) PG_DB.query_all(request, ids, as: InvidiousChannel)
end end
end end
@ -114,7 +114,7 @@ module Invidious::Database::ChannelVideos
RETURNING (xmax=0) AS was_insert RETURNING (xmax=0) AS was_insert
SQL SQL
return PG_DB.query_one(request, *video.to_tuple, as: Bool) PG_DB.query_one(request, *video.to_tuple, as: Bool)
end end
# ------------------- # -------------------
@ -130,7 +130,7 @@ module Invidious::Database::ChannelVideos
ORDER BY published DESC ORDER BY published DESC
SQL SQL
return PG_DB.query_all(request, ids, as: ChannelVideo) PG_DB.query_all(request, ids, as: ChannelVideo)
end end
def select_notfications(ucid : String, since : Time) : Array(ChannelVideo) def select_notfications(ucid : String, since : Time) : Array(ChannelVideo)
@ -141,7 +141,7 @@ module Invidious::Database::ChannelVideos
LIMIT 15 LIMIT 15
SQL SQL
return PG_DB.query_all(request, ucid, since, as: ChannelVideo) PG_DB.query_all(request, ucid, since, as: ChannelVideo)
end end
def select_popular_videos : Array(ChannelVideo) def select_popular_videos : Array(ChannelVideo)

View File

@ -50,6 +50,6 @@ module Invidious::Database::Nonces
WHERE nonce = $1 WHERE nonce = $1
SQL SQL
return PG_DB.query_one?(request, nonce, as: {String, Time}) PG_DB.query_one?(request, nonce, as: {String, Time})
end end
end end

View File

@ -100,7 +100,7 @@ module Invidious::Database::Playlists
WHERE id = $1 WHERE id = $1
SQL SQL
return PG_DB.query_one?(request, id, as: InvidiousPlaylist) PG_DB.query_one?(request, id, as: InvidiousPlaylist)
end end
def select_all(*, author : String) : Array(InvidiousPlaylist) def select_all(*, author : String) : Array(InvidiousPlaylist)
@ -109,7 +109,7 @@ module Invidious::Database::Playlists
WHERE author = $1 WHERE author = $1
SQL SQL
return PG_DB.query_all(request, author, as: InvidiousPlaylist) PG_DB.query_all(request, author, as: InvidiousPlaylist)
end end
# ------------------- # -------------------
@ -157,7 +157,7 @@ module Invidious::Database::Playlists
WHERE id = $1 WHERE id = $1
SQL SQL
return PG_DB.query_one?(request, id, as: String).nil? PG_DB.query_one?(request, id, as: String).nil?
end end
# Count how many playlist a user has created. # Count how many playlist a user has created.
@ -167,7 +167,7 @@ module Invidious::Database::Playlists
WHERE author = $1 WHERE author = $1
SQL SQL
return PG_DB.query_one?(request, author, as: Int64) || 0_i64 PG_DB.query_one?(request, author, as: Int64) || 0_i64
end end
end end
@ -225,7 +225,7 @@ module Invidious::Database::PlaylistVideos
OFFSET $4 OFFSET $4
SQL SQL
return PG_DB.query_all(request, plid, index, limit, offset, as: PlaylistVideo) PG_DB.query_all(request, plid, index, limit, offset, as: PlaylistVideo)
end end
def select_index(plid : String, vid : String) : Int64? def select_index(plid : String, vid : String) : Int64?
@ -235,7 +235,7 @@ module Invidious::Database::PlaylistVideos
LIMIT 1 LIMIT 1
SQL SQL
return PG_DB.query_one?(request, plid, vid, as: Int64) PG_DB.query_one?(request, plid, vid, as: Int64)
end end
def select_one_id(plid : String, index : VideoIndex) : String? def select_one_id(plid : String, index : VideoIndex) : String?
@ -246,7 +246,7 @@ module Invidious::Database::PlaylistVideos
LIMIT 1 LIMIT 1
SQL SQL
return PG_DB.query_one?(request, plid, index, as: String) PG_DB.query_one?(request, plid, index, as: String)
end end
def select_ids(plid : String, index : VideoIndex, limit = 500) : Array(String) def select_ids(plid : String, index : VideoIndex, limit = 500) : Array(String)
@ -257,6 +257,6 @@ module Invidious::Database::PlaylistVideos
LIMIT $3 LIMIT $3
SQL SQL
return PG_DB.query_all(request, plid, index, limit, as: String) PG_DB.query_all(request, plid, index, limit, as: String)
end end
end end

View File

@ -194,7 +194,7 @@ module Invidious::Database::Users
WHERE email = $1 WHERE email = $1
SQL SQL
return PG_DB.query_one?(request, email, as: User) PG_DB.query_one?(request, email, as: User)
end end
# Same as select, but can raise an exception # Same as select, but can raise an exception
@ -204,7 +204,7 @@ module Invidious::Database::Users
WHERE email = $1 WHERE email = $1
SQL SQL
return PG_DB.query_one(request, email, as: User) PG_DB.query_one(request, email, as: User)
end end
def select(*, token : String) : User? def select(*, token : String) : User?
@ -213,7 +213,7 @@ module Invidious::Database::Users
WHERE token = $1 WHERE token = $1
SQL SQL
return PG_DB.query_one?(request, token, as: User) PG_DB.query_one?(request, token, as: User)
end end
def select_notifications(user : User) : Array(String) def select_notifications(user : User) : Array(String)
@ -223,6 +223,6 @@ module Invidious::Database::Users
WHERE email = $1 WHERE email = $1
SQL SQL
return PG_DB.query_one(request, user.email, as: Array(String)) PG_DB.query_one(request, user.email, as: Array(String))
end end
end end

View File

@ -47,6 +47,6 @@ module Invidious::Database::Videos
WHERE id = $1 WHERE id = $1
SQL SQL
return PG_DB.query_one?(request, id, as: Video) PG_DB.query_one?(request, id, as: Video)
end end
end end

View File

@ -23,7 +23,7 @@ class BrokenTubeException < Exception
end end
def message def message
return "Missing JSON element \"#{@element}\"" "Missing JSON element \"#{@element}\""
end end
end end

View File

@ -14,12 +14,12 @@ module Invidious::Frontend::ChannelPage
end end
def generate_tabs_links(locale : String, channel : AboutChannel, selected_tab : TabsAvailable) def generate_tabs_links(locale : String, channel : AboutChannel, selected_tab : TabsAvailable)
return String.build(1500) do |str| String.build(1500) do |str|
base_url = "/channel/#{channel.ucid}" base_url = "/channel/#{channel.ucid}"
TabsAvailable.each do |tab| TabsAvailable.each do |tab|
# Ignore playlists, as it is not supported for auto-generated channels yet # Ignore playlists, as it is not supported for auto-generated channels yet
next if (tab.playlists? && channel.auto_generated) next if tab.playlists? && channel.auto_generated
tab_name = tab.to_s.downcase tab_name = tab.to_s.downcase

View File

@ -11,24 +11,24 @@ module Invidious::Frontend::Comments
replies_html = "" replies_html = ""
if child.replies.is_a?(RedditThing) if child.replies.is_a?(RedditThing)
replies = child.replies.as(RedditThing) replies = child.replies.as(RedditThing)
replies_html = self.template_reddit(replies.data.as(RedditListing).children, locale) replies_html = template_reddit(replies.data.as(RedditListing).children, locale)
end end
if child.depth > 0 if child.depth > 0
html << <<-END_HTML html << <<-HTML
<div class="pure-g"> <div class="pure-g">
<div class="pure-u-1-24"> <div class="pure-u-1-24">
</div> </div>
<div class="pure-u-23-24"> <div class="pure-u-23-24">
END_HTML HTML
else else
html << <<-END_HTML html << <<-HTML
<div class="pure-g"> <div class="pure-g">
<div class="pure-u-1"> <div class="pure-u-1">
END_HTML HTML
end end
html << <<-END_HTML html << <<-HTML
<p> <p>
<a href="javascript:void(0)" data-onclick="toggle_parent">[ ]</a> <a href="javascript:void(0)" data-onclick="toggle_parent">[ ]</a>
<b><a href="https://www.reddit.com/user/#{child.author}">#{child.author}</a></b> <b><a href="https://www.reddit.com/user/#{child.author}">#{child.author}</a></b>
@ -42,7 +42,7 @@ module Invidious::Frontend::Comments
</div> </div>
</div> </div>
</div> </div>
END_HTML HTML
end end
end end
end end

View File

@ -12,7 +12,7 @@ module Invidious::Frontend::Comments
NumberFormatting::Separator NumberFormatting::Separator
) )
replies_html = <<-END_HTML replies_html = <<-HTML
<div id="replies" class="pure-g"> <div id="replies" class="pure-g">
<div class="pure-u-1-24"></div> <div class="pure-u-1-24"></div>
<div class="pure-u-23-24"> <div class="pure-u-23-24">
@ -22,7 +22,7 @@ module Invidious::Frontend::Comments
</p> </p>
</div> </div>
</div> </div>
END_HTML HTML
elsif comments["authorId"]? && !comments["singlePost"]? elsif comments["authorId"]? && !comments["singlePost"]?
# for posts we should display a link to the post # for posts we should display a link to the post
replies_count_text = translate_count(locale, replies_count_text = translate_count(locale,
@ -31,7 +31,7 @@ module Invidious::Frontend::Comments
NumberFormatting::Separator NumberFormatting::Separator
) )
replies_html = <<-END_HTML replies_html = <<-HTML
<div class="pure-g"> <div class="pure-g">
<div class="pure-u-1-24"></div> <div class="pure-u-1-24"></div>
<div class="pure-u-23-24"> <div class="pure-u-23-24">
@ -40,7 +40,7 @@ module Invidious::Frontend::Comments
</p> </p>
</div> </div>
</div> </div>
END_HTML HTML
end end
if !thin_mode if !thin_mode
@ -65,7 +65,7 @@ module Invidious::Frontend::Comments
str << %(width="16" height="16" />) str << %(width="16" height="16" />)
end end
end end
html << <<-END_HTML html << <<-HTML
<div class="pure-g" style="width:100%"> <div class="pure-g" style="width:100%">
<div class="channel-profile pure-u-4-24 pure-u-md-2-24"> <div class="channel-profile pure-u-4-24 pure-u-md-2-24">
<img loading="lazy" style="margin-right:1em;margin-top:1em;width:90%" src="#{author_thumbnail}" alt="" /> <img loading="lazy" style="margin-right:1em;margin-top:1em;width:90%" src="#{author_thumbnail}" alt="" />
@ -77,7 +77,7 @@ module Invidious::Frontend::Comments
</b> </b>
#{sponsor_icon} #{sponsor_icon}
<p style="white-space:pre-wrap">#{child["contentHtml"]}</p> <p style="white-space:pre-wrap">#{child["contentHtml"]}</p>
END_HTML HTML
if child["attachment"]? if child["attachment"]?
attachment = child["attachment"] attachment = child["attachment"]
@ -86,82 +86,81 @@ module Invidious::Frontend::Comments
when "image" when "image"
attachment = attachment["imageThumbnails"][1] attachment = attachment["imageThumbnails"][1]
html << <<-END_HTML html << <<-HTML
<div class="pure-g"> <div class="pure-g">
<div class="pure-u-1 pure-u-md-1-2"> <div class="pure-u-1 pure-u-md-1-2">
<img loading="lazy" style="width:100%" src="/ggpht#{URI.parse(attachment["url"].as_s).request_target}" alt="" /> <img loading="lazy" style="width:100%" src="/ggpht#{URI.parse(attachment["url"].as_s).request_target}" alt="" />
</div> </div>
</div> </div>
END_HTML HTML
when "video" when "video"
if attachment["error"]? if attachment["error"]?
html << <<-END_HTML html << <<-HTML
<div class="pure-g video-iframe-wrapper"> <div class="pure-g video-iframe-wrapper">
<p>#{attachment["error"]}</p> <p>#{attachment["error"]}</p>
</div> </div>
END_HTML HTML
else else
html << <<-END_HTML html << <<-HTML
<div class="pure-g video-iframe-wrapper"> <div class="pure-g video-iframe-wrapper">
<iframe class="video-iframe" src='/embed/#{attachment["videoId"]?}?autoplay=0'></iframe> <iframe class="video-iframe" src='/embed/#{attachment["videoId"]?}?autoplay=0'></iframe>
</div> </div>
END_HTML HTML
end end
when "multiImage" when "multiImage"
html << <<-END_HTML html << <<-HTML
<section class="carousel"> <section class="carousel">
<a class="skip-link" href="#skip-#{child["commentId"]}">#{translate(locale, "carousel_skip")}</a> <a class="skip-link" href="#skip-#{child["commentId"]}">#{translate(locale, "carousel_skip")}</a>
<div class="slides"> <div class="slides">
END_HTML HTML
image_array = attachment["images"].as_a image_array = attachment["images"].as_a
image_array.each_index do |i| image_array.each_index do |i|
html << <<-END_HTML html << <<-HTML
<div class="slides-item slide-#{i + 1}" id="#{child["commentId"]}-slide-#{i + 1}" aria-label="#{translate(locale, "carousel_slide", {"current" => (i + 1).to_s, "total" => image_array.size.to_s})}" tabindex="0"> <div class="slides-item slide-#{i + 1}" id="#{child["commentId"]}-slide-#{i + 1}" aria-label="#{translate(locale, "carousel_slide", {"current" => (i + 1).to_s, "total" => image_array.size.to_s})}" tabindex="0">
<img loading="lazy" src="/ggpht#{URI.parse(image_array[i][1]["url"].as_s).request_target}" alt="" /> <img loading="lazy" src="/ggpht#{URI.parse(image_array[i][1]["url"].as_s).request_target}" alt="" />
</div> </div>
END_HTML HTML
end end
html << <<-END_HTML html << <<-HTML
</div> </div>
<div class="carousel__nav"> <div class="carousel__nav">
END_HTML HTML
attachment["images"].as_a.each_index do |i| attachment["images"].as_a.each_index do |i|
html << <<-END_HTML html << <<-HTML
<a class="slider-nav" href="##{child["commentId"]}-slide-#{i + 1}" aria-label="#{translate(locale, "carousel_go_to", (i + 1).to_s)}" tabindex="-1" aria-hidden="true">#{i + 1}</a> <a class="slider-nav" href="##{child["commentId"]}-slide-#{i + 1}" aria-label="#{translate(locale, "carousel_go_to", (i + 1).to_s)}" tabindex="-1" aria-hidden="true">#{i + 1}</a>
END_HTML HTML
end end
html << <<-END_HTML html << <<-HTML
</div> </div>
<div id="skip-#{child["commentId"]}"></div> <div id="skip-#{child["commentId"]}"></div>
</section> </section>
END_HTML HTML
else nil # Ignore
end end
end end
html << <<-END_HTML html << <<-HTML
<p> <p>
<span title="#{Time.unix(child["published"].as_i64).to_s(translate(locale, "%A %B %-d, %Y"))}">#{translate(locale, "`x` ago", recode_date(Time.unix(child["published"].as_i64), locale))} #{child["isEdited"] == true ? translate(locale, "(edited)") : ""}</span> <span title="#{Time.unix(child["published"].as_i64).to_s(translate(locale, "%A %B %-d, %Y"))}">#{translate(locale, "`x` ago", recode_date(Time.unix(child["published"].as_i64), locale))} #{child["isEdited"] == true ? translate(locale, "(edited)") : ""}</span>
| |
END_HTML HTML
if comments["videoId"]? if comments["videoId"]?
html << <<-END_HTML html << <<-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 rel="noreferrer noopener" href="https://www.youtube.com/watch?v=#{comments["videoId"]}&lc=#{child["commentId"]}" title="#{translate(locale, "YouTube comment permalink")}">[YT]</a>
| |
END_HTML HTML
elsif comments["authorId"]? elsif comments["authorId"]?
html << <<-END_HTML html << <<-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 rel="noreferrer noopener" href="https://www.youtube.com/channel/#{comments["authorId"]}/community?lb=#{child["commentId"]}" title="#{translate(locale, "YouTube comment permalink")}">[YT]</a>
| |
END_HTML HTML
end end
html << <<-END_HTML html << <<-HTML
<i class="icon ion-ios-thumbs-up"></i> #{number_with_separator(child["likeCount"])} <i class="icon ion-ios-thumbs-up"></i> #{number_with_separator(child["likeCount"])}
END_HTML HTML
if child["creatorHeart"]? if child["creatorHeart"]?
if !thin_mode if !thin_mode
@ -170,7 +169,7 @@ module Invidious::Frontend::Comments
creator_thumbnail = "" creator_thumbnail = ""
end end
html << <<-END_HTML html << <<-HTML
&nbsp; &nbsp;
<span class="creator-heart-container" title="#{translate(locale, "`x` marked it with a ❤", child["creatorHeart"]["creatorName"].as_s)}"> <span class="creator-heart-container" title="#{translate(locale, "`x` marked it with a ❤", child["creatorHeart"]["creatorName"].as_s)}">
<span class="creator-heart"> <span class="creator-heart">
@ -180,19 +179,19 @@ module Invidious::Frontend::Comments
</span> </span>
</span> </span>
</span> </span>
END_HTML HTML
end end
html << <<-END_HTML html << <<-HTML
</p> </p>
#{replies_html} #{replies_html}
</div> </div>
</div> </div>
END_HTML HTML
end end
if comments["continuation"]? if comments["continuation"]?
html << <<-END_HTML html << <<-HTML
<div class="pure-g"> <div class="pure-g">
<div class="pure-u-1"> <div class="pure-u-1">
<p> <p>
@ -201,7 +200,7 @@ module Invidious::Frontend::Comments
</p> </p>
</div> </div>
</div> </div>
END_HTML HTML
end end
end end
end end

View File

@ -6,9 +6,9 @@ module Invidious::Frontend::Misc
if preferences.automatic_instance_redirect if preferences.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?referer=#{current_page}"
else else
return "https://redirect.invidious.io#{env.request.resource}" "https://redirect.invidious.io#{env.request.resource}"
end end
end end
end end

View File

@ -60,7 +60,7 @@ module Invidious::Frontend::Pagination
end end
def nav_numeric(locale : String?, *, base_url : String | URI, current_page : Int, show_next : Bool = true) def nav_numeric(locale : String?, *, base_url : String | URI, current_page : Int, show_next : Bool = true)
return String.build do |str| String.build do |str|
str << %(<div class="h-box">\n) str << %(<div class="h-box">\n)
str << %(<div class="page-nav-container flexible">\n) str << %(<div class="page-nav-container flexible">\n)
@ -70,7 +70,7 @@ module Invidious::Frontend::Pagination
params_prev = URI::Params{"page" => (current_page - 1).to_s} params_prev = URI::Params{"page" => (current_page - 1).to_s}
url_prev = HttpServer::Utils.add_params_to_url(base_url, params_prev) url_prev = HttpServer::Utils.add_params_to_url(base_url, params_prev)
self.previous_page(str, locale, url_prev.to_s) previous_page(str, locale, url_prev.to_s)
end end
str << %(</div>\n) str << %(</div>\n)
@ -80,7 +80,7 @@ module Invidious::Frontend::Pagination
params_next = URI::Params{"page" => (current_page + 1).to_s} params_next = URI::Params{"page" => (current_page + 1).to_s}
url_next = HttpServer::Utils.add_params_to_url(base_url, params_next) url_next = HttpServer::Utils.add_params_to_url(base_url, params_next)
self.next_page(str, locale, url_next.to_s) next_page(str, locale, url_next.to_s)
end end
str << %(</div>\n) str << %(</div>\n)
@ -91,7 +91,7 @@ module Invidious::Frontend::Pagination
end end
def nav_ctoken(locale : String?, *, base_url : String | URI, ctoken : String?, first_page : Bool, params : URI::Params) def nav_ctoken(locale : String?, *, base_url : String | URI, ctoken : String?, first_page : Bool, params : URI::Params)
return String.build do |str| String.build do |str|
str << %(<div class="h-box">\n) str << %(<div class="h-box">\n)
str << %(<div class="page-nav-container flexible">\n) str << %(<div class="page-nav-container flexible">\n)
@ -109,7 +109,7 @@ module Invidious::Frontend::Pagination
params["continuation"] = ctoken params["continuation"] = ctoken
url_next = HttpServer::Utils.add_params_to_url(base_url, params) url_next = HttpServer::Utils.add_params_to_url(base_url, params)
self.next_page(str, locale, url_next.to_s) next_page(str, locale, url_next.to_s)
end end
str << %(</div>\n) str << %(</div>\n)

View File

@ -3,7 +3,7 @@ module Invidious::Frontend::SearchFilters
# Generate the search filters collapsable widget. # Generate the search filters collapsable widget.
def generate(filters : Search::Filters, query : String, page : Int, locale : String) : String def generate(filters : Search::Filters, query : String, page : Int, locale : String) : String
return String.build(8000) do |str| String.build(8000) do |str|
str << "<div id='filters'>\n" str << "<div id='filters'>\n"
str << "\t<details id='filters-collapse'>" str << "\t<details id='filters-collapse'>"
str << "\t\t<summary>" << translate(locale, "search_filters_title") << "</summary>\n" str << "\t\t<summary>" << translate(locale, "search_filters_title") << "</summary>\n"

View File

@ -28,12 +28,12 @@ module Invidious::Frontend::WatchPage
end end
url = "/download" url = "/download"
if (CONFIG.invidious_companion.present?) if CONFIG.invidious_companion.present?
invidious_companion = CONFIG.invidious_companion.sample invidious_companion = CONFIG.invidious_companion.sample
url = "#{invidious_companion.public_url}/download?check=#{invidious_companion_encrypt(video.id)}" url = "#{invidious_companion.public_url}/download?check=#{invidious_companion_encrypt(video.id)}"
end end
return String.build(4000) do |str| String.build(4000) do |str|
str << "<form" str << "<form"
str << " class=\"pure-form pure-form-stacked\"" str << " class=\"pure-form pure-form-stacked\""
str << " action='#{url}'" str << " action='#{url}'"

View File

@ -9,7 +9,7 @@ module Invidious::Hashtag
response = YoutubeAPI.browse(continuation: ctoken, client_config: client_config) response = YoutubeAPI.browse(continuation: ctoken, client_config: client_config)
items, _ = extract_items(response) items, _ = extract_items(response)
return items items
end end
def generate_continuation(hashtag : String, cursor : Int) def generate_continuation(hashtag : String, cursor : Int)
@ -37,6 +37,6 @@ module Invidious::Hashtag
.try { |i| Base64.urlsafe_encode(i) } .try { |i| Base64.urlsafe_encode(i) }
.try { |i| URI.encode_www_form(i) } .try { |i| URI.encode_www_form(i) }
return continuation continuation
end end
end end

View File

@ -15,7 +15,7 @@ def github_details(summary : String, content : String)
details += %(\n```) details += %(\n```)
details += %(\n</p>) details += %(\n</p>)
details += %(\n</details>) details += %(\n</details>)
return HTML.escape(details) HTML.escape(details)
end end
def get_issue_template(env : HTTP::Server::Context, exception : Exception) : Tuple(String, String) def get_issue_template(env : HTTP::Server::Context, exception : Exception) : Tuple(String, String)
@ -61,7 +61,8 @@ def error_template_helper(env : HTTP::Server::Context, status_code : Int32, exce
url_new_issue += "?labels=bug&template=bug_report.md&title=" url_new_issue += "?labels=bug&template=bug_report.md&title="
url_new_issue += URI.encode_www_form("[Bug] " + issue_title) url_new_issue += URI.encode_www_form("[Bug] " + issue_title)
error_message = <<-END_HTML # ameba:disable Lint/UselessAssign
error_message = <<-HTML
<div class="error_message"> <div class="error_message">
<h2>#{translate(locale, "crash_page_you_found_a_bug")}</h2> <h2>#{translate(locale, "crash_page_you_found_a_bug")}</h2>
<br/><br/> <br/><br/>
@ -80,13 +81,14 @@ def error_template_helper(env : HTTP::Server::Context, status_code : Int32, exce
<!-- TODO: Add a "copy to clipboard" button --> <!-- TODO: Add a "copy to clipboard" button -->
<pre class="error-issue-template">#{issue_template}</pre> <pre class="error-issue-template">#{issue_template}</pre>
</div> </div>
END_HTML HTML
# Don't show the usual "next steps" widget. The same options are # Don't show the usual "next steps" widget. The same options are
# proposed above the error message, just worded differently. # proposed above the error message, just worded differently.
# ameba:disable Lint/UselessAssign
next_steps = "" next_steps = ""
return templated "error" templated "error"
end end
def error_template_helper(env : HTTP::Server::Context, status_code : Int32, message : String) def error_template_helper(env : HTTP::Server::Context, status_code : Int32, message : String)
@ -95,10 +97,12 @@ def error_template_helper(env : HTTP::Server::Context, status_code : Int32, mess
locale = env.get("preferences").as(Preferences).locale locale = env.get("preferences").as(Preferences).locale
# ameba:disable Lint/UselessAssign
error_message = translate(locale, message) error_message = translate(locale, message)
# ameba:disable Lint/UselessAssign
next_steps = error_redirect_helper(env) next_steps = error_redirect_helper(env)
return templated "error" templated "error"
end end
# ------------------- # -------------------
@ -117,14 +121,14 @@ def error_atom_helper(env : HTTP::Server::Context, status_code : Int32, exceptio
env.response.content_type = "application/atom+xml" env.response.content_type = "application/atom+xml"
env.response.status_code = status_code env.response.status_code = status_code
return "<error>#{exception.inspect_with_backtrace}</error>" "<error>#{exception.inspect_with_backtrace}</error>"
end end
def error_atom_helper(env : HTTP::Server::Context, status_code : Int32, message : String) def error_atom_helper(env : HTTP::Server::Context, status_code : Int32, message : String)
env.response.content_type = "application/atom+xml" env.response.content_type = "application/atom+xml"
env.response.status_code = status_code env.response.status_code = status_code
return "<error>#{message}</error>" "<error>#{message}</error>"
end end
# ------------------- # -------------------
@ -139,7 +143,7 @@ def error_json_helper(
env : HTTP::Server::Context, env : HTTP::Server::Context,
status_code : Int32, status_code : Int32,
exception : Exception, exception : Exception,
additional_fields : Hash(String, Object) | Nil = nil, additional_fields : Hash(String, Object)? = nil,
) )
if exception.is_a?(InfoException) if exception.is_a?(InfoException)
return error_json_helper(env, status_code, exception.message || "", additional_fields) return error_json_helper(env, status_code, exception.message || "", additional_fields)
@ -154,14 +158,14 @@ def error_json_helper(
error_message = error_message.merge(additional_fields) error_message = error_message.merge(additional_fields)
end end
return error_message.to_json error_message.to_json
end end
def error_json_helper( def error_json_helper(
env : HTTP::Server::Context, env : HTTP::Server::Context,
status_code : Int32, status_code : Int32,
message : String, message : String,
additional_fields : Hash(String, Object) | Nil = nil, additional_fields : Hash(String, Object)? = nil,
) )
env.response.content_type = "application/json" env.response.content_type = "application/json"
env.response.status_code = status_code env.response.status_code = status_code
@ -172,7 +176,7 @@ def error_json_helper(
error_message = error_message.merge(additional_fields) error_message = error_message.merge(additional_fields)
end end
return error_message.to_json error_message.to_json
end end
# ------------------- # -------------------
@ -191,7 +195,7 @@ def error_redirect_helper(env : HTTP::Server::Context)
go_to_youtube = translate(locale, "next_steps_error_message_go_to_youtube") go_to_youtube = translate(locale, "next_steps_error_message_go_to_youtube")
switch_instance = translate(locale, "Switch Invidious Instance") switch_instance = translate(locale, "Switch Invidious Instance")
return <<-END_HTML <<-HTML
<p style="margin-bottom: 4px;">#{next_steps_text}</p> <p style="margin-bottom: 4px;">#{next_steps_text}</p>
<ul> <ul>
<li> <li>
@ -204,8 +208,8 @@ def error_redirect_helper(env : HTTP::Server::Context)
<a rel="noreferrer noopener" href="https://youtube.com#{env.request.resource}">#{go_to_youtube}</a> <a rel="noreferrer noopener" href="https://youtube.com#{env.request.resource}">#{go_to_youtube}</a>
</li> </li>
</ul> </ul>
END_HTML HTML
else else
return "" ""
end end
end end

View File

@ -20,7 +20,7 @@ module HTTP::Handler
end end
class Kemal::RouteHandler class Kemal::RouteHandler
{% for method in %w(GET POST PUT HEAD DELETE PATCH OPTIONS) %} {% for method in %w[GET POST PUT HEAD DELETE PATCH OPTIONS] %}
exclude ["/api/v1/*"], {{ method }} exclude ["/api/v1/*"], {{ method }}
{% end %} {% end %}
@ -44,7 +44,7 @@ class Kemal::RouteHandler
end end
class Kemal::ExceptionHandler class Kemal::ExceptionHandler
{% for method in %w(GET POST PUT HEAD DELETE PATCH OPTIONS) %} {% for method in %w[GET POST PUT HEAD DELETE PATCH OPTIONS] %}
exclude ["/api/v1/*"], {{ method }} exclude ["/api/v1/*"], {{ method }}
{% end %} {% end %}
@ -72,7 +72,7 @@ class FilteredCompressHandler < HTTP::CompressHandler
end end
class AuthHandler < Kemal::Handler class AuthHandler < Kemal::Handler
{% for method in %w(GET POST PUT HEAD DELETE PATCH OPTIONS) %} {% for method in %w[GET POST PUT HEAD DELETE PATCH OPTIONS] %}
only ["/api/v1/auth/*"], {{ method }} only ["/api/v1/auth/*"], {{ method }}
{% end %} {% end %}
@ -121,7 +121,7 @@ class AuthHandler < Kemal::Handler
end end
class APIHandler < Kemal::Handler class APIHandler < Kemal::Handler
{% for method in %w(GET POST PUT HEAD DELETE PATCH OPTIONS) %} {% for method in %w[GET POST PUT HEAD DELETE PATCH OPTIONS] %}
only ["/api/v1/*"], {{ method }} only ["/api/v1/*"], {{ method }}
{% end %} {% end %}
exclude ["/api/v1/auth/notifications"], "GET" exclude ["/api/v1/auth/notifications"], "GET"

View File

@ -32,7 +32,7 @@ def html_to_content(description_html : String)
description = XML.parse_html(description).content.strip("\n ") description = XML.parse_html(description).content.strip("\n ")
end end
return description description
end end
def cache_annotation(id, annotations) def cache_annotation(id, annotations)
@ -165,7 +165,7 @@ def create_notification_stream(env, topics, connection_channel)
end end
def extract_initial_data(body) : Hash(String, JSON::Any) def extract_initial_data(body) : Hash(String, JSON::Any)
return JSON.parse(body.match(/(window\["ytInitialData"\]|var\s*ytInitialData)\s*=\s*(?<info>{.*?});<\/script>/mx).try &.["info"] || "{}").as_h JSON.parse(body.match(/(window\["ytInitialData"\]|var\s*ytInitialData)\s*=\s*(?<info>{.*?});<\/script>/mx).try &.["info"] || "{}").as_h
end end
def proxy_file(response, env) def proxy_file(response, env)
@ -196,5 +196,5 @@ def get_playback_statistic
Invidious::Jobs::StatisticsRefreshJob::STATISTICS["playback"] = tracker Invidious::Jobs::StatisticsRefreshJob::STATISTICS["playback"] = tracker
end end
return tracker.as(Hash(String, Int64 | Float64)) tracker.as(Hash(String, Int64 | Float64))
end end

View File

@ -91,10 +91,10 @@ def load_all_locales
locales[name] = JSON.parse(File.read("locales/#{name}.json")).as_h locales[name] = JSON.parse(File.read("locales/#{name}.json")).as_h
end end
return locales locales
end end
def translate(locale : String?, key : String, text : String | Hash(String, String) | Nil = nil) : String def translate(locale : String?, key : String, text : String | Hash(String, String)? = nil) : String
# Log a warning if "key" doesn't exist in en-US locale and return # 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. # that key as the text, so this is more or less transparent to the user.
if !LOCALES["en-US"].has_key?(key) if !LOCALES["en-US"].has_key?(key)
@ -141,7 +141,7 @@ def translate(locale : String?, key : String, text : String | Hash(String, Strin
end end
end end
return translation translation
end end
def translate_count(locale : String, key : String, count : Int, format = NumberFormatting::None) : String def translate_count(locale : String, key : String, count : Int, format = NumberFormatting::None) : String
@ -177,15 +177,15 @@ def translate_count(locale : String, key : String, count : Int, format = NumberF
else count_txt = count.to_s else count_txt = count.to_s
end end
return translation.gsub("{{count}}", count_txt) translation.gsub("{{count}}", count_txt)
end end
def translate_bool(locale : String?, translation : Bool) def translate_bool(locale : String?, translation : Bool)
case translation case translation
when true when true
return translate(locale, "Yes") translate(locale, "Yes")
when false when false
return translate(locale, "No") translate(locale, "No")
end end
end end
@ -195,5 +195,5 @@ def locale_is_rtl?(locale : String?)
# Arabic, Persian, Hebrew # Arabic, Persian, Hebrew
# See https://en.wikipedia.org/wiki/Right-to-left_script#List_of_RTL_scripts # See https://en.wikipedia.org/wiki/Right-to-left_script#List_of_RTL_scripts
return {"ar", "fa", "he"}.includes? locale {"ar", "fa", "he"}.includes? locale
end end

View File

@ -151,17 +151,17 @@ module I18next::Plurals
@version = version.to_u8 @version = version.to_u8
end end
self.init_rules init_rules
end end
def init_rules def init_rules
# Look into sets # Look into sets
PLURAL_SETS.each do |form, langs| PLURAL_SETS.each do |form, langs|
langs.each { |lang| self.forms[lang] = form } langs.each { |lang| forms[lang] = form }
end end
# Add plurals from the "singles" set # Add plurals from the "singles" set
self.forms.merge!(PLURAL_SINGLES) forms.merge!(PLURAL_SINGLES)
end end
def get_plural_form(locale : String) : PluralForms def get_plural_form(locale : String) : PluralForms
@ -170,12 +170,12 @@ module I18next::Plurals
locale = locale.split('-')[0] locale = locale.split('-')[0]
end end
return self.forms[locale] if self.forms[locale]? return forms[locale] if forms[locale]?
# If nothing was found, then use the most common form, i.e # If nothing was found, then use the most common form, i.e
# one singular and one plural, as in english. Not perfect, # one singular and one plural, as in english. Not perfect,
# but better than yielding an exception at the user. # but better than yielding an exception at the user.
return PluralForms::Single_not_one PluralForms::Single_not_one
end end
def get_suffix(locale : String, count : Int) : String def get_suffix(locale : String, count : Int) : String
@ -183,19 +183,19 @@ module I18next::Plurals
# determine if comparison should be done on a signed or unsigned integer, # determine if comparison should be done on a signed or unsigned integer,
# but this variable is never set, resulting in the comparison always # but this variable is never set, resulting in the comparison always
# being done on absolute numbers. # being done on absolute numbers.
return get_suffix_retrocompat(locale, count.abs) get_suffix_retrocompat(locale, count.abs)
end end
# 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 simple_plural?(form : PluralForms) : Bool
case form case form
when .single_gt_one? then return true when .single_gt_one? then true
when .single_not_one? then return true when .single_not_one? then true
when .special_icelandic? then return true when .special_icelandic? then true
when .special_macedonian? then return true when .special_macedonian? then true
else else
return false false
end end
end end
@ -226,7 +226,7 @@ module I18next::Plurals
# when 2 # when 2
# return "_#{suffix}" # return "_#{suffix}"
# else # v3 # else # v3
return "_#{idx}" "_#{idx}"
# end # end
end end
end end
@ -238,35 +238,35 @@ module I18next::Plurals
module SuffixIndex module SuffixIndex
def self.get_index(plural_form : PluralForms, count : Int) : UInt8 def self.get_index(plural_form : PluralForms, count : Int) : UInt8
case plural_form case plural_form
when .single_gt_one? then return (count > 1) ? 1_u8 : 0_u8 when .single_gt_one? then (count > 1) ? 1_u8 : 0_u8
when .single_not_one? then return (count != 1) ? 1_u8 : 0_u8 when .single_not_one? then (count != 1) ? 1_u8 : 0_u8
when .none? then return 0_u8 when .none? then 0_u8
when .dual_slavic? then return dual_slavic(count) when .dual_slavic? then dual_slavic(count)
when .special_arabic? then return special_arabic(count) when .special_arabic? then special_arabic(count)
when .special_czech_slovak? then return special_czech_slovak(count) when .special_czech_slovak? then special_czech_slovak(count)
when .special_polish_kashubian? then return special_polish_kashubian(count) when .special_polish_kashubian? then special_polish_kashubian(count)
when .special_welsh? then return special_welsh(count) when .special_welsh? then special_welsh(count)
when .special_irish? then return special_irish(count) when .special_irish? then special_irish(count)
when .special_scottish_gaelic? then return special_scottish_gaelic(count) when .special_scottish_gaelic? then special_scottish_gaelic(count)
when .special_icelandic? then return special_icelandic(count) when .special_icelandic? then special_icelandic(count)
when .special_javanese? then return special_javanese(count) when .special_javanese? then special_javanese(count)
when .special_cornish? then return special_cornish(count) when .special_cornish? then special_cornish(count)
when .special_lithuanian? then return special_lithuanian(count) when .special_lithuanian? then special_lithuanian(count)
when .special_latvian? then return special_latvian(count) when .special_latvian? then special_latvian(count)
when .special_macedonian? then return special_macedonian(count) when .special_macedonian? then special_macedonian(count)
when .special_mandinka? then return special_mandinka(count) when .special_mandinka? then special_mandinka(count)
when .special_maltese? then return special_maltese(count) when .special_maltese? then special_maltese(count)
when .special_romanian? then return special_romanian(count) when .special_romanian? then special_romanian(count)
when .special_slovenian? then return special_slovenian(count) when .special_slovenian? then special_slovenian(count)
when .special_hebrew? then return special_hebrew(count) when .special_hebrew? then special_hebrew(count)
when .special_odia? then return special_odia(count) when .special_odia? then 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 special_cldr_spanish_italian(count)
when .special_french_portuguese? then return special_cldr_french_portuguese(count) when .special_french_portuguese? then special_cldr_french_portuguese(count)
when .special_hungarian_serbian? then return special_cldr_hungarian_serbian(count) when .special_hungarian_serbian? then special_cldr_hungarian_serbian(count)
else else
# default, if nothing matched above # default, if nothing matched above
return 0_u8 0_u8
end end
end end
@ -280,11 +280,11 @@ module I18next::Plurals
n_mod_100 = count % 100 n_mod_100 = count % 100
if n_mod_10 == 1 && n_mod_100 != 11 if n_mod_10 == 1 && n_mod_100 != 11
return 0_u8 0_u8
elsif n_mod_10 >= 2 && n_mod_10 <= 4 && (n_mod_100 < 10 || n_mod_100 >= 20) elsif n_mod_10 >= 2 && n_mod_10 <= 4 && (n_mod_100 < 10 || n_mod_100 >= 20)
return 1_u8 1_u8
else else
return 2_u8 2_u8
end end
end end
@ -294,13 +294,13 @@ module I18next::Plurals
# Rule: (n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n%100>=3 && n%100<=10 ? 3 : n%100>=11 ? 4 : 5) # Rule: (n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n%100>=3 && n%100<=10 ? 3 : n%100>=11 ? 4 : 5)
# #
def self.special_arabic(count : Int) : UInt8 def self.special_arabic(count : Int) : UInt8
return count.to_u8 if (count == 0 || count == 1 || count == 2) return count.to_u8 if count == 0 || count == 1 || count == 2
n_mod_100 = count % 100 n_mod_100 = count % 100
return 3_u8 if (n_mod_100 >= 3 && n_mod_100 <= 10) return 3_u8 if n_mod_100 >= 3 && n_mod_100 <= 10
return 4_u8 if (n_mod_100 >= 11) return 4_u8 if n_mod_100 >= 11
return 5_u8 5_u8
end end
# Plural form for Czech and Slovak languages # Plural form for Czech and Slovak languages
@ -309,9 +309,9 @@ module I18next::Plurals
# Rule: ((n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2) # Rule: ((n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2)
# #
def self.special_czech_slovak(count : Int) : UInt8 def self.special_czech_slovak(count : Int) : UInt8
return 0_u8 if (count == 1) return 0_u8 if count == 1
return 1_u8 if (count >= 2 && count <= 4) return 1_u8 if count >= 2 && count <= 4
return 2_u8 2_u8
end end
# Plural form for Polish and Kashubian languages # Plural form for Polish and Kashubian languages
@ -320,15 +320,15 @@ module I18next::Plurals
# Rule: (n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2) # Rule: (n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2)
# #
def self.special_polish_kashubian(count : Int) : UInt8 def self.special_polish_kashubian(count : Int) : UInt8
return 0_u8 if (count == 1) return 0_u8 if count == 1
n_mod_10 = count % 10 n_mod_10 = count % 10
n_mod_100 = count % 100 n_mod_100 = count % 100
if n_mod_10 >= 2 && n_mod_10 <= 4 && (n_mod_100 < 10 || n_mod_100 >= 20) if n_mod_10 >= 2 && n_mod_10 <= 4 && (n_mod_100 < 10 || n_mod_100 >= 20)
return 1_u8 1_u8
else else
return 2_u8 2_u8
end end
end end
@ -338,10 +338,10 @@ module I18next::Plurals
# Rule: ((n==1) ? 0 : (n==2) ? 1 : (n != 8 && n != 11) ? 2 : 3) # Rule: ((n==1) ? 0 : (n==2) ? 1 : (n != 8 && n != 11) ? 2 : 3)
# #
def self.special_welsh(count : Int) : UInt8 def self.special_welsh(count : Int) : UInt8
return 0_u8 if (count == 1) return 0_u8 if count == 1
return 1_u8 if (count == 2) return 1_u8 if count == 2
return 2_u8 if (count != 8 && count != 11) return 2_u8 if count != 8 && count != 11
return 3_u8 3_u8
end end
# Plural form for Irish language # Plural form for Irish language
@ -350,11 +350,11 @@ module I18next::Plurals
# Rule: (n==1 ? 0 : n==2 ? 1 : n<7 ? 2 : n<11 ? 3 : 4) # Rule: (n==1 ? 0 : n==2 ? 1 : n<7 ? 2 : n<11 ? 3 : 4)
# #
def self.special_irish(count : Int) : UInt8 def self.special_irish(count : Int) : UInt8
return 0_u8 if (count == 1) return 0_u8 if count == 1
return 1_u8 if (count == 2) return 1_u8 if count == 2
return 2_u8 if (count < 7) return 2_u8 if count < 7
return 3_u8 if (count < 11) return 3_u8 if count < 11
return 4_u8 4_u8
end end
# Plural form for Gaelic language # Plural form for Gaelic language
@ -363,10 +363,10 @@ module I18next::Plurals
# Rule: ((n==1 || n==11) ? 0 : (n==2 || n==12) ? 1 : (n > 2 && n < 20) ? 2 : 3) # Rule: ((n==1 || n==11) ? 0 : (n==2 || n==12) ? 1 : (n > 2 && n < 20) ? 2 : 3)
# #
def self.special_scottish_gaelic(count : Int) : UInt8 def self.special_scottish_gaelic(count : Int) : UInt8
return 0_u8 if (count == 1 || count == 11) return 0_u8 if count == 1 || count == 11
return 1_u8 if (count == 2 || count == 12) return 1_u8 if count == 2 || count == 12
return 2_u8 if (count > 2 && count < 20) return 2_u8 if count > 2 && count < 20
return 3_u8 3_u8
end end
# Plural form for Icelandic language # Plural form for Icelandic language
@ -376,9 +376,9 @@ module I18next::Plurals
# #
def self.special_icelandic(count : Int) : UInt8 def self.special_icelandic(count : Int) : UInt8
if (count % 10) != 1 || (count % 100) == 11 if (count % 10) != 1 || (count % 100) == 11
return 1_u8 1_u8
else else
return 0_u8 0_u8
end end
end end
@ -388,7 +388,7 @@ module I18next::Plurals
# Rule: (n !== 0) # Rule: (n !== 0)
# #
def self.special_javanese(count : Int) : UInt8 def self.special_javanese(count : Int) : UInt8
return (count != 0) ? 1_u8 : 0_u8 (count != 0) ? 1_u8 : 0_u8
end end
# Plural form for Cornish language # Plural form for Cornish language
@ -400,7 +400,7 @@ module I18next::Plurals
return 0_u8 if count == 1 return 0_u8 if count == 1
return 1_u8 if count == 2 return 1_u8 if count == 2
return 2_u8 if count == 3 return 2_u8 if count == 3
return 3_u8 3_u8
end end
# Plural form for Lithuanian language # Plural form for Lithuanian language
@ -413,11 +413,11 @@ module I18next::Plurals
n_mod_100 = count % 100 n_mod_100 = count % 100
if n_mod_10 == 1 && n_mod_100 != 11 if n_mod_10 == 1 && n_mod_100 != 11
return 0_u8 0_u8
elsif n_mod_10 >= 2 && (n_mod_100 < 10 || n_mod_100 >= 20) elsif n_mod_10 >= 2 && (n_mod_100 < 10 || n_mod_100 >= 20)
return 1_u8 1_u8
else else
return 2_u8 2_u8
end end
end end
@ -428,11 +428,11 @@ module I18next::Plurals
# #
def self.special_latvian(count : Int) : UInt8 def self.special_latvian(count : Int) : UInt8
if (count % 10) == 1 && (count % 100) != 11 if (count % 10) == 1 && (count % 100) != 11
return 0_u8 0_u8
elsif count != 0 elsif count != 0
return 1_u8 1_u8
else else
return 2_u8 2_u8
end end
end end
@ -443,9 +443,9 @@ module I18next::Plurals
# #
def self.special_macedonian(count : Int) : UInt8 def self.special_macedonian(count : Int) : UInt8
if count == 1 || ((count % 10) == 1 && (count % 100) != 11) if count == 1 || ((count % 10) == 1 && (count % 100) != 11)
return 0_u8 0_u8
else else
return 1_u8 1_u8
end end
end end
@ -455,7 +455,7 @@ module I18next::Plurals
# Rule: (n==0 ? 0 : n==1 ? 1 : 2) # Rule: (n==0 ? 0 : n==1 ? 1 : 2)
# #
def self.special_mandinka(count : Int) : UInt8 def self.special_mandinka(count : Int) : UInt8
return (count == 0 || count == 1) ? count.to_u8 : 2_u8 (count == 0 || count == 1) ? count.to_u8 : 2_u8
end end
# Plural form for Maltese language # Plural form for Maltese language
@ -468,9 +468,9 @@ module I18next::Plurals
return 1_u8 if count == 0 return 1_u8 if count == 0
n_mod_100 = count % 100 n_mod_100 = count % 100
return 1_u8 if (n_mod_100 > 1 && n_mod_100 < 11) return 1_u8 if n_mod_100 > 1 && n_mod_100 < 11
return 2_u8 if (n_mod_100 > 10 && n_mod_100 < 20) return 2_u8 if n_mod_100 > 10 && n_mod_100 < 20
return 3_u8 3_u8
end end
# Plural form for Romanian language # Plural form for Romanian language
@ -483,8 +483,8 @@ module I18next::Plurals
return 1_u8 if count == 0 return 1_u8 if count == 0
n_mod_100 = count % 100 n_mod_100 = count % 100
return 1_u8 if (n_mod_100 > 0 && n_mod_100 < 20) return 1_u8 if n_mod_100 > 0 && n_mod_100 < 20
return 2_u8 2_u8
end end
# Plural form for Slovenian language # Plural form for Slovenian language
@ -494,10 +494,10 @@ module I18next::Plurals
# #
def self.special_slovenian(count : Int) : UInt8 def self.special_slovenian(count : Int) : UInt8
n_mod_100 = count % 100 n_mod_100 = count % 100
return 1_u8 if (n_mod_100 == 1) return 1_u8 if n_mod_100 == 1
return 2_u8 if (n_mod_100 == 2) return 2_u8 if n_mod_100 == 2
return 3_u8 if (n_mod_100 == 3 || n_mod_100 == 4) return 3_u8 if n_mod_100 == 3 || n_mod_100 == 4
return 0_u8 0_u8
end end
# Plural form for Hebrew language # Plural form for Hebrew language
@ -506,13 +506,13 @@ module I18next::Plurals
# Rule: (n==1 ? 0 : n==2 ? 1 : (n<0 || n>10) && n%10==0 ? 2 : 3) # Rule: (n==1 ? 0 : n==2 ? 1 : (n<0 || n>10) && n%10==0 ? 2 : 3)
# #
def self.special_hebrew(count : Int) : UInt8 def self.special_hebrew(count : Int) : UInt8
return 0_u8 if (count == 1) return 0_u8 if count == 1
return 1_u8 if (count == 2) return 1_u8 if count == 2
if (count < 0 || count > 10) && (count % 10) == 0 if (count < 0 || count > 10) && (count % 10) == 0
return 2_u8 2_u8
else else
return 3_u8 3_u8
end end
end end
@ -523,7 +523,7 @@ module I18next::Plurals
# special rule for it. # special rule for it.
# #
def self.special_odia(count : Int) : UInt8 def self.special_odia(count : Int) : UInt8
return (count == 1) ? 0_u8 : 1_u8 (count == 1) ? 0_u8 : 1_u8
end end
# ------------------- # -------------------
@ -535,9 +535,9 @@ 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 2_u8 # other
end end
# Plural form for French and Portuguese # Plural form for French and Portuguese
@ -545,9 +545,9 @@ 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 2_u8 # other
end end
# Plural form for Hungarian and Serbian # Plural form for Hungarian and Serbian
@ -558,9 +558,9 @@ module I18next::Plurals
n_mod_10 = count % 10 n_mod_10 = count % 10
n_mod_100 = count % 100 n_mod_100 = count % 100
return 0_u8 if (n_mod_10 == 1 && n_mod_100 != 11) # one return 0_u8 if n_mod_10 == 1 && n_mod_100 != 11 # one
return 1_u8 if (2 <= n_mod_10 <= 4 && (n_mod_100 < 12 || 14 < n_mod_100)) # few return 1_u8 if 2 <= n_mod_10 <= 4 && (n_mod_100 < 12 || 14 < n_mod_100) # few
return 2_u8 # other 2_u8 # other
end end
end end
end end

View File

@ -55,7 +55,7 @@ class Invidious::LogHandler < Kemal::BaseLogHandler
end end
end 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
puts("#{Time.utc} [{{ level.id }}] #{message}".colorize(color(LogLevel::{{ level.id.capitalize }}))) puts("#{Time.utc} [{{ level.id }}] #{message}".colorize(color(LogLevel::{{ level.id.capitalize }})))

View File

@ -28,19 +28,19 @@ struct SearchVideo
property badges : VideoBadges property badges : VideoBadges
def to_xml(auto_generated, query_params, xml : XML::Builder) def to_xml(auto_generated, query_params, xml : XML::Builder)
query_params["v"] = self.id query_params["v"] = id
xml.element("entry") do xml.element("entry") do
xml.element("id") { xml.text "yt:video:#{self.id}" } xml.element("id") { xml.text "yt:video:#{id}" }
xml.element("yt:videoId") { xml.text self.id } xml.element("yt:videoId") { xml.text id }
xml.element("yt:channelId") { xml.text self.ucid } xml.element("yt:channelId") { xml.text ucid }
xml.element("title") { xml.text self.title } xml.element("title") { xml.text title }
xml.element("link", rel: "alternate", href: "#{HOST_URL}/watch?#{query_params}") xml.element("link", rel: "alternate", href: "#{HOST_URL}/watch?#{query_params}")
xml.element("author") do xml.element("author") do
if auto_generated if auto_generated
xml.element("name") { xml.text self.author } xml.element("name") { xml.text author }
xml.element("uri") { xml.text "#{HOST_URL}/channel/#{self.ucid}" } xml.element("uri") { xml.text "#{HOST_URL}/channel/#{ucid}" }
else else
xml.element("name") { xml.text author } xml.element("name") { xml.text author }
xml.element("uri") { xml.text "#{HOST_URL}/channel/#{ucid}" } xml.element("uri") { xml.text "#{HOST_URL}/channel/#{ucid}" }
@ -50,24 +50,24 @@ struct SearchVideo
xml.element("content", type: "xhtml") do xml.element("content", type: "xhtml") do
xml.element("div", xmlns: "http://www.w3.org/1999/xhtml") do xml.element("div", xmlns: "http://www.w3.org/1999/xhtml") do
xml.element("a", href: "#{HOST_URL}/watch?#{query_params}") do xml.element("a", href: "#{HOST_URL}/watch?#{query_params}") do
xml.element("img", src: "#{HOST_URL}/vi/#{self.id}/mqdefault.jpg") xml.element("img", src: "#{HOST_URL}/vi/#{id}/mqdefault.jpg")
end end
xml.element("p", style: "word-break:break-word;white-space:pre-wrap") { xml.text html_to_content(self.description_html) } xml.element("p", style: "word-break:break-word;white-space:pre-wrap") { xml.text html_to_content(description_html) }
end end
end end
xml.element("published") { xml.text self.published.to_s("%Y-%m-%dT%H:%M:%S%:z") } xml.element("published") { xml.text published.to_s("%Y-%m-%dT%H:%M:%S%:z") }
xml.element("media:group") do xml.element("media:group") do
xml.element("media:title") { xml.text self.title } xml.element("media:title") { xml.text title }
xml.element("media:thumbnail", url: "#{HOST_URL}/vi/#{self.id}/mqdefault.jpg", xml.element("media:thumbnail", url: "#{HOST_URL}/vi/#{id}/mqdefault.jpg",
width: "320", height: "180") width: "320", height: "180")
xml.element("media:description") { xml.text html_to_content(self.description_html) } xml.element("media:description") { xml.text html_to_content(description_html) }
end end
xml.element("media:community") do xml.element("media:community") do
xml.element("media:statistics", views: self.views) xml.element("media:statistics", views: views)
end end
end end
end end
@ -81,13 +81,13 @@ struct SearchVideo
def to_json(locale : String?, json : JSON::Builder) def to_json(locale : String?, json : JSON::Builder)
json.object do json.object do
json.field "type", "video" json.field "type", "video"
json.field "title", self.title json.field "title", title
json.field "videoId", self.id json.field "videoId", id
json.field "author", self.author json.field "author", author
json.field "authorId", self.ucid json.field "authorId", ucid
json.field "authorUrl", "/channel/#{self.ucid}" json.field "authorUrl", "/channel/#{ucid}"
json.field "authorVerified", self.author_verified json.field "authorVerified", author_verified
author_thumbnail = self.author_thumbnail author_thumbnail = self.author_thumbnail
@ -108,31 +108,31 @@ struct SearchVideo
end end
json.field "videoThumbnails" do json.field "videoThumbnails" do
Invidious::JSONify::APIv1.thumbnails(json, self.id) Invidious::JSONify::APIv1.thumbnails(json, id)
end end
json.field "description", html_to_content(self.description_html) json.field "description", html_to_content(description_html)
json.field "descriptionHtml", self.description_html json.field "descriptionHtml", description_html
json.field "viewCount", self.views json.field "viewCount", views
json.field "viewCountText", translate_count(locale, "generic_views_count", self.views, NumberFormatting::Short) json.field "viewCountText", translate_count(locale, "generic_views_count", views, NumberFormatting::Short)
json.field "published", self.published.to_unix json.field "published", published.to_unix
json.field "publishedText", translate(locale, "`x` ago", recode_date(self.published, locale)) json.field "publishedText", translate(locale, "`x` ago", recode_date(published, locale))
json.field "lengthSeconds", self.length_seconds json.field "lengthSeconds", length_seconds
json.field "liveNow", self.badges.live_now? json.field "liveNow", badges.live_now?
json.field "premium", self.badges.premium? json.field "premium", badges.premium?
json.field "isUpcoming", self.upcoming? json.field "isUpcoming", upcoming?
if self.premiere_timestamp if premiere_timestamp
json.field "premiereTimestamp", self.premiere_timestamp.try &.to_unix json.field "premiereTimestamp", premiere_timestamp.try &.to_unix
end end
json.field "isNew", self.badges.new? json.field "isNew", badges.new?
json.field "is4k", self.badges.four_k? json.field "is4k", badges.four_k?
json.field "is8k", self.badges.eight_k? json.field "is8k", badges.eight_k?
json.field "isVr180", self.badges.vr180? json.field "isVr180", badges.vr180?
json.field "isVr360", self.badges.vr360? json.field "isVr360", badges.vr360?
json.field "is3d", self.badges.three_d? json.field "is3d", badges.three_d?
json.field "hasCaptions", self.badges.closed_captions? json.field "hasCaptions", badges.closed_captions?
end end
end end
@ -175,20 +175,20 @@ struct SearchPlaylist
def to_json(locale : String?, json : JSON::Builder) def to_json(locale : String?, json : JSON::Builder)
json.object do json.object do
json.field "type", "playlist" json.field "type", "playlist"
json.field "title", self.title json.field "title", title
json.field "playlistId", self.id json.field "playlistId", id
json.field "playlistThumbnail", self.thumbnail json.field "playlistThumbnail", thumbnail
json.field "author", self.author json.field "author", author
json.field "authorId", self.ucid json.field "authorId", ucid
json.field "authorUrl", "/channel/#{self.ucid}" json.field "authorUrl", "/channel/#{ucid}"
json.field "authorVerified", self.author_verified json.field "authorVerified", author_verified
json.field "videoCount", self.video_count json.field "videoCount", video_count
json.field "videos" do json.field "videos" do
json.array do json.array do
self.videos.each do |video| videos.each do |video|
json.object do json.object do
json.field "title", video.title json.field "title", video.title
json.field "videoId", video.id json.field "videoId", video.id
@ -232,17 +232,17 @@ struct SearchChannel
def to_json(locale : String?, json : JSON::Builder) def to_json(locale : String?, json : JSON::Builder)
json.object do json.object do
json.field "type", "channel" json.field "type", "channel"
json.field "author", self.author json.field "author", author
json.field "authorId", self.ucid json.field "authorId", ucid
json.field "authorUrl", "/channel/#{self.ucid}" json.field "authorUrl", "/channel/#{ucid}"
json.field "authorVerified", self.author_verified json.field "authorVerified", author_verified
json.field "authorThumbnails" do json.field "authorThumbnails" do
json.array do json.array do
qualities = {32, 48, 76, 100, 176, 512} qualities = {32, 48, 76, 100, 176, 512}
qualities.each do |quality| qualities.each do |quality|
json.object do json.object do
json.field "url", self.author_thumbnail.gsub(/=s\d+/, "=s#{quality}") json.field "url", author_thumbnail.gsub(/=s\d+/, "=s#{quality}")
json.field "width", quality json.field "width", quality
json.field "height", quality json.field "height", quality
end end
@ -250,13 +250,13 @@ struct SearchChannel
end end
end end
json.field "autoGenerated", self.auto_generated json.field "autoGenerated", auto_generated
json.field "subCount", self.subscriber_count json.field "subCount", subscriber_count
json.field "videoCount", self.video_count json.field "videoCount", video_count
json.field "channelHandle", self.channel_handle json.field "channelHandle", channel_handle
json.field "description", html_to_content(self.description_html) json.field "description", html_to_content(description_html)
json.field "descriptionHtml", self.description_html json.field "descriptionHtml", description_html
end end
end end
@ -283,10 +283,10 @@ struct SearchHashtag
def to_json(locale : String?, json : JSON::Builder) def to_json(locale : String?, json : JSON::Builder)
json.object do json.object do
json.field "type", "hashtag" json.field "type", "hashtag"
json.field "title", self.title json.field "title", title
json.field "url", self.url json.field "url", url
json.field "videoCount", self.video_count json.field "videoCount", video_count
json.field "channelCount", self.channel_count json.field "channelCount", channel_count
end end
end end
end end
@ -315,7 +315,7 @@ struct ProblematicTimelineItem
# Provides compatibility with PlaylistVideo # Provides compatibility with PlaylistVideo
def to_json(json : JSON::Builder, *args, **kwargs) def to_json(json : JSON::Builder, *args, **kwargs)
return to_json("", json) to_json("", json)
end end
def to_xml(env, locale, xml : XML::Builder) def to_xml(env, locale, xml : XML::Builder)
@ -352,10 +352,10 @@ class Category
def to_json(locale : String?, json : JSON::Builder) def to_json(locale : String?, json : JSON::Builder)
json.object do json.object do
json.field "type", "category" json.field "type", "category"
json.field "title", self.title json.field "title", title
json.field "contents" do json.field "contents" do
json.array do json.array do
self.contents.each do |item| contents.each do |item|
item.to_json(locale, json) item.to_json(locale, json)
end end
end end

View File

@ -16,7 +16,7 @@ def generate_token(email, scopes, expire, key)
token["signature"] = sign_token(key, token) token["signature"] = sign_token(key, token)
return token.to_json token.to_json
end end
def generate_response(session, scopes, key, expire = 6.hours, use_nonce = false) def generate_response(session, scopes, key, expire = 6.hours, use_nonce = false)
@ -36,7 +36,7 @@ def generate_response(session, scopes, key, expire = 6.hours, use_nonce = false)
token["signature"] = sign_token(key, token) token["signature"] = sign_token(key, token)
return token.to_json token.to_json
end end
def sign_token(key, hash) def sign_token(key, hash)
@ -45,6 +45,7 @@ def sign_token(key, hash)
# TODO: figure out which "key" variable is used # TODO: figure out which "key" variable is used
# Ameba reports a warning for "Lint/ShadowingOuterLocalVar" on this # Ameba reports a warning for "Lint/ShadowingOuterLocalVar" on this
# variable, but it's preferable to not touch that (works fine atm). # variable, but it's preferable to not touch that (works fine atm).
# ameba:disable Lint/ShadowingOuterLocalVar
hash.each do |key, value| hash.each do |key, value|
next if key == "signature" next if key == "signature"
@ -63,7 +64,7 @@ def sign_token(key, hash)
end end
string_to_sign = string_to_sign.sort.join("\n") string_to_sign = string_to_sign.sort.join("\n")
return Base64.urlsafe_encode(OpenSSL::HMAC.digest(:sha256, key, string_to_sign)).strip Base64.urlsafe_encode(OpenSSL::HMAC.digest(:sha256, key, string_to_sign)).strip
end end
def validate_request(token, session, request, key, locale = nil) def validate_request(token, session, request, key, locale = nil)
@ -116,7 +117,7 @@ def scope_includes_scope(scope, subset)
subset_endpoint = subset_endpoint.downcase subset_endpoint = subset_endpoint.downcase
if methods.empty? if methods.empty?
methods = %w(GET POST PUT HEAD DELETE PATCH OPTIONS) methods = %w[GET POST PUT HEAD DELETE PATCH OPTIONS]
end end
if methods & subset_methods != subset_methods if methods & subset_methods != subset_methods
@ -131,7 +132,7 @@ def scope_includes_scope(scope, subset)
return false return false
end end
return true true
end end
def scopes_include_scope(scopes, subset) def scopes_include_scope(scopes, subset)
@ -141,5 +142,5 @@ def scopes_include_scope(scopes, subset)
end end
end end
return false false
end end

View File

@ -8,7 +8,7 @@ def ci_lower_bound(pos, n)
z = 1.96 z = 1.96
phat = 1.0*pos/n 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) (phat + z*z/(2*n) - z * Math.sqrt((phat*(1 - phat) + z*z/(4*n))/n))/(1 + z*z/n)
end end
def elapsed_text(elapsed) def elapsed_text(elapsed)
@ -31,12 +31,12 @@ def decode_length_seconds(string)
seconds: length_seconds[2] seconds: length_seconds[2]
).total_seconds.to_i32 ).total_seconds.to_i32
return length_seconds length_seconds
end end
def recode_length_seconds(time) def recode_length_seconds(time)
if time <= 0 if time <= 0
return "" ""
else else
time = time.seconds time = time.seconds
text = "#{time.minutes.to_s.rjust(2, '0')}:#{time.seconds.to_s.rjust(2, '0')}" text = "#{time.minutes.to_s.rjust(2, '0')}:#{time.seconds.to_s.rjust(2, '0')}"
@ -47,7 +47,7 @@ def recode_length_seconds(time)
text = text.lchop('0') text = text.lchop('0')
return text text
end end
end end
@ -66,7 +66,7 @@ def decode_interval(string : String) : Time::Span
time = Time::Span.new(minutes: raw_minutes) time = Time::Span.new(minutes: raw_minutes)
end end
return time time
end end
def decode_time(string) def decode_time(string)
@ -88,7 +88,7 @@ def decode_time(string)
time = hours * 3600 + minutes * 60 + seconds + millis // 1000 time = hours * 3600 + minutes * 60 + seconds + millis // 1000
end end
return time time
end end
def decode_date(string : String) def decode_date(string : String)
@ -108,7 +108,6 @@ def decode_date(string : String)
return Time.utc return Time.utc
when "yesterday" when "yesterday"
return Time.utc - 1.day return Time.utc - 1.day
else nil # Continue
end end
# String matches format "20 hours ago", "4 months ago", "20s ago", "15min ago"... # String matches format "20 hours ago", "4 months ago", "20s ago", "15min ago"...
@ -137,26 +136,26 @@ def decode_date(string : String)
raise "Could not parse #{string}" raise "Could not parse #{string}"
end end
return Time.utc - delta Time.utc - delta
end end
def recode_date(time : Time, locale) def recode_date(time : Time, locale)
span = Time.utc - time span = Time.utc - time
if span.total_days > 365.0 if span.total_days > 365.0
return translate_count(locale, "generic_count_years", span.total_days.to_i // 365) translate_count(locale, "generic_count_years", span.total_days.to_i // 365)
elsif span.total_days > 30.0 elsif span.total_days > 30.0
return translate_count(locale, "generic_count_months", span.total_days.to_i // 30) translate_count(locale, "generic_count_months", span.total_days.to_i // 30)
elsif span.total_days > 7.0 elsif span.total_days > 7.0
return translate_count(locale, "generic_count_weeks", span.total_days.to_i // 7) translate_count(locale, "generic_count_weeks", span.total_days.to_i // 7)
elsif span.total_hours > 24.0 elsif span.total_hours > 24.0
return translate_count(locale, "generic_count_days", span.total_days.to_i) translate_count(locale, "generic_count_days", span.total_days.to_i)
elsif span.total_minutes > 60.0 elsif span.total_minutes > 60.0
return translate_count(locale, "generic_count_hours", span.total_hours.to_i) translate_count(locale, "generic_count_hours", span.total_hours.to_i)
elsif span.total_seconds > 60.0 elsif span.total_seconds > 60.0
return translate_count(locale, "generic_count_minutes", span.total_minutes.to_i) translate_count(locale, "generic_count_minutes", span.total_minutes.to_i)
else else
return translate_count(locale, "generic_count_seconds", span.total_seconds.to_i) translate_count(locale, "generic_count_seconds", span.total_seconds.to_i)
end end
end end
@ -174,9 +173,9 @@ def short_text_to_number(short_text : String) : Int64
when "b" then number *= 1_000_000_000 when "b" then number *= 1_000_000_000
end end
return number.to_i64 number.to_i64
rescue ex rescue ex
return 0_i64 0_i64
end end
def number_to_short_text(number) def number_to_short_text(number)
@ -209,7 +208,7 @@ def arg_array(array, start = 1)
args = args.join(",") args = args.join(",")
end end
return args args
end end
def make_host_url(kemal_config) def make_host_url(kemal_config)
@ -235,7 +234,7 @@ def make_host_url(kemal_config)
host = CONFIG.domain.not_nil!.lchop(".") host = CONFIG.domain.not_nil!.lchop(".")
return "#{scheme}#{host}#{port}" "#{scheme}#{host}#{port}"
end end
def get_referer(env, fallback = "/", unroll = true) def get_referer(env, fallback = "/", unroll = true)
@ -268,13 +267,13 @@ def get_referer(env, fallback = "/", unroll = true)
referer = fallback referer = fallback
end end
return referer referer
end end
def sha256(text) def sha256(text)
digest = OpenSSL::Digest.new("SHA256") digest = OpenSSL::Digest.new("SHA256")
digest << text digest << text
return digest.final.hexstring digest.final.hexstring
end end
def subscribe_pubsub(topic, key) def subscribe_pubsub(topic, key)
@ -302,7 +301,7 @@ def subscribe_pubsub(topic, key)
"hub.secret" => key.to_s, "hub.secret" => key.to_s,
} }
return make_client(PUBSUB_URL, &.post("/subscribe", form: body)) make_client(PUBSUB_URL, &.post("/subscribe", form: body))
end end
def parse_range(range) def parse_range(range)
@ -328,7 +327,7 @@ def reduce_uri(uri : URI | String, max_length : Int32 = 50, suffix : String = "
if str.size > max_length if str.size > max_length
str = "#{str[0, max_length]}#{suffix}" str = "#{str[0, max_length]}#{suffix}"
end end
return str str
end end
# Get the html link from a NavigationEndpoint or an innertubeCommand # Get the html link from a NavigationEndpoint or an innertubeCommand
@ -381,7 +380,7 @@ def parse_link_endpoint(endpoint : JSON::Any, text : String, video_id : String)
text = %(<a href="#{url}">#{reduce_uri(text)}</a>) text = %(<a href="#{url}">#{reduce_uri(text)}</a>)
end end
end end
return text text
end end
def encrypt_ecb_without_salt(data, key) def encrypt_ecb_without_salt(data, key)
@ -394,11 +393,11 @@ def encrypt_ecb_without_salt(data, key)
io.write(cipher.final) io.write(cipher.final)
io.rewind io.rewind
return io io
end end
def invidious_companion_encrypt(data) def invidious_companion_encrypt(data)
timestamp = Time.utc.to_unix timestamp = Time.utc.to_unix
encrypted_data = encrypt_ecb_without_salt("#{timestamp}|#{data}", CONFIG.invidious_companion_key) encrypted_data = encrypt_ecb_without_salt("#{timestamp}|#{data}", CONFIG.invidious_companion_key)
return Base64.urlsafe_encode(encrypted_data) Base64.urlsafe_encode(encrypted_data)
end end

View File

@ -20,7 +20,7 @@ module WebVTT
# Writes an vtt cue with the specified time stamp and contents # Writes an vtt cue with the specified time stamp and contents
def cue(start_time : Time::Span, end_time : Time::Span, text : String) def cue(start_time : Time::Span, end_time : Time::Span, text : String)
timestamp(start_time, end_time) timestamp(start_time, end_time)
@io << self.escape(text) @io << escape(text)
@io << "\n\n" @io << "\n\n"
end end
@ -40,7 +40,7 @@ module WebVTT
end end
private def escape(text : String) : String private def escape(text : String) : String
return text.gsub(ESCAPE_SUBSTITUTIONS) text.gsub(ESCAPE_SUBSTITUTIONS)
end end
def document(setting_fields : Hash(String, String)? = nil, &) def document(setting_fields : Hash(String, String)? = nil, &)

View File

@ -68,7 +68,7 @@ module Invidious::HttpServer
end end
end end
return flush_io_to_cache(retrieve_bytes_from, file_path, file_info) flush_io_to_cache(retrieve_bytes_from, file_path, file_info)
end end
# Writes file data to the cache # Writes file data to the cache
@ -114,7 +114,7 @@ module Invidious::HttpServer
# This is only used in the specs to clear the cache before each handler test # This is only used in the specs to clear the cache before each handler test
def self.clear_cache def self.clear_cache
@@current_cache_size = 0 @@current_cache_size = 0
return @@cached_files.clear @@cached_files.clear
end end
end end
end end

View File

@ -14,9 +14,9 @@ module Invidious::HttpServer
url.query_params = params url.query_params = params
if absolute if absolute
return "#{HOST_URL}#{url.request_target}" "#{HOST_URL}#{url.request_target}"
else else
return url.request_target url.request_target
end end
end end
@ -35,7 +35,7 @@ module Invidious::HttpServer
str << params str << params
end end
return url url
end end
end end
end end

View File

@ -25,7 +25,7 @@ class Invidious::Jobs::InstanceListRefreshJob < Invidious::Jobs::BaseJob
# - Is it an instance with a good uptime? # - Is it an instance with a good uptime?
# - Is it an updated instance? # - Is it an updated instance?
private def refresh_instances private def refresh_instances
raw_instance_list = self.fetch_instances raw_instance_list = fetch_instances
filtered_instance_list = [] of Tuple(String, String) filtered_instance_list = [] of Tuple(String, String)
raw_instance_list.each do |instance_data| raw_instance_list.each do |instance_data|
@ -73,7 +73,7 @@ class Invidious::Jobs::InstanceListRefreshJob < Invidious::Jobs::BaseJob
raw_instance_list = [] of JSON::Any raw_instance_list = [] of JSON::Any
end end
return raw_instance_list raw_instance_list
end end
# Checks if the given target instance is outdated # Checks if the given target instance is outdated
@ -84,7 +84,7 @@ class Invidious::Jobs::InstanceListRefreshJob < Invidious::Jobs::BaseJob
remote_commit_date = Time.parse(remote_commit_date[0], "%Y.%m.%d", Time::Location::UTC) remote_commit_date = Time.parse(remote_commit_date[0], "%Y.%m.%d", Time::Location::UTC)
local_commit_date = Time.parse(CURRENT_VERSION, "%Y.%m.%d", Time::Location::UTC) local_commit_date = Time.parse(CURRENT_VERSION, "%Y.%m.%d", Time::Location::UTC)
return (remote_commit_date - local_commit_date).abs.days > 30 (remote_commit_date - local_commit_date).abs.days > 30
end end
# Checks if the uptime of the target instance is greater than 90% over a 30 day period # Checks if the uptime of the target instance is greater than 90% over a 30 day period
@ -92,6 +92,6 @@ class Invidious::Jobs::InstanceListRefreshJob < Invidious::Jobs::BaseJob
return true if !target_instance_health_monitor["down"].as_bool == false return true if !target_instance_health_monitor["down"].as_bool == false
return true if target_instance_health_monitor["uptime"].as_f < 90 return true if target_instance_health_monitor["uptime"].as_f < 90
return false false
end end
end end

View File

@ -13,10 +13,10 @@ module Invidious::JSONify::APIv1
json.field "error", video.info["reason"] if video.info["reason"]? json.field "error", video.info["reason"] if video.info["reason"]?
json.field "videoThumbnails" do json.field "videoThumbnails" do
self.thumbnails(json, video.id) thumbnails(json, video.id)
end end
json.field "storyboards" do json.field "storyboards" do
self.storyboards(json, video.id, video.storyboards) storyboards(json, video.id, video.storyboards)
end end
json.field "description", video.description json.field "description", video.description
@ -138,7 +138,7 @@ module Invidious::JSONify::APIv1
if fmt_info = Invidious::Videos::Formats.itag_to_metadata?(fmt["itag"]) if fmt_info = Invidious::Videos::Formats.itag_to_metadata?(fmt["itag"])
json.field "container", fmt_info["ext"] json.field "container", fmt_info["ext"]
json.field "encoding", fmt_info["vcodec"]? || fmt_info["acodec"] json.field "encoding", (fmt_info["vcodec"]? || fmt_info["acodec"])
end end
# Livestream chunk infos # Livestream chunk infos
@ -199,7 +199,7 @@ module Invidious::JSONify::APIv1
if fmt_info = Invidious::Videos::Formats.itag_to_metadata?(fmt["itag"]) if fmt_info = Invidious::Videos::Formats.itag_to_metadata?(fmt["itag"])
json.field "container", fmt_info["ext"] json.field "container", fmt_info["ext"]
json.field "encoding", fmt_info["vcodec"]? || fmt_info["acodec"] json.field "encoding", (fmt_info["vcodec"]? || fmt_info["acodec"])
end end
end end
end end
@ -241,7 +241,7 @@ module Invidious::JSONify::APIv1
json.field "videoId", rv["id"] json.field "videoId", rv["id"]
json.field "title", rv["title"] json.field "title", rv["title"]
json.field "videoThumbnails" do json.field "videoThumbnails" do
self.thumbnails(json, rv["id"]) thumbnails(json, rv["id"])
end end
json.field "author", rv["author"] json.field "author", rv["author"]

View File

@ -74,7 +74,7 @@ def fetch_mix(rdid, video_id, cookies = nil, locale = nil)
videos.uniq!(&.id) videos.uniq!(&.id)
videos = videos.first(50) videos = videos.first(50)
return Mix.new({ Mix.new({
title: mix_title, title: mix_title,
id: rdid, id: rdid,
videos: videos, videos: videos,
@ -82,7 +82,7 @@ def fetch_mix(rdid, video_id, cookies = nil, locale = nil)
end end
def template_mix(mix, listen) def template_mix(mix, listen)
html = <<-END_HTML html = <<-HTML
<h3> <h3>
<a href="/mix?list=#{mix["mixId"]}"> <a href="/mix?list=#{mix["mixId"]}">
#{mix["title"]} #{mix["title"]}
@ -90,10 +90,10 @@ def template_mix(mix, listen)
</h3> </h3>
<div class="pure-menu pure-menu-scrollable playlist-restricted"> <div class="pure-menu pure-menu-scrollable playlist-restricted">
<ol class="pure-menu-list"> <ol class="pure-menu-list">
END_HTML HTML
mix["videos"].as_a.each do |video| mix["videos"].as_a.each do |video|
html += <<-END_HTML html += <<-HTML
<li class="pure-menu-item"> <li class="pure-menu-item">
<a href="/watch?v=#{video["videoId"]}&list=#{mix["mixId"]}#{listen ? "&listen=1" : ""}"> <a href="/watch?v=#{video["videoId"]}&list=#{mix["mixId"]}#{listen ? "&listen=1" : ""}">
<div class="thumbnail"> <div class="thumbnail">
@ -106,14 +106,14 @@ def template_mix(mix, listen)
</p> </p>
</a> </a>
</li> </li>
END_HTML HTML
end end
html += <<-END_HTML html += <<-HTML
</ol> </ol>
</div> </div>
<hr> <hr>
END_HTML HTML
html html
end end

View File

@ -13,30 +13,30 @@ struct PlaylistVideo
def to_xml(xml : XML::Builder) def to_xml(xml : XML::Builder)
xml.element("entry") do xml.element("entry") do
xml.element("id") { xml.text "yt:video:#{self.id}" } xml.element("id") { xml.text "yt:video:#{id}" }
xml.element("yt:videoId") { xml.text self.id } xml.element("yt:videoId") { xml.text id }
xml.element("yt:channelId") { xml.text self.ucid } xml.element("yt:channelId") { xml.text ucid }
xml.element("title") { xml.text self.title } xml.element("title") { xml.text title }
xml.element("link", rel: "alternate", href: "#{HOST_URL}/watch?v=#{self.id}") xml.element("link", rel: "alternate", href: "#{HOST_URL}/watch?v=#{id}")
xml.element("author") do xml.element("author") do
xml.element("name") { xml.text self.author } xml.element("name") { xml.text author }
xml.element("uri") { xml.text "#{HOST_URL}/channel/#{self.ucid}" } xml.element("uri") { xml.text "#{HOST_URL}/channel/#{ucid}" }
end end
xml.element("content", type: "xhtml") do xml.element("content", type: "xhtml") do
xml.element("div", xmlns: "http://www.w3.org/1999/xhtml") do xml.element("div", xmlns: "http://www.w3.org/1999/xhtml") do
xml.element("a", href: "#{HOST_URL}/watch?v=#{self.id}") do xml.element("a", href: "#{HOST_URL}/watch?v=#{id}") do
xml.element("img", src: "#{HOST_URL}/vi/#{self.id}/mqdefault.jpg") xml.element("img", src: "#{HOST_URL}/vi/#{id}/mqdefault.jpg")
end end
end end
end end
xml.element("published") { xml.text self.published.to_s("%Y-%m-%dT%H:%M:%S%:z") } xml.element("published") { xml.text published.to_s("%Y-%m-%dT%H:%M:%S%:z") }
xml.element("media:group") do xml.element("media:group") do
xml.element("media:title") { xml.text self.title } xml.element("media:title") { xml.text title }
xml.element("media:thumbnail", url: "#{HOST_URL}/vi/#{self.id}/mqdefault.jpg", xml.element("media:thumbnail", url: "#{HOST_URL}/vi/#{id}/mqdefault.jpg",
width: "320", height: "180") width: "320", height: "180")
end end
end end
@ -54,15 +54,15 @@ struct PlaylistVideo
json.object do json.object do
json.field "type", "video" json.field "type", "video"
json.field "title", self.title json.field "title", title
json.field "videoId", self.id json.field "videoId", id
json.field "author", self.author json.field "author", author
json.field "authorId", self.ucid json.field "authorId", ucid
json.field "authorUrl", "/channel/#{self.ucid}" json.field "authorUrl", "/channel/#{ucid}"
json.field "videoThumbnails" do json.field "videoThumbnails" do
Invidious::JSONify::APIv1.thumbnails(json, self.id) Invidious::JSONify::APIv1.thumbnails(json, id)
end end
if index if index
@ -72,8 +72,8 @@ struct PlaylistVideo
json.field "index", self.index json.field "index", self.index
end end
json.field "lengthSeconds", self.length_seconds json.field "lengthSeconds", length_seconds
json.field "liveNow", self.live_now json.field "liveNow", live_now
end end
end end
@ -101,14 +101,14 @@ struct Playlist
def to_json(offset, json : JSON::Builder, video_id : String? = nil) def to_json(offset, json : JSON::Builder, video_id : String? = nil)
json.object do json.object do
json.field "type", "playlist" json.field "type", "playlist"
json.field "title", self.title json.field "title", title
json.field "playlistId", self.id json.field "playlistId", id
json.field "playlistThumbnail", self.thumbnail json.field "playlistThumbnail", thumbnail
json.field "author", self.author json.field "author", author
json.field "authorId", self.ucid json.field "authorId", ucid
json.field "authorUrl", "/channel/#{self.ucid}" json.field "authorUrl", "/channel/#{ucid}"
json.field "subtitle", self.subtitle json.field "subtitle", subtitle
json.field "authorThumbnails" do json.field "authorThumbnails" do
json.array do json.array do
@ -116,7 +116,7 @@ struct Playlist
qualities.each do |quality| qualities.each do |quality|
json.object do json.object do
json.field "url", self.author_thumbnail.not_nil!.gsub(/=\d+/, "=s#{quality}") json.field "url", author_thumbnail.not_nil!.gsub(/=\d+/, "=s#{quality}")
json.field "width", quality json.field "width", quality
json.field "height", quality json.field "height", quality
end end
@ -124,13 +124,13 @@ struct Playlist
end end
end end
json.field "description", self.description json.field "description", description
json.field "descriptionHtml", self.description_html json.field "descriptionHtml", description_html
json.field "videoCount", self.video_count json.field "videoCount", video_count
json.field "viewCount", self.views json.field "viewCount", views
json.field "updated", self.updated.to_unix json.field "updated", updated.to_unix
json.field "isListed", self.privacy.public? json.field "isListed", privacy.public?
json.field "videos" do json.field "videos" do
json.array do json.array do
@ -180,33 +180,33 @@ struct InvidiousPlaylist
module PlaylistPrivacyConverter module PlaylistPrivacyConverter
def self.from_rs(rs) def self.from_rs(rs)
return PlaylistPrivacy.parse(String.new(rs.read(Slice(UInt8)))) PlaylistPrivacy.parse(String.new(rs.read(Slice(UInt8))))
end end
end end
def to_json(offset, json : JSON::Builder, video_id : String? = nil) def to_json(offset, json : JSON::Builder, video_id : String? = nil)
json.object do json.object do
json.field "type", "invidiousPlaylist" json.field "type", "invidiousPlaylist"
json.field "title", self.title json.field "title", title
json.field "playlistId", self.id json.field "playlistId", id
json.field "author", self.author json.field "author", author
json.field "authorId", self.ucid json.field "authorId", ucid
json.field "authorUrl", nil json.field "authorUrl", nil
json.field "authorThumbnails", [] of String json.field "authorThumbnails", [] of String
json.field "description", html_to_content(self.description_html) json.field "description", html_to_content(description_html)
json.field "descriptionHtml", self.description_html json.field "descriptionHtml", description_html
json.field "videoCount", self.video_count json.field "videoCount", video_count
json.field "viewCount", self.views json.field "viewCount", views
json.field "updated", self.updated.to_unix json.field "updated", updated.to_unix
json.field "isListed", self.privacy.public? json.field "isListed", privacy.public?
json.field "videos" do json.field "videos" do
json.array do json.array do
if (!offset || offset == 0) && !video_id.nil? if (!offset || offset == 0) && !video_id.nil?
index = Invidious::Database::PlaylistVideos.select_index(self.id, video_id) index = Invidious::Database::PlaylistVideos.select_index(id, video_id)
offset = self.index.index(index) || 0 offset = self.index.index(index) || 0
end end
@ -227,7 +227,7 @@ struct InvidiousPlaylist
def thumbnail def thumbnail
# TODO: Get playlist thumbnail from playlist data rather than first video # TODO: Get playlist thumbnail from playlist data rather than first video
@thumbnail_id ||= Invidious::Database::PlaylistVideos.select_one_id(self.id, self.index) || "-----------" @thumbnail_id ||= Invidious::Database::PlaylistVideos.select_one_id(id, index) || "-----------"
"/vi/#{@thumbnail_id}/mqdefault.jpg" "/vi/#{@thumbnail_id}/mqdefault.jpg"
end end
@ -244,7 +244,7 @@ struct InvidiousPlaylist
end end
def description_html def description_html
HTML.escape(self.description) HTML.escape(description)
end end
end end
@ -265,7 +265,7 @@ def create_playlist(title, privacy, user)
Invidious::Database::Playlists.insert(playlist) Invidious::Database::Playlists.insert(playlist)
return playlist playlist
end end
def subscribe_playlist(user, playlist) def subscribe_playlist(user, playlist)
@ -283,7 +283,7 @@ def subscribe_playlist(user, playlist)
Invidious::Database::Playlists.insert(playlist) Invidious::Database::Playlists.insert(playlist)
return playlist playlist
end end
def produce_playlist_continuation(id, index) def produce_playlist_continuation(id, index)
@ -318,18 +318,18 @@ def produce_playlist_continuation(id, index)
.try { |i| Base64.urlsafe_encode(i) } .try { |i| Base64.urlsafe_encode(i) }
.try { |i| URI.encode_www_form(i) } .try { |i| URI.encode_www_form(i) }
return continuation continuation
end end
def get_playlist(plid : String) def get_playlist(plid : String)
if plid.starts_with? "IV" if plid.starts_with? "IV"
if playlist = Invidious::Database::Playlists.select(id: plid) if playlist = Invidious::Database::Playlists.select(id: plid)
return playlist playlist
else else
raise NotFoundException.new("Playlist does not exist.") raise NotFoundException.new("Playlist does not exist.")
end end
else else
return fetch_playlist(plid) fetch_playlist(plid)
end end
end end
@ -398,7 +398,7 @@ def fetch_playlist(plid : String)
ucid = author_info.dig?("title", "runs", 0, "navigationEndpoint", "browseEndpoint", "browseId").try &.as_s || "" ucid = author_info.dig?("title", "runs", 0, "navigationEndpoint", "browseEndpoint", "browseId").try &.as_s || ""
end end
return Playlist.new({ Playlist.new({
title: title, title: title,
id: plid, id: plid,
author: author, author: author,
@ -443,7 +443,7 @@ def get_playlist_videos(playlist : InvidiousPlaylist | Playlist, offset : Int32,
offset += 100 offset += 100
end end
return videos videos
end end
end end
@ -504,11 +504,11 @@ def extract_playlist_videos(initial_data : Hash(String, JSON::Any))
videos << ProblematicTimelineItem.new(parse_exception: ex) videos << ProblematicTimelineItem.new(parse_exception: ex)
end end
return videos videos
end end
def template_playlist(playlist, listen) def template_playlist(playlist, listen)
html = <<-END_HTML html = <<-HTML
<h3> <h3>
<a href="/playlist?list=#{playlist["playlistId"]}"> <a href="/playlist?list=#{playlist["playlistId"]}">
#{playlist["title"]} #{playlist["title"]}
@ -516,10 +516,10 @@ def template_playlist(playlist, listen)
</h3> </h3>
<div class="pure-menu pure-menu-scrollable playlist-restricted"> <div class="pure-menu pure-menu-scrollable playlist-restricted">
<ol class="pure-menu-list"> <ol class="pure-menu-list">
END_HTML HTML
playlist["videos"].as_a.each do |video| playlist["videos"].as_a.each do |video|
html += <<-END_HTML html += <<-HTML
<li class="pure-menu-item" id="#{video["videoId"]}"> <li class="pure-menu-item" id="#{video["videoId"]}">
<a href="/watch?v=#{video["videoId"]}&list=#{playlist["playlistId"]}&index=#{video["index"]}#{listen ? "&listen=1" : ""}"> <a href="/watch?v=#{video["videoId"]}&list=#{playlist["playlistId"]}&index=#{video["index"]}#{listen ? "&listen=1" : ""}">
<div class="thumbnail"> <div class="thumbnail">
@ -532,14 +532,14 @@ def template_playlist(playlist, listen)
</p> </p>
</a> </a>
</li> </li>
END_HTML HTML
end end
html += <<-END_HTML html += <<-HTML
</ol> </ol>
</div> </div>
<hr> <hr>
END_HTML HTML
html html
end end

View File

@ -337,10 +337,10 @@ module Invidious::Routes::Account
end end
if redirect if redirect
return env.redirect referer env.redirect referer
else else
env.response.content_type = "application/json" env.response.content_type = "application/json"
return "{}" "{}"
end end
end end
end end

View File

@ -140,7 +140,7 @@ module Invidious::Routes::API::Manifest
end end
end end
return manifest manifest
end end
# /api/manifest/dash/id/videoplayback # /api/manifest/dash/id/videoplayback

View File

@ -17,6 +17,7 @@ module Invidious::Routes::API::V1::Authenticated
user.preferences.to_json user.preferences.to_json
end end
# ameba:disable Naming/AccessorMethodName
def self.set_preferences(env) def self.set_preferences(env)
env.response.content_type = "application/json" env.response.content_type = "application/json"
user = env.get("user").as(User) user = env.get("user").as(User)
@ -35,7 +36,7 @@ module Invidious::Routes::API::V1::Authenticated
env.response.content_type = "application/json" env.response.content_type = "application/json"
user = env.get("user").as(User) user = env.get("user").as(User)
return Invidious::User::Export.to_invidious(user) Invidious::User::Export.to_invidious(user)
end end
def self.import_invidious(env) def self.import_invidious(env)
@ -71,7 +72,7 @@ module Invidious::Routes::API::V1::Authenticated
end end
watched ||= [] of String watched ||= [] of String
return watched.to_json watched.to_json
end end
def self.mark_watched(env) def self.mark_watched(env)
@ -423,7 +424,7 @@ module Invidious::Routes::API::V1::Authenticated
env.response.content_type = "text/html" env.response.content_type = "text/html"
csrf_token = generate_response(sid, {":authorize_token"}, HMAC_KEY, use_nonce: true) csrf_token = generate_response(sid, {":authorize_token"}, HMAC_KEY, use_nonce: true)
return templated "user/authorize_token" templated "user/authorize_token"
else else
env.response.content_type = "application/json" env.response.content_type = "application/json"
@ -482,7 +483,7 @@ module Invidious::Routes::API::V1::Authenticated
env.response.content_type = "text/event-stream" env.response.content_type = "text/event-stream"
raw_topics = env.params.body["topics"]? || env.params.query["topics"]? raw_topics = env.params.body["topics"]? || env.params.query["topics"]?
topics = raw_topics.try &.split(",").uniq.first(1000) topics = raw_topics.try &.split(",").uniq!.first(1000)
topics ||= [] of String topics ||= [] of String
create_notification_stream(env, topics, CONNECTION_CHANNEL) create_notification_stream(env, topics, CONNECTION_CHANNEL)

View File

@ -137,7 +137,7 @@ module Invidious::Routes::API::V1::Channels
env.params.query.delete("sort_by") if env.params.query.has_key?("sort_by") env.params.query.delete("sort_by") if env.params.query.has_key?("sort_by")
env.params.query.delete("continuation") if env.params.query.has_key?("continuation") env.params.query.delete("continuation") if env.params.query.has_key?("continuation")
return self.videos(env) videos(env)
end end
def self.videos(env) def self.videos(env)
@ -173,7 +173,7 @@ module Invidious::Routes::API::V1::Channels
end end
end end
return JSON.build do |json| JSON.build do |json|
json.object do json.object do
json.field "videos" do json.field "videos" do
json.array do json.array do
@ -219,7 +219,7 @@ module Invidious::Routes::API::V1::Channels
end end
end end
return JSON.build do |json| JSON.build do |json|
json.object do json.object do
json.field "videos" do json.field "videos" do
json.array do json.array do
@ -265,7 +265,7 @@ module Invidious::Routes::API::V1::Channels
end end
end end
return JSON.build do |json| JSON.build do |json|
json.object do json.object do
json.field "videos" do json.field "videos" do
json.array do json.array do
@ -416,7 +416,7 @@ module Invidious::Routes::API::V1::Channels
begin begin
fetch_channel_community(ucid, continuation, locale, format, thin_mode) fetch_channel_community(ucid, continuation, locale, format, thin_mode)
rescue ex rescue ex
return error_json(500, ex) error_json(500, ex)
end end
end end
@ -444,7 +444,7 @@ module Invidious::Routes::API::V1::Channels
begin begin
fetch_channel_community_post(ucid, id, locale, format, thin_mode) fetch_channel_community_post(ucid, id, locale, format, thin_mode)
rescue ex rescue ex
return error_json(500, ex) error_json(500, ex)
end end
end end
@ -472,7 +472,7 @@ module Invidious::Routes::API::V1::Channels
else else
comments = YoutubeAPI.browse(continuation: continuation) comments = YoutubeAPI.browse(continuation: continuation)
end end
return Comments.parse_youtube(id, comments, format, locale, thin_mode, is_post: true) Comments.parse_youtube(id, comments, format, locale, thin_mode, is_post: true)
end end
def self.channels(env) def self.channels(env)

View File

@ -4,10 +4,10 @@ module Invidious::Routes::API::V1::Misc
env.response.content_type = "application/json" env.response.content_type = "application/json"
if !CONFIG.statistics_enabled if !CONFIG.statistics_enabled
return {"software" => SOFTWARE}.to_json {"software" => SOFTWARE}.to_json
else else
# Calculate playback success rate # Calculate playback success rate
if (tracker = Invidious::Jobs::StatisticsRefreshJob::STATISTICS["playback"]?) if tracker = Invidious::Jobs::StatisticsRefreshJob::STATISTICS["playback"]?
tracker = tracker.as(Hash(String, Int64 | Float64)) tracker = tracker.as(Hash(String, Int64 | Float64))
if !tracker.empty? if !tracker.empty?
@ -22,7 +22,7 @@ module Invidious::Routes::API::V1::Misc
end end
end end
return Invidious::Jobs::StatisticsRefreshJob::STATISTICS.to_json Invidious::Jobs::StatisticsRefreshJob::STATISTICS.to_json
end end
end end

View File

@ -53,7 +53,7 @@ module Invidious::Routes::API::V1::Search
end end
end end
rescue ex rescue ex
return error_json(500, ex) error_json(500, ex)
end end
end end

View File

@ -18,7 +18,7 @@ module Invidious::Routes::API::V1::Videos
return error_json(500, ex) return error_json(500, ex)
end end
return JSON.build do |json| JSON.build do |json|
Invidious::JSONify::APIv1.video(video, json, locale: locale, proxy: proxy) Invidious::JSONify::APIv1.video(video, json, locale: locale, proxy: proxy)
end end
end end
@ -247,7 +247,7 @@ module Invidious::Routes::API::V1::Videos
# videojs-vtt-thumbnails is not compliant to the VTT specification, it # videojs-vtt-thumbnails is not compliant to the VTT specification, it
# doesn't unescape the HTML entities, so we have to do it here: # doesn't unescape the HTML entities, so we have to do it here:
# TODO: remove this when we migrate to VideoJS 8 # TODO: remove this when we migrate to VideoJS 8
return HTML.unescape(vtt_file) HTML.unescape(vtt_file)
end end
def self.annotations(env) def self.annotations(env)
@ -352,7 +352,7 @@ module Invidious::Routes::API::V1::Videos
return error_json(500, ex) return error_json(500, ex)
end end
return comments comments
elsif source == "reddit" elsif source == "reddit"
sort_by ||= "confidence" sort_by ||= "confidence"
@ -418,7 +418,7 @@ module Invidious::Routes::API::V1::Videos
return error_json(500, ex) return error_json(500, ex)
end end
return JSON.build do |json| JSON.build do |json|
json.object do json.object do
json.field "startTime", start_time json.field "startTime", start_time
json.field "endTime", end_time json.field "endTime", end_time
@ -513,6 +513,6 @@ module Invidious::Routes::API::V1::Videos
return error_json(500, ex) return error_json(500, ex)
end end
return transcript.to_json transcript.to_json
end end
end end

View File

@ -4,15 +4,15 @@ module Invidious::Routes::Channels
# Redirection for unsupported routes ("tabs") # Redirection for unsupported routes ("tabs")
def self.redirect_home(env) def self.redirect_home(env)
ucid = env.params.url["ucid"] ucid = env.params.url["ucid"]
return env.redirect "/channel/#{URI.encode_www_form(ucid)}" env.redirect "/channel/#{URI.encode_www_form(ucid)}"
end end
def self.home(env) def self.home(env)
self.videos(env) videos(env)
end end
def self.videos(env) def self.videos(env)
data = self.fetch_basic_information(env) data = fetch_basic_information(env)
return data if !data.is_a?(Tuple) return data if !data.is_a?(Tuple)
locale, user, subscriptions, continuation, ucid, channel = data locale, user, subscriptions, continuation, ucid, channel = data
@ -64,7 +64,7 @@ module Invidious::Routes::Channels
end end
def self.shorts(env) def self.shorts(env)
data = self.fetch_basic_information(env) data = fetch_basic_information(env)
return data if !data.is_a?(Tuple) return data if !data.is_a?(Tuple)
locale, user, subscriptions, continuation, ucid, channel = data locale, user, subscriptions, continuation, ucid, channel = data
@ -99,7 +99,7 @@ module Invidious::Routes::Channels
end end
def self.streams(env) def self.streams(env)
data = self.fetch_basic_information(env) data = fetch_basic_information(env)
return data if !data.is_a?(Tuple) return data if !data.is_a?(Tuple)
locale, user, subscriptions, continuation, ucid, channel = data locale, user, subscriptions, continuation, ucid, channel = data
@ -134,7 +134,7 @@ module Invidious::Routes::Channels
end end
def self.playlists(env) def self.playlists(env)
data = self.fetch_basic_information(env) data = fetch_basic_information(env)
return data if !data.is_a?(Tuple) return data if !data.is_a?(Tuple)
locale, user, subscriptions, continuation, ucid, channel = data locale, user, subscriptions, continuation, ucid, channel = data
@ -158,7 +158,7 @@ module Invidious::Routes::Channels
end end
def self.podcasts(env) def self.podcasts(env)
data = self.fetch_basic_information(env) data = fetch_basic_information(env)
return data if !data.is_a?(Tuple) return data if !data.is_a?(Tuple)
locale, user, subscriptions, continuation, ucid, channel = data locale, user, subscriptions, continuation, ucid, channel = data
@ -178,7 +178,7 @@ module Invidious::Routes::Channels
end end
def self.releases(env) def self.releases(env)
data = self.fetch_basic_information(env) data = fetch_basic_information(env)
return data if !data.is_a?(Tuple) return data if !data.is_a?(Tuple)
locale, user, subscriptions, continuation, ucid, channel = data locale, user, subscriptions, continuation, ucid, channel = data
@ -198,7 +198,7 @@ module Invidious::Routes::Channels
end end
def self.courses(env) def self.courses(env)
data = self.fetch_basic_information(env) data = fetch_basic_information(env)
return data if !data.is_a?(Tuple) return data if !data.is_a?(Tuple)
locale, user, subscriptions, continuation, ucid, channel = data locale, user, subscriptions, continuation, ucid, channel = data
@ -220,7 +220,7 @@ module Invidious::Routes::Channels
def self.community(env) def self.community(env)
return env.redirect env.request.path.sub("posts", "community") if env.request.path.split("/").last == "posts" return env.redirect env.request.path.sub("posts", "community") if env.request.path.split("/").last == "posts"
data = self.fetch_basic_information(env) data = fetch_basic_information(env)
if !data.is_a?(Tuple) if !data.is_a?(Tuple)
return data return data
end end
@ -298,7 +298,7 @@ module Invidious::Routes::Channels
end end
def self.channels(env) def self.channels(env)
data = self.fetch_basic_information(env) data = fetch_basic_information(env)
return data if !data.is_a?(Tuple) return data if !data.is_a?(Tuple)
locale, user, subscriptions, continuation, ucid, channel = data locale, user, subscriptions, continuation, ucid, channel = data
@ -318,7 +318,7 @@ module Invidious::Routes::Channels
end end
def self.about(env) def self.about(env)
data = self.fetch_basic_information(env) data = fetch_basic_information(env)
if !data.is_a?(Tuple) if !data.is_a?(Tuple)
return data return data
end end
@ -365,7 +365,7 @@ module Invidious::Routes::Channels
url += "?#{invidious_url_params}" if !invidious_url_params.empty? url += "?#{invidious_url_params}" if !invidious_url_params.empty?
return env.redirect url env.redirect url
end end
# Handles redirects for the /profile endpoint # Handles redirects for the /profile endpoint
@ -378,7 +378,7 @@ module Invidious::Routes::Channels
user = env.params.query["user"]? user = env.params.query["user"]?
if !user if !user
return error_template(404, "This channel does not exist.") error_template(404, "This channel does not exist.")
else else
env.redirect "/user/#{user}#{uri_params}" env.redirect "/user/#{user}#{uri_params}"
end end

View File

@ -9,7 +9,7 @@ module Invidious::Routes::Companion
begin begin
COMPANION_POOL.client do |wrapper| COMPANION_POOL.client do |wrapper|
wrapper.client.get(url, env.request.headers) do |resp| wrapper.client.get(url, env.request.headers) do |resp|
return self.proxy_companion(env, resp) return proxy_companion(env, resp)
end end
end end
rescue ex rescue ex
@ -26,7 +26,7 @@ module Invidious::Routes::Companion
begin begin
COMPANION_POOL.client do |wrapper| COMPANION_POOL.client do |wrapper|
wrapper.client.post(url, env.request.headers, env.request.body) do |resp| wrapper.client.post(url, env.request.headers, env.request.body) do |resp|
return self.proxy_companion(env, resp) return proxy_companion(env, resp)
end end
end end
rescue ex rescue ex
@ -42,7 +42,7 @@ module Invidious::Routes::Companion
begin begin
COMPANION_POOL.client do |wrapper| COMPANION_POOL.client do |wrapper|
wrapper.client.options(url, env.request.headers) do |resp| wrapper.client.options(url, env.request.headers) do |resp|
return self.proxy_companion(env, resp) return proxy_companion(env, resp)
end end
end end
rescue ex rescue ex
@ -55,6 +55,6 @@ module Invidious::Routes::Companion
env.response.headers[key] = value env.response.headers[key] = value
end end
return IO.copy response.body_io, env.response IO.copy response.body_io, env.response
end end
end end

View File

@ -119,7 +119,6 @@ module Invidious::Routes::Embed
end end
return env.redirect url return env.redirect url
else nil # Continue
end end
params = process_video_params(env.params.query, preferences) params = process_video_params(env.params.query, preferences)
@ -182,14 +181,14 @@ module Invidious::Routes::Embed
captions = video.captions captions = video.captions
preferred_captions = captions.select { |caption| preferred_captions = captions.select do |caption|
params.preferred_captions.includes?(caption.name) || params.preferred_captions.includes?(caption.name) ||
params.preferred_captions.includes?(caption.language_code.split("-")[0]) params.preferred_captions.includes?(caption.language_code.split("-")[0])
} end
preferred_captions.sort_by! { |caption| preferred_captions.sort_by! do |caption|
(params.preferred_captions.index(caption.name) || (params.preferred_captions.index(caption.name) ||
params.preferred_captions.index(caption.language_code.split("-")[0])).not_nil! params.preferred_captions.index(caption.language_code.split("-")[0])).not_nil!
} end
captions = captions - preferred_captions captions = captions - preferred_captions
aspect_ratio = nil aspect_ratio = nil

View File

@ -322,7 +322,6 @@ module Invidious::Routes::Feeds
request_target = URI.parse(node[attribute.name]).request_target request_target = URI.parse(node[attribute.name]).request_target
query_string_opt = request_target.starts_with?("/watch?v=") ? "&#{params}" : "" query_string_opt = request_target.starts_with?("/watch?v=") ? "&#{params}" : ""
node[attribute.name] = "#{HOST_URL}#{request_target}#{query_string_opt}" node[attribute.name] = "#{HOST_URL}#{request_target}#{query_string_opt}"
else nil # Skip
end end
end end
end end

View File

@ -13,7 +13,7 @@ module Invidious::Routes::Images
begin begin
GGPHT_POOL.client &.get(url, headers) do |resp| GGPHT_POOL.client &.get(url, headers) do |resp|
return self.proxy_image(env, resp) return proxy_image(env, resp)
end end
rescue ex rescue ex
end end
@ -44,7 +44,7 @@ module Invidious::Routes::Images
begin begin
get_ytimg_pool(authority).client &.get(url, headers) do |resp| get_ytimg_pool(authority).client &.get(url, headers) do |resp|
env.response.headers["Connection"] = "close" env.response.headers["Connection"] = "close"
return self.proxy_image(env, resp) return proxy_image(env, resp)
end end
rescue ex rescue ex
end end
@ -66,7 +66,7 @@ module Invidious::Routes::Images
begin begin
get_ytimg_pool("i9").client &.get(url, headers) do |resp| get_ytimg_pool("i9").client &.get(url, headers) do |resp|
return self.proxy_image(env, resp) return proxy_image(env, resp)
end end
rescue ex rescue ex
end end
@ -128,7 +128,7 @@ module Invidious::Routes::Images
begin begin
get_ytimg_pool("i").client &.get(url, headers) do |resp| get_ytimg_pool("i").client &.get(url, headers) do |resp|
return self.proxy_image(env, resp) return proxy_image(env, resp)
end end
rescue ex rescue ex
end end
@ -148,6 +148,6 @@ module Invidious::Routes::Images
return env.response.headers.delete("Transfer-Encoding") return env.response.headers.delete("Transfer-Encoding")
end end
return proxy_file(response, env) proxy_file(response, env)
end end
end end

View File

@ -347,7 +347,6 @@ module Invidious::Routes::PreferencesRoute
response: error_template(415, "Uploaded file is too large") response: error_template(415, "Uploaded file is too large")
) )
end end
else nil # Ignore
end end
end end
end end

View File

@ -241,7 +241,7 @@ module Invidious::Routes::VideoPlayback
query_params = HTTP::Params.new(raw_params) query_params = HTTP::Params.new(raw_params)
env.response.headers["Access-Control-Allow-Origin"] = "*" env.response.headers["Access-Control-Allow-Origin"] = "*"
return env.redirect "/videoplayback?#{query_params}" env.redirect "/videoplayback?#{query_params}"
end end
# /videoplayback/* && /videoplayback/* # /videoplayback/* && /videoplayback/*
@ -307,6 +307,6 @@ module Invidious::Routes::VideoPlayback
url += "&title=#{URI.encode_www_form(title, space_to_plus: false)}" if title url += "&title=#{URI.encode_www_form(title, space_to_plus: false)}" if title
end end
return env.redirect url env.redirect url
end end
end end

View File

@ -145,14 +145,14 @@ module Invidious::Routes::Watch
captions = video.captions captions = video.captions
preferred_captions = captions.select { |caption| preferred_captions = captions.select do |caption|
params.preferred_captions.includes?(caption.name) || params.preferred_captions.includes?(caption.name) ||
params.preferred_captions.includes?(caption.language_code.split("-")[0]) params.preferred_captions.includes?(caption.language_code.split("-")[0])
} end
preferred_captions.sort_by! { |caption| preferred_captions.sort_by! do |caption|
(params.preferred_captions.index(caption.name) || (params.preferred_captions.index(caption.name) ||
params.preferred_captions.index(caption.language_code.split("-")[0])).not_nil! params.preferred_captions.index(caption.language_code.split("-")[0])).not_nil!
} end
captions = captions - preferred_captions captions = captions - preferred_captions
aspect_ratio = "16:9" aspect_ratio = "16:9"
@ -215,7 +215,7 @@ module Invidious::Routes::Watch
url += "&#{env.params.query}" url += "&#{env.params.query}"
end end
return env.redirect url env.redirect url
end end
def self.mark_watched(env) def self.mark_watched(env)
@ -289,9 +289,9 @@ module Invidious::Routes::Watch
env.params.query["end"] = end_time.to_s if end_time != nil env.params.query["end"] = end_time.to_s if end_time != nil
end end
return env.redirect "/watch?v=#{video_id}&#{env.params.query}" env.redirect "/watch?v=#{video_id}&#{env.params.query}"
else else
return error_template(404, "The requested clip doesn't exist") error_template(404, "The requested clip doesn't exist")
end end
end end
@ -330,16 +330,16 @@ module Invidious::Routes::Watch
env.params.query["title"] = filename env.params.query["title"] = filename
env.params.query["label"] = URI.decode_www_form(label.as_s) env.params.query["label"] = URI.decode_www_form(label.as_s)
return Invidious::Routes::API::V1::Videos.captions(env) Invidious::Routes::API::V1::Videos.captions(env)
elsif itag = download_widget["itag"]?.try &.as_i.to_s elsif itag = download_widget["itag"]?.try &.as_i.to_s
# URL params specific to /latest_version # URL params specific to /latest_version
env.params.query["id"] = video_id env.params.query["id"] = video_id
env.params.query["title"] = filename env.params.query["title"] = filename
env.params.query["local"] = "true" env.params.query["local"] = "true"
return Invidious::Routes::VideoPlayback.latest_version(env) Invidious::Routes::VideoPlayback.latest_version(env)
else else
return error_template(400, "Invalid label or itag") error_template(400, "Invalid label or itag")
end end
end end
end end

View File

@ -42,11 +42,11 @@ module Invidious::Routing
end end
{% end %} {% end %}
self.register_image_routes register_image_routes
self.register_api_v1_routes register_api_v1_routes
self.register_api_manifest_routes register_api_manifest_routes
self.register_video_playback_routes register_video_playback_routes
self.register_companion_routes register_companion_routes
end end
# ------------------- # -------------------

View File

@ -28,5 +28,5 @@ def produce_channel_search_continuation(ucid, query, page)
.try { |i| Base64.urlsafe_encode(i) } .try { |i| Base64.urlsafe_encode(i) }
.try { |i| URI.encode_www_form(i) } .try { |i| URI.encode_www_form(i) }
return continuation continuation
end end

View File

@ -80,7 +80,7 @@ module Invidious::Search
end end
def default? : Bool def default? : Bool
return @date.none? && @type.all? && @duration.none? && \ @date.none? && @type.all? && @duration.none? && \
@features.none? && @sort.relevance? @features.none? && @sort.relevance?
end end
@ -110,7 +110,7 @@ module Invidious::Search
end end
end end
return features features
end end
def self.format_features(features : Features) : String def self.format_features(features : Features) : String
@ -132,7 +132,7 @@ module Invidious::Search
str << "location" if features.location? str << "location" if features.location?
str << "purchased" if features.purchased? str << "purchased" if features.purchased?
return str.join(',') str.join(',')
end end
def self.from_legacy_filters(str : String) : {Filters, String, String, Bool} def self.from_legacy_filters(str : String) : {Filters, String, String, Bool}
@ -230,7 +230,7 @@ module Invidious::Search
params.delete("sort") params.delete("sort")
end end
return filters filters
end end
def to_iv_params : HTTP::Params def to_iv_params : HTTP::Params
@ -249,7 +249,7 @@ module Invidious::Search
raw_params["features"] = [Filters.format_features(@features)] raw_params["features"] = [Filters.format_features(@features)]
end end
return HTTP::Params.new(raw_params) HTTP::Params.new(raw_params)
end end
# ------------------- # -------------------
@ -304,7 +304,7 @@ module Invidious::Search
# See https://github.com/iv-org/invidious/issues/4398 # See https://github.com/iv-org/invidious/issues/4398
object["30:varint"] = 1.to_i64 object["30:varint"] = 1.to_i64
return object object
.try { |i| Protodec::Any.cast_json(i) } .try { |i| Protodec::Any.cast_json(i) }
.try { |i| Protodec::Any.from_json(i) } .try { |i| Protodec::Any.from_json(i) }
.try { |i| Base64.urlsafe_encode(i) } .try { |i| Base64.urlsafe_encode(i) }
@ -370,7 +370,7 @@ module Invidious::Search
# Remove URL parameter and return result # Remove URL parameter and return result
params.delete("sp") params.delete("sp")
return filters filters
end end
end end
end end

View File

@ -10,7 +10,7 @@ module Invidious::Search
initial_data = YoutubeAPI.search(query.text, search_params, client_config: client_config) initial_data = YoutubeAPI.search(query.text, search_params, client_config: client_config)
items, _ = extract_items(initial_data) items, _ = extract_items(initial_data)
return items.reject!(Category) items.reject!(Category)
end end
# Search a youtube channel # Search a youtube channel
@ -32,14 +32,14 @@ module Invidious::Search
response_json = YoutubeAPI.browse(continuation) response_json = YoutubeAPI.browse(continuation)
items, _ = extract_items(response_json, "", ucid) items, _ = extract_items(response_json, "", ucid)
return items.reject!(Category) items.reject!(Category)
end end
# Search inside of user subscriptions # Search inside of user subscriptions
def subscriptions(query : Query, user : Invidious::User) : Array(ChannelVideo) def subscriptions(query : Query, user : Invidious::User) : Array(ChannelVideo)
view_name = "subscriptions_#{sha256(user.email)}" view_name = "subscriptions_#{sha256(user.email)}"
return PG_DB.query_all(" PG_DB.query_all(<<-SQL, query.text, (query.page - 1) * 20, as: ChannelVideo)
SELECT id,title,published,updated,ucid,author,length_seconds SELECT id,title,published,updated,ucid,author,length_seconds
FROM ( FROM (
SELECT *, SELECT *,
@ -47,10 +47,8 @@ module Invidious::Search
to_tsvector(#{view_name}.author) to_tsvector(#{view_name}.author)
as document as document
FROM #{view_name} FROM #{view_name}
) v_search WHERE v_search.document @@ plainto_tsquery($1) LIMIT 20 OFFSET $2;", ) v_search WHERE v_search.document @@ plainto_tsquery($1) LIMIT 20 OFFSET $2;
query.text, (query.page - 1) * 20, SQL
as: ChannelVideo
)
end end
end end
end end

View File

@ -25,19 +25,19 @@ module Invidious::Search
# Return true if @raw_query is either `nil` or empty # Return true if @raw_query is either `nil` or empty
private def empty_raw_query? private def empty_raw_query?
return @raw_query.empty? @raw_query.empty?
end end
# Same as `empty_raw_query?`, but named for external use # Same as `empty_raw_query?`, but named for external use
def empty? def empty?
return self.empty_raw_query? empty_raw_query?
end end
# Getter for the query string. # Getter for the query string.
# It is named `text` to reduce confusion (`search_query.text` makes more # It is named `text` to reduce confusion (`search_query.text` makes more
# sense than `search_query.query`) # sense than `search_query.query`)
def text def text
return @query @query
end end
# Initialize a new search query. # Initialize a new search query.
@ -70,7 +70,7 @@ module Invidious::Search
# Stop here if raw query is empty # Stop here if raw query is empty
# NOTE: maybe raise in the future? # NOTE: maybe raise in the future?
return if self.empty_raw_query? return if empty_raw_query?
# Specific handling # Specific handling
case @type case @type
@ -120,7 +120,7 @@ module Invidious::Search
items = [] of SearchItem items = [] of SearchItem
# Don't bother going further if search query is empty # Don't bother going further if search query is empty
return items if self.empty_raw_query? return items if empty_raw_query?
case @type case @type
when .regular?, .playlist? when .regular?, .playlist?
@ -135,7 +135,7 @@ module Invidious::Search
end end
end end
return items items
end end
# Return the HTTP::Params corresponding to this Query (invidious format) # Return the HTTP::Params corresponding to this Query (invidious format)
@ -145,7 +145,7 @@ module Invidious::Search
params["q"] = @query params["q"] = @query
params["channel"] = @channel if !@channel.empty? params["channel"] = @channel if !@channel.empty?
return params params
end end
# Checks if the query is a standalone URL # Checks if the query is a standalone URL
@ -160,7 +160,7 @@ module Invidious::Search
return false if !@filters.default? return false if !@filters.default?
# Simple heuristics: domain name # Simple heuristics: domain name
return @raw_query.starts_with?( @raw_query.starts_with?(
/(https?:\/\/)?(www\.)?(m\.)?youtu(\.be|be\.com)\// /(https?:\/\/)?(www\.)?(m\.)?youtu(\.be|be\.com)\//
) )
end end

View File

@ -35,7 +35,7 @@ def fetch_trending(trending_type, region, locale)
# Ignore the smaller categories, as they generally contain a sponsored # Ignore the smaller categories, as they generally contain a sponsored
# channel, which brings a lot of noise on the trending page. # channel, which brings a lot of noise on the trending page.
# See: https://github.com/iv-org/invidious/issues/2989 # See: https://github.com/iv-org/invidious/issues/2989
next if (itm.contents.size < 24 && deduplicate) next if itm.contents.size < 24 && deduplicate
extracted.concat itm.contents.select(SearchItem) extracted.concat itm.contents.select(SearchItem)
else else

View File

@ -19,7 +19,7 @@ struct Invidious::User
hour = 12 hour = 12
end end
clock_svg = <<-END_SVG clock_svg = <<-SVG
<svg viewBox="0 0 100 100" width="200px" height="200px"> <svg viewBox="0 0 100 100" width="200px" height="200px">
<circle cx="50" cy="50" r="45" fill="#eee" stroke="black" stroke-width="2"></circle> <circle cx="50" cy="50" r="45" fill="#eee" stroke="black" stroke-width="2"></circle>
@ -41,7 +41,7 @@ struct Invidious::User
<line id="minute" transform="rotate(#{minute_angle}, 50, 50)" x1="50" y1="50" x2="50" y2="16" fill="black" stroke="black" stroke-width="2"></line> <line id="minute" transform="rotate(#{minute_angle}, 50, 50)" x1="50" y1="50" x2="50" y2="16" fill="black" stroke="black" stroke-width="2"></line>
<line id="hour" transform="rotate(#{hour_angle}, 50, 50)" x1="50" y1="50" x2="50" y2="24" fill="black" stroke="black" stroke-width="2"></line> <line id="hour" transform="rotate(#{hour_angle}, 50, 50)" x1="50" y1="50" x2="50" y2="24" fill="black" stroke="black" stroke-width="2"></line>
</svg> </svg>
END_SVG SVG
image = "data:image/png;base64," image = "data:image/png;base64,"
image += Process.run(%(rsvg-convert -w 400 -h 400 -b none -f png), shell: true, image += Process.run(%(rsvg-convert -w 400 -h 400 -b none -f png), shell: true,
@ -53,7 +53,7 @@ struct Invidious::User
answer = "#{hour}:#{minute.to_s.rjust(2, '0')}:#{second.to_s.rjust(2, '0')}" answer = "#{hour}:#{minute.to_s.rjust(2, '0')}:#{second.to_s.rjust(2, '0')}"
answer = OpenSSL::HMAC.hexdigest(:sha256, key, answer) answer = OpenSSL::HMAC.hexdigest(:sha256, key, answer)
return { {
question: image, question: image,
tokens: {generate_response(answer, {":login"}, key, use_nonce: true)}, tokens: {generate_response(answer, {":login"}, key, use_nonce: true)},
} }

View File

@ -11,7 +11,7 @@ struct Invidious::User
# Session ID (SID) cookie # Session ID (SID) cookie
# Parameter "domain" comes from the global config # Parameter "domain" comes from the global config
def sid(domain : String?, sid) : HTTP::Cookie def sid(domain : String?, sid) : HTTP::Cookie
return HTTP::Cookie.new( HTTP::Cookie.new(
name: "SID", name: "SID",
domain: domain, domain: domain,
value: sid, value: sid,
@ -25,7 +25,7 @@ struct Invidious::User
# Preferences (PREFS) cookie # Preferences (PREFS) cookie
# Parameter "domain" comes from the global config # Parameter "domain" comes from the global config
def prefs(domain : String?, preferences : Preferences) : HTTP::Cookie def prefs(domain : String?, preferences : Preferences) : HTTP::Cookie
return HTTP::Cookie.new( HTTP::Cookie.new(
name: "PREFS", name: "PREFS",
domain: domain, domain: domain,
value: URI.encode_www_form(preferences.to_json), value: URI.encode_www_form(preferences.to_json),

View File

@ -5,7 +5,7 @@ struct Invidious::User
def to_invidious(user : User) def to_invidious(user : User)
playlists = Invidious::Database::Playlists.select_like_iv(user.email) playlists = Invidious::Database::Playlists.select_like_iv(user.email)
return JSON.build do |json| JSON.build do |json|
json.object do json.object do
json.field "subscriptions", user.subscriptions json.field "subscriptions", user.subscriptions
json.field "watch_history", user.watched json.field "watch_history", user.watched

View File

@ -27,7 +27,7 @@ struct Invidious::User
subscriptions << channel_id subscriptions << channel_id
end end
return subscriptions subscriptions
end end
def parse_playlist_export_csv(user : User, raw_input : String) def parse_playlist_export_csv(user : User, raw_input : String)
@ -81,7 +81,7 @@ struct Invidious::User
end end
end end
return playlist playlist
end end
# ------------------- # -------------------
@ -171,7 +171,7 @@ struct Invidious::User
opml_extensions = ["xml", "opml"] opml_extensions = ["xml", "opml"]
return opml_mimetypes.any?(&.== mimetype) || opml_extensions.any?(&.== extension) opml_mimetypes.any?(&.== mimetype) || opml_extensions.any?(&.== extension)
end end
# Import subscribed channels from Youtube # Import subscribed channels from Youtube
@ -200,7 +200,7 @@ struct Invidious::User
user.subscriptions = get_batch_channels(user.subscriptions) user.subscriptions = get_batch_channels(user.subscriptions)
Invidious::Database::Users.update_subscriptions(user) Invidious::Database::Users.update_subscriptions(user)
return true true
end end
def from_youtube_pl(user : User, body : String, filename : String, type : String) : Bool def from_youtube_pl(user : User, body : String, filename : String, type : String) : Bool
@ -209,12 +209,12 @@ struct Invidious::User
if extension == "csv" || type == "text/csv" if extension == "csv" || type == "text/csv"
playlist = parse_playlist_export_csv(user, body) playlist = parse_playlist_export_csv(user, body)
if playlist if playlist
return true true
else else
return false false
end end
else else
return false false
end end
end end
@ -232,9 +232,9 @@ struct Invidious::User
user.watched += watched user.watched += watched
user.watched.uniq! user.watched.uniq!
Invidious::Database::Users.update_watch_history(user) Invidious::Database::Users.update_watch_history(user)
return true true
else else
return false false
end end
end end
@ -328,7 +328,7 @@ struct Invidious::User
end end
# Success! # Success!
return true true
end end
end # module end # module
end end

View File

@ -64,7 +64,6 @@ struct Preferences
end end
def self.from_json(value : JSON::PullParser) : String def self.from_json(value : JSON::PullParser) : String
begin
result = value.read_string result = value.read_string
if result.empty? if result.empty?
@ -79,7 +78,6 @@ struct Preferences
"light" "light"
end end
end end
end
def self.to_yaml(value : String, yaml : YAML::Nodes::Builder) def self.to_yaml(value : String, yaml : YAML::Nodes::Builder)
yaml.scalar value yaml.scalar value
@ -262,12 +260,12 @@ struct Preferences
module TimeSpanConverter module TimeSpanConverter
def self.to_yaml(value : Time::Span, yaml : YAML::Nodes::Builder) def self.to_yaml(value : Time::Span, yaml : YAML::Nodes::Builder)
return yaml.scalar value.total_minutes.to_i32 yaml.scalar value.total_minutes.to_i32
end end
def self.from_yaml(ctx : YAML::ParseContext, node : YAML::Nodes::Node) : Time::Span def self.from_yaml(ctx : YAML::ParseContext, node : YAML::Nodes::Node) : Time::Span
if node.is_a?(YAML::Nodes::Scalar) if node.is_a?(YAML::Nodes::Scalar)
return decode_interval(node.value) decode_interval(node.value)
else else
node.raise "Expected scalar, not #{node.class}" node.raise "Expected scalar, not #{node.class}"
end end

View File

@ -17,11 +17,9 @@ struct Invidious::User
module PreferencesConverter module PreferencesConverter
def self.from_rs(rs) def self.from_rs(rs)
begin
Preferences.from_json(rs.read(String)) Preferences.from_json(rs.read(String))
rescue ex rescue ex
Preferences.from_json("{}") Preferences.from_json("{}")
end end
end end
end end
end

View File

@ -45,7 +45,6 @@ def get_subscription_feed(user, max_results = 40, page = 1)
notifications.sort_by!(&.author) notifications.sort_by!(&.author)
when "channel name - reverse" when "channel name - reverse"
notifications.sort_by!(&.author).reverse! notifications.sort_by!(&.author).reverse!
else nil # Ignore
end end
else else
if user.preferences.latest_only if user.preferences.latest_only
@ -94,7 +93,6 @@ def get_subscription_feed(user, max_results = 40, page = 1)
videos.sort_by!(&.author) videos.sort_by!(&.author)
when "channel name - reverse" when "channel name - reverse"
videos.sort_by!(&.author).reverse! videos.sort_by!(&.author).reverse!
else nil # Ignore
end end
notifications = Invidious::Database::Users.select_notifications(user) notifications = Invidious::Database::Users.select_notifications(user)

View File

@ -48,7 +48,7 @@ struct Video
end end
end end
def to_json(json : JSON::Builder | Nil = nil) def to_json(json : JSON::Builder? = nil)
to_json(nil, json) to_json(nil, json)
end end
@ -56,15 +56,15 @@ struct Video
def video_type : VideoType def video_type : VideoType
video_type = info["videoType"]?.try &.as_s || "video" video_type = info["videoType"]?.try &.as_s || "video"
return VideoType.parse?(video_type) || VideoType::Video VideoType.parse?(video_type) || VideoType::Video
end end
def schema_version : Int def schema_version : Int
return info["version"]?.try &.as_i || 1 info["version"]?.try &.as_i || 1
end end
def published : Time def published : Time
return info["published"]? info["published"]?
.try { |t| Time.parse(t.as_s, "%Y-%m-%d", Time::Location::UTC) } || Time.utc .try { |t| Time.parse(t.as_s, "%Y-%m-%d", Time::Location::UTC) } || Time.utc
end end
@ -73,11 +73,11 @@ struct Video
end end
def live_now def live_now
return (self.video_type == VideoType::Livestream) (video_type == VideoType::Livestream)
end end
def post_live_dvr def post_live_dvr
return info["isPostLiveDvr"].as_bool info["isPostLiveDvr"].as_bool
end end
def premiere_timestamp : Time? def premiere_timestamp : Time?
@ -94,21 +94,21 @@ struct Video
def fmt_stream : Array(Hash(String, JSON::Any)) def fmt_stream : Array(Hash(String, JSON::Any))
if formats = info.dig?("streamingData", "formats") if formats = info.dig?("streamingData", "formats")
return formats formats
.as_a.map(&.as_h) .as_a.map(&.as_h)
.sort_by! { |f| f["width"]?.try &.as_i || 0 } .sort_by! { |f| f["width"]?.try &.as_i || 0 }
else else
return [] of Hash(String, JSON::Any) [] of Hash(String, JSON::Any)
end end
end end
def adaptive_fmts : Array(Hash(String, JSON::Any)) def adaptive_fmts : Array(Hash(String, JSON::Any))
if formats = info.dig?("streamingData", "adaptiveFormats") if formats = info.dig?("streamingData", "adaptiveFormats")
return formats formats
.as_a.map(&.as_h) .as_a.map(&.as_h)
.sort_by! { |f| f["width"]?.try &.as_i || f["audioTrack"]?.try { |a| a["audioIsDefault"]?.try { |v| v.as_bool ? -1 : 0 } } || 0 } .sort_by! { |f| f["width"]?.try &.as_i || f["audioTrack"]?.try { |a| a["audioIsDefault"]?.try { |v| v.as_bool ? -1 : 0 } } || 0 }
else else
return [] of Hash(String, JSON::Any) [] of Hash(String, JSON::Any)
end end
end end
@ -124,11 +124,11 @@ struct Video
def storyboards def storyboards
container = info.dig?("storyboards") || JSON::Any.new("{}") container = info.dig?("storyboards") || JSON::Any.new("{}")
return IV::Videos::Storyboard.from_yt_json(container, self.length_seconds) IV::Videos::Storyboard.from_yt_json(container, length_seconds)
end end
def paid def paid
return (self.reason || "").includes? "requires payment" (reason || "").includes? "requires payment"
end end
def premium def premium
@ -140,7 +140,7 @@ struct Video
@captions = Invidious::Videos::Captions::Metadata.from_yt_json(info["captions"]) @captions = Invidious::Videos::Captions::Metadata.from_yt_json(info["captions"])
end end
return @captions @captions
end end
def hls_manifest_url : String? def hls_manifest_url : String?
@ -149,7 +149,7 @@ struct Video
def dash_manifest_url : String? def dash_manifest_url : String?
raw_dash_url = info.dig?("streamingData", "dashManifestUrl").try &.as_s raw_dash_url = info.dig?("streamingData", "dashManifestUrl").try &.as_s
return nil if raw_dash_url.nil? return if raw_dash_url.nil?
# Use manifest v5 parameter to reduce file size # Use manifest v5 parameter to reduce file size
# See https://github.com/iv-org/invidious/issues/4186 # See https://github.com/iv-org/invidious/issues/4186
@ -162,7 +162,7 @@ struct Video
dash_url.query = "#{dash_query}&mpd_version=5" dash_url.query = "#{dash_query}&mpd_version=5"
end end
return dash_url.to_s dash_url.to_s
end end
def genre_url : String? def genre_url : String?
@ -170,11 +170,11 @@ struct Video
end end
def vr? : Bool? def vr? : Bool?
return {"EQUIRECTANGULAR", "MESH"}.includes? self.projection_type {"EQUIRECTANGULAR", "MESH"}.includes? projection_type
end end
def projection_type : String? def projection_type : String?
return info.dig?("streamingData", "adaptiveFormats", 0, "projectionType").try &.as_s info.dig?("streamingData", "adaptiveFormats", 0, "projectionType").try &.as_s
end end
def reason : String? def reason : String?
@ -182,14 +182,14 @@ struct Video
end end
def music : Array(VideoMusic) def music : Array(VideoMusic)
info["music"].as_a.map { |music_json| info["music"].as_a.map do |music_json|
VideoMusic.new( VideoMusic.new(
music_json["song"].as_s, music_json["song"].as_s,
music_json["album"].as_s, music_json["album"].as_s,
music_json["artist"].as_s, music_json["artist"].as_s,
music_json["license"].as_s music_json["license"].as_s
) )
} end
end end
# Macros defining getters/setters for various types of data # Macros defining getters/setters for various types of data
@ -197,7 +197,7 @@ struct Video
private macro getset_string(name) private macro getset_string(name)
# Return {{ name.stringify }} from `info` # Return {{ name.stringify }} from `info`
def {{ name.id.underscore }} : String def {{ name.id.underscore }} : String
return info[{{name.stringify}}]?.try &.as_s || "" info[{{ name.stringify }}]?.try &.as_s || ""
end end
# Update {{ name.stringify }} into `info` # Update {{ name.stringify }} into `info`
@ -211,7 +211,7 @@ struct Video
private macro getset_string_array(name) private macro getset_string_array(name)
# Return {{ name.stringify }} from `info` # Return {{ name.stringify }} from `info`
def {{ name.id.underscore }} : Array(String) def {{ name.id.underscore }} : Array(String)
return info[{{name.stringify}}]?.try &.as_a.map &.as_s || [] of String info[{{ name.stringify }}]?.try &.as_a.map &.as_s || [] of String
end end
# Update {{ name.stringify }} into `info` # Update {{ name.stringify }} into `info`
@ -225,7 +225,7 @@ struct Video
{% for op, type in {i32: Int32, i64: Int64} %} {% for op, type in {i32: Int32, i64: Int64} %}
private macro getset_{{ op }}(name) private macro getset_{{ op }}(name)
def \{{name.id.underscore}} : {{ type }} def \{{name.id.underscore}} : {{ type }}
return info[\{{name.stringify}}]?.try &.as_i64.to_{{op}} || 0_{{op}} info[\{{name.stringify}}]?.try &.as_i64.to_{{ op }} || 0_{{ op }}
end end
def \{{name.id.underscore}}=(value : Int) def \{{name.id.underscore}}=(value : Int)
@ -239,7 +239,7 @@ struct Video
private macro getset_bool(name) private macro getset_bool(name)
# Return {{ name.stringify }} from `info` # Return {{ name.stringify }} from `info`
def {{ name.id.underscore }} : Bool def {{ name.id.underscore }} : Bool
return info[{{name.stringify}}]?.try &.as_bool || false info[{{ name.stringify }}]?.try &.as_bool || false
end end
# Update {{ name.stringify }} into `info` # Update {{ name.stringify }} into `info`
@ -254,7 +254,7 @@ struct Video
private macro predicate_bool(method_name, name) private macro predicate_bool(method_name, name)
# Return {{ name.stringify }} from `info` # Return {{ name.stringify }} from `info`
def {{ method_name.id.underscore }}? : Bool def {{ method_name.id.underscore }}? : Bool
return info[{{name.stringify}}]?.try &.as_bool || false info[{{ name.stringify }}]?.try &.as_bool || false
end end
# Update {{ name.stringify }} into `info` # Update {{ name.stringify }} into `info`
@ -316,11 +316,11 @@ def get_video(id, refresh = true, region = nil, force_refresh = false)
Invidious::Database::Videos.insert(video) if !region Invidious::Database::Videos.insert(video) if !region
end end
return video video
rescue DB::Error rescue DB::Error
# Avoid common `DB::PoolRetryAttemptsExceeded` error and friends # Avoid common `DB::PoolRetryAttemptsExceeded` error and friends
# Note: All DB errors inherit from `DB::Error` # Note: All DB errors inherit from `DB::Error`
return fetch_video(id, region) fetch_video(id, region)
end end
def fetch_video(id, region) def fetch_video(id, region)
@ -350,7 +350,7 @@ def fetch_video(id, region)
updated: Time.utc, updated: Time.utc,
}) })
return video video
end end
def process_continuation(query, plid, id) def process_continuation(query, plid, id)

View File

@ -33,7 +33,7 @@ module Invidious::Videos
captions_list << Captions::Metadata.new(name, language_code, base_url, auto_generated) captions_list << Captions::Metadata.new(name, language_code, base_url, auto_generated)
end end
return captions_list captions_list
end end
def timedtext_to_vtt(timedtext : String, tlang = nil) : String def timedtext_to_vtt(timedtext : String, tlang = nil) : String
@ -82,7 +82,7 @@ module Invidious::Videos
end end
end end
return result result
end end
end end

View File

@ -26,7 +26,7 @@ private def copy_string(str : String::Builder, iter : Iterator, count : Int) : I
copied += 1 copied += 1
end end
return copied copied
end end
def parse_description(desc, video_id : String) : String? def parse_description(desc, video_id : String) : String?
@ -52,7 +52,7 @@ def parse_description(desc, video_id : String) : String?
index = 0 index = 0
return String.build do |str| String.build do |str|
commands.each do |command| commands.each do |command|
cmd_start = command["startIndex"].as_i cmd_start = command["startIndex"].as_i
cmd_length = command["length"].as_i cmd_length = command["length"].as_i

View File

@ -1,6 +1,6 @@
module Invidious::Videos::Formats module Invidious::Videos::Formats
def self.itag_to_metadata?(itag : JSON::Any) def self.itag_to_metadata?(itag : JSON::Any)
return FORMATS[itag.to_s]? FORMATS[itag.to_s]?
end end
# See https://github.com/rg3/youtube-dl/blob/master/youtube_dl/extractor/youtube.py#L380-#L476 # See https://github.com/rg3/youtube-dl/blob/master/youtube_dl/extractor/youtube.py#L380-#L476

View File

@ -7,7 +7,7 @@ require "json"
# TODO: "compactRadioRenderer" (Mix) and # TODO: "compactRadioRenderer" (Mix) and
# TODO: Use a proper struct/class instead of a hacky JSON object # TODO: Use a proper struct/class instead of a hacky JSON object
def parse_related_video(related : JSON::Any) : Hash(String, JSON::Any)? def parse_related_video(related : JSON::Any) : Hash(String, JSON::Any)?
return nil if !related["videoId"]? return if !related["videoId"]?
# The compact renderer has video length in seconds, where the end # The compact renderer has video length in seconds, where the end
# screen rendered has a full text version ("42:40") # screen rendered has a full text version ("42:40")
@ -40,7 +40,7 @@ def parse_related_video(related : JSON::Any) : Hash(String, JSON::Any)?
# TODO: when refactoring video types, make a struct for related videos # TODO: when refactoring video types, make a struct for related videos
# or reuse an existing type, if that fits. # or reuse an existing type, if that fits.
return { {
"id" => related["videoId"], "id" => related["videoId"],
"title" => related["title"]["simpleText"], "title" => related["title"]["simpleText"],
"author" => author || JSON::Any.new(""), "author" => author || JSON::Any.new(""),
@ -57,7 +57,7 @@ def extract_video_info(video_id : String)
player_response = YoutubeAPI.player(video_id: video_id) player_response = YoutubeAPI.player(video_id: video_id)
if player_response.nil? if player_response.nil?
return nil return
end end
playability_status = player_response.dig?("playabilityStatus", "status").try &.as_s playability_status = player_response.dig?("playabilityStatus", "status").try &.as_s
@ -128,7 +128,7 @@ def extract_video_info(video_id : String)
# Data structure version, for cache control # Data structure version, for cache control
params["version"] = JSON::Any.new(Video::SCHEMA_VERSION.to_i64) params["version"] = JSON::Any.new(Video::SCHEMA_VERSION.to_i64)
return params params
end end
def try_fetch_streaming_data(id : String, client_config : YoutubeAPI::ClientConfig) : Hash(String, JSON::Any)? def try_fetch_streaming_data(id : String, client_config : YoutubeAPI::ClientConfig) : Hash(String, JSON::Any)?
@ -145,9 +145,9 @@ def try_fetch_streaming_data(id : String, client_config : YoutubeAPI::ClientConf
"The video returned by YouTube isn't the requested one. (#{client_config.client_type} client)" "The video returned by YouTube isn't the requested one. (#{client_config.client_type} client)"
) )
elsif playability_status == "OK" elsif playability_status == "OK"
return response response
else else
return nil return
end end
end end
@ -440,7 +440,7 @@ def parse_video_info(video_id : String, player_response : Hash(String, JSON::Any
"subCountText" => JSON::Any.new(subs_text || "-"), "subCountText" => JSON::Any.new(subs_text || "-"),
} }
return params params
end end
private def convert_url(fmt) private def convert_url(fmt)
@ -457,9 +457,9 @@ private def convert_url(fmt)
url.query_params = params url.query_params = params
LOGGER.trace("convert_url: new url is '#{url}'") LOGGER.trace("convert_url: new url is '#{url}'")
return url.to_s url.to_s
rescue ex rescue ex
LOGGER.debug("convert_url: Error when parsing video URL") LOGGER.debug("convert_url: Error when parsing video URL")
LOGGER.trace(ex.inspect_with_backtrace) LOGGER.trace(ex.inspect_with_backtrace)
return "" ""
end end

View File

@ -62,7 +62,7 @@ module Invidious::Videos
# The base URL is the first chunk # The base URL is the first chunk
base_url = URI.parse(storyboards.shift) base_url = URI.parse(storyboards.shift)
return storyboards.map_with_index do |sb, i| storyboards.map_with_index do |sb, i|
# Separate the different storyboard parameters: # Separate the different storyboard parameters:
# width/height: respective dimensions, in pixels, of a single thumbnail # width/height: respective dimensions, in pixels, of a single thumbnail
# count: how many thumbnails are displayed across the full video # count: how many thumbnails are displayed across the full video

View File

@ -45,7 +45,7 @@ module Invidious::Videos
.try { |i| Base64.urlsafe_encode(i) } .try { |i| Base64.urlsafe_encode(i) }
.try { |i| URI.encode_www_form(i) } .try { |i| URI.encode_www_form(i) }
return params params
end end
# Constructs a Transcripts struct from the initial YouTube response # Constructs a Transcripts struct from the initial YouTube response
@ -92,7 +92,7 @@ module Invidious::Videos
lines << line_type.new(start_ms, end_ms, text) lines << line_type.new(start_ms, end_ms, text)
end end
return Transcript.new( Transcript.new(
lines: lines, lines: lines,
language_code: language_code, language_code: language_code,
auto_generated: auto_generated, auto_generated: auto_generated,
@ -120,7 +120,7 @@ module Invidious::Videos
end end
end end
return vtt vtt
end end
def to_json(json : JSON::Builder) def to_json(json : JSON::Builder)

View File

@ -158,5 +158,5 @@ def process_video_params(query, preferences)
save_player_pos: save_player_pos, save_player_pos: save_player_pos,
}) })
return params params
end end

View File

@ -16,7 +16,7 @@
best_m4a_stream_bitrate = 0 best_m4a_stream_bitrate = 0
audio_streams.each_with_index do |fmt, i| audio_streams.each_with_index do |fmt, i|
bandwidth = fmt["bitrate"].as_i bandwidth = fmt["bitrate"].as_i
if (fmt["mimeType"].as_s.starts_with?("audio/mp4") && bandwidth > best_m4a_stream_bitrate) if fmt["mimeType"].as_s.starts_with?("audio/mp4") && bandwidth > best_m4a_stream_bitrate
best_m4a_stream_bitrate = bandwidth best_m4a_stream_bitrate = bandwidth
best_m4a_stream_index = i best_m4a_stream_index = i
end end
@ -26,7 +26,7 @@
src_url = "/latest_version?id=#{video.id}&itag=#{fmt["itag"]}" src_url = "/latest_version?id=#{video.id}&itag=#{fmt["itag"]}"
src_url += "&local=true" if params.local src_url += "&local=true" if params.local
src_url = invidious_companion.public_url.to_s + src_url + src_url = invidious_companion.public_url.to_s + src_url +
"&check=#{invidious_companion_check_id}" if (invidious_companion) "&check=#{invidious_companion_check_id}" if invidious_companion
bitrate = fmt["bitrate"] bitrate = fmt["bitrate"]
mimetype = HTML.escape(fmt["mimeType"].as_s) mimetype = HTML.escape(fmt["mimeType"].as_s)
@ -42,7 +42,7 @@
<% if params.quality == "dash" <% if params.quality == "dash"
src_url = "/api/manifest/dash/id/" + video.id + "?local=true&unique_res=1" src_url = "/api/manifest/dash/id/" + video.id + "?local=true&unique_res=1"
src_url = invidious_companion.public_url.to_s + src_url + src_url = invidious_companion.public_url.to_s + src_url +
"&check=#{invidious_companion_check_id}" if (invidious_companion) "&check=#{invidious_companion_check_id}" if invidious_companion
%> %>
<source src="<%= src_url %>" type='application/dash+xml' label="dash"> <source src="<%= src_url %>" type='application/dash+xml' label="dash">
<% end %> <% end %>
@ -54,7 +54,7 @@
src_url = "/latest_version?id=#{video.id}&itag=#{fmt["itag"]}" src_url = "/latest_version?id=#{video.id}&itag=#{fmt["itag"]}"
src_url += "&local=true" if params.local src_url += "&local=true" if params.local
src_url = invidious_companion.public_url.to_s + src_url + src_url = invidious_companion.public_url.to_s + src_url +
"&check=#{invidious_companion_check_id}" if (invidious_companion) "&check=#{invidious_companion_check_id}" if invidious_companion
quality = fmt["quality"] quality = fmt["quality"]
mimetype = HTML.escape(fmt["mimeType"].as_s) mimetype = HTML.escape(fmt["mimeType"].as_s)
@ -70,7 +70,7 @@
<% preferred_captions.each do |caption| <% preferred_captions.each do |caption|
api_captions_url = "/api/v1/captions/" api_captions_url = "/api/v1/captions/"
api_captions_url = invidious_companion.public_url.to_s + api_captions_url if (invidious_companion) api_captions_url = invidious_companion.public_url.to_s + api_captions_url if invidious_companion
api_captions_check_id = "&check=#{invidious_companion_check_id}" api_captions_check_id = "&check=#{invidious_companion_check_id}"
%> %>
<track kind="captions" src="<%= api_captions_url %><%= video.id %>?label=<%= caption.name %><%= api_captions_check_id %>" label="<%= caption.name %>"> <track kind="captions" src="<%= api_captions_url %><%= video.id %>?label=<%= caption.name %><%= api_captions_check_id %>" label="<%= caption.name %>">
@ -78,7 +78,7 @@
<% captions.each do |caption| <% captions.each do |caption|
api_captions_url = "/api/v1/captions/" api_captions_url = "/api/v1/captions/"
api_captions_url = invidious_companion.public_url.to_s + api_captions_url if (invidious_companion) api_captions_url = invidious_companion.public_url.to_s + api_captions_url if invidious_companion
api_captions_check_id = "&check=#{invidious_companion_check_id}" api_captions_check_id = "&check=#{invidious_companion_check_id}"
%> %>
<track kind="captions" src="<%= api_captions_url %><%= video.id %>?label=<%= caption.name %><%= api_captions_check_id %>" label="<%= caption.name %>"> <track kind="captions" src="<%= api_captions_url %><%= video.id %>?label=<%= caption.name %><%= api_captions_check_id %>" label="<%= caption.name %>">

View File

@ -25,7 +25,6 @@
<% end %> <% end %>
<% if captcha %> <% if captcha %>
<% captcha = captcha.not_nil! %>
<img style="width:50%" src='<%= captcha[:question] %>'/> <img style="width:50%" src='<%= captcha[:question] %>'/>
<% captcha[:tokens].each_with_index do |token, i| %> <% captcha[:tokens].each_with_index do |token, i| %>
<input type="hidden" name="token[<%= i %>]" value="<%= HTML.escape(token) %>"> <input type="hidden" name="token[<%= i %>]" value="<%= HTML.escape(token) %>">

View File

@ -41,7 +41,7 @@ struct YoutubeConnectionPool
) )
DB::Pool(HTTP::Client).new(options) do DB::Pool(HTTP::Client).new(options) do
next make_client(url, force_resolve: true) make_client(url, force_resolve: true)
end end
end end
end end
@ -133,7 +133,7 @@ def make_client(url : URI, region = nil, force_resolve : Bool = false, force_you
client.read_timeout = 10.seconds client.read_timeout = 10.seconds
client.connect_timeout = 10.seconds client.connect_timeout = 10.seconds
return client client
end end
def make_client(url : URI, region = nil, force_resolve : Bool = false, use_http_proxy : Bool = true, &) def make_client(url : URI, region = nil, force_resolve : Bool = false, use_http_proxy : Bool = true, &)
@ -149,7 +149,7 @@ def make_configured_http_proxy_client
# This method is only called when configuration for an HTTP proxy are set # This method is only called when configuration for an HTTP proxy are set
config_proxy = CONFIG.http_proxy.not_nil! config_proxy = CONFIG.http_proxy.not_nil!
return HTTP::Proxy::Client.new( HTTP::Proxy::Client.new(
config_proxy.host, config_proxy.host,
config_proxy.port, config_proxy.port,
@ -163,12 +163,12 @@ end
# Creates a new one when the specified pool for the subdomain does not exist # Creates a new one when the specified pool for the subdomain does not exist
def get_ytimg_pool(subdomain) def get_ytimg_pool(subdomain)
if pool = YTIMG_POOLS[subdomain]? if pool = YTIMG_POOLS[subdomain]?
return pool pool
else else
LOGGER.info("ytimg_pool: Creating a new HTTP pool for \"https://#{subdomain}.ytimg.com\"") LOGGER.info("ytimg_pool: Creating a new HTTP pool for \"https://#{subdomain}.ytimg.com\"")
pool = YoutubeConnectionPool.new(URI.parse("https://#{subdomain}.ytimg.com"), capacity: CONFIG.pool_size) pool = YoutubeConnectionPool.new(URI.parse("https://#{subdomain}.ytimg.com"), capacity: CONFIG.pool_size)
YTIMG_POOLS[subdomain] = pool YTIMG_POOLS[subdomain] = pool
return pool pool
end end
end end

View File

@ -37,8 +37,7 @@ record AuthorFallback, name : String, id : String
private module Parsers private module Parsers
module BaseParser module BaseParser
def parse(*args) def parse(*args)
begin parse_internal(*args)
return parse_internal(*args)
rescue ex rescue ex
LOGGER.debug("#{{{ @type.name }}}: Failed to render item.") LOGGER.debug("#{{{ @type.name }}}: Failed to render item.")
LOGGER.debug("#{{{ @type.name }}}: Got exception: #{ex.message}") LOGGER.debug("#{{{ @type.name }}}: Got exception: #{ex.message}")
@ -47,7 +46,6 @@ private module Parsers
) )
end end
end end
end
# Parses a InnerTube videoRenderer into a SearchVideo. Returns nil when the given object isn't a videoRenderer # Parses a InnerTube videoRenderer into a SearchVideo. Returns nil when the given object isn't a videoRenderer
# #
@ -64,7 +62,7 @@ private module Parsers
def process(item : JSON::Any, author_fallback : AuthorFallback) def process(item : JSON::Any, author_fallback : AuthorFallback)
if item_contents = (item["videoRenderer"]? || item["gridVideoRenderer"]?) if item_contents = (item["videoRenderer"]? || item["gridVideoRenderer"]?)
return self.parse(item_contents, author_fallback) return parse(item_contents, author_fallback)
end end
end end
@ -152,7 +150,6 @@ private module Parsers
when "Premium" when "Premium"
# TODO: Potentially available as item_contents["topStandaloneBadge"]["metadataBadgeRenderer"] # TODO: Potentially available as item_contents["topStandaloneBadge"]["metadataBadgeRenderer"]
badges |= VideoBadges::Premium badges |= VideoBadges::Premium
else nil # Ignore
end end
end end
@ -173,7 +170,7 @@ private module Parsers
end end
def self.parser_name def self.parser_name
return {{@type.name}} {{ @type.name }}
end end
end end
@ -192,7 +189,7 @@ private module Parsers
def process(item : JSON::Any, author_fallback : AuthorFallback) def process(item : JSON::Any, author_fallback : AuthorFallback)
if item_contents = (item["channelRenderer"]? || item["gridChannelRenderer"]?) if item_contents = (item["channelRenderer"]? || item["gridChannelRenderer"]?)
return self.parse(item_contents, author_fallback) return parse(item_contents, author_fallback)
end end
end end
@ -207,7 +204,7 @@ private module Parsers
# TODO change default value to nil # TODO change default value to nil
subscriber_count = item_contents.dig?("subscriberCountText", "simpleText").try &.as_s subscriber_count = item_contents.dig?("subscriberCountText", "simpleText").try &.as_s
channel_handle = subscriber_count if (subscriber_count.try &.starts_with? "@") channel_handle = subscriber_count if subscriber_count.try &.starts_with? "@"
# Since youtube added channel handles, `VideoCountText` holds the number of # Since youtube added channel handles, `VideoCountText` holds the number of
# subscribers and `subscriberCountText` holds the handle, except when the # subscribers and `subscriberCountText` holds the handle, except when the
@ -240,7 +237,7 @@ private module Parsers
end end
def self.parser_name def self.parser_name
return {{@type.name}} {{ @type.name }}
end end
end end
@ -255,7 +252,7 @@ private module Parsers
def process(item : JSON::Any, author_fallback : AuthorFallback) def process(item : JSON::Any, author_fallback : AuthorFallback)
if item_contents = item["hashtagTileRenderer"]? if item_contents = item["hashtagTileRenderer"]?
return self.parse(item_contents) return parse(item_contents)
end end
end end
@ -280,7 +277,7 @@ private module Parsers
end end
end end
return SearchHashtag.new({ SearchHashtag.new({
title: title, title: title,
url: url, url: url,
video_count: short_text_to_number(video_count_txt || ""), video_count: short_text_to_number(video_count_txt || ""),
@ -289,7 +286,7 @@ private module Parsers
end end
def self.parser_name def self.parser_name
return {{@type.name}} {{ @type.name }}
end end
end end
@ -308,7 +305,7 @@ private module Parsers
def process(item : JSON::Any, author_fallback : AuthorFallback) def process(item : JSON::Any, author_fallback : AuthorFallback)
if item_contents = item["gridPlaylistRenderer"]? if item_contents = item["gridPlaylistRenderer"]?
return self.parse(item_contents, author_fallback) return parse(item_contents, author_fallback)
end end
end end
@ -334,7 +331,7 @@ private module Parsers
end end
def self.parser_name def self.parser_name
return {{@type.name}} {{ @type.name }}
end end
end end
@ -352,7 +349,7 @@ private module Parsers
def process(item : JSON::Any, author_fallback : AuthorFallback) def process(item : JSON::Any, author_fallback : AuthorFallback)
if item_contents = item["playlistRenderer"]? if item_contents = item["playlistRenderer"]?
return self.parse(item_contents, author_fallback) return parse(item_contents, author_fallback)
end end
end end
@ -395,7 +392,7 @@ private module Parsers
end end
def self.parser_name def self.parser_name
return {{@type.name}} {{ @type.name }}
end end
end end
@ -415,7 +412,7 @@ private module Parsers
def process(item : JSON::Any, author_fallback : AuthorFallback) def process(item : JSON::Any, author_fallback : AuthorFallback)
if item_contents = item["shelfRenderer"]? if item_contents = item["shelfRenderer"]?
return self.parse(item_contents, author_fallback) return parse(item_contents, author_fallback)
end end
end end
@ -467,7 +464,7 @@ private module Parsers
end end
def self.parser_name def self.parser_name
return {{@type.name}} {{ @type.name }}
end end
end end
@ -484,7 +481,7 @@ private module Parsers
def process(item : JSON::Any, author_fallback : AuthorFallback) def process(item : JSON::Any, author_fallback : AuthorFallback)
if item_contents = item.dig?("itemSectionRenderer", "contents", 0) if item_contents = item.dig?("itemSectionRenderer", "contents", 0)
return self.parse(item_contents, author_fallback) return parse(item_contents, author_fallback)
end end
end end
@ -492,11 +489,11 @@ private module Parsers
child = VideoRendererParser.process(item_contents, author_fallback) child = VideoRendererParser.process(item_contents, author_fallback)
child ||= PlaylistRendererParser.process(item_contents, author_fallback) child ||= PlaylistRendererParser.process(item_contents, author_fallback)
return child child
end end
def self.parser_name def self.parser_name
return {{@type.name}} {{ @type.name }}
end end
end end
@ -513,7 +510,7 @@ private module Parsers
def process(item : JSON::Any, author_fallback : AuthorFallback) def process(item : JSON::Any, author_fallback : AuthorFallback)
if item_contents = item.dig?("richItemRenderer", "content") if item_contents = item.dig?("richItemRenderer", "content")
return self.parse(item_contents, author_fallback) return parse(item_contents, author_fallback)
end end
end end
@ -523,11 +520,11 @@ private module Parsers
child ||= PlaylistRendererParser.process(item_contents, author_fallback) child ||= PlaylistRendererParser.process(item_contents, author_fallback)
child ||= LockupViewModelParser.process(item_contents, author_fallback) child ||= LockupViewModelParser.process(item_contents, author_fallback)
child ||= ShortsLockupViewModelParser.process(item_contents, author_fallback) child ||= ShortsLockupViewModelParser.process(item_contents, author_fallback)
return child child
end end
def self.parser_name def self.parser_name
return {{@type.name}} {{ @type.name }}
end end
end end
@ -546,7 +543,7 @@ private module Parsers
def process(item : JSON::Any, author_fallback : AuthorFallback) def process(item : JSON::Any, author_fallback : AuthorFallback)
if item_contents = item["reelItemRenderer"]? if item_contents = item["reelItemRenderer"]?
return self.parse(item_contents, author_fallback) return parse(item_contents, author_fallback)
end end
end end
@ -626,7 +623,7 @@ private module Parsers
end end
def self.parser_name def self.parser_name
return {{@type.name}} {{ @type.name }}
end end
end end
@ -643,7 +640,7 @@ private module Parsers
def process(item : JSON::Any, author_fallback : AuthorFallback) def process(item : JSON::Any, author_fallback : AuthorFallback)
if item_contents = item["lockupViewModel"]? if item_contents = item["lockupViewModel"]?
return self.parse(item_contents, author_fallback) return parse(item_contents, author_fallback)
end end
end end
@ -674,9 +671,9 @@ private module Parsers
video_count = thumbnail_view_model.dig("overlays").as_a video_count = thumbnail_view_model.dig("overlays").as_a
.compact_map(&.dig?("thumbnailOverlayBadgeViewModel", "thumbnailBadges").try &.as_a) .compact_map(&.dig?("thumbnailOverlayBadgeViewModel", "thumbnailBadges").try &.as_a)
.flatten .flatten
.find(nil, &.dig?("thumbnailBadgeViewModel", "text").try { |node| .find(nil, &.dig?("thumbnailBadgeViewModel", "text").try do |node|
{"episodes", "videos"}.any? { |str| node.as_s.ends_with?(str) } {"episodes", "videos"}.any? { |str| node.as_s.ends_with?(str) }
}) end)
.try &.dig("thumbnailBadgeViewModel", "text").as_s.to_i(strict: false) .try &.dig("thumbnailBadgeViewModel", "text").as_s.to_i(strict: false)
metadata = item_contents.dig("metadata", "lockupMetadataViewModel") metadata = item_contents.dig("metadata", "lockupMetadataViewModel")
@ -691,7 +688,7 @@ private module Parsers
# item_contents.dig("rendererContext", "commandContext", "onTap", "innertubeCommand", "watchEndpoint") # item_contents.dig("rendererContext", "commandContext", "onTap", "innertubeCommand", "watchEndpoint")
# Available fields: "videoId", "playlistId", "params" # Available fields: "videoId", "playlistId", "params"
return SearchPlaylist.new({ SearchPlaylist.new({
title: title, title: title,
id: playlist_id, id: playlist_id,
author: author_fallback.name, author: author_fallback.name,
@ -704,7 +701,7 @@ private module Parsers
end end
def self.parser_name def self.parser_name
return {{@type.name}} {{ @type.name }}
end end
end end
@ -721,7 +718,7 @@ private module Parsers
def process(item : JSON::Any, author_fallback : AuthorFallback) def process(item : JSON::Any, author_fallback : AuthorFallback)
if item_contents = item["shortsLockupViewModel"]? if item_contents = item["shortsLockupViewModel"]?
return self.parse(item_contents, author_fallback) return parse(item_contents, author_fallback)
end end
end end
@ -762,7 +759,7 @@ private module Parsers
end end
def self.parser_name def self.parser_name
return {{@type.name}} {{ @type.name }}
end end
end end
@ -778,7 +775,7 @@ private module Parsers
module ContinuationItemRendererParser module ContinuationItemRendererParser
def self.process(item : JSON::Any, author_fallback : AuthorFallback) def self.process(item : JSON::Any, author_fallback : AuthorFallback)
if item_contents = item["continuationItemRenderer"]? if item_contents = item["continuationItemRenderer"]?
return self.parse(item_contents) return parse(item_contents)
end end
end end
@ -791,7 +788,7 @@ private module Parsers
end end
def self.parser_name def self.parser_name
return {{@type.name}} {{ @type.name }}
end end
end end
end end
@ -831,7 +828,7 @@ private module Extractors
module YouTubeTabs module YouTubeTabs
def self.process(initial_data : InitialData) def self.process(initial_data : InitialData)
if target = initial_data["twoColumnBrowseResultsRenderer"]? if target = initial_data["twoColumnBrowseResultsRenderer"]?
self.extract(target) extract(target)
end end
end end
@ -845,7 +842,7 @@ private module Extractors
raw_items = rich_grid_contents.as_a raw_items = rich_grid_contents.as_a
end end
return raw_items raw_items
end end
private def self.unpack_section_list(contents) private def self.unpack_section_list(contents)
@ -853,13 +850,13 @@ private module Extractors
contents.as_a.each do |item| contents.as_a.each do |item|
if item_section_content = item.dig?("itemSectionRenderer", "contents") if item_section_content = item.dig?("itemSectionRenderer", "contents")
raw_items += self.unpack_item_section(item_section_content) raw_items += unpack_item_section(item_section_content)
else else
raw_items << item raw_items << item
end end
end end
return raw_items raw_items
end end
private def self.unpack_item_section(contents) private def self.unpack_item_section(contents)
@ -874,11 +871,11 @@ private module Extractors
end end
end end
return raw_items raw_items
end end
def self.extractor_name def self.extractor_name
return {{@type.name}} {{ @type.name }}
end end
end end
@ -902,7 +899,7 @@ private module Extractors
module SearchResults module SearchResults
def self.process(initial_data : InitialData) def self.process(initial_data : InitialData)
if target = initial_data["twoColumnSearchResultsRenderer"]? if target = initial_data["twoColumnSearchResultsRenderer"]?
self.extract(target) extract(target)
end end
end end
@ -915,11 +912,11 @@ private module Extractors
end end
end end
return raw_items.flatten raw_items.flatten
end end
def self.extractor_name def self.extractor_name
return {{@type.name}} {{ @type.name }}
end end
end end
@ -936,11 +933,11 @@ private module Extractors
module ContinuationContent module ContinuationContent
def self.process(initial_data : InitialData) def self.process(initial_data : InitialData)
if target = initial_data["continuationContents"]? if target = initial_data["continuationContents"]?
self.extract(target) extract(target)
elsif target = initial_data["appendContinuationItemsAction"]? elsif target = initial_data["appendContinuationItemsAction"]?
self.extract(target) extract(target)
elsif target = initial_data["reloadContinuationItemsCommand"]? elsif target = initial_data["reloadContinuationItemsCommand"]?
self.extract(target) extract(target)
end end
end end
@ -949,11 +946,11 @@ private module Extractors
content ||= target.dig?("gridContinuation", "items") content ||= target.dig?("gridContinuation", "items")
content ||= target.dig?("richGridContinuation", "contents") content ||= target.dig?("richGridContinuation", "contents")
return content.nil? ? [] of JSON::Any : content.as_a content.nil? ? [] of JSON::Any : content.as_a
end end
def self.extractor_name def self.extractor_name
return {{@type.name}} {{ @type.name }}
end end
end end
end end
@ -969,14 +966,14 @@ module HelperExtractors
def self.get_video_count(container : JSON::Any) : Int32 def self.get_video_count(container : JSON::Any) : Int32
if box = container["videoCountText"]? if box = container["videoCountText"]?
if (extracted_text = extract_text(box)) && !extracted_text.includes? " subscriber" if (extracted_text = extract_text(box)) && !extracted_text.includes? " subscriber"
return extracted_text.gsub(/\D/, "").to_i extracted_text.gsub(/\D/, "").to_i
else else
return 0 0
end end
elsif box = container["videoCount"]? elsif box = container["videoCount"]?
return box.as_s.to_i box.as_s.to_i
else else
return 0 0
end end
end end
@ -990,7 +987,7 @@ module HelperExtractors
# Simpletext: "4M views" # Simpletext: "4M views"
# runs: {"text": "1.1K"},{"text":" watching"} # runs: {"text": "1.1K"},{"text":" watching"}
return box["simpleText"]?.try &.as_s.sub(" views", "") || box["simpleText"]?.try &.as_s.sub(" views", "") ||
box.dig?("runs", 0, "text").try &.as_s || "0" box.dig?("runs", 0, "text").try &.as_s || "0"
end end
@ -1000,7 +997,7 @@ module HelperExtractors
# #
# Raises when it's unable to parse from the given JSON data. # Raises when it's unable to parse from the given JSON data.
def self.get_thumbnails(container : JSON::Any) : String def self.get_thumbnails(container : JSON::Any) : String
return container.dig("thumbnail", "thumbnails", 0, "url").as_s container.dig("thumbnail", "thumbnails", 0, "url").as_s
end end
# ditto # ditto
@ -1008,13 +1005,13 @@ module HelperExtractors
# YouTube sometimes sends the thumbnail as: # YouTube sometimes sends the thumbnail as:
# {"thumbnails": [{"thumbnails": [{"url": "example.com"}, ...]}]} # {"thumbnails": [{"thumbnails": [{"url": "example.com"}, ...]}]}
def self.get_thumbnails_plural(container : JSON::Any) : String def self.get_thumbnails_plural(container : JSON::Any) : String
return container.dig("thumbnails", 0, "thumbnails", 0, "url").as_s container.dig("thumbnails", 0, "thumbnails", 0, "url").as_s
end end
# Retrieves the ID required for querying the InnerTube browse endpoint. # Retrieves the ID required for querying the InnerTube browse endpoint.
# Returns an empty string when it's unable to do so # Returns an empty string when it's unable to do so
def self.get_browse_id(container) def self.get_browse_id(container)
return container.dig?("navigationEndpoint", "browseEndpoint", "browseId").try &.as_s || "" container.dig?("navigationEndpoint", "browseEndpoint", "browseId").try &.as_s || ""
end end
end end

View File

@ -17,15 +17,13 @@
# another nil will be returned. # another nil will be returned.
def extract_text(item : JSON::Any?) : String? def extract_text(item : JSON::Any?) : String?
if item.nil? if item.nil?
return nil return
end end
if text_container = item["simpleText"]? if text_container = item["simpleText"]?
return text_container.as_s text_container.as_s
elsif text_container = item["runs"]? elsif text_container = item["runs"]?
return text_container.as_a.map(&.["text"].as_s).join("") return text_container.as_a.map(&.["text"].as_s).join("")
else
nil
end end
end end
@ -60,18 +58,18 @@ def has_verified_badge?(badges : JSON::Any?)
return true if style == "BADGE_STYLE_TYPE_VERIFIED_ARTIST" return true if style == "BADGE_STYLE_TYPE_VERIFIED_ARTIST"
end end
return false false
rescue ex rescue ex
LOGGER.debug("Unable to parse owner badges. Got exception: #{ex.message}") LOGGER.debug("Unable to parse owner badges. Got exception: #{ex.message}")
LOGGER.trace("Owner badges data: #{badges.to_json}") LOGGER.trace("Owner badges data: #{badges.to_json}")
return false false
end end
# This function extracts SearchVideo items from a Category. # This function extracts SearchVideo items from a Category.
# Categories are commonly returned in search results and trending pages. # Categories are commonly returned in search results and trending pages.
def extract_category(category : Category) : Array(SearchVideo) def extract_category(category : Category) : Array(SearchVideo)
return category.contents.select(SearchVideo) category.contents.select(SearchVideo)
end end
# :ditto: # :ditto:
@ -83,5 +81,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"] tabs.as_a.select(&.["tabRenderer"]?.try &.["selected"]?.try &.as_bool)[0]["tabRenderer"]
end end

View File

@ -30,7 +30,7 @@ module UrlSanitizer
return false return false
end end
return true true
end end
# Return which kind of parameters are allowed based on the # Return which kind of parameters are allowed based on the
@ -38,15 +38,15 @@ module UrlSanitizer
private def determine_allowed(path_root : String) private def determine_allowed(path_root : String)
case path_root case path_root
when "watch", "w", "v", "embed", "e", "shorts", "clip" when "watch", "w", "v", "embed", "e", "shorts", "clip"
return :watch :watch
when .starts_with?("@"), "c", "channel", "user", "profile", "attribution_link" when .starts_with?("@"), "c", "channel", "user", "profile", "attribution_link"
return :channel :channel
when "playlist", "mix" when "playlist", "mix"
return :playlist :playlist
when "results", "search" when "results", "search"
return :search :search
else # hashtag, post, trending, brand URLs, etc.. else # hashtag, post, trending, brand URLs, etc..
return nil return
end end
end end
@ -61,7 +61,7 @@ module UrlSanitizer
end end
end end
return new_params new_params
end end
# Transform any user-supplied youtube URL into something we can trust # Transform any user-supplied youtube URL into something we can trust
@ -78,7 +78,7 @@ module UrlSanitizer
new_uri = URI.new(path: "/") new_uri = URI.new(path: "/")
# Redirect to homepage for bogus URLs # Redirect to homepage for bogus URLs
return new_uri if (unsafe_host.nil? || unsafe_path.nil?) return new_uri if unsafe_host.nil? || unsafe_path.nil?
breadcrumbs = unsafe_path breadcrumbs = unsafe_path
.split('/', remove_empty: true) .split('/', remove_empty: true)
@ -116,6 +116,6 @@ module UrlSanitizer
new_uri.query_params = new_params new_uri.query_params = new_params
end end
return new_uri new_uri
end end
end end

View File

@ -207,7 +207,7 @@ module YoutubeAPI
# Region to provide to youtube, e.g to alter search results # Region to provide to youtube, e.g to alter search results
# (this is passed as the `gl` parameter). # (this is passed as the `gl` parameter).
property region : String | Nil property region : String?
# Initialization function # Initialization function
def initialize( def initialize(
@ -267,8 +267,8 @@ module YoutubeAPI
# Convert to string, for logging purposes # Convert to string, for logging purposes
def to_s def to_s
return { {
client_type: self.name, client_type: name,
region: @region, region: @region,
}.to_s }.to_s
end end
@ -283,7 +283,7 @@ module YoutubeAPI
# Return, as a Hash, the "context" data required to request the # Return, as a Hash, the "context" data required to request the
# youtube API endpoints. # youtube API endpoints.
# #
private def make_context(client_config : ClientConfig | Nil, video_id = "dQw4w9WgXcQ") : Hash private def make_context(client_config : ClientConfig?, video_id = "dQw4w9WgXcQ") : Hash
# Use the default client config if nil is passed # Use the default client config if nil is passed
client_config ||= DEFAULT_CLIENT_CONFIG client_config ||= DEFAULT_CLIENT_CONFIG
@ -331,7 +331,7 @@ module YoutubeAPI
client_context["client"]["platform"] = platform client_context["client"]["platform"] = platform
end end
return client_context client_context
end end
#################################################################### ####################################################################
@ -353,14 +353,14 @@ module YoutubeAPI
# #
# - A playlist ID (parameters MUST be an empty string) # - A playlist ID (parameters MUST be an empty string)
# #
def browse(continuation : String, client_config : ClientConfig | Nil = nil) def browse(continuation : String, client_config : ClientConfig? = nil)
# JSON Request data, required by the API # JSON Request data, required by the API
data = { data = {
"context" => self.make_context(client_config), "context" => make_context(client_config),
"continuation" => continuation, "continuation" => continuation,
} }
return self._post_json("/youtubei/v1/browse", data, client_config) _post_json("/youtubei/v1/browse", data, client_config)
end end
# :ditto: # :ditto:
@ -368,12 +368,12 @@ module YoutubeAPI
browse_id : String, browse_id : String,
*, # Force the following parameters to be passed by name *, # Force the following parameters to be passed by name
params : String, params : String,
client_config : ClientConfig | Nil = nil, client_config : ClientConfig? = nil,
) )
# JSON Request data, required by the API # JSON Request data, required by the API
data = { data = {
"browseId" => browse_id, "browseId" => browse_id,
"context" => self.make_context(client_config), "context" => make_context(client_config),
} }
# Append the additional parameters if those were provided # Append the additional parameters if those were provided
@ -382,7 +382,7 @@ module YoutubeAPI
data["params"] = params data["params"] = params
end end
return self._post_json("/youtubei/v1/browse", data, client_config) _post_json("/youtubei/v1/browse", data, client_config)
end end
#################################################################### ####################################################################
@ -421,29 +421,29 @@ module YoutubeAPI
# }) # })
# ``` # ```
# #
def next(continuation : String, *, client_config : ClientConfig | Nil = nil) def next(continuation : String, *, client_config : ClientConfig? = nil)
# JSON Request data, required by the API # JSON Request data, required by the API
data = { data = {
"context" => self.make_context(client_config), "context" => make_context(client_config),
"continuation" => continuation, "continuation" => continuation,
} }
return self._post_json("/youtubei/v1/next", data, client_config) _post_json("/youtubei/v1/next", data, client_config)
end end
# :ditto: # :ditto:
def next(data : Hash, *, client_config : ClientConfig | Nil = nil) def next(data : Hash, *, client_config : ClientConfig? = nil)
# JSON Request data, required by the API # JSON Request data, required by the API
data2 = data.merge({ data2 = data.merge({
"context" => self.make_context(client_config), "context" => make_context(client_config),
}) })
return self._post_json("/youtubei/v1/next", data2, client_config) _post_json("/youtubei/v1/next", data2, client_config)
end end
# Allow a NamedTuple to be passed, too. # Allow a NamedTuple to be passed, too.
def next(data : NamedTuple, *, client_config : ClientConfig | Nil = nil) def next(data : NamedTuple, *, client_config : ClientConfig? = nil)
return self.next(data.to_h, client_config: client_config) self.next(data.to_h, client_config: client_config)
end end
#################################################################### ####################################################################
@ -461,9 +461,9 @@ module YoutubeAPI
} }
if CONFIG.invidious_companion.present? if CONFIG.invidious_companion.present?
return self._post_invidious_companion("/youtubei/v1/player", data) _post_invidious_companion("/youtubei/v1/player", data)
else else
return nil return
end end
end end
@ -495,13 +495,13 @@ module YoutubeAPI
# channel_b = YoutubeAPI.resolve_url("https://youtube.com/c/invalid") # channel_b = YoutubeAPI.resolve_url("https://youtube.com/c/invalid")
# ``` # ```
# #
def resolve_url(url : String, client_config : ClientConfig | Nil = nil) def resolve_url(url : String, client_config : ClientConfig? = nil)
data = { data = {
"context" => self.make_context(nil), "context" => make_context(nil),
"url" => url, "url" => url,
} }
return self._post_json("/youtubei/v1/navigation/resolve_url", data, client_config) _post_json("/youtubei/v1/navigation/resolve_url", data, client_config)
end end
#################################################################### ####################################################################
@ -521,16 +521,16 @@ module YoutubeAPI
def search( def search(
search_query : String, search_query : String,
params : String, params : String,
client_config : ClientConfig | Nil = nil, client_config : ClientConfig? = nil,
) )
# JSON Request data, required by the API # JSON Request data, required by the API
data = { data = {
"query" => search_query, "query" => search_query,
"context" => self.make_context(client_config), "context" => make_context(client_config),
"params" => params, "params" => params,
} }
return self._post_json("/youtubei/v1/search", data, client_config) _post_json("/youtubei/v1/search", data, client_config)
end end
#################################################################### ####################################################################
@ -547,14 +547,14 @@ module YoutubeAPI
def get_transcript( def get_transcript(
params : String, params : String,
client_config : ClientConfig | Nil = nil, client_config : ClientConfig? = nil,
) : Hash(String, JSON::Any) ) : Hash(String, JSON::Any)
data = { data = {
"context" => self.make_context(client_config), "context" => make_context(client_config),
"params" => params, "params" => params,
} }
return self._post_json("/youtubei/v1/get_transcript", data, client_config) _post_json("/youtubei/v1/get_transcript", data, client_config)
end end
#################################################################### ####################################################################
@ -569,7 +569,7 @@ module YoutubeAPI
def _post_json( def _post_json(
endpoint : String, endpoint : String,
data : Hash, data : Hash,
client_config : ClientConfig | Nil, client_config : ClientConfig?,
) : Hash(String, JSON::Any) ) : Hash(String, JSON::Any)
# Use the default client config if nil is passed # Use the default client config if nil is passed
client_config ||= DEFAULT_CLIENT_CONFIG client_config ||= DEFAULT_CLIENT_CONFIG
@ -602,7 +602,7 @@ module YoutubeAPI
status code #{response.status_code}. See <a href=\"https://docs.invidious.io/youtube-errors-explained/\"> \ status code #{response.status_code}. See <a href=\"https://docs.invidious.io/youtube-errors-explained/\"> \
https://docs.invidious.io/youtube-errors-explained/</a> for troubleshooting.") https://docs.invidious.io/youtube-errors-explained/</a> for troubleshooting.")
end end
self._decompress(response.body_io, response.headers["Content-Encoding"]?) _decompress(response.body_io, response.headers["Content-Encoding"]?)
end end
end end
@ -623,7 +623,7 @@ module YoutubeAPI
error #{code} with message:<br>\"#{message}\"") error #{code} with message:<br>\"#{message}\"")
end end
return initial_data initial_data
end end
#################################################################### ####################################################################
@ -661,7 +661,7 @@ module YoutubeAPI
end end
end end
return response_body response_body
rescue ex rescue ex
raise InfoException.new("Error while communicating with Invidious companion: " + (ex.message || "no extra info found")) raise InfoException.new("Error while communicating with Invidious companion: " + (ex.message || "no extra info found"))
end end
@ -685,7 +685,7 @@ module YoutubeAPI
# Multiple encodings can be combined, and are listed in the order # Multiple encodings can be combined, and are listed in the order
# in which they were applied. E.g: "deflate, gzip" means that the # in which they were applied. E.g: "deflate, gzip" means that the
# content must be first "gunzipped", then "defated". # content must be first "gunzipped", then "defated".
encodings.split(',').reverse.each do |enc| encodings.split(',').reverse!.each do |enc|
case enc.strip(' ') case enc.strip(' ')
when "gzip" when "gzip"
body_io = Compress::Gzip::Reader.new(body_io, sync_close: true) body_io = Compress::Gzip::Reader.new(body_io, sync_close: true)
@ -695,6 +695,6 @@ module YoutubeAPI
end end
end end
return body_io.gets_to_end body_io.gets_to_end
end end
end # End of module end # End of module