diff options
Diffstat (limited to 'src')
47 files changed, 673 insertions, 1055 deletions
diff --git a/src/invidious.cr b/src/invidious.cr index 27c4775e..84e1895d 100644 --- a/src/invidious.cr +++ b/src/invidious.cr @@ -57,13 +57,11 @@ end # Simple alias to make code easier to read alias IV = Invidious -CONFIG = Config.load -HMAC_KEY_CONFIGURED = CONFIG.hmac_key != nil -HMAC_KEY = CONFIG.hmac_key || Random::Secure.hex(32) +CONFIG = Config.load +HMAC_KEY = CONFIG.hmac_key PG_DB = DB.open CONFIG.database_url ARCHIVE_URL = URI.parse("https://archive.org") -LOGIN_URL = URI.parse("https://accounts.google.com") PUBSUB_URL = URI.parse("https://pubsubhubbub.appspot.com") REDDIT_URL = URI.parse("https://www.reddit.com") YT_URL = URI.parse("https://www.youtube.com") @@ -231,10 +229,6 @@ Kemal.config.host_binding = Kemal.config.host_binding != "0.0.0.0" ? Kemal.confi Kemal.config.port = Kemal.config.port != 3000 ? Kemal.config.port : CONFIG.port Kemal.config.app_name = "Invidious" -if !HMAC_KEY_CONFIGURED - LOGGER.warn("Please configure hmac_key by July 1st, see more here: https://github.com/iv-org/invidious/issues/3854") -end - # Use in kemal's production mode. # Users can also set the KEMAL_ENV environmental variable for this to be set automatically. {% if flag?(:release) || flag?(:production) %} diff --git a/src/invidious/channels/playlists.cr b/src/invidious/channels/playlists.cr index 8dc824b2..91029fe3 100644 --- a/src/invidious/channels/playlists.cr +++ b/src/invidious/channels/playlists.cr @@ -26,3 +26,21 @@ def fetch_channel_playlists(ucid, author, continuation, sort_by) return extract_items(initial_data, author, ucid) end + +def fetch_channel_podcasts(ucid, author, continuation) + if continuation + initial_data = YoutubeAPI.browse(continuation) + else + initial_data = YoutubeAPI.browse(ucid, params: "Eghwb2RjYXN0c_IGBQoDugEA") + end + return extract_items(initial_data, author, ucid) +end + +def fetch_channel_releases(ucid, author, continuation) + if continuation + initial_data = YoutubeAPI.browse(continuation) + else + initial_data = YoutubeAPI.browse(ucid, params: "EghyZWxlYXNlc_IGBQoDsgEA") + end + return extract_items(initial_data, author, ucid) +end diff --git a/src/invidious/channels/videos.cr b/src/invidious/channels/videos.cr index 12ed4a7d..beb86e08 100644 --- a/src/invidious/channels/videos.cr +++ b/src/invidious/channels/videos.cr @@ -20,7 +20,7 @@ def produce_channel_videos_continuation(ucid, page = 1, auto_generated = nil, so case sort_by when "newest" then 1_i64 when "popular" then 2_i64 - when "oldest" then 3_i64 # Broken as of 10/2022 :c + when "oldest" then 4_i64 else 1_i64 # Fallback to "newest" end diff --git a/src/invidious/config.cr b/src/invidious/config.cr index 9fc58409..e5f1e822 100644 --- a/src/invidious/config.cr +++ b/src/invidious/config.cr @@ -85,7 +85,7 @@ class Config # Used to tell Invidious it is behind a proxy, so links to resources should be https:// property https_only : Bool? # HMAC signing key for CSRF tokens and verifying pubsub subscriptions - property hmac_key : String? + property hmac_key : String = "" # Domain to be used for links to resources on the site where an absolute URL is required property domain : String? # Subscribe to channels using PubSubHubbub (requires domain, hmac_key) @@ -204,6 +204,16 @@ class Config end {% end %} + # HMAC_key is mandatory + # See: https://github.com/iv-org/invidious/issues/3854 + if config.hmac_key.empty? + puts "Config: 'hmac_key' is required/can't be empty" + exit(1) + elsif config.hmac_key == "CHANGE_ME!!" + puts "Config: The value of 'hmac_key' needs to be changed!!" + exit(1) + end + # Build database_url from db.* if it's not set directly if config.database_url.to_s.empty? if db = config.db @@ -216,7 +226,7 @@ class Config path: db.dbname, ) else - puts "Config : Either database_url or db.* is required" + puts "Config: Either database_url or db.* is required" exit(1) end end diff --git a/src/invidious/frontend/channel_page.cr b/src/invidious/frontend/channel_page.cr index 53745dd5..fe7d6d6e 100644 --- a/src/invidious/frontend/channel_page.cr +++ b/src/invidious/frontend/channel_page.cr @@ -5,6 +5,8 @@ module Invidious::Frontend::ChannelPage Videos Shorts Streams + Podcasts + Releases Playlists Community Channels diff --git a/src/invidious/frontend/pagination.cr b/src/invidious/frontend/pagination.cr new file mode 100644 index 00000000..3f931f4e --- /dev/null +++ b/src/invidious/frontend/pagination.cr @@ -0,0 +1,97 @@ +require "uri" + +module Invidious::Frontend::Pagination + extend self + + private def previous_page(str : String::Builder, locale : String?, url : String) + # Link + str << %(<a href=") << url << %(" class="pure-button pure-button-secondary">) + + if locale_is_rtl?(locale) + # Inverted arrow ("previous" points to the right) + str << translate(locale, "Previous page") + str << " " + str << %(<i class="icon ion-ios-arrow-forward"></i>) + else + # Regular arrow ("previous" points to the left) + str << %(<i class="icon ion-ios-arrow-back"></i>) + str << " " + str << translate(locale, "Previous page") + end + + str << "</a>" + end + + private def next_page(str : String::Builder, locale : String?, url : String) + # Link + str << %(<a href=") << url << %(" class="pure-button pure-button-secondary">) + + if locale_is_rtl?(locale) + # Inverted arrow ("next" points to the left) + str << %(<i class="icon ion-ios-arrow-back"></i>) + str << " " + str << translate(locale, "Next page") + else + # Regular arrow ("next" points to the right) + str << translate(locale, "Next page") + str << " " + str << %(<i class="icon ion-ios-arrow-forward"></i>) + end + + str << "</a>" + end + + def nav_numeric(locale : String?, *, base_url : String | URI, current_page : Int, show_next : Bool = true) + return String.build do |str| + str << %(<div class="h-box">\n) + str << %(<div class="page-nav-container flexible">\n) + + str << %(<div class="page-prev-container flex-left">) + + if current_page > 1 + params_prev = URI::Params{"page" => (current_page - 1).to_s} + url_prev = HttpServer::Utils.add_params_to_url(base_url, params_prev) + + self.previous_page(str, locale, url_prev.to_s) + end + + str << %(</div>\n) + str << %(<div class="page-next-container flex-right">) + + if show_next + params_next = URI::Params{"page" => (current_page + 1).to_s} + url_next = HttpServer::Utils.add_params_to_url(base_url, params_next) + + self.next_page(str, locale, url_next.to_s) + end + + str << %(</div>\n) + + str << %(</div>\n) + str << %(</div>\n\n) + end + end + + def nav_ctoken(locale : String?, *, base_url : String | URI, ctoken : String?) + return String.build do |str| + str << %(<div class="h-box">\n) + str << %(<div class="page-nav-container flexible">\n) + + str << %(<div class="page-prev-container flex-left"></div>\n) + + str << %(<div class="page-next-container flex-right">) + + if !ctoken.nil? + params_next = URI::Params{"continuation" => ctoken} + url_next = HttpServer::Utils.add_params_to_url(base_url, params_next) + + self.next_page(str, locale, url_next.to_s) + end + + str << %(</div>\n) + + str << %(</div>\n) + str << %(</div>\n\n) + end + end +end diff --git a/src/invidious/helpers/helpers.cr b/src/invidious/helpers/helpers.cr index c3b53339..23ff0da9 100644 --- a/src/invidious/helpers/helpers.cr +++ b/src/invidious/helpers/helpers.cr @@ -22,31 +22,6 @@ struct Annotation property annotations : String end -def login_req(f_req) - data = { - # Unfortunately there's not much information available on `bgRequest`; part of Google's BotGuard - # Generally this is much longer (>1250 characters), see also - # https://github.com/ytdl-org/youtube-dl/commit/baf67a604d912722b0fe03a40e9dc5349a2208cb . - # For now this can be empty. - "bgRequest" => %|["identifier",""]|, - "pstMsg" => "1", - "checkConnection" => "youtube", - "checkedDomains" => "youtube", - "hl" => "en", - "deviceinfo" => %|[null,null,null,[],null,"US",null,null,[],"GlifWebSignIn",null,[null,null,[]]]|, - "f.req" => f_req, - "flowName" => "GlifWebSignIn", - "flowEntry" => "ServiceLogin", - # "cookiesDisabled" => "false", - # "gmscoreversion" => "undefined", - # "continue" => "https://accounts.google.com/ManageAccount", - # "azt" => "", - # "bgHash" => "", - } - - return HTTP::Params.encode(data) -end - def html_to_content(description_html : String) description = description_html.gsub(/(<br>)|(<br\/>)/, { "<br>": "\n", diff --git a/src/invidious/helpers/i18n.cr b/src/invidious/helpers/i18n.cr index a9ed1f64..76e477a4 100644 --- a/src/invidious/helpers/i18n.cr +++ b/src/invidious/helpers/i18n.cr @@ -165,3 +165,12 @@ def translate_bool(locale : String?, translation : Bool) return translate(locale, "No") end end + +def locale_is_rtl?(locale : String?) + # Fallback to en-US + return false if locale.nil? + + # Arabic, Persian, Hebrew + # See https://en.wikipedia.org/wiki/Right-to-left_script#List_of_RTL_scripts + return {"ar", "fa", "he"}.includes? locale +end diff --git a/src/invidious/helpers/utils.cr b/src/invidious/helpers/utils.cr index 48bf769f..a006d602 100644 --- a/src/invidious/helpers/utils.cr +++ b/src/invidious/helpers/utils.cr @@ -440,7 +440,7 @@ def parse_link_endpoint(endpoint : JSON::Any, text : String, video_id : String) # - https://github.com/iv-org/invidious/issues/3062 text = %(<a href="#{url}">#{text}</a>) else - text = %(<a href="#{url}">#{reduce_uri(url)}</a>) + text = %(<a href="#{url}">#{reduce_uri(text)}</a>) end end return text diff --git a/src/invidious/http_server/utils.cr b/src/invidious/http_server/utils.cr index e3f1fa0f..222dfc4a 100644 --- a/src/invidious/http_server/utils.cr +++ b/src/invidious/http_server/utils.cr @@ -1,3 +1,5 @@ +require "uri" + module Invidious::HttpServer module Utils extend self @@ -16,5 +18,23 @@ module Invidious::HttpServer return "#{url.request_target}?#{params}" end end + + def add_params_to_url(url : String | URI, params : URI::Params) : URI + url = URI.parse(url) if url.is_a?(String) + + url_query = url.query || "" + + # Append the parameters + url.query = String.build do |str| + if !url_query.empty? + str << url_query + str << '&' + end + + str << params + end + + return url + end end end diff --git a/src/invidious/jobs.cr b/src/invidious/jobs.cr index 524a3624..b6b673f7 100644 --- a/src/invidious/jobs.cr +++ b/src/invidious/jobs.cr @@ -2,7 +2,7 @@ module Invidious::Jobs JOBS = [] of BaseJob # Automatically generate a structure that wraps the various - # jobs' configs, so that the follwing YAML config can be used: + # jobs' configs, so that the following YAML config can be used: # # jobs: # job_name: diff --git a/src/invidious/routes/account.cr b/src/invidious/routes/account.cr index 5aa4452c..9d930841 100644 --- a/src/invidious/routes/account.cr +++ b/src/invidious/routes/account.cr @@ -42,11 +42,6 @@ module Invidious::Routes::Account sid = sid.as(String) token = env.params.body["csrf_token"]? - # We don't store passwords for Google accounts - if !user.password - return error_template(400, "Cannot change password for Google accounts") - end - begin validate_request(token, sid, env.request, HMAC_KEY, locale) rescue ex @@ -54,7 +49,7 @@ module Invidious::Routes::Account end password = env.params.body["password"]? - if !password + if password.nil? || password.empty? return error_template(401, "Password is a required field") end diff --git a/src/invidious/routes/api/v1/authenticated.cr b/src/invidious/routes/api/v1/authenticated.cr index ce2ee812..a35d2f2b 100644 --- a/src/invidious/routes/api/v1/authenticated.cr +++ b/src/invidious/routes/api/v1/authenticated.cr @@ -178,10 +178,6 @@ module Invidious::Routes::API::V1::Authenticated Invidious::Database::Users.subscribe_channel(user, ucid) end - # For Google accounts, access tokens don't have enough information to - # make a request on the user's behalf, which is why we don't sync with - # YouTube. - env.response.status_code = 204 end diff --git a/src/invidious/routes/api/v1/channels.cr b/src/invidious/routes/api/v1/channels.cr index bcb4db2c..adf05d30 100644 --- a/src/invidious/routes/api/v1/channels.cr +++ b/src/invidious/routes/api/v1/channels.cr @@ -245,7 +245,7 @@ module Invidious::Routes::API::V1::Channels channel = nil # Make the compiler happy get_channel() - items, continuation = fetch_channel_playlists(channel.ucid, channel.author, continuation, sort_by) + items, next_continuation = fetch_channel_playlists(channel.ucid, channel.author, continuation, sort_by) JSON.build do |json| json.object do @@ -257,7 +257,65 @@ module Invidious::Routes::API::V1::Channels end end - json.field "continuation", continuation + json.field "continuation", next_continuation if next_continuation + end + end + end + + def self.podcasts(env) + locale = env.get("preferences").as(Preferences).locale + + env.response.content_type = "application/json" + + ucid = env.params.url["ucid"] + continuation = env.params.query["continuation"]? + + # Use the macro defined above + channel = nil # Make the compiler happy + get_channel() + + items, next_continuation = fetch_channel_podcasts(channel.ucid, channel.author, continuation) + + JSON.build do |json| + json.object do + json.field "playlists" do + json.array do + items.each do |item| + item.to_json(locale, json) if item.is_a?(SearchPlaylist) + end + end + end + + json.field "continuation", next_continuation if next_continuation + end + end + end + + def self.releases(env) + locale = env.get("preferences").as(Preferences).locale + + env.response.content_type = "application/json" + + ucid = env.params.url["ucid"] + continuation = env.params.query["continuation"]? + + # Use the macro defined above + channel = nil # Make the compiler happy + get_channel() + + items, next_continuation = fetch_channel_releases(channel.ucid, channel.author, continuation) + + JSON.build do |json| + json.object do + json.field "playlists" do + json.array do + items.each do |item| + item.to_json(locale, json) if item.is_a?(SearchPlaylist) + end + end + end + + json.field "continuation", next_continuation if next_continuation end end end diff --git a/src/invidious/routes/before_all.cr b/src/invidious/routes/before_all.cr index 8e2a253f..396840a4 100644 --- a/src/invidious/routes/before_all.cr +++ b/src/invidious/routes/before_all.cr @@ -80,49 +80,23 @@ module Invidious::Routes::BeforeAll raise "Cannot use token as SID" end - # Invidious users only have SID - if !env.request.cookies.has_key? "SSID" - if email = Invidious::Database::SessionIDs.select_email(sid) - user = Invidious::Database::Users.select!(email: email) - csrf_token = generate_response(sid, { - ":authorize_token", - ":playlist_ajax", - ":signout", - ":subscription_ajax", - ":token_ajax", - ":watch_ajax", - }, HMAC_KEY, 1.week) - - preferences = user.preferences - env.set "preferences", preferences - - env.set "sid", sid - env.set "csrf_token", csrf_token - env.set "user", user - end - else - headers = HTTP::Headers.new - headers["Cookie"] = env.request.headers["Cookie"] - - begin - user, sid = get_user(sid, headers, false) - csrf_token = generate_response(sid, { - ":authorize_token", - ":playlist_ajax", - ":signout", - ":subscription_ajax", - ":token_ajax", - ":watch_ajax", - }, HMAC_KEY, 1.week) - - preferences = user.preferences - env.set "preferences", preferences - - env.set "sid", sid - env.set "csrf_token", csrf_token - env.set "user", user - rescue ex - end + if email = Database::SessionIDs.select_email(sid) + user = Database::Users.select!(email: email) + csrf_token = generate_response(sid, { + ":authorize_token", + ":playlist_ajax", + ":signout", + ":subscription_ajax", + ":token_ajax", + ":watch_ajax", + }, HMAC_KEY, 1.week) + + preferences = user.preferences + env.set "preferences", preferences + + env.set "sid", sid + env.set "csrf_token", csrf_token + env.set "user", user end end diff --git a/src/invidious/routes/channels.cr b/src/invidious/routes/channels.cr index 16621994..9892ae2a 100644 --- a/src/invidious/routes/channels.cr +++ b/src/invidious/routes/channels.cr @@ -27,7 +27,7 @@ module Invidious::Routes::Channels item.author end end - items = items.select(SearchPlaylist).map(&.as(SearchPlaylist)) + items = items.select(SearchPlaylist) items.each(&.author = "") else sort_options = {"newest", "oldest", "popular"} @@ -105,13 +105,53 @@ module Invidious::Routes::Channels channel.ucid, channel.author, continuation, (sort_by || "last") ) - items = items.select(SearchPlaylist).map(&.as(SearchPlaylist)) + items = items.select(SearchPlaylist) items.each(&.author = "") selected_tab = Frontend::ChannelPage::TabsAvailable::Playlists templated "channel" end + def self.podcasts(env) + data = self.fetch_basic_information(env) + return data if !data.is_a?(Tuple) + + locale, user, subscriptions, continuation, ucid, channel = data + + sort_by = "" + sort_options = [] of String + + items, next_continuation = fetch_channel_podcasts( + channel.ucid, channel.author, continuation + ) + + items = items.select(SearchPlaylist) + items.each(&.author = "") + + selected_tab = Frontend::ChannelPage::TabsAvailable::Podcasts + templated "channel" + end + + def self.releases(env) + data = self.fetch_basic_information(env) + return data if !data.is_a?(Tuple) + + locale, user, subscriptions, continuation, ucid, channel = data + + sort_by = "" + sort_options = [] of String + + items, next_continuation = fetch_channel_releases( + channel.ucid, channel.author, continuation + ) + + items = items.select(SearchPlaylist) + items.each(&.author = "") + + selected_tab = Frontend::ChannelPage::TabsAvailable::Releases + templated "channel" + end + def self.community(env) data = self.fetch_basic_information(env) if !data.is_a?(Tuple) diff --git a/src/invidious/routes/feeds.cr b/src/invidious/routes/feeds.cr index fb482e33..a8246b2e 100644 --- a/src/invidious/routes/feeds.cr +++ b/src/invidious/routes/feeds.cr @@ -83,10 +83,6 @@ module Invidious::Routes::Feeds headers = HTTP::Headers.new headers["Cookie"] = env.request.headers["Cookie"] - if !user.password - user, sid = get_user(sid, headers) - end - max_results = env.params.query["max_results"]?.try &.to_i?.try &.clamp(0, MAX_ITEMS_PER_PAGE) max_results ||= user.preferences.max_results max_results ||= CONFIG.default_user_preferences.max_results @@ -106,6 +102,10 @@ module Invidious::Routes::Feeds end env.set "user", user + # Used for pagination links + base_url = "/feed/subscriptions" + base_url += "?max_results=#{max_results}" if env.params.query.has_key?("max_results") + templated "feeds/subscriptions" end @@ -133,6 +133,10 @@ module Invidious::Routes::Feeds end watched ||= [] of String + # Used for pagination links + base_url = "/feed/history" + base_url += "?max_results=#{max_results}" if env.params.query.has_key?("max_results") + templated "feeds/history" end diff --git a/src/invidious/routes/login.cr b/src/invidious/routes/login.cr index 6454131a..d0f7ac22 100644 --- a/src/invidious/routes/login.cr +++ b/src/invidious/routes/login.cr @@ -24,9 +24,6 @@ module Invidious::Routes::Login captcha_type = env.params.query["captcha"]? captcha_type ||= "image" - tfa = env.params.query["tfa"]? - prompt = nil - templated "user/login" end @@ -47,283 +44,18 @@ module Invidious::Routes::Login account_type ||= "invidious" case account_type - when "google" - tfa_code = env.params.body["tfa"]?.try &.lchop("G-") - traceback = IO::Memory.new - - # See https://github.com/ytdl-org/youtube-dl/blob/2019.04.07/youtube_dl/extractor/youtube.py#L82 - begin - client = nil # Declare variable - {% unless flag?(:disable_quic) %} - client = CONFIG.use_quic ? QUIC::Client.new(LOGIN_URL) : HTTP::Client.new(LOGIN_URL) - {% else %} - client = HTTP::Client.new(LOGIN_URL) - {% end %} - - headers = HTTP::Headers.new - - login_page = client.get("/ServiceLogin") - headers = login_page.cookies.add_request_headers(headers) - - lookup_req = { - email, nil, [] of String, nil, "US", nil, nil, 2, false, true, - {nil, nil, - {2, 1, nil, 1, - "https://accounts.google.com/ServiceLogin?passive=true&continue=https%3A%2F%2Fwww.youtube.com%2Fsignin%3Fnext%3D%252F%26action_handle_signin%3Dtrue%26hl%3Den%26app%3Ddesktop%26feature%3Dsign_in_button&hl=en&service=youtube&uilel=3&requestPath=%2FServiceLogin&Page=PasswordSeparationSignIn", - nil, [] of String, 4}, - 1, - {nil, nil, [] of String}, - nil, nil, nil, true, - }, - email, - }.to_json - - traceback << "Getting lookup..." - - headers["Content-Type"] = "application/x-www-form-urlencoded;charset=utf-8" - headers["Google-Accounts-XSRF"] = "1" - - response = client.post("/_/signin/sl/lookup", headers, login_req(lookup_req)) - lookup_results = JSON.parse(response.body[5..-1]) - - traceback << "done, returned #{response.status_code}.<br/>" - - user_hash = lookup_results[0][2] - - if token = env.params.body["token"]? - answer = env.params.body["answer"]? - captcha = {token, answer} - else - captcha = nil - end - - challenge_req = { - user_hash, nil, 1, nil, - {1, nil, nil, nil, - {password, captcha, true}, - }, - {nil, nil, - {2, 1, nil, 1, - "https://accounts.google.com/ServiceLogin?passive=true&continue=https%3A%2F%2Fwww.youtube.com%2Fsignin%3Fnext%3D%252F%26action_handle_signin%3Dtrue%26hl%3Den%26app%3Ddesktop%26feature%3Dsign_in_button&hl=en&service=youtube&uilel=3&requestPath=%2FServiceLogin&Page=PasswordSeparationSignIn", - nil, [] of String, 4}, - 1, - {nil, nil, [] of String}, - nil, nil, nil, true, - }, - }.to_json - - traceback << "Getting challenge..." - - response = client.post("/_/signin/sl/challenge", headers, login_req(challenge_req)) - headers = response.cookies.add_request_headers(headers) - challenge_results = JSON.parse(response.body[5..-1]) - - traceback << "done, returned #{response.status_code}.<br/>" - - headers["Cookie"] = URI.decode_www_form(headers["Cookie"]) - - if challenge_results[0][3]?.try &.== 7 - return error_template(423, "Account has temporarily been disabled") - end - - if token = challenge_results[0][-1]?.try &.[-1]?.try &.as_h?.try &.["5001"]?.try &.[-1].as_a?.try &.[-1].as_s - account_type = "google" - captcha_type = "image" - prompt = nil - tfa = tfa_code - captcha = {tokens: [token], question: ""} - - return templated "user/login" - end - - if challenge_results[0][-1]?.try &.[5] == "INCORRECT_ANSWER_ENTERED" - return error_template(401, "Incorrect password") - end - - prompt_type = challenge_results[0][-1]?.try &.[0].as_a?.try &.[0][2]? - if {"TWO_STEP_VERIFICATION", "LOGIN_CHALLENGE"}.includes? prompt_type - traceback << "Handling prompt #{prompt_type}.<br/>" - case prompt_type - when "TWO_STEP_VERIFICATION" - prompt_type = 2 - else # "LOGIN_CHALLENGE" - prompt_type = 4 - end - - # Prefer Authenticator app and SMS over unsupported protocols - if !{6, 9, 12, 15}.includes?(challenge_results[0][-1][0][0][8].as_i) && prompt_type == 2 - tfa = challenge_results[0][-1][0].as_a.select { |auth_type| {6, 9, 12, 15}.includes? auth_type[8] }[0] - - traceback << "Selecting challenge #{tfa[8]}..." - select_challenge = {prompt_type, nil, nil, nil, {tfa[8]}}.to_json - - tl = challenge_results[1][2] - - tfa = client.post("/_/signin/selectchallenge?TL=#{tl}", headers, login_req(select_challenge)).body - tfa = tfa[5..-1] - tfa = JSON.parse(tfa)[0][-1] - - traceback << "done.<br/>" - else - traceback << "Using challenge #{challenge_results[0][-1][0][0][8]}.<br/>" - tfa = challenge_results[0][-1][0][0] - end - - if tfa[5] == "QUOTA_EXCEEDED" - return error_template(423, "Quota exceeded, try again in a few hours") - end - - if !tfa_code - account_type = "google" - captcha_type = "image" - - case tfa[8] - when 6, 9 - prompt = "Google verification code" - when 12 - prompt = "Login verification, recovery email: #{tfa[-1][tfa[-1].as_h.keys[0]][0]}" - when 15 - prompt = "Login verification, security question: #{tfa[-1][tfa[-1].as_h.keys[0]][0]}" - else - prompt = "Google verification code" - end - - tfa = nil - captcha = nil - return templated "user/login" - end - - tl = challenge_results[1][2] - - request_type = tfa[8] - case request_type - when 6 # Authenticator app - tfa_req = { - user_hash, nil, 2, nil, - {6, nil, nil, nil, nil, - {tfa_code, false}, - }, - }.to_json - when 9 # Voice or text message - tfa_req = { - user_hash, nil, 2, nil, - {9, nil, nil, nil, nil, nil, nil, nil, - {nil, tfa_code, false, 2}, - }, - }.to_json - when 12 # Recovery email - tfa_req = { - user_hash, nil, 4, nil, - {12, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, - {tfa_code}, - }, - }.to_json - when 15 # Security question - tfa_req = { - user_hash, nil, 5, nil, - {15, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, - {tfa_code}, - }, - }.to_json - else - return error_template(500, "Unable to log in, make sure two-factor authentication (Authenticator or SMS) is turned on.") - end - - traceback << "Submitting challenge..." - - response = client.post("/_/signin/challenge?hl=en&TL=#{tl}", headers, login_req(tfa_req)) - headers = response.cookies.add_request_headers(headers) - challenge_results = JSON.parse(response.body[5..-1]) - - if (challenge_results[0][-1]?.try &.[5] == "INCORRECT_ANSWER_ENTERED") || - (challenge_results[0][-1]?.try &.[5] == "INVALID_INPUT") - return error_template(401, "Invalid TFA code") - end - - traceback << "done.<br/>" - end - - traceback << "Logging in..." - - location = URI.parse(challenge_results[0][-1][2].to_s) - cookies = HTTP::Cookies.from_client_headers(headers) - - headers.delete("Content-Type") - headers.delete("Google-Accounts-XSRF") - - loop do - if !location || location.path == "/ManageAccount" - break - end - - # Occasionally there will be a second page after login confirming - # the user's phone number ("/b/0/SmsAuthInterstitial"), which we currently don't handle. - - if location.path.starts_with? "/b/0/SmsAuthInterstitial" - traceback << "Unhandled dialog /b/0/SmsAuthInterstitial." - end - - login = client.get(location.request_target, headers) - - headers = login.cookies.add_request_headers(headers) - location = login.headers["Location"]?.try { |u| URI.parse(u) } - end - - cookies = HTTP::Cookies.from_client_headers(headers) - sid = cookies["SID"]?.try &.value - if !sid - raise "Couldn't get SID." - end - - user, sid = get_user(sid, headers) - - # We are now logged in - traceback << "done.<br/>" - - host = URI.parse(env.request.headers["Host"]).host - - cookies.each do |cookie| - cookie.secure = Invidious::User::Cookies::SECURE - - if cookie.extension - cookie.extension = cookie.extension.not_nil!.gsub(".youtube.com", host) - cookie.extension = cookie.extension.not_nil!.gsub("Secure; ", "") - end - env.response.cookies << cookie - end - - if env.request.cookies["PREFS"]? - user.preferences = env.get("preferences").as(Preferences) - Invidious::Database::Users.update_preferences(user) - - cookie = env.request.cookies["PREFS"] - cookie.expires = Time.utc(1990, 1, 1) - env.response.cookies << cookie - end - - env.redirect referer - rescue ex - traceback.rewind - # error_message = translate(locale, "Login failed. This may be because two-factor authentication is not turned on for your account.") - error_message = %(#{ex.message}<br/>Traceback:<br/><div style="padding-left:2em" id="traceback">#{traceback.gets_to_end}</div>) - return error_template(500, error_message) - end when "invidious" - if !email + if email.nil? || email.empty? return error_template(401, "User ID is a required field") end - if !password + if password.nil? || password.empty? return error_template(401, "Password is a required field") end user = Invidious::Database::Users.select(email: email) if user - if !user.password - return error_template(400, "Please sign in using 'Log in with Google'") - end - if Crypto::Bcrypt::Password.new(user.password.not_nil!).verify(password.byte_slice(0, 55)) sid = Base64.urlsafe_encode(Random::Secure.random_bytes(32)) Invidious::Database::SessionIDs.insert(sid, email) @@ -367,8 +99,6 @@ module Invidious::Routes::Login captcha_type ||= "image" account_type = "invidious" - tfa = false - prompt = "" if captcha_type == "image" captcha = Invidious::User::Captcha.generate_image(HMAC_KEY) @@ -481,11 +211,4 @@ module Invidious::Routes::Login env.redirect referer end - - def self.captcha(env) - headers = HTTP::Headers{":authority" => "accounts.google.com"} - response = YT_POOL.client &.get(env.request.resource, headers) - env.response.headers["Content-Type"] = response.headers["Content-Type"] - response.body - end end diff --git a/src/invidious/routes/notifications.cr b/src/invidious/routes/notifications.cr index 272a3dc7..8922b740 100644 --- a/src/invidious/routes/notifications.cr +++ b/src/invidious/routes/notifications.cr @@ -24,50 +24,6 @@ module Invidious::Routes::Notifications user = user.as(User) - if !user.password - channel_req = {} of String => String - - channel_req["receive_all_updates"] = env.params.query["receive_all_updates"]? || "true" - channel_req["receive_no_updates"] = env.params.query["receive_no_updates"]? || "" - channel_req["receive_post_updates"] = env.params.query["receive_post_updates"]? || "true" - - channel_req.reject! { |k, v| v != "true" && v != "false" } - - headers = HTTP::Headers.new - headers["Cookie"] = env.request.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"] - else - return env.redirect referer - end - - headers["content-type"] = "application/x-www-form-urlencoded" - channel_req["session_token"] = session_token - - subs = XML.parse_html(html.body) - subs.xpath_nodes(%q(//a[@class="subscription-title yt-uix-sessionlink"]/@href)).each do |channel| - channel_id = channel.content.lstrip("/channel/").not_nil! - channel_req["channel_id"] = channel_id - - YT_POOL.client &.post("/subscription_ajax?action_update_subscription_preferences=1", headers, form: channel_req) - end - end - if redirect env.redirect referer else diff --git a/src/invidious/routes/playlists.cr b/src/invidious/routes/playlists.cr index a65ff64c..9c6843e9 100644 --- a/src/invidious/routes/playlists.cr +++ b/src/invidious/routes/playlists.cr @@ -163,13 +163,20 @@ module Invidious::Routes::Playlists end begin - videos = get_playlist_videos(playlist, offset: (page - 1) * 100) + items = get_playlist_videos(playlist, offset: (page - 1) * 100) rescue ex - videos = [] of PlaylistVideo + items = [] of PlaylistVideo end csrf_token = generate_response(sid, {":edit_playlist"}, HMAC_KEY) + # Pagination + page_nav_html = Frontend::Pagination.nav_numeric(locale, + base_url: "/playlist?list=#{playlist.id}", + current_page: page, + show_next: (items.size == 100) + ) + templated "edit_playlist" end @@ -247,11 +254,19 @@ module Invidious::Routes::Playlists begin query = Invidious::Search::Query.new(env.params.query, :playlist, region) - videos = query.process.select(SearchVideo).map(&.as(SearchVideo)) + items = query.process.select(SearchVideo).map(&.as(SearchVideo)) rescue ex - videos = [] of SearchVideo + items = [] of SearchVideo end + # Pagination + query_encoded = URI.encode_www_form(query.try &.text || "", space_to_plus: true) + page_nav_html = Frontend::Pagination.nav_numeric(locale, + base_url: "/add_playlist_items?list=#{playlist.id}&q=#{query_encoded}", + current_page: page, + show_next: (items.size >= 20) + ) + env.set "add_playlist_items", plid templated "add_playlist_items" end @@ -320,10 +335,6 @@ module Invidious::Routes::Playlists end end - if !user.password - # TODO: Playlist stub, sync with YouTube for Google accounts - # playlist_ajax(playlist_id, action, env.request.headers) - end email = user.email case action @@ -428,9 +439,9 @@ module Invidious::Routes::Playlists begin if playlist.is_a? InvidiousPlaylist - videos = get_playlist_videos(playlist, offset: (page - 1) * 100) + items = get_playlist_videos(playlist, offset: (page - 1) * 100) else - videos = get_playlist_videos(playlist, offset: (page - 1) * 200) + items = get_playlist_videos(playlist, offset: (page - 1) * 200) end rescue ex return error_template(500, "Error encountered while retrieving playlist videos.<br>#{ex.message}") @@ -440,6 +451,13 @@ module Invidious::Routes::Playlists env.set "remove_playlist_items", plid end + # Pagination + page_nav_html = Frontend::Pagination.nav_numeric(locale, + base_url: "/playlist?list=#{playlist.id}", + current_page: page, + show_next: (page_count != 1 && page < page_count) + ) + templated "playlist" end diff --git a/src/invidious/routes/search.cr b/src/invidious/routes/search.cr index 6c3088de..5be33533 100644 --- a/src/invidious/routes/search.cr +++ b/src/invidious/routes/search.cr @@ -52,24 +52,28 @@ module Invidious::Routes::Search user = env.get? "user" begin - videos = query.process + items = query.process rescue ex : ChannelSearchException return error_template(404, "Unable to find channel with id of '#{HTML.escape(ex.channel)}'. Are you sure that's an actual channel id? It should look like 'UC4QobU6STFB0P71PMvOGN5A'.") rescue ex return error_template(500, ex) end - params = query.to_http_params - url_prev_page = "/search?#{params}&page=#{query.page - 1}" - url_next_page = "/search?#{params}&page=#{query.page + 1}" - redirect_url = Invidious::Frontend::Misc.redirect_url(env) + # Pagination + page_nav_html = Frontend::Pagination.nav_numeric(locale, + base_url: "/search?#{query.to_http_params}", + current_page: query.page, + show_next: (items.size >= 20) + ) + if query.type == Invidious::Search::Query::Type::Channel env.set "search", "channel:#{query.channel} #{query.text}" else env.set "search", query.text end + templated "search" end end @@ -91,16 +95,18 @@ module Invidious::Routes::Search end begin - videos = Invidious::Hashtag.fetch(hashtag, page) + items = Invidious::Hashtag.fetch(hashtag, page) rescue ex return error_template(500, ex) end - params = env.params.query.empty? ? "" : "&#{env.params.query}" - + # Pagination hashtag_encoded = URI.encode_www_form(hashtag, space_to_plus: false) - url_prev_page = "/hashtag/#{hashtag_encoded}?page=#{page - 1}#{params}" - url_next_page = "/hashtag/#{hashtag_encoded}?page=#{page + 1}#{params}" + page_nav_html = Frontend::Pagination.nav_numeric(locale, + base_url: "/hashtag/#{hashtag_encoded}", + current_page: page, + show_next: (items.size >= 60) + ) templated "hashtag" end diff --git a/src/invidious/routes/subscriptions.cr b/src/invidious/routes/subscriptions.cr index 0704c05e..7f9ec592 100644 --- a/src/invidious/routes/subscriptions.cr +++ b/src/invidious/routes/subscriptions.cr @@ -43,11 +43,6 @@ module Invidious::Routes::Subscriptions channel_id = env.params.query["c"]? channel_id ||= "" - if !user.password - # Sync subscriptions with YouTube - subscribe_ajax(channel_id, action, env.request.headers) - end - case action when "action_create_subscription_to_channel" if !user.subscriptions.includes? channel_id @@ -82,14 +77,6 @@ module Invidious::Routes::Subscriptions user = user.as(User) sid = sid.as(String) - if !user.password - # Refresh account - headers = HTTP::Headers.new - headers["Cookie"] = env.request.headers["Cookie"] - - user, sid = get_user(sid, headers) - end - action_takeout = env.params.query["action_takeout"]?.try &.to_i? action_takeout ||= 0 action_takeout = action_takeout == 1 diff --git a/src/invidious/routing.cr b/src/invidious/routing.cr index 72ee9194..9c43171c 100644 --- a/src/invidious/routing.cr +++ b/src/invidious/routing.cr @@ -57,7 +57,6 @@ module Invidious::Routing get "/login", Routes::Login, :login_page post "/login", Routes::Login, :login post "/signout", Routes::Login, :signout - get "/Captcha", Routes::Login, :captcha # User preferences get "/preferences", Routes::PreferencesRoute, :show @@ -119,6 +118,8 @@ module Invidious::Routing get "/channel/:ucid/videos", Routes::Channels, :videos get "/channel/:ucid/shorts", Routes::Channels, :shorts get "/channel/:ucid/streams", Routes::Channels, :streams + get "/channel/:ucid/podcasts", Routes::Channels, :podcasts + get "/channel/:ucid/releases", Routes::Channels, :releases get "/channel/:ucid/playlists", Routes::Channels, :playlists get "/channel/:ucid/community", Routes::Channels, :community get "/channel/:ucid/channels", Routes::Channels, :channels @@ -229,6 +230,9 @@ module Invidious::Routing get "/api/v1/channels/:ucid", {{namespace}}::Channels, :home get "/api/v1/channels/:ucid/shorts", {{namespace}}::Channels, :shorts get "/api/v1/channels/:ucid/streams", {{namespace}}::Channels, :streams + get "/api/v1/channels/:ucid/podcasts", {{namespace}}::Channels, :podcasts + get "/api/v1/channels/:ucid/releases", {{namespace}}::Channels, :releases + get "/api/v1/channels/:ucid/channels", {{namespace}}::Channels, :channels {% for route in {"videos", "latest", "playlists", "community", "search"} %} diff --git a/src/invidious/user/imports.cr b/src/invidious/user/imports.cr index e4b25156..0a2fe1e2 100644 --- a/src/invidious/user/imports.cr +++ b/src/invidious/user/imports.cr @@ -6,7 +6,7 @@ struct Invidious::User # Parse a youtube CSV subscription file def parse_subscription_export_csv(csv_content : String) - rows = CSV.new(csv_content, headers: true) + rows = CSV.new(csv_content.strip('\n'), headers: true) subscriptions = Array(String).new # Counter to limit the amount of imports. @@ -32,10 +32,10 @@ struct Invidious::User def parse_playlist_export_csv(user : User, raw_input : String) # Split the input into head and body content - raw_head, raw_body = raw_input.split("\n\n", limit: 2, remove_empty: true) + raw_head, raw_body = raw_input.strip('\n').split("\n\n", limit: 2, remove_empty: true) # Create the playlist from the head content - csv_head = CSV.new(raw_head, headers: true) + csv_head = CSV.new(raw_head.strip('\n'), headers: true) csv_head.next title = csv_head[4] description = csv_head[5] @@ -51,7 +51,7 @@ struct Invidious::User Invidious::Database::Playlists.update_description(playlist.id, description) # Add each video to the playlist from the body content - csv_body = CSV.new(raw_body, headers: true) + csv_body = CSV.new(raw_body.strip('\n'), headers: true) csv_body.each do |row| video_id = row[0] if playlist diff --git a/src/invidious/users.cr b/src/invidious/users.cr index b763596b..65566d20 100644 --- a/src/invidious/users.cr +++ b/src/invidious/users.cr @@ -3,75 +3,6 @@ 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)) @@ -91,38 +22,6 @@ def create_user(sid, email, password) 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 diff --git a/src/invidious/videos.cr b/src/invidious/videos.cr index 0038a97a..f38b33e5 100644 --- a/src/invidious/videos.cr +++ b/src/invidious/videos.cr @@ -394,7 +394,9 @@ def fetch_video(id, region) if reason = info["reason"]? if reason == "Video unavailable" raise NotFoundException.new(reason.as_s || "") - else + elsif !reason.as_s.starts_with? "Premieres" + # dont error when it's a premiere. + # we already parsed most of the data and display the premiere date raise InfoException.new(reason.as_s || "") end end diff --git a/src/invidious/videos/parser.cr b/src/invidious/videos/parser.cr index 2e8eecc3..9cc0ffdc 100644 --- a/src/invidious/videos/parser.cr +++ b/src/invidious/videos/parser.cr @@ -78,7 +78,11 @@ def extract_video_info(video_id : String, proxy_region : String? = nil) elsif video_id != player_response.dig("videoDetails", "videoId") # YouTube may return a different video player response than expected. # See: https://github.com/TeamNewPipe/NewPipe/issues/8713 - raise VideoNotAvailableException.new("The video returned by YouTube isn't the requested one. (WEB client)") + # Line to be reverted if one day we solve the video not available issue. + return { + "version" => JSON::Any.new(Video::SCHEMA_VERSION.to_i64), + "reason" => JSON::Any.new("Can't load the video on this Invidious instance. YouTube is currently trying to block Invidious instances. <a href=\"https://github.com/iv-org/invidious/issues/3822\">Click here for more info about the issue.</a>"), + } else reason = nil end diff --git a/src/invidious/views/add_playlist_items.ecr b/src/invidious/views/add_playlist_items.ecr index bcba74cf..6aea82ae 100644 --- a/src/invidious/views/add_playlist_items.ecr +++ b/src/invidious/views/add_playlist_items.ecr @@ -31,33 +31,5 @@ </script> <script src="/js/playlist_widget.js?v=<%= ASSET_COMMIT %>"></script> -<div class="pure-g"> - <% videos.each_slice(4) do |slice| %> - <% slice.each do |item| %> - <%= rendered "components/item" %> - <% end %> - <% end %> -</div> -<script src="/js/watched_indicator.js"></script> - -<% if query %> - <%- query_encoded = URI.encode_www_form(query.text, space_to_plus: true) -%> - <div class="pure-g h-box"> - <div class="pure-u-1 pure-u-lg-1-5"> - <% if query.page > 1 %> - <a href="/add_playlist_items?list=<%= plid %>&q=<%= query_encoded %>&page=<%= page - 1 %>"> - <%= translate(locale, "Previous page") %> - </a> - <% end %> - </div> - <div class="pure-u-1 pure-u-lg-3-5"></div> - <div class="pure-u-1 pure-u-lg-1-5" style="text-align:right"> - <% if videos.size >= 20 %> - <a href="/add_playlist_items?list=<%= plid %>&q=<%= query_encoded %>&page=<%= page + 1 %>"> - <%= translate(locale, "Next page") %> - </a> - <% end %> - </div> - </div> -<% end %> +<%= rendered "components/items_paginated" %> diff --git a/src/invidious/views/channel.ecr b/src/invidious/views/channel.ecr index 6e62a471..09df106d 100644 --- a/src/invidious/views/channel.ecr +++ b/src/invidious/views/channel.ecr @@ -9,13 +9,20 @@ when .streams? then "/channel/#{ucid}/streams" when .playlists? then "/channel/#{ucid}/playlists" when .channels? then "/channel/#{ucid}/channels" + when .podcasts? then "/channel/#{ucid}/podcasts" + when .releases? then "/channel/#{ucid}/releases" else "/channel/#{ucid}" end youtube_url = "https://www.youtube.com#{relative_url}" redirect_url = Invidious::Frontend::Misc.redirect_url(env) --%> + + page_nav_html = IV::Frontend::Pagination.nav_ctoken(locale, + base_url: relative_url, + ctoken: next_continuation + ) +%> <% content_for "header" do %> <%- if selected_tab.videos? -%> @@ -43,21 +50,5 @@ <hr> </div> -<div class="pure-g"> -<% items.each do |item| %> - <%= rendered "components/item" %> -<% end %> -</div> -<script src="/js/watched_indicator.js"></script> - -<div class="pure-g h-box"> - <div class="pure-u-1 pure-u-md-4-5"></div> - <div class="pure-u-1 pure-u-lg-1-5" style="text-align:right"> - <% if next_continuation %> - <a href="<%= relative_url %>?continuation=<%= next_continuation %><% if sort_options.any? sort_by %>&sort_by=<%= sort_by %><% end %>"> - <%= translate(locale, "Next page") %> - </a> - <% end %> - </div> -</div> +<%= rendered "components/items_paginated" %> diff --git a/src/invidious/views/components/channel_info.ecr b/src/invidious/views/components/channel_info.ecr index 59888760..f4164f31 100644 --- a/src/invidious/views/components/channel_info.ecr +++ b/src/invidious/views/components/channel_info.ecr @@ -8,29 +8,30 @@ </div> <% end %> -<div class="pure-g h-box"> - <div class="pure-u-2-3"> +<div class="pure-g h-box flexible title"> + <div class="pure-u-1-2 flex-left flexible"> <div class="channel-profile"> <img src="/ggpht<%= channel_profile_pic %>" alt="" /> <span><%= author %></span><% if !channel.verified.nil? && channel.verified %> <i class="icon ion ion-md-checkmark-circle"></i><% end %> </div> </div> - <div class="pure-u-1-3"> - <h3 style="text-align:right"> - <a href="/feed/channel/<%= ucid %>"><i class="icon ion-logo-rss"></i></a> - </h3> - </div> -</div> -<div class="h-box"> - <div id="descriptionWrapper"> - <p><span style="white-space:pre-wrap"><%= channel.description_html %></span></p> + <div class="pure-u-1-2 flex-right flexible button-container"> + <div class="pure-u"> + <% sub_count_text = number_to_short_text(channel.sub_count) %> + <%= rendered "components/subscribe_widget" %> + </div> + + <div class="pure-u"> + <a class="pure-button pure-button-secondary" dir="auto" href="/feed/channel/<%= ucid %>"> + <i class="icon ion-logo-rss"></i> <%= translate(locale, "generic_button_rss") %> + </a> + </div> </div> </div> <div class="h-box"> - <% sub_count_text = number_to_short_text(channel.sub_count) %> - <%= rendered "components/subscribe_widget" %> + <div id="descriptionWrapper"><p><span style="white-space:pre-wrap"><%= channel.description_html %></span></p></div> </div> <div class="pure-g h-box"> diff --git a/src/invidious/views/components/item.ecr b/src/invidious/views/components/item.ecr index 7cfd38db..7ffd2d93 100644 --- a/src/invidious/views/components/item.ecr +++ b/src/invidious/views/components/item.ecr @@ -1,157 +1,146 @@ -<% item_watched = !item.is_a?(SearchChannel | SearchPlaylist | InvidiousPlaylist | Category) && env.get?("user").try &.as(User).watched.index(item.id) != nil %> +<%- + thin_mode = env.get("preferences").as(Preferences).thin_mode + item_watched = !item.is_a?(SearchChannel | SearchPlaylist | InvidiousPlaylist | Category) && env.get?("user").try &.as(User).watched.index(item.id) != nil + author_verified = item.responds_to?(:author_verified) && item.author_verified +-%> <div class="pure-u-1 pure-u-md-1-4"> <div class="h-box"> <% case item when %> <% when SearchChannel %> - <a href="/channel/<%= item.ucid %>"> - <% if !env.get("preferences").as(Preferences).thin_mode %> + <% if !thin_mode %> + <a tabindex="-1" href="/channel/<%= item.ucid %>"> <center> - <img loading="lazy" tabindex="-1" style="width:56.25%" src="/ggpht<%= URI.parse(item.author_thumbnail).request_target.gsub(/=s\d+/, "=s176") %>" alt="" /> + <img loading="lazy" style="width:56.25%" src="/ggpht<%= URI.parse(item.author_thumbnail).request_target.gsub(/=s\d+/, "=s176") %>" alt="" /> </center> - <% end %> - <p dir="auto"><%= HTML.escape(item.author) %><% if !item.author_verified.nil? && item.author_verified %> <i class="icon ion ion-md-checkmark-circle"></i><% end %></p> - </a> + </a> + <%- else -%> + <div class="thumbnail-placeholder" style="width:56.25%"></div> + <% end %> + + <div class="video-card-row flexible"> + <div class="flex-left"><a href="/channel/<%= item.ucid %>"> + <p class="channel-name" dir="auto"><%= HTML.escape(item.author) %> + <%- if author_verified %> <i class="icon ion ion-md-checkmark-circle"></i><% end -%> + </p> + </a></div> + </div> + <p><%= translate_count(locale, "generic_subscribers_count", item.subscriber_count, NumberFormatting::Separator) %></p> <% if !item.auto_generated %><p><%= translate_count(locale, "generic_videos_count", item.video_count, NumberFormatting::Separator) %></p><% end %> <h5><%= item.description_html %></h5> <% when SearchPlaylist, InvidiousPlaylist %> - <% if item.id.starts_with? "RD" %> - <% url = "/mix?list=#{item.id}&continuation=#{URI.parse(item.thumbnail || "/vi/-----------").request_target.split("/")[2]}" %> - <% else %> - <% url = "/playlist?list=#{item.id}" %> - <% end %> - - <a style="width:100%" href="<%= url %>"> - <% if !env.get("preferences").as(Preferences).thin_mode %> - <div class="thumbnail"> - <img loading="lazy" tabindex="-1" class="thumbnail" src="<%= URI.parse(item.thumbnail || "/").request_target %>" alt="" /> - <p class="length"><%= translate_count(locale, "generic_videos_count", item.video_count, NumberFormatting::Separator) %></p> - </div> - <% end %> - <p dir="auto"><%= HTML.escape(item.title) %></p> - </a> - <a href="/channel/<%= item.ucid %>"> - <p dir="auto"><b><%= HTML.escape(item.author) %><% if !item.is_a?(InvidiousPlaylist) && !item.author_verified.nil? && item.author_verified %> <i class="icon ion ion-md-checkmark-circle"></i><% end %></b></p> - </a> - <% when MixVideo %> - <a href="/watch?v=<%= item.id %>&list=<%= item.rdid %>"> - <% if !env.get("preferences").as(Preferences).thin_mode %> - <div class="thumbnail"> - <img loading="lazy" tabindex="-1" class="thumbnail" src="/vi/<%= item.id %>/mqdefault.jpg" alt="" /> - <% if item.length_seconds != 0 %> - <p class="length"><%= recode_length_seconds(item.length_seconds) %></p> - <% end %> + <%- + if item.id.starts_with? "RD" + link_url = "/mix?list=#{item.id}&continuation=#{URI.parse(item.thumbnail || "/vi/-----------").request_target.split("/")[2]}" + else + link_url = "/playlist?list=#{item.id}" + end + -%> - <% if item_watched %> - <div class="watched-overlay"></div> - <div class="watched-indicator" data-length="<%= item.length_seconds %>" data-id="<%= item.id %>"></div> - <% end %> - </div> - <% end %> - <p dir="auto"><%= HTML.escape(item.title) %></p> - </a> - <a href="/channel/<%= item.ucid %>"> - <p dir="auto"><b><%= HTML.escape(item.author) %></b></p> - </a> - <% when PlaylistVideo %> - <a style="width:100%" href="/watch?v=<%= item.id %>&list=<%= item.plid %>&index=<%= item.index %>"> - <% if !env.get("preferences").as(Preferences).thin_mode %> - <div class="thumbnail"> - <img loading="lazy" tabindex="-1" class="thumbnail" src="/vi/<%= item.id %>/mqdefault.jpg" alt="" /> - - <% if plid_form = env.get?("remove_playlist_items") %> - <form data-onsubmit="return_false" action="/playlist_ajax?action_remove_video=1&set_video_id=<%= item.index %>&playlist_id=<%= plid_form %>&referer=<%= env.get("current_page") %>" method="post"> - <input type="hidden" name="csrf_token" value="<%= HTML.escape(env.get?("csrf_token").try &.as(String) || "") %>"> - <p class="watched"> - <button type="submit" style="all:unset" data-onclick="remove_playlist_item" data-index="<%= item.index %>" data-plid="<%= plid_form %>"><i class="icon ion-md-trash"></i></button> - </p> - </form> - <% end %> + <div class="thumbnail"> + <%- if !thin_mode %> + <a tabindex="-1" href="<%= link_url %>"> + <img loading="lazy" class="thumbnail" src="<%= URI.parse(item.thumbnail || "/").request_target %>" alt="" /> + </a> + <%- else -%> + <div class="thumbnail-placeholder"></div> + <%- end -%> - <% if item.responds_to?(:live_now) && item.live_now %> - <p class="length"><i class="icon ion-ios-play-circle"></i> <%= translate(locale, "LIVE") %></p> - <% elsif item.length_seconds != 0 %> - <p class="length"><%= recode_length_seconds(item.length_seconds) %></p> - <% end %> + <div class="bottom-right-overlay"> + <p class="length"><%= translate_count(locale, "generic_videos_count", item.video_count, NumberFormatting::Separator) %></p> + </div> + </div> - <% if item_watched %> - <div class="watched-overlay"></div> - <div class="watched-indicator" data-length="<%= item.length_seconds %>" data-id="<%= item.id %>"></div> - <% end %> - </div> - <% end %> - <p dir="auto"><%= HTML.escape(item.title) %></p> - </a> + <div class="video-card-row"> + <a href="<%= link_url %>"><p dir="auto"><%= HTML.escape(item.title) %></p></a> + </div> <div class="video-card-row flexible"> <div class="flex-left"><a href="/channel/<%= item.ucid %>"> - <p class="channel-name" dir="auto"><%= HTML.escape(item.author) %></p> + <p class="channel-name" dir="auto"><%= HTML.escape(item.author) %> + <%- if author_verified %> <i class="icon ion ion-md-checkmark-circle"></i><% end -%> + </p> </a></div> - <% endpoint_params = "?v=#{item.id}&list=#{item.plid}" %> - <%= rendered "components/video-context-buttons" %> - </div> - - <div class="video-card-row flexible"> - <div class="flex-left"> - <% if item.responds_to?(:premiere_timestamp) && item.premiere_timestamp.try &.> Time.utc %> - <p dir="auto"><%= translate(locale, "Premieres in `x`", recode_date((item.premiere_timestamp.as(Time) - Time.utc).ago, locale)) %></p> - <% elsif Time.utc - item.published > 1.minute %> - <p dir="auto"><%= translate(locale, "Shared `x` ago", recode_date(item.published, locale)) %></p> - <% end %> - </div> - - <% if item.responds_to?(:views) && item.views %> - <div class="flex-right"> - <p dir="auto"><%= translate_count(locale, "generic_views_count", item.views || 0, NumberFormatting::Short) %></p> - </div> - <% end %> </div> <% when Category %> <% else %> - <a style="width:100%" href="/watch?v=<%= item.id %>"> - <% if !env.get("preferences").as(Preferences).thin_mode %> - <div class="thumbnail"> - <img loading="lazy" tabindex="-1" class="thumbnail" src="/vi/<%= item.id %>/mqdefault.jpg" alt="" /> - <% if env.get? "show_watched" %> - <form data-onsubmit="return_false" action="/watch_ajax?action_mark_watched=1&id=<%= item.id %>&referer=<%= env.get("current_page") %>" method="post"> - <input type="hidden" name="csrf_token" value="<%= HTML.escape(env.get?("csrf_token").try &.as(String) || "") %>"> - <p class="watched"> - <button type="submit" style="all:unset" data-onclick="mark_watched" data-id="<%= item.id %>"> - <i data-mouse="switch_classes" data-switch-classes="ion-ios-eye-off,ion-ios-eye" class="icon ion-ios-eye"></i> - </button> - </p> - </form> - <% elsif plid_form = env.get? "add_playlist_items" %> - <form data-onsubmit="return_false" action="/playlist_ajax?action_add_video=1&video_id=<%= item.id %>&playlist_id=<%= plid_form %>&referer=<%= env.get("current_page") %>" method="post"> - <input type="hidden" name="csrf_token" value="<%= HTML.escape(env.get?("csrf_token").try &.as(String) || "") %>"> - <p class="watched"> - <button type="submit" style="all:unset" data-onclick="add_playlist_item" data-id="<%= item.id %>" data-plid="<%= plid_form %>"><i class="icon ion-md-add"></i></button> - </p> - </form> - <% end %> + <%- + # `endpoint_params` is used for the "video-context-buttons" component + if item.is_a?(PlaylistVideo) + link_url = "/watch?v=#{item.id}&list=#{item.plid}&index=#{item.index}" + endpoint_params = "?v=#{item.id}&list=#{item.plid}" + elsif item.is_a?(MixVideo) + link_url = "/watch?v=#{item.id}&list=#{item.rdid}" + endpoint_params = "?v=#{item.id}&list=#{item.rdid}" + else + link_url = "/watch?v=#{item.id}" + endpoint_params = "?v=#{item.id}" + end + -%> - <% if item.responds_to?(:live_now) && item.live_now %> - <p class="length" dir="auto"><i class="icon ion-ios-play-circle"></i> <%= translate(locale, "LIVE") %></p> - <% elsif item.length_seconds != 0 %> - <p class="length"><%= recode_length_seconds(item.length_seconds) %></p> - <% end %> + <div class="thumbnail"> + <%- if !thin_mode -%> + <a tabindex="-1" href="<%= link_url %>"> + <img loading="lazy" class="thumbnail" src="/vi/<%= item.id %>/mqdefault.jpg" alt="" /> <% if item_watched %> <div class="watched-overlay"></div> <div class="watched-indicator" data-length="<%= item.length_seconds %>" data-id="<%= item.id %>"></div> <% end %> - </div> - <% end %> - <p dir="auto"><%= HTML.escape(item.title) %></p> - </a> + </a> + <%- else -%> + <div class="thumbnail-placeholder"></div> + <%- end -%> + + <div class="top-left-overlay"> + <%- if env.get? "show_watched" -%> + <form data-onsubmit="return_false" action="/watch_ajax?action_mark_watched=1&id=<%= item.id %>&referer=<%= env.get("current_page") %>" method="post"> + <input type="hidden" name="csrf_token" value="<%= HTML.escape(env.get?("csrf_token").try &.as(String) || "") %>"> + <button type="submit" class="pure-button pure-button-secondary low-profile" + data-onclick="mark_watched" data-id="<%= item.id %>"> + <i data-mouse="switch_classes" data-switch-classes="ion-ios-eye-off,ion-ios-eye" class="icon ion-ios-eye"></i> + </button> + </form> + <%- end -%> + + <%- if plid_form = env.get?("add_playlist_items") -%> + <%- form_parameters = "action_add_video=1&video_id=#{item.id}&playlist_id=#{plid_form}&referer=#{env.get("current_page")}" -%> + <form data-onsubmit="return_false" action="/playlist_ajax?<%= form_parameters %>" method="post"> + <input type="hidden" name="csrf_token" value="<%= HTML.escape(env.get?("csrf_token").try &.as(String) || "") %>"> + <button type="submit" class="pure-button pure-button-secondary low-profile" + data-onclick="add_playlist_item" data-id="<%= item.id %>" data-plid="<%= plid_form %>"><i class="icon ion-md-add"></i></button> + </form> + <%- elsif item.is_a?(PlaylistVideo) && (plid_form = env.get?("remove_playlist_items")) -%> + <%- form_parameters = "action_remove_video=1&set_video_id=#{item.index}&playlist_id=#{plid_form}&referer=#{env.get("current_page")}" -%> + <form data-onsubmit="return_false" action="/playlist_ajax?<%= form_parameters %>" method="post"> + <input type="hidden" name="csrf_token" value="<%= HTML.escape(env.get?("csrf_token").try &.as(String) || "") %>"> + <button type="submit" class="pure-button pure-button-secondary low-profile" + data-onclick="remove_playlist_item" data-index="<%= item.index %>" data-plid="<%= plid_form %>"><i class="icon ion-md-trash"></i></button> + </form> + <%- end -%> + </div> + + <div class="bottom-right-overlay"> + <%- if item.responds_to?(:live_now) && item.live_now -%> + <p class="length" dir="auto"><i class="icon ion-ios-play-circle"></i> <%= translate(locale, "LIVE") %></p> + <%- elsif item.length_seconds != 0 -%> + <p class="length"><%= recode_length_seconds(item.length_seconds) %></p> + <%- end -%> + </div> + </div> + + <div class="video-card-row"> + <a href="<%= link_url %>"><p dir="auto"><%= HTML.escape(item.title) %></p></a> + </div> <div class="video-card-row flexible"> <div class="flex-left"><a href="/channel/<%= item.ucid %>"> - <p class="channel-name" dir="auto"><%= HTML.escape(item.author) %><% if !item.is_a?(ChannelVideo) && !item.author_verified.nil? && item.author_verified %> <i class="icon ion ion-md-checkmark-circle"></i><% end %></p> + <p class="channel-name" dir="auto"><%= HTML.escape(item.author) %> + <%- if author_verified %> <i class="icon ion ion-md-checkmark-circle"></i><% end -%> + </p> </a></div> - <% endpoint_params = "?v=#{item.id}" %> <%= rendered "components/video-context-buttons" %> </div> @@ -159,7 +148,7 @@ <div class="flex-left"> <% if item.responds_to?(:premiere_timestamp) && item.premiere_timestamp.try &.> Time.utc %> <p class="video-data" dir="auto"><%= translate(locale, "Premieres in `x`", recode_date((item.premiere_timestamp.as(Time) - Time.utc).ago, locale)) %></p> - <% elsif Time.utc - item.published > 1.minute %> + <% elsif item.responds_to?(:published) && (Time.utc - item.published) > 1.minute %> <p class="video-data" dir="auto"><%= translate(locale, "Shared `x` ago", recode_date(item.published, locale)) %></p> <% end %> </div> diff --git a/src/invidious/views/components/items_paginated.ecr b/src/invidious/views/components/items_paginated.ecr new file mode 100644 index 00000000..4534a0a3 --- /dev/null +++ b/src/invidious/views/components/items_paginated.ecr @@ -0,0 +1,11 @@ +<%= page_nav_html %> + +<div class="pure-g"> + <%- items.each do |item| -%> + <%= rendered "components/item" %> + <%- end -%> +</div> + +<%= page_nav_html %> + +<script src="/js/watched_indicator.js"></script> diff --git a/src/invidious/views/components/subscribe_widget.ecr b/src/invidious/views/components/subscribe_widget.ecr index b9d5f783..05e4e253 100644 --- a/src/invidious/views/components/subscribe_widget.ecr +++ b/src/invidious/views/components/subscribe_widget.ecr @@ -1,22 +1,18 @@ <% if user %> <% if subscriptions.includes? ucid %> - <p> <form action="/subscription_ajax?action_remove_subscriptions=1&c=<%= ucid %>&referer=<%= env.get("current_page") %>" method="post"> <input type="hidden" name="csrf_token" value="<%= HTML.escape(env.get?("csrf_token").try &.as(String) || "") %>"> <button data-type="unsubscribe" id="subscribe" class="pure-button pure-button-primary"> <b><input style="all:unset" type="submit" value="<%= translate(locale, "Unsubscribe") %> | <%= sub_count_text %>"></b> </button> </form> - </p> <% else %> - <p> <form action="/subscription_ajax?action_create_subscription_to_channel=1&c=<%= ucid %>&referer=<%= env.get("current_page") %>" method="post"> <input type="hidden" name="csrf_token" value="<%= HTML.escape(env.get?("csrf_token").try &.as(String) || "") %>"> <button data-type="subscribe" id="subscribe" class="pure-button pure-button-primary"> <b><input style="all:unset" type="submit" value="<%= translate(locale, "Subscribe") %> | <%= sub_count_text %>"></b> </button> </form> - </p> <% end %> <script id="subscribe_data" type="application/json"> @@ -33,10 +29,8 @@ </script> <script src="/js/subscribe_widget.js?v=<%= ASSET_COMMIT %>"></script> <% else %> - <p> <a id="subscribe" class="pure-button pure-button-primary" href="/login?referer=<%= env.get("current_page") %>"> <b><%= translate(locale, "Subscribe") %> | <%= sub_count_text %></b> </a> - </p> <% end %> diff --git a/src/invidious/views/components/video-context-buttons.ecr b/src/invidious/views/components/video-context-buttons.ecr index ddb6c983..385ed6b3 100644 --- a/src/invidious/views/components/video-context-buttons.ecr +++ b/src/invidious/views/components/video-context-buttons.ecr @@ -1,4 +1,4 @@ -<div class="flex-right"> +<div class="flex-right flexible"> <div class="icon-buttons"> <a title="<%=translate(locale, "videoinfo_watch_on_youTube")%>" href="https://www.youtube.com/watch<%=endpoint_params%>"> <i class="icon ion-logo-youtube"></i> @@ -6,7 +6,7 @@ <a title="<%=translate(locale, "Audio mode")%>" href="/watch<%=endpoint_params%>&listen=1"> <i class="icon ion-md-headset"></i> </a> - + <% if env.get("preferences").as(Preferences).automatic_instance_redirect%> <a title="<%=translate(locale, "Switch Invidious Instance")%>" href="/redirect?referer=%2Fwatch<%=URI.encode_www_form(endpoint_params)%>"> <i class="icon ion-md-jet"></i> diff --git a/src/invidious/views/edit_playlist.ecr b/src/invidious/views/edit_playlist.ecr index 548104c8..34157c67 100644 --- a/src/invidious/views/edit_playlist.ecr +++ b/src/invidious/views/edit_playlist.ecr @@ -6,35 +6,43 @@ <% end %> <form class="pure-form" action="/edit_playlist?list=<%= plid %>" method="post"> - <div class="pure-g h-box"> - <div class="pure-u-2-3"> + <div class="h-box flexible"> + <div class="flex-right button-container"> + <div class="pure-u"> + <a class="pure-button pure-button-secondary low-profile" dir="auto" href="/playlist?list=<%= plid %>"> + <i class="icon ion-md-close"></i> <%= translate(locale, "generic_button_cancel") %> + </a> + </div> + <div class="pure-u"> + <button class="pure-button pure-button-secondary low-profile" dir="auto" type="submit"> + <i class="icon ion-md-save"></i> <%= translate(locale, "generic_button_save") %> + </button> + </div> + <div class="pure-u"> + <a class="pure-button pure-button-secondary low-profile" dir="auto" href="/delete_playlist?list=<%= plid %>"> + <i class="icon ion-md-trash"></i> <%= translate(locale, "generic_button_delete") %> + </a> + </div> + </div> + </div> + + <div class="h-box flexible title"> + <div> <h3><input class="pure-input-1" maxlength="150" name="title" type="text" value="<%= title %>"></h3> + </div> + </div> + + <div class="h-box"> + <div class="pure-u-1-1"> <b> <%= HTML.escape(playlist.author) %> | <%= translate_count(locale, "generic_videos_count", playlist.video_count) %> | - <%= translate(locale, "Updated `x` ago", recode_date(playlist.updated, locale)) %> | - <i class="icon <%= {"ion-md-globe", "ion-ios-unlock", "ion-ios-lock"}[playlist.privacy.value] %>"></i> - <select name="privacy"> - <% {"Public", "Unlisted", "Private"}.each do |option| %> - <option value="<%= option %>" <% if option == playlist.privacy.to_s %>selected<% end %>><%= translate(locale, option) %></option> - <% end %> - </select> </b> - </div> - <div class="pure-u-1-3" style="text-align:right"> - <h3> - <div class="pure-g user-field"> - <div class="pure-u-1-3"> - <a href="javascript:void(0)"> - <button type="submit" style="all:unset"> - <i class="icon ion-md-save"></i> - </button> - </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="/feed/playlist/<%= plid %>"><i class="icon ion-logo-rss"></i></a></div> - </div> - </h3> + <select name="privacy"> + <%- {"Public", "Unlisted", "Private"}.each do |option| -%> + <option value="<%= option %>" <% if option == playlist.privacy.to_s %>selected<% end %>><%= translate(locale, option) %></option> + <%- end -%> + </select> </div> </div> @@ -44,40 +52,9 @@ <input type="hidden" name="csrf_token" value="<%= HTML.escape(csrf_token) %>"> </form> -<% if playlist.is_a?(InvidiousPlaylist) && playlist.author == user.try &.email %> -<div class="h-box" style="text-align:right"> - <h3> - <a href="/add_playlist_items?list=<%= plid %>"><i class="icon ion-md-add"></i></a> - </h3> -</div> -<% end %> - <div class="h-box"> <hr> </div> -<div class="pure-g"> -<% videos.each do |item| %> - <%= rendered "components/item" %> -<% end %> -</div> - -<script src="/js/watched_indicator.js"></script> -<div class="pure-g h-box"> - <div class="pure-u-1 pure-u-lg-1-5"> - <% if page > 1 %> - <a href="/playlist?list=<%= playlist.id %>&page=<%= page - 1 %>"> - <%= translate(locale, "Previous page") %> - </a> - <% end %> - </div> - <div class="pure-u-1 pure-u-lg-3-5"></div> - <div class="pure-u-1 pure-u-lg-1-5" style="text-align:right"> - <% if videos.size == 100 %> - <a href="/playlist?list=<%= playlist.id %>&page=<%= page + 1 %>"> - <%= translate(locale, "Next page") %> - </a> - <% end %> - </div> -</div> +<%= rendered "components/items_paginated" %> diff --git a/src/invidious/views/feeds/history.ecr b/src/invidious/views/feeds/history.ecr index 2234b297..bda4e1f3 100644 --- a/src/invidious/views/feeds/history.ecr +++ b/src/invidious/views/feeds/history.ecr @@ -31,39 +31,29 @@ <% watched.each do |item| %> <div class="pure-u-1 pure-u-md-1-4"> <div class="h-box"> - <a style="width:100%" href="/watch?v=<%= item %>"> - <% if !env.get("preferences").as(Preferences).thin_mode %> - <div class="thumbnail"> - <img class="thumbnail" src="/vi/<%= item %>/mqdefault.jpg" alt="" /> - <form data-onsubmit="return_false" action="/watch_ajax?action_mark_unwatched=1&id=<%= item %>&referer=<%= env.get("current_page") %>" method="post"> - <input type="hidden" name="csrf_token" value="<%= URI.encode_www_form(env.get?("csrf_token").try &.as(String) || "") %>"> - <p class="watched"> - <button type="submit" style="all:unset" data-onclick="mark_unwatched" data-id="<%= item %>"><i class="icon ion-md-trash"></i></button> - </p> - </form> - </div> - <p></p> - <% end %> - </a> + <div class="thumbnail"> + <a style="width:100%" href="/watch?v=<%= item %>"> + <img class="thumbnail" src="/vi/<%= item %>/mqdefault.jpg" alt="" /> + </a> + + <div class="top-left-overlay"><div class="watched"> + <form data-onsubmit="return_false" action="/watch_ajax?action_mark_unwatched=1&id=<%= item %>&referer=<%= env.get("current_page") %>" method="post"> + <input type="hidden" name="csrf_token" value="<%= URI.encode_www_form(env.get?("csrf_token").try &.as(String) || "") %>"> + <button type="submit" class="pure-button pure-button-secondary low-profile" + data-onclick="mark_unwatched" data-id="<%= item %>"><i class="icon ion-md-trash"></i></button> + </form> + </div></div> + </div> + <p></p> </div> </div> <% end %> </div> -<div class="pure-g h-box"> - <div class="pure-u-1 pure-u-lg-1-5"> - <% if page > 1 %> - <a href="/feed/history?page=<%= page - 1 %><% if env.params.query["max_results"]? %>&max_results=<%= max_results %><% end %>"> - <%= translate(locale, "Previous page") %> - </a> - <% end %> - </div> - <div class="pure-u-1 pure-u-lg-3-5"></div> - <div class="pure-u-1 pure-u-lg-1-5" style="text-align:right"> - <% if watched.size >= max_results %> - <a href="/feed/history?page=<%= page + 1 %><% if env.params.query["max_results"]? %>&max_results=<%= max_results %><% end %>"> - <%= translate(locale, "Next page") %> - </a> - <% end %> - </div> -</div> +<%= + IV::Frontend::Pagination.nav_numeric(locale, + base_url: base_url, + current_page: page, + show_next: (watched.size >= max_results) + ) +%> diff --git a/src/invidious/views/feeds/subscriptions.ecr b/src/invidious/views/feeds/subscriptions.ecr index 9c69c5b0..c36bd00f 100644 --- a/src/invidious/views/feeds/subscriptions.ecr +++ b/src/invidious/views/feeds/subscriptions.ecr @@ -56,6 +56,7 @@ </script> <script src="/js/watched_widget.js"></script> + <div class="pure-g"> <% videos.each do |item| %> <%= rendered "components/item" %> @@ -64,20 +65,10 @@ <script src="/js/watched_indicator.js"></script> -<div class="pure-g h-box"> - <div class="pure-u-1 pure-u-lg-1-5"> - <% if page > 1 %> - <a href="/feed/subscriptions?page=<%= page - 1 %><% if env.params.query["max_results"]? %>&max_results=<%= max_results %><% end %>"> - <%= translate(locale, "Previous page") %> - </a> - <% end %> - </div> - <div class="pure-u-1 pure-u-lg-3-5"></div> - <div class="pure-u-1 pure-u-lg-1-5" style="text-align:right"> - <% if (videos.size + notifications.size) == max_results %> - <a href="/feed/subscriptions?page=<%= page + 1 %><% if env.params.query["max_results"]? %>&max_results=<%= max_results %><% end %>"> - <%= translate(locale, "Next page") %> - </a> - <% end %> - </div> -</div> +<%= + IV::Frontend::Pagination.nav_numeric(locale, + base_url: base_url, + current_page: page, + show_next: ((videos.size + notifications.size) == max_results) + ) +%> diff --git a/src/invidious/views/hashtag.ecr b/src/invidious/views/hashtag.ecr index 3351c21c..2000337e 100644 --- a/src/invidious/views/hashtag.ecr +++ b/src/invidious/views/hashtag.ecr @@ -4,38 +4,5 @@ <hr/> -<div class="pure-g h-box v-box"> - <div class="pure-u-1 pure-u-lg-1-5"> - <%- if page > 1 -%> - <a href="<%= url_prev_page %>"><%= translate(locale, "Previous page") %></a> - <%- end -%> - </div> - <div class="pure-u-1 pure-u-lg-3-5"></div> - <div class="pure-u-1 pure-u-lg-1-5" style="text-align:right"> - <%- if videos.size >= 60 -%> - <a href="<%= url_next_page %>"><%= translate(locale, "Next page") %></a> - <%- end -%> - </div> -</div> -<div class="pure-g"> - <%- videos.each do |item| -%> - <%= rendered "components/item" %> - <%- end -%> -</div> - -<script src="/js/watched_indicator.js"></script> - -<div class="pure-g h-box"> - <div class="pure-u-1 pure-u-lg-1-5"> - <%- if page > 1 -%> - <a href="<%= url_prev_page %>"><%= translate(locale, "Previous page") %></a> - <%- end -%> - </div> - <div class="pure-u-1 pure-u-lg-3-5"></div> - <div class="pure-u-1 pure-u-lg-1-5" style="text-align:right"> - <%- if videos.size >= 60 -%> - <a href="<%= url_next_page %>"><%= translate(locale, "Next page") %></a> - <%- end -%> - </div> -</div> +<%= rendered "components/items_paginated" %> diff --git a/src/invidious/views/playlist.ecr b/src/invidious/views/playlist.ecr index a04acf4c..ee9ba87b 100644 --- a/src/invidious/views/playlist.ecr +++ b/src/invidious/views/playlist.ecr @@ -6,9 +6,50 @@ <link rel="alternate" type="application/rss+xml" title="RSS" href="/feed/playlist/<%= plid %>" /> <% end %> -<div class="pure-g h-box"> - <div class="pure-u-2-3"> - <h3><%= title %></h3> +<div class="h-box flexible title"> + <div class="flex-left"><h3><%= title %></h3></div> + + <div class="flex-right button-container"> + <%- if playlist.is_a?(InvidiousPlaylist) && playlist.author == user.try &.email -%> + <div class="pure-u"> + <a class="pure-button pure-button-secondary low-profile" dir="auto" href="/add_playlist_items?list=<%= plid %>"> + <i class="icon ion-md-add"></i> <%= translate(locale, "playlist_button_add_items") %> + </a> + </div> + <div class="pure-u"> + <a class="pure-button pure-button-secondary low-profile" dir="auto" href="/edit_playlist?list=<%= plid %>"> + <i class="icon ion-md-create"></i> <%= translate(locale, "generic_button_edit") %> + </a> + </div> + <div class="pure-u"> + <a class="pure-button pure-button-secondary low-profile" dir="auto" href="/delete_playlist?list=<%= plid %>"> + <i class="icon ion-md-trash"></i> <%= translate(locale, "generic_button_delete") %> + </a> + </div> + <%- else -%> + <div class="pure-u"> + <%- if IV::Database::Playlists.exists?(playlist.id) -%> + <a class="pure-button pure-button-secondary low-profile" dir="auto" href="/subscribe_playlist?list=<%= plid %>"> + <i class="icon ion-md-add"></i> <%= translate(locale, "Subscribe") %> + </a> + <%- else -%> + <a class="pure-button pure-button-secondary low-profile" dir="auto" href="/delete_playlist?list=<%= plid %>"> + <i class="icon ion-md-trash"></i> <%= translate(locale, "Unsubscribe") %> + </a> + <%- end -%> + </div> + <%- end -%> + + <div class="pure-u"> + <a class="pure-button pure-button-secondary low-profile" dir="auto" href="/feed/playlist/<%= plid %>"> + <i class="icon ion-logo-rss"></i> <%= translate(locale, "generic_button_rss") %> + </a> + </div> + </div> +</div> + +<div class="h-box"> + <div class="pure-u-1-1"> <% if playlist.is_a? InvidiousPlaylist %> <b> <% if playlist.author == user.try &.email %> @@ -54,37 +95,12 @@ </div> <% end %> </div> - <div class="pure-u-1-3" style="text-align:right"> - <h3> - <div class="pure-g user-field"> - <% if playlist.is_a?(InvidiousPlaylist) && playlist.author == user.try &.email %> - <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> - <% else %> - <% 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> - <% else %> - <div class="pure-u-1-3"><a href="/delete_playlist?list=<%= plid %>"><i class="icon ion-md-trash"></i></a></div> - <% end %> - <% end %> - <div class="pure-u-1-3"><a href="/feed/playlist/<%= plid %>"><i class="icon ion-logo-rss"></i></a></div> - </div> - </h3> - </div> </div> <div class="h-box"> <div id="descriptionWrapper"><%= playlist.description_html %></div> </div> -<% if playlist.is_a?(InvidiousPlaylist) && playlist.author == user.try &.email %> -<div class="h-box" style="text-align:right"> - <h3> - <a href="/add_playlist_items?list=<%= plid %>"><i class="icon ion-md-add"></i></a> - </h3> -</div> -<% end %> - <div class="h-box"> <hr> </div> @@ -100,28 +116,5 @@ <script src="/js/playlist_widget.js?v=<%= ASSET_COMMIT %>"></script> <% end %> -<div class="pure-g"> -<% videos.each do |item| %> - <%= rendered "components/item" %> -<% end %> -</div> - -<script src="/js/watched_indicator.js"></script> -<div class="pure-g h-box"> - <div class="pure-u-1 pure-u-lg-1-5"> - <% if page > 1 %> - <a href="/playlist?list=<%= playlist.id %>&page=<%= page - 1 %>"> - <%= translate(locale, "Previous page") %> - </a> - <% end %> - </div> - <div class="pure-u-1 pure-u-lg-3-5"></div> - <div class="pure-u-1 pure-u-lg-1-5" style="text-align:right"> - <% if page_count != 1 && page < page_count %> - <a href="/playlist?list=<%= playlist.id %>&page=<%= page + 1 %>"> - <%= translate(locale, "Next page") %> - </a> - <% end %> - </div> -</div> +<%= rendered "components/items_paginated" %> diff --git a/src/invidious/views/privacy.ecr b/src/invidious/views/privacy.ecr index 643f880b..bc5ff40b 100644 --- a/src/invidious/views/privacy.ecr +++ b/src/invidious/views/privacy.ecr @@ -16,12 +16,11 @@ <li>a list of channel UCIDs the user is subscribed to</li> <li>a user ID (for persistent storage of subscriptions and preferences)</li> <li>a json object containing user preferences</li> - <li>a hashed password if applicable (not present on google accounts)</li> + <li>a hashed password</li> <li>a randomly generated token for providing an RSS feed of a user's subscriptions</li> <li>a list of video IDs identifying watched videos</li> </ul> <p>Users can clear their watch history using the <a href="/clear_watch_history">clear watch history</a> page.</p> - <p>If a user is logged in with a Google account, no password will ever be stored. This website uses the session token provided by Google to identify a user, but does not store the information required to make requests on a user's behalf without their knowledge or consent.</p> <h3>Data you passively provide</h3> <p>When you request any resource from this website (for example: a page, a font, an image, or an API endpoint) information about the request may be logged.</p> diff --git a/src/invidious/views/search.ecr b/src/invidious/views/search.ecr index a7469e36..b1300214 100644 --- a/src/invidious/views/search.ecr +++ b/src/invidious/views/search.ecr @@ -7,21 +7,8 @@ <%= Invidious::Frontend::SearchFilters.generate(query.filters, query.text, query.page, locale) %> <hr/> -<div class="pure-g h-box v-box"> - <div class="pure-u-1 pure-u-lg-1-5"> - <%- if query.page > 1 -%> - <a href="<%= url_prev_page %>"><%= translate(locale, "Previous page") %></a> - <%- end -%> - </div> - <div class="pure-u-1 pure-u-lg-3-5"></div> - <div class="pure-u-1 pure-u-lg-1-5" style="text-align:right"> - <%- if videos.size >= 20 -%> - <a href="<%= url_next_page %>"><%= translate(locale, "Next page") %></a> - <%- end -%> - </div> -</div> -<%- if videos.empty? -%> +<%- if items.empty? -%> <div class="h-box no-results-error"> <div> <%= translate(locale, "search_message_no_results") %><br/><br/> @@ -30,25 +17,5 @@ </div> </div> <%- else -%> -<div class="pure-g"> - <%- videos.each do |item| -%> - <%= rendered "components/item" %> - <%- end -%> -</div> + <%= rendered "components/items_paginated" %> <%- end -%> - -<script src="/js/watched_indicator.js"></script> - -<div class="pure-g h-box"> - <div class="pure-u-1 pure-u-lg-1-5"> - <%- if query.page > 1 -%> - <a href="<%= url_prev_page %>"><%= translate(locale, "Previous page") %></a> - <%- end -%> - </div> - <div class="pure-u-1 pure-u-lg-3-5"></div> - <div class="pure-u-1 pure-u-lg-1-5" style="text-align:right"> - <%- if videos.size >= 20 -%> - <a href="<%= url_next_page %>"><%= translate(locale, "Next page") %></a> - <%- end -%> - </div> -</div> diff --git a/src/invidious/views/template.ecr b/src/invidious/views/template.ecr index aa0fc15f..77265679 100644 --- a/src/invidious/views/template.ecr +++ b/src/invidious/views/template.ecr @@ -111,14 +111,6 @@ </div> <% end %> - <% if env.get? "user" %> - <% if !HMAC_KEY_CONFIGURED && CONFIG.admins.includes? env.get("user").as(Invidious::User).email %> - <div class="h-box"> - <h3><p>Message for admin: please configure hmac_key, <a href="https://github.com/iv-org/invidious/issues/3854">see more here</a>.</p></h3> - </div> - <% end %> - <% end %> - <%= content %> <footer> diff --git a/src/invidious/views/user/login.ecr b/src/invidious/views/user/login.ecr index 01d7a210..2b03d280 100644 --- a/src/invidious/views/user/login.ecr +++ b/src/invidious/views/user/login.ecr @@ -7,42 +7,6 @@ <div class="pure-u-1 pure-u-lg-3-5"> <div class="h-box"> <% case account_type when %> - <% when "google" %> - <form class="pure-form pure-form-stacked" action="/login?referer=<%= URI.encode_www_form(referer) %>&type=google" method="post"> - <fieldset> - <% if email %> - <input name="email" type="hidden" value="<%= HTML.escape(email) %>"> - <% else %> - <label for="email"><%= translate(locale, "E-mail") %> :</label> - <input required class="pure-input-1" name="email" type="email" placeholder="<%= translate(locale, "E-mail") %>"> - <% end %> - - <% if password %> - <input name="password" type="hidden" value="<%= HTML.escape(password) %>"> - <% else %> - <label for="password"><%= translate(locale, "Password") %> :</label> - <input required class="pure-input-1" name="password" type="password" placeholder="<%= translate(locale, "Password") %>"> - <% end %> - - <% if prompt %> - <label for="tfa"><%= translate(locale, prompt) %> :</label> - <input required class="pure-input-1" name="tfa" type="text" placeholder="<%= translate(locale, prompt) %>"> - <% end %> - - <% if tfa %> - <input type="hidden" name="tfa" value="<%= tfa %>"> - <% end %> - - <% if captcha %> - <img style="width:50%" src="/Captcha?v=2&ctoken=<%= captcha[:tokens][0] %>"/> - <input type="hidden" name="token" value="<%= captcha[:tokens][0] %>"> - <label for="answer"><%= translate(locale, "Answer") %> :</label> - <input type="text" name="answer" type="text" placeholder="<%= translate(locale, "Answer") %>"> - <% end %> - - <button type="submit" class="pure-button pure-button-primary"><%= translate(locale, "Sign In") %></button> - </fieldset> - </form> <% else # "invidious" %> <form class="pure-form pure-form-stacked" action="/login?referer=<%= URI.encode_www_form(referer) %>&type=invidious" method="post"> <fieldset> diff --git a/src/invidious/views/watch.ecr b/src/invidious/views/watch.ecr index 5b3190f3..498d57a1 100644 --- a/src/invidious/views/watch.ecr +++ b/src/invidious/views/watch.ecr @@ -204,19 +204,28 @@ we're going to need to do it here in order to allow for translations. </div> <div class="pure-u-1 <% if params.related_videos || plid %>pure-u-lg-3-5<% else %>pure-u-md-4-5<% end %>"> - <div class="h-box"> - <a href="/channel/<%= video.ucid %>" style="display:block;width:fit-content;width:-moz-fit-content"> - <div class="channel-profile"> - <% if !video.author_thumbnail.empty? %> - <img src="/ggpht<%= URI.parse(video.author_thumbnail).request_target %>" alt="" /> - <% end %> - <span id="channel-name"><%= author %><% if !video.author_verified.nil? && video.author_verified %> <i class="icon ion ion-md-checkmark-circle"></i><% end %></span> - </div> - </a> - <% sub_count_text = video.sub_count_text %> - <%= rendered "components/subscribe_widget" %> + <div class="pure-g h-box flexible title"> + <div class="pure-u-1-2 flex-left flexible"> + <a href="/channel/<%= video.ucid %>"> + <div class="channel-profile"> + <% if !video.author_thumbnail.empty? %> + <img src="/ggpht<%= URI.parse(video.author_thumbnail).request_target %>" alt="" /> + <% end %> + <span id="channel-name"><%= author %><% if !video.author_verified.nil? && video.author_verified %> <i class="icon ion ion-md-checkmark-circle"></i><% end %></span> + </div> + </a> + </div> + <div class="pure-u-1-2 flex-right flexible button-container"> + <div class="pure-u"> + <% sub_count_text = video.sub_count_text %> + <%= rendered "components/subscribe_widget" %> + </div> + </div> + </div> + + <div class="h-box"> <p id="published-date"> <% if video.premiere_timestamp.try &.> Time.utc %> <b><%= video.premiere_timestamp.try { |t| translate(locale, "Premieres `x`", t.to_s("%B %-d, %R UTC")) } %></b> @@ -295,15 +304,28 @@ we're going to need to do it here in order to allow for translations. <% video.related_videos.each do |rv| %> <% if rv["id"]? %> - <a href="/watch?v=<%= rv["id"] %>&listen=<%= params.listen %>"> - <% if !env.get("preferences").as(Preferences).thin_mode %> - <div class="thumbnail"> + <div class="pure-u-1"> + + <div class="thumbnail"> + <%- if !env.get("preferences").as(Preferences).thin_mode -%> + <a tabindex="-1" href="/watch?v=<%= rv["id"] %>&listen=<%= params.listen %>"> <img loading="lazy" class="thumbnail" src="/vi/<%= rv["id"] %>/mqdefault.jpg" alt="" /> - <p class="length"><%= recode_length_seconds(rv["length_seconds"]?.try &.to_i? || 0) %></p> - </div> - <% end %> - <p style="width:100%"><%= rv["title"] %></p> - </a> + </a> + <%- else -%> + <div class="thumbnail-placeholder"></div> + <%- end -%> + + <div class="bottom-right-overlay"> + <%- if (length_seconds = rv["length_seconds"]?.try &.to_i?) && length_seconds != 0 -%> + <p class="length"><%= recode_length_seconds(length_seconds) %></p> + <%- end -%> + </div> + </div> + + <div class="video-card-row"> + <a href="/watch?v=<%= rv["id"] %>&listen=<%= params.listen %>"><p dir="auto"><%= HTML.escape(rv["title"]) %></p></a> + </div> + <h5 class="pure-g"> <div class="pure-u-14-24"> <% if rv["ucid"]? %> @@ -321,6 +343,8 @@ we're going to need to do it here in order to allow for translations. %></b> </div> </h5> + + </div> <% end %> <% end %> </div> diff --git a/src/invidious/yt_backend/connection_pool.cr b/src/invidious/yt_backend/connection_pool.cr index 46e5bf85..658731cf 100644 --- a/src/invidious/yt_backend/connection_pool.cr +++ b/src/invidious/yt_backend/connection_pool.cr @@ -8,13 +8,15 @@ def add_yt_headers(request) if request.headers["User-Agent"] == "Crystal" - request.headers["User-Agent"] ||= "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.0.0 Safari/537.36" + request.headers["User-Agent"] ||= "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36" end + request.headers["Accept-Charset"] ||= "ISO-8859-1,utf-8;q=0.7,*;q=0.7" request.headers["Accept"] ||= "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8" request.headers["Accept-Language"] ||= "en-us,en;q=0.5" + # Preserve original cookies and add new YT consent cookie for EU servers - request.headers["Cookie"] = "#{request.headers["cookie"]?}; CONSENT=YES+" + request.headers["Cookie"] = "#{request.headers["cookie"]?}; CONSENT=PENDING+#{Random.rand(100..999)}" if !CONFIG.cookies.empty? request.headers["Cookie"] = "#{(CONFIG.cookies.map { |c| "#{c.name}=#{c.value}" }).join("; ")}; #{request.headers["cookie"]?}" end diff --git a/src/invidious/yt_backend/extractors.cr b/src/invidious/yt_backend/extractors.cr index 6686e6e7..e5029dc5 100644 --- a/src/invidious/yt_backend/extractors.cr +++ b/src/invidious/yt_backend/extractors.cr @@ -408,8 +408,8 @@ private module Parsers # Returns nil when the given object isn't a RichItemRenderer # # A richItemRenderer seems to be a simple wrapper for a videoRenderer, used - # by the result page for hashtags. It is located inside a continuationItems - # container. + # by the result page for hashtags and for the podcast tab on channels. + # It is located inside a continuationItems container for hashtags. # module RichItemRendererParser def self.process(item : JSON::Any, author_fallback : AuthorFallback) @@ -421,6 +421,7 @@ private module Parsers private def self.parse(item_contents, author_fallback) child = VideoRendererParser.process(item_contents, author_fallback) child ||= ReelItemRendererParser.process(item_contents, author_fallback) + child ||= PlaylistRendererParser.process(item_contents, author_fallback) return child end diff --git a/src/invidious/yt_backend/youtube_api.cr b/src/invidious/yt_backend/youtube_api.cr index 91a9332c..3dd9e9d8 100644 --- a/src/invidious/yt_backend/youtube_api.cr +++ b/src/invidious/yt_backend/youtube_api.cr @@ -7,16 +7,18 @@ module YoutubeAPI private DEFAULT_API_KEY = "AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8" - private ANDROID_APP_VERSION = "17.33.42" + private ANDROID_APP_VERSION = "18.20.38" # github.com/TeamNewPipe/NewPipeExtractor/blob/943b7c033bb9d07ead63ddab4441c287653e4384/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/YoutubeParsingHelper.java#L1308 - private ANDROID_USER_AGENT = "com.google.android.youtube/17.33.42 (Linux; U; Android 12; US) gzip" + private ANDROID_USER_AGENT = "com.google.android.youtube/18.20.38 (Linux; U; Android 12; US) gzip" private ANDROID_SDK_VERSION = 31_i64 private ANDROID_VERSION = "12" - private IOS_APP_VERSION = "17.33.2" + + private IOS_APP_VERSION = "18.21.3" # github.com/TeamNewPipe/NewPipeExtractor/blob/943b7c033bb9d07ead63ddab4441c287653e4384/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/YoutubeParsingHelper.java#L1330 - private IOS_USER_AGENT = "com.google.ios.youtube/17.33.2 (iPhone14,5; U; CPU iOS 15_6 like Mac OS X;)" + private IOS_USER_AGENT = "com.google.ios.youtube/18.21.3 (iPhone14,5; U; CPU iOS 15_6 like Mac OS X;)" # github.com/TeamNewPipe/NewPipeExtractor/blob/943b7c033bb9d07ead63ddab4441c287653e4384/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/YoutubeParsingHelper.java#L1224 - private IOS_VERSION = "15.6.0.19G71" + private IOS_VERSION = "15.6.0.19G71" + private WINDOWS_VERSION = "10.0" # Enumerate used to select one of the clients supported by the API @@ -43,7 +45,7 @@ module YoutubeAPI ClientType::Web => { name: "WEB", name_proto: "1", - version: "2.20221118.01.00", + version: "2.20230602.01.00", api_key: DEFAULT_API_KEY, screen: "WATCH_FULL_SCREEN", os_name: "Windows", @@ -63,7 +65,7 @@ module YoutubeAPI ClientType::WebMobile => { name: "MWEB", name_proto: "2", - version: "2.20220805.01.00", + version: "2.20230531.05.00", api_key: DEFAULT_API_KEY, os_name: "Android", os_version: ANDROID_VERSION, |
