Merge pull request #2685 from SamantazFox/database-improvments

Database improvements
This commit is contained in:
TheFrenchGhosty 2022-01-05 09:40:39 +00:00 committed by GitHub
commit 326a362eb8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
35 changed files with 1237 additions and 447 deletions

View File

@ -20,12 +20,13 @@ require "kemal"
require "athena-negotiation" require "athena-negotiation"
require "openssl/hmac" require "openssl/hmac"
require "option_parser" require "option_parser"
require "pg"
require "sqlite3" require "sqlite3"
require "xml" require "xml"
require "yaml" require "yaml"
require "compress/zip" require "compress/zip"
require "protodec/utils" require "protodec/utils"
require "./invidious/database/*"
require "./invidious/helpers/*" require "./invidious/helpers/*"
require "./invidious/yt_backend/*" require "./invidious/yt_backend/*"
require "./invidious/*" require "./invidious/*"
@ -112,19 +113,19 @@ LOGGER = Invidious::LogHandler.new(OUTPUT, CONFIG.log_level)
# Check table integrity # Check table integrity
if CONFIG.check_tables if CONFIG.check_tables
check_enum(PG_DB, "privacy", PlaylistPrivacy) Invidious::Database.check_enum(PG_DB, "privacy", PlaylistPrivacy)
check_table(PG_DB, "channels", InvidiousChannel) Invidious::Database.check_table(PG_DB, "channels", InvidiousChannel)
check_table(PG_DB, "channel_videos", ChannelVideo) Invidious::Database.check_table(PG_DB, "channel_videos", ChannelVideo)
check_table(PG_DB, "playlists", InvidiousPlaylist) Invidious::Database.check_table(PG_DB, "playlists", InvidiousPlaylist)
check_table(PG_DB, "playlist_videos", PlaylistVideo) Invidious::Database.check_table(PG_DB, "playlist_videos", PlaylistVideo)
check_table(PG_DB, "nonces", Nonce) Invidious::Database.check_table(PG_DB, "nonces", Nonce)
check_table(PG_DB, "session_ids", SessionId) Invidious::Database.check_table(PG_DB, "session_ids", SessionId)
check_table(PG_DB, "users", User) Invidious::Database.check_table(PG_DB, "users", User)
check_table(PG_DB, "videos", Video) Invidious::Database.check_table(PG_DB, "videos", Video)
if CONFIG.cache_annotations if CONFIG.cache_annotations
check_table(PG_DB, "annotations", Annotation) Invidious::Database.check_table(PG_DB, "annotations", Annotation)
end end
end end
@ -246,8 +247,8 @@ before_all do |env|
# Invidious users only have SID # Invidious users only have SID
if !env.request.cookies.has_key? "SSID" if !env.request.cookies.has_key? "SSID"
if email = PG_DB.query_one?("SELECT email FROM session_ids WHERE id = $1", sid, as: String) if email = Invidious::Database::SessionIDs.select_email(sid)
user = PG_DB.query_one("SELECT * FROM users WHERE email = $1", email, as: User) user = Invidious::Database::Users.select!(email: email)
csrf_token = generate_response(sid, { csrf_token = generate_response(sid, {
":authorize_token", ":authorize_token",
":playlist_ajax", ":playlist_ajax",
@ -255,7 +256,7 @@ before_all do |env|
":subscription_ajax", ":subscription_ajax",
":token_ajax", ":token_ajax",
":watch_ajax", ":watch_ajax",
}, HMAC_KEY, PG_DB, 1.week) }, HMAC_KEY, 1.week)
preferences = user.preferences preferences = user.preferences
env.set "preferences", preferences env.set "preferences", preferences
@ -269,7 +270,7 @@ before_all do |env|
headers["Cookie"] = env.request.headers["Cookie"] headers["Cookie"] = env.request.headers["Cookie"]
begin begin
user, sid = get_user(sid, headers, PG_DB, false) user, sid = get_user(sid, headers, false)
csrf_token = generate_response(sid, { csrf_token = generate_response(sid, {
":authorize_token", ":authorize_token",
":playlist_ajax", ":playlist_ajax",
@ -277,7 +278,7 @@ before_all do |env|
":subscription_ajax", ":subscription_ajax",
":token_ajax", ":token_ajax",
":watch_ajax", ":watch_ajax",
}, HMAC_KEY, PG_DB, 1.week) }, HMAC_KEY, 1.week)
preferences = user.preferences preferences = user.preferences
env.set "preferences", preferences env.set "preferences", preferences
@ -437,7 +438,7 @@ post "/watch_ajax" do |env|
end end
begin begin
validate_request(token, sid, env.request, HMAC_KEY, PG_DB, locale) validate_request(token, sid, env.request, HMAC_KEY, locale)
rescue ex rescue ex
if redirect if redirect
next error_template(400, ex) next error_template(400, ex)
@ -457,10 +458,10 @@ post "/watch_ajax" do |env|
case action case action
when "action_mark_watched" when "action_mark_watched"
if !user.watched.includes? id if !user.watched.includes? id
PG_DB.exec("UPDATE users SET watched = array_append(watched, $1) WHERE email = $2", id, user.email) Invidious::Database::Users.mark_watched(user, id)
end end
when "action_mark_unwatched" when "action_mark_unwatched"
PG_DB.exec("UPDATE users SET watched = array_remove(watched, $1) WHERE email = $2", id, user.email) Invidious::Database::Users.mark_unwatched(user, id)
else else
next error_json(400, "Unsupported action #{action}") next error_json(400, "Unsupported action #{action}")
end end
@ -574,7 +575,7 @@ post "/subscription_ajax" do |env|
token = env.params.body["csrf_token"]? token = env.params.body["csrf_token"]?
begin begin
validate_request(token, sid, env.request, HMAC_KEY, PG_DB, locale) validate_request(token, sid, env.request, HMAC_KEY, locale)
rescue ex rescue ex
if redirect if redirect
next error_template(400, ex) next error_template(400, ex)
@ -598,16 +599,15 @@ post "/subscription_ajax" do |env|
# Sync subscriptions with YouTube # Sync subscriptions with YouTube
subscribe_ajax(channel_id, action, env.request.headers) subscribe_ajax(channel_id, action, env.request.headers)
end end
email = user.email
case action case action
when "action_create_subscription_to_channel" when "action_create_subscription_to_channel"
if !user.subscriptions.includes? channel_id if !user.subscriptions.includes? channel_id
get_channel(channel_id, PG_DB, false, false) get_channel(channel_id, false, false)
PG_DB.exec("UPDATE users SET feed_needs_update = true, subscriptions = array_append(subscriptions, $1) WHERE email = $2", channel_id, email) Invidious::Database::Users.subscribe_channel(user, channel_id)
end end
when "action_remove_subscriptions" when "action_remove_subscriptions"
PG_DB.exec("UPDATE users SET feed_needs_update = true, subscriptions = array_remove(subscriptions, $1) WHERE email = $2", channel_id, email) Invidious::Database::Users.unsubscribe_channel(user, channel_id)
else else
next error_json(400, "Unsupported action #{action}") next error_json(400, "Unsupported action #{action}")
end end
@ -632,13 +632,14 @@ get "/subscription_manager" do |env|
end end
user = user.as(User) user = user.as(User)
sid = sid.as(String)
if !user.password if !user.password
# Refresh account # Refresh account
headers = HTTP::Headers.new headers = HTTP::Headers.new
headers["Cookie"] = env.request.headers["Cookie"] headers["Cookie"] = env.request.headers["Cookie"]
user, sid = get_user(sid, headers, PG_DB) user, sid = get_user(sid, headers)
end end
action_takeout = env.params.query["action_takeout"]?.try &.to_i? action_takeout = env.params.query["action_takeout"]?.try &.to_i?
@ -648,20 +649,14 @@ get "/subscription_manager" do |env|
format = env.params.query["format"]? format = env.params.query["format"]?
format ||= "rss" format ||= "rss"
if user.subscriptions.empty? subscriptions = Invidious::Database::Channels.select(user.subscriptions)
values = "'{}'"
else
values = "VALUES #{user.subscriptions.map { |id| %(('#{id}')) }.join(",")}"
end
subscriptions = PG_DB.query_all("SELECT * FROM channels WHERE id = ANY(#{values})", as: InvidiousChannel)
subscriptions.sort_by!(&.author.downcase) subscriptions.sort_by!(&.author.downcase)
if action_takeout if action_takeout
if format == "json" if format == "json"
env.response.content_type = "application/json" env.response.content_type = "application/json"
env.response.headers["content-disposition"] = "attachment" env.response.headers["content-disposition"] = "attachment"
playlists = PG_DB.query_all("SELECT * FROM playlists WHERE author = $1 AND id LIKE 'IV%' ORDER BY created", user.email, as: InvidiousPlaylist) playlists = Invidious::Database::Playlists.select_like_iv(user.email)
next JSON.build do |json| next JSON.build do |json|
json.object do json.object do
@ -677,7 +672,7 @@ get "/subscription_manager" do |env|
json.field "privacy", playlist.privacy.to_s json.field "privacy", playlist.privacy.to_s
json.field "videos" do json.field "videos" do
json.array do json.array do
PG_DB.query_all("SELECT id FROM playlist_videos WHERE plid = $1 ORDER BY array_position($2, index) LIMIT 500", playlist.id, playlist.index, as: String).each do |video_id| Invidious::Database::PlaylistVideos.select_ids(playlist.id, playlist.index, limit: 500).each do |video_id|
json.string video_id json.string video_id
end end
end end
@ -762,20 +757,20 @@ post "/data_control" do |env|
user.subscriptions += body["subscriptions"].as_a.map(&.as_s) user.subscriptions += body["subscriptions"].as_a.map(&.as_s)
user.subscriptions.uniq! user.subscriptions.uniq!
user.subscriptions = get_batch_channels(user.subscriptions, PG_DB, false, false) user.subscriptions = get_batch_channels(user.subscriptions, false, false)
PG_DB.exec("UPDATE users SET feed_needs_update = true, subscriptions = $1 WHERE email = $2", user.subscriptions, user.email) Invidious::Database::Users.update_subscriptions(user)
end end
if body["watch_history"]? if body["watch_history"]?
user.watched += body["watch_history"].as_a.map(&.as_s) user.watched += body["watch_history"].as_a.map(&.as_s)
user.watched.uniq! user.watched.uniq!
PG_DB.exec("UPDATE users SET watched = $1 WHERE email = $2", user.watched, user.email) Invidious::Database::Users.update_watch_history(user)
end end
if body["preferences"]? if body["preferences"]?
user.preferences = Preferences.from_json(body["preferences"].to_json) user.preferences = Preferences.from_json(body["preferences"].to_json)
PG_DB.exec("UPDATE users SET preferences = $1 WHERE email = $2", user.preferences.to_json, user.email) Invidious::Database::Users.update_preferences(user)
end end
if playlists = body["playlists"]?.try &.as_a? if playlists = body["playlists"]?.try &.as_a?
@ -788,8 +783,8 @@ post "/data_control" do |env|
next if !description next if !description
next if !privacy next if !privacy
playlist = create_playlist(PG_DB, title, privacy, user) playlist = create_playlist(title, privacy, user)
PG_DB.exec("UPDATE playlists SET description = $1 WHERE id = $2", description, playlist.id) Invidious::Database::Playlists.update_description(playlist.id, description)
videos = item["videos"]?.try &.as_a?.try &.each_with_index do |video_id, idx| videos = item["videos"]?.try &.as_a?.try &.each_with_index do |video_id, idx|
raise InfoException.new("Playlist cannot have more than 500 videos") if idx > 500 raise InfoException.new("Playlist cannot have more than 500 videos") if idx > 500
@ -798,7 +793,7 @@ post "/data_control" do |env|
next if !video_id next if !video_id
begin begin
video = get_video(video_id, PG_DB) video = get_video(video_id)
rescue ex rescue ex
next next
end end
@ -815,11 +810,8 @@ post "/data_control" do |env|
index: Random::Secure.rand(0_i64..Int64::MAX), index: Random::Secure.rand(0_i64..Int64::MAX),
}) })
video_array = playlist_video.to_a Invidious::Database::PlaylistVideos.insert(playlist_video)
args = arg_array(video_array) Invidious::Database::Playlists.update_video_added(playlist.id, playlist_video.index)
PG_DB.exec("INSERT INTO playlist_videos VALUES (#{args})", args: video_array)
PG_DB.exec("UPDATE playlists SET index = array_append(index, $1), video_count = cardinality(index) + 1, updated = $2 WHERE id = $3", playlist_video.index, Time.utc, playlist.id)
end end
end end
end end
@ -837,18 +829,18 @@ post "/data_control" do |env|
end end
user.subscriptions.uniq! user.subscriptions.uniq!
user.subscriptions = get_batch_channels(user.subscriptions, PG_DB, false, false) user.subscriptions = get_batch_channels(user.subscriptions, false, false)
PG_DB.exec("UPDATE users SET feed_needs_update = true, subscriptions = $1 WHERE email = $2", user.subscriptions, user.email) Invidious::Database::Users.update_subscriptions(user)
when "import_freetube" when "import_freetube"
user.subscriptions += body.scan(/"channelId":"(?<channel_id>[a-zA-Z0-9_-]{24})"/).map do |md| user.subscriptions += body.scan(/"channelId":"(?<channel_id>[a-zA-Z0-9_-]{24})"/).map do |md|
md["channel_id"] md["channel_id"]
end end
user.subscriptions.uniq! user.subscriptions.uniq!
user.subscriptions = get_batch_channels(user.subscriptions, PG_DB, false, false) user.subscriptions = get_batch_channels(user.subscriptions, false, false)
PG_DB.exec("UPDATE users SET feed_needs_update = true, subscriptions = $1 WHERE email = $2", user.subscriptions, user.email) Invidious::Database::Users.update_subscriptions(user)
when "import_newpipe_subscriptions" when "import_newpipe_subscriptions"
body = JSON.parse(body) body = JSON.parse(body)
user.subscriptions += body["subscriptions"].as_a.compact_map do |channel| user.subscriptions += body["subscriptions"].as_a.compact_map do |channel|
@ -865,9 +857,9 @@ post "/data_control" do |env|
end end
user.subscriptions.uniq! user.subscriptions.uniq!
user.subscriptions = get_batch_channels(user.subscriptions, PG_DB, false, false) user.subscriptions = get_batch_channels(user.subscriptions, false, false)
PG_DB.exec("UPDATE users SET feed_needs_update = true, subscriptions = $1 WHERE email = $2", user.subscriptions, user.email) Invidious::Database::Users.update_subscriptions(user)
when "import_newpipe" when "import_newpipe"
Compress::Zip::Reader.open(IO::Memory.new(body)) do |file| Compress::Zip::Reader.open(IO::Memory.new(body)) do |file|
file.each_entry do |entry| file.each_entry do |entry|
@ -879,14 +871,14 @@ post "/data_control" do |env|
user.watched += db.query_all("SELECT url FROM streams", as: String).map(&.lchop("https://www.youtube.com/watch?v=")) user.watched += db.query_all("SELECT url FROM streams", as: String).map(&.lchop("https://www.youtube.com/watch?v="))
user.watched.uniq! user.watched.uniq!
PG_DB.exec("UPDATE users SET watched = $1 WHERE email = $2", user.watched, user.email) Invidious::Database::Users.update_watch_history(user)
user.subscriptions += db.query_all("SELECT url FROM subscriptions", as: String).map(&.lchop("https://www.youtube.com/channel/")) user.subscriptions += db.query_all("SELECT url FROM subscriptions", as: String).map(&.lchop("https://www.youtube.com/channel/"))
user.subscriptions.uniq! user.subscriptions.uniq!
user.subscriptions = get_batch_channels(user.subscriptions, PG_DB, false, false) user.subscriptions = get_batch_channels(user.subscriptions, false, false)
PG_DB.exec("UPDATE users SET feed_needs_update = true, subscriptions = $1 WHERE email = $2", user.subscriptions, user.email) Invidious::Database::Users.update_subscriptions(user)
db.close db.close
tempfile.delete tempfile.delete
@ -914,7 +906,7 @@ get "/change_password" do |env|
user = user.as(User) user = user.as(User)
sid = sid.as(String) sid = sid.as(String)
csrf_token = generate_response(sid, {":change_password"}, HMAC_KEY, PG_DB) csrf_token = generate_response(sid, {":change_password"}, HMAC_KEY)
templated "change_password" templated "change_password"
end end
@ -940,7 +932,7 @@ post "/change_password" do |env|
end end
begin begin
validate_request(token, sid, env.request, HMAC_KEY, PG_DB, locale) validate_request(token, sid, env.request, HMAC_KEY, locale)
rescue ex rescue ex
next error_template(400, ex) next error_template(400, ex)
end end
@ -970,7 +962,7 @@ post "/change_password" do |env|
end end
new_password = Crypto::Bcrypt::Password.create(new_password, cost: 10) new_password = Crypto::Bcrypt::Password.create(new_password, cost: 10)
PG_DB.exec("UPDATE users SET password = $1 WHERE email = $2", new_password.to_s, user.email) Invidious::Database::Users.update_password(user, new_password.to_s)
env.redirect referer env.redirect referer
end end
@ -988,7 +980,7 @@ get "/delete_account" do |env|
user = user.as(User) user = user.as(User)
sid = sid.as(String) sid = sid.as(String)
csrf_token = generate_response(sid, {":delete_account"}, HMAC_KEY, PG_DB) csrf_token = generate_response(sid, {":delete_account"}, HMAC_KEY)
templated "delete_account" templated "delete_account"
end end
@ -1009,14 +1001,14 @@ post "/delete_account" do |env|
token = env.params.body["csrf_token"]? token = env.params.body["csrf_token"]?
begin begin
validate_request(token, sid, env.request, HMAC_KEY, PG_DB, locale) validate_request(token, sid, env.request, HMAC_KEY, locale)
rescue ex rescue ex
next error_template(400, ex) next error_template(400, ex)
end end
view_name = "subscriptions_#{sha256(user.email)}" view_name = "subscriptions_#{sha256(user.email)}"
PG_DB.exec("DELETE FROM users * WHERE email = $1", user.email) Invidious::Database::Users.delete(user)
PG_DB.exec("DELETE FROM session_ids * WHERE email = $1", user.email) Invidious::Database::SessionIDs.delete(email: user.email)
PG_DB.exec("DROP MATERIALIZED VIEW #{view_name}") PG_DB.exec("DROP MATERIALIZED VIEW #{view_name}")
env.request.cookies.each do |cookie| env.request.cookies.each do |cookie|
@ -1040,7 +1032,7 @@ get "/clear_watch_history" do |env|
user = user.as(User) user = user.as(User)
sid = sid.as(String) sid = sid.as(String)
csrf_token = generate_response(sid, {":clear_watch_history"}, HMAC_KEY, PG_DB) csrf_token = generate_response(sid, {":clear_watch_history"}, HMAC_KEY)
templated "clear_watch_history" templated "clear_watch_history"
end end
@ -1061,12 +1053,12 @@ post "/clear_watch_history" do |env|
token = env.params.body["csrf_token"]? token = env.params.body["csrf_token"]?
begin begin
validate_request(token, sid, env.request, HMAC_KEY, PG_DB, locale) validate_request(token, sid, env.request, HMAC_KEY, locale)
rescue ex rescue ex
next error_template(400, ex) next error_template(400, ex)
end end
PG_DB.exec("UPDATE users SET watched = '{}' WHERE email = $1", user.email) Invidious::Database::Users.clear_watch_history(user)
env.redirect referer env.redirect referer
end end
@ -1083,7 +1075,7 @@ get "/authorize_token" do |env|
user = user.as(User) user = user.as(User)
sid = sid.as(String) sid = sid.as(String)
csrf_token = generate_response(sid, {":authorize_token"}, HMAC_KEY, PG_DB) csrf_token = generate_response(sid, {":authorize_token"}, HMAC_KEY)
scopes = env.params.query["scopes"]?.try &.split(",") scopes = env.params.query["scopes"]?.try &.split(",")
scopes ||= [] of String scopes ||= [] of String
@ -1114,7 +1106,7 @@ post "/authorize_token" do |env|
token = env.params.body["csrf_token"]? token = env.params.body["csrf_token"]?
begin begin
validate_request(token, sid, env.request, HMAC_KEY, PG_DB, locale) validate_request(token, sid, env.request, HMAC_KEY, locale)
rescue ex rescue ex
next error_template(400, ex) next error_template(400, ex)
end end
@ -1123,7 +1115,7 @@ post "/authorize_token" do |env|
callback_url = env.params.body["callbackUrl"]? callback_url = env.params.body["callbackUrl"]?
expire = env.params.body["expire"]?.try &.to_i? expire = env.params.body["expire"]?.try &.to_i?
access_token = generate_token(user.email, scopes, expire, HMAC_KEY, PG_DB) access_token = generate_token(user.email, scopes, expire, HMAC_KEY)
if callback_url if callback_url
access_token = URI.encode_www_form(access_token) access_token = URI.encode_www_form(access_token)
@ -1158,8 +1150,7 @@ get "/token_manager" do |env|
end end
user = user.as(User) user = user.as(User)
tokens = Invidious::Database::SessionIDs.select_all(user.email)
tokens = PG_DB.query_all("SELECT id, issued FROM session_ids WHERE email = $1 ORDER BY issued DESC", user.email, as: {session: String, issued: Time})
templated "token_manager" templated "token_manager"
end end
@ -1188,7 +1179,7 @@ post "/token_ajax" do |env|
token = env.params.body["csrf_token"]? token = env.params.body["csrf_token"]?
begin begin
validate_request(token, sid, env.request, HMAC_KEY, PG_DB, locale) validate_request(token, sid, env.request, HMAC_KEY, locale)
rescue ex rescue ex
if redirect if redirect
next error_template(400, ex) next error_template(400, ex)
@ -1208,7 +1199,7 @@ post "/token_ajax" do |env|
case action case action
when .starts_with? "action_revoke_token" when .starts_with? "action_revoke_token"
PG_DB.exec("DELETE FROM session_ids * WHERE id = $1 AND email = $2", session, user.email) Invidious::Database::SessionIDs.delete(sid: session, email: user.email)
else else
next error_json(400, "Unsupported action #{action}") next error_json(400, "Unsupported action #{action}")
end end

