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,23 +2,15 @@
# 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

@ -6,7 +6,7 @@ authors:
- Contributors! - Contributors!
description: | description: |
Invidious is an alternative front-end to YouTube Invidious is an alternative front-end to YouTube
dependencies: dependencies:
pg: pg:
@ -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
@ -59,7 +59,7 @@ end
# Get relative file path to a file within the static_assets_handler folder # Get relative file path to a file within the static_assets_handler folder
macro get_file_path(basename) macro get_file_path(basename)
"spec/http_server/handlers/static_assets_handler/#{ {{basename}} }" "spec/http_server/handlers/static_assets_handler/#{ {{ basename }} }"
end end
Spectator.describe StaticAssetsHandler do Spectator.describe StaticAssetsHandler do
@ -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,22 +7,22 @@ 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
UC1sELGmy5jp5fQUugmuYlXQ,http://www.youtube.com/channel/UC1sELGmy5jp5fQUugmuYlXQ,Minecraft UC1sELGmy5jp5fQUugmuYlXQ,http://www.youtube.com/channel/UC1sELGmy5jp5fQUugmuYlXQ,Minecraft
UC9kFnwdCRrX7oTjqKd6-tiQ,http://www.youtube.com/channel/UC9kFnwdCRrX7oTjqKd6-tiQ,LUMOX - Topic UC9kFnwdCRrX7oTjqKd6-tiQ,http://www.youtube.com/channel/UC9kFnwdCRrX7oTjqKd6-tiQ,LUMOX - Topic
UCBa659QWEk1AI4Tg--mrJ2A,http://www.youtube.com/channel/UCBa659QWEk1AI4Tg--mrJ2A,Tom Scott UCBa659QWEk1AI4Tg--mrJ2A,http://www.youtube.com/channel/UCBa659QWEk1AI4Tg--mrJ2A,Tom Scott
UCGu6_XQ64rXPR6nuitMQE_A,http://www.youtube.com/channel/UCGu6_XQ64rXPR6nuitMQE_A,Callcenter Fun UCGu6_XQ64rXPR6nuitMQE_A,http://www.youtube.com/channel/UCGu6_XQ64rXPR6nuitMQE_A,Callcenter Fun
UCGwu0nbY2wSkW8N-cghnLpA,http://www.youtube.com/channel/UCGwu0nbY2wSkW8N-cghnLpA,Jaiden Animations UCGwu0nbY2wSkW8N-cghnLpA,http://www.youtube.com/channel/UCGwu0nbY2wSkW8N-cghnLpA,Jaiden Animations
UCQ0OvZ54pCFZwsKxbltg_tg,http://www.youtube.com/channel/UCQ0OvZ54pCFZwsKxbltg_tg,Methos UCQ0OvZ54pCFZwsKxbltg_tg,http://www.youtube.com/channel/UCQ0OvZ54pCFZwsKxbltg_tg,Methos
UCRE6itj4Jte4manQEu3Y7OA,http://www.youtube.com/channel/UCRE6itj4Jte4manQEu3Y7OA,Chipflake UCRE6itj4Jte4manQEu3Y7OA,http://www.youtube.com/channel/UCRE6itj4Jte4manQEu3Y7OA,Chipflake
UCRLc6zsv_d0OEBO8OOkz-DA,http://www.youtube.com/channel/UCRLc6zsv_d0OEBO8OOkz-DA,Kegy UCRLc6zsv_d0OEBO8OOkz-DA,http://www.youtube.com/channel/UCRLc6zsv_d0OEBO8OOkz-DA,Kegy
UCSl5Uxu2LyaoAoMMGp6oTJA,http://www.youtube.com/channel/UCSl5Uxu2LyaoAoMMGp6oTJA,Atomic Shrimp UCSl5Uxu2LyaoAoMMGp6oTJA,http://www.youtube.com/channel/UCSl5Uxu2LyaoAoMMGp6oTJA,Atomic Shrimp
UCXuqSBlHAE6Xw-yeJA0Tunw,http://www.youtube.com/channel/UCXuqSBlHAE6Xw-yeJA0Tunw,Linus Tech Tips UCXuqSBlHAE6Xw-yeJA0Tunw,http://www.youtube.com/channel/UCXuqSBlHAE6Xw-yeJA0Tunw,Linus Tech Tips
UCZ5XnGb-3t7jCkXdawN2tkA,http://www.youtube.com/channel/UCZ5XnGb-3t7jCkXdawN2tkA,Discord UCZ5XnGb-3t7jCkXdawN2tkA,http://www.youtube.com/channel/UCZ5XnGb-3t7jCkXdawN2tkA,Discord
CSV CSV
end end
Spectator.describe Invidious::User::Import do Spectator.describe Invidious::User::Import do

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
@ -93,7 +93,7 @@ struct ChannelVideo
def to_tuple def to_tuple
{% begin %} {% begin %}
{ {
{{@type.instance_vars.map(&.name).splat}} {{ @type.instance_vars.map(&.name).splat }}
} }
{% 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

@ -58,7 +58,7 @@ struct ConfigPreferences
def to_tuple def to_tuple
{% begin %} {% begin %}
{ {
{{(@type.instance_vars.map { |var| "#{var.name}: #{var.name}".id }).splat}} {{ (@type.instance_vars.map { |var| "#{var.name}: #{var.name}".id }).splat }}
} }
{% end %} {% end %}
end end
@ -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
@ -212,14 +212,14 @@ class Config
{% for ivar in Config.instance_vars %} {% for ivar in Config.instance_vars %}
{% env_id = "INVIDIOUS_#{ivar.id.upcase}" %} {% env_id = "INVIDIOUS_#{ivar.id.upcase}" %}
if ENV.has_key?({{env_id}}) if ENV.has_key?({{ env_id }})
env_value = ENV.fetch({{env_id}}) env_value = ENV.fetch({{ env_id }})
success = false success = false
# Use YAML converter if specified # Use YAML converter if specified
{% ann = ivar.annotation(::YAML::Field) %} {% ann = ivar.annotation(::YAML::Field) %}
{% if ann && ann[:converter] %} {% if ann && ann[:converter] %}
config.{{ivar.id}} = {{ann[:converter]}}.from_yaml(YAML::ParseContext.new, YAML::Nodes.parse(ENV.fetch({{env_id}})).nodes[0]) config.{{ ivar.id }} = {{ ann[:converter] }}.from_yaml(YAML::ParseContext.new, YAML::Nodes.parse(ENV.fetch({{ env_id }})).nodes[0])
success = true success = true
# Use regular YAML parser otherwise # Use regular YAML parser otherwise
@ -227,10 +227,10 @@ class Config
{% ivar_types = ivar.type.union? ? ivar.type.union_types : [ivar.type] %} {% ivar_types = ivar.type.union? ? ivar.type.union_types : [ivar.type] %}
# Sort types to avoid parsing nulls and numbers as strings # Sort types to avoid parsing nulls and numbers as strings
{% ivar_types = ivar_types.sort_by { |ivar_type| ivar_type == Nil ? 0 : ivar_type == Int32 ? 1 : 2 } %} {% ivar_types = ivar_types.sort_by { |ivar_type| ivar_type == Nil ? 0 : ivar_type == Int32 ? 1 : 2 } %}
{{ivar_types}}.each do |ivar_type| {{ ivar_types }}.each do |ivar_type|
if !success if !success
begin begin
config.{{ivar.id}} = ivar_type.from_yaml(env_value) config.{{ ivar.id }} = ivar_type.from_yaml(env_value)
success = true success = true
rescue rescue
# nop # nop
@ -241,14 +241,14 @@ class Config
# Exit on fail # Exit on fail
if !success if !success
puts %(Config.{{ivar.id}} failed to parse #{env_value} as {{ivar.type}}) puts %(Config.{{ ivar.id }} failed to parse #{env_value} as {{ ivar.type }})
exit(1) exit(1)
end end
end end
# Warn when any config attribute is set to "CHANGE_ME!!" # Warn when any config attribute is set to "CHANGE_ME!!"
if config.{{ivar.id}} == "CHANGE_ME!!" if config.{{ ivar.id }} == "CHANGE_ME!!"
puts "Config: The value of '#{ {{ivar.stringify}} }' needs to be changed!!" puts "Config: The value of '#{ {{ ivar.stringify }} }' needs to be changed!!"
exit(1) exit(1)
end end
{% end %} {% end %}
@ -318,6 +318,6 @@ class Config
end end
end end
return config config
end end
end end

View File

@ -8,7 +8,7 @@ module Invidious::Database::Annotations
INSERT INTO annotations INSERT INTO annotations
VALUES ($1, $2) VALUES ($1, $2)
ON CONFLICT DO NOTHING ON CONFLICT DO NOTHING
SQL SQL
PG_DB.exec(request, id, annotations) PG_DB.exec(request, id, annotations)
end end
@ -17,8 +17,8 @@ module Invidious::Database::Annotations
request = <<-SQL request = <<-SQL
SELECT * FROM annotations SELECT * FROM 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

@ -16,13 +16,13 @@ module Invidious::Database::Channels
request = <<-SQL request = <<-SQL
INSERT INTO channels INSERT INTO channels
VALUES (#{arg_array(channel_array)}) VALUES (#{arg_array(channel_array)})
SQL SQL
if update_on_conflict if update_on_conflict
request += <<-SQL request += <<-SQL
ON CONFLICT (id) DO UPDATE ON CONFLICT (id) DO UPDATE
SET author = $2, updated = $3 SET author = $2, updated = $3
SQL SQL
end end
PG_DB.exec(request, args: channel_array) PG_DB.exec(request, args: channel_array)
@ -37,7 +37,7 @@ module Invidious::Database::Channels
UPDATE channels UPDATE channels
SET updated = now(), author = $1, deleted = false SET updated = now(), author = $1, deleted = false
WHERE id = $2 WHERE id = $2
SQL SQL
PG_DB.exec(request, author, id) PG_DB.exec(request, author, id)
end end
@ -47,7 +47,7 @@ module Invidious::Database::Channels
UPDATE channels UPDATE channels
SET subscribed = now() SET subscribed = now()
WHERE id = $1 WHERE id = $1
SQL SQL
PG_DB.exec(request, id) PG_DB.exec(request, id)
end end
@ -57,7 +57,7 @@ module Invidious::Database::Channels
UPDATE channels UPDATE channels
SET updated = now(), deleted = true SET updated = now(), deleted = true
WHERE id = $1 WHERE id = $1
SQL SQL
PG_DB.exec(request, id) PG_DB.exec(request, id)
end end
@ -70,9 +70,9 @@ module Invidious::Database::Channels
request = <<-SQL request = <<-SQL
SELECT * FROM channels SELECT * FROM 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)?
@ -81,9 +81,9 @@ module Invidious::Database::Channels
request = <<-SQL request = <<-SQL
SELECT * FROM channels SELECT * FROM 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
@ -112,9 +112,9 @@ module Invidious::Database::ChannelVideos
SET title = $2, published = $3, updated = $4, ucid = $5, SET title = $2, published = $3, updated = $4, ucid = $5,
author = $6, length_seconds = $7, live_now = $8, #{last_items} author = $6, length_seconds = $7, live_now = $8, #{last_items}
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
# ------------------- # -------------------
@ -128,9 +128,9 @@ module Invidious::Database::ChannelVideos
SELECT * FROM channel_videos SELECT * FROM channel_videos
WHERE id = ANY($1) WHERE id = ANY($1)
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)
@ -139,9 +139,9 @@ module Invidious::Database::ChannelVideos
WHERE ucid = $1 AND published > $2 WHERE ucid = $1 AND published > $2
ORDER BY published DESC ORDER BY published DESC
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)
@ -151,7 +151,7 @@ module Invidious::Database::ChannelVideos
WHERE ucid IN (SELECT channel FROM (SELECT UNNEST(subscriptions) AS channel FROM users) AS d WHERE ucid IN (SELECT channel FROM (SELECT UNNEST(subscriptions) AS channel FROM users) AS d
GROUP BY channel ORDER BY COUNT(channel) DESC LIMIT 40) GROUP BY channel ORDER BY COUNT(channel) DESC LIMIT 40)
ORDER BY ucid, published DESC ORDER BY ucid, published DESC
SQL SQL
PG_DB.query_all(request, as: ChannelVideo) PG_DB.query_all(request, as: ChannelVideo)
end end

