208 lines
6.8 KiB
Crystal
Raw Normal View History

2018-11-08 20:08:03 -06:00
require "crypto/bcrypt/password"
# Materialized views may not be defined using bound parameters (`$1` as used elsewhere)
2019-07-09 09:34:19 -05:00
MATERIALIZED_VIEW_SQL = ->(email : String) { "SELECT cv.* FROM channel_videos cv WHERE EXISTS (SELECT subscriptions FROM users u WHERE cv.ucid = ANY (u.subscriptions) AND u.email = E'#{email.gsub({'\'' => "\\'", '\\' => "\\\\"})}') ORDER BY published DESC" }
2021-12-06 22:28:16 +01:00
def get_user(sid, headers, refresh = true)
if email = Invidious::Database::SessionIDs.select_email(sid)
user = Invidious::Database::Users.select!(email: email)
2018-08-04 15:30:44 -05:00
2019-06-07 19:56:41 -05:00
if refresh && Time.utc - user.updated > 1.minute
2021-12-06 22:28:16 +01:00
user, sid = fetch_user(sid, headers)
2019-02-10 12:33:29 -06:00
Invidious::Database::Users.insert(user, update_on_conflict: true)
Invidious::Database::SessionIDs.insert(sid, user.email, handle_conflicts: true)
begin
2019-04-10 19:56:38 -05:00
view_name = "subscriptions_#{sha256(user.email)}"
2021-12-06 22:28:16 +01:00
PG_DB.exec("CREATE MATERIALIZED VIEW #{view_name} AS #{MATERIALIZED_VIEW_SQL.call(user.email)}")
rescue ex
end
2018-08-04 15:30:44 -05:00
end
else
2021-12-06 22:28:16 +01:00
user, sid = fetch_user(sid, headers)
2019-02-10 12:33:29 -06:00
Invidious::Database::Users.insert(user, update_on_conflict: true)
Invidious::Database::SessionIDs.insert(sid, user.email, handle_conflicts: true)
begin
2019-04-10 19:56:38 -05:00
view_name = "subscriptions_#{sha256(user.email)}"
2021-12-06 22:28:16 +01:00
PG_DB.exec("CREATE MATERIALIZED VIEW #{view_name} AS #{MATERIALIZED_VIEW_SQL.call(user.email)}")
rescue ex
end
2018-08-04 15:30:44 -05:00
end
2019-02-10 12:33:29 -06:00
return user, sid
2018-08-04 15:30:44 -05:00
end
2021-12-06 22:28:16 +01:00
def fetch_user(sid, headers)
2019-10-25 12:58:16 -04:00
feed = YT_POOL.client &.get("/subscription_manager?disable_polymer=1", headers)
2018-08-04 15:30:44 -05:00
feed = XML.parse_html(feed.body)
channels = feed.xpath_nodes(%q(//ul[@id="guide-channels"]/li/a)).compact_map do |channel|
if {"Popular on YouTube", "Music", "Sports", "Gaming"}.includes? channel["title"]
nil
else
channel["href"].lstrip("/channel/")
2018-08-04 15:30:44 -05:00
end
end
2022-01-21 21:27:50 -06:00
channels = get_batch_channels(channels)
2018-08-04 15:30:44 -05:00
email = feed.xpath_node(%q(//a[@class="yt-masthead-picker-header yt-masthead-picker-active-account"]))
if email
email = email.content.strip
else
email = ""
end
token = Base64.urlsafe_encode(Random::Secure.random_bytes(32))
user = Invidious::User.new({
updated: Time.utc,
notifications: [] of String,
subscriptions: channels,
email: email,
preferences: Preferences.new(CONFIG.default_user_preferences.to_tuple),
password: nil,
token: token,
watched: [] of String,
feed_needs_update: true,
})
2019-02-10 12:33:29 -06:00
return user, sid
2018-08-04 15:30:44 -05:00
end
def create_user(sid, email, password)
password = Crypto::Bcrypt::Password.create(password, cost: 10)
token = Base64.urlsafe_encode(Random::Secure.random_bytes(32))
user = Invidious::User.new({
updated: Time.utc,
notifications: [] of String,
subscriptions: [] of String,
email: email,
preferences: Preferences.new(CONFIG.default_user_preferences.to_tuple),
password: password.to_s,
token: token,
watched: [] of String,
feed_needs_update: true,
})
2018-08-04 15:30:44 -05:00
2019-02-10 12:33:29 -06:00
return user, sid
2018-08-04 15:30:44 -05:00
end
2018-11-11 09:44:16 -06:00
2019-05-15 12:26:29 -05:00
def subscribe_ajax(channel_id, action, env_headers)
headers = HTTP::Headers.new
headers["Cookie"] = env_headers["Cookie"]
2019-10-25 12:58:16 -04:00
html = YT_POOL.client &.get("/subscription_manager?disable_polymer=1", headers)
2019-05-15 12:26:29 -05:00
2021-05-24 06:45:50 -07:00
cookies = HTTP::Cookies.from_client_headers(headers)
2019-05-15 12:26:29 -05:00
html.cookies.each do |cookie|
if {"VISITOR_INFO1_LIVE", "YSC", "SIDCC"}.includes? cookie.name
if cookies[cookie.name]?
cookies[cookie.name] = cookie
else
cookies << cookie
end
end
end
headers = cookies.add_request_headers(headers)
2020-06-15 17:33:23 -05:00
if match = html.body.match(/'XSRF_TOKEN': "(?<session_token>[^"]+)"/)
2019-05-15 12:26:29 -05:00
session_token = match["session_token"]
headers["content-type"] = "application/x-www-form-urlencoded"
post_req = {
2019-06-07 19:56:41 -05:00
session_token: session_token,
2019-05-15 12:26:29 -05:00
}
post_url = "/subscription_ajax?#{action}=1&c=#{channel_id}"
2019-10-25 12:58:16 -04:00
YT_POOL.client &.post(post_url, headers, form: post_req)
2019-05-15 12:26:29 -05:00
end
end
2019-06-07 12:39:12 -05:00
2021-12-07 03:57:18 +01:00
def get_subscription_feed(user, max_results = 40, page = 1)
2019-06-07 12:39:12 -05:00
limit = max_results.clamp(0, MAX_ITEMS_PER_PAGE)
offset = (page - 1) * limit
notifications = Invidious::Database::Users.select_notifications(user)
2019-06-07 12:39:12 -05:00
view_name = "subscriptions_#{sha256(user.email)}"
if user.preferences.notifications_only && !notifications.empty?
# Only show notifications
notifications = Invidious::Database::ChannelVideos.select(notifications)
2019-06-07 12:39:12 -05:00
videos = [] of ChannelVideo
2021-09-24 19:42:43 -07:00
notifications.sort_by!(&.published).reverse!
2019-06-07 12:39:12 -05:00
case user.preferences.sort
when "alphabetically"
2021-09-24 19:42:43 -07:00
notifications.sort_by!(&.title)
2019-06-07 12:39:12 -05:00
when "alphabetically - reverse"
2021-09-24 19:42:43 -07:00
notifications.sort_by!(&.title).reverse!
2019-06-07 12:39:12 -05:00
when "channel name"
2021-09-24 19:42:43 -07:00
notifications.sort_by!(&.author)
2019-06-07 12:39:12 -05:00
when "channel name - reverse"
2021-09-24 19:42:43 -07:00
notifications.sort_by!(&.author).reverse!
else nil # Ignore
2019-06-07 12:39:12 -05:00
end
else
if user.preferences.latest_only
if user.preferences.unseen_only
# Show latest video from a channel that a user hasn't watched
# "unseen_only" isn't really correct here, more accurate would be "unwatched_only"
if user.watched.empty?
values = "'{}'"
else
values = "VALUES #{user.watched.map { |id| %(('#{id}')) }.join(",")}"
end
2019-08-27 08:08:26 -05:00
videos = PG_DB.query_all("SELECT DISTINCT ON (ucid) * FROM #{view_name} WHERE NOT id = ANY (#{values}) ORDER BY ucid, published DESC", as: ChannelVideo)
2019-06-07 12:39:12 -05:00
else
# Show latest video from each channel
2019-08-27 08:08:26 -05:00
videos = PG_DB.query_all("SELECT DISTINCT ON (ucid) * FROM #{view_name} ORDER BY ucid, published DESC", as: ChannelVideo)
2019-06-07 12:39:12 -05:00
end
2021-09-24 19:42:43 -07:00
videos.sort_by!(&.published).reverse!
2019-06-07 12:39:12 -05:00
else
if user.preferences.unseen_only
# Only show unwatched
if user.watched.empty?
values = "'{}'"
else
values = "VALUES #{user.watched.map { |id| %(('#{id}')) }.join(",")}"
end
2019-08-27 08:08:26 -05:00
videos = PG_DB.query_all("SELECT * FROM #{view_name} WHERE NOT id = ANY (#{values}) ORDER BY published DESC LIMIT $1 OFFSET $2", limit, offset, as: ChannelVideo)
2019-06-07 12:39:12 -05:00
else
# Sort subscriptions as normal
2019-08-27 08:08:26 -05:00
videos = PG_DB.query_all("SELECT * FROM #{view_name} ORDER BY published DESC LIMIT $1 OFFSET $2", limit, offset, as: ChannelVideo)
2019-06-07 12:39:12 -05:00
end
end
case user.preferences.sort
when "published - reverse"
2021-09-24 19:42:43 -07:00
videos.sort_by!(&.published)
2019-06-07 12:39:12 -05:00
when "alphabetically"
2021-09-24 19:42:43 -07:00
videos.sort_by!(&.title)
2019-06-07 12:39:12 -05:00
when "alphabetically - reverse"
2021-09-24 19:42:43 -07:00
videos.sort_by!(&.title).reverse!
2019-06-07 12:39:12 -05:00
when "channel name"
2021-09-24 19:42:43 -07:00
videos.sort_by!(&.author)
2019-06-07 12:39:12 -05:00
when "channel name - reverse"
2021-09-24 19:42:43 -07:00
videos.sort_by!(&.author).reverse!
else nil # Ignore
2019-06-07 12:39:12 -05:00
end
notifications = Invidious::Database::Users.select_notifications(user)
2019-06-07 12:39:12 -05:00
notifications = videos.select { |v| notifications.includes? v.id }
videos = videos - notifications
end
return videos, notifications
end