View File

@ -114,7 +114,7 @@ class ChannelRedirect < Exception
end end
end end
def get_batch_channels(channels, db, refresh = false, pull_all_videos = true, max_threads = 10) def get_batch_channels(channels, refresh = false, pull_all_videos = true, max_threads = 10)
finished_channel = Channel(String | Nil).new finished_channel = Channel(String | Nil).new
spawn do spawn do
@ -130,7 +130,7 @@ def get_batch_channels(channels, db, refresh = false, pull_all_videos = true, ma
active_threads += 1 active_threads += 1
spawn do spawn do
begin begin
get_channel(ucid, db, refresh, pull_all_videos) get_channel(ucid, refresh, pull_all_videos)
finished_channel.send(ucid) finished_channel.send(ucid)
rescue ex rescue ex
finished_channel.send(nil) finished_channel.send(nil)
@ -151,28 +151,21 @@ def get_batch_channels(channels, db, refresh = false, pull_all_videos = true, ma
return final return final
end end
def get_channel(id, db, refresh = true, pull_all_videos = true) def get_channel(id, refresh = true, pull_all_videos = true)
if channel = db.query_one?("SELECT * FROM channels WHERE id = $1", id, as: InvidiousChannel) if channel = Invidious::Database::Channels.select(id)
if refresh && Time.utc - channel.updated > 10.minutes if refresh && Time.utc - channel.updated > 10.minutes
channel = fetch_channel(id, db, pull_all_videos: pull_all_videos) channel = fetch_channel(id, pull_all_videos: pull_all_videos)
channel_array = channel.to_a Invidious::Database::Channels.insert(channel, update_on_conflict: true)
args = arg_array(channel_array)
db.exec("INSERT INTO channels VALUES (#{args}) \
ON CONFLICT (id) DO UPDATE SET author = $2, updated = $3", args: channel_array)
end end
else else
channel = fetch_channel(id, db, pull_all_videos: pull_all_videos) channel = fetch_channel(id, pull_all_videos: pull_all_videos)
channel_array = channel.to_a Invidious::Database::Channels.insert(channel)
args = arg_array(channel_array)
db.exec("INSERT INTO channels VALUES (#{args})", args: channel_array)
end end
return channel return channel
end end
def fetch_channel(ucid, db, pull_all_videos = true, locale = nil) def fetch_channel(ucid, pull_all_videos = true, locale = nil)
LOGGER.debug("fetch_channel: #{ucid}") LOGGER.debug("fetch_channel: #{ucid}")
LOGGER.trace("fetch_channel: #{ucid} : pull_all_videos = #{pull_all_videos}, locale = #{locale}") LOGGER.trace("fetch_channel: #{ucid} : pull_all_videos = #{pull_all_videos}, locale = #{locale}")
@ -241,15 +234,11 @@ def fetch_channel(ucid, db, pull_all_videos = true, locale = nil)
# We don't include the 'premiere_timestamp' here because channel pages don't include them, # We don't include the 'premiere_timestamp' here because channel pages don't include them,
# meaning the above timestamp is always null # meaning the above timestamp is always null
was_insert = db.query_one("INSERT INTO channel_videos VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) \ was_insert = Invidious::Database::ChannelVideos.insert(video)
ON CONFLICT (id) DO UPDATE SET title = $2, published = $3, \
updated = $4, ucid = $5, author = $6, length_seconds = $7, \
live_now = $8, views = $10 returning (xmax=0) as was_insert", *video.to_tuple, as: Bool)
if was_insert if was_insert
LOGGER.trace("fetch_channel: #{ucid} : video #{video_id} : Inserted, updating subscriptions") LOGGER.trace("fetch_channel: #{ucid} : video #{video_id} : Inserted, updating subscriptions")
db.exec("UPDATE users SET notifications = array_append(notifications, $1), \ Invidious::Database::Users.add_notification(video)
feed_needs_update = true WHERE $2 = ANY(subscriptions)", video.id, video.ucid)
else else
LOGGER.trace("fetch_channel: #{ucid} : video #{video_id} : Updated") LOGGER.trace("fetch_channel: #{ucid} : video #{video_id} : Updated")
end end
@ -284,13 +273,8 @@ 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, # 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. # so since they don't provide a published date here we can safely ignore them.
if Time.utc - video.published > 1.minute if Time.utc - video.published > 1.minute
was_insert = db.query_one("INSERT INTO channel_videos VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) \ was_insert = Invidious::Database::ChannelVideos.insert(video)
ON CONFLICT (id) DO UPDATE SET title = $2, published = $3, \ Invidious::Database::Users.add_notification(video) if was_insert
updated = $4, ucid = $5, author = $6, length_seconds = $7, \
live_now = $8, views = $10 returning (xmax=0) as was_insert", *video.to_tuple, as: Bool)
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
end end

View File

@ -0,0 +1,24 @@
require "./base.cr"
module Invidious::Database::Annotations
extend self
def insert(id : String, annotations : String)
request = <<-SQL
INSERT INTO annotations
VALUES ($1, $2)
ON CONFLICT DO NOTHING
SQL
PG_DB.exec(request, id, annotations)
end
def select(id : String) : Annotation?
request = <<-SQL
SELECT * FROM annotations
WHERE id = $1
SQL
return PG_DB.query_one?(request, id, as: Annotation)
end
end

View File

@ -0,0 +1,110 @@
require "pg"
module Invidious::Database
extend self
def check_enum(db, enum_name, struct_type = nil)
return # TODO
if !db.query_one?("SELECT true FROM pg_type WHERE typname = $1", enum_name, as: Bool)
LOGGER.info("check_enum: CREATE TYPE #{enum_name}")
db.using_connection do |conn|
conn.as(PG::Connection).exec_all(File.read("config/sql/#{enum_name}.sql"))
end
end
end
def check_table(db, table_name, struct_type = nil)
# Create table if it doesn't exist
begin
db.exec("SELECT * FROM #{table_name} LIMIT 0")
rescue ex
LOGGER.info("check_table: check_table: CREATE TABLE #{table_name}")
db.using_connection do |conn|
conn.as(PG::Connection).exec_all(File.read("config/sql/#{table_name}.sql"))
end
end
return if !struct_type
struct_array = struct_type.type_array
column_array = get_column_array(db, table_name)
column_types = File.read("config/sql/#{table_name}.sql").match(/CREATE TABLE public\.#{table_name}\n\((?<types>[\d\D]*?)\);/)
.try &.["types"].split(",").map(&.strip).reject &.starts_with?("CONSTRAINT")
return if !column_types
struct_array.each_with_index do |name, i|
if name != column_array[i]?
if !column_array[i]?
new_column = column_types.select(&.starts_with?(name))[0]
LOGGER.info("check_table: ALTER TABLE #{table_name} ADD COLUMN #{new_column}")
db.exec("ALTER TABLE #{table_name} ADD COLUMN #{new_column}")
next
end
# Column doesn't exist
if !column_array.includes? name
new_column = column_types.select(&.starts_with?(name))[0]
db.exec("ALTER TABLE #{table_name} ADD COLUMN #{new_column}")
end
# Column exists but in the wrong position, rotate
if struct_array.includes? column_array[i]
until name == column_array[i]
new_column = column_types.select(&.starts_with?(column_array[i]))[0]?.try &.gsub("#{column_array[i]}", "#{column_array[i]}_new")
# There's a column we didn't expect
if !new_column
LOGGER.info("check_table: ALTER TABLE #{table_name} DROP COLUMN #{column_array[i]}")
db.exec("ALTER TABLE #{table_name} DROP COLUMN #{column_array[i]} CASCADE")
column_array = get_column_array(db, table_name)
next
end
LOGGER.info("check_table: ALTER TABLE #{table_name} ADD COLUMN #{new_column}")
db.exec("ALTER TABLE #{table_name} ADD COLUMN #{new_column}")
LOGGER.info("check_table: UPDATE #{table_name} SET #{column_array[i]}_new=#{column_array[i]}")
db.exec("UPDATE #{table_name} SET #{column_array[i]}_new=#{column_array[i]}")
LOGGER.info("check_table: ALTER TABLE #{table_name} DROP COLUMN #{column_array[i]} CASCADE")
db.exec("ALTER TABLE #{table_name} DROP COLUMN #{column_array[i]} CASCADE")
LOGGER.info("check_table: ALTER TABLE #{table_name} RENAME COLUMN #{column_array[i]}_new TO #{column_array[i]}")
db.exec("ALTER TABLE #{table_name} RENAME COLUMN #{column_array[i]}_new TO #{column_array[i]}")
column_array = get_column_array(db, table_name)
end
else
LOGGER.info("check_table: ALTER TABLE #{table_name} DROP COLUMN #{column_array[i]} CASCADE")
db.exec("ALTER TABLE #{table_name} DROP COLUMN #{column_array[i]} CASCADE")
end
end
end
return if column_array.size <= struct_array.size
column_array.each do |column|
if !struct_array.includes? column
LOGGER.info("check_table: ALTER TABLE #{table_name} DROP COLUMN #{column} CASCADE")
db.exec("ALTER TABLE #{table_name} DROP COLUMN #{column} CASCADE")
end
end
end
def get_column_array(db, table_name)
column_array = [] of String
db.query("SELECT * FROM #{table_name} LIMIT 0") do |rs|
rs.column_count.times do |i|
column = rs.as(PG::ResultSet).field(i)
column_array << column.name
end
end
return column_array
end
end

View File

@ -0,0 +1,149 @@
require "./base.cr"
#
# This module contains functions related to the "channels" table.
#
module Invidious::Database::Channels
extend self
# -------------------
# Insert / delete
# -------------------
def insert(channel : InvidiousChannel, update_on_conflict : Bool = false)
channel_array = channel.to_a
request = <<-SQL
INSERT INTO channels
VALUES (#{arg_array(channel_array)})
SQL
if update_on_conflict
request += <<-SQL
ON CONFLICT (id) DO UPDATE
SET author = $2, updated = $3
SQL
end
PG_DB.exec(request, args: channel_array)
end
# -------------------
# Update
# -------------------
def update_author(id : String, author : String)
request = <<-SQL
UPDATE channels
SET updated = $1, author = $2, deleted = false
WHERE id = $3
SQL
PG_DB.exec(request, Time.utc, author, id)
end
def update_mark_deleted(id : String)
request = <<-SQL
UPDATE channels
SET updated = $1, deleted = true
WHERE id = $2
SQL
PG_DB.exec(request, Time.utc, id)
end
# -------------------
# Select
# -------------------
def select(id : String) : InvidiousChannel?
request = <<-SQL
SELECT * FROM channels
WHERE id = $1
SQL
return PG_DB.query_one?(request, id, as: InvidiousChannel)
end
def select(ids : Array(String)) : Array(InvidiousChannel)?
return [] of InvidiousChannel if ids.empty?
values = ids.map { |id| %(('#{id}')) }.join(",")
request = <<-SQL
SELECT * FROM channels
WHERE id = ANY(VALUES #{values})
SQL
return PG_DB.query_all(request, as: InvidiousChannel)
end
end
#
# This module contains functions related to the "channel_videos" table.
#
module Invidious::Database::ChannelVideos
extend self
# -------------------
# Insert
# -------------------
# This function returns the status of the query (i.e: success?)
def insert(video : ChannelVideo, with_premiere_timestamp : Bool = false) : Bool
if with_premiere_timestamp
last_items = "premiere_timestamp = $9, views = $10"
else
last_items = "views = $10"
end
request = <<-SQL
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, #{last_items}
RETURNING (xmax=0) AS was_insert
SQL
return PG_DB.query_one(request, *video.to_tuple, as: Bool)
end
# -------------------
# Select
# -------------------
def select(ids : Array(String)) : Array(ChannelVideo)
return [] of ChannelVideo if ids.empty?
request = <<-SQL
SELECT * FROM channel_videos
WHERE id IN (#{arg_array(ids)})
ORDER BY published DESC
SQL
return PG_DB.query_all(request, args: ids, as: ChannelVideo)
end
def select_notfications(ucid : String, since : Time) : Array(ChannelVideo)
request = <<-SQL
SELECT * FROM channel_videos
WHERE ucid = $1 AND published > $2
ORDER BY published DESC
LIMIT 15
SQL
return PG_DB.query_all(request, ucid, since, as: ChannelVideo)
end
def select_popular_videos : Array(ChannelVideo)
request = <<-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
PG_DB.query_all(request, as: ChannelVideo)
end
end

View File

@ -0,0 +1,46 @@
require "./base.cr"
module Invidious::Database::Nonces
extend self
# -------------------
# Insert
# -------------------
def insert(nonce : String, expire : Time)
request = <<-SQL
INSERT INTO nonces
VALUES ($1, $2)
ON CONFLICT DO NOTHING
SQL
PG_DB.exec(request, nonce, expire)
end
# -------------------
# Update
# -------------------
def update_set_expired(nonce : String)
request = <<-SQL
UPDATE nonces
SET expire = $1
WHERE nonce = $2
SQL
PG_DB.exec(request, Time.utc(1990, 1, 1), nonce)
end
# -------------------
# Select
# -------------------
def select(nonce : String) : Tuple(String, Time)?
request = <<-SQL
SELECT * FROM nonces
WHERE nonce = $1
SQL
return PG_DB.query_one?(request, nonce, as: {String, Time})
end
end

View File

@ -0,0 +1,257 @@
require "./base.cr"
#
# This module contains functions related to the "playlists" table.
#
module Invidious::Database::Playlists
extend self
# -------------------
# Insert / delete
# -------------------
def insert(playlist : InvidiousPlaylist)
playlist_array = playlist.to_a
request = <<-SQL
INSERT INTO playlists
VALUES (#{arg_array(playlist_array)})
SQL
PG_DB.exec(request, args: playlist_array)
end
# this function is a bit special: it will also remove all videos
# related to the given playlist ID in the "playlist_videos" table,
# in addition to deleting said ID from "playlists".
def delete(id : String)
request = <<-SQL
DELETE FROM playlist_videos * WHERE plid = $1;
DELETE FROM playlists * WHERE id = $1
SQL
PG_DB.exec(request, id)
end
# -------------------
# Update
# -------------------
def update(id : String, title : String, privacy, description, updated)
request = <<-SQL
UPDATE playlists
SET title = $1, privacy = $2, description = $3, updated = $4
WHERE id = $5
SQL
PG_DB.exec(request, title, privacy, description, updated, id)
end
def update_description(id : String, description)
request = <<-SQL
UPDATE playlists
SET description = $1
WHERE id = $2
SQL
PG_DB.exec(request, description, id)
end
def update_subscription_time(id : String)
request = <<-SQL
UPDATE playlists
SET subscribed = $1
WHERE id = $2
SQL
PG_DB.exec(request, Time.utc, id)
end
def update_video_added(id : String, index : String | Int64)
request = <<-SQL
UPDATE playlists
SET index = array_append(index, $1),
video_count = cardinality(index) + 1,
updated = $2
WHERE id = $3
SQL
PG_DB.exec(request, index, Time.utc, id)
end
def update_video_removed(id : String, index : String | Int64)
request = <<-SQL
UPDATE playlists
SET index = array_remove(index, $1),
video_count = cardinality(index) - 1,
updated = $2
WHERE id = $3
SQL
PG_DB.exec(request, index, Time.utc, id)
end
# -------------------
# Salect
# -------------------
def select(*, id : String, raise_on_fail : Bool = false) : InvidiousPlaylist?
request = <<-SQL
SELECT * FROM playlists
WHERE id = $1
SQL
if raise_on_fail
return PG_DB.query_one(request, id, as: InvidiousPlaylist)
else
return PG_DB.query_one?(request, id, as: InvidiousPlaylist)
end
end
def select_all(*, author : String) : Array(InvidiousPlaylist)
request = <<-SQL
SELECT * FROM playlists
WHERE author = $1
SQL
return PG_DB.query_all(request, author, as: InvidiousPlaylist)
end
# -------------------
# Salect (filtered)
# -------------------
def select_like_iv(email : String) : Array(InvidiousPlaylist)
request = <<-SQL
SELECT * FROM playlists
WHERE author = $1 AND id LIKE 'IV%'
ORDER BY created
SQL
PG_DB.query_all(request, email, as: InvidiousPlaylist)
end
def select_not_like_iv(email : String) : Array(InvidiousPlaylist)
request = <<-SQL
SELECT * FROM playlists
WHERE author = $1 AND id NOT LIKE 'IV%'
ORDER BY created
SQL
PG_DB.query_all(request, email, as: InvidiousPlaylist)
end
def select_user_created_playlists(email : String) : Array({String, String})
request = <<-SQL
SELECT id,title FROM playlists
WHERE author = $1 AND id LIKE 'IV%'
SQL
PG_DB.query_all(request, email, as: {String, String})
end
# -------------------
# Misc checks
# -------------------
# Check if given playlist ID exists
def exists?(id : String) : Bool
request = <<-SQL
SELECT id FROM playlists
WHERE id = $1
SQL
return PG_DB.query_one?(request, id, as: String).nil?
end
# Count how many playlist a user has created.
def count_owned_by(author : String) : Int64
request = <<-SQL
SELECT count(*) FROM playlists
WHERE author = $1
SQL
return PG_DB.query_one?(request, author, as: Int64) || 0_i64
end
end
#
# This module contains functions related to the "playlist_videos" table.
#
module Invidious::Database::PlaylistVideos
extend self
private alias VideoIndex = Int64 | Array(Int64)
# -------------------
# Insert / Delete
# -------------------
def insert(video : PlaylistVideo)
video_array = video.to_a
request = <<-SQL
INSERT INTO playlist_videos
VALUES (#{arg_array(video_array)})
SQL
PG_DB.exec(request, args: video_array)
end
def delete(index)
request = <<-SQL
DELETE FROM playlist_videos *
WHERE index = $1
SQL
PG_DB.exec(request, index)
end
# -------------------
# Salect
# -------------------
def select(plid : String, index : VideoIndex, offset, limit = 100) : Array(PlaylistVideo)
request = <<-SQL
SELECT * FROM playlist_videos
WHERE plid = $1
ORDER BY array_position($2, index)
LIMIT $3
OFFSET $4
SQL
return PG_DB.query_all(request, plid, index, limit, offset, as: PlaylistVideo)
end
def select_index(plid : String, vid : String) : Int64?
request = <<-SQL
SELECT index FROM playlist_videos
WHERE plid = $1 AND id = $2
LIMIT 1
SQL
return PG_DB.query_one?(request, plid, vid, as: Int64)
end
def select_one_id(plid : String, index : VideoIndex) : String?
request = <<-SQL
SELECT id FROM playlist_videos
WHERE plid = $1
ORDER BY array_position($2, index)
LIMIT 1
SQL
return PG_DB.query_one?(request, plid, index, as: String)
end
def select_ids(plid : String, index : VideoIndex, limit = 500) : Array(String)
request = <<-SQL
SELECT id FROM playlist_videos
WHERE plid = $1
ORDER BY array_position($2, index)
LIMIT $3
SQL
return PG_DB.query_all(request, plid, index, limit, as: String)
end
end

View File

@ -0,0 +1,74 @@
require "./base.cr"
module Invidious::Database::SessionIDs
extend self
# -------------------
# Insert
# -------------------
def insert(sid : String, email : String, handle_conflicts : Bool = false)
request = <<-SQL
INSERT INTO session_ids
VALUES ($1, $2, $3)
SQL
request += " ON CONFLICT (id) DO NOTHING" if handle_conflicts
PG_DB.exec(request, sid, email, Time.utc)
end
# -------------------
# Delete
# -------------------
def delete(*, sid : String)
request = <<-SQL
DELETE FROM session_ids *
WHERE id = $1
SQL
PG_DB.exec(request, sid)
end
def delete(*, email : String)
request = <<-SQL
DELETE FROM session_ids *
WHERE email = $1
SQL
PG_DB.exec(request, email)
end
def delete(*, sid : String, email : String)
request = <<-SQL
DELETE FROM session_ids *
WHERE id = $1 AND email = $2
SQL
PG_DB.exec(request, sid, email)
end
# -------------------
# Select
# -------------------
def select_email(sid : String) : String?
request = <<-SQL
SELECT email FROM session_ids
WHERE id = $1
SQL
PG_DB.query_one?(request, sid, as: String)
end
def select_all(email : String) : Array({session: String, issued: Time})
request = <<-SQL
SELECT id, issued FROM session_ids
WHERE email = $1
ORDER BY issued DESC
SQL
PG_DB.query_all(request, email, as: {session: String, issued: Time})
end
end

View File

@ -0,0 +1,49 @@
require "./base.cr"
module Invidious::Database::Statistics
extend self
# -------------------
# User stats
# -------------------
def count_users_total : Int64
request = <<-SQL
SELECT count(*) FROM users
SQL
PG_DB.query_one(request, as: Int64)
end
def count_users_active_1m : Int64
request = <<-SQL
SELECT count(*) FROM users
WHERE CURRENT_TIMESTAMP - updated < '6 months'
SQL
PG_DB.query_one(request, as: Int64)
end
def count_users_active_6m : Int64
request = <<-SQL
SELECT count(*) FROM users
WHERE CURRENT_TIMESTAMP - updated < '1 month'
SQL
PG_DB.query_one(request, as: Int64)
end
# -------------------
# Channel stats
# -------------------
def channel_last_update : Time?
request = <<-SQL
SELECT updated FROM channels
ORDER BY updated DESC
LIMIT 1
SQL
PG_DB.query_one?(request, as: Time)
end
end

View File

@ -0,0 +1,218 @@
require "./base.cr"
module Invidious::Database::Users
extend self
# -------------------
# Insert / delete
# -------------------
def insert(user : User, update_on_conflict : Bool = false)
user_array = user.to_a
user_array[4] = user_array[4].to_json # User preferences
request = <<-SQL
INSERT INTO users
VALUES (#{arg_array(user_array)})
SQL
if update_on_conflict
request += <<-SQL
ON CONFLICT (email) DO UPDATE
SET updated = $1, subscriptions = $3
SQL
end
PG_DB.exec(request, args: user_array)
end
def delete(user : User)
request = <<-SQL
DELETE FROM users *
WHERE email = $1
SQL
PG_DB.exec(request, user.email)
end
# -------------------
# Update (history)
# -------------------
def update_watch_history(user : User)
request = <<-SQL
UPDATE users
SET watched = $1
WHERE email = $2
SQL
PG_DB.exec(request, user.watched, user.email)
end
def mark_watched(user : User, vid : String)
request = <<-SQL
UPDATE users
SET watched = array_append(watched, $1)
WHERE email = $2
SQL
PG_DB.exec(request, vid, user.email)
end
def mark_unwatched(user : User, vid : String)
request = <<-SQL
UPDATE users
SET watched = array_remove(watched, $1)
WHERE email = $2
SQL
PG_DB.exec(request, vid, user.email)
end
def clear_watch_history(user : User)
request = <<-SQL
UPDATE users
SET watched = '{}'
WHERE email = $1
SQL
PG_DB.exec(request, user.email)
end
# -------------------
# Update (channels)
# -------------------
def update_subscriptions(user : User)
request = <<-SQL
UPDATE users
SET feed_needs_update = true, subscriptions = $1
WHERE email = $2
SQL
PG_DB.exec(request, user.subscriptions, user.email)
end
def subscribe_channel(user : User, ucid : String)
request = <<-SQL
UPDATE users
SET feed_needs_update = true,
subscriptions = array_append(subscriptions,$1)
WHERE email = $2
SQL
PG_DB.exec(request, ucid, user.email)
end
def unsubscribe_channel(user : User, ucid : String)
request = <<-SQL
UPDATE users
SET feed_needs_update = true,
subscriptions = array_remove(subscriptions, $1)
WHERE email = $2
SQL
PG_DB.exec(request, ucid, user.email)
end
# -------------------
# Update (notifs)
# -------------------
def add_notification(video : ChannelVideo)
request = <<-SQL
UPDATE users
SET notifications = array_append(notifications, $1),
feed_needs_update = true
WHERE $2 = ANY(subscriptions)
SQL
PG_DB.exec(request, video.id, video.ucid)
end
def remove_notification(user : User, vid : String)
request = <<-SQL
UPDATE users
SET notifications = array_remove(notifications, $1)
WHERE email = $2
SQL
PG_DB.exec(request, vid, user.email)
end
def clear_notifications(user : User)
request = <<-SQL
UPDATE users
SET notifications = $1, updated = $2
WHERE email = $3
SQL
PG_DB.exec(request, [] of String, Time.utc, user)
end
# -------------------
# Update (misc)
# -------------------
def update_preferences(user : User)
request = <<-SQL
UPDATE users
SET preferences = $1
WHERE email = $2
SQL
PG_DB.exec(request, user.preferences.to_json, user.email)
end
def update_password(user : User, pass : String)
request = <<-SQL
UPDATE users
SET password = $1
WHERE email = $2
SQL
PG_DB.exec(request, user.email, pass)
end
# -------------------
# Select
# -------------------
def select(*, email : String) : User?
request = <<-SQL
SELECT * FROM users
WHERE email = $1
SQL
return PG_DB.query_one?(request, email, as: User)
end
# Same as select, but can raise an exception
def select!(*, email : String) : User
request = <<-SQL
SELECT * FROM users
WHERE email = $1
SQL
return PG_DB.query_one(request, email, as: User)
end
def select(*, token : String) : User?
request = <<-SQL
SELECT * FROM users
WHERE token = $1
SQL
return PG_DB.query_one?(request, token, as: User)
end
def select_notifications(user : User) : Array(String)
request = <<-SQL
SELECT notifications
FROM users
WHERE email = $1
SQL
return PG_DB.query_one(request, user.email, as: Array(String))
end
end

View File

@ -0,0 +1,43 @@
require "./base.cr"
module Invidious::Database::Videos
extend self
def insert(video : Video)
request = <<-SQL
INSERT INTO videos
VALUES ($1, $2, $3)
ON CONFLICT (id) DO NOTHING
SQL
PG_DB.exec(request, video.id, video.info.to_json, video.updated)
end
def delete(id)
request = <<-SQL
DELETE FROM videos *
WHERE id = $1
SQL
PG_DB.exec(request, id)
end
def update(video : Video)
request = <<-SQL
UPDATE videos
SET (id, info, updated) = ($1, $2, $3)
WHERE id = $1
SQL
PG_DB.exec(request, video.id, video.info.to_json, video.updated)
end
def select(id : String) : Video?
request = <<-SQL
SELECT * FROM videos
WHERE id = $1
SQL
return PG_DB.query_one?(request, id, as: Video)
end
end

View File

@ -97,18 +97,18 @@ class AuthHandler < Kemal::Handler
if token = env.request.headers["Authorization"]? if token = env.request.headers["Authorization"]?
token = JSON.parse(URI.decode_www_form(token.lchop("Bearer "))) token = JSON.parse(URI.decode_www_form(token.lchop("Bearer ")))
session = URI.decode_www_form(token["session"].as_s) session = URI.decode_www_form(token["session"].as_s)
scopes, expire, signature = validate_request(token, session, env.request, HMAC_KEY, PG_DB, nil) scopes, expire, signature = validate_request(token, session, env.request, HMAC_KEY, nil)
if email = PG_DB.query_one?("SELECT email FROM session_ids WHERE id = $1", session, as: String) if email = Invidious::Database::SessionIDs.select_email(session)
user = PG_DB.query_one("SELECT * FROM users WHERE email = $1", email, as: User) user = Invidious::Database::Users.select!(email: email)
end end
elsif sid = env.request.cookies["SID"]?.try &.value elsif sid = env.request.cookies["SID"]?.try &.value
if sid.starts_with? "v1:" if sid.starts_with? "v1:"
raise "Cannot use token as SID" raise "Cannot use token as SID"
end end
if email = PG_DB.query_one?("SELECT email FROM session_ids WHERE id = $1", sid, as: String) if email = Invidious::Database::SessionIDs.select_email(sid)
user = PG_DB.query_one("SELECT * FROM users WHERE email = $1", email, as: User) user = Invidious::Database::Users.select!(email: email)
end end
scopes = [":*"] scopes = [":*"]

View File

@ -60,112 +60,7 @@ def html_to_content(description_html : String)
return description return description
end end
def check_enum(db, enum_name, struct_type = nil) def cache_annotation(id, annotations)
return # TODO
if !db.query_one?("SELECT true FROM pg_type WHERE typname = $1", enum_name, as: Bool)
LOGGER.info("check_enum: CREATE TYPE #{enum_name}")
db.using_connection do |conn|
conn.as(PG::Connection).exec_all(File.read("config/sql/#{enum_name}.sql"))
end
end
end
def check_table(db, table_name, struct_type = nil)
# Create table if it doesn't exist
begin
db.exec("SELECT * FROM #{table_name} LIMIT 0")
rescue ex
LOGGER.info("check_table: check_table: CREATE TABLE #{table_name}")
db.using_connection do |conn|
conn.as(PG::Connection).exec_all(File.read("config/sql/#{table_name}.sql"))
end
end
return if !struct_type
struct_array = struct_type.type_array
column_array = get_column_array(db, table_name)
column_types = File.read("config/sql/#{table_name}.sql").match(/CREATE TABLE public\.#{table_name}\n\((?<types>[\d\D]*?)\);/)
.try &.["types"].split(",").map(&.strip).reject &.starts_with?("CONSTRAINT")
return if !column_types
struct_array.each_with_index do |name, i|
if name != column_array[i]?
if !column_array[i]?
new_column = column_types.select(&.starts_with?(name))[0]
LOGGER.info("check_table: ALTER TABLE #{table_name} ADD COLUMN #{new_column}")
db.exec("ALTER TABLE #{table_name} ADD COLUMN #{new_column}")
next
end
# Column doesn't exist
if !column_array.includes? name
new_column = column_types.select(&.starts_with?(name))[0]
db.exec("ALTER TABLE #{table_name} ADD COLUMN #{new_column}")
end
# Column exists but in the wrong position, rotate
if struct_array.includes? column_array[i]
until name == column_array[i]
new_column = column_types.select(&.starts_with?(column_array[i]))[0]?.try &.gsub("#{column_array[i]}", "#{column_array[i]}_new")
# There's a column we didn't expect
if !new_column
LOGGER.info("check_table: ALTER TABLE #{table_name} DROP COLUMN #{column_array[i]}")
db.exec("ALTER TABLE #{table_name} DROP COLUMN #{column_array[i]} CASCADE")
column_array = get_column_array(db, table_name)
next
end
LOGGER.info("check_table: ALTER TABLE #{table_name} ADD COLUMN #{new_column}")
db.exec("ALTER TABLE #{table_name} ADD COLUMN #{new_column}")
LOGGER.info("check_table: UPDATE #{table_name} SET #{column_array[i]}_new=#{column_array[i]}")
db.exec("UPDATE #{table_name} SET #{column_array[i]}_new=#{column_array[i]}")
LOGGER.info("check_table: ALTER TABLE #{table_name} DROP COLUMN #{column_array[i]} CASCADE")
db.exec("ALTER TABLE #{table_name} DROP COLUMN #{column_array[i]} CASCADE")
LOGGER.info("check_table: ALTER TABLE #{table_name} RENAME COLUMN #{column_array[i]}_new TO #{column_array[i]}")
db.exec("ALTER TABLE #{table_name} RENAME COLUMN #{column_array[i]}_new TO #{column_array[i]}")
column_array = get_column_array(db, table_name)
end
else
LOGGER.info("check_table: ALTER TABLE #{table_name} DROP COLUMN #{column_array[i]} CASCADE")
db.exec("ALTER TABLE #{table_name} DROP COLUMN #{column_array[i]} CASCADE")
end
end
end
return if column_array.size <= struct_array.size
column_array.each do |column|
if !struct_array.includes? column
LOGGER.info("check_table: ALTER TABLE #{table_name} DROP COLUMN #{column} CASCADE")
db.exec("ALTER TABLE #{table_name} DROP COLUMN #{column} CASCADE")
end
end
end
def get_column_array(db, table_name)
column_array = [] of String
db.query("SELECT * FROM #{table_name} LIMIT 0") do |rs|
rs.column_count.times do |i|
column = rs.as(PG::ResultSet).field(i)
column_array << column.name
end
end
return column_array
end
def cache_annotation(db, id, annotations)
if !CONFIG.cache_annotations if !CONFIG.cache_annotations
return return
end end
@ -183,7 +78,7 @@ def cache_annotation(db, id, annotations)
end end
end end
db.exec("INSERT INTO annotations VALUES ($1, $2) ON CONFLICT DO NOTHING", id, annotations) if has_legacy_annotations Invidious::Database::Annotations.insert(id, annotations) if has_legacy_annotations
end end
def create_notification_stream(env, topics, connection_channel) def create_notification_stream(env, topics, connection_channel)
@ -204,7 +99,7 @@ def create_notification_stream(env, topics, connection_channel)
published = Time.utc - Time::Span.new(days: time_span[0], hours: time_span[1], minutes: time_span[2], seconds: time_span[3]) published = Time.utc - Time::Span.new(days: time_span[0], hours: time_span[1], minutes: time_span[2], seconds: time_span[3])
video_id = TEST_IDS[rand(TEST_IDS.size)] video_id = TEST_IDS[rand(TEST_IDS.size)]
video = get_video(video_id, PG_DB) video = get_video(video_id)
video.published = published video.published = published
response = JSON.parse(video.to_json(locale, nil)) response = JSON.parse(video.to_json(locale, nil))
@ -235,11 +130,12 @@ def create_notification_stream(env, topics, connection_channel)
spawn do spawn do
begin begin
if since if since
since_unix = Time.unix(since.not_nil!)
topics.try &.each do |topic| topics.try &.each do |topic|
case topic case topic
when .match(/UC[A-Za-z0-9_-]{22}/) when .match(/UC[A-Za-z0-9_-]{22}/)
PG_DB.query_all("SELECT * FROM channel_videos WHERE ucid = $1 AND published > $2 ORDER BY published DESC LIMIT 15", Invidious::Database::ChannelVideos.select_notfications(topic, since_unix).each do |video|
topic, Time.unix(since.not_nil!), as: ChannelVideo).each do |video|
response = JSON.parse(video.to_json(locale)) response = JSON.parse(video.to_json(locale))
if fields_text = env.params.query["fields"]? if fields_text = env.params.query["fields"]?
@ -280,7 +176,7 @@ def create_notification_stream(env, topics, connection_channel)
next next
end end
video = get_video(video_id, PG_DB) video = get_video(video_id)
video.published = Time.unix(published) video.published = Time.unix(published)
response = JSON.parse(video.to_json(locale, nil)) response = JSON.parse(video.to_json(locale, nil))

View File

@ -1,8 +1,8 @@
require "crypto/subtle" require "crypto/subtle"
def generate_token(email, scopes, expire, key, db) def generate_token(email, scopes, expire, key)
session = "v1:#{Base64.urlsafe_encode(Random::Secure.random_bytes(32))}" session = "v1:#{Base64.urlsafe_encode(Random::Secure.random_bytes(32))}"
PG_DB.exec("INSERT INTO session_ids VALUES ($1, $2, $3)", session, email, Time.utc) Invidious::Database::SessionIDs.insert(session, email)
token = { token = {
"session" => session, "session" => session,
@ -19,7 +19,7 @@ def generate_token(email, scopes, expire, key, db)
return token.to_json return token.to_json
end end
def generate_response(session, scopes, key, db, expire = 6.hours, use_nonce = false) def generate_response(session, scopes, key, expire = 6.hours, use_nonce = false)
expire = Time.utc + expire expire = Time.utc + expire
token = { token = {
@ -30,7 +30,7 @@ def generate_response(session, scopes, key, db, expire = 6.hours, use_nonce = fa
if use_nonce if use_nonce
nonce = Random::Secure.hex(16) nonce = Random::Secure.hex(16)
db.exec("INSERT INTO nonces VALUES ($1, $2) ON CONFLICT DO NOTHING", nonce, expire) Invidious::Database::Nonces.insert(nonce, expire)
token["nonce"] = nonce token["nonce"] = nonce
end end
@ -63,7 +63,7 @@ def sign_token(key, hash)
return Base64.urlsafe_encode(OpenSSL::HMAC.digest(:sha256, key, string_to_sign)).strip return Base64.urlsafe_encode(OpenSSL::HMAC.digest(:sha256, key, string_to_sign)).strip
end end
def validate_request(token, session, request, key, db, locale = nil) def validate_request(token, session, request, key, locale = nil)
case token case token
when String when String
token = JSON.parse(URI.decode_www_form(token)).as_h token = JSON.parse(URI.decode_www_form(token)).as_h
@ -92,9 +92,9 @@ def validate_request(token, session, request, key, db, locale = nil)
raise InfoException.new("Invalid signature") raise InfoException.new("Invalid signature")
end end
if token["nonce"]? && (nonce = db.query_one?("SELECT * FROM nonces WHERE nonce = $1", token["nonce"], as: {String, Time})) if token["nonce"]? && (nonce = Invidious::Database::Nonces.select(token["nonce"].as_s))
if nonce[1] > Time.utc if nonce[1] > Time.utc
db.exec("UPDATE nonces SET expire = $1 WHERE nonce = $2", Time.utc(1990, 1, 1), nonce[0]) Invidious::Database::Nonces.update_set_expired(nonce[0])
else else
raise InfoException.new("Erroneous token") raise InfoException.new("Erroneous token")
end end

View File

@ -1,5 +1,3 @@
require "db"
# See http://www.evanmiller.org/how-not-to-sort-by-average-rating.html # See http://www.evanmiller.org/how-not-to-sort-by-average-rating.html
def ci_lower_bound(pos, n) def ci_lower_bound(pos, n)
if n == 0 if n == 0

View File

@ -1,11 +1,4 @@
class Invidious::Jobs::PullPopularVideosJob < Invidious::Jobs::BaseJob 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) POPULAR_VIDEOS = Atomic.new([] of ChannelVideo)
private getter db : DB::Database private getter db : DB::Database
@ -14,7 +7,7 @@ class Invidious::Jobs::PullPopularVideosJob < Invidious::Jobs::BaseJob
def begin def begin
loop do loop do
videos = db.query_all(QUERY, as: ChannelVideo) videos = Invidious::Database::ChannelVideos.select_popular_videos
.sort_by!(&.published) .sort_by!(&.published)
.reverse! .reverse!

View File

@ -35,11 +35,11 @@ class Invidious::Jobs::RefreshChannelsJob < Invidious::Jobs::BaseJob
lim_fibers = max_fibers lim_fibers = max_fibers
LOGGER.trace("RefreshChannelsJob: #{id} fiber : Updating DB") LOGGER.trace("RefreshChannelsJob: #{id} fiber : Updating DB")
db.exec("UPDATE channels SET updated = $1, author = $2, deleted = false WHERE id = $3", Time.utc, channel.author, id) Invidious::Database::Channels.update_author(id, channel.author)
rescue ex rescue ex
LOGGER.error("RefreshChannelsJob: #{id} : #{ex.message}") LOGGER.error("RefreshChannelsJob: #{id} : #{ex.message}")
if ex.message == "Deleted or invalid channel" if ex.message == "Deleted or invalid channel"
db.exec("UPDATE channels SET updated = $1, deleted = true WHERE id = $2", Time.utc, id) Invidious::Database::Channels.update_mark_deleted(id)
else else
lim_fibers = 1 lim_fibers = 1
LOGGER.error("RefreshChannelsJob: #{id} fiber : backing off for #{backoff}s") LOGGER.error("RefreshChannelsJob: #{id} fiber : backing off for #{backoff}s")

View File

@ -25,7 +25,7 @@ class Invidious::Jobs::RefreshFeedsJob < Invidious::Jobs::BaseJob
spawn do spawn do
begin begin
# Drop outdated views # Drop outdated views
column_array = get_column_array(db, view_name) column_array = Invidious::Database.get_column_array(db, view_name)
ChannelVideo.type_array.each_with_index do |name, i| ChannelVideo.type_array.each_with_index do |name, i|
if name != column_array[i]? if name != column_array[i]?
LOGGER.info("RefreshFeedsJob: DROP MATERIALIZED VIEW #{view_name}") LOGGER.info("RefreshFeedsJob: DROP MATERIALIZED VIEW #{view_name}")

View File

@ -47,12 +47,14 @@ class Invidious::Jobs::StatisticsRefreshJob < Invidious::Jobs::BaseJob
private def refresh_stats private def refresh_stats
users = STATISTICS.dig("usage", "users").as(Hash(String, Int64)) users = STATISTICS.dig("usage", "users").as(Hash(String, Int64))
users["total"] = db.query_one("SELECT count(*) FROM users", as: Int64)
users["activeHalfyear"] = db.query_one("SELECT count(*) FROM users WHERE CURRENT_TIMESTAMP - updated < '6 months'", as: Int64) users["total"] = Invidious::Database::Statistics.count_users_total
users["activeMonth"] = db.query_one("SELECT count(*) FROM users WHERE CURRENT_TIMESTAMP - updated < '1 month'", as: Int64) users["activeHalfyear"] = Invidious::Database::Statistics.count_users_active_1m
users["activeMonth"] = Invidious::Database::Statistics.count_users_active_6m
STATISTICS["metadata"] = { STATISTICS["metadata"] = {
"updatedAt" => Time.utc.to_unix, "updatedAt" => Time.utc.to_unix,
"lastChannelRefreshedAt" => db.query_one?("SELECT updated FROM channels ORDER BY updated DESC LIMIT 1", as: Time).try &.to_unix || 0_i64, "lastChannelRefreshedAt" => Invidious::Database::Statistics.channel_last_update.try &.to_unix || 0_i64,
} }
end end
end end

View File

@ -125,7 +125,7 @@ struct Playlist
json.field "videos" do json.field "videos" do
json.array do json.array do
videos = get_playlist_videos(PG_DB, self, offset: offset, locale: locale, video_id: video_id) videos = get_playlist_videos(self, offset: offset, locale: locale, video_id: video_id)
videos.each do |video| videos.each do |video|
video.to_json(json) video.to_json(json)
end end
@ -200,12 +200,12 @@ struct InvidiousPlaylist
json.field "videos" do json.field "videos" do
json.array do json.array do
if !offset || offset == 0 if (!offset || offset == 0) && !video_id.nil?
index = PG_DB.query_one?("SELECT index FROM playlist_videos WHERE plid = $1 AND id = $2 LIMIT 1", self.id, video_id, as: Int64) index = Invidious::Database::PlaylistVideos.select_index(self.id, video_id)
offset = self.index.index(index) || 0 offset = self.index.index(index) || 0
end end
videos = get_playlist_videos(PG_DB, self, offset: offset, locale: locale, video_id: video_id) videos = get_playlist_videos(self, offset: offset, locale: locale, video_id: video_id)
videos.each_with_index do |video, index| videos.each_with_index do |video, index|
video.to_json(json, offset + index) video.to_json(json, offset + index)
end end
@ -225,7 +225,8 @@ struct InvidiousPlaylist
end end
def thumbnail def thumbnail
@thumbnail_id ||= PG_DB.query_one?("SELECT id FROM playlist_videos WHERE plid = $1 ORDER BY array_position($2, index) LIMIT 1", self.id, self.index, as: String) || "-----------" # TODO: Get playlist thumbnail from playlist data rather than first video
@thumbnail_id ||= Invidious::Database::PlaylistVideos.select_one_id(self.id, self.index) || "-----------"
"/vi/#{@thumbnail_id}/mqdefault.jpg" "/vi/#{@thumbnail_id}/mqdefault.jpg"
end end
@ -246,7 +247,7 @@ struct InvidiousPlaylist
end end
end end
def create_playlist(db, title, privacy, user) def create_playlist(title, privacy, user)
plid = "IVPL#{Random::Secure.urlsafe_base64(24)[0, 31]}" plid = "IVPL#{Random::Secure.urlsafe_base64(24)[0, 31]}"
playlist = InvidiousPlaylist.new({ playlist = InvidiousPlaylist.new({
@ -261,15 +262,12 @@ def create_playlist(db, title, privacy, user)
index: [] of Int64, index: [] of Int64,
}) })
playlist_array = playlist.to_a Invidious::Database::Playlists.insert(playlist)
args = arg_array(playlist_array)
db.exec("INSERT INTO playlists VALUES (#{args})", args: playlist_array)
return playlist return playlist
end end
def subscribe_playlist(db, user, playlist) def subscribe_playlist(user, playlist)
playlist = InvidiousPlaylist.new({ playlist = InvidiousPlaylist.new({
title: playlist.title.byte_slice(0, 150), title: playlist.title.byte_slice(0, 150),
id: playlist.id, id: playlist.id,
@ -282,10 +280,7 @@ def subscribe_playlist(db, user, playlist)
index: [] of Int64, index: [] of Int64,
}) })
playlist_array = playlist.to_a Invidious::Database::Playlists.insert(playlist)
args = arg_array(playlist_array)
db.exec("INSERT INTO playlists VALUES (#{args})", args: playlist_array)
return playlist return playlist
end end
@ -327,9 +322,9 @@ def produce_playlist_continuation(id, index)
return continuation return continuation
end end
def get_playlist(db, plid, locale, refresh = true, force_refresh = false) def get_playlist(plid, locale, refresh = true, force_refresh = false)
if plid.starts_with? "IV" if plid.starts_with? "IV"
if playlist = db.query_one?("SELECT * FROM playlists WHERE id = $1", plid, as: InvidiousPlaylist) if playlist = Invidious::Database::Playlists.select(id: plid)
return playlist return playlist
else else
raise InfoException.new("Playlist does not exist.") raise InfoException.new("Playlist does not exist.")
@ -409,7 +404,7 @@ def fetch_playlist(plid, locale)
}) })
end end
def get_playlist_videos(db, playlist, offset, locale = nil, video_id = nil) def get_playlist_videos(playlist, offset, locale = nil, video_id = nil)
# Show empy playlist if requested page is out of range # Show empy playlist if requested page is out of range
# (e.g, when a new playlist has been created, offset will be negative) # (e.g, when a new playlist has been created, offset will be negative)
if offset >= playlist.video_count || offset < 0 if offset >= playlist.video_count || offset < 0
@ -417,8 +412,7 @@ def get_playlist_videos(db, playlist, offset, locale = nil, video_id = nil)
end end
if playlist.is_a? InvidiousPlaylist if playlist.is_a? InvidiousPlaylist
db.query_all("SELECT * FROM playlist_videos WHERE plid = $1 ORDER BY array_position($2, index) LIMIT 100 OFFSET $3", Invidious::Database::PlaylistVideos.select(playlist.id, playlist.index, offset, limit: 100)
playlist.id, playlist.index, offset, as: PlaylistVideo)
else else
if video_id if video_id
initial_data = YoutubeAPI.next({ initial_data = YoutubeAPI.next({

View File

@ -13,7 +13,7 @@ module Invidious::Routes::API::Manifest
unique_res = env.params.query["unique_res"]?.try { |q| (q == "true" || q == "1").to_unsafe } unique_res = env.params.query["unique_res"]?.try { |q| (q == "true" || q == "1").to_unsafe }
begin begin
video = get_video(id, PG_DB, region: region) video = get_video(id, region: region)
rescue ex : VideoRedirect rescue ex : VideoRedirect
return env.redirect env.request.resource.gsub(id, ex.video_id) return env.redirect env.request.resource.gsub(id, ex.video_id)
rescue ex rescue ex

View File

@ -22,12 +22,11 @@ module Invidious::Routes::API::V1::Authenticated
user = env.get("user").as(User) user = env.get("user").as(User)
begin begin
preferences = Preferences.from_json(env.request.body || "{}") user.preferences = Preferences.from_json(env.request.body || "{}")
rescue rescue
preferences = user.preferences
end end
PG_DB.exec("UPDATE users SET preferences = $1 WHERE email = $2", preferences.to_json, user.email) Invidious::Database::Users.update_preferences(user)
env.response.status_code = 204 env.response.status_code = 204
end end
@ -45,7 +44,7 @@ module Invidious::Routes::API::V1::Authenticated
page = env.params.query["page"]?.try &.to_i? page = env.params.query["page"]?.try &.to_i?
page ||= 1 page ||= 1
videos, notifications = get_subscription_feed(PG_DB, user, max_results, page) videos, notifications = get_subscription_feed(user, max_results, page)
JSON.build do |json| JSON.build do |json|
json.object do json.object do
@ -72,13 +71,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)
if user.subscriptions.empty? subscriptions = Invidious::Database::Channels.select(user.subscriptions)
values = "'{}'"
else
values = "VALUES #{user.subscriptions.map { |id| %(('#{id}')) }.join(",")}"
end
subscriptions = PG_DB.query_all("SELECT * FROM channels WHERE id = ANY(#{values})", as: InvidiousChannel)
JSON.build do |json| JSON.build do |json|
json.array do json.array do
@ -99,8 +92,8 @@ module Invidious::Routes::API::V1::Authenticated
ucid = env.params.url["ucid"] ucid = env.params.url["ucid"]
if !user.subscriptions.includes? ucid if !user.subscriptions.includes? ucid
get_channel(ucid, PG_DB, false, false) get_channel(ucid, false, false)
PG_DB.exec("UPDATE users SET feed_needs_update = true, subscriptions = array_append(subscriptions,$1) WHERE email = $2", ucid, user.email) Invidious::Database::Users.subscribe_channel(user, ucid)
end end
# For Google accounts, access tokens don't have enough information to # For Google accounts, access tokens don't have enough information to
@ -116,7 +109,7 @@ module Invidious::Routes::API::V1::Authenticated
ucid = env.params.url["ucid"] ucid = env.params.url["ucid"]
PG_DB.exec("UPDATE users SET feed_needs_update = true, subscriptions = array_remove(subscriptions, $1) WHERE email = $2", ucid, user.email) Invidious::Database::Users.unsubscribe_channel(user, ucid)
env.response.status_code = 204 env.response.status_code = 204
end end
@ -127,7 +120,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)
playlists = PG_DB.query_all("SELECT * FROM playlists WHERE author = $1", user.email, as: InvidiousPlaylist) playlists = Invidious::Database::Playlists.select_all(author: user.email)
JSON.build do |json| JSON.build do |json|
json.array do json.array do
@ -153,11 +146,11 @@ module Invidious::Routes::API::V1::Authenticated
return error_json(400, "Invalid privacy setting.") return error_json(400, "Invalid privacy setting.")
end end
if PG_DB.query_one("SELECT count(*) FROM playlists WHERE author = $1", user.email, as: Int64) >= 100 if Invidious::Database::Playlists.count_owned_by(user.email) >= 100
return error_json(400, "User cannot have more than 100 playlists.") return error_json(400, "User cannot have more than 100 playlists.")
end end
playlist = create_playlist(PG_DB, title, privacy, user) playlist = create_playlist(title, privacy, user)
env.response.headers["Location"] = "#{HOST_URL}/api/v1/auth/playlists/#{playlist.id}" env.response.headers["Location"] = "#{HOST_URL}/api/v1/auth/playlists/#{playlist.id}"
env.response.status_code = 201 env.response.status_code = 201
{ {
@ -172,9 +165,12 @@ 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)
plid = env.params.url["plid"] plid = env.params.url["plid"]?
if !plid || plid.empty?
return error_json(400, "A playlist ID is required")
end
playlist = PG_DB.query_one?("SELECT * FROM playlists WHERE id = $1", plid, as: InvidiousPlaylist) playlist = Invidious::Database::Playlists.select(id: plid)
if !playlist || playlist.author != user.email && playlist.privacy.private? if !playlist || playlist.author != user.email && playlist.privacy.private?
return error_json(404, "Playlist does not exist.") return error_json(404, "Playlist does not exist.")
end end
@ -195,7 +191,8 @@ module Invidious::Routes::API::V1::Authenticated
updated = playlist.updated updated = playlist.updated
end end
PG_DB.exec("UPDATE playlists SET title = $1, privacy = $2, description = $3, updated = $4 WHERE id = $5", title, privacy, description, updated, plid) Invidious::Database::Playlists.update(plid, title, privacy, description, updated)
env.response.status_code = 204 env.response.status_code = 204
end end
@ -207,7 +204,7 @@ module Invidious::Routes::API::V1::Authenticated
plid = env.params.url["plid"] plid = env.params.url["plid"]
playlist = PG_DB.query_one?("SELECT * FROM playlists WHERE id = $1", plid, as: InvidiousPlaylist) playlist = Invidious::Database::Playlists.select(id: plid)
if !playlist || playlist.author != user.email && playlist.privacy.private? if !playlist || playlist.author != user.email && playlist.privacy.private?
return error_json(404, "Playlist does not exist.") return error_json(404, "Playlist does not exist.")
end end
@ -216,8 +213,7 @@ module Invidious::Routes::API::V1::Authenticated
return error_json(403, "Invalid user") return error_json(403, "Invalid user")
end end
PG_DB.exec("DELETE FROM playlist_videos * WHERE plid = $1", plid) Invidious::Database::Playlists.delete(plid)
PG_DB.exec("DELETE FROM playlists * WHERE id = $1", plid)
env.response.status_code = 204 env.response.status_code = 204
end end
@ -230,7 +226,7 @@ module Invidious::Routes::API::V1::Authenticated
plid = env.params.url["plid"] plid = env.params.url["plid"]
playlist = PG_DB.query_one?("SELECT * FROM playlists WHERE id = $1", plid, as: InvidiousPlaylist) playlist = Invidious::Database::Playlists.select(id: plid)
if !playlist || playlist.author != user.email && playlist.privacy.private? if !playlist || playlist.author != user.email && playlist.privacy.private?
return error_json(404, "Playlist does not exist.") return error_json(404, "Playlist does not exist.")
end end
@ -249,7 +245,7 @@ module Invidious::Routes::API::V1::Authenticated
end end
begin begin
video = get_video(video_id, PG_DB) video = get_video(video_id)
rescue ex rescue ex
return error_json(500, ex) return error_json(500, ex)
end end
@ -266,11 +262,8 @@ module Invidious::Routes::API::V1::Authenticated
index: Random::Secure.rand(0_i64..Int64::MAX), index: Random::Secure.rand(0_i64..Int64::MAX),
}) })
video_array = playlist_video.to_a Invidious::Database::PlaylistVideos.insert(playlist_video)
args = arg_array(video_array) Invidious::Database::Playlists.update_video_added(plid, playlist_video.index)
PG_DB.exec("INSERT INTO playlist_videos VALUES (#{args})", args: video_array)
PG_DB.exec("UPDATE playlists SET index = array_append(index, $1), video_count = cardinality(index) + 1, updated = $2 WHERE id = $3", playlist_video.index, Time.utc, plid)
env.response.headers["Location"] = "#{HOST_URL}/api/v1/auth/playlists/#{plid}/videos/#{playlist_video.index.to_u64.to_s(16).upcase}" env.response.headers["Location"] = "#{HOST_URL}/api/v1/auth/playlists/#{plid}/videos/#{playlist_video.index.to_u64.to_s(16).upcase}"
env.response.status_code = 201 env.response.status_code = 201
@ -289,7 +282,7 @@ module Invidious::Routes::API::V1::Authenticated
plid = env.params.url["plid"] plid = env.params.url["plid"]
index = env.params.url["index"].to_i64(16) index = env.params.url["index"].to_i64(16)
playlist = PG_DB.query_one?("SELECT * FROM playlists WHERE id = $1", plid, as: InvidiousPlaylist) playlist = Invidious::Database::Playlists.select(id: plid)
if !playlist || playlist.author != user.email && playlist.privacy.private? if !playlist || playlist.author != user.email && playlist.privacy.private?
return error_json(404, "Playlist does not exist.") return error_json(404, "Playlist does not exist.")
end end
@ -302,8 +295,8 @@ module Invidious::Routes::API::V1::Authenticated
return error_json(404, "Playlist does not contain index") return error_json(404, "Playlist does not contain index")
end end
PG_DB.exec("DELETE FROM playlist_videos * WHERE index = $1", index) Invidious::Database::PlaylistVideos.delete(index)
PG_DB.exec("UPDATE playlists SET index = array_remove(index, $1), video_count = cardinality(index) - 1, updated = $2 WHERE id = $3", index, Time.utc, plid) Invidious::Database::Playlists.update_video_removed(plid, index)
env.response.status_code = 204 env.response.status_code = 204
end end
@ -318,7 +311,7 @@ module Invidious::Routes::API::V1::Authenticated
user = env.get("user").as(User) user = env.get("user").as(User)
scopes = env.get("scopes").as(Array(String)) scopes = env.get("scopes").as(Array(String))
tokens = PG_DB.query_all("SELECT id, issued FROM session_ids WHERE email = $1", user.email, as: {session: String, issued: Time}) tokens = Invidious::Database::SessionIDs.select_all(user.email)
JSON.build do |json| JSON.build do |json|
json.array do json.array do
@ -360,7 +353,7 @@ module Invidious::Routes::API::V1::Authenticated
if sid = env.get?("sid").try &.as(String) if sid = env.get?("sid").try &.as(String)
env.response.content_type = "text/html" env.response.content_type = "text/html"
csrf_token = generate_response(sid, {":authorize_token"}, HMAC_KEY, PG_DB, use_nonce: true) csrf_token = generate_response(sid, {":authorize_token"}, HMAC_KEY, use_nonce: true)
return templated "authorize_token" return templated "authorize_token"
else else
env.response.content_type = "application/json" env.response.content_type = "application/json"
@ -374,7 +367,7 @@ module Invidious::Routes::API::V1::Authenticated
end end
end end
access_token = generate_token(user.email, authorized_scopes, expire, HMAC_KEY, PG_DB) access_token = generate_token(user.email, authorized_scopes, expire, HMAC_KEY)
if callback_url if callback_url
access_token = URI.encode_www_form(access_token) access_token = URI.encode_www_form(access_token)
@ -406,9 +399,9 @@ module Invidious::Routes::API::V1::Authenticated
# Allow tokens to revoke other tokens with correct scope # Allow tokens to revoke other tokens with correct scope
if session == env.get("session").as(String) if session == env.get("session").as(String)
PG_DB.exec("DELETE FROM session_ids * WHERE id = $1", session) Invidious::Database::SessionIDs.delete(sid: session)
elsif scopes_include_scope(scopes, "GET:tokens") elsif scopes_include_scope(scopes, "GET:tokens")
PG_DB.exec("DELETE FROM session_ids * WHERE id = $1", session) Invidious::Database::SessionIDs.delete(sid: session)
else else
return error_json(400, "Cannot revoke session #{session}") return error_json(400, "Cannot revoke session #{session}")
end end

View File

@ -34,7 +34,7 @@ module Invidious::Routes::API::V1::Misc
end end
begin begin
playlist = get_playlist(PG_DB, plid, locale) playlist = get_playlist(plid, locale)
rescue ex : InfoException rescue ex : InfoException
return error_json(404, ex) return error_json(404, ex)
rescue ex rescue ex

View File

@ -8,7 +8,7 @@ module Invidious::Routes::API::V1::Videos
region = env.params.query["region"]? region = env.params.query["region"]?
begin begin
video = get_video(id, PG_DB, region: region) video = get_video(id, region: region)
rescue ex : VideoRedirect rescue ex : VideoRedirect
env.response.headers["Location"] = env.request.resource.gsub(id, ex.video_id) env.response.headers["Location"] = env.request.resource.gsub(id, ex.video_id)
return error_json(302, "Video is unavailable", {"videoId" => ex.video_id}) return error_json(302, "Video is unavailable", {"videoId" => ex.video_id})
@ -36,7 +36,7 @@ module Invidious::Routes::API::V1::Videos
# getting video info. # getting video info.
begin begin
video = get_video(id, PG_DB, region: region) video = get_video(id, region: region)
rescue ex : VideoRedirect rescue ex : VideoRedirect
env.response.headers["Location"] = env.request.resource.gsub(id, ex.video_id) env.response.headers["Location"] = env.request.resource.gsub(id, ex.video_id)
return error_json(302, "Video is unavailable", {"videoId" => ex.video_id}) return error_json(302, "Video is unavailable", {"videoId" => ex.video_id})
@ -157,7 +157,7 @@ module Invidious::Routes::API::V1::Videos
region = env.params.query["region"]? region = env.params.query["region"]?
begin begin
video = get_video(id, PG_DB, region: region) video = get_video(id, region: region)
rescue ex : VideoRedirect rescue ex : VideoRedirect
env.response.headers["Location"] = env.request.resource.gsub(id, ex.video_id) env.response.headers["Location"] = env.request.resource.gsub(id, ex.video_id)
return error_json(302, "Video is unavailable", {"videoId" => ex.video_id}) return error_json(302, "Video is unavailable", {"videoId" => ex.video_id})
@ -239,7 +239,7 @@ module Invidious::Routes::API::V1::Videos
case source case source
when "archive" when "archive"
if CONFIG.cache_annotations && (cached_annotation = PG_DB.query_one?("SELECT * FROM annotations WHERE id = $1", id, as: Annotation)) if CONFIG.cache_annotations && (cached_annotation = Invidious::Database::Annotations.select(id))
annotations = cached_annotation.annotations annotations = cached_annotation.annotations
else else
index = CHARS_SAFE.index(id[0]).not_nil!.to_s.rjust(2, '0') index = CHARS_SAFE.index(id[0]).not_nil!.to_s.rjust(2, '0')
@ -271,7 +271,7 @@ module Invidious::Routes::API::V1::Videos
annotations = response.body annotations = response.body
cache_annotation(PG_DB, id, annotations) cache_annotation(id, annotations)
end end
else # "youtube" else # "youtube"
response = YT_POOL.client &.get("/annotations_invideo?video_id=#{id}") response = YT_POOL.client &.get("/annotations_invideo?video_id=#{id}")

View File

@ -6,9 +6,9 @@ module Invidious::Routes::Embed
if plid = env.params.query["list"]?.try &.gsub(/[^a-zA-Z0-9_-]/, "") if plid = env.params.query["list"]?.try &.gsub(/[^a-zA-Z0-9_-]/, "")
begin begin
playlist = get_playlist(PG_DB, plid, locale: locale) playlist = get_playlist(plid, locale: locale)
offset = env.params.query["index"]?.try &.to_i? || 0 offset = env.params.query["index"]?.try &.to_i? || 0
videos = get_playlist_videos(PG_DB, playlist, offset: offset, locale: locale) videos = get_playlist_videos(playlist, offset: offset, locale: locale)
rescue ex rescue ex
return error_template(500, ex) return error_template(500, ex)
end end
@ -30,7 +30,7 @@ module Invidious::Routes::Embed
id = env.params.url["id"] id = env.params.url["id"]
plid = env.params.query["list"]?.try &.gsub(/[^a-zA-Z0-9_-]/, "") plid = env.params.query["list"]?.try &.gsub(/[^a-zA-Z0-9_-]/, "")
continuation = process_continuation(PG_DB, env.params.query, plid, id) continuation = process_continuation(env.params.query, plid, id)
if md = env.params.query["playlist"]? if md = env.params.query["playlist"]?
.try &.match(/[a-zA-Z0-9_-]{11}(,[a-zA-Z0-9_-]{11})*/) .try &.match(/[a-zA-Z0-9_-]{11}(,[a-zA-Z0-9_-]{11})*/)
@ -60,9 +60,9 @@ module Invidious::Routes::Embed
if plid if plid
begin begin
playlist = get_playlist(PG_DB, plid, locale: locale) playlist = get_playlist(plid, locale: locale)
offset = env.params.query["index"]?.try &.to_i? || 0 offset = env.params.query["index"]?.try &.to_i? || 0
videos = get_playlist_videos(PG_DB, playlist, offset: offset, locale: locale) videos = get_playlist_videos(playlist, offset: offset, locale: locale)
rescue ex rescue ex
return error_template(500, ex) return error_template(500, ex)
end end
@ -119,7 +119,7 @@ module Invidious::Routes::Embed
subscriptions ||= [] of String subscriptions ||= [] of String
begin begin
video = get_video(id, PG_DB, region: params.region) video = get_video(id, region: params.region)
rescue ex : VideoRedirect rescue ex : VideoRedirect
return env.redirect env.request.resource.gsub(id, ex.video_id) return env.redirect env.request.resource.gsub(id, ex.video_id)
rescue ex rescue ex
@ -137,7 +137,7 @@ module Invidious::Routes::Embed
# end # end
if notifications && notifications.includes? id if notifications && notifications.includes? id
PG_DB.exec("UPDATE users SET notifications = array_remove(notifications, $1) WHERE email = $2", id, user.as(User).email) Invidious::Database::Users.remove_notification(user.as(User), id)
env.get("user").as(User).notifications.delete(id) env.get("user").as(User).notifications.delete(id)
notifications.delete(id) notifications.delete(id)
end end

View File

@ -15,13 +15,14 @@ module Invidious::Routes::Feeds
user = user.as(User) user = user.as(User)
items_created = PG_DB.query_all("SELECT * FROM playlists WHERE author = $1 AND id LIKE 'IV%' ORDER BY created", user.email, as: InvidiousPlaylist) # TODO: make a single DB call and separate the items here?
items_created = Invidious::Database::Playlists.select_like_iv(user.email)
items_created.map! do |item| items_created.map! do |item|
item.author = "" item.author = ""
item item
end end
items_saved = PG_DB.query_all("SELECT * FROM playlists WHERE author = $1 AND id NOT LIKE 'IV%' ORDER BY created", user.email, as: InvidiousPlaylist) items_saved = Invidious::Database::Playlists.select_not_like_iv(user.email)
items_saved.map! do |item| items_saved.map! do |item|
item.author = "" item.author = ""
item item
@ -83,7 +84,7 @@ module Invidious::Routes::Feeds
headers["Cookie"] = env.request.headers["Cookie"] headers["Cookie"] = env.request.headers["Cookie"]
if !user.password if !user.password
user, sid = get_user(sid, headers, PG_DB) user, sid = get_user(sid, headers)
end end
max_results = env.params.query["max_results"]?.try &.to_i?.try &.clamp(0, MAX_ITEMS_PER_PAGE) max_results = env.params.query["max_results"]?.try &.to_i?.try &.clamp(0, MAX_ITEMS_PER_PAGE)
@ -93,14 +94,13 @@ module Invidious::Routes::Feeds
page = env.params.query["page"]?.try &.to_i? page = env.params.query["page"]?.try &.to_i?
page ||= 1 page ||= 1
videos, notifications = get_subscription_feed(PG_DB, user, max_results, page) videos, notifications = get_subscription_feed(user, max_results, page)
# "updated" here is used for delivering new notifications, so if # "updated" here is used for delivering new notifications, so if
# we know a user has looked at their feed e.g. in the past 10 minutes, # we know a user has looked at their feed e.g. in the past 10 minutes,
# they've already seen a video posted 20 minutes ago, and don't need # they've already seen a video posted 20 minutes ago, and don't need
# to be notified. # to be notified.
PG_DB.exec("UPDATE users SET notifications = $1, updated = $2 WHERE email = $3", [] of String, Time.utc, Invidious::Database::Users.clear_notifications(user)
user.email)
user.notifications = [] of String user.notifications = [] of String
env.set "user", user env.set "user", user
@ -220,7 +220,7 @@ module Invidious::Routes::Feeds
haltf env, status_code: 403 haltf env, status_code: 403
end end
user = PG_DB.query_one?("SELECT * FROM users WHERE token = $1", token.strip, as: User) user = Invidious::Database::Users.select(token: token.strip)
if !user if !user
haltf env, status_code: 403 haltf env, status_code: 403
end end
@ -234,7 +234,7 @@ module Invidious::Routes::Feeds
params = HTTP::Params.parse(env.params.query["params"]? || "") params = HTTP::Params.parse(env.params.query["params"]? || "")
videos, notifications = get_subscription_feed(PG_DB, user, max_results, page) videos, notifications = get_subscription_feed(user, max_results, page)
XML.build(indent: " ", encoding: "UTF-8") do |xml| XML.build(indent: " ", encoding: "UTF-8") do |xml|
xml.element("feed", "xmlns:yt": "http://www.youtube.com/xml/schemas/2015", xml.element("feed", "xmlns:yt": "http://www.youtube.com/xml/schemas/2015",
@ -264,8 +264,8 @@ module Invidious::Routes::Feeds
path = env.request.path path = env.request.path
if plid.starts_with? "IV" if plid.starts_with? "IV"
if playlist = PG_DB.query_one?("SELECT * FROM playlists WHERE id = $1", plid, as: InvidiousPlaylist) if playlist = Invidious::Database::Playlists.select(id: plid)
videos = get_playlist_videos(PG_DB, playlist, offset: 0, locale: locale) videos = get_playlist_videos(playlist, offset: 0, locale: locale)
return XML.build(indent: " ", encoding: "UTF-8") do |xml| return XML.build(indent: " ", encoding: "UTF-8") do |xml|
xml.element("feed", "xmlns:yt": "http://www.youtube.com/xml/schemas/2015", xml.element("feed", "xmlns:yt": "http://www.youtube.com/xml/schemas/2015",
@ -364,7 +364,7 @@ module Invidious::Routes::Feeds
if ucid = HTTP::Params.parse(URI.parse(topic).query.not_nil!)["channel_id"]? if ucid = HTTP::Params.parse(URI.parse(topic).query.not_nil!)["channel_id"]?
PG_DB.exec("UPDATE channels SET subscribed = $1 WHERE id = $2", Time.utc, ucid) PG_DB.exec("UPDATE channels SET subscribed = $1 WHERE id = $2", Time.utc, ucid)
elsif plid = HTTP::Params.parse(URI.parse(topic).query.not_nil!)["playlist_id"]? elsif plid = HTTP::Params.parse(URI.parse(topic).query.not_nil!)["playlist_id"]?
PG_DB.exec("UPDATE playlists SET subscribed = $1 WHERE id = $2", Time.utc, ucid) Invidious::Database::Playlists.update_subscription_time(plid)
else else
haltf env, status_code: 400 haltf env, status_code: 400
end end
@ -393,7 +393,7 @@ module Invidious::Routes::Feeds
published = Time.parse_rfc3339(entry.xpath_node("published").not_nil!.content) published = Time.parse_rfc3339(entry.xpath_node("published").not_nil!.content)
updated = Time.parse_rfc3339(entry.xpath_node("updated").not_nil!.content) updated = Time.parse_rfc3339(entry.xpath_node("updated").not_nil!.content)
video = get_video(id, PG_DB, force_refresh: true) video = get_video(id, force_refresh: true)
# Deliver notifications to `/api/v1/auth/notifications` # Deliver notifications to `/api/v1/auth/notifications`
payload = { payload = {
@ -416,13 +416,8 @@ module Invidious::Routes::Feeds
views: video.views, views: video.views,
}) })
was_insert = PG_DB.query_one("INSERT INTO channel_videos VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) was_insert = Invidious::Database::ChannelVideos.insert(video, with_premiere_timestamp: true)
ON CONFLICT (id) DO UPDATE SET title = $2, published = $3, Invidious::Database::Users.add_notification(video) if was_insert
updated = $4, ucid = $5, author = $6, length_seconds = $7,
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
end end

View File

@ -275,7 +275,7 @@ module Invidious::Routes::Login
raise "Couldn't get SID." raise "Couldn't get SID."
end end
user, sid = get_user(sid, headers, PG_DB) user, sid = get_user(sid, headers)
# We are now logged in # We are now logged in
traceback << "done.<br/>" traceback << "done.<br/>"
@ -303,8 +303,8 @@ module Invidious::Routes::Login
end end
if env.request.cookies["PREFS"]? if env.request.cookies["PREFS"]?
preferences = env.get("preferences").as(Preferences) user.preferences = env.get("preferences").as(Preferences)
PG_DB.exec("UPDATE users SET preferences = $1 WHERE email = $2", preferences.to_json, user.email) Invidious::Database::Users.update_preferences(user)
cookie = env.request.cookies["PREFS"] cookie = env.request.cookies["PREFS"]
cookie.expires = Time.utc(1990, 1, 1) cookie.expires = Time.utc(1990, 1, 1)
@ -327,7 +327,7 @@ module Invidious::Routes::Login
return error_template(401, "Password is a required field") return error_template(401, "Password is a required field")
end end
user = PG_DB.query_one?("SELECT * FROM users WHERE email = $1", email, as: User) user = Invidious::Database::Users.select(email: email)
if user if user
if !user.password if !user.password
@ -336,7 +336,7 @@ module Invidious::Routes::Login
if Crypto::Bcrypt::Password.new(user.password.not_nil!).verify(password.byte_slice(0, 55)) if Crypto::Bcrypt::Password.new(user.password.not_nil!).verify(password.byte_slice(0, 55))
sid = Base64.urlsafe_encode(Random::Secure.random_bytes(32)) sid = Base64.urlsafe_encode(Random::Secure.random_bytes(32))
PG_DB.exec("INSERT INTO session_ids VALUES ($1, $2, $3)", sid, email, Time.utc) Invidious::Database::SessionIDs.insert(sid, email)
if Kemal.config.ssl || CONFIG.https_only if Kemal.config.ssl || CONFIG.https_only
secure = true secure = true
@ -393,9 +393,9 @@ module Invidious::Routes::Login
prompt = "" prompt = ""
if captcha_type == "image" if captcha_type == "image"
captcha = generate_captcha(HMAC_KEY, PG_DB) captcha = generate_captcha(HMAC_KEY)
else else
captcha = generate_text_captcha(HMAC_KEY, PG_DB) captcha = generate_text_captcha(HMAC_KEY)
end end
return templated "login" return templated "login"
@ -412,7 +412,7 @@ module Invidious::Routes::Login
answer = OpenSSL::HMAC.hexdigest(:sha256, HMAC_KEY, answer) answer = OpenSSL::HMAC.hexdigest(:sha256, HMAC_KEY, answer)
begin begin
validate_request(tokens[0], answer, env.request, HMAC_KEY, PG_DB, locale) validate_request(tokens[0], answer, env.request, HMAC_KEY, locale)
rescue ex rescue ex
return error_template(400, ex) return error_template(400, ex)
end end
@ -427,7 +427,7 @@ module Invidious::Routes::Login
error_exception = Exception.new error_exception = Exception.new
tokens.each do |token| tokens.each do |token|
begin begin
validate_request(token, answer, env.request, HMAC_KEY, PG_DB, locale) validate_request(token, answer, env.request, HMAC_KEY, locale)
found_valid_captcha = true found_valid_captcha = true
rescue ex rescue ex
error_exception = ex error_exception = ex
@ -449,13 +449,8 @@ module Invidious::Routes::Login
end end
end end
user_array = user.to_a Invidious::Database::Users.insert(user)
user_array[4] = user_array[4].to_json # User preferences Invidious::Database::SessionIDs.insert(sid, email)
args = arg_array(user_array)
PG_DB.exec("INSERT INTO users VALUES (#{args})", args: user_array)
PG_DB.exec("INSERT INTO session_ids VALUES ($1, $2, $3)", sid, email, Time.utc)
view_name = "subscriptions_#{sha256(user.email)}" view_name = "subscriptions_#{sha256(user.email)}"
PG_DB.exec("CREATE MATERIALIZED VIEW #{view_name} AS #{MATERIALIZED_VIEW_SQL.call(user.email)}") PG_DB.exec("CREATE MATERIALIZED VIEW #{view_name} AS #{MATERIALIZED_VIEW_SQL.call(user.email)}")
@ -475,8 +470,8 @@ module Invidious::Routes::Login
end end
if env.request.cookies["PREFS"]? if env.request.cookies["PREFS"]?
preferences = env.get("preferences").as(Preferences) user.preferences = env.get("preferences").as(Preferences)
PG_DB.exec("UPDATE users SET preferences = $1 WHERE email = $2", preferences.to_json, user.email) Invidious::Database::Users.update_preferences(user)
cookie = env.request.cookies["PREFS"] cookie = env.request.cookies["PREFS"]
cookie.expires = Time.utc(1990, 1, 1) cookie.expires = Time.utc(1990, 1, 1)
@ -506,12 +501,12 @@ module Invidious::Routes::Login
token = env.params.body["csrf_token"]? token = env.params.body["csrf_token"]?
begin begin
validate_request(token, sid, env.request, HMAC_KEY, PG_DB, locale) validate_request(token, sid, env.request, HMAC_KEY, locale)
rescue ex rescue ex
return error_template(400, ex) return error_template(400, ex)
end end
PG_DB.exec("DELETE FROM session_ids * WHERE id = $1", sid) Invidious::Database::SessionIDs.delete(sid: sid)
env.request.cookies.each do |cookie| env.request.cookies.each do |cookie|
cookie.expires = Time.utc(1990, 1, 1) cookie.expires = Time.utc(1990, 1, 1)

View File

@ -12,7 +12,7 @@ module Invidious::Routes::Playlists
user = user.as(User) user = user.as(User)
sid = sid.as(String) sid = sid.as(String)
csrf_token = generate_response(sid, {":create_playlist"}, HMAC_KEY, PG_DB) csrf_token = generate_response(sid, {":create_playlist"}, HMAC_KEY)
templated "create_playlist" templated "create_playlist"
end end
@ -31,7 +31,7 @@ module Invidious::Routes::Playlists
token = env.params.body["csrf_token"]? token = env.params.body["csrf_token"]?
begin begin
validate_request(token, sid, env.request, HMAC_KEY, PG_DB, locale) validate_request(token, sid, env.request, HMAC_KEY, locale)
rescue ex rescue ex
return error_template(400, ex) return error_template(400, ex)
end end
@ -46,11 +46,11 @@ module Invidious::Routes::Playlists
return error_template(400, "Invalid privacy setting.") return error_template(400, "Invalid privacy setting.")
end end
if PG_DB.query_one("SELECT count(*) FROM playlists WHERE author = $1", user.email, as: Int64) >= 100 if Invidious::Database::Playlists.count_owned_by(user.email) >= 100
return error_template(400, "User cannot have more than 100 playlists.") return error_template(400, "User cannot have more than 100 playlists.")
end end
playlist = create_playlist(PG_DB, title, privacy, user) playlist = create_playlist(title, privacy, user)
env.redirect "/playlist?list=#{playlist.id}" env.redirect "/playlist?list=#{playlist.id}"
end end
@ -66,8 +66,8 @@ module Invidious::Routes::Playlists
user = user.as(User) user = user.as(User)
playlist_id = env.params.query["list"] playlist_id = env.params.query["list"]
playlist = get_playlist(PG_DB, playlist_id, locale) playlist = get_playlist(playlist_id, locale)
subscribe_playlist(PG_DB, user, playlist) subscribe_playlist(user, playlist)
env.redirect "/playlist?list=#{playlist.id}" env.redirect "/playlist?list=#{playlist.id}"
end end
@ -85,12 +85,16 @@ module Invidious::Routes::Playlists
sid = sid.as(String) sid = sid.as(String)
plid = env.params.query["list"]? plid = env.params.query["list"]?
playlist = PG_DB.query_one?("SELECT * FROM playlists WHERE id = $1", plid, as: InvidiousPlaylist) if !plid || plid.empty?
return error_template(400, "A playlist ID is required")
end
playlist = Invidious::Database::Playlists.select(id: plid)
if !playlist || playlist.author != user.email if !playlist || playlist.author != user.email
return env.redirect referer return env.redirect referer
end end
csrf_token = generate_response(sid, {":delete_playlist"}, HMAC_KEY, PG_DB) csrf_token = generate_response(sid, {":delete_playlist"}, HMAC_KEY)
templated "delete_playlist" templated "delete_playlist"
end end
@ -112,18 +116,17 @@ module Invidious::Routes::Playlists
token = env.params.body["csrf_token"]? token = env.params.body["csrf_token"]?
begin begin
validate_request(token, sid, env.request, HMAC_KEY, PG_DB, locale) validate_request(token, sid, env.request, HMAC_KEY, locale)
rescue ex rescue ex
return error_template(400, ex) return error_template(400, ex)
end end
playlist = PG_DB.query_one?("SELECT * FROM playlists WHERE id = $1", plid, as: InvidiousPlaylist) playlist = Invidious::Database::Playlists.select(id: plid)
if !playlist || playlist.author != user.email if !playlist || playlist.author != user.email
return env.redirect referer return env.redirect referer
end end
PG_DB.exec("DELETE FROM playlist_videos * WHERE plid = $1", plid) Invidious::Database::Playlists.delete(plid)
PG_DB.exec("DELETE FROM playlists * WHERE id = $1", plid)
env.redirect "/feed/playlists" env.redirect "/feed/playlists"
end end
@ -149,7 +152,7 @@ module Invidious::Routes::Playlists
page ||= 1 page ||= 1
begin begin
playlist = PG_DB.query_one("SELECT * FROM playlists WHERE id = $1", plid, as: InvidiousPlaylist) playlist = Invidious::Database::Playlists.select(id: plid, raise_on_fail: true)
if !playlist || playlist.author != user.email if !playlist || playlist.author != user.email
return env.redirect referer return env.redirect referer
end end
@ -158,12 +161,12 @@ module Invidious::Routes::Playlists
end end
begin begin
videos = get_playlist_videos(PG_DB, playlist, offset: (page - 1) * 100, locale: locale) videos = get_playlist_videos(playlist, offset: (page - 1) * 100, locale: locale)
rescue ex rescue ex
videos = [] of PlaylistVideo videos = [] of PlaylistVideo
end end
csrf_token = generate_response(sid, {":edit_playlist"}, HMAC_KEY, PG_DB) csrf_token = generate_response(sid, {":edit_playlist"}, HMAC_KEY)
templated "edit_playlist" templated "edit_playlist"
end end
@ -185,12 +188,12 @@ module Invidious::Routes::Playlists
token = env.params.body["csrf_token"]? token = env.params.body["csrf_token"]?
begin begin
validate_request(token, sid, env.request, HMAC_KEY, PG_DB, locale) validate_request(token, sid, env.request, HMAC_KEY, locale)
rescue ex rescue ex
return error_template(400, ex) return error_template(400, ex)
end end
playlist = PG_DB.query_one?("SELECT * FROM playlists WHERE id = $1", plid, as: InvidiousPlaylist) playlist = Invidious::Database::Playlists.select(id: plid)
if !playlist || playlist.author != user.email if !playlist || playlist.author != user.email
return env.redirect referer return env.redirect referer
end end
@ -207,7 +210,7 @@ module Invidious::Routes::Playlists
updated = playlist.updated updated = playlist.updated
end end
PG_DB.exec("UPDATE playlists SET title = $1, privacy = $2, description = $3, updated = $4 WHERE id = $5", title, privacy, description, updated, plid) Invidious::Database::Playlists.update(plid, title, privacy, description, updated)
env.redirect "/playlist?list=#{plid}" env.redirect "/playlist?list=#{plid}"
end end
@ -233,7 +236,7 @@ module Invidious::Routes::Playlists
page ||= 1 page ||= 1
begin begin
playlist = PG_DB.query_one("SELECT * FROM playlists WHERE id = $1", plid, as: InvidiousPlaylist) playlist = Invidious::Database::Playlists.select(id: plid, raise_on_fail: true)
if !playlist || playlist.author != user.email if !playlist || playlist.author != user.email
return env.redirect referer return env.redirect referer
end end
@ -283,7 +286,7 @@ module Invidious::Routes::Playlists
token = env.params.body["csrf_token"]? token = env.params.body["csrf_token"]?
begin begin
validate_request(token, sid, env.request, HMAC_KEY, PG_DB, locale) validate_request(token, sid, env.request, HMAC_KEY, locale)
rescue ex rescue ex
if redirect if redirect
return error_template(400, ex) return error_template(400, ex)
@ -311,7 +314,7 @@ module Invidious::Routes::Playlists
begin begin
playlist_id = env.params.query["playlist_id"] playlist_id = env.params.query["playlist_id"]
playlist = get_playlist(PG_DB, playlist_id, locale).as(InvidiousPlaylist) playlist = get_playlist(playlist_id, locale).as(InvidiousPlaylist)
raise "Invalid user" if playlist.author != user.email raise "Invalid user" if playlist.author != user.email
rescue ex rescue ex
if redirect if redirect
@ -342,7 +345,7 @@ module Invidious::Routes::Playlists
video_id = env.params.query["video_id"] video_id = env.params.query["video_id"]
begin begin
video = get_video(video_id, PG_DB) video = get_video(video_id)
rescue ex rescue ex
if redirect if redirect
return error_template(500, ex) return error_template(500, ex)
@ -363,15 +366,12 @@ module Invidious::Routes::Playlists
index: Random::Secure.rand(0_i64..Int64::MAX), index: Random::Secure.rand(0_i64..Int64::MAX),
}) })
video_array = playlist_video.to_a Invidious::Database::PlaylistVideos.insert(playlist_video)
args = arg_array(video_array) Invidious::Database::Playlists.update_video_added(playlist_id, playlist_video.index)
PG_DB.exec("INSERT INTO playlist_videos VALUES (#{args})", args: video_array)
PG_DB.exec("UPDATE playlists SET index = array_append(index, $1), video_count = cardinality(index) + 1, updated = $2 WHERE id = $3", playlist_video.index, Time.utc, playlist_id)
when "action_remove_video" when "action_remove_video"
index = env.params.query["set_video_id"] index = env.params.query["set_video_id"]
PG_DB.exec("DELETE FROM playlist_videos * WHERE index = $1", index) Invidious::Database::PlaylistVideos.delete(index)
PG_DB.exec("UPDATE playlists SET index = array_remove(index, $1), video_count = cardinality(index) - 1, updated = $2 WHERE id = $3", index, Time.utc, playlist_id) Invidious::Database::Playlists.update_video_removed(playlist_id, index)
when "action_move_video_before" when "action_move_video_before"
# TODO: Playlist stub # TODO: Playlist stub
else else
@ -405,7 +405,7 @@ module Invidious::Routes::Playlists
end end
begin begin
playlist = get_playlist(PG_DB, plid, locale) playlist = get_playlist(plid, locale)
rescue ex rescue ex
return error_template(500, ex) return error_template(500, ex)
end end
@ -422,7 +422,7 @@ module Invidious::Routes::Playlists
end end
begin begin
videos = get_playlist_videos(PG_DB, playlist, offset: (page - 1) * 100, locale: locale) videos = get_playlist_videos(playlist, offset: (page - 1) * 100, locale: locale)
rescue ex rescue ex
return error_template(500, "Error encountered while retrieving playlist videos.<br>#{ex.message}") return error_template(500, "Error encountered while retrieving playlist videos.<br>#{ex.message}")
end end

View File

@ -170,11 +170,12 @@ module Invidious::Routes::PreferencesRoute
vr_mode: vr_mode, vr_mode: vr_mode,
show_nick: show_nick, show_nick: show_nick,
save_player_pos: save_player_pos, save_player_pos: save_player_pos,
}.to_json).to_json }.to_json)
if user = env.get? "user" if user = env.get? "user"
user = user.as(User) user = user.as(User)
PG_DB.exec("UPDATE users SET preferences = $1 WHERE email = $2", preferences, user.email) user.preferences = preferences
Invidious::Database::Users.update_preferences(user)
if CONFIG.admins.includes? user.email if CONFIG.admins.includes? user.email
CONFIG.default_user_preferences.default_home = env.params.body["admin_default_home"]?.try &.as(String) || CONFIG.default_user_preferences.default_home CONFIG.default_user_preferences.default_home = env.params.body["admin_default_home"]?.try &.as(String) || CONFIG.default_user_preferences.default_home
@ -220,10 +221,10 @@ module Invidious::Routes::PreferencesRoute
end end
if CONFIG.domain if CONFIG.domain
env.response.cookies["PREFS"] = HTTP::Cookie.new(name: "PREFS", domain: "#{CONFIG.domain}", value: URI.encode_www_form(preferences), expires: Time.utc + 2.years, env.response.cookies["PREFS"] = HTTP::Cookie.new(name: "PREFS", domain: "#{CONFIG.domain}", value: URI.encode_www_form(preferences.to_json), expires: Time.utc + 2.years,
secure: secure, http_only: true) secure: secure, http_only: true)
else else
env.response.cookies["PREFS"] = HTTP::Cookie.new(name: "PREFS", value: URI.encode_www_form(preferences), expires: Time.utc + 2.years, env.response.cookies["PREFS"] = HTTP::Cookie.new(name: "PREFS", value: URI.encode_www_form(preferences.to_json), expires: Time.utc + 2.years,
secure: secure, http_only: true) secure: secure, http_only: true)
end end
end end
@ -241,18 +242,15 @@ module Invidious::Routes::PreferencesRoute
if user = env.get? "user" if user = env.get? "user"
user = user.as(User) user = user.as(User)
preferences = user.preferences
case preferences.dark_mode case user.preferences.dark_mode
when "dark" when "dark"
preferences.dark_mode = "light" user.preferences.dark_mode = "light"
else else
preferences.dark_mode = "dark" user.preferences.dark_mode = "dark"
end end
preferences = preferences.to_json Invidious::Database::Users.update_preferences(user)
PG_DB.exec("UPDATE users SET preferences = $1 WHERE email = $2", preferences, user.email)
else else
preferences = env.get("preferences").as(Preferences) preferences = env.get("preferences").as(Preferences)

View File

@ -263,7 +263,7 @@ module Invidious::Routes::VideoPlayback
haltf env, status_code: 400, response: "TESTING" haltf env, status_code: 400, response: "TESTING"
end end
video = get_video(id, PG_DB, region: region) video = get_video(id, region: region)
fmt = video.fmt_stream.find(nil) { |f| f["itag"].as_i == itag } || video.adaptive_fmts.find(nil) { |f| f["itag"].as_i == itag } fmt = video.fmt_stream.find(nil) { |f| f["itag"].as_i == itag } || video.adaptive_fmts.find(nil) { |f| f["itag"].as_i == itag }
url = fmt.try &.["url"]?.try &.as_s url = fmt.try &.["url"]?.try &.as_s

View File

@ -39,7 +39,7 @@ module Invidious::Routes::Watch
end end
plid = env.params.query["list"]?.try &.gsub(/[^a-zA-Z0-9_-]/, "") plid = env.params.query["list"]?.try &.gsub(/[^a-zA-Z0-9_-]/, "")
continuation = process_continuation(PG_DB, env.params.query, plid, id) continuation = process_continuation(env.params.query, plid, id)
nojs = env.params.query["nojs"]? nojs = env.params.query["nojs"]?
@ -60,7 +60,7 @@ module Invidious::Routes::Watch
env.params.query.delete_all("listen") env.params.query.delete_all("listen")
begin begin
video = get_video(id, PG_DB, region: params.region) video = get_video(id, region: params.region)
rescue ex : VideoRedirect rescue ex : VideoRedirect
return env.redirect env.request.resource.gsub(id, ex.video_id) return env.redirect env.request.resource.gsub(id, ex.video_id)
rescue ex rescue ex
@ -76,11 +76,11 @@ module Invidious::Routes::Watch
env.params.query.delete_all("iv_load_policy") env.params.query.delete_all("iv_load_policy")
if watched && !watched.includes? id if watched && !watched.includes? id
PG_DB.exec("UPDATE users SET watched = array_append(watched, $1) WHERE email = $2", id, user.as(User).email) Invidious::Database::Users.mark_watched(user.as(User), id)
end end
if notifications && notifications.includes? id if notifications && notifications.includes? id
PG_DB.exec("UPDATE users SET notifications = array_remove(notifications, $1) WHERE email = $2", id, user.as(User).email) Invidious::Database::Users.remove_notification(user.as(User), id)
env.get("user").as(User).notifications.delete(id) env.get("user").as(User).notifications.delete(id)
notifications.delete(id) notifications.delete(id)
end end

View File

@ -29,43 +29,31 @@ struct User
end end
end end
def get_user(sid, headers, db, refresh = true) def get_user(sid, headers, refresh = true)
if email = db.query_one?("SELECT email FROM session_ids WHERE id = $1", sid, as: String) if email = Invidious::Database::SessionIDs.select_email(sid)
user = db.query_one("SELECT * FROM users WHERE email = $1", email, as: User) user = Invidious::Database::Users.select!(email: email)
if refresh && Time.utc - user.updated > 1.minute if refresh && Time.utc - user.updated > 1.minute
user, sid = fetch_user(sid, headers, db) user, sid = fetch_user(sid, headers)
user_array = user.to_a
user_array[4] = user_array[4].to_json # User preferences
args = arg_array(user_array)
db.exec("INSERT INTO users VALUES (#{args}) \ Invidious::Database::Users.insert(user, update_on_conflict: true)
ON CONFLICT (email) DO UPDATE SET updated = $1, subscriptions = $3", args: user_array) Invidious::Database::SessionIDs.insert(sid, user.email, handle_conflicts: true)
db.exec("INSERT INTO session_ids VALUES ($1,$2,$3) \
ON CONFLICT (id) DO NOTHING", sid, user.email, Time.utc)
begin begin
view_name = "subscriptions_#{sha256(user.email)}" view_name = "subscriptions_#{sha256(user.email)}"
db.exec("CREATE MATERIALIZED VIEW #{view_name} AS #{MATERIALIZED_VIEW_SQL.call(user.email)}") PG_DB.exec("CREATE MATERIALIZED VIEW #{view_name} AS #{MATERIALIZED_VIEW_SQL.call(user.email)}")
rescue ex rescue ex
end end
end end
else else
user, sid = fetch_user(sid, headers, db) user, sid = fetch_user(sid, headers)
user_array = user.to_a
user_array[4] = user_array[4].to_json # User preferences
args = arg_array(user.to_a)
db.exec("INSERT INTO users VALUES (#{args}) \ Invidious::Database::Users.insert(user, update_on_conflict: true)
ON CONFLICT (email) DO UPDATE SET updated = $1, subscriptions = $3", args: user_array) Invidious::Database::SessionIDs.insert(sid, user.email, handle_conflicts: true)
db.exec("INSERT INTO session_ids VALUES ($1,$2,$3) \
ON CONFLICT (id) DO NOTHING", sid, user.email, Time.utc)
begin begin
view_name = "subscriptions_#{sha256(user.email)}" view_name = "subscriptions_#{sha256(user.email)}"
db.exec("CREATE MATERIALIZED VIEW #{view_name} AS #{MATERIALIZED_VIEW_SQL.call(user.email)}") PG_DB.exec("CREATE MATERIALIZED VIEW #{view_name} AS #{MATERIALIZED_VIEW_SQL.call(user.email)}")
rescue ex rescue ex
end end
end end
@ -73,7 +61,7 @@ def get_user(sid, headers, db, refresh = true)
return user, sid return user, sid
end end
def fetch_user(sid, headers, db) def fetch_user(sid, headers)
feed = YT_POOL.client &.get("/subscription_manager?disable_polymer=1", headers) feed = YT_POOL.client &.get("/subscription_manager?disable_polymer=1", headers)
feed = XML.parse_html(feed.body) feed = XML.parse_html(feed.body)
@ -86,7 +74,7 @@ def fetch_user(sid, headers, db)
end end
end end
channels = get_batch_channels(channels, db, false, false) channels = get_batch_channels(channels, false, false)
email = feed.xpath_node(%q(//a[@class="yt-masthead-picker-header yt-masthead-picker-active-account"])) email = feed.xpath_node(%q(//a[@class="yt-masthead-picker-header yt-masthead-picker-active-account"]))
if email if email
@ -130,7 +118,7 @@ def create_user(sid, email, password)
return user, sid return user, sid
end end
def generate_captcha(key, db) def generate_captcha(key)
second = Random::Secure.rand(12) second = Random::Secure.rand(12)
second_angle = second * 30 second_angle = second * 30
second = second * 5 second = second * 5
@ -182,16 +170,16 @@ def generate_captcha(key, db)
return { return {
question: image, question: image,
tokens: {generate_response(answer, {":login"}, key, db, use_nonce: true)}, tokens: {generate_response(answer, {":login"}, key, use_nonce: true)},
} }
end end
def generate_text_captcha(key, db) def generate_text_captcha(key)
response = make_client(TEXTCAPTCHA_URL, &.get("/github.com/iv.org/invidious.json").body) response = make_client(TEXTCAPTCHA_URL, &.get("/github.com/iv.org/invidious.json").body)
response = JSON.parse(response) response = JSON.parse(response)
tokens = response["a"].as_a.map do |answer| tokens = response["a"].as_a.map do |answer|
generate_response(answer.as_s, {":login"}, key, db, use_nonce: true) generate_response(answer.as_s, {":login"}, key, use_nonce: true)
end end
return { return {
@ -232,20 +220,16 @@ def subscribe_ajax(channel_id, action, env_headers)
end end
end end
def get_subscription_feed(db, user, max_results = 40, page = 1) def get_subscription_feed(user, max_results = 40, page = 1)
limit = max_results.clamp(0, MAX_ITEMS_PER_PAGE) limit = max_results.clamp(0, MAX_ITEMS_PER_PAGE)
offset = (page - 1) * limit offset = (page - 1) * limit
notifications = db.query_one("SELECT notifications FROM users WHERE email = $1", user.email, notifications = Invidious::Database::Users.select_notifications(user)
as: Array(String))
view_name = "subscriptions_#{sha256(user.email)}" view_name = "subscriptions_#{sha256(user.email)}"
if user.preferences.notifications_only && !notifications.empty? if user.preferences.notifications_only && !notifications.empty?
# Only show notifications # Only show notifications
notifications = Invidious::Database::ChannelVideos.select(notifications)
args = arg_array(notifications)
notifications = db.query_all("SELECT * FROM channel_videos WHERE id IN (#{args}) ORDER BY published DESC", args: notifications, as: ChannelVideo)
videos = [] of ChannelVideo videos = [] of ChannelVideo
notifications.sort_by!(&.published).reverse! notifications.sort_by!(&.published).reverse!
@ -311,8 +295,7 @@ def get_subscription_feed(db, user, max_results = 40, page = 1)
else nil # Ignore else nil # Ignore
end end
notifications = PG_DB.query_one("SELECT notifications FROM users WHERE email = $1", user.email, as: Array(String)) notifications = Invidious::Database::Users.select_notifications(user)
notifications = videos.select { |v| notifications.includes? v.id } notifications = videos.select { |v| notifications.includes? v.id }
videos = videos - notifications videos = videos - notifications
end end

View File

@ -993,8 +993,8 @@ def extract_video_info(video_id : String, proxy_region : String? = nil, context_
return params return params
end end
def get_video(id, db, refresh = true, region = nil, force_refresh = false) def get_video(id, refresh = true, region = nil, force_refresh = false)
if (video = db.query_one?("SELECT * FROM videos WHERE id = $1", id, as: Video)) && !region if (video = Invidious::Database::Videos.select(id)) && !region
# If record was last updated over 10 minutes ago, or video has since premiered, # If record was last updated over 10 minutes ago, or video has since premiered,
# refresh (expire param in response lasts for 6 hours) # refresh (expire param in response lasts for 6 hours)
if (refresh && if (refresh &&
@ -1003,17 +1003,15 @@ def get_video(id, db, refresh = true, region = nil, force_refresh = false)
force_refresh force_refresh
begin begin
video = fetch_video(id, region) video = fetch_video(id, region)
db.exec("UPDATE videos SET (id, info, updated) = ($1, $2, $3) WHERE id = $1", video.id, video.info.to_json, video.updated) Invidious::Database::Videos.update(video)
rescue ex rescue ex
db.exec("DELETE FROM videos * WHERE id = $1", id) Invidious::Database::Videos.delete(id)
raise ex raise ex
end end
end end
else else
video = fetch_video(id, region) video = fetch_video(id, region)
if !region Invidious::Database::Videos.insert(video) if !region
db.exec("INSERT INTO videos VALUES ($1, $2, $3) ON CONFLICT (id) DO NOTHING", video.id, video.info.to_json, video.updated)
end
end end
return video return video
@ -1058,7 +1056,7 @@ def itag_to_metadata?(itag : JSON::Any)
return VIDEO_FORMATS[itag.to_s]? return VIDEO_FORMATS[itag.to_s]?
end end
def process_continuation(db, query, plid, id) def process_continuation(query, plid, id)
continuation = nil continuation = nil
if plid if plid
if index = query["index"]?.try &.to_i? if index = query["index"]?.try &.to_i?

View File

@ -61,7 +61,7 @@
<div class="pure-u-1-3"><a href="/edit_playlist?list=<%= plid %>"><i class="icon ion-md-create"></i></a></div> <div class="pure-u-1-3"><a href="/edit_playlist?list=<%= plid %>"><i class="icon ion-md-create"></i></a></div>
<div class="pure-u-1-3"><a href="/delete_playlist?list=<%= plid %>"><i class="icon ion-md-trash"></i></a></div> <div class="pure-u-1-3"><a href="/delete_playlist?list=<%= plid %>"><i class="icon ion-md-trash"></i></a></div>
<% else %> <% else %>
<% if PG_DB.query_one?("SELECT id FROM playlists WHERE id = $1", playlist.id, as: String).nil? %> <% if Invidious::Database::Playlists.exists?(playlist.id) %>
<div class="pure-u-1-3"><a href="/subscribe_playlist?list=<%= plid %>"><i class="icon ion-md-add"></i></a></div> <div class="pure-u-1-3"><a href="/subscribe_playlist?list=<%= plid %>"><i class="icon ion-md-add"></i></a></div>
<% else %> <% else %>
<div class="pure-u-1-3"><a href="/delete_playlist?list=<%= plid %>"><i class="icon ion-md-trash"></i></a></div> <div class="pure-u-1-3"><a href="/delete_playlist?list=<%= plid %>"><i class="icon ion-md-trash"></i></a></div>

View File

@ -138,7 +138,7 @@ we're going to need to do it here in order to allow for translations.
</p> </p>
<% if user %> <% if user %>
<% playlists = PG_DB.query_all("SELECT id,title FROM playlists WHERE author = $1 AND id LIKE 'IV%'", user.email, as: {String, String}) %> <% playlists = Invidious::Database::Playlists.select_user_created_playlists(user.email) %>
<% if !playlists.empty? %> <% if !playlists.empty? %>
<form data-onsubmit="return_false" class="pure-form pure-form-stacked" action="/playlist_ajax" method="post"> <form data-onsubmit="return_false" class="pure-form pure-form-stacked" action="/playlist_ajax" method="post">
<div class="pure-control-group"> <div class="pure-control-group">