View File

@ -4,27 +4,27 @@ module Invidious::Database::Migrations
def up(conn : DB::Connection) def up(conn : DB::Connection)
conn.exec <<-SQL conn.exec <<-SQL
CREATE TABLE IF NOT EXISTS public.channels CREATE TABLE IF NOT EXISTS public.channels
( (
id text NOT NULL, id text NOT NULL,
author text, author text,
updated timestamp with time zone, updated timestamp with time zone,
deleted boolean, deleted boolean,
subscribed timestamp with time zone, subscribed timestamp with time zone,
CONSTRAINT channels_id_key UNIQUE (id) CONSTRAINT channels_id_key UNIQUE (id)
); );
SQL SQL
conn.exec <<-SQL conn.exec <<-SQL
GRANT ALL ON TABLE public.channels TO current_user; GRANT ALL ON TABLE public.channels TO current_user;
SQL SQL
conn.exec <<-SQL conn.exec <<-SQL
CREATE INDEX IF NOT EXISTS channels_id_idx CREATE INDEX IF NOT EXISTS channels_id_idx
ON public.channels ON public.channels
USING btree USING btree
(id COLLATE pg_catalog."default"); (id COLLATE pg_catalog."default");
SQL SQL
end end
end end
end end

View File

@ -4,25 +4,25 @@ module Invidious::Database::Migrations
def up(conn : DB::Connection) def up(conn : DB::Connection)
conn.exec <<-SQL conn.exec <<-SQL
CREATE UNLOGGED TABLE IF NOT EXISTS public.videos CREATE UNLOGGED TABLE IF NOT EXISTS public.videos
( (
id text NOT NULL, id text NOT NULL,
info text, info text,
updated timestamp with time zone, updated timestamp with time zone,
CONSTRAINT videos_pkey PRIMARY KEY (id) CONSTRAINT videos_pkey PRIMARY KEY (id)
); );
SQL SQL
conn.exec <<-SQL conn.exec <<-SQL
GRANT ALL ON TABLE public.videos TO current_user; GRANT ALL ON TABLE public.videos TO current_user;
SQL SQL
conn.exec <<-SQL conn.exec <<-SQL
CREATE UNIQUE INDEX IF NOT EXISTS id_idx CREATE UNIQUE INDEX IF NOT EXISTS id_idx
ON public.videos ON public.videos
USING btree USING btree
(id COLLATE pg_catalog."default"); (id COLLATE pg_catalog."default");
SQL SQL
end end
end end
end end

View File

@ -4,32 +4,32 @@ module Invidious::Database::Migrations
def up(conn : DB::Connection) def up(conn : DB::Connection)
conn.exec <<-SQL conn.exec <<-SQL
CREATE TABLE IF NOT EXISTS public.channel_videos CREATE TABLE IF NOT EXISTS public.channel_videos
( (
id text NOT NULL, id text NOT NULL,
title text, title text,
published timestamp with time zone, published timestamp with time zone,
updated timestamp with time zone, updated timestamp with time zone,
ucid text, ucid text,
author text, author text,
length_seconds integer, length_seconds integer,
live_now boolean, live_now boolean,
premiere_timestamp timestamp with time zone, premiere_timestamp timestamp with time zone,
views bigint, views bigint,
CONSTRAINT channel_videos_id_key UNIQUE (id) CONSTRAINT channel_videos_id_key UNIQUE (id)
); );
SQL SQL
conn.exec <<-SQL conn.exec <<-SQL
GRANT ALL ON TABLE public.channel_videos TO current_user; GRANT ALL ON TABLE public.channel_videos TO current_user;
SQL SQL
conn.exec <<-SQL conn.exec <<-SQL
CREATE INDEX IF NOT EXISTS channel_videos_ucid_idx CREATE INDEX IF NOT EXISTS channel_videos_ucid_idx
ON public.channel_videos ON public.channel_videos
USING btree USING btree
(ucid COLLATE pg_catalog."default"); (ucid COLLATE pg_catalog."default");
SQL SQL
end end
end end
end end

View File

@ -4,31 +4,31 @@ module Invidious::Database::Migrations
def up(conn : DB::Connection) def up(conn : DB::Connection)
conn.exec <<-SQL conn.exec <<-SQL
CREATE TABLE IF NOT EXISTS public.users CREATE TABLE IF NOT EXISTS public.users
( (
updated timestamp with time zone, updated timestamp with time zone,
notifications text[], notifications text[],
subscriptions text[], subscriptions text[],
email text NOT NULL, email text NOT NULL,
preferences text, preferences text,
password text, password text,
token text, token text,
watched text[], watched text[],
feed_needs_update boolean, feed_needs_update boolean,
CONSTRAINT users_email_key UNIQUE (email) CONSTRAINT users_email_key UNIQUE (email)
); );
SQL SQL
conn.exec <<-SQL conn.exec <<-SQL
GRANT ALL ON TABLE public.users TO current_user; GRANT ALL ON TABLE public.users TO current_user;
SQL SQL
conn.exec <<-SQL conn.exec <<-SQL
CREATE UNIQUE INDEX IF NOT EXISTS email_unique_idx CREATE UNIQUE INDEX IF NOT EXISTS email_unique_idx
ON public.users ON public.users
USING btree USING btree
(lower(email) COLLATE pg_catalog."default"); (lower(email) COLLATE pg_catalog."default");
SQL SQL
end end
end end
end end

View File

@ -4,25 +4,25 @@ module Invidious::Database::Migrations
def up(conn : DB::Connection) def up(conn : DB::Connection)
conn.exec <<-SQL conn.exec <<-SQL
CREATE TABLE IF NOT EXISTS public.session_ids CREATE TABLE IF NOT EXISTS public.session_ids
( (
id text NOT NULL, id text NOT NULL,
email text, email text,
issued timestamp with time zone, issued timestamp with time zone,
CONSTRAINT session_ids_pkey PRIMARY KEY (id) CONSTRAINT session_ids_pkey PRIMARY KEY (id)
); );
SQL SQL
conn.exec <<-SQL conn.exec <<-SQL
GRANT ALL ON TABLE public.session_ids TO current_user; GRANT ALL ON TABLE public.session_ids TO current_user;
SQL SQL
conn.exec <<-SQL conn.exec <<-SQL
CREATE INDEX IF NOT EXISTS session_ids_id_idx CREATE INDEX IF NOT EXISTS session_ids_id_idx
ON public.session_ids ON public.session_ids
USING btree USING btree
(id COLLATE pg_catalog."default"); (id COLLATE pg_catalog."default");
SQL SQL
end end
end end
end end

