diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/invidious.cr | 67 | ||||
| -rw-r--r-- | src/invidious/channels.cr | 86 | ||||
| -rw-r--r-- | src/invidious/helpers/helpers.cr | 16 | ||||
| -rw-r--r-- | src/invidious/helpers/jobs.cr | 4 | ||||
| -rw-r--r-- | src/invidious/jobs.cr | 13 | ||||
| -rw-r--r-- | src/invidious/jobs/base_job.cr | 3 | ||||
| -rw-r--r-- | src/invidious/jobs/pull_popular_videos_job.cr | 27 | ||||
| -rw-r--r-- | src/invidious/playlists.cr | 2 | ||||
| -rw-r--r-- | src/invidious/routes/base_route.cr | 8 | ||||
| -rw-r--r-- | src/invidious/routes/home.cr | 34 | ||||
| -rw-r--r-- | src/invidious/routes/licenses.cr | 6 | ||||
| -rw-r--r-- | src/invidious/routes/privacy.cr | 6 | ||||
| -rw-r--r-- | src/invidious/routing.cr | 8 | ||||
| -rw-r--r-- | src/invidious/views/components/feed_menu.ecr | 28 | ||||
| -rw-r--r-- | src/invidious/views/components/player.ecr | 2 |
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 %> |
