forked from midou/invidious
208 lines
6.8 KiB
Crystal
208 lines
6.8 KiB
Crystal
require "crypto/bcrypt/password"
|
|
|
|
# Materialized views may not be defined using bound parameters (`$1` as used elsewhere)
|
|
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" }
|
|
|
|
def get_user(sid, headers, refresh = true)
|
|
if email = Invidious::Database::SessionIDs.select_email(sid)
|
|
user = Invidious::Database::Users.select!(email: email)
|
|
|
|
if refresh && Time.utc - user.updated > 1.minute
|
|
user, sid = fetch_user(sid, headers)
|
|
|
|
Invidious::Database::Users.insert(user, update_on_conflict: true)
|
|
Invidious::Database::SessionIDs.insert(sid, user.email, handle_conflicts: true)
|
|
|
|
begin
|
|
view_name = "subscriptions_#{sha256(user.email)}"
|
|
PG_DB.exec("CREATE MATERIALIZED VIEW #{view_name} AS #{MATERIALIZED_VIEW_SQL.call(user.email)}")
|
|
rescue ex
|
|
end
|
|
end
|
|
else
|
|
user, sid = fetch_user(sid, headers)
|
|
|
|
Invidious::Database::Users.insert(user, update_on_conflict: true)
|
|
Invidious::Database::SessionIDs.insert(sid, user.email, handle_conflicts: true)
|
|
|
|
begin
|
|
view_name = "subscriptions_#{sha256(user.email)}"
|
|
PG_DB.exec("CREATE MATERIALIZED VIEW #{view_name} AS #{MATERIALIZED_VIEW_SQL.call(user.email)}")
|
|
rescue ex
|
|
end
|
|
end
|
|
|
|
return user, sid
|
|
end
|
|
|
|
def fetch_user(sid, headers)
|
|
feed = YT_POOL.client &.get("/subscription_manager?disable_polymer=1", headers)
|
|
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/")
|
|
end
|
|
end
|
|
|
|
channels = get_batch_channels(channels)
|
|
|
|
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,
|
|
})
|
|
return user, sid
|
|
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,
|
|
})
|
|
|
|
return user, sid
|
|
end
|
|
|
|
def subscribe_ajax(channel_id, action, env_headers)
|
|
headers = HTTP::Headers.new
|
|
headers["Cookie"] = env_headers["Cookie"]
|
|
|
|
html = YT_POOL.client &.get("/subscription_manager?disable_polymer=1", headers)
|
|
|
|
cookies = HTTP::Cookies.from_client_headers(headers)
|
|
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)
|
|
|
|
if match = html.body.match(/'XSRF_TOKEN': "(?<session_token>[^"]+)"/)
|
|
session_token = match["session_token"]
|
|
|
|
headers["content-type"] = "application/x-www-form-urlencoded"
|
|
|
|
post_req = {
|
|
session_token: session_token,
|
|
}
|
|
post_url = "/subscription_ajax?#{action}=1&c=#{channel_id}"
|
|
|
|
YT_POOL.client &.post(post_url, headers, form: post_req)
|
|
end
|
|
end
|
|
|
|
def get_subscription_feed(user, max_results = 40, page = 1)
|
|
limit = max_results.clamp(0, MAX_ITEMS_PER_PAGE)
|
|
offset = (page - 1) * limit
|
|
|
|
notifications = Invidious::Database::Users.select_notifications(user)
|
|
view_name = "subscriptions_#{sha256(user.email)}"
|
|
|
|
if user.preferences.notifications_only && !notifications.empty?
|
|
# Only show notifications
|
|
notifications = Invidious::Database::ChannelVideos.select(notifications)
|
|
videos = [] of ChannelVideo
|
|
|
|
notifications.sort_by!(&.published).reverse!
|
|
|
|
case user.preferences.sort
|
|
when "alphabetically"
|
|
notifications.sort_by!(&.title)
|
|
when "alphabetically - reverse"
|
|
notifications.sort_by!(&.title).reverse!
|
|
when "channel name"
|
|
notifications.sort_by!(&.author)
|
|
when "channel name - reverse"
|
|
notifications.sort_by!(&.author).reverse!
|
|
else nil # Ignore
|
|
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
|
|
videos = PG_DB.query_all("SELECT DISTINCT ON (ucid) * FROM #{view_name} WHERE NOT id = ANY (#{values}) ORDER BY ucid, published DESC", as: ChannelVideo)
|
|
else
|
|
# Show latest video from each channel
|
|
|
|
videos = PG_DB.query_all("SELECT DISTINCT ON (ucid) * FROM #{view_name} ORDER BY ucid, published DESC", as: ChannelVideo)
|
|
end
|
|
|
|
videos.sort_by!(&.published).reverse!
|
|
else
|
|
if user.preferences.unseen_only
|
|
# Only show unwatched
|
|
|
|
if user.watched.empty?
|
|
values = "'{}'"
|
|
else
|
|
values = "VALUES #{user.watched.map { |id| %(('#{id}')) }.join(",")}"
|
|
end
|
|
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)
|
|
else
|
|
# Sort subscriptions as normal
|
|
|
|
videos = PG_DB.query_all("SELECT * FROM #{view_name} ORDER BY published DESC LIMIT $1 OFFSET $2", limit, offset, as: ChannelVideo)
|
|
end
|
|
end
|
|
|
|
case user.preferences.sort
|
|
when "published - reverse"
|
|
videos.sort_by!(&.published)
|
|
when "alphabetically"
|
|
videos.sort_by!(&.title)
|
|
when "alphabetically - reverse"
|
|
videos.sort_by!(&.title).reverse!
|
|
when "channel name"
|
|
videos.sort_by!(&.author)
|
|
when "channel name - reverse"
|
|
videos.sort_by!(&.author).reverse!
|
|
else nil # Ignore
|
|
end
|
|
|
|
notifications = Invidious::Database::Users.select_notifications(user)
|
|
notifications = videos.select { |v| notifications.includes? v.id }
|
|
videos = videos - notifications
|
|
end
|
|
|
|
return videos, notifications
|
|
end
|