View File

@ -4,24 +4,24 @@ module Invidious::Database::Migrations
def up(conn : DB::Connection) def up(conn : DB::Connection)
conn.exec <<-SQL conn.exec <<-SQL
CREATE TABLE IF NOT EXISTS public.nonces CREATE TABLE IF NOT EXISTS public.nonces
( (
nonce text, nonce text,
expire timestamp with time zone, expire timestamp with time zone,
CONSTRAINT nonces_id_key UNIQUE (nonce) CONSTRAINT nonces_id_key UNIQUE (nonce)
); );
SQL SQL
conn.exec <<-SQL conn.exec <<-SQL
GRANT ALL ON TABLE public.nonces TO current_user; GRANT ALL ON TABLE public.nonces TO current_user;
SQL SQL
conn.exec <<-SQL conn.exec <<-SQL
CREATE INDEX IF NOT EXISTS nonces_nonce_idx CREATE INDEX IF NOT EXISTS nonces_nonce_idx
ON public.nonces ON public.nonces
USING btree USING btree
(nonce COLLATE pg_catalog."default"); (nonce COLLATE pg_catalog."default");
SQL SQL
end end
end end
end end

View File

@ -4,17 +4,17 @@ module Invidious::Database::Migrations
def up(conn : DB::Connection) def up(conn : DB::Connection)
conn.exec <<-SQL conn.exec <<-SQL
CREATE TABLE IF NOT EXISTS public.annotations CREATE TABLE IF NOT EXISTS public.annotations
( (
id text NOT NULL, id text NOT NULL,
annotations xml, annotations xml,
CONSTRAINT annotations_id_key UNIQUE (id) CONSTRAINT annotations_id_key UNIQUE (id)
); );
SQL SQL
conn.exec <<-SQL conn.exec <<-SQL
GRANT ALL ON TABLE public.annotations TO current_user; GRANT ALL ON TABLE public.annotations TO current_user;
SQL SQL
end end
end end
end end

View File

@ -5,33 +5,33 @@ module Invidious::Database::Migrations
def up(conn : DB::Connection) def up(conn : DB::Connection)
if !privacy_type_exists?(conn) if !privacy_type_exists?(conn)
conn.exec <<-SQL conn.exec <<-SQL
CREATE TYPE public.privacy AS ENUM CREATE TYPE public.privacy AS ENUM
( (
'Public', 'Public',
'Unlisted', 'Unlisted',
'Private' 'Private'
); );
SQL SQL
end end
conn.exec <<-SQL conn.exec <<-SQL
CREATE TABLE IF NOT EXISTS public.playlists CREATE TABLE IF NOT EXISTS public.playlists
( (
title text, title text,
id text primary key, id text primary key,
author text, author text,
description text, description text,
video_count integer, video_count integer,
created timestamptz, created timestamptz,
updated timestamptz, updated timestamptz,
privacy privacy, privacy privacy,
index int8[] index int8[]
); );
SQL SQL
conn.exec <<-SQL conn.exec <<-SQL
GRANT ALL ON public.playlists TO current_user; GRANT ALL ON public.playlists TO current_user;
SQL SQL
end end
private def privacy_type_exists?(conn : DB::Connection) : Bool private def privacy_type_exists?(conn : DB::Connection) : Bool
@ -42,7 +42,7 @@ module Invidious::Database::Migrations
WHERE pg_namespace.nspname = 'public' WHERE pg_namespace.nspname = 'public'
AND pg_type.typname = 'privacy' AND pg_type.typname = 'privacy'
LIMIT 1; LIMIT 1;
SQL SQL
!conn.query_one?(request, as: Int32).nil? !conn.query_one?(request, as: Int32).nil?
end end

View File

@ -4,24 +4,24 @@ module Invidious::Database::Migrations
def up(conn : DB::Connection) def up(conn : DB::Connection)
conn.exec <<-SQL conn.exec <<-SQL
CREATE TABLE IF NOT EXISTS public.playlist_videos CREATE TABLE IF NOT EXISTS public.playlist_videos
( (
title text, title text,
id text, id text,
author text, author text,
ucid text, ucid text,
length_seconds integer, length_seconds integer,
published timestamptz, published timestamptz,
plid text references playlists(id), plid text references playlists(id),
index int8, index int8,
live_now boolean, live_now boolean,
PRIMARY KEY (index,plid) PRIMARY KEY (index,plid)
); );
SQL SQL
conn.exec <<-SQL conn.exec <<-SQL
GRANT ALL ON TABLE public.playlist_videos TO current_user; GRANT ALL ON TABLE public.playlist_videos TO current_user;
SQL SQL
end end
end end
end end

View File

@ -4,8 +4,8 @@ module Invidious::Database::Migrations
def up(conn : DB::Connection) def up(conn : DB::Connection)
conn.exec <<-SQL conn.exec <<-SQL
ALTER TABLE public.videos SET UNLOGGED; ALTER TABLE public.videos SET UNLOGGED;
SQL SQL
end end
end end
end end

View File

@ -44,6 +44,6 @@ class Invidious::Database::Migrator
id bigserial PRIMARY KEY, id bigserial PRIMARY KEY,
version bigint NOT NULL version bigint NOT NULL
) )
SQL SQL
end end
end end

View File

@ -12,7 +12,7 @@ module Invidious::Database::Nonces
INSERT INTO nonces INSERT INTO nonces
VALUES ($1, $2) VALUES ($1, $2)
ON CONFLICT DO NOTHING ON CONFLICT DO NOTHING
SQL SQL
PG_DB.exec(request, nonce, expire) PG_DB.exec(request, nonce, expire)
end end
@ -21,7 +21,7 @@ module Invidious::Database::Nonces
request = <<-SQL request = <<-SQL
DELETE FROM nonces * DELETE FROM nonces *
WHERE expire < now() WHERE expire < now()
SQL SQL
PG_DB.exec(request) PG_DB.exec(request)
end end
@ -35,7 +35,7 @@ module Invidious::Database::Nonces
UPDATE nonces UPDATE nonces
SET expire = $1 SET expire = $1
WHERE nonce = $2 WHERE nonce = $2
SQL SQL
PG_DB.exec(request, Time.utc(1990, 1, 1), nonce) PG_DB.exec(request, Time.utc(1990, 1, 1), nonce)
end end
@ -48,8 +48,8 @@ module Invidious::Database::Nonces
request = <<-SQL request = <<-SQL
SELECT * FROM nonces SELECT * FROM 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

