diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/invidious.cr | 51 | ||||
| -rw-r--r-- | src/invidious/channels.cr | 175 | ||||
| -rw-r--r-- | src/invidious/helpers/helpers.cr | 3 | ||||
| -rw-r--r-- | src/invidious/helpers/i18n.cr | 30 | ||||
| -rw-r--r-- | src/invidious/helpers/macros.cr | 3 | ||||
| -rw-r--r-- | src/invidious/helpers/utils.cr | 20 | ||||
| -rw-r--r-- | src/invidious/jobs/statistics_refresh_job.cr | 2 | ||||
| -rw-r--r-- | src/invidious/playlists.cr | 3 | ||||
| -rw-r--r-- | src/invidious/routes/misc.cr | 2 | ||||
| -rw-r--r-- | src/invidious/routes/playlists.cr | 2 | ||||
| -rw-r--r-- | src/invidious/routes/watch.cr | 9 | ||||
| -rw-r--r-- | src/invidious/search.cr | 51 | ||||
| -rw-r--r-- | src/invidious/trending.cr | 24 | ||||
| -rw-r--r-- | src/invidious/users.cr | 2 | ||||
| -rw-r--r-- | src/invidious/videos.cr | 18 | ||||
| -rw-r--r-- | src/invidious/views/empty.ecr | 8 | ||||
| -rw-r--r-- | src/invidious/views/preferences.ecr | 4 | ||||
| -rw-r--r-- | src/invidious/views/search_homepage.ecr | 24 | ||||
| -rw-r--r-- | src/invidious/views/template.ecr | 29 | ||||
| -rw-r--r-- | src/invidious/views/trending.ecr | 2 |
20 files changed, 273 insertions, 189 deletions
diff --git a/src/invidious.cr b/src/invidious.cr index 8d579f92..ae20e13e 100644 --- a/src/invidious.cr +++ b/src/invidious.cr @@ -65,37 +65,7 @@ SOFTWARE = { "branch" => "#{CURRENT_BRANCH}", } -LOCALES = { - "ar" => load_locale("ar"), - "de" => load_locale("de"), - "el" => load_locale("el"), - "en-US" => load_locale("en-US"), - "eo" => load_locale("eo"), - "es" => load_locale("es"), - "fa" => load_locale("fa"), - "fi" => load_locale("fi"), - "fr" => load_locale("fr"), - "he" => load_locale("he"), - "hr" => load_locale("hr"), - "id" => load_locale("id"), - "is" => load_locale("is"), - "it" => load_locale("it"), - "ja" => load_locale("ja"), - "nb-NO" => load_locale("nb-NO"), - "nl" => load_locale("nl"), - "pl" => load_locale("pl"), - "pt-BR" => load_locale("pt-BR"), - "pt-PT" => load_locale("pt-PT"), - "ro" => load_locale("ro"), - "ru" => load_locale("ru"), - "sv" => load_locale("sv-SE"), - "tr" => load_locale("tr"), - "uk" => load_locale("uk"), - "zh-CN" => load_locale("zh-CN"), - "zh-TW" => load_locale("zh-TW"), -} - -YT_POOL = QUICPool.new(YT_URL, capacity: CONFIG.pool_size, timeout: 2.0) +YT_POOL = YoutubeConnectionPool.new(YT_URL, capacity: CONFIG.pool_size, timeout: 2.0, use_quic: CONFIG.use_quic) # CLI Kemal.config.extra_options do |parser| @@ -308,9 +278,17 @@ end Invidious::Routing.get "/", Invidious::Routes::Misc, :home Invidious::Routing.get "/privacy", Invidious::Routes::Misc, :privacy Invidious::Routing.get "/licenses", Invidious::Routes::Misc, :licenses -Invidious::Routing.get "/watch", Invidious::Routes::Watch + +Invidious::Routing.get "/watch", Invidious::Routes::Watch, :handle +Invidious::Routing.get "/watch/:id", Invidious::Routes::Watch, :redirect +Invidious::Routing.get "/shorts/:id", Invidious::Routes::Watch, :redirect +Invidious::Routing.get "/w/:id", Invidious::Routes::Watch, :redirect +Invidious::Routing.get "/v/:id", Invidious::Routes::Watch, :redirect +Invidious::Routing.get "/e/:id", Invidious::Routes::Watch, :redirect + Invidious::Routing.get "/embed/", Invidious::Routes::Embed, :redirect Invidious::Routing.get "/embed/:id", Invidious::Routes::Embed, :show + Invidious::Routing.get "/view_all_playlists", Invidious::Routes::Playlists, :index Invidious::Routing.get "/create_playlist", Invidious::Routes::Playlists, :new Invidious::Routing.post "/create_playlist", Invidious::Routes::Playlists, :create @@ -323,12 +301,15 @@ Invidious::Routing.get "/add_playlist_items", Invidious::Routes::Playlists, :add Invidious::Routing.post "/playlist_ajax", Invidious::Routes::Playlists, :playlist_ajax Invidious::Routing.get "/playlist", Invidious::Routes::Playlists, :show Invidious::Routing.get "/mix", Invidious::Routes::Playlists, :mix + Invidious::Routing.get "/opensearch.xml", Invidious::Routes::Search, :opensearch Invidious::Routing.get "/results", Invidious::Routes::Search, :results Invidious::Routing.get "/search", Invidious::Routes::Search, :search + Invidious::Routing.get "/login", Invidious::Routes::Login, :login_page Invidious::Routing.post "/login", Invidious::Routes::Login, :login Invidious::Routing.post "/signout", Invidious::Routes::Login, :signout + Invidious::Routing.get "/preferences", Invidious::Routes::PreferencesRoute, :show Invidious::Routing.post "/preferences", Invidious::Routes::PreferencesRoute, :update Invidious::Routing.get "/toggle_theme", Invidious::Routes::PreferencesRoute, :toggle_theme @@ -1699,7 +1680,7 @@ get "/channel/:ucid" do |env| sort_options = {"last", "oldest", "newest"} sort_by ||= "last" - items, continuation = fetch_channel_playlists(channel.ucid, channel.author, channel.auto_generated, continuation, sort_by) + items, continuation = fetch_channel_playlists(channel.ucid, channel.author, continuation, sort_by) items.uniq! do |item| if item.responds_to?(:title) item.title @@ -1766,7 +1747,7 @@ get "/channel/:ucid/playlists" do |env| next env.redirect "/channel/#{channel.ucid}" end - items, continuation = fetch_channel_playlists(channel.ucid, channel.author, channel.auto_generated, continuation, sort_by) + items, continuation = fetch_channel_playlists(channel.ucid, channel.author, continuation, sort_by) items = items.select { |item| item.is_a?(SearchPlaylist) }.map { |item| item.as(SearchPlaylist) } items.each { |item| item.author = "" } @@ -2467,7 +2448,7 @@ end next error_json(500, ex) end - items, continuation = fetch_channel_playlists(channel.ucid, channel.author, channel.auto_generated, continuation, sort_by) + items, continuation = fetch_channel_playlists(channel.ucid, channel.author, continuation, sort_by) JSON.build do |json| json.object do diff --git a/src/invidious/channels.cr b/src/invidious/channels.cr index 9a129e1e..3109b508 100644 --- a/src/invidious/channels.cr +++ b/src/invidious/channels.cr @@ -355,14 +355,22 @@ def fetch_channel(ucid, db, pull_all_videos = true, locale = nil) return channel end -def fetch_channel_playlists(ucid, author, auto_generated, continuation, sort_by) - if continuation || auto_generated - url = produce_channel_playlists_url(ucid, continuation, sort_by, auto_generated) - - response = YT_POOL.client &.get(url) +def fetch_channel_playlists(ucid, author, continuation, sort_by) + if continuation + response_json = request_youtube_api_browse(continuation) + result = JSON.parse(response_json) + continuationItems = result["onResponseReceivedActions"]? + .try &.[0]["appendContinuationItemsAction"]["continuationItems"] + + return [] of SearchItem, nil if !continuationItems + + items = [] of SearchItem + continuationItems.as_a.select(&.as_h.has_key?("gridPlaylistRenderer")).each { |item| + extract_item(item, author, ucid).try { |t| items << t } + } - continuation = response.body.match(/"continuation":"(?<continuation>[^"]+)"/).try &.["continuation"]? - initial_data = JSON.parse(response.body).as_a.find(&.["response"]?).try &.as_h + continuation = continuationItems.as_a.last["continuationItemRenderer"]? + .try &.["continuationEndpoint"]["continuationCommand"]["token"].as_s else url = "/channel/#{ucid}/playlists?flow=list&view=1" @@ -377,13 +385,12 @@ def fetch_channel_playlists(ucid, author, auto_generated, continuation, sort_by) end response = YT_POOL.client &.get(url) - continuation = response.body.match(/"continuation":"(?<continuation>[^"]+)"/).try &.["continuation"]? initial_data = extract_initial_data(response.body) - end + return [] of SearchItem, nil if !initial_data - return [] of SearchItem, nil if !initial_data - items = extract_items(initial_data) - continuation = extract_channel_playlists_cursor(continuation, auto_generated) if continuation + items = extract_items(initial_data, author, ucid) + continuation = response.body.match(/"token":"(?<continuation>[^"]+)"/).try &.["continuation"]? + end return items, continuation end @@ -453,6 +460,15 @@ def produce_channel_videos_url(ucid, page = 1, auto_generated = nil, sort_by = " return "/browse_ajax?continuation=#{continuation}&gl=US&hl=en" end +# ## NOTE: DEPRECATED +# Reason -> Unstable +# The Protobuf object must be provided with an id of the last playlist from the current "page" +# in order to fetch the next one accurately +# (if the id isn't included, entries shift around erratically between pages, +# leading to repetitions and skip overs) +# +# Since it's impossible to produce the appropriate Protobuf without an id being provided by the user, +# it's better to stick to continuation tokens provided by the first request and onward def produce_channel_playlists_url(ucid, cursor, sort = "newest", auto_generated = false) object = { "80226972:embedded" => { @@ -499,31 +515,6 @@ def produce_channel_playlists_url(ucid, cursor, sort = "newest", auto_generated return "/browse_ajax?continuation=#{continuation}&gl=US&hl=en" end -def extract_channel_playlists_cursor(cursor, auto_generated) - cursor = URI.decode_www_form(cursor) - .try { |i| Base64.decode(i) } - .try { |i| IO::Memory.new(i) } - .try { |i| Protodec::Any.parse(i) } - .try { |i| i["80226972:0:embedded"]["3:1:base64"].as_h.find { |k, v| k.starts_with? "15:" } } - .try &.[1] - - if cursor.try &.as_h? - cursor = cursor.try { |i| Protodec::Any.cast_json(i.as_h) } - .try { |i| Protodec::Any.from_json(i) } - .try { |i| Base64.urlsafe_encode(i) } - .try { |i| URI.encode_www_form(i) } || "" - else - cursor = cursor.try &.as_s || "" - end - - if !auto_generated - cursor = URI.decode_www_form(cursor) - .try { |i| Base64.decode_string(i) } - end - - return cursor -end - # TODO: Add "sort_by" def fetch_channel_community(ucid, continuation, locale, format, thin_mode) response = YT_POOL.client &.get("/channel/#{ucid}/community?gl=US&hl=en") @@ -830,63 +821,87 @@ def get_about_info(ucid, locale) raise ChannelRedirect.new(channel_id: browse_endpoint["browseId"].to_s) end - author = initdata["metadata"]["channelMetadataRenderer"]["title"].as_s - author_url = initdata["metadata"]["channelMetadataRenderer"]["channelUrl"].as_s - author_thumbnail = initdata["metadata"]["channelMetadataRenderer"]["avatar"]["thumbnails"][0]["url"].as_s + auto_generated = false + # Check for special auto generated gaming channels + if !initdata.has_key?("metadata") + auto_generated = true + end + + if auto_generated + author = initdata["header"]["interactiveTabbedHeaderRenderer"]["title"]["simpleText"].as_s + author_url = initdata["microformat"]["microformatDataRenderer"]["urlCanonical"].as_s + author_thumbnail = initdata["header"]["interactiveTabbedHeaderRenderer"]["boxArt"]["thumbnails"][0]["url"].as_s - ucid = initdata["metadata"]["channelMetadataRenderer"]["externalId"].as_s + # Raises a KeyError on failure. + banners = initdata["header"]["interactiveTabbedHeaderRenderer"]?.try &.["banner"]?.try &.["thumbnails"]? + banner = banners.try &.[-1]?.try &.["url"].as_s? - # Raises a KeyError on failure. - banners = initdata["header"]["c4TabbedHeaderRenderer"]?.try &.["banner"]?.try &.["thumbnails"]? - banner = banners.try &.[-1]?.try &.["url"].as_s? + description = initdata["header"]["interactiveTabbedHeaderRenderer"]["description"]["simpleText"].as_s + description_html = HTML.escape(description).gsub("\n", "<br>") - # if banner.includes? "channels/c4/default_banner" - # banner = nil - # end + paid = false + is_family_friendly = initdata["microformat"]["microformatDataRenderer"]["familySafe"].as_bool + allowed_regions = initdata["microformat"]["microformatDataRenderer"]["availableCountries"].as_a.map { |a| a.as_s } - description = initdata["metadata"]["channelMetadataRenderer"]?.try &.["description"]?.try &.as_s? || "" - description_html = HTML.escape(description).gsub("\n", "<br>") + related_channels = [] of AboutRelatedChannel + else + author = initdata["metadata"]["channelMetadataRenderer"]["title"].as_s + author_url = initdata["metadata"]["channelMetadataRenderer"]["channelUrl"].as_s + author_thumbnail = initdata["metadata"]["channelMetadataRenderer"]["avatar"]["thumbnails"][0]["url"].as_s - paid = about.xpath_node(%q(//meta[@itemprop="paid"])).not_nil!["content"] == "True" - is_family_friendly = about.xpath_node(%q(//meta[@itemprop="isFamilyFriendly"])).not_nil!["content"] == "True" - allowed_regions = about.xpath_node(%q(//meta[@itemprop="regionsAllowed"])).not_nil!["content"].split(",") + ucid = initdata["metadata"]["channelMetadataRenderer"]["externalId"].as_s - related_channels = initdata["contents"]["twoColumnBrowseResultsRenderer"] - .["secondaryContents"]?.try &.["browseSecondaryContentsRenderer"]["contents"][0]? - .try &.["verticalChannelSectionRenderer"]?.try &.["items"]?.try &.as_a.map do |node| - renderer = node["miniChannelRenderer"]? - related_id = renderer.try &.["channelId"]?.try &.as_s? - related_id ||= "" + # Raises a KeyError on failure. + banners = initdata["header"]["c4TabbedHeaderRenderer"]?.try &.["banner"]?.try &.["thumbnails"]? + banner = banners.try &.[-1]?.try &.["url"].as_s? - related_title = renderer.try &.["title"]?.try &.["simpleText"]?.try &.as_s? - related_title ||= "" + # if banner.includes? "channels/c4/default_banner" + # banner = nil + # end - related_author_url = renderer.try &.["navigationEndpoint"]?.try &.["commandMetadata"]?.try &.["webCommandMetadata"]? - .try &.["url"]?.try &.as_s? - related_author_url ||= "" + description = initdata["metadata"]["channelMetadataRenderer"]?.try &.["description"]?.try &.as_s? || "" + description_html = HTML.escape(description).gsub("\n", "<br>") - related_author_thumbnails = renderer.try &.["thumbnail"]?.try &.["thumbnails"]?.try &.as_a? - related_author_thumbnails ||= [] of JSON::Any + paid = about.xpath_node(%q(//meta[@itemprop="paid"])).not_nil!["content"] == "True" + is_family_friendly = about.xpath_node(%q(//meta[@itemprop="isFamilyFriendly"])).not_nil!["content"] == "True" + allowed_regions = about.xpath_node(%q(//meta[@itemprop="regionsAllowed"])).not_nil!["content"].split(",") - related_author_thumbnail = "" - if related_author_thumbnails.size > 0 - related_author_thumbnail = related_author_thumbnails[-1]["url"]?.try &.as_s? - related_author_thumbnail ||= "" - end + related_channels = initdata["contents"]["twoColumnBrowseResultsRenderer"] + .["secondaryContents"]?.try &.["browseSecondaryContentsRenderer"]["contents"][0]? + .try &.["verticalChannelSectionRenderer"]?.try &.["items"]?.try &.as_a.map do |node| + renderer = node["miniChannelRenderer"]? + related_id = renderer.try &.["channelId"]?.try &.as_s? + related_id ||= "" - AboutRelatedChannel.new({ - ucid: related_id, - author: related_title, - author_url: related_author_url, - author_thumbnail: related_author_thumbnail, - }) - end - related_channels ||= [] of AboutRelatedChannel + related_title = renderer.try &.["title"]?.try &.["simpleText"]?.try &.as_s? + related_title ||= "" + + related_author_url = renderer.try &.["navigationEndpoint"]?.try &.["commandMetadata"]?.try &.["webCommandMetadata"]? + .try &.["url"]?.try &.as_s? + related_author_url ||= "" + + related_author_thumbnails = renderer.try &.["thumbnail"]?.try &.["thumbnails"]?.try &.as_a? + related_author_thumbnails ||= [] of JSON::Any + + related_author_thumbnail = "" + if related_author_thumbnails.size > 0 + related_author_thumbnail = related_author_thumbnails[-1]["url"]?.try &.as_s? + related_author_thumbnail ||= "" + end + + AboutRelatedChannel.new({ + ucid: related_id, + author: related_title, + author_url: related_author_url, + author_thumbnail: related_author_thumbnail, + }) + end + related_channels ||= [] of AboutRelatedChannel + end total_views = 0_i64 joined = Time.unix(0) tabs = [] of String - auto_generated = false tabs_json = initdata["contents"]["twoColumnBrowseResultsRenderer"]["tabs"]?.try &.as_a? if !tabs_json.nil? @@ -904,7 +919,7 @@ def get_about_info(ucid, locale) joined = channel_about_meta["joinedDateText"]?.try &.["runs"]?.try &.as_a.reduce("") { |acc, node| acc + node["text"].as_s } .try { |text| Time.parse(text, "Joined %b %-d, %Y", Time::Location.local) } || Time.unix(0) - # Auto-generated channels + # Normal Auto-generated channels # https://support.google.com/youtube/answer/2579942 # For auto-generated channels, channel_about_meta only has ["description"]["simpleText"] and ["primaryLinks"][0]["title"]["simpleText"] if (channel_about_meta["primaryLinks"]?.try &.size || 0) == 1 && (channel_about_meta["primaryLinks"][0]?) && diff --git a/src/invidious/helpers/helpers.cr b/src/invidious/helpers/helpers.cr index 5e49afb7..e9cb3989 100644 --- a/src/invidious/helpers/helpers.cr +++ b/src/invidious/helpers/helpers.cr @@ -42,7 +42,7 @@ struct ConfigPreferences property player_style : String = "invidious" property quality : String = "hd720" property quality_dash : String = "auto" - property default_home : String = "Popular" + property default_home : String? = "Popular" property feed_menu : Array(String) = ["Popular", "Trending", "Subscriptions", "Playlists"] property related_videos : Bool = true property sort : String = "published" @@ -98,6 +98,7 @@ class Config property port : Int32 = 3000 # Port to listen for connections (overrided by command line argument) property host_binding : String = "0.0.0.0" # Host to bind (overrided by command line argument) property pool_size : Int32 = 100 # Pool size for HTTP requests to youtube.com and ytimg.com (each domain has a separate pool of `pool_size`) + property use_quic : Bool = true # Use quic transport for youtube api @[YAML::Field(converter: Preferences::StringToCookies)] property cookies : HTTP::Cookies = HTTP::Cookies.new # Saved cookies in "name1=value1; name2=value2..." format diff --git a/src/invidious/helpers/i18n.cr b/src/invidious/helpers/i18n.cr index 0faa2e32..45a3f1ae 100644 --- a/src/invidious/helpers/i18n.cr +++ b/src/invidious/helpers/i18n.cr @@ -1,3 +1,33 @@ +LOCALES = { + "ar" => load_locale("ar"), + "de" => load_locale("de"), + "el" => load_locale("el"), + "en-US" => load_locale("en-US"), + "eo" => load_locale("eo"), + "es" => load_locale("es"), + "fa" => load_locale("fa"), + "fi" => load_locale("fi"), + "fr" => load_locale("fr"), + "he" => load_locale("he"), + "hr" => load_locale("hr"), + "id" => load_locale("id"), + "is" => load_locale("is"), + "it" => load_locale("it"), + "ja" => load_locale("ja"), + "nb-NO" => load_locale("nb-NO"), + "nl" => load_locale("nl"), + "pl" => load_locale("pl"), + "pt-BR" => load_locale("pt-BR"), + "pt-PT" => load_locale("pt-PT"), + "ro" => load_locale("ro"), + "ru" => load_locale("ru"), + "sv" => load_locale("sv-SE"), + "tr" => load_locale("tr"), + "uk" => load_locale("uk"), + "zh-CN" => load_locale("zh-CN"), + "zh-TW" => load_locale("zh-TW"), +} + def load_locale(name) return JSON.parse(File.read("locales/#{name}.json")).as_h end diff --git a/src/invidious/helpers/macros.cr b/src/invidious/helpers/macros.cr index 8b74bc86..5d426a8b 100644 --- a/src/invidious/helpers/macros.cr +++ b/src/invidious/helpers/macros.cr @@ -48,7 +48,8 @@ module JSON::Serializable end end -macro templated(filename, template = "template") +macro templated(filename, template = "template", navbar_search = true) + navbar_search = {{navbar_search}} render "src/invidious/views/#{{{filename}}}.ecr", "src/invidious/views/#{{{template}}}.ecr" end diff --git a/src/invidious/helpers/utils.cr b/src/invidious/helpers/utils.cr index 2c95a373..10d4e6b6 100644 --- a/src/invidious/helpers/utils.cr +++ b/src/invidious/helpers/utils.cr @@ -9,20 +9,22 @@ def add_yt_headers(request) return if request.resource.starts_with? "/sorry/index" request.headers["x-youtube-client-name"] ||= "1" request.headers["x-youtube-client-version"] ||= "2.20200609" + # Preserve original cookies and add new YT consent cookie for EU servers + request.headers["cookie"] = "#{request.headers["cookie"]?}; CONSENT=YES+" if !CONFIG.cookies.empty? request.headers["cookie"] = "#{(CONFIG.cookies.map { |c| "#{c.name}=#{c.value}" }).join("; ")}; #{request.headers["cookie"]?}" end end -struct QUICPool +struct YoutubeConnectionPool property! url : URI property! capacity : Int32 property! timeout : Float64 - property pool : ConnectionPool(QUIC::Client) + property pool : ConnectionPool(QUIC::Client | HTTP::Client) - def initialize(url : URI, @capacity = 5, @timeout = 5.0) + def initialize(url : URI, @capacity = 5, @timeout = 5.0, use_quic = true) @url = url - @pool = build_pool + @pool = build_pool(use_quic) end def client(region = nil, &block) @@ -48,9 +50,13 @@ struct QUICPool response end - private def build_pool - ConnectionPool(QUIC::Client).new(capacity: capacity, timeout: timeout) do - conn = QUIC::Client.new(url) + private def build_pool(use_quic) + ConnectionPool(QUIC::Client | HTTP::Client).new(capacity: capacity, timeout: timeout) do + if use_quic + conn = QUIC::Client.new(url) + else + conn = HTTP::Client.new(url) + end conn.family = (url.host == "www.youtube.com") ? CONFIG.force_resolve : Socket::Family::INET conn.family = Socket::Family::INET if conn.family == Socket::Family::UNSPEC conn.before_request { |r| add_yt_headers(r) } if url.host == "www.youtube.com" diff --git a/src/invidious/jobs/statistics_refresh_job.cr b/src/invidious/jobs/statistics_refresh_job.cr index aa46fb0e..6569c0a1 100644 --- a/src/invidious/jobs/statistics_refresh_job.cr +++ b/src/invidious/jobs/statistics_refresh_job.cr @@ -42,7 +42,7 @@ class Invidious::Jobs::StatisticsRefreshJob < Invidious::Jobs::BaseJob "version" => @software_config["version"], "branch" => @software_config["branch"], } - STATISTICS["openRegistration"] = CONFIG.registration_enabled + STATISTICS["openRegistrations"] = CONFIG.registration_enabled end private def refresh_stats diff --git a/src/invidious/playlists.cr b/src/invidious/playlists.cr index 71f6a9b8..073a9986 100644 --- a/src/invidious/playlists.cr +++ b/src/invidious/playlists.cr @@ -437,7 +437,8 @@ end def get_playlist_videos(db, playlist, offset, locale = nil, continuation = nil) # Show empy playlist if requested page is out of range - if offset >= playlist.video_count + # (e.g, when a new playlist has been created, offset will be negative) + if offset >= playlist.video_count || offset < 0 return [] of PlaylistVideo end diff --git a/src/invidious/routes/misc.cr b/src/invidious/routes/misc.cr index bc009633..d32ba892 100644 --- a/src/invidious/routes/misc.cr +++ b/src/invidious/routes/misc.cr @@ -22,7 +22,7 @@ class Invidious::Routes::Misc < Invidious::Routes::BaseRoute env.redirect "/feed/popular" end else - templated "empty" + templated "search_homepage", navbar_search: false end end diff --git a/src/invidious/routes/playlists.cr b/src/invidious/routes/playlists.cr index 73c14155..1f7fa27d 100644 --- a/src/invidious/routes/playlists.cr +++ b/src/invidious/routes/playlists.cr @@ -434,7 +434,7 @@ class Invidious::Routes::Playlists < Invidious::Routes::BaseRoute end page_count = (playlist.video_count / 100).to_i - page_count = 1 if page_count == 0 + page_count += 1 if (playlist.video_count % 100) > 0 if page > page_count return env.redirect "/playlist?list=#{plid}&page=#{page_count}" diff --git a/src/invidious/routes/watch.cr b/src/invidious/routes/watch.cr index 8169e1ed..d0338882 100644 --- a/src/invidious/routes/watch.cr +++ b/src/invidious/routes/watch.cr @@ -187,4 +187,13 @@ class Invidious::Routes::Watch < Invidious::Routes::BaseRoute templated "watch" end + + def redirect(env) + url = "/watch?v=#{env.params.url["id"]}" + if env.params.query.size > 0 + url += "&#{env.params.query}" + end + + return env.redirect url + end end diff --git a/src/invidious/search.cr b/src/invidious/search.cr index cf8fd790..4b216613 100644 --- a/src/invidious/search.cr +++ b/src/invidious/search.cr @@ -231,20 +231,32 @@ end alias SearchItem = SearchVideo | SearchChannel | SearchPlaylist def channel_search(query, page, channel) - response = YT_POOL.client &.get("/channel/#{channel}?hl=en&gl=US") - response = YT_POOL.client &.get("/user/#{channel}?hl=en&gl=US") if response.headers["location"]? - response = YT_POOL.client &.get("/c/#{channel}?hl=en&gl=US") if response.headers["location"]? + response = YT_POOL.client &.get("/channel/#{channel}") + + if response.status_code == 404 + response = YT_POOL.client &.get("/user/#{channel}") + response = YT_POOL.client &.get("/c/#{channel}") if response.status_code == 404 + initial_data = extract_initial_data(response.body) + ucid = initial_data["header"]["c4TabbedHeaderRenderer"]?.try &.["channelId"].as_s? + raise InfoException.new("Impossible to extract channel ID from page") if !ucid + else + ucid = channel + end - ucid = response.body.match(/\\"channelId\\":\\"(?<ucid>[^\\]+)\\"/).try &.["ucid"]? + continuation = produce_channel_search_continuation(ucid, query, page) + response_json = request_youtube_api_browse(continuation) - return 0, [] of SearchItem if !ucid + result = JSON.parse(response_json) + continuationItems = result["onResponseReceivedActions"]? + .try &.[0]["appendContinuationItemsAction"]["continuationItems"] - url = produce_channel_search_url(ucid, query, page) - response = YT_POOL.client &.get(url) - initial_data = JSON.parse(response.body).as_a.find &.["response"]? - return 0, [] of SearchItem if !initial_data - author = initial_data["response"]?.try &.["metadata"]?.try &.["channelMetadataRenderer"]?.try &.["title"]?.try &.as_s - items = extract_items(initial_data.as_h, author, ucid) + return 0, [] of SearchItem if !continuationItems + + items = [] of SearchItem + continuationItems.as_a.select(&.as_h.has_key?("itemSectionRenderer")).each { |item| + extract_item(item["itemSectionRenderer"]["contents"].as_a[0]) + .try { |t| items << t } + } return items.size, items end @@ -361,17 +373,28 @@ def produce_search_params(page = 1, sort : String = "relevance", date : String = return params end -def produce_channel_search_url(ucid, query, page) +def produce_channel_search_continuation(ucid, query, page) + if page <= 1 + idx = 0_i64 + else + idx = 30_i64 * (page - 1) + end + object = { "80226972:embedded" => { "2:string" => ucid, "3:base64" => { "2:string" => "search", + "6:varint" => 1_i64, "7:varint" => 1_i64, - "15:string" => "#{page}", + "12:varint" => 1_i64, + "15:base64" => { + "3:varint" => idx, + }, "23:varint" => 0_i64, }, "11:string" => query, + "35:string" => "browse-feed#{ucid}search", }, } @@ -380,7 +403,7 @@ def produce_channel_search_url(ucid, query, page) .try { |i| Base64.urlsafe_encode(i) } .try { |i| URI.encode_www_form(i) } - return "/browse_ajax?continuation=#{continuation}&gl=US&hl=en" + return continuation end def process_search_query(query, page, user, region) diff --git a/src/invidious/trending.cr b/src/invidious/trending.cr index 8d078387..910a99d8 100644 --- a/src/invidious/trending.cr +++ b/src/invidious/trending.cr @@ -6,24 +6,22 @@ def fetch_trending(trending_type, region, locale) plid = nil if trending_type && trending_type != "Default" - trending_type = trending_type.downcase.capitalize + if trending_type == "Music" + trending_type = 1 + elsif trending_type == "Gaming" + trending_type = 2 + elsif trending_type == "Movies" + trending_type = 3 + end response = YT_POOL.client &.get("/feed/trending?gl=#{region}&hl=en").body initial_data = extract_initial_data(response) + url = initial_data["contents"]["twoColumnBrowseResultsRenderer"]["tabs"][trending_type]["tabRenderer"]["endpoint"]["commandMetadata"]["webCommandMetadata"]["url"] + url = "#{url}&gl=#{region}&hl=en" - tabs = initial_data["contents"]["twoColumnBrowseResultsRenderer"]["tabs"][0]["tabRenderer"]["content"]["sectionListRenderer"]["subMenu"]["channelListSubMenuRenderer"]["contents"].as_a - url = tabs.select { |tab| tab["channelListSubMenuAvatarRenderer"]["title"]["simpleText"] == trending_type }[0]? - - if url - url["channelListSubMenuAvatarRenderer"]["navigationEndpoint"]["commandMetadata"]["webCommandMetadata"]["url"] - url = url["channelListSubMenuAvatarRenderer"]["navigationEndpoint"]["commandMetadata"]["webCommandMetadata"]["url"].as_s - url = "#{url}&gl=#{region}&hl=en" - trending = YT_POOL.client &.get(url).body - plid = extract_plid(url) - else - trending = YT_POOL.client &.get("/feed/trending?gl=#{region}&hl=en").body - end + trending = YT_POOL.client &.get(url).body + plid = extract_plid(url) else trending = YT_POOL.client &.get("/feed/trending?gl=#{region}&hl=en").body end diff --git a/src/invidious/users.cr b/src/invidious/users.cr index 7a948b76..8fef64a0 100644 --- a/src/invidious/users.cr +++ b/src/invidious/users.cr @@ -68,7 +68,7 @@ struct Preferences property quality : String = CONFIG.default_user_preferences.quality @[JSON::Field(converter: Preferences::ProcessString)] property quality_dash : String = CONFIG.default_user_preferences.quality_dash - property default_home : String = CONFIG.default_user_preferences.default_home + property default_home : String? = CONFIG.default_user_preferences.default_home property feed_menu : Array(String) = CONFIG.default_user_preferences.feed_menu property related_videos : Bool = CONFIG.default_user_preferences.related_videos diff --git a/src/invidious/videos.cr b/src/invidious/videos.cr index 2b793a1b..40d63ae8 100644 --- a/src/invidious/videos.cr +++ b/src/invidious/videos.cr @@ -1150,15 +1150,15 @@ end def build_thumbnails(id) return { - {name: "maxres", host: "#{HOST_URL}", url: "maxres", height: 720, width: 1280}, - {name: "maxresdefault", host: "https://i.ytimg.com", url: "maxresdefault", height: 720, width: 1280}, - {name: "sddefault", host: "https://i.ytimg.com", url: "sddefault", height: 480, width: 640}, - {name: "high", host: "https://i.ytimg.com", url: "hqdefault", height: 360, width: 480}, - {name: "medium", host: "https://i.ytimg.com", url: "mqdefault", height: 180, width: 320}, - {name: "default", host: "https://i.ytimg.com", url: "default", height: 90, width: 120}, - {name: "start", host: "https://i.ytimg.com", url: "1", height: 90, width: 120}, - {name: "middle", host: "https://i.ytimg.com", url: "2", height: 90, width: 120}, - {name: "end", host: "https://i.ytimg.com", url: "3", height: 90, width: 120}, + {host: HOST_URL, height: 720, width: 1280, name: "maxres", url: "maxres"}, + {host: HOST_URL, height: 720, width: 1280, name: "maxresdefault", url: "maxresdefault"}, + {host: HOST_URL, height: 480, width: 640, name: "sddefault", url: "sddefault"}, + {host: HOST_URL, height: 360, width: 480, name: "high", url: "hqdefault"}, + {host: HOST_URL, height: 180, width: 320, name: "medium", url: "mqdefault"}, + {host: HOST_URL, height: 90, width: 120, name: "default", url: "default"}, + {host: HOST_URL, height: 90, width: 120, name: "start", url: "1"}, + {host: HOST_URL, height: 90, width: 120, name: "middle", url: "2"}, + {host: HOST_URL, height: 90, width: 120, name: "end", url: "3"}, } end diff --git a/src/invidious/views/empty.ecr b/src/invidious/views/empty.ecr deleted file mode 100644 index c10c097e..00000000 --- a/src/invidious/views/empty.ecr +++ /dev/null @@ -1,8 +0,0 @@ -<% content_for "header" do %> -<meta name="description" content="<%= translate(locale, "An alternative front-end to YouTube") %>"> -<title> - Invidious -</title> -<% end %> - -<%= rendered "components/feed_menu" %> diff --git a/src/invidious/views/preferences.ecr b/src/invidious/views/preferences.ecr index 14d63536..c1f10818 100644 --- a/src/invidious/views/preferences.ecr +++ b/src/invidious/views/preferences.ecr @@ -150,7 +150,7 @@ <label for="default_home"><%= translate(locale, "Default homepage: ") %></label> <select name="default_home" id="default_home"> <% feed_options.each do |option| %> - <option value="<%= option %>" <% if preferences.default_home == option %> selected <% end %>><%= translate(locale, option.blank? ? "none" : option) %></option> + <option value="<%= option %>" <% if preferences.default_home == option %> selected <% end %>><%= translate(locale, option.blank? ? "Search" : option) %></option> <% end %> </select> </div> @@ -160,7 +160,7 @@ <% (feed_options.size - 1).times do |index| %> <select name="feed_menu[<%= index %>]" id="feed_menu[<%= index %>]"> <% feed_options.each do |option| %> - <option value="<%= option %>" <% if preferences.feed_menu[index]? == option %> selected <% end %>><%= translate(locale, option.blank? ? "none" : option) %></option> + <option value="<%= option %>" <% if preferences.feed_menu[index]? == option %> selected <% end %>><%= translate(locale, option.blank? ? "Search" : option) %></option> <% end %> </select> <% end %> diff --git a/src/invidious/views/search_homepage.ecr b/src/invidious/views/search_homepage.ecr new file mode 100644 index 00000000..b36500e9 --- /dev/null +++ b/src/invidious/views/search_homepage.ecr @@ -0,0 +1,24 @@ +<% content_for "header" do %> +<meta name="description" content="<%= translate(locale, "An alternative front-end to YouTube") %>"> +<title> + Invidious +</title> +<link rel="stylesheet" href="/css/empty.css?v=<%= ASSET_COMMIT %>"> +<% end %> + +<%= rendered "components/feed_menu" %> + +<div class="pure-g h-box" id="search-widget"> + <div class="pure-u-1" id="logo"> + <h1 href="/" class="pure-menu-heading">Invidious</h1> + </div> + <div class="pure-u-1-4"></div> + <div class="pure-u-1 pure-u-md-12-24 searchbar"> + <form class="pure-form" action="/search" method="get"> + <fieldset> + <input type="search" style="width:100%" name="q" placeholder="<%= translate(locale, "search") %>" value="<%= env.get?("search").try {|x| HTML.escape(x.as(String)) } %>"> + </fieldset> + </form> + </div> + <div class="pure-u-1-4"></div> +</div> diff --git a/src/invidious/views/template.ecr b/src/invidious/views/template.ecr index 61b900e3..5b63bf1f 100644 --- a/src/invidious/views/template.ecr +++ b/src/invidious/views/template.ecr @@ -26,18 +26,21 @@ <span style="display:none" id="dark_mode_pref"><%= env.get("preferences").as(Preferences).dark_mode %></span> <div class="pure-g"> <div class="pure-u-1 pure-u-md-2-24"></div> - <div class="pure-u-1 pure-u-md-20-24"> + <div class="pure-u-1 pure-u-md-20-24", id="contents"> <div class="pure-g navbar h-box"> - <div class="pure-u-1 pure-u-md-4-24"> - <a href="/" class="index-link pure-menu-heading">Invidious</a> - </div> - <div class="pure-u-1 pure-u-md-12-24 searchbar"> - <form class="pure-form" action="/search" method="get"> - <fieldset> - <input type="search" style="width:100%" name="q" placeholder="<%= translate(locale, "search") %>" value="<%= env.get?("search").try {|x| HTML.escape(x.as(String)) } %>"> - </fieldset> - </form> - </div> + <% if navbar_search %> + <div class="pure-u-1 pure-u-md-4-24"> + <a href="/" class="index-link pure-menu-heading">Invidious</a> + </div> + <div class="pure-u-1 pure-u-md-12-24 searchbar"> + <form class="pure-form" action="/search" method="get"> + <fieldset> + <input type="search" style="width:100%" name="q" placeholder="<%= translate(locale, "search") %>" value="<%= env.get?("search").try {|x| HTML.escape(x.as(String)) } %>"> + </fieldset> + </form> + </div> + <% end %> + <div class="pure-u-1 pure-u-md-8-24 user-field"> <% if env.get? "user" %> <div class="pure-u-1-4"> @@ -106,7 +109,7 @@ <%= content %> - <div class="footer"> + <footer> <div class="pure-g"> <div class="pure-u-1 pure-u-md-1-3"> <a href="https://github.com/iv-org/invidious"> @@ -140,7 +143,7 @@ <%= translate(locale, "Current version: ") %> <%= CURRENT_VERSION %>-<%= CURRENT_COMMIT %> @ <%= CURRENT_BRANCH %> </div> </div> - </div> + </footer> </div> <div class="pure-u-1 pure-u-md-2-24"></div> </div> diff --git a/src/invidious/views/trending.ecr b/src/invidious/views/trending.ecr index 42acb15c..3ec62555 100644 --- a/src/invidious/views/trending.ecr +++ b/src/invidious/views/trending.ecr @@ -21,7 +21,7 @@ </div> <div class="pure-u-1-3"> <div class="pure-g" style="text-align:right"> - <% {"Default", "Music", "Gaming", "News", "Movies"}.each do |option| %> + <% {"Default", "Music", "Gaming", "Movies"}.each do |option| %> <div class="pure-u-1 pure-md-1-3"> <% if trending_type == option %> <b><%= translate(locale, option) %></b> |
