summaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/invidious.cr51
-rw-r--r--src/invidious/channels.cr175
-rw-r--r--src/invidious/helpers/helpers.cr3
-rw-r--r--src/invidious/helpers/i18n.cr30
-rw-r--r--src/invidious/helpers/macros.cr3
-rw-r--r--src/invidious/helpers/utils.cr20
-rw-r--r--src/invidious/jobs/statistics_refresh_job.cr2
-rw-r--r--src/invidious/playlists.cr3
-rw-r--r--src/invidious/routes/misc.cr2
-rw-r--r--src/invidious/routes/playlists.cr2
-rw-r--r--src/invidious/routes/watch.cr9
-rw-r--r--src/invidious/search.cr51
-rw-r--r--src/invidious/trending.cr24
-rw-r--r--src/invidious/users.cr2
-rw-r--r--src/invidious/videos.cr18
-rw-r--r--src/invidious/views/empty.ecr8
-rw-r--r--src/invidious/views/preferences.ecr4
-rw-r--r--src/invidious/views/search_homepage.ecr24
-rw-r--r--src/invidious/views/template.ecr29
-rw-r--r--src/invidious/views/trending.ecr2
20 files changed, 273 insertions, 189 deletions
diff --git a/src/invidious.cr b/src/invidious.cr
index 8d579f92..ae20e13e 100644
--- a/src/invidious.cr
+++ b/src/invidious.cr
@@ -65,37 +65,7 @@ SOFTWARE = {
"branch" => "#{CURRENT_BRANCH}",
}
-LOCALES = {
- "ar" => load_locale("ar"),
- "de" => load_locale("de"),
- "el" => load_locale("el"),
- "en-US" => load_locale("en-US"),
- "eo" => load_locale("eo"),
- "es" => load_locale("es"),
- "fa" => load_locale("fa"),
- "fi" => load_locale("fi"),
- "fr" => load_locale("fr"),
- "he" => load_locale("he"),
- "hr" => load_locale("hr"),
- "id" => load_locale("id"),
- "is" => load_locale("is"),
- "it" => load_locale("it"),
- "ja" => load_locale("ja"),
- "nb-NO" => load_locale("nb-NO"),
- "nl" => load_locale("nl"),
- "pl" => load_locale("pl"),
- "pt-BR" => load_locale("pt-BR"),
- "pt-PT" => load_locale("pt-PT"),
- "ro" => load_locale("ro"),
- "ru" => load_locale("ru"),
- "sv" => load_locale("sv-SE"),
- "tr" => load_locale("tr"),
- "uk" => load_locale("uk"),
- "zh-CN" => load_locale("zh-CN"),
- "zh-TW" => load_locale("zh-TW"),
-}
-
-YT_POOL = QUICPool.new(YT_URL, capacity: CONFIG.pool_size, timeout: 2.0)
+YT_POOL = YoutubeConnectionPool.new(YT_URL, capacity: CONFIG.pool_size, timeout: 2.0, use_quic: CONFIG.use_quic)
# CLI
Kemal.config.extra_options do |parser|
@@ -308,9 +278,17 @@ end
Invidious::Routing.get "/", Invidious::Routes::Misc, :home
Invidious::Routing.get "/privacy", Invidious::Routes::Misc, :privacy
Invidious::Routing.get "/licenses", Invidious::Routes::Misc, :licenses
-Invidious::Routing.get "/watch", Invidious::Routes::Watch
+
+Invidious::Routing.get "/watch", Invidious::Routes::Watch, :handle
+Invidious::Routing.get "/watch/:id", Invidious::Routes::Watch, :redirect
+Invidious::Routing.get "/shorts/:id", Invidious::Routes::Watch, :redirect
+Invidious::Routing.get "/w/:id", Invidious::Routes::Watch, :redirect
+Invidious::Routing.get "/v/:id", Invidious::Routes::Watch, :redirect
+Invidious::Routing.get "/e/:id", Invidious::Routes::Watch, :redirect
+
Invidious::Routing.get "/embed/", Invidious::Routes::Embed, :redirect
Invidious::Routing.get "/embed/:id", Invidious::Routes::Embed, :show
+
Invidious::Routing.get "/view_all_playlists", Invidious::Routes::Playlists, :index
Invidious::Routing.get "/create_playlist", Invidious::Routes::Playlists, :new
Invidious::Routing.post "/create_playlist", Invidious::Routes::Playlists, :create
@@ -323,12 +301,15 @@ Invidious::Routing.get "/add_playlist_items", Invidious::Routes::Playlists, :add
Invidious::Routing.post "/playlist_ajax", Invidious::Routes::Playlists, :playlist_ajax
Invidious::Routing.get "/playlist", Invidious::Routes::Playlists, :show
Invidious::Routing.get "/mix", Invidious::Routes::Playlists, :mix
+
Invidious::Routing.get "/opensearch.xml", Invidious::Routes::Search, :opensearch
Invidious::Routing.get "/results", Invidious::Routes::Search, :results
Invidious::Routing.get "/search", Invidious::Routes::Search, :search
+
Invidious::Routing.get "/login", Invidious::Routes::Login, :login_page
Invidious::Routing.post "/login", Invidious::Routes::Login, :login
Invidious::Routing.post "/signout", Invidious::Routes::Login, :signout
+
Invidious::Routing.get "/preferences", Invidious::Routes::PreferencesRoute, :show
Invidious::Routing.post "/preferences", Invidious::Routes::PreferencesRoute, :update
Invidious::Routing.get "/toggle_theme", Invidious::Routes::PreferencesRoute, :toggle_theme
@@ -1699,7 +1680,7 @@ get "/channel/:ucid" do |env|
sort_options = {"last", "oldest", "newest"}
sort_by ||= "last"
- items, continuation = fetch_channel_playlists(channel.ucid, channel.author, channel.auto_generated, continuation, sort_by)
+ items, continuation = fetch_channel_playlists(channel.ucid, channel.author, continuation, sort_by)
items.uniq! do |item|
if item.responds_to?(:title)
item.title
@@ -1766,7 +1747,7 @@ get "/channel/:ucid/playlists" do |env|
next env.redirect "/channel/#{channel.ucid}"
end
- items, continuation = fetch_channel_playlists(channel.ucid, channel.author, channel.auto_generated, continuation, sort_by)
+ items, continuation = fetch_channel_playlists(channel.ucid, channel.author, continuation, sort_by)
items = items.select { |item| item.is_a?(SearchPlaylist) }.map { |item| item.as(SearchPlaylist) }
items.each { |item| item.author = "" }
@@ -2467,7 +2448,7 @@ end
next error_json(500, ex)
end
- items, continuation = fetch_channel_playlists(channel.ucid, channel.author, channel.auto_generated, continuation, sort_by)
+ items, continuation = fetch_channel_playlists(channel.ucid, channel.author, continuation, sort_by)
JSON.build do |json|
json.object do
diff --git a/src/invidious/channels.cr b/src/invidious/channels.cr
index 9a129e1e..3109b508 100644
--- a/src/invidious/channels.cr
+++ b/src/invidious/channels.cr
@@ -355,14 +355,22 @@ def fetch_channel(ucid, db, pull_all_videos = true, locale = nil)
return channel
end
-def fetch_channel_playlists(ucid, author, auto_generated, continuation, sort_by)
- if continuation || auto_generated
- url = produce_channel_playlists_url(ucid, continuation, sort_by, auto_generated)
-
- response = YT_POOL.client &.get(url)
+def fetch_channel_playlists(ucid, author, continuation, sort_by)
+ if continuation
+ response_json = request_youtube_api_browse(continuation)
+ result = JSON.parse(response_json)
+ continuationItems = result["onResponseReceivedActions"]?
+ .try &.[0]["appendContinuationItemsAction"]["continuationItems"]
+
+ return [] of SearchItem, nil if !continuationItems
+
+ items = [] of SearchItem
+ continuationItems.as_a.select(&.as_h.has_key?("gridPlaylistRenderer")).each { |item|
+ extract_item(item, author, ucid).try { |t| items << t }
+ }
- continuation = response.body.match(/"continuation":"(?<continuation>[^"]+)"/).try &.["continuation"]?
- initial_data = JSON.parse(response.body).as_a.find(&.["response"]?).try &.as_h
+ continuation = continuationItems.as_a.last["continuationItemRenderer"]?
+ .try &.["continuationEndpoint"]["continuationCommand"]["token"].as_s
else
url = "/channel/#{ucid}/playlists?flow=list&view=1"
@@ -377,13 +385,12 @@ def fetch_channel_playlists(ucid, author, auto_generated, continuation, sort_by)
end
response = YT_POOL.client &.get(url)
- continuation = response.body.match(/"continuation":"(?<continuation>[^"]+)"/).try &.["continuation"]?
initial_data = extract_initial_data(response.body)
- end
+ return [] of SearchItem, nil if !initial_data
- return [] of SearchItem, nil if !initial_data
- items = extract_items(initial_data)
- continuation = extract_channel_playlists_cursor(continuation, auto_generated) if continuation
+ items = extract_items(initial_data, author, ucid)
+ continuation = response.body.match(/"token":"(?<continuation>[^"]+)"/).try &.["continuation"]?
+ end
return items, continuation
end
@@ -453,6 +460,15 @@ def produce_channel_videos_url(ucid, page = 1, auto_generated = nil, sort_by = "
return "/browse_ajax?continuation=#{continuation}&gl=US&hl=en"
end
+# ## NOTE: DEPRECATED
+# Reason -> Unstable
+# The Protobuf object must be provided with an id of the last playlist from the current "page"
+# in order to fetch the next one accurately
+# (if the id isn't included, entries shift around erratically between pages,
+# leading to repetitions and skip overs)
+#
+# Since it's impossible to produce the appropriate Protobuf without an id being provided by the user,
+# it's better to stick to continuation tokens provided by the first request and onward
def produce_channel_playlists_url(ucid, cursor, sort = "newest", auto_generated = false)
object = {
"80226972:embedded" => {
@@ -499,31 +515,6 @@ def produce_channel_playlists_url(ucid, cursor, sort = "newest", auto_generated
return "/browse_ajax?continuation=#{continuation}&gl=US&hl=en"
end
-def extract_channel_playlists_cursor(cursor, auto_generated)
- cursor = URI.decode_www_form(cursor)
- .try { |i| Base64.decode(i) }
- .try { |i| IO::Memory.new(i) }
- .try { |i| Protodec::Any.parse(i) }
- .try { |i| i["80226972:0:embedded"]["3:1:base64"].as_h.find { |k, v| k.starts_with? "15:" } }
- .try &.[1]
-
- if cursor.try &.as_h?
- cursor = cursor.try { |i| Protodec::Any.cast_json(i.as_h) }
- .try { |i| Protodec::Any.from_json(i) }
- .try { |i| Base64.urlsafe_encode(i) }
- .try { |i| URI.encode_www_form(i) } || ""
- else
- cursor = cursor.try &.as_s || ""
- end
-
- if !auto_generated
- cursor = URI.decode_www_form(cursor)
- .try { |i| Base64.decode_string(i) }
- end
-
- return cursor
-end
-
# TODO: Add "sort_by"
def fetch_channel_community(ucid, continuation, locale, format, thin_mode)
response = YT_POOL.client &.get("/channel/#{ucid}/community?gl=US&hl=en")
@@ -830,63 +821,87 @@ def get_about_info(ucid, locale)
raise ChannelRedirect.new(channel_id: browse_endpoint["browseId"].to_s)
end
- author = initdata["metadata"]["channelMetadataRenderer"]["title"].as_s
- author_url = initdata["metadata"]["channelMetadataRenderer"]["channelUrl"].as_s
- author_thumbnail = initdata["metadata"]["channelMetadataRenderer"]["avatar"]["thumbnails"][0]["url"].as_s
+ auto_generated = false
+ # Check for special auto generated gaming channels
+ if !initdata.has_key?("metadata")
+ auto_generated = true
+ end
+
+ if auto_generated
+ author = initdata["header"]["interactiveTabbedHeaderRenderer"]["title"]["simpleText"].as_s
+ author_url = initdata["microformat"]["microformatDataRenderer"]["urlCanonical"].as_s
+ author_thumbnail = initdata["header"]["interactiveTabbedHeaderRenderer"]["boxArt"]["thumbnails"][0]["url"].as_s
- ucid = initdata["metadata"]["channelMetadataRenderer"]["externalId"].as_s
+ # Raises a KeyError on failure.
+ banners = initdata["header"]["interactiveTabbedHeaderRenderer"]?.try &.["banner"]?.try &.["thumbnails"]?
+ banner = banners.try &.[-1]?.try &.["url"].as_s?
- # Raises a KeyError on failure.
- banners = initdata["header"]["c4TabbedHeaderRenderer"]?.try &.["banner"]?.try &.["thumbnails"]?
- banner = banners.try &.[-1]?.try &.["url"].as_s?
+ description = initdata["header"]["interactiveTabbedHeaderRenderer"]["description"]["simpleText"].as_s
+ description_html = HTML.escape(description).gsub("\n", "<br>")
- # if banner.includes? "channels/c4/default_banner"
- # banner = nil
- # end
+ paid = false
+ is_family_friendly = initdata["microformat"]["microformatDataRenderer"]["familySafe"].as_bool
+ allowed_regions = initdata["microformat"]["microformatDataRenderer"]["availableCountries"].as_a.map { |a| a.as_s }
- description = initdata["metadata"]["channelMetadataRenderer"]?.try &.["description"]?.try &.as_s? || ""
- description_html = HTML.escape(description).gsub("\n", "<br>")
+ related_channels = [] of AboutRelatedChannel
+ else
+ author = initdata["metadata"]["channelMetadataRenderer"]["title"].as_s
+ author_url = initdata["metadata"]["channelMetadataRenderer"]["channelUrl"].as_s
+ author_thumbnail = initdata["metadata"]["channelMetadataRenderer"]["avatar"]["thumbnails"][0]["url"].as_s
- paid = about.xpath_node(%q(//meta[@itemprop="paid"])).not_nil!["content"] == "True"
- is_family_friendly = about.xpath_node(%q(//meta[@itemprop="isFamilyFriendly"])).not_nil!["content"] == "True"
- allowed_regions = about.xpath_node(%q(//meta[@itemprop="regionsAllowed"])).not_nil!["content"].split(",")
+ ucid = initdata["metadata"]["channelMetadataRenderer"]["externalId"].as_s
- related_channels = initdata["contents"]["twoColumnBrowseResultsRenderer"]
- .["secondaryContents"]?.try &.["browseSecondaryContentsRenderer"]["contents"][0]?
- .try &.["verticalChannelSectionRenderer"]?.try &.["items"]?.try &.as_a.map do |node|
- renderer = node["miniChannelRenderer"]?
- related_id = renderer.try &.["channelId"]?.try &.as_s?
- related_id ||= ""
+ # Raises a KeyError on failure.
+ banners = initdata["header"]["c4TabbedHeaderRenderer"]?.try &.["banner"]?.try &.["thumbnails"]?
+ banner = banners.try &.[-1]?.try &.["url"].as_s?
- related_title = renderer.try &.["title"]?.try &.["simpleText"]?.try &.as_s?
- related_title ||= ""
+ # if banner.includes? "channels/c4/default_banner"
+ # banner = nil
+ # end
- related_author_url = renderer.try &.["navigationEndpoint"]?.try &.["commandMetadata"]?.try &.["webCommandMetadata"]?
- .try &.["url"]?.try &.as_s?
- related_author_url ||= ""
+ description = initdata["metadata"]["channelMetadataRenderer"]?.try &.["description"]?.try &.as_s? || ""
+ description_html = HTML.escape(description).gsub("\n", "<br>")
- related_author_thumbnails = renderer.try &.["thumbnail"]?.try &.["thumbnails"]?.try &.as_a?
- related_author_thumbnails ||= [] of JSON::Any
+ paid = about.xpath_node(%q(//meta[@itemprop="paid"])).not_nil!["content"] == "True"
+ is_family_friendly = about.xpath_node(%q(//meta[@itemprop="isFamilyFriendly"])).not_nil!["content"] == "True"
+ allowed_regions = about.xpath_node(%q(//meta[@itemprop="regionsAllowed"])).not_nil!["content"].split(",")
- related_author_thumbnail = ""
- if related_author_thumbnails.size > 0
- related_author_thumbnail = related_author_thumbnails[-1]["url"]?.try &.as_s?
- related_author_thumbnail ||= ""
- end
+ related_channels = initdata["contents"]["twoColumnBrowseResultsRenderer"]
+ .["secondaryContents"]?.try &.["browseSecondaryContentsRenderer"]["contents"][0]?
+ .try &.["verticalChannelSectionRenderer"]?.try &.["items"]?.try &.as_a.map do |node|
+ renderer = node["miniChannelRenderer"]?
+ related_id = renderer.try &.["channelId"]?.try &.as_s?
+ related_id ||= ""
- AboutRelatedChannel.new({
- ucid: related_id,
- author: related_title,
- author_url: related_author_url,
- author_thumbnail: related_author_thumbnail,
- })
- end
- related_channels ||= [] of AboutRelatedChannel
+ related_title = renderer.try &.["title"]?.try &.["simpleText"]?.try &.as_s?
+ related_title ||= ""
+
+ related_author_url = renderer.try &.["navigationEndpoint"]?.try &.["commandMetadata"]?.try &.["webCommandMetadata"]?
+ .try &.["url"]?.try &.as_s?
+ related_author_url ||= ""
+
+ related_author_thumbnails = renderer.try &.["thumbnail"]?.try &.["thumbnails"]?.try &.as_a?
+ related_author_thumbnails ||= [] of JSON::Any
+
+ related_author_thumbnail = ""
+ if related_author_thumbnails.size > 0
+ related_author_thumbnail = related_author_thumbnails[-1]["url"]?.try &.as_s?
+ related_author_thumbnail ||= ""
+ end
+
+ AboutRelatedChannel.new({
+ ucid: related_id,
+ author: related_title,
+ author_url: related_author_url,
+ author_thumbnail: related_author_thumbnail,
+ })
+ end
+ related_channels ||= [] of AboutRelatedChannel
+ end
total_views = 0_i64
joined = Time.unix(0)
tabs = [] of String
- auto_generated = false
tabs_json = initdata["contents"]["twoColumnBrowseResultsRenderer"]["tabs"]?.try &.as_a?
if !tabs_json.nil?
@@ -904,7 +919,7 @@ def get_about_info(ucid, locale)
joined = channel_about_meta["joinedDateText"]?.try &.["runs"]?.try &.as_a.reduce("") { |acc, node| acc + node["text"].as_s }
.try { |text| Time.parse(text, "Joined %b %-d, %Y", Time::Location.local) } || Time.unix(0)
- # Auto-generated channels
+ # Normal Auto-generated channels
# https://support.google.com/youtube/answer/2579942
# For auto-generated channels, channel_about_meta only has ["description"]["simpleText"] and ["primaryLinks"][0]["title"]["simpleText"]
if (channel_about_meta["primaryLinks"]?.try &.size || 0) == 1 && (channel_about_meta["primaryLinks"][0]?) &&
diff --git a/src/invidious/helpers/helpers.cr b/src/invidious/helpers/helpers.cr
index 5e49afb7..e9cb3989 100644
--- a/src/invidious/helpers/helpers.cr
+++ b/src/invidious/helpers/helpers.cr
@@ -42,7 +42,7 @@ struct ConfigPreferences
property player_style : String = "invidious"
property quality : String = "hd720"
property quality_dash : String = "auto"
- property default_home : String = "Popular"
+ property default_home : String? = "Popular"
property feed_menu : Array(String) = ["Popular", "Trending", "Subscriptions", "Playlists"]
property related_videos : Bool = true
property sort : String = "published"
@@ -98,6 +98,7 @@ class Config
property port : Int32 = 3000 # Port to listen for connections (overrided by command line argument)
property host_binding : String = "0.0.0.0" # Host to bind (overrided by command line argument)
property pool_size : Int32 = 100 # Pool size for HTTP requests to youtube.com and ytimg.com (each domain has a separate pool of `pool_size`)
+ property use_quic : Bool = true # Use quic transport for youtube api
@[YAML::Field(converter: Preferences::StringToCookies)]
property cookies : HTTP::Cookies = HTTP::Cookies.new # Saved cookies in "name1=value1; name2=value2..." format
diff --git a/src/invidious/helpers/i18n.cr b/src/invidious/helpers/i18n.cr
index 0faa2e32..45a3f1ae 100644
--- a/src/invidious/helpers/i18n.cr
+++ b/src/invidious/helpers/i18n.cr
@@ -1,3 +1,33 @@
+LOCALES = {
+ "ar" => load_locale("ar"),
+ "de" => load_locale("de"),
+ "el" => load_locale("el"),
+ "en-US" => load_locale("en-US"),
+ "eo" => load_locale("eo"),
+ "es" => load_locale("es"),
+ "fa" => load_locale("fa"),
+ "fi" => load_locale("fi"),
+ "fr" => load_locale("fr"),
+ "he" => load_locale("he"),
+ "hr" => load_locale("hr"),
+ "id" => load_locale("id"),
+ "is" => load_locale("is"),
+ "it" => load_locale("it"),
+ "ja" => load_locale("ja"),
+ "nb-NO" => load_locale("nb-NO"),
+ "nl" => load_locale("nl"),
+ "pl" => load_locale("pl"),
+ "pt-BR" => load_locale("pt-BR"),
+ "pt-PT" => load_locale("pt-PT"),
+ "ro" => load_locale("ro"),
+ "ru" => load_locale("ru"),
+ "sv" => load_locale("sv-SE"),
+ "tr" => load_locale("tr"),
+ "uk" => load_locale("uk"),
+ "zh-CN" => load_locale("zh-CN"),
+ "zh-TW" => load_locale("zh-TW"),
+}
+
def load_locale(name)
return JSON.parse(File.read("locales/#{name}.json")).as_h
end
diff --git a/src/invidious/helpers/macros.cr b/src/invidious/helpers/macros.cr
index 8b74bc86..5d426a8b 100644
--- a/src/invidious/helpers/macros.cr
+++ b/src/invidious/helpers/macros.cr
@@ -48,7 +48,8 @@ module JSON::Serializable
end
end
-macro templated(filename, template = "template")
+macro templated(filename, template = "template", navbar_search = true)
+ navbar_search = {{navbar_search}}
render "src/invidious/views/#{{{filename}}}.ecr", "src/invidious/views/#{{{template}}}.ecr"
end
diff --git a/src/invidious/helpers/utils.cr b/src/invidious/helpers/utils.cr
index 2c95a373..10d4e6b6 100644
--- a/src/invidious/helpers/utils.cr
+++ b/src/invidious/helpers/utils.cr
@@ -9,20 +9,22 @@ def add_yt_headers(request)
return if request.resource.starts_with? "/sorry/index"
request.headers["x-youtube-client-name"] ||= "1"
request.headers["x-youtube-client-version"] ||= "2.20200609"
+ # Preserve original cookies and add new YT consent cookie for EU servers
+ request.headers["cookie"] = "#{request.headers["cookie"]?}; CONSENT=YES+"
if !CONFIG.cookies.empty?
request.headers["cookie"] = "#{(CONFIG.cookies.map { |c| "#{c.name}=#{c.value}" }).join("; ")}; #{request.headers["cookie"]?}"
end
end
-struct QUICPool
+struct YoutubeConnectionPool
property! url : URI
property! capacity : Int32
property! timeout : Float64
- property pool : ConnectionPool(QUIC::Client)
+ property pool : ConnectionPool(QUIC::Client | HTTP::Client)
- def initialize(url : URI, @capacity = 5, @timeout = 5.0)
+ def initialize(url : URI, @capacity = 5, @timeout = 5.0, use_quic = true)
@url = url
- @pool = build_pool
+ @pool = build_pool(use_quic)
end
def client(region = nil, &block)
@@ -48,9 +50,13 @@ struct QUICPool
response
end
- private def build_pool
- ConnectionPool(QUIC::Client).new(capacity: capacity, timeout: timeout) do
- conn = QUIC::Client.new(url)
+ private def build_pool(use_quic)
+ ConnectionPool(QUIC::Client | HTTP::Client).new(capacity: capacity, timeout: timeout) do
+ if use_quic
+ conn = QUIC::Client.new(url)
+ else
+ conn = HTTP::Client.new(url)
+ end
conn.family = (url.host == "www.youtube.com") ? CONFIG.force_resolve : Socket::Family::INET
conn.family = Socket::Family::INET if conn.family == Socket::Family::UNSPEC
conn.before_request { |r| add_yt_headers(r) } if url.host == "www.youtube.com"
diff --git a/src/invidious/jobs/statistics_refresh_job.cr b/src/invidious/jobs/statistics_refresh_job.cr
index aa46fb0e..6569c0a1 100644
--- a/src/invidious/jobs/statistics_refresh_job.cr
+++ b/src/invidious/jobs/statistics_refresh_job.cr
@@ -42,7 +42,7 @@ class Invidious::Jobs::StatisticsRefreshJob < Invidious::Jobs::BaseJob
"version" => @software_config["version"],
"branch" => @software_config["branch"],
}
- STATISTICS["openRegistration"] = CONFIG.registration_enabled
+ STATISTICS["openRegistrations"] = CONFIG.registration_enabled
end
private def refresh_stats
diff --git a/src/invidious/playlists.cr b/src/invidious/playlists.cr
index 71f6a9b8..073a9986 100644
--- a/src/invidious/playlists.cr
+++ b/src/invidious/playlists.cr
@@ -437,7 +437,8 @@ end
def get_playlist_videos(db, playlist, offset, locale = nil, continuation = nil)
# Show empy playlist if requested page is out of range
- if offset >= playlist.video_count
+ # (e.g, when a new playlist has been created, offset will be negative)
+ if offset >= playlist.video_count || offset < 0
return [] of PlaylistVideo
end
diff --git a/src/invidious/routes/misc.cr b/src/invidious/routes/misc.cr
index bc009633..d32ba892 100644
--- a/src/invidious/routes/misc.cr
+++ b/src/invidious/routes/misc.cr
@@ -22,7 +22,7 @@ class Invidious::Routes::Misc < Invidious::Routes::BaseRoute
env.redirect "/feed/popular"
end
else
- templated "empty"
+ templated "search_homepage", navbar_search: false
end
end
diff --git a/src/invidious/routes/playlists.cr b/src/invidious/routes/playlists.cr
index 73c14155..1f7fa27d 100644
--- a/src/invidious/routes/playlists.cr
+++ b/src/invidious/routes/playlists.cr
@@ -434,7 +434,7 @@ class Invidious::Routes::Playlists < Invidious::Routes::BaseRoute
end
page_count = (playlist.video_count / 100).to_i
- page_count = 1 if page_count == 0
+ page_count += 1 if (playlist.video_count % 100) > 0
if page > page_count
return env.redirect "/playlist?list=#{plid}&page=#{page_count}"
diff --git a/src/invidious/routes/watch.cr b/src/invidious/routes/watch.cr
index 8169e1ed..d0338882 100644
--- a/src/invidious/routes/watch.cr
+++ b/src/invidious/routes/watch.cr
@@ -187,4 +187,13 @@ class Invidious::Routes::Watch < Invidious::Routes::BaseRoute
templated "watch"
end
+
+ def redirect(env)
+ url = "/watch?v=#{env.params.url["id"]}"
+ if env.params.query.size > 0
+ url += "&#{env.params.query}"
+ end
+
+ return env.redirect url
+ end
end
diff --git a/src/invidious/search.cr b/src/invidious/search.cr
index cf8fd790..4b216613 100644
--- a/src/invidious/search.cr
+++ b/src/invidious/search.cr
@@ -231,20 +231,32 @@ end
alias SearchItem = SearchVideo | SearchChannel | SearchPlaylist
def channel_search(query, page, channel)
- response = YT_POOL.client &.get("/channel/#{channel}?hl=en&gl=US")
- response = YT_POOL.client &.get("/user/#{channel}?hl=en&gl=US") if response.headers["location"]?
- response = YT_POOL.client &.get("/c/#{channel}?hl=en&gl=US") if response.headers["location"]?
+ response = YT_POOL.client &.get("/channel/#{channel}")
+
+ if response.status_code == 404
+ response = YT_POOL.client &.get("/user/#{channel}")
+ response = YT_POOL.client &.get("/c/#{channel}") if response.status_code == 404
+ initial_data = extract_initial_data(response.body)
+ ucid = initial_data["header"]["c4TabbedHeaderRenderer"]?.try &.["channelId"].as_s?
+ raise InfoException.new("Impossible to extract channel ID from page") if !ucid
+ else
+ ucid = channel
+ end
- ucid = response.body.match(/\\"channelId\\":\\"(?<ucid>[^\\]+)\\"/).try &.["ucid"]?
+ continuation = produce_channel_search_continuation(ucid, query, page)
+ response_json = request_youtube_api_browse(continuation)
- return 0, [] of SearchItem if !ucid
+ result = JSON.parse(response_json)
+ continuationItems = result["onResponseReceivedActions"]?
+ .try &.[0]["appendContinuationItemsAction"]["continuationItems"]
- url = produce_channel_search_url(ucid, query, page)
- response = YT_POOL.client &.get(url)
- initial_data = JSON.parse(response.body).as_a.find &.["response"]?
- return 0, [] of SearchItem if !initial_data
- author = initial_data["response"]?.try &.["metadata"]?.try &.["channelMetadataRenderer"]?.try &.["title"]?.try &.as_s
- items = extract_items(initial_data.as_h, author, ucid)
+ return 0, [] of SearchItem if !continuationItems
+
+ items = [] of SearchItem
+ continuationItems.as_a.select(&.as_h.has_key?("itemSectionRenderer")).each { |item|
+ extract_item(item["itemSectionRenderer"]["contents"].as_a[0])
+ .try { |t| items << t }
+ }
return items.size, items
end
@@ -361,17 +373,28 @@ def produce_search_params(page = 1, sort : String = "relevance", date : String =
return params
end
-def produce_channel_search_url(ucid, query, page)
+def produce_channel_search_continuation(ucid, query, page)
+ if page <= 1
+ idx = 0_i64
+ else
+ idx = 30_i64 * (page - 1)
+ end
+
object = {
"80226972:embedded" => {
"2:string" => ucid,
"3:base64" => {
"2:string" => "search",
+ "6:varint" => 1_i64,
"7:varint" => 1_i64,
- "15:string" => "#{page}",
+ "12:varint" => 1_i64,
+ "15:base64" => {
+ "3:varint" => idx,
+ },
"23:varint" => 0_i64,
},
"11:string" => query,
+ "35:string" => "browse-feed#{ucid}search",
},
}
@@ -380,7 +403,7 @@ def produce_channel_search_url(ucid, query, page)
.try { |i| Base64.urlsafe_encode(i) }
.try { |i| URI.encode_www_form(i) }
- return "/browse_ajax?continuation=#{continuation}&gl=US&hl=en"
+ return continuation
end
def process_search_query(query, page, user, region)
diff --git a/src/invidious/trending.cr b/src/invidious/trending.cr
index 8d078387..910a99d8 100644
--- a/src/invidious/trending.cr
+++ b/src/invidious/trending.cr
@@ -6,24 +6,22 @@ def fetch_trending(trending_type, region, locale)
plid = nil
if trending_type && trending_type != "Default"
- trending_type = trending_type.downcase.capitalize
+ if trending_type == "Music"
+ trending_type = 1
+ elsif trending_type == "Gaming"
+ trending_type = 2
+ elsif trending_type == "Movies"
+ trending_type = 3
+ end
response = YT_POOL.client &.get("/feed/trending?gl=#{region}&hl=en").body
initial_data = extract_initial_data(response)
+ url = initial_data["contents"]["twoColumnBrowseResultsRenderer"]["tabs"][trending_type]["tabRenderer"]["endpoint"]["commandMetadata"]["webCommandMetadata"]["url"]
+ url = "#{url}&gl=#{region}&hl=en"
- tabs = initial_data["contents"]["twoColumnBrowseResultsRenderer"]["tabs"][0]["tabRenderer"]["content"]["sectionListRenderer"]["subMenu"]["channelListSubMenuRenderer"]["contents"].as_a
- url = tabs.select { |tab| tab["channelListSubMenuAvatarRenderer"]["title"]["simpleText"] == trending_type }[0]?
-
- if url
- url["channelListSubMenuAvatarRenderer"]["navigationEndpoint"]["commandMetadata"]["webCommandMetadata"]["url"]
- url = url["channelListSubMenuAvatarRenderer"]["navigationEndpoint"]["commandMetadata"]["webCommandMetadata"]["url"].as_s
- url = "#{url}&gl=#{region}&hl=en"
- trending = YT_POOL.client &.get(url).body
- plid = extract_plid(url)
- else
- trending = YT_POOL.client &.get("/feed/trending?gl=#{region}&hl=en").body
- end
+ trending = YT_POOL.client &.get(url).body
+ plid = extract_plid(url)
else
trending = YT_POOL.client &.get("/feed/trending?gl=#{region}&hl=en").body
end
diff --git a/src/invidious/users.cr b/src/invidious/users.cr
index 7a948b76..8fef64a0 100644
--- a/src/invidious/users.cr
+++ b/src/invidious/users.cr
@@ -68,7 +68,7 @@ struct Preferences
property quality : String = CONFIG.default_user_preferences.quality
@[JSON::Field(converter: Preferences::ProcessString)]
property quality_dash : String = CONFIG.default_user_preferences.quality_dash
- property default_home : String = CONFIG.default_user_preferences.default_home
+ property default_home : String? = CONFIG.default_user_preferences.default_home
property feed_menu : Array(String) = CONFIG.default_user_preferences.feed_menu
property related_videos : Bool = CONFIG.default_user_preferences.related_videos
diff --git a/src/invidious/videos.cr b/src/invidious/videos.cr
index 2b793a1b..40d63ae8 100644
--- a/src/invidious/videos.cr
+++ b/src/invidious/videos.cr
@@ -1150,15 +1150,15 @@ end
def build_thumbnails(id)
return {
- {name: "maxres", host: "#{HOST_URL}", url: "maxres", height: 720, width: 1280},
- {name: "maxresdefault", host: "https://i.ytimg.com", url: "maxresdefault", height: 720, width: 1280},
- {name: "sddefault", host: "https://i.ytimg.com", url: "sddefault", height: 480, width: 640},
- {name: "high", host: "https://i.ytimg.com", url: "hqdefault", height: 360, width: 480},
- {name: "medium", host: "https://i.ytimg.com", url: "mqdefault", height: 180, width: 320},
- {name: "default", host: "https://i.ytimg.com", url: "default", height: 90, width: 120},
- {name: "start", host: "https://i.ytimg.com", url: "1", height: 90, width: 120},
- {name: "middle", host: "https://i.ytimg.com", url: "2", height: 90, width: 120},
- {name: "end", host: "https://i.ytimg.com", url: "3", height: 90, width: 120},
+ {host: HOST_URL, height: 720, width: 1280, name: "maxres", url: "maxres"},
+ {host: HOST_URL, height: 720, width: 1280, name: "maxresdefault", url: "maxresdefault"},
+ {host: HOST_URL, height: 480, width: 640, name: "sddefault", url: "sddefault"},
+ {host: HOST_URL, height: 360, width: 480, name: "high", url: "hqdefault"},
+ {host: HOST_URL, height: 180, width: 320, name: "medium", url: "mqdefault"},
+ {host: HOST_URL, height: 90, width: 120, name: "default", url: "default"},
+ {host: HOST_URL, height: 90, width: 120, name: "start", url: "1"},
+ {host: HOST_URL, height: 90, width: 120, name: "middle", url: "2"},
+ {host: HOST_URL, height: 90, width: 120, name: "end", url: "3"},
}
end
diff --git a/src/invidious/views/empty.ecr b/src/invidious/views/empty.ecr
deleted file mode 100644
index c10c097e..00000000
--- a/src/invidious/views/empty.ecr
+++ /dev/null
@@ -1,8 +0,0 @@
-<% content_for "header" do %>
-<meta name="description" content="<%= translate(locale, "An alternative front-end to YouTube") %>">
-<title>
- Invidious
-</title>
-<% end %>
-
-<%= rendered "components/feed_menu" %>
diff --git a/src/invidious/views/preferences.ecr b/src/invidious/views/preferences.ecr
index 14d63536..c1f10818 100644
--- a/src/invidious/views/preferences.ecr
+++ b/src/invidious/views/preferences.ecr
@@ -150,7 +150,7 @@
<label for="default_home"><%= translate(locale, "Default homepage: ") %></label>
<select name="default_home" id="default_home">
<% feed_options.each do |option| %>
- <option value="<%= option %>" <% if preferences.default_home == option %> selected <% end %>><%= translate(locale, option.blank? ? "none" : option) %></option>
+ <option value="<%= option %>" <% if preferences.default_home == option %> selected <% end %>><%= translate(locale, option.blank? ? "Search" : option) %></option>
<% end %>
</select>
</div>
@@ -160,7 +160,7 @@
<% (feed_options.size - 1).times do |index| %>
<select name="feed_menu[<%= index %>]" id="feed_menu[<%= index %>]">
<% feed_options.each do |option| %>
- <option value="<%= option %>" <% if preferences.feed_menu[index]? == option %> selected <% end %>><%= translate(locale, option.blank? ? "none" : option) %></option>
+ <option value="<%= option %>" <% if preferences.feed_menu[index]? == option %> selected <% end %>><%= translate(locale, option.blank? ? "Search" : option) %></option>
<% end %>
</select>
<% end %>
diff --git a/src/invidious/views/search_homepage.ecr b/src/invidious/views/search_homepage.ecr
new file mode 100644
index 00000000..b36500e9
--- /dev/null
+++ b/src/invidious/views/search_homepage.ecr
@@ -0,0 +1,24 @@
+<% content_for "header" do %>
+<meta name="description" content="<%= translate(locale, "An alternative front-end to YouTube") %>">
+<title>
+ Invidious
+</title>
+<link rel="stylesheet" href="/css/empty.css?v=<%= ASSET_COMMIT %>">
+<% end %>
+
+<%= rendered "components/feed_menu" %>
+
+<div class="pure-g h-box" id="search-widget">
+ <div class="pure-u-1" id="logo">
+ <h1 href="/" class="pure-menu-heading">Invidious</h1>
+ </div>
+ <div class="pure-u-1-4"></div>
+ <div class="pure-u-1 pure-u-md-12-24 searchbar">
+ <form class="pure-form" action="/search" method="get">
+ <fieldset>
+ <input type="search" style="width:100%" name="q" placeholder="<%= translate(locale, "search") %>" value="<%= env.get?("search").try {|x| HTML.escape(x.as(String)) } %>">
+ </fieldset>
+ </form>
+ </div>
+ <div class="pure-u-1-4"></div>
+</div>
diff --git a/src/invidious/views/template.ecr b/src/invidious/views/template.ecr
index 61b900e3..5b63bf1f 100644
--- a/src/invidious/views/template.ecr
+++ b/src/invidious/views/template.ecr
@@ -26,18 +26,21 @@
<span style="display:none" id="dark_mode_pref"><%= env.get("preferences").as(Preferences).dark_mode %></span>
<div class="pure-g">
<div class="pure-u-1 pure-u-md-2-24"></div>
- <div class="pure-u-1 pure-u-md-20-24">
+ <div class="pure-u-1 pure-u-md-20-24", id="contents">
<div class="pure-g navbar h-box">
- <div class="pure-u-1 pure-u-md-4-24">
- <a href="/" class="index-link pure-menu-heading">Invidious</a>
- </div>
- <div class="pure-u-1 pure-u-md-12-24 searchbar">
- <form class="pure-form" action="/search" method="get">
- <fieldset>
- <input type="search" style="width:100%" name="q" placeholder="<%= translate(locale, "search") %>" value="<%= env.get?("search").try {|x| HTML.escape(x.as(String)) } %>">
- </fieldset>
- </form>
- </div>
+ <% if navbar_search %>
+ <div class="pure-u-1 pure-u-md-4-24">
+ <a href="/" class="index-link pure-menu-heading">Invidious</a>
+ </div>
+ <div class="pure-u-1 pure-u-md-12-24 searchbar">
+ <form class="pure-form" action="/search" method="get">
+ <fieldset>
+ <input type="search" style="width:100%" name="q" placeholder="<%= translate(locale, "search") %>" value="<%= env.get?("search").try {|x| HTML.escape(x.as(String)) } %>">
+ </fieldset>
+ </form>
+ </div>
+ <% end %>
+
<div class="pure-u-1 pure-u-md-8-24 user-field">
<% if env.get? "user" %>
<div class="pure-u-1-4">
@@ -106,7 +109,7 @@
<%= content %>
- <div class="footer">
+ <footer>
<div class="pure-g">
<div class="pure-u-1 pure-u-md-1-3">
<a href="https://github.com/iv-org/invidious">
@@ -140,7 +143,7 @@
<%= translate(locale, "Current version: ") %> <%= CURRENT_VERSION %>-<%= CURRENT_COMMIT %> @ <%= CURRENT_BRANCH %>
</div>
</div>
- </div>
+ </footer>
</div>
<div class="pure-u-1 pure-u-md-2-24"></div>
</div>
diff --git a/src/invidious/views/trending.ecr b/src/invidious/views/trending.ecr
index 42acb15c..3ec62555 100644
--- a/src/invidious/views/trending.ecr
+++ b/src/invidious/views/trending.ecr
@@ -21,7 +21,7 @@
</div>
<div class="pure-u-1-3">
<div class="pure-g" style="text-align:right">
- <% {"Default", "Music", "Gaming", "News", "Movies"}.each do |option| %>
+ <% {"Default", "Music", "Gaming", "Movies"}.each do |option| %>
<div class="pure-u-1 pure-md-1-3">
<% if trending_type == option %>
<b><%= translate(locale, option) %></b>