@ -16,7 +16,7 @@ module Invidious::Database::Playlists
request = <<-SQL request = <<-SQL
INSERT INTO playlists INSERT INTO playlists
VALUES (#{arg_array(playlist_array)}) VALUES (#{arg_array(playlist_array)})
SQL SQL
PG_DB.exec(request, args: playlist_array) PG_DB.exec(request, args: playlist_array)
end end
@ -27,7 +27,7 @@ module Invidious::Database::Playlists
request = <<-SQL request = <<-SQL
DELETE FROM playlists * DELETE FROM playlists *
WHERE id = $1 WHERE id = $1
SQL SQL
PG_DB.exec(request, id) PG_DB.exec(request, id)
end end
@ -41,7 +41,7 @@ module Invidious::Database::Playlists
UPDATE playlists UPDATE playlists
SET title = $1, privacy = $2, description = $3, updated = $4 SET title = $1, privacy = $2, description = $3, updated = $4
WHERE id = $5 WHERE id = $5
SQL SQL
PG_DB.exec(request, title, privacy, description, updated, id) PG_DB.exec(request, title, privacy, description, updated, id)
end end
@ -51,7 +51,7 @@ module Invidious::Database::Playlists
UPDATE playlists UPDATE playlists
SET description = $1 SET description = $1
WHERE id = $2 WHERE id = $2
SQL SQL
PG_DB.exec(request, description, id) PG_DB.exec(request, description, id)
end end
@ -61,7 +61,7 @@ module Invidious::Database::Playlists
UPDATE playlists UPDATE playlists
SET subscribed = now() SET subscribed = now()
WHERE id = $1 WHERE id = $1
SQL SQL
PG_DB.exec(request, id) PG_DB.exec(request, id)
end end
@ -73,7 +73,7 @@ module Invidious::Database::Playlists
video_count = cardinality(index) + 1, video_count = cardinality(index) + 1,
updated = now() updated = now()
WHERE id = $2 WHERE id = $2
SQL SQL
PG_DB.exec(request, index, id) PG_DB.exec(request, index, id)
end end
@ -85,7 +85,7 @@ module Invidious::Database::Playlists
video_count = cardinality(index) - 1, video_count = cardinality(index) - 1,
updated = now() updated = now()
WHERE id = $2 WHERE id = $2
SQL SQL
PG_DB.exec(request, index, id) PG_DB.exec(request, index, id)
end end
@ -98,18 +98,18 @@ module Invidious::Database::Playlists
request = <<-SQL request = <<-SQL
SELECT * FROM playlists SELECT * FROM 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)
request = <<-SQL request = <<-SQL
SELECT * FROM playlists SELECT * FROM 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
# ------------------- # -------------------
@ -121,7 +121,7 @@ module Invidious::Database::Playlists
SELECT * FROM playlists SELECT * FROM playlists
WHERE author = $1 AND id LIKE 'IV%' WHERE author = $1 AND id LIKE 'IV%'
ORDER BY created ORDER BY created
SQL SQL
PG_DB.query_all(request, email, as: InvidiousPlaylist) PG_DB.query_all(request, email, as: InvidiousPlaylist)
end end
@ -131,7 +131,7 @@ module Invidious::Database::Playlists
SELECT * FROM playlists SELECT * FROM playlists
WHERE author = $1 AND id NOT LIKE 'IV%' WHERE author = $1 AND id NOT LIKE 'IV%'
ORDER BY created ORDER BY created
SQL SQL
PG_DB.query_all(request, email, as: InvidiousPlaylist) PG_DB.query_all(request, email, as: InvidiousPlaylist)
end end
@ -141,7 +141,7 @@ module Invidious::Database::Playlists
SELECT id,title FROM playlists SELECT id,title FROM playlists
WHERE author = $1 AND id LIKE 'IV%' WHERE author = $1 AND id LIKE 'IV%'
ORDER BY title ORDER BY title
SQL SQL
PG_DB.query_all(request, email, as: {String, String}) PG_DB.query_all(request, email, as: {String, String})
end end
@ -155,9 +155,9 @@ module Invidious::Database::Playlists
request = <<-SQL request = <<-SQL
SELECT id FROM playlists SELECT id FROM 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.
@ -165,9 +165,9 @@ module Invidious::Database::Playlists
request = <<-SQL request = <<-SQL
SELECT count(*) FROM playlists SELECT count(*) FROM 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
@ -189,7 +189,7 @@ module Invidious::Database::PlaylistVideos
request = <<-SQL request = <<-SQL
INSERT INTO playlist_videos INSERT INTO playlist_videos
VALUES (#{arg_array(video_array)}) VALUES (#{arg_array(video_array)})
SQL SQL
PG_DB.exec(request, args: video_array) PG_DB.exec(request, args: video_array)
end end
@ -198,7 +198,7 @@ module Invidious::Database::PlaylistVideos
request = <<-SQL request = <<-SQL
DELETE FROM playlist_videos * DELETE FROM playlist_videos *
WHERE index = $1 WHERE index = $1
SQL SQL
PG_DB.exec(request, index) PG_DB.exec(request, index)
end end
@ -207,7 +207,7 @@ module Invidious::Database::PlaylistVideos
request = <<-SQL request = <<-SQL
DELETE FROM playlist_videos * DELETE FROM playlist_videos *
WHERE plid = $1 WHERE plid = $1
SQL SQL
PG_DB.exec(request, plid) PG_DB.exec(request, plid)
end end
@ -223,9 +223,9 @@ module Invidious::Database::PlaylistVideos
ORDER BY array_position($2, index) ORDER BY array_position($2, index)
LIMIT $3 LIMIT $3
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?
@ -233,9 +233,9 @@ module Invidious::Database::PlaylistVideos
SELECT index FROM playlist_videos SELECT index FROM playlist_videos
WHERE plid = $1 AND id = $2 WHERE plid = $1 AND id = $2
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?
@ -244,9 +244,9 @@ module Invidious::Database::PlaylistVideos
WHERE plid = $1 WHERE plid = $1
ORDER BY array_position($2, index) ORDER BY array_position($2, index)
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)
@ -255,8 +255,8 @@ module Invidious::Database::PlaylistVideos
WHERE plid = $1 WHERE plid = $1
ORDER BY array_position($2, index) ORDER BY array_position($2, index)
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

@ -11,7 +11,7 @@ module Invidious::Database::SessionIDs
request = <<-SQL request = <<-SQL
INSERT INTO session_ids INSERT INTO session_ids
VALUES ($1, $2, now()) VALUES ($1, $2, now())
SQL SQL
request += " ON CONFLICT (id) DO NOTHING" if handle_conflicts request += " ON CONFLICT (id) DO NOTHING" if handle_conflicts
@ -26,7 +26,7 @@ module Invidious::Database::SessionIDs
request = <<-SQL request = <<-SQL
DELETE FROM session_ids * DELETE FROM session_ids *
WHERE id = $1 WHERE id = $1
SQL SQL
PG_DB.exec(request, sid) PG_DB.exec(request, sid)
end end
@ -35,7 +35,7 @@ module Invidious::Database::SessionIDs
request = <<-SQL request = <<-SQL
DELETE FROM session_ids * DELETE FROM session_ids *
WHERE email = $1 WHERE email = $1
SQL SQL
PG_DB.exec(request, email) PG_DB.exec(request, email)
end end
@ -44,7 +44,7 @@ module Invidious::Database::SessionIDs
request = <<-SQL request = <<-SQL
DELETE FROM session_ids * DELETE FROM session_ids *
WHERE id = $1 AND email = $2 WHERE id = $1 AND email = $2
SQL SQL
PG_DB.exec(request, sid, email) PG_DB.exec(request, sid, email)
end end
@ -57,7 +57,7 @@ module Invidious::Database::SessionIDs
request = <<-SQL request = <<-SQL
SELECT email FROM session_ids SELECT email FROM session_ids
WHERE id = $1 WHERE id = $1
SQL SQL
PG_DB.query_one?(request, sid, as: String) PG_DB.query_one?(request, sid, as: String)
end end
@ -67,7 +67,7 @@ module Invidious::Database::SessionIDs
SELECT id, issued FROM session_ids SELECT id, issued FROM session_ids
WHERE email = $1 WHERE email = $1
ORDER BY issued DESC ORDER BY issued DESC
SQL SQL
PG_DB.query_all(request, email, as: {session: String, issued: Time}) PG_DB.query_all(request, email, as: {session: String, issued: Time})
end end

View File

@ -10,7 +10,7 @@ module Invidious::Database::Statistics
def count_users_total : Int64 def count_users_total : Int64
request = <<-SQL request = <<-SQL
SELECT count(*) FROM users SELECT count(*) FROM users
SQL SQL
PG_DB.query_one(request, as: Int64) PG_DB.query_one(request, as: Int64)
end end
@ -19,7 +19,7 @@ module Invidious::Database::Statistics
request = <<-SQL request = <<-SQL
SELECT count(*) FROM users SELECT count(*) FROM users
WHERE CURRENT_TIMESTAMP - updated < '6 months' WHERE CURRENT_TIMESTAMP - updated < '6 months'
SQL SQL
PG_DB.query_one(request, as: Int64) PG_DB.query_one(request, as: Int64)
end end
@ -28,7 +28,7 @@ module Invidious::Database::Statistics
request = <<-SQL request = <<-SQL
SELECT count(*) FROM users SELECT count(*) FROM users
WHERE CURRENT_TIMESTAMP - updated < '1 month' WHERE CURRENT_TIMESTAMP - updated < '1 month'
SQL SQL
PG_DB.query_one(request, as: Int64) PG_DB.query_one(request, as: Int64)
end end
@ -42,7 +42,7 @@ module Invidious::Database::Statistics
SELECT updated FROM channels SELECT updated FROM channels
ORDER BY updated DESC ORDER BY updated DESC
LIMIT 1 LIMIT 1
SQL SQL
PG_DB.query_one?(request, as: Time) PG_DB.query_one?(request, as: Time)
end end

View File

