diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/invidious.cr | 184 | ||||
| -rw-r--r-- | src/invidious/helpers.cr | 30 | ||||
| -rw-r--r-- | src/invidious/views/preferences.ecr | 14 |
3 files changed, 186 insertions, 42 deletions
diff --git a/src/invidious.cr b/src/invidious.cr index 89b2702e..fa7c0b21 100644 --- a/src/invidious.cr +++ b/src/invidious.cr @@ -271,20 +271,24 @@ get "/" do |env| end get "/watch" do |env| + if env.params.query["v"]? + id = env.params.query["v"] + else + next env.redirect "/" + end + user = env.get? "user" if user user = user.as(User) + if user.watched != ["N/A"] && !user.watched.includes? id + PG_DB.exec("UPDATE users SET watched = watched || $1 WHERE id = $2", [id], user.id) + end + preferences = user.preferences subscriptions = user.subscriptions.as(Array(String)) end subscriptions ||= [] of String - if env.params.query["v"]? - id = env.params.query["v"] - else - next env.redirect "/" - end - autoplay = env.params.query["autoplay"]?.try &.to_i video_loop = env.params.query["video_loop"]?.try &.to_i @@ -324,6 +328,7 @@ get "/watch" do |env| video = get_video(id, client, PG_DB) rescue ex error_message = ex.message + env.response.status_code = 500 next templated "error" end @@ -499,7 +504,9 @@ get "/api/v1/captions/:id" do |env| track_xml.xpath_nodes("//transcript/text").each do |node| start_time = node["start"].to_f.seconds - end_time = start_time + node["dur"].to_f.seconds + duration = node["dur"]?.try &.to_f.seconds + duration ||= start_time + end_time = start_time + duration start_time = "#{start_time.hours.to_s.rjust(2, '0')}:#{start_time.minutes.to_s.rjust(2, '0')}:#{start_time.seconds.to_s.rjust(2, '0')}.#{start_time.milliseconds.to_s.rjust(3, '0')}" end_time = "#{end_time.hours.to_s.rjust(2, '0')}:#{end_time.minutes.to_s.rjust(2, '0')}:#{end_time.seconds.to_s.rjust(2, '0')}.#{end_time.milliseconds.to_s.rjust(3, '0')}" @@ -569,18 +576,31 @@ get "/api/v1/comments/:id" do |env| response = client.post("/comment_service_ajax?action_get_comments=1&pbj=1&ctoken=#{ctoken}&continuation=#{continuation}&itct=#{itct}", headers, post_req).body response = JSON.parse(response) + env.response.content_type = "application/json" + + if !response["response"]["continuationContents"]? + halt env, status_code: 401 + end + response = response["response"]["continuationContents"] if response["commentRepliesContinuation"]? body = response["commentRepliesContinuation"] else body = response["itemSectionContinuation"] end - contents = body["contents"] + contents = body["contents"]? + if !contents + if format == "json" + next {"comments" => [] of String}.to_json + else + next {"content_html" => ""}.to_json + end + end comments = JSON.build do |json| json.object do if body["header"]? - comment_count = body["header"]["commentsHeaderRenderer"]["countText"]["simpleText"].as_s.rchop(" Comments").delete(',').to_i + comment_count = body["header"]["commentsHeaderRenderer"]["countText"]["simpleText"].as_s.delete("Comments,").to_i json.field "commentCount", comment_count end @@ -626,7 +646,8 @@ get "/api/v1/comments/:id" do |env| json.field "commentId", item_comment["commentId"] if item_replies && !response["commentRepliesContinuation"]? - reply_count = item_replies["moreText"]["simpleText"].as_s.match(/View all (?<count>\d+) replies/).try &.["count"].to_i + reply_count = item_replies["moreText"]["simpleText"].as_s.match(/View all (?<count>\d+) replies/) + .try &.["count"].to_i reply_count ||= 1 continuation = item_replies["continuations"].as_a[0]["nextContinuationData"]["continuation"].as_s @@ -650,7 +671,6 @@ get "/api/v1/comments/:id" do |env| end end - env.response.content_type = "application/json" if format == "json" next comments else @@ -677,7 +697,6 @@ get "/api/v1/comments/:id" do |env| halt env, status_code: 404 end - env.response.content_type = "application/json" {"title" => reddit_thread.title, "permalink" => reddit_thread.permalink, "content_html" => content_html}.to_json @@ -1049,7 +1068,8 @@ get "/api/v1/channels/:ucid" do |env| total_views = total_views.content.rchop(" views").lchop(" • ").delete(",").to_i64 joined = Time.parse(joined.content.lchop("Joined "), "%b %-d, %Y", Time::Location.local) - latest_videos = PG_DB.query_all("SELECT * FROM channel_videos WHERE ucid = $1 ORDER BY published DESC LIMIT 15", channel.id, as: ChannelVideo) + latest_videos = PG_DB.query_all("SELECT * FROM channel_videos WHERE ucid = $1 ORDER BY published DESC LIMIT 15", + channel.id, as: ChannelVideo) channel_info = JSON.build do |json| json.object do @@ -1139,7 +1159,7 @@ end get "/api/v1/channels/:ucid/videos" do |env| ucid = env.params.url["ucid"] - page = env.params.query["page"]? + page = env.params.query["page"]?.try &.to_i? page ||= 1 url = produce_videos_url(ucid, page) @@ -1147,9 +1167,15 @@ get "/api/v1/channels/:ucid/videos" do |env| response = client.get(url) json = JSON.parse(response.body) + if !json["content_html"]? + env.response.content_type = "application/json" + next {"error" => "No videos or nonexistent channel"}.to_json + end + content_html = json["content_html"].as_s if content_html.empty? - halt env, status_code: 403 + env.response.content_type = "application/json" + next Hash(String, String).new.to_json end document = XML.parse_html(content_html) @@ -1324,6 +1350,15 @@ get "/embed/:id" do |env| rendered "embed" end +get "/results" do |env| + search_query = env.params.query["search_query"]? + if search_query + env.redirect "/search?q=#{URI.escape(search_query)}" + else + env.redirect "/" + end +end + get "/search" do |env| if env.params.query["q"]? query = env.params.query["q"] @@ -1331,7 +1366,7 @@ get "/search" do |env| next env.redirect "/" end - page = env.params.query["page"]?.try &.to_i + page = env.params.query["page"]?.try &.to_i? page ||= 1 client = make_client(YT_URL) @@ -1620,7 +1655,8 @@ post "/login" do |env| secure = false end - env.response.cookies["SID"] = HTTP::Cookie.new(name: "SID", value: sid, expires: Time.now + 2.years, secure: secure, http_only: true) + env.response.cookies["SID"] = HTTP::Cookie.new(name: "SID", value: sid, expires: Time.now + 2.years, + secure: secure, http_only: true) else error_message = "Invalid username or password" next templated "error" @@ -1634,8 +1670,12 @@ post "/login" do |env| sid = Base64.encode(Random::Secure.random_bytes(50)) user = create_user(sid, email, password) + if user.watched = ["N/A"] + user_array = user.to_a[0..-2] + else + user_array = user.to_a + end - user_array = user.to_a user_array[5] = user_array[5].to_json args = arg_array(user_array) @@ -1647,7 +1687,8 @@ post "/login" do |env| secure = false end - env.response.cookies["SID"] = HTTP::Cookie.new(name: "SID", value: sid, expires: Time.now + 2.years, secure: secure, http_only: true) + env.response.cookies["SID"] = HTTP::Cookie.new(name: "SID", value: sid, expires: Time.now + 2.years, + secure: secure, http_only: true) end env.redirect referer @@ -1735,6 +1776,10 @@ post "/preferences" do |env| latest_only ||= "off" latest_only = latest_only == "on" + unseen_only = env.params.body["unseen_only"]?.try &.as(String) + unseen_only ||= "off" + unseen_only = unseen_only == "on" + preferences = { "video_loop" => video_loop, "autoplay" => autoplay, @@ -1748,6 +1793,7 @@ post "/preferences" do |env| "max_results" => max_results, "sort" => sort, "latest_only" => latest_only, + "unseen_only" => unseen_only, }.to_json PG_DB.exec("UPDATE users SET preferences = $1 WHERE email = $2", preferences, user.email) @@ -1777,7 +1823,7 @@ get "/feed/subscriptions" do |env| max_results ||= env.params.query["max_results"]?.try &.to_i max_results ||= 40 - page = env.params.query["page"]?.try &.to_i + page = env.params.query["page"]?.try &.to_i? page ||= 1 if max_results < 0 @@ -1789,14 +1835,41 @@ get "/feed/subscriptions" do |env| end if preferences.latest_only - args = arg_array(user.subscriptions) - videos = PG_DB.query_all("SELECT DISTINCT ON (ucid) * FROM channel_videos WHERE \ - ucid IN (#{args}) ORDER BY ucid, published DESC", user.subscriptions, as: ChannelVideo) + if preferences.unseen_only + ucids = arg_array(user.subscriptions) + if user.watched.empty? + watched = "'{}'" + else + watched = arg_array(user.watched, user.subscriptions.size + 1) + end + + videos = PG_DB.query_all("SELECT DISTINCT ON (ucid) * FROM channel_videos WHERE \ + ucid IN (#{ucids}) AND id NOT IN (#{watched}) ORDER BY ucid, published DESC", + user.subscriptions + user.watched, as: ChannelVideo) + else + args = arg_array(user.subscriptions) + videos = PG_DB.query_all("SELECT DISTINCT ON (ucid) * FROM channel_videos WHERE \ + ucid IN (#{args}) ORDER BY ucid, published DESC", user.subscriptions, as: ChannelVideo) + end + videos.sort_by! { |video| video.published }.reverse! else - args = arg_array(user.subscriptions, 3) - videos = PG_DB.query_all("SELECT * FROM channel_videos WHERE ucid IN (#{args}) \ - ORDER BY published DESC LIMIT $1 OFFSET $2", [limit, offset] + user.subscriptions, as: ChannelVideo) + if preferences.unseen_only + ucids = arg_array(user.subscriptions, 3) + if user.watched.empty? + watched = "'{}'" + else + watched = arg_array(user.watched, user.subscriptions.size + 3) + end + + videos = PG_DB.query_all("SELECT * FROM channel_videos WHERE ucid IN (#{ucids}) \ + AND id NOT IN (#{watched}) ORDER BY published DESC LIMIT $1 OFFSET $2", + [limit, offset] + user.subscriptions + user.watched, as: ChannelVideo) + else + args = arg_array(user.subscriptions, 3) + videos = PG_DB.query_all("SELECT * FROM channel_videos WHERE ucid IN (#{args}) \ + ORDER BY published DESC LIMIT $1 OFFSET $2", [limit, offset] + user.subscriptions, as: ChannelVideo) + end end case preferences.sort @@ -1811,7 +1884,8 @@ get "/feed/subscriptions" do |env| end # TODO: Add option to disable picking out notifications from regular feed - notifications = PG_DB.query_one("SELECT notifications FROM users WHERE email = $1", user.email, as: Array(String)) + notifications = PG_DB.query_one("SELECT notifications FROM users WHERE email = $1", user.email, + as: Array(String)) notifications = videos.select { |v| notifications.includes? v.id } videos = videos - notifications @@ -1820,7 +1894,8 @@ get "/feed/subscriptions" do |env| videos = videos[0..max_results] end - PG_DB.exec("UPDATE users SET notifications = $1, updated = $2 WHERE id = $3", [] of String, Time.now, user.id) + PG_DB.exec("UPDATE users SET notifications = $1, updated = $2 WHERE id = $3", [] of String, Time.now, + user.id) user.notifications = [] of String env.set "user", user @@ -1840,6 +1915,11 @@ get "/feed/channel/:ucid" do |env| channel = get_channel(ucid, client, PG_DB, pull_all_videos: false) json = JSON.parse(response.body) + if !json["content_html"]? + error_message = "This channel does not exist or has no videos." + next templated "error" + end + content_html = json["content_html"].as_s if content_html.empty? halt env, status_code: 403 @@ -1910,7 +1990,8 @@ get "/feed/channel/:ucid" do |env| xml.element("media:group") do xml.element("media:title") { xml.text title } - xml.element("media:thumbnail", url: "https://i.ytimg.com/vi/#{video_id}/hqdefault.jpg", width: "480", height: "360") + xml.element("media:thumbnail", url: "https://i.ytimg.com/vi/#{video_id}/hqdefault.jpg", + width: "480", height: "360") xml.element("media:description") { xml.text description } end @@ -1941,7 +2022,7 @@ get "/feed/private" do |env| max_results = env.params.query["max_results"]?.try &.to_i max_results ||= 40 - page = env.params.query["page"]?.try &.to_i + page = env.params.query["page"]?.try &.to_i? page ||= 1 if max_results < 0 @@ -1996,7 +2077,8 @@ get "/feed/private" do |env| query = env.request.query.not_nil! feed = XML.build(indent: " ", encoding: "UTF-8") do |xml| - xml.element("feed", xmlns: "http://www.w3.org/2005/Atom", "xmlns:media": "http://search.yahoo.com/mrss/", "xml:lang": "en-US") do + xml.element("feed", xmlns: "http://www.w3.org/2005/Atom", "xmlns:media": "http://search.yahoo.com/mrss/", + "xml:lang": "en-US") do xml.element("link", "type": "text/html", rel: "alternate", href: "#{scheme}#{host}/feed/subscriptions") xml.element("link", "type": "application/atom+xml", rel: "self", href: "#{scheme}#{host}#{path}?#{query}") xml.element("title") { xml.text "Invidious Private Feed for #{user.email}" } @@ -2019,7 +2101,8 @@ get "/feed/private" do |env| xml.element("media:group") do xml.element("media:title") { xml.text video.title } - xml.element("media:thumbnail", url: "https://i.ytimg.com/vi/#{video.id}/hqdefault.jpg", width: "480", height: "360") + xml.element("media:thumbnail", url: "https://i.ytimg.com/vi/#{video.id}/hqdefault.jpg", + width: "480", height: "360") end end end @@ -2076,7 +2159,8 @@ get "/modify_notifications" do |env| channel_req["channel_id"] = channel_id - client.post("/subscription_ajax?action_update_subscription_preferences=1", headers, HTTP::Params.encode(channel_req)).body + client.post("/subscription_ajax?action_update_subscription_preferences=1", headers, + HTTP::Params.encode(channel_req)).body end end @@ -2183,6 +2267,20 @@ get "/subscription_ajax" do |env| env.redirect referer end +get "/clear_watch_history" do |env| + user = env.get? "user" + referer = env.request.headers["referer"]? + referer ||= "/" + + if user + user = user.as(User) + + PG_DB.exec("UPDATE users SET watched = '{}' WHERE id = $1", user.id) + end + + env.redirect referer +end + get "/user/:user" do |env| user = env.params.url["user"] env.redirect "/channel/#{user}" @@ -2198,7 +2296,7 @@ get "/channel/:ucid" do |env| ucid = env.params.url["ucid"] - page = env.params.query["page"]?.try &.to_i + page = env.params.query["page"]?.try &.to_i? page ||= 1 client = make_client(YT_URL) @@ -2215,6 +2313,21 @@ get "/channel/:ucid" do |env| response = client.get(url) json = JSON.parse(response.body) + if !json["content_html"]? + error_message = "This channel does not exist or has no videos." + next templated "error" + end + + if json["content_html"].as_s.strip(" \n").empty? + rss = client.get("/feeds/videos.xml?channel_id=#{ucid}").body + rss = XML.parse_html(rss) + author = rss.xpath_node("//feed/author/name").not_nil!.content + + videos = [] of ChannelVideo + + next templated "channel" + end + document = XML.parse_html(json["content_html"].as_s) author = document.xpath_node(%q(//div[@class="pl-video-owner"]/a)).not_nil!.content @@ -2328,7 +2441,8 @@ get "/api/manifest/dash/id/:id" do |env| url = url.gsub("=", "/") xml.element("Representation", id: fmt["itag"], codecs: codecs, bandwidth: bandwidth) do - xml.element("AudioChannelConfiguration", schemeIdUri: "urn:mpeg:dash:23003:3:audio_channel_configuration:2011", value: "2") + xml.element("AudioChannelConfiguration", schemeIdUri: "urn:mpeg:dash:23003:3:audio_channel_configuration:2011", + value: "2") xml.element("BaseURL") { xml.text url } xml.element("SegmentBase", indexRange: fmt["init"]) do xml.element("Initialization", range: fmt["index"]) diff --git a/src/invidious/helpers.cr b/src/invidious/helpers.cr index 9c949bf5..26a1aaa2 100644 --- a/src/invidious/helpers.cr +++ b/src/invidious/helpers.cr @@ -29,6 +29,7 @@ DEFAULT_USER_PREFERENCES = Preferences.from_json({ "max_results" => 40, "sort" => "published", "latest_only" => false, + "unseen_only" => false, }.to_json) class Config @@ -147,6 +148,10 @@ class User }, password: String?, token: String, + watched: { + type: Array(String), + default: ["N/A"], + }, }) end @@ -177,6 +182,7 @@ class Preferences max_results: Int32, sort: String, latest_only: Bool, + unseen_only: Bool, }) end @@ -299,7 +305,8 @@ def fetch_video(id, client) is_family_friendly = html.xpath_node(%q(//meta[@itemprop="isFamilyFriendly"])).not_nil!["content"] == "True" genre = html.xpath_node(%q(//meta[@itemprop="genre"])).not_nil!["content"] - video = Video.new(id, info, Time.now, title, views, likes, dislikes, wilson_score, published, description, nil, author, ucid, allowed_regions, is_family_friendly, genre) + video = Video.new(id, info, Time.now, title, views, likes, dislikes, wilson_score, published, description, + nil, author, ucid, allowed_regions, is_family_friendly, genre) return video end @@ -818,7 +825,12 @@ def get_user(sid, client, headers, db, refresh = true) if refresh && Time.now - user.updated > 1.minute user = fetch_user(sid, client, headers, db) - user_array = user.to_a + if user.watched = ["N/A"] + user_array = user.to_a[0..-2] + else + user_array = user.to_a + end + user_array[5] = user_array[5].to_json args = arg_array(user_array) @@ -827,7 +839,12 @@ def get_user(sid, client, headers, db, refresh = true) end else user = fetch_user(sid, client, headers, db) - user_array = user.to_a + if user.watched = ["N/A"] + user_array = user.to_a[0..-2] + else + user_array = user.to_a + end + user_array[5] = user_array[5].to_json args = arg_array(user.to_a) @@ -865,7 +882,7 @@ def fetch_user(sid, client, headers, db) token = Base64.encode(Random::Secure.random_bytes(32)) - user = User.new(sid, Time.now, [] of String, channels, email, DEFAULT_USER_PREFERENCES, nil, token) + user = User.new(sid, Time.now, [] of String, channels, email, DEFAULT_USER_PREFERENCES, nil, token, [] of String) return user end @@ -873,7 +890,7 @@ def create_user(sid, email, password) password = Crypto::Bcrypt::Password.create(password, cost: 10) token = Base64.encode(Random::Secure.random_bytes(32)) - user = User.new(sid, Time.now, [] of String, [] of String, email, DEFAULT_USER_PREFERENCES, password.to_s, token) + user = User.new(sid, Time.now, [] of String, [] of String, email, DEFAULT_USER_PREFERENCES, password.to_s, token, [] of String) return user end @@ -1066,7 +1083,8 @@ def generate_captcha(key) END_SVG challenge = "" - convert = Process.run(%(convert -density 1200 -resize 400x400 -background none svg:- png:-), shell: true, input: IO::Memory.new(clock_svg), output: Process::Redirect::Pipe) do |proc| + convert = Process.run(%(convert -density 1200 -resize 400x400 -background none svg:- png:-), shell: true, + input: IO::Memory.new(clock_svg), output: Process::Redirect::Pipe) do |proc| challenge = proc.output.gets_to_end challenge = Base64.strict_encode(challenge) challenge = "data:image/png;base64,#{challenge}" diff --git a/src/invidious/views/preferences.ecr b/src/invidious/views/preferences.ecr index 7f675bcc..70f4d73a 100644 --- a/src/invidious/views/preferences.ecr +++ b/src/invidious/views/preferences.ecr @@ -88,10 +88,22 @@ function update_value(element) { </div> <div class="pure-control-group"> - <label for="latest_only">Only show latest video from channel: </label> + <label for="latest_only">Only show latest <% if user.preferences.unseen_only %>unseen<% end %> video from channel: </label> <input name="latest_only" id="latest_only" type="checkbox" <% if user.preferences.latest_only %>checked<% end %>> </div> + <div class="pure-control-group"> + <label for="unseen_only">Only show unseen: </label> + <input name="unseen_only" id="unseen_only" type="checkbox" <% if user.preferences.unseen_only %>checked<% end %>> + </div> + + <legend>Data preferences</legend> + <div class="pure-control-group"> + <label> + <a href="/clear_watch_history">Clear watch history</a> + </labe> + </div> + <div class="pure-controls"> <button type="submit" class="pure-button pure-button-primary">Save preferences</button> </div> |
