invidious-mod/src/invidious/users.cr

208 行
6.8 KiB
Crystal
Raw 通常表示 履歴

2018-11-09 11:08:03 +09:00
require "crypto/bcrypt/password"
# Materialized views may not be defined using bound parameters (`$1` as used elsewhere)
2019-07-09 23:34:19 +09: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-07 06:28:16 +09: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-05 05:30:44 +09:00
2019-06-08 09:56:41 +09:00
if refresh && Time.utc - user.updated > 1.minute
2021-12-07 06:28:16 +09:00
user, sid = fetch_user(sid, headers)
2019-02-11 03:33:29 +09:00
Invidious::Database::Users.insert(user, update_on_conflict: true)
Invidious::Database::SessionIDs.insert(sid, user.email, handle_conflicts: true)
begin
2019-04-11 09:56:38 +09:00
view_name = "subscriptions_#{sha256(user.email)}"
2021-12-07 06:28:16 +09:00
PG_DB.exec("CREATE MATERIALIZED VIEW #{view_name} AS #{MATERIALIZED_VIEW_SQL.call(user.email)}")
rescue ex
end
2018-08-05 05:30:44 +09:00
end
else
2021-12-07 06:28:16 +09:00
user, sid = fetch_user(sid, headers)
2019-02-11 03:33:29 +09:00
Invidious::Database::Users.insert(user, update_on_conflict: true)
Invidious::Database::SessionIDs.insert(sid, user.email, handle_conflicts: true)
begin
2019-04-11 09:56:38 +09:00
view_name = "subscriptions_#{sha256(user.email)}"
2021-12-07 06:28:16 +09:00
PG_DB.exec("CREATE MATERIALIZED VIEW #{view_name} AS #{MATERIALIZED_VIEW_SQL.call(user.email)}")
rescue ex
end
2018-08-05 05:30:44 +09:00
end
2019-02-11 03:33:29 +09:00
return user, sid
2018-08-05 05:30:44 +09:00
end
2021-12-07 06:28:16 +09:00
def fetch_user(sid, headers)
2019-10-26 01:58:16 +09:00
feed = YT_POOL.client &.get("/subscription_manager?disable_polymer=1", headers)
2018-08-05 05:30:44 +09: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-05 05:30:44 +09:00
end
end
2022-01-22 12:27:50 +09:00
channels = get_batch_channels(channels)
2018-08-05 05:30:44 +09: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-11 03:33:29 +09:00
return user, sid
2018-08-05 05:30:44 +09: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-05 05:30:44 +09:00
2019-02-11 03:33:29 +09:00
return user, sid
2018-08-05 05:30:44 +09:00
end
2018-11-12 00:44:16 +09:00
2019-05-16 02:26:29 +09:00
def subscribe_ajax(channel_id, action, env_headers)
headers = HTTP::Headers.new
headers["Cookie"] = env_headers["Cookie"]
2019-10-26 01:58:16 +09:00
html = YT_POOL.client &.get("/subscription_manager?disable_polymer=1", headers)
2019-05-16 02:26:29 +09:00
2021-05-24 22:45:50 +09:00
cookies = HTTP::Cookies.from_client_headers(headers)
2019-05-16 02:26:29 +09: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-16 07:33:23 +09:00
if match = html.body.match(/'XSRF_TOKEN': "(?<session_token>[^"]+)"/)
2019-05-16 02:26:29 +09:00
session_token = match["session_token"]
headers["content-type"] = "application/x-www-form-urlencoded"
post_req = {
2019-06-08 09:56:41 +09:00
session_token: session_token,
2019-05-16 02:26:29 +09:00
}
post_url = "/subscription_ajax?#{action}=1&c=#{channel_id}"
2019-10-26 01:58:16 +09:00
YT_POOL.client &.post(post_url, headers, form: post_req)
2019-05-16 02:26:29 +09:00
end
end
2019-06-08 02:39:12 +09:00
2021-12-07 11:57:18 +09:00
def get_subscription_feed(user, max_results = 40, page = 1)
2019-06-08 02:39:12 +09:00
limit = max_results.clamp(0, MAX_ITEMS_PER_PAGE)
offset = (page - 1) * limit
notifications = Invidious::Database::Users.select_notifications(user)
2019-06-08 02:39:12 +09: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-08 02:39:12 +09:00
videos = [] of ChannelVideo
2021-09-25 11:42:43 +09:00
notifications.sort_by!(&.published).reverse!
2019-06-08 02:39:12 +09:00
case user.preferences.sort
when "alphabetically"
2021-09-25 11:42:43 +09:00
notifications.sort_by!(&.title)
2019-06-08 02:39:12 +09:00
when "alphabetically - reverse"
2021-09-25 11:42:43 +09:00
notifications.sort_by!(&.title).reverse!
2019-06-08 02:39:12 +09:00
when "channel name"
2021-09-25 11:42:43 +09:00
notifications.sort_by!(&.author)
2019-06-08 02:39:12 +09:00
when "channel name - reverse"
2021-09-25 11:42:43 +09:00
notifications.sort_by!(&.author).reverse!
else nil # Ignore
2019-06-08 02:39:12 +09: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 22:08:26 +09: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-08 02:39:12 +09:00
else
# Show latest video from each channel
2019-08-27 22:08:26 +09:00
videos = PG_DB.query_all("SELECT DISTINCT ON (ucid) * FROM #{view_name} ORDER BY ucid, published DESC", as: ChannelVideo)
2019-06-08 02:39:12 +09:00
end
2021-09-25 11:42:43 +09:00
videos.sort_by!(&.published).reverse!
2019-06-08 02:39:12 +09: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 22:08:26 +09: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-08 02:39:12 +09:00
else
# Sort subscriptions as normal
2019-08-27 22:08:26 +09:00
videos = PG_DB.query_all("SELECT * FROM #{view_name} ORDER BY published DESC LIMIT $1 OFFSET $2", limit, offset, as: ChannelVideo)
2019-06-08 02:39:12 +09:00
end
end
case user.preferences.sort
when "published - reverse"
2021-09-25 11:42:43 +09:00
videos.sort_by!(&.published)
2019-06-08 02:39:12 +09:00
when "alphabetically"
2021-09-25 11:42:43 +09:00
videos.sort_by!(&.title)
2019-06-08 02:39:12 +09:00
when "alphabetically - reverse"
2021-09-25 11:42:43 +09:00
videos.sort_by!(&.title).reverse!
2019-06-08 02:39:12 +09:00
when "channel name"
2021-09-25 11:42:43 +09:00
videos.sort_by!(&.author)
2019-06-08 02:39:12 +09:00
when "channel name - reverse"
2021-09-25 11:42:43 +09:00
videos.sort_by!(&.author).reverse!
else nil # Ignore
2019-06-08 02:39:12 +09:00
end
notifications = Invidious::Database::Users.select_notifications(user)
2019-06-08 02:39:12 +09:00
notifications = videos.select { |v| notifications.includes? v.id }
videos = videos - notifications
end
return videos, notifications
end