@ -14,13 +14,13 @@ module Invidious::Database::Users
request = <<-SQL request = <<-SQL
INSERT INTO users INSERT INTO users
VALUES (#{arg_array(user_array)}) VALUES (#{arg_array(user_array)})
SQL SQL
if update_on_conflict if update_on_conflict
request += <<-SQL request += <<-SQL
ON CONFLICT (email) DO UPDATE ON CONFLICT (email) DO UPDATE
SET updated = $1, subscriptions = $3 SET updated = $1, subscriptions = $3
SQL SQL
end end
PG_DB.exec(request, args: user_array) PG_DB.exec(request, args: user_array)
@ -30,7 +30,7 @@ module Invidious::Database::Users
request = <<-SQL request = <<-SQL
DELETE FROM users * DELETE FROM users *
WHERE email = $1 WHERE email = $1
SQL SQL
PG_DB.exec(request, user.email) PG_DB.exec(request, user.email)
end end
@ -44,7 +44,7 @@ module Invidious::Database::Users
UPDATE users UPDATE users
SET watched = $1 SET watched = $1
WHERE email = $2 WHERE email = $2
SQL SQL
PG_DB.exec(request, user.watched, user.email) PG_DB.exec(request, user.watched, user.email)
end end
@ -54,7 +54,7 @@ module Invidious::Database::Users
UPDATE users UPDATE users
SET watched = array_append(array_remove(watched, $1), $1) SET watched = array_append(array_remove(watched, $1), $1)
WHERE email = $2 WHERE email = $2
SQL SQL
PG_DB.exec(request, vid, user.email) PG_DB.exec(request, vid, user.email)
end end
@ -64,7 +64,7 @@ module Invidious::Database::Users
UPDATE users UPDATE users
SET watched = array_remove(watched, $1) SET watched = array_remove(watched, $1)
WHERE email = $2 WHERE email = $2
SQL SQL
PG_DB.exec(request, vid, user.email) PG_DB.exec(request, vid, user.email)
end end
@ -74,7 +74,7 @@ module Invidious::Database::Users
UPDATE users UPDATE users
SET watched = '{}' SET watched = '{}'
WHERE email = $1 WHERE email = $1
SQL SQL
PG_DB.exec(request, user.email) PG_DB.exec(request, user.email)
end end
@ -88,7 +88,7 @@ module Invidious::Database::Users
UPDATE users UPDATE users
SET feed_needs_update = true, subscriptions = $1 SET feed_needs_update = true, subscriptions = $1
WHERE email = $2 WHERE email = $2
SQL SQL
PG_DB.exec(request, user.subscriptions, user.email) PG_DB.exec(request, user.subscriptions, user.email)
end end
@ -99,7 +99,7 @@ module Invidious::Database::Users
SET feed_needs_update = true, SET feed_needs_update = true,
subscriptions = array_append(subscriptions,$1) subscriptions = array_append(subscriptions,$1)
WHERE email = $2 WHERE email = $2
SQL SQL
PG_DB.exec(request, ucid, user.email) PG_DB.exec(request, ucid, user.email)
end end
@ -110,7 +110,7 @@ module Invidious::Database::Users
SET feed_needs_update = true, SET feed_needs_update = true,
subscriptions = array_remove(subscriptions, $1) subscriptions = array_remove(subscriptions, $1)
WHERE email = $2 WHERE email = $2
SQL SQL
PG_DB.exec(request, ucid, user.email) PG_DB.exec(request, ucid, user.email)
end end
@ -125,7 +125,7 @@ module Invidious::Database::Users
SET notifications = array_cat(notifications, $1), SET notifications = array_cat(notifications, $1),
feed_needs_update = true feed_needs_update = true
WHERE $2 = ANY(subscriptions) WHERE $2 = ANY(subscriptions)
SQL SQL
PG_DB.exec(request, video_ids, channel_id) PG_DB.exec(request, video_ids, channel_id)
end end
@ -135,7 +135,7 @@ module Invidious::Database::Users
UPDATE users UPDATE users
SET notifications = array_remove(notifications, $1) SET notifications = array_remove(notifications, $1)
WHERE email = $2 WHERE email = $2
SQL SQL
PG_DB.exec(request, vid, user.email) PG_DB.exec(request, vid, user.email)
end end
@ -145,7 +145,7 @@ module Invidious::Database::Users
UPDATE users UPDATE users
SET notifications = '{}', updated = now() SET notifications = '{}', updated = now()
WHERE email = $1 WHERE email = $1
SQL SQL
PG_DB.exec(request, user.email) PG_DB.exec(request, user.email)
end end
@ -159,7 +159,7 @@ module Invidious::Database::Users
UPDATE users UPDATE users
SET feed_needs_update = true SET feed_needs_update = true
WHERE $1 = ANY(subscriptions) WHERE $1 = ANY(subscriptions)
SQL SQL
PG_DB.exec(request, channel_id) PG_DB.exec(request, channel_id)
end end
@ -169,7 +169,7 @@ module Invidious::Database::Users
UPDATE users UPDATE users
SET preferences = $1 SET preferences = $1
WHERE email = $2 WHERE email = $2
SQL SQL
PG_DB.exec(request, user.preferences.to_json, user.email) PG_DB.exec(request, user.preferences.to_json, user.email)
end end
@ -179,7 +179,7 @@ module Invidious::Database::Users
UPDATE users UPDATE users
SET password = $1 SET password = $1
WHERE email = $2 WHERE email = $2
SQL SQL
PG_DB.exec(request, pass, user.email) PG_DB.exec(request, pass, user.email)
end end
@ -192,9 +192,9 @@ module Invidious::Database::Users
request = <<-SQL request = <<-SQL
SELECT * FROM users SELECT * FROM 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
@ -202,18 +202,18 @@ module Invidious::Database::Users
request = <<-SQL request = <<-SQL
SELECT * FROM users SELECT * FROM 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?
request = <<-SQL request = <<-SQL
SELECT * FROM users SELECT * FROM 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)
@ -221,8 +221,8 @@ module Invidious::Database::Users
SELECT notifications SELECT notifications
FROM users FROM 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

@ -8,7 +8,7 @@ module Invidious::Database::Videos
INSERT INTO videos INSERT INTO videos
VALUES ($1, $2, $3) VALUES ($1, $2, $3)
ON CONFLICT (id) DO NOTHING ON CONFLICT (id) DO NOTHING
SQL SQL
PG_DB.exec(request, video.id, video.info.to_json, video.updated) PG_DB.exec(request, video.id, video.info.to_json, video.updated)
end end
@ -17,7 +17,7 @@ module Invidious::Database::Videos
request = <<-SQL request = <<-SQL
DELETE FROM videos * DELETE FROM videos *
WHERE id = $1 WHERE id = $1
SQL SQL
PG_DB.exec(request, id) PG_DB.exec(request, id)
end end
@ -26,7 +26,7 @@ module Invidious::Database::Videos
request = <<-SQL request = <<-SQL
DELETE FROM videos * DELETE FROM videos *
WHERE updated < (now() - interval '6 hours') WHERE updated < (now() - interval '6 hours')
SQL SQL
PG_DB.exec(request) PG_DB.exec(request)
end end
@ -36,7 +36,7 @@ module Invidious::Database::Videos
UPDATE videos UPDATE videos
SET (id, info, updated) = ($1, $2, $3) SET (id, info, updated) = ($1, $2, $3)
WHERE id = $1 WHERE id = $1
SQL SQL
PG_DB.exec(request, video.id, video.info.to_json, video.updated) PG_DB.exec(request, video.id, video.info.to_json, video.updated)
end end
@ -45,8 +45,8 @@ module Invidious::Database::Videos
request = <<-SQL request = <<-SQL
SELECT * FROM videos SELECT * FROM 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,38 +11,38 @@ 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>
#{translate_count(locale, "comments_points_count", child.score, NumberFormatting::Separator)} #{translate_count(locale, "comments_points_count", child.score, NumberFormatting::Separator)}
<span title="#{child.created_utc.to_s("%a %B %-d %T %Y UTC")}">#{translate(locale, "`x` ago", recode_date(child.created_utc, locale))}</span> <span title="#{child.created_utc.to_s("%a %B %-d %T %Y UTC")}">#{translate(locale, "`x` ago", recode_date(child.created_utc, locale))}</span>
<a href="https://www.reddit.com#{child.permalink}" title="#{translate(locale, "permalink")}">#{translate(locale, "permalink")}</a> <a href="https://www.reddit.com#{child.permalink}" title="#{translate(locale, "permalink")}">#{translate(locale, "permalink")}</a>
</p> </p>
<div> <div>
#{body_html} #{body_html}
#{replies_html} #{replies_html}
</div> </div>
</div> </div>
</div> </div>
END_HTML HTML
end end
end end
end end

View File

