summaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/invidious.cr67
-rw-r--r--src/invidious/channels.cr86
-rw-r--r--src/invidious/helpers/helpers.cr16
-rw-r--r--src/invidious/helpers/jobs.cr4
-rw-r--r--src/invidious/jobs.cr13
-rw-r--r--src/invidious/jobs/base_job.cr3
-rw-r--r--src/invidious/jobs/pull_popular_videos_job.cr27
-rw-r--r--src/invidious/playlists.cr2
-rw-r--r--src/invidious/routes/base_route.cr8
-rw-r--r--src/invidious/routes/home.cr34
-rw-r--r--src/invidious/routes/licenses.cr6
-rw-r--r--src/invidious/routes/privacy.cr6
-rw-r--r--src/invidious/routing.cr8
-rw-r--r--src/invidious/views/components/feed_menu.ecr28
-rw-r--r--src/invidious/views/components/player.ecr2
15 files changed, 164 insertions, 146 deletions
diff --git a/src/invidious.cr b/src/invidious.cr
index 2a4c373c..ad63fcad 100644
--- a/src/invidious.cr
+++ b/src/invidious.cr
@@ -27,6 +27,8 @@ require "compress/zip"
require "protodec/utils"
require "./invidious/helpers/*"
require "./invidious/*"
+require "./invidious/routes/**"
+require "./invidious/jobs/**"
ENV_CONFIG_NAME = "INVIDIOUS_CONFIG"
@@ -196,11 +198,11 @@ if config.statistics_enabled
end
end
-popular_videos = [] of ChannelVideo
-spawn do
- pull_popular_videos(PG_DB) do |videos|
- popular_videos = videos
- end
+Invidious::Jobs.register Invidious::Jobs::PullPopularVideosJob.new(PG_DB)
+Invidious::Jobs.start_all
+
+def popular_videos
+ Invidious::Jobs::PullPopularVideosJob::POPULAR_VIDEOS.get
end
DECRYPT_FUNCTION = [] of {SigProc, Int32}
@@ -350,44 +352,9 @@ before_all do |env|
env.set "current_page", URI.encode_www_form(current_page)
end
-get "/" do |env|
- preferences = env.get("preferences").as(Preferences)
- locale = LOCALES[preferences.locale]?
- user = env.get? "user"
-
- case preferences.default_home
- when ""
- templated "empty"
- when "Popular"
- templated "popular"
- when "Trending"
- env.redirect "/feed/trending"
- when "Subscriptions"
- if user
- env.redirect "/feed/subscriptions"
- else
- templated "popular"
- end
- when "Playlists"
- if user
- env.redirect "/view_all_playlists"
- else
- templated "popular"
- end
- else
- templated "empty"
- end
-end
-
-get "/privacy" do |env|
- locale = LOCALES[env.get("preferences").as(Preferences).locale]?
- templated "privacy"
-end
-
-get "/licenses" do |env|
- locale = LOCALES[env.get("preferences").as(Preferences).locale]?
- rendered "licenses"
-end
+Invidious::Routing.get "/", Invidious::Routes::Home
+Invidious::Routing.get "/privacy", Invidious::Routes::Privacy
+Invidious::Routing.get "/licenses", Invidious::Routes::Licenses
# Videos
@@ -3412,17 +3379,13 @@ post "/feed/webhook/:token" do |env|
views: video.views,
})
- PG_DB.query_all("UPDATE users SET feed_needs_update = true, notifications = array_append(notifications, $1) \
- WHERE updated < $2 AND $3 = ANY(subscriptions) AND $1 <> ALL(notifications)",
- video.id, video.published, video.ucid, as: String)
-
- video_array = video.to_a
- args = arg_array(video_array)
-
- PG_DB.exec("INSERT INTO channel_videos VALUES (#{args}) \
+ was_insert = PG_DB.query_one("INSERT INTO channel_videos VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) \
ON CONFLICT (id) DO UPDATE SET title = $2, published = $3, \
updated = $4, ucid = $5, author = $6, length_seconds = $7, \
- live_now = $8, premiere_timestamp = $9, views = $10", args: video_array)
+ live_now = $8, premiere_timestamp = $9, views = $10 returning (xmax=0) as was_insert", *video.to_tuple, as: Bool)
+
+ PG_DB.exec("UPDATE users SET notifications = array_append(notifications, $1), \
+ feed_needs_update = true WHERE $2 = ANY(subscriptions)", video.id, video.ucid) if was_insert
end
end
diff --git a/src/invidious/channels.cr b/src/invidious/channels.cr
index 007aa06c..18bdac09 100644
--- a/src/invidious/channels.cr
+++ b/src/invidious/channels.cr
@@ -97,6 +97,14 @@ struct ChannelVideo
end
end
end
+
+ def to_tuple
+ {% begin %}
+ {
+ {{*@type.instance_vars.map { |var| var.name }}}
+ }
+ {% end %}
+ end
end
struct AboutRelatedChannel
@@ -260,28 +268,15 @@ def fetch_channel(ucid, db, pull_all_videos = true, locale = nil)
views: views,
})
- emails = db.query_all("UPDATE users SET notifications = array_append(notifications, $1) \
- WHERE updated < $2 AND $3 = ANY(subscriptions) AND $1 <> ALL(notifications) RETURNING email",
- video.id, video.published, ucid, as: String)
-
- video_array = video.to_a
- args = arg_array(video_array)
-
# We don't include the 'premiere_timestamp' here because channel pages don't include them,
# meaning the above timestamp is always null
- db.exec("INSERT INTO channel_videos VALUES (#{args}) \
+ was_insert = db.query_one("INSERT INTO channel_videos VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) \
ON CONFLICT (id) DO UPDATE SET title = $2, published = $3, \
updated = $4, ucid = $5, author = $6, length_seconds = $7, \
- live_now = $8, views = $10", args: video_array)
-
- # Update all users affected by insert
- if emails.empty?
- values = "'{}'"
- else
- values = "VALUES #{emails.map { |email| %((E'#{email.gsub({'\'' => "\\'", '\\' => "\\\\"})}')) }.join(",")}"
- end
+ live_now = $8, views = $10 returning (xmax=0) as was_insert", *video.to_tuple, as: Bool)
- db.exec("UPDATE users SET feed_needs_update = true WHERE email = ANY(#{values})")
+ db.exec("UPDATE users SET notifications = array_append(notifications, $1), \
+ feed_needs_update = true WHERE $2 = ANY(subscriptions)", video.id, video.ucid) if was_insert
end
if pull_all_videos
@@ -315,39 +310,19 @@ def fetch_channel(ucid, db, pull_all_videos = true, locale = nil)
# We are notified of Red videos elsewhere (PubSub), which includes a correct published date,
# so since they don't provide a published date here we can safely ignore them.
if Time.utc - video.published > 1.minute
- emails = db.query_all("UPDATE users SET notifications = array_append(notifications, $1) \
- WHERE updated < $2 AND $3 = ANY(subscriptions) AND $1 <> ALL(notifications) RETURNING email",
- video.id, video.published, video.ucid, as: String)
-
- video_array = video.to_a
- args = arg_array(video_array)
-
- # We don't update the 'premire_timestamp' here because channel pages don't include them
- db.exec("INSERT INTO channel_videos VALUES (#{args}) \
+ was_insert = db.query_one("INSERT INTO channel_videos VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) \
ON CONFLICT (id) DO UPDATE SET title = $2, published = $3, \
updated = $4, ucid = $5, author = $6, length_seconds = $7, \
- live_now = $8, views = $10", args: video_array)
+ live_now = $8, views = $10 returning (xmax=0) as was_insert", *video.to_tuple, as: Bool)
- # Update all users affected by insert
- if emails.empty?
- values = "'{}'"
- else
- values = "VALUES #{emails.map { |email| %((E'#{email.gsub({'\'' => "\\'", '\\' => "\\\\"})}')) }.join(",")}"
- end
-
- db.exec("UPDATE users SET feed_needs_update = true WHERE email = ANY(#{values})")
+ db.exec("UPDATE users SET notifications = array_append(notifications, $1), \
+ feed_needs_update = true WHERE $2 = ANY(subscriptions)", video.id, video.ucid) if was_insert
end
end
- if count < 25
- break
- end
-
+ break if count < 25
page += 1
end
-
- # When a video is deleted from a channel, we find and remove it here
- db.exec("DELETE FROM channel_videos * WHERE NOT id = ANY ('{#{ids.map { |id| %("#{id}") }.join(",")}}') AND ucid = $1", ucid)
end
channel = InvidiousChannel.new({
@@ -427,14 +402,9 @@ def produce_channel_videos_url(ucid, page = 1, auto_generated = nil, sort_by = "
object["80226972:embedded"]["3:base64"].as(Hash)["4:varint"] = 0_i64
object["80226972:embedded"]["3:base64"].as(Hash)["61:string"] = Base64.urlsafe_encode(Protodec::Any.from_json(Protodec::Any.cast_json({
- "1:embedded" => {
- "1:varint" => 6307666885028338688_i64,
- "2:embedded" => {
- "1:string" => Base64.urlsafe_encode(Protodec::Any.from_json(Protodec::Any.cast_json({
- "1:varint" => 30_i64 * (page - 1),
- }))),
- },
- },
+ "1:string" => Base64.urlsafe_encode(Protodec::Any.from_json(Protodec::Any.cast_json({
+ "1:varint" => 30_i64 * (page - 1),
+ }))),
})))
end
@@ -915,20 +885,8 @@ def get_about_info(ucid, locale)
end
def get_channel_videos_response(ucid, page = 1, auto_generated = nil, sort_by = "newest")
- url = produce_channel_videos_url(ucid, page, auto_generated: auto_generated, sort_by: sort_by, v2: false)
- response = YT_POOL.client &.get(url)
- initial_data = JSON.parse(response.body).as_a.find &.["response"]?
- return response if !initial_data
- needs_v2 = initial_data
- .try &.["response"]?.try &.["alerts"]?
- .try &.as_a.any? { |alert|
- alert.try &.["alertRenderer"]?.try &.["type"]?.try { |t| t == "ERROR" }
- }
- if needs_v2
- url = produce_channel_videos_url(ucid, page, auto_generated: auto_generated, sort_by: sort_by, v2: true)
- response = YT_POOL.client &.get(url)
- end
- response
+ url = produce_channel_videos_url(ucid, page, auto_generated: auto_generated, sort_by: sort_by, v2: true)
+ return YT_POOL.client &.get(url)
end
def get_60_videos(ucid, author, page, auto_generated, sort_by = "newest")
diff --git a/src/invidious/helpers/helpers.cr b/src/invidious/helpers/helpers.cr
index 6571f818..fb220eab 100644
--- a/src/invidious/helpers/helpers.cr
+++ b/src/invidious/helpers/helpers.cr
@@ -177,8 +177,8 @@ def extract_item(item : JSON::Any, author_fallback : String? = nil, author_id_fa
view_count = i["viewCountText"]?.try &.["simpleText"]?.try &.as_s.gsub(/\D+/, "").to_i64? || 0_i64
description_html = i["descriptionSnippet"]?.try { |t| parse_content(t) } || ""
length_seconds = i["lengthText"]?.try &.["simpleText"]?.try &.as_s.try { |t| decode_length_seconds(t) } ||
- i["thumbnailOverlays"]?.try &.as_a.find(&.["thumbnailOverlayTimeStatusRenderer"]?).try &.["thumbnailOverlayTimeStatusRenderer"]?
- .try &.["text"]?.try &.["simpleText"]?.try &.as_s.try { |t| decode_length_seconds(t) } || 0
+ i["thumbnailOverlays"]?.try &.as_a.find(&.["thumbnailOverlayTimeStatusRenderer"]?).try &.["thumbnailOverlayTimeStatusRenderer"]?
+ .try &.["text"]?.try &.["simpleText"]?.try &.as_s.try { |t| decode_length_seconds(t) } || 0
live_now = false
paid = false
@@ -302,14 +302,14 @@ def extract_items(initial_data : Hash(String, JSON::Any), author_fallback : Stri
channel_v2_response = initial_data
.try &.["response"]?
- .try &.["continuationContents"]?
- .try &.["gridContinuation"]?
- .try &.["items"]?
+ .try &.["continuationContents"]?
+ .try &.["gridContinuation"]?
+ .try &.["items"]?
if channel_v2_response
channel_v2_response.try &.as_a.each { |item|
- extract_item(item, author_fallback, author_id_fallback)
- .try { |t| items << t }
+ extract_item(item, author_fallback, author_id_fallback)
+ .try { |t| items << t }
}
else
initial_data.try { |t| t["contents"]? || t["response"]? }
@@ -325,7 +325,7 @@ def extract_items(initial_data : Hash(String, JSON::Any), author_fallback : Stri
extract_item(item, author_fallback, author_id_fallback)
.try { |t| items << t }
} }
- end
+ end
items
end
diff --git a/src/invidious/helpers/jobs.cr b/src/invidious/helpers/jobs.cr
index 43334718..ca3d44d0 100644
--- a/src/invidious/helpers/jobs.cr
+++ b/src/invidious/helpers/jobs.cr
@@ -258,7 +258,7 @@ def bypass_captcha(captcha_key, logger)
end
inputs["g-recaptcha-response"] = response["solution"]["gRecaptchaResponse"].as_s
- headers["Cookies"] = response["solution"]["cookies"].as_h.map { |k, v| "#{k}=#{v}" }.join("; ")
+ headers["Cookies"] = response["solution"]["cookies"].as_h?.try &.map { |k, v| "#{k}=#{v}" }.join("; ") || ""
response = YT_POOL.client &.post("/das_captcha", headers, form: inputs)
yield response.cookies.select { |cookie| cookie.name != "PREF" }
@@ -308,7 +308,7 @@ def bypass_captcha(captcha_key, logger)
end
inputs["g-recaptcha-response"] = response["solution"]["gRecaptchaResponse"].as_s
- headers["Cookies"] = response["solution"]["cookies"].as_h.map { |k, v| "#{k}=#{v}" }.join("; ")
+ headers["Cookies"] = response["solution"]["cookies"].as_h?.try &.map { |k, v| "#{k}=#{v}" }.join("; ") || ""
response = YT_POOL.client &.post("/sorry/index", headers: headers, form: inputs)
headers = HTTP::Headers{
"Cookie" => URI.parse(response.headers["location"]).query_params["google_abuse"].split(";")[0],
diff --git a/src/invidious/jobs.cr b/src/invidious/jobs.cr
new file mode 100644
index 00000000..ec0cad64
--- /dev/null
+++ b/src/invidious/jobs.cr
@@ -0,0 +1,13 @@
+module Invidious::Jobs
+ JOBS = [] of BaseJob
+
+ def self.register(job : BaseJob)
+ JOBS << job
+ end
+
+ def self.start_all
+ JOBS.each do |job|
+ spawn { job.begin }
+ end
+ end
+end
diff --git a/src/invidious/jobs/base_job.cr b/src/invidious/jobs/base_job.cr
new file mode 100644
index 00000000..47e75864
--- /dev/null
+++ b/src/invidious/jobs/base_job.cr
@@ -0,0 +1,3 @@
+abstract class Invidious::Jobs::BaseJob
+ abstract def begin
+end
diff --git a/src/invidious/jobs/pull_popular_videos_job.cr b/src/invidious/jobs/pull_popular_videos_job.cr
new file mode 100644
index 00000000..7a8ab84e
--- /dev/null
+++ b/src/invidious/jobs/pull_popular_videos_job.cr
@@ -0,0 +1,27 @@
+class Invidious::Jobs::PullPopularVideosJob < Invidious::Jobs::BaseJob
+ QUERY = <<-SQL
+ SELECT DISTINCT ON (ucid) *
+ FROM channel_videos
+ 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)
+ ORDER BY ucid, published DESC
+ SQL
+ POPULAR_VIDEOS = Atomic.new([] of ChannelVideo)
+ private getter db : DB::Database
+
+ def initialize(@db)
+ end
+
+ def begin
+ loop do
+ videos = db.query_all(QUERY, as: ChannelVideo)
+ .sort_by(&.published)
+ .reverse
+
+ POPULAR_VIDEOS.set(videos)
+
+ sleep 1.minute
+ Fiber.yield
+ end
+ end
+end
diff --git a/src/invidious/playlists.cr b/src/invidious/playlists.cr
index 9190e4e6..c984a12a 100644
--- a/src/invidious/playlists.cr
+++ b/src/invidious/playlists.cr
@@ -483,7 +483,7 @@ def extract_playlist_videos(initial_data : Hash(String, JSON::Any))
published: Time.utc,
plid: plid,
live_now: live,
- index: index - 1,
+ index: index,
})
end
end
diff --git a/src/invidious/routes/base_route.cr b/src/invidious/routes/base_route.cr
new file mode 100644
index 00000000..576c1746
--- /dev/null
+++ b/src/invidious/routes/base_route.cr
@@ -0,0 +1,8 @@
+abstract class Invidious::Routes::BaseRoute
+ private getter config : Config
+
+ def initialize(@config)
+ end
+
+ abstract def handle(env)
+end
diff --git a/src/invidious/routes/home.cr b/src/invidious/routes/home.cr
new file mode 100644
index 00000000..9b1bf61b
--- /dev/null
+++ b/src/invidious/routes/home.cr
@@ -0,0 +1,34 @@
+class Invidious::Routes::Home < Invidious::Routes::BaseRoute
+ def handle(env)
+ preferences = env.get("preferences").as(Preferences)
+ locale = LOCALES[preferences.locale]?
+ user = env.get? "user"
+
+ case preferences.default_home
+ when ""
+ templated "empty"
+ when "Popular"
+ templated "popular"
+ when "Trending"
+ env.redirect "/feed/trending"
+ when "Subscriptions"
+ if user
+ env.redirect "/feed/subscriptions"
+ else
+ templated "popular"
+ end
+ when "Playlists"
+ if user
+ env.redirect "/view_all_playlists"
+ else
+ templated "popular"
+ end
+ else
+ templated "empty"
+ end
+ end
+
+ private def popular_videos
+ Jobs::PullPopularVideosJob::POPULAR_VIDEOS.get
+ end
+end
diff --git a/src/invidious/routes/licenses.cr b/src/invidious/routes/licenses.cr
new file mode 100644
index 00000000..38fde7bb
--- /dev/null
+++ b/src/invidious/routes/licenses.cr
@@ -0,0 +1,6 @@
+class Invidious::Routes::Licenses < Invidious::Routes::BaseRoute
+ def handle(env)
+ locale = LOCALES[env.get("preferences").as(Preferences).locale]?
+ rendered "licenses"
+ end
+end
diff --git a/src/invidious/routes/privacy.cr b/src/invidious/routes/privacy.cr
new file mode 100644
index 00000000..4565c94c
--- /dev/null
+++ b/src/invidious/routes/privacy.cr
@@ -0,0 +1,6 @@
+class Invidious::Routes::Privacy < Invidious::Routes::BaseRoute
+ def handle(env)
+ locale = LOCALES[env.get("preferences").as(Preferences).locale]?
+ templated "privacy"
+ end
+end
diff --git a/src/invidious/routing.cr b/src/invidious/routing.cr
new file mode 100644
index 00000000..a096db44
--- /dev/null
+++ b/src/invidious/routing.cr
@@ -0,0 +1,8 @@
+module Invidious::Routing
+ macro get(path, controller)
+ get {{ path }} do |env|
+ controller_instance = {{ controller }}.new(config)
+ controller_instance.handle(env)
+ end
+ end
+end
diff --git a/src/invidious/views/components/feed_menu.ecr b/src/invidious/views/components/feed_menu.ecr
index f72db2da..3dbeaf37 100644
--- a/src/invidious/views/components/feed_menu.ecr
+++ b/src/invidious/views/components/feed_menu.ecr
@@ -1,19 +1,11 @@
-<div class="h-box pure-g">
- <div class="pure-u-1 pure-u-md-1-4"></div>
- <div class="pure-u-1 pure-u-md-1-2">
- <div class="pure-g">
- <% feed_menu = env.get("preferences").as(Preferences).feed_menu.dup %>
- <% if !env.get?("user") %>
- <% feed_menu.reject! {|item| {"Subscriptions", "Playlists"}.includes? item} %>
- <% end %>
- <% feed_menu.each do |feed| %>
- <div class="pure-u-1-2 pure-u-md-1-<%= feed_menu.size %>">
- <a href="/feed/<%= feed.downcase %>" class="pure-menu-heading" style="text-align:center">
- <%= translate(locale, feed) %>
- </a>
- </div>
- <% end %>
- </div>
- </div>
- <div class="pure-u-1 pure-u-md-1-4"></div>
+<div class="feed-menu">
+ <% feed_menu = env.get("preferences").as(Preferences).feed_menu.dup %>
+ <% if !env.get?("user") %>
+ <% feed_menu.reject! {|item| {"Subscriptions", "Playlists"}.includes? item} %>
+ <% end %>
+ <% feed_menu.each do |feed| %>
+ <a href="/feed/<%= feed.downcase %>" class="feed-menu-item pure-menu-heading">
+ <%= translate(locale, feed) %>
+ </a>
+ <% end %>
</div>
diff --git a/src/invidious/views/components/player.ecr b/src/invidious/views/components/player.ecr
index 6b01d25f..0e6664fa 100644
--- a/src/invidious/views/components/player.ecr
+++ b/src/invidious/views/components/player.ecr
@@ -1,4 +1,4 @@
-<video style="outline:none;width:100%;background-color:#000" playsinline poster="<%= thumbnail %>" title="<%= HTML.escape(video.title) %>"
+<video style="outline:none;width:100%;background-color:#000" playsinline poster="<%= thumbnail %>"
id="player" class="on-video_player video-js player-style-<%= params.player_style %>"
<% if params.autoplay %>autoplay<% end %>
<% if params.video_loop %>loop<% end %>