@ -12,17 +12,17 @@ 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">
<p> <p>
<a href="javascript:void(0)" data-continuation="#{child["replies"]["continuation"]}" <a href="javascript:void(0)" data-continuation="#{child["replies"]["continuation"]}"
data-onclick="get_youtube_replies" data-load-replies>#{replies_count_text}</a> data-onclick="get_youtube_replies" data-load-replies>#{replies_count_text}</a>
</p> </p>
</div>
</div> </div>
</div> HTML
END_HTML
elsif comments["authorId"]? && !comments["singlePost"]? elsif comments["authorId"]? && !comments["singlePost"]?
# for posts we should display a link to the post # for posts we should display a link to the post
replies_count_text = translate_count(locale, replies_count_text = translate_count(locale,
@ -31,16 +31,16 @@ 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">
<p> <p>
<a href="/post/#{child["commentId"]}?ucid=#{comments["authorId"]}">#{replies_count_text}</a> <a href="/post/#{child["commentId"]}?ucid=#{comments["authorId"]}">#{replies_count_text}</a>
</p> </p>
</div>
</div> </div>
</div> HTML
END_HTML
end end
if !thin_mode if !thin_mode
@ -65,19 +65,19 @@ 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="" />
</div> </div>
<div class="pure-u-20-24 pure-u-md-22-24"> <div class="pure-u-20-24 pure-u-md-22-24">
<p> <p>
<b> <b>
<a class="#{child["authorIsChannelOwner"] == true ? "channel-owner" : ""}" href="#{child["authorUrl"]}">#{author_name}</a> <a class="#{child["authorIsChannelOwner"] == true ? "channel-owner" : ""}" href="#{child["authorUrl"]}">#{author_name}</a>
</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> HTML
END_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,28 +179,28 @@ 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> HTML
END_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>
<a href="javascript:void(0)" data-continuation="#{comments["continuation"]}" <a href="javascript:void(0)" data-continuation="#{comments["continuation"]}"
data-onclick="get_youtube_replies" data-load-more #{"data-load-replies" if is_replies}>#{translate(locale, "Load more")}</a> data-onclick="get_youtube_replies" data-load-more #{"data-load-replies" if is_replies}>#{translate(locale, "Load more")}</a>
</p> </p>
</div>
</div> </div>
</div> HTML
END_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"
@ -41,11 +41,11 @@ module Invidious::Frontend::SearchFilters
str << "\t\t\t\t<div class=\"filter-column\"><fieldset>\n" str << "\t\t\t\t<div class=\"filter-column\"><fieldset>\n"
str << "\t\t\t\t\t<legend><div class=\"filter-name underlined\">" str << "\t\t\t\t\t<legend><div class=\"filter-name underlined\">"
str << translate(locale, "search_filters_{{name}}_label") str << translate(locale, "search_filters_{{ name }}_label")
str << "</div></legend>\n" str << "</div></legend>\n"
str << "\t\t\t\t\t<div class=\"filter-options\">\n" str << "\t\t\t\t\t<div class=\"filter-options\">\n"
make_{{name}}_filter_options(str, filters.{{name}}, locale) make_{{ name }}_filter_options(str, filters.{{ name }}, locale)
str << "\t\t\t\t\t</div>" str << "\t\t\t\t\t</div>"
str << "\t\t\t\t</fieldset></div>\n" str << "\t\t\t\t</fieldset></div>\n"
@ -57,12 +57,12 @@ module Invidious::Frontend::SearchFilters
{% date = value.underscore %} {% date = value.underscore %}
str << "\t\t\t\t\t\t<div>" str << "\t\t\t\t\t\t<div>"
str << "<input type='radio' name='date' id='filter-date-{{date}}' value='{{date}}'" str << "<input type='radio' name='date' id='filter-date-{{ date }}' value='{{ date }}'"
str << " checked" if value.{{date}}? str << " checked" if value.{{ date }}?
str << '>' str << '>'
str << "<label for='filter-date-{{date}}'>" str << "<label for='filter-date-{{ date }}'>"
str << translate(locale, "search_filters_date_option_{{date}}") str << translate(locale, "search_filters_date_option_{{ date }}")
str << "</label></div>\n" str << "</label></div>\n"
{% end %} {% end %}
end end
@ -73,12 +73,12 @@ module Invidious::Frontend::SearchFilters
{% type = value.underscore %} {% type = value.underscore %}
str << "\t\t\t\t\t\t<div>" str << "\t\t\t\t\t\t<div>"
str << "<input type='radio' name='type' id='filter-type-{{type}}' value='{{type}}'" str << "<input type='radio' name='type' id='filter-type-{{ type }}' value='{{ type }}'"
str << " checked" if value.{{type}}? str << " checked" if value.{{ type }}?
str << '>' str << '>'
str << "<label for='filter-type-{{type}}'>" str << "<label for='filter-type-{{ type }}'>"
str << translate(locale, "search_filters_type_option_{{type}}") str << translate(locale, "search_filters_type_option_{{ type }}")
str << "</label></div>\n" str << "</label></div>\n"
{% end %} {% end %}
end end
@ -89,12 +89,12 @@ module Invidious::Frontend::SearchFilters
{% duration = value.underscore %} {% duration = value.underscore %}
str << "\t\t\t\t\t\t<div>" str << "\t\t\t\t\t\t<div>"
str << "<input type='radio' name='duration' id='filter-duration-{{duration}}' value='{{duration}}'" str << "<input type='radio' name='duration' id='filter-duration-{{ duration }}' value='{{ duration }}'"
str << " checked" if value.{{duration}}? str << " checked" if value.{{ duration }}?
str << '>' str << '>'
str << "<label for='filter-duration-{{duration}}'>" str << "<label for='filter-duration-{{ duration }}'>"
str << translate(locale, "search_filters_duration_option_{{duration}}") str << translate(locale, "search_filters_duration_option_{{ duration }}")
str << "</label></div>\n" str << "</label></div>\n"
{% end %} {% end %}
end end
@ -106,12 +106,12 @@ module Invidious::Frontend::SearchFilters
{% feature = value.underscore %} {% feature = value.underscore %}
str << "\t\t\t\t\t\t<div>" str << "\t\t\t\t\t\t<div>"
str << "<input type='checkbox' name='features' id='filter-feature-{{feature}}' value='{{feature}}'" str << "<input type='checkbox' name='features' id='filter-feature-{{ feature }}' value='{{ feature }}'"
str << " checked" if value.{{feature}}? str << " checked" if value.{{ feature }}?
str << '>' str << '>'
str << "<label for='filter-feature-{{feature}}'>" str << "<label for='filter-feature-{{ feature }}'>"
str << translate(locale, "search_filters_features_option_{{feature}}") str << translate(locale, "search_filters_features_option_{{ feature }}")
str << "</label></div>\n" str << "</label></div>\n"
{% end %} {% end %}
{% end %} {% end %}
@ -123,12 +123,12 @@ module Invidious::Frontend::SearchFilters
{% sort = value.underscore %} {% sort = value.underscore %}
str << "\t\t\t\t\t\t<div>" str << "\t\t\t\t\t\t<div>"
str << "<input type='radio' name='sort' id='filter-sort-{{sort}}' value='{{sort}}'" str << "<input type='radio' name='sort' id='filter-sort-{{ sort }}' value='{{ sort }}'"
str << " checked" if value.{{sort}}? str << " checked" if value.{{ sort }}?
str << '>' str << '>'
str << "<label for='filter-sort-{{sort}}'>" str << "<label for='filter-sort-{{ sort }}'>"
str << translate(locale, "search_filters_sort_option_{{sort}}") str << translate(locale, "search_filters_sort_option_{{ sort }}")
str << "</label></div>\n" str << "</label></div>\n"
{% end %} {% end %}
end end

View File

@ -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

@ -3,7 +3,7 @@
# ------------------- # -------------------
macro error_template(*args) macro error_template(*args)
error_template_helper(env, {{args.splat}}) error_template_helper(env, {{ args.splat }})
end end
def github_details(summary : String, content : String) def github_details(summary : String, content : String)
@ -15,19 +15,19 @@ 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)
issue_title = "#{exception.message} (#{exception.class})" issue_title = "#{exception.message} (#{exception.class})"
issue_template = <<-TEXT issue_template = <<-TEXT
Title: `#{HTML.escape(issue_title)}` Title: `#{HTML.escape(issue_title)}`
Date: `#{Time::Format::ISO_8601_DATE_TIME.format(Time.utc)}` Date: `#{Time::Format::ISO_8601_DATE_TIME.format(Time.utc)}`
Route: `#{HTML.escape(env.request.resource)}` Route: `#{HTML.escape(env.request.resource)}`
Version: `#{SOFTWARE["version"]} @ #{SOFTWARE["branch"]}` Version: `#{SOFTWARE["version"]} @ #{SOFTWARE["branch"]}`
TEXT TEXT
issue_template += github_details("Backtrace", exception.inspect_with_backtrace) issue_template += github_details("Backtrace", exception.inspect_with_backtrace)
@ -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
# ------------------- # -------------------
@ -106,7 +110,7 @@ end
# ------------------- # -------------------
macro error_atom(*args) macro error_atom(*args)
error_atom_helper(env, {{args.splat}}) error_atom_helper(env, {{ args.splat }})
end end
def error_atom_helper(env : HTTP::Server::Context, status_code : Int32, exception : Exception) def error_atom_helper(env : HTTP::Server::Context, status_code : Int32, exception : Exception)
@ -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
# ------------------- # -------------------
@ -132,14 +136,14 @@ end
# ------------------- # -------------------
macro error_json(*args) macro error_json(*args)
error_json_helper(env, {{args.splat}}) error_json_helper(env, {{ args.splat }})
end end
def error_json_helper( 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

@ -2,10 +2,10 @@ module HTTP::Handler
@@exclude_routes_tree = Radix::Tree(String).new @@exclude_routes_tree = Radix::Tree(String).new
macro exclude(paths, method = "GET") macro exclude(paths, method = "GET")
class_name = {{@type.name}} class_name = {{ @type.name }}
method_downcase = {{method.downcase}} method_downcase = {{ method.downcase }}
class_name_method = "#{class_name}/#{method_downcase}" class_name_method = "#{class_name}/#{method_downcase}"
({{paths}}).each do |path| ({{ paths }}).each do |path|
@@exclude_routes_tree.add class_name_method + path, '/' + method_downcase + path @@exclude_routes_tree.add class_name_method + path, '/' + method_downcase + path
end end
end end
@ -20,8 +20,8 @@ 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 %}
# Processes the route if it's a match. Otherwise renders 404. # Processes the route if it's a match. Otherwise renders 404.
@ -44,8 +44,8 @@ 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 %}
private def call_exception_with_status_code(context : HTTP::Server::Context, exception : Exception, status_code : Int32) private def call_exception_with_status_code(context : HTTP::Server::Context, exception : Exception, status_code : Int32)
@ -72,8 +72,8 @@ 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 %}
def call(env) def call(env)
@ -121,8 +121,8 @@ 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"
exclude ["/api/v1/auth/notifications"], "POST" exclude ["/api/v1/auth/notifications"], "POST"

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,10 +55,10 @@ 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 }})))
end end
end end
{% end %} {% end %}

View File

@ -49,24 +49,24 @@ module JSON::Serializable
end end
macro templated(_filename, template = "template", navbar_search = true) macro templated(_filename, template = "template", navbar_search = true)
navbar_search = {{navbar_search}} navbar_search = {{ navbar_search }}
{{ filename = "src/invidious/views/" + _filename + ".ecr" }} {{ filename = "src/invidious/views/" + _filename + ".ecr" }}
{{ layout = "src/invidious/views/" + template + ".ecr" }} {{ layout = "src/invidious/views/" + template + ".ecr" }}
__content_filename__ = {{filename}} __content_filename__ = {{ filename }}
render {{filename}}, {{layout}} render {{ filename }}, {{ layout }}
end end
macro rendered(filename) macro rendered(filename)
render("src/invidious/views/#{{{filename}}}.ecr") render("src/invidious/views/#{{{ filename }}}.ecr")
end end
# Similar to Kemals halt method but works in a # Similar to Kemals halt method but works in a
# method. # method.
macro haltf(env, status_code = 200, response = "") macro haltf(env, status_code = 200, response = "")
{{env}}.response.status_code = {{status_code}} {{ env }}.response.status_code = {{ status_code }}
{{env}}.response.print {{response}} {{ env }}.response.print {{ response }}
{{env}}.response.close {{ env }}.response.close
return return
end end

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
@ -106,7 +106,7 @@ module Invidious::HttpServer
# Can be removed once https://github.com/crystal-lang/crystal/issues/15817 is fixed. # Can be removed once https://github.com/crystal-lang/crystal/issues/15817 is fixed.
private def serve_file_range(context : HTTP::Server::Context, file : IO, range_header : String, file_info) private def serve_file_range(context : HTTP::Server::Context, file : IO, range_header : String, file_info)
# Paste in the body of inherited serve_file_range # Paste in the body of inherited serve_file_range
{{@type.superclass.methods.select(&.name.==("serve_file_range"))[0].body}} {{ @type.superclass.methods.select(&.name.==("serve_file_range"))[0].body }}
end end
# Clear cached files. # Clear cached files.
@ -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,18 +82,18 @@ 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"]}
</a> </a>
</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,22 +504,22 @@ 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"]}
</a> </a>
</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

@ -3,12 +3,12 @@ module Invidious::Routing
{% for http_method in {"get", "post", "delete", "options", "patch", "put"} %} {% for http_method in {"get", "post", "delete", "options", "patch", "put"} %}
macro {{http_method.id}}(path, controller, method = :handle) macro {{ http_method.id }}(path, controller, method = :handle)
unless Kemal::Utils.path_starts_with_slash?(\{{path}}) unless Kemal::Utils.path_starts_with_slash?(\{{path}})
raise Kemal::Exceptions::InvalidPathStartException.new({{http_method}}, \{{path}}) raise Kemal::Exceptions::InvalidPathStartException.new({{ http_method }}, \{{path}})
end end
Kemal::RouteHandler::INSTANCE.add_route({{http_method.upcase}}, \{{path}}) do |env| Kemal::RouteHandler::INSTANCE.add_route({{ http_method.upcase }}, \{{path}}) do |env|
\{{ controller }}.\{{ method.id }}(env) \{{ controller }}.\{{ method.id }}(env)
end end
end end
@ -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
# ------------------- # -------------------
@ -238,91 +238,91 @@ module Invidious::Routing
def register_api_v1_routes def register_api_v1_routes
{% begin %} {% begin %}
{{namespace = Routes::API::V1}} {{ namespace = Routes::API::V1 }}
# Videos # Videos
get "/api/v1/videos/:id", {{namespace}}::Videos, :videos get "/api/v1/videos/:id", {{ namespace }}::Videos, :videos
get "/api/v1/storyboards/:id", {{namespace}}::Videos, :storyboards get "/api/v1/storyboards/:id", {{ namespace }}::Videos, :storyboards
get "/api/v1/captions/:id", {{namespace}}::Videos, :captions get "/api/v1/captions/:id", {{ namespace }}::Videos, :captions
get "/api/v1/annotations/:id", {{namespace}}::Videos, :annotations get "/api/v1/annotations/:id", {{ namespace }}::Videos, :annotations
get "/api/v1/comments/:id", {{namespace}}::Videos, :comments get "/api/v1/comments/:id", {{ namespace }}::Videos, :comments
get "/api/v1/clips/:id", {{namespace}}::Videos, :clips get "/api/v1/clips/:id", {{ namespace }}::Videos, :clips
get "/api/v1/transcripts/:id", {{namespace}}::Videos, :transcripts get "/api/v1/transcripts/:id", {{ namespace }}::Videos, :transcripts
# Feeds # Feeds
get "/api/v1/trending", {{namespace}}::Feeds, :trending get "/api/v1/trending", {{ namespace }}::Feeds, :trending
get "/api/v1/popular", {{namespace}}::Feeds, :popular get "/api/v1/popular", {{ namespace }}::Feeds, :popular
# Channels # Channels
get "/api/v1/channels/:ucid", {{namespace}}::Channels, :home get "/api/v1/channels/:ucid", {{ namespace }}::Channels, :home
get "/api/v1/channels/:ucid/latest", {{namespace}}::Channels, :latest get "/api/v1/channels/:ucid/latest", {{ namespace }}::Channels, :latest
get "/api/v1/channels/:ucid/videos", {{namespace}}::Channels, :videos get "/api/v1/channels/:ucid/videos", {{ namespace }}::Channels, :videos
get "/api/v1/channels/:ucid/shorts", {{namespace}}::Channels, :shorts get "/api/v1/channels/:ucid/shorts", {{ namespace }}::Channels, :shorts
get "/api/v1/channels/:ucid/streams", {{namespace}}::Channels, :streams get "/api/v1/channels/:ucid/streams", {{ namespace }}::Channels, :streams
get "/api/v1/channels/:ucid/podcasts", {{namespace}}::Channels, :podcasts get "/api/v1/channels/:ucid/podcasts", {{ namespace }}::Channels, :podcasts
get "/api/v1/channels/:ucid/releases", {{namespace}}::Channels, :releases get "/api/v1/channels/:ucid/releases", {{ namespace }}::Channels, :releases
get "/api/v1/channels/:ucid/courses", {{namespace}}::Channels, :courses get "/api/v1/channels/:ucid/courses", {{ namespace }}::Channels, :courses
get "/api/v1/channels/:ucid/playlists", {{namespace}}::Channels, :playlists get "/api/v1/channels/:ucid/playlists", {{ namespace }}::Channels, :playlists
get "/api/v1/channels/:ucid/community", {{namespace}}::Channels, :community get "/api/v1/channels/:ucid/community", {{ namespace }}::Channels, :community
get "/api/v1/channels/:ucid/posts", {{namespace}}::Channels, :community get "/api/v1/channels/:ucid/posts", {{ namespace }}::Channels, :community
get "/api/v1/channels/:ucid/channels", {{namespace}}::Channels, :channels get "/api/v1/channels/:ucid/channels", {{ namespace }}::Channels, :channels
get "/api/v1/channels/:ucid/search", {{namespace}}::Channels, :search get "/api/v1/channels/:ucid/search", {{ namespace }}::Channels, :search
# Posts # Posts
get "/api/v1/post/:id", {{namespace}}::Channels, :post get "/api/v1/post/:id", {{ namespace }}::Channels, :post
get "/api/v1/post/:id/comments", {{namespace}}::Channels, :post_comments get "/api/v1/post/:id/comments", {{ namespace }}::Channels, :post_comments
# 301 redirects to new /api/v1/channels/community/:ucid and /:ucid/community # 301 redirects to new /api/v1/channels/community/:ucid and /:ucid/community
get "/api/v1/channels/comments/:ucid", {{namespace}}::Channels, :channel_comments_redirect get "/api/v1/channels/comments/:ucid", {{ namespace }}::Channels, :channel_comments_redirect
get "/api/v1/channels/:ucid/comments", {{namespace}}::Channels, :channel_comments_redirect get "/api/v1/channels/:ucid/comments", {{ namespace }}::Channels, :channel_comments_redirect
# Search # Search
get "/api/v1/search", {{namespace}}::Search, :search get "/api/v1/search", {{ namespace }}::Search, :search
get "/api/v1/search/suggestions", {{namespace}}::Search, :search_suggestions get "/api/v1/search/suggestions", {{ namespace }}::Search, :search_suggestions
get "/api/v1/hashtag/:hashtag", {{namespace}}::Search, :hashtag get "/api/v1/hashtag/:hashtag", {{ namespace }}::Search, :hashtag
# Authenticated # Authenticated
get "/api/v1/auth/preferences", {{namespace}}::Authenticated, :get_preferences get "/api/v1/auth/preferences", {{ namespace }}::Authenticated, :get_preferences
post "/api/v1/auth/preferences", {{namespace}}::Authenticated, :set_preferences post "/api/v1/auth/preferences", {{ namespace }}::Authenticated, :set_preferences
get "/api/v1/auth/export/invidious", {{namespace}}::Authenticated, :export_invidious get "/api/v1/auth/export/invidious", {{ namespace }}::Authenticated, :export_invidious
post "/api/v1/auth/import/invidious", {{namespace}}::Authenticated, :import_invidious post "/api/v1/auth/import/invidious", {{ namespace }}::Authenticated, :import_invidious
get "/api/v1/auth/history", {{namespace}}::Authenticated, :get_history get "/api/v1/auth/history", {{ namespace }}::Authenticated, :get_history
post "/api/v1/auth/history/:id", {{namespace}}::Authenticated, :mark_watched post "/api/v1/auth/history/:id", {{ namespace }}::Authenticated, :mark_watched
delete "/api/v1/auth/history/:id", {{namespace}}::Authenticated, :mark_unwatched delete "/api/v1/auth/history/:id", {{ namespace }}::Authenticated, :mark_unwatched
delete "/api/v1/auth/history", {{namespace}}::Authenticated, :clear_history delete "/api/v1/auth/history", {{ namespace }}::Authenticated, :clear_history
get "/api/v1/auth/feed", {{namespace}}::Authenticated, :feed get "/api/v1/auth/feed", {{ namespace }}::Authenticated, :feed
get "/api/v1/auth/subscriptions", {{namespace}}::Authenticated, :get_subscriptions get "/api/v1/auth/subscriptions", {{ namespace }}::Authenticated, :get_subscriptions
post "/api/v1/auth/subscriptions/:ucid", {{namespace}}::Authenticated, :subscribe_channel post "/api/v1/auth/subscriptions/:ucid", {{ namespace }}::Authenticated, :subscribe_channel
delete "/api/v1/auth/subscriptions/:ucid", {{namespace}}::Authenticated, :unsubscribe_channel delete "/api/v1/auth/subscriptions/:ucid", {{ namespace }}::Authenticated, :unsubscribe_channel
get "/api/v1/auth/playlists", {{namespace}}::Authenticated, :list_playlists get "/api/v1/auth/playlists", {{ namespace }}::Authenticated, :list_playlists
post "/api/v1/auth/playlists", {{namespace}}::Authenticated, :create_playlist post "/api/v1/auth/playlists", {{ namespace }}::Authenticated, :create_playlist
patch "/api/v1/auth/playlists/:plid",{{namespace}}:: Authenticated, :update_playlist_attribute patch "/api/v1/auth/playlists/:plid",{{ namespace }}:: Authenticated, :update_playlist_attribute
delete "/api/v1/auth/playlists/:plid", {{namespace}}::Authenticated, :delete_playlist delete "/api/v1/auth/playlists/:plid", {{ namespace }}::Authenticated, :delete_playlist
post "/api/v1/auth/playlists/:plid/videos", {{namespace}}::Authenticated, :insert_video_into_playlist post "/api/v1/auth/playlists/:plid/videos", {{ namespace }}::Authenticated, :insert_video_into_playlist
delete "/api/v1/auth/playlists/:plid/videos/:index", {{namespace}}::Authenticated, :delete_video_in_playlist delete "/api/v1/auth/playlists/:plid/videos/:index", {{ namespace }}::Authenticated, :delete_video_in_playlist
get "/api/v1/auth/tokens", {{namespace}}::Authenticated, :get_tokens get "/api/v1/auth/tokens", {{ namespace }}::Authenticated, :get_tokens
post "/api/v1/auth/tokens/register", {{namespace}}::Authenticated, :register_token post "/api/v1/auth/tokens/register", {{ namespace }}::Authenticated, :register_token
post "/api/v1/auth/tokens/unregister", {{namespace}}::Authenticated, :unregister_token post "/api/v1/auth/tokens/unregister", {{ namespace }}::Authenticated, :unregister_token
if CONFIG.enable_user_notifications if CONFIG.enable_user_notifications
get "/api/v1/auth/notifications", {{namespace}}::Authenticated, :notifications get "/api/v1/auth/notifications", {{ namespace }}::Authenticated, :notifications
post "/api/v1/auth/notifications", {{namespace}}::Authenticated, :notifications post "/api/v1/auth/notifications", {{ namespace }}::Authenticated, :notifications
end end
# Misc # Misc
get "/api/v1/stats", {{namespace}}::Misc, :stats get "/api/v1/stats", {{ namespace }}::Misc, :stats
get "/api/v1/playlists/:plid", {{namespace}}::Misc, :get_playlist get "/api/v1/playlists/:plid", {{ namespace }}::Misc, :get_playlist
get "/api/v1/auth/playlists/:plid", {{namespace}}::Misc, :get_playlist get "/api/v1/auth/playlists/:plid", {{ namespace }}::Misc, :get_playlist
get "/api/v1/mixes/:rdid", {{namespace}}::Misc, :mixes get "/api/v1/mixes/:rdid", {{ namespace }}::Misc, :mixes
get "/api/v1/resolveurl", {{namespace}}::Misc, :resolve_url get "/api/v1/resolveurl", {{ namespace }}::Misc, :resolve_url
{% end %} {% end %}
end end
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,29 +19,29 @@ 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>
<text x="69" y="20.091" text-anchor="middle" fill="black" font-family="Arial" font-size="10px"> 1</text> <text x="69" y="20.091" text-anchor="middle" fill="black" font-family="Arial" font-size="10px"> 1</text>
<text x="82.909" y="34" text-anchor="middle" fill="black" font-family="Arial" font-size="10px"> 2</text> <text x="82.909" y="34" text-anchor="middle" fill="black" font-family="Arial" font-size="10px"> 2</text>
<text x="88" y="53" text-anchor="middle" fill="black" font-family="Arial" font-size="10px"> 3</text> <text x="88" y="53" text-anchor="middle" fill="black" font-family="Arial" font-size="10px"> 3</text>
<text x="82.909" y="72" text-anchor="middle" fill="black" font-family="Arial" font-size="10px"> 4</text> <text x="82.909" y="72" text-anchor="middle" fill="black" font-family="Arial" font-size="10px"> 4</text>
<text x="69" y="85.909" text-anchor="middle" fill="black" font-family="Arial" font-size="10px"> 5</text> <text x="69" y="85.909" text-anchor="middle" fill="black" font-family="Arial" font-size="10px"> 5</text>
<text x="50" y="91" text-anchor="middle" fill="black" font-family="Arial" font-size="10px"> 6</text> <text x="50" y="91" text-anchor="middle" fill="black" font-family="Arial" font-size="10px"> 6</text>
<text x="31" y="85.909" text-anchor="middle" fill="black" font-family="Arial" font-size="10px"> 7</text> <text x="31" y="85.909" text-anchor="middle" fill="black" font-family="Arial" font-size="10px"> 7</text>
<text x="17.091" y="72" text-anchor="middle" fill="black" font-family="Arial" font-size="10px"> 8</text> <text x="17.091" y="72" text-anchor="middle" fill="black" font-family="Arial" font-size="10px"> 8</text>
<text x="12" y="53" text-anchor="middle" fill="black" font-family="Arial" font-size="10px"> 9</text> <text x="12" y="53" text-anchor="middle" fill="black" font-family="Arial" font-size="10px"> 9</text>
<text x="17.091" y="34" text-anchor="middle" fill="black" font-family="Arial" font-size="10px">10</text> <text x="17.091" y="34" text-anchor="middle" fill="black" font-family="Arial" font-size="10px">10</text>
<text x="31" y="20.091" text-anchor="middle" fill="black" font-family="Arial" font-size="10px">11</text> <text x="31" y="20.091" text-anchor="middle" fill="black" font-family="Arial" font-size="10px">11</text>
<text x="50" y="15" text-anchor="middle" fill="black" font-family="Arial" font-size="10px">12</text> <text x="50" y="15" text-anchor="middle" fill="black" font-family="Arial" font-size="10px">12</text>
<circle cx="50" cy="50" r="3" fill="black"></circle> <circle cx="50" cy="50" r="3" fill="black"></circle>
<line id="second" transform="rotate(#{second_angle}, 50, 50)" x1="50" y1="50" x2="50" y2="12" fill="black" stroke="black" stroke-width="1"></line> <line id="second" transform="rotate(#{second_angle}, 50, 50)" x1="50" y1="50" x2="50" y2="12" fill="black" stroke="black" stroke-width="1"></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="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,20 +64,18 @@ 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?
CONFIG.default_user_preferences.dark_mode CONFIG.default_user_preferences.dark_mode
else else
result result
end end
rescue ex rescue ex
if value.read_bool if value.read_bool
"dark" "dark"
else else
"light" "light"
end
end end
end end
@ -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,50 +182,50 @@ 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
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`
def {{name.id.underscore}}=(value : String) def {{ name.id.underscore }}=(value : String)
info[{{name.stringify}}] = JSON::Any.new(value) info[{{ name.stringify }}] = JSON::Any.new(value)
end end
{% if flag?(:debug_macros) %} {{debug}} {% end %} {% if flag?(:debug_macros) %} {{ debug }} {% end %}
end end
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`
def {{name.id.underscore}}=(value : Array(String)) def {{ name.id.underscore }}=(value : Array(String))
info[{{name.stringify}}] = JSON::Any.new(value) info[{{ name.stringify }}] = JSON::Any.new(value)
end end
{% if flag?(:debug_macros) %} {{debug}} {% end %} {% if flag?(:debug_macros) %} {{ debug }} {% end %}
end end
{% 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)
@ -237,32 +237,32 @@ struct Video
{% end %} {% end %}
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`
def {{name.id.underscore}}=(value : Bool) def {{ name.id.underscore }}=(value : Bool)
info[{{name.stringify}}] = JSON::Any.new(value) info[{{ name.stringify }}] = JSON::Any.new(value)
end end
{% if flag?(:debug_macros) %} {{debug}} {% end %} {% if flag?(:debug_macros) %} {{ debug }} {% end %}
end end
# Macro to generate ? and = accessor methods for attributes in `info` # Macro to generate ? and = accessor methods for attributes in `info`
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`
def {{method_name.id.underscore}}=(value : Bool) def {{ method_name.id.underscore }}=(value : Bool)
info[{{name.stringify}}] = JSON::Any.new(value) info[{{ name.stringify }}] = JSON::Any.new(value)
end end
{% if flag?(:debug_macros) %} {{debug}} {% end %} {% if flag?(:debug_macros) %} {{ debug }} {% end %}
end end
# Method definitions, using the macros above # Method definitions, using the macros above
@ -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 %>">

Some files were not shown because too many files have changed in this diff Show More