summaryrefslogtreecommitdiffstats
path: root/src/invidious.cr
diff options
context:
space:
mode:
Diffstat (limited to 'src/invidious.cr')
-rw-r--r--src/invidious.cr735
1 files changed, 343 insertions, 392 deletions
diff --git a/src/invidious.cr b/src/invidious.cr
index 0a10027b..2a4c373c 100644
--- a/src/invidious.cr
+++ b/src/invidious.cr
@@ -23,7 +23,7 @@ require "pg"
require "sqlite3"
require "xml"
require "yaml"
-require "zip"
+require "compress/zip"
require "protodec/utils"
require "./invidious/helpers/*"
require "./invidious/*"
@@ -48,9 +48,9 @@ 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")
-TEXTCAPTCHA_URL = URI.parse("http://textcaptcha.com")
+TEXTCAPTCHA_URL = URI.parse("https://textcaptcha.com")
YT_URL = URI.parse("https://www.youtube.com")
-YT_IMG_URL = URI.parse("https://i.ytimg.com")
+HOST_URL = make_host_url(CONFIG, Kemal.config)
CHARS_SAFE = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"
TEST_IDS = {"AgbeGFYluEA", "BaW_jenozKc", "a9LDPn-MO4I", "ddFvjfvPnqk", "iqKdEhx-dD4"}
@@ -84,23 +84,25 @@ LOCALES = {
"es" => load_locale("es"),
"eu" => load_locale("eu"),
"fr" => load_locale("fr"),
+ "hu" => load_locale("hu-HU"),
"is" => load_locale("is"),
"it" => load_locale("it"),
"ja" => load_locale("ja"),
"nb-NO" => load_locale("nb-NO"),
"nl" => load_locale("nl"),
- "pt-BR" => load_locale("pt-BR"),
"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: 0.05)
-YT_IMG_POOL = QUICPool.new(YT_IMG_URL, capacity: CONFIG.pool_size, timeout: 0.05)
+YT_POOL = QUICPool.new(YT_URL, capacity: CONFIG.pool_size, timeout: 0.1)
config = CONFIG
logger = Invidious::LogHandler.new
@@ -194,15 +196,6 @@ if config.statistics_enabled
end
end
-top_videos = [] of Video
-if config.top_enabled
- spawn do
- pull_top_videos(config, PG_DB) do |videos|
- top_videos = videos
- end
- end
-end
-
popular_videos = [] of ChannelVideo
spawn do
pull_popular_videos(PG_DB) do |videos|
@@ -210,10 +203,11 @@ spawn do
end
end
-decrypt_function = [] of {SigProc, Int32}
+DECRYPT_FUNCTION = [] of {SigProc, Int32}
spawn do
update_decrypt_function do |function|
- decrypt_function = function
+ DECRYPT_FUNCTION.clear
+ function.each { |i| DECRYPT_FUNCTION << i }
end
end
@@ -250,10 +244,20 @@ spawn do
end
before_all do |env|
- host_url = make_host_url(config, Kemal.config)
+ begin
+ preferences = Preferences.from_json(env.request.cookies["PREFS"]?.try &.value || "{}")
+ rescue
+ preferences = Preferences.from_json("{}")
+ end
+
env.response.headers["X-XSS-Protection"] = "1; mode=block"
env.response.headers["X-Content-Type-Options"] = "nosniff"
- env.response.headers["Content-Security-Policy"] = "default-src blob: data: 'self' #{host_url} 'unsafe-inline' 'unsafe-eval'; media-src blob: 'self' #{host_url} https://*.googlevideo.com:443"
+ extra_media_csp = ""
+ if CONFIG.disabled?("local") || !preferences.local
+ extra_media_csp += " https://*.googlevideo.com:443"
+ end
+ # TODO: Remove style-src's 'unsafe-inline', requires to remove all inline styles (<style> [..] </style>, style=" [..] ")
+ env.response.headers["Content-Security-Policy"] = "default-src 'none'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self' data:; connect-src 'self'; manifest-src 'self'; media-src 'self' blob:#{extra_media_csp}"
env.response.headers["Referrer-Policy"] = "same-origin"
if (Kemal.config.ssl || config.https_only) && config.hsts
@@ -271,12 +275,6 @@ before_all do |env|
"/latest_version",
}.any? { |r| env.request.resource.starts_with? r }
- begin
- preferences = Preferences.from_json(env.request.cookies["PREFS"]?.try &.value || "{}")
- rescue
- preferences = Preferences.from_json("{}")
- end
-
if env.request.cookies.has_key? "SID"
sid = env.request.cookies["SID"].value
@@ -362,12 +360,6 @@ get "/" do |env|
templated "empty"
when "Popular"
templated "popular"
- when "Top"
- if config.top_enabled
- templated "top"
- else
- templated "empty"
- end
when "Trending"
env.redirect "/feed/trending"
when "Subscriptions"
@@ -382,6 +374,8 @@ get "/" do |env|
else
templated "popular"
end
+ else
+ templated "empty"
end
end
@@ -516,16 +510,16 @@ get "/watch" do |env|
comment_html ||= ""
end
- fmt_stream = video.fmt_stream(decrypt_function)
- adaptive_fmts = video.adaptive_fmts(decrypt_function)
+ fmt_stream = video.fmt_stream
+ adaptive_fmts = video.adaptive_fmts
if params.local
- fmt_stream.each { |fmt| fmt["url"] = URI.parse(fmt["url"]).full_path }
- adaptive_fmts.each { |fmt| fmt["url"] = URI.parse(fmt["url"]).full_path }
+ fmt_stream.each { |fmt| fmt["url"] = JSON::Any.new(URI.parse(fmt["url"].as_s).full_path) }
+ adaptive_fmts.each { |fmt| fmt["url"] = JSON::Any.new(URI.parse(fmt["url"].as_s).full_path) }
end
- video_streams = video.video_streams(adaptive_fmts)
- audio_streams = video.audio_streams(adaptive_fmts)
+ video_streams = video.video_streams
+ audio_streams = video.audio_streams
# Older videos may not have audio sources available.
# We redirect here so they're not unplayable
@@ -555,33 +549,23 @@ get "/watch" do |env|
aspect_ratio = "16:9"
- video.description_html = fill_links(video.description_html, "https", "www.youtube.com")
- video.description_html = replace_links(video.description_html)
-
- host_url = make_host_url(config, Kemal.config)
-
- if video.player_response["streamingData"]?.try &.["hlsManifestUrl"]?
- hlsvp = video.player_response["streamingData"]["hlsManifestUrl"].as_s
- hlsvp = hlsvp.gsub("https://manifest.googlevideo.com", host_url)
- end
-
thumbnail = "/vi/#{video.id}/maxres.jpg"
if params.raw
if params.listen
- url = audio_streams[0]["url"]
+ url = audio_streams[0]["url"].as_s
audio_streams.each do |fmt|
- if fmt["bitrate"] == params.quality.rchop("k")
- url = fmt["url"]
+ if fmt["bitrate"].as_i == params.quality.rchop("k").to_i
+ url = fmt["url"].as_s
end
end
else
- url = fmt_stream[0]["url"]
+ url = fmt_stream[0]["url"].as_s
fmt_stream.each do |fmt|
- if fmt["label"].split(" - ")[0] == params.quality
- url = fmt["url"]
+ if fmt["quality"].as_s == params.quality
+ url = fmt["url"].as_s
end
end
end
@@ -589,24 +573,6 @@ get "/watch" do |env|
next env.redirect url
end
- rvs = [] of Hash(String, String)
- video.info["rvs"]?.try &.split(",").each do |rv|
- rvs << HTTP::Params.parse(rv).to_h
- end
-
- rating = video.info["avg_rating"].to_f64
- if video.views > 0
- engagement = ((video.dislikes.to_f + video.likes.to_f)/video.views * 100)
- else
- engagement = 0
- end
-
- playability_status = video.player_response["playabilityStatus"]?
- if playability_status && playability_status["status"] == "LIVE_STREAM_OFFLINE" && !video.premiere_timestamp
- reason = playability_status["reason"]?.try &.as_s
- end
- reason ||= ""
-
templated "watch"
end
@@ -719,6 +685,7 @@ get "/embed/:id" do |env|
end
next env.redirect url
+ else nil # Continue
end
params = process_video_params(env.params.query, preferences)
@@ -757,16 +724,16 @@ get "/embed/:id" do |env|
notifications.delete(id)
end
- fmt_stream = video.fmt_stream(decrypt_function)
- adaptive_fmts = video.adaptive_fmts(decrypt_function)
+ fmt_stream = video.fmt_stream
+ adaptive_fmts = video.adaptive_fmts
if params.local
- fmt_stream.each { |fmt| fmt["url"] = URI.parse(fmt["url"]).full_path }
- adaptive_fmts.each { |fmt| fmt["url"] = URI.parse(fmt["url"]).full_path }
+ fmt_stream.each { |fmt| fmt["url"] = JSON::Any.new(URI.parse(fmt["url"].as_s).full_path) }
+ adaptive_fmts.each { |fmt| fmt["url"] = JSON::Any.new(URI.parse(fmt["url"].as_s).full_path) }
end
- video_streams = video.video_streams(adaptive_fmts)
- audio_streams = video.audio_streams(adaptive_fmts)
+ video_streams = video.video_streams
+ audio_streams = video.audio_streams
if audio_streams.empty? && !video.live_now
if params.quality == "dash"
@@ -793,25 +760,13 @@ get "/embed/:id" do |env|
aspect_ratio = nil
- video.description_html = fill_links(video.description_html, "https", "www.youtube.com")
- video.description_html = replace_links(video.description_html)
-
- host_url = make_host_url(config, Kemal.config)
-
- if video.player_response["streamingData"]?.try &.["hlsManifestUrl"]?
- hlsvp = video.player_response["streamingData"]["hlsManifestUrl"].as_s
- hlsvp = hlsvp.gsub("https://manifest.googlevideo.com", host_url)
- end
-
thumbnail = "/vi/#{video.id}/maxres.jpg"
if params.raw
- url = fmt_stream[0]["url"]
+ url = fmt_stream[0]["url"].as_s
fmt_stream.each do |fmt|
- if fmt["label"].split(" - ")[0] == params.quality
- url = fmt["url"]
- end
+ url = fmt["url"].as_s if fmt["quality"].as_s == params.quality
end
next env.redirect url
@@ -838,8 +793,14 @@ get "/view_all_playlists" do |env|
user = user.as(User)
- items = PG_DB.query_all("SELECT * FROM playlists WHERE author = $1 ORDER BY created", user.email, as: InvidiousPlaylist)
- items.map! do |item|
+ items_created = PG_DB.query_all("SELECT * FROM playlists WHERE author = $1 AND id LIKE 'IV%' ORDER BY created", user.email, as: InvidiousPlaylist)
+ items_created.map! do |item|
+ item.author = ""
+ item
+ end
+
+ items_saved = PG_DB.query_all("SELECT * FROM playlists WHERE author = $1 AND id NOT LIKE 'IV%' ORDER BY created", user.email, as: InvidiousPlaylist)
+ items_saved.map! do |item|
item.author = ""
item
end
@@ -910,6 +871,25 @@ post "/create_playlist" do |env|
env.redirect "/playlist?list=#{playlist.id}"
end
+get "/subscribe_playlist" do |env|
+ locale = LOCALES[env.get("preferences").as(Preferences).locale]?
+
+ user = env.get? "user"
+ referer = get_referer(env)
+
+ if !user
+ next env.redirect "/"
+ end
+
+ user = user.as(User)
+
+ playlist_id = env.params.query["list"]
+ playlist = get_playlist(PG_DB, playlist_id, locale)
+ subscribe_playlist(PG_DB, user, playlist)
+
+ env.redirect "/playlist?list=#{playlist.id}"
+end
+
get "/delete_playlist" do |env|
locale = LOCALES[env.get("preferences").as(Preferences).locale]?
@@ -925,10 +905,6 @@ get "/delete_playlist" do |env|
sid = sid.as(String)
plid = env.params.query["list"]?
- if !plid || !plid.starts_with?("IV")
- next env.redirect referer
- end
-
playlist = PG_DB.query_one?("SELECT * FROM playlists WHERE id = $1", plid, as: InvidiousPlaylist)
if !playlist || playlist.author != user.email
next env.redirect referer
@@ -1227,17 +1203,17 @@ post "/playlist_ajax" do |env|
end
end
- playlist_video = PlaylistVideo.new(
- title: video.title,
- id: video.id,
- author: video.author,
- ucid: video.ucid,
+ playlist_video = PlaylistVideo.new({
+ title: video.title,
+ id: video.id,
+ author: video.author,
+ ucid: video.ucid,
length_seconds: video.length_seconds,
- published: video.published,
- plid: playlist_id,
- live_now: video.live_now,
- index: Random::Secure.rand(0_i64..Int64::MAX)
- )
+ published: video.published,
+ plid: playlist_id,
+ live_now: video.live_now,
+ index: Random::Secure.rand(0_i64..Int64::MAX),
+ })
video_array = playlist_video.to_a
args = arg_array(video_array)
@@ -1250,6 +1226,10 @@ post "/playlist_ajax" do |env|
PG_DB.exec("UPDATE playlists SET index = array_remove(index, $1), video_count = cardinality(index), updated = $2 WHERE id = $3", index, Time.utc, playlist_id)
when "action_move_video_before"
# TODO: Playlist stub
+ else
+ error_message = {"error" => "Unsupported action #{action}"}.to_json
+ env.response.status_code = 400
+ next error_message
end
if redirect
@@ -1333,16 +1313,14 @@ get "/opensearch.xml" do |env|
locale = LOCALES[env.get("preferences").as(Preferences).locale]?
env.response.content_type = "application/opensearchdescription+xml"
- host = make_host_url(config, Kemal.config)
-
XML.build(indent: " ", encoding: "UTF-8") do |xml|
xml.element("OpenSearchDescription", xmlns: "http://a9.com/-/spec/opensearch/1.1/") do
xml.element("ShortName") { xml.text "Invidious" }
xml.element("LongName") { xml.text "Invidious Search" }
xml.element("Description") { xml.text "Search for videos, channels, and playlists on Invidious" }
xml.element("InputEncoding") { xml.text "UTF-8" }
- xml.element("Image", width: 48, height: 48, type: "image/x-icon") { xml.text "#{host}/favicon.ico" }
- xml.element("Url", type: "text/html", method: "get", template: "#{host}/search?q={searchTerms}")
+ xml.element("Image", width: 48, height: 48, type: "image/x-icon") { xml.text "#{HOST_URL}/favicon.ico" }
+ xml.element("Url", type: "text/html", method: "get", template: "#{HOST_URL}/search?q={searchTerms}")
end
end
end
@@ -1451,7 +1429,6 @@ post "/login" do |env|
traceback = IO::Memory.new
# See https://github.com/ytdl-org/youtube-dl/blob/2019.04.07/youtube_dl/extractor/youtube.py#L82
- # TODO: Convert to QUIC
begin
client = QUIC::Client.new(LOGIN_URL)
headers = HTTP::Headers.new
@@ -1544,7 +1521,7 @@ post "/login" do |env|
case prompt_type
when "TWO_STEP_VERIFICATION"
prompt_type = 2
- when "LOGIN_CHALLENGE"
+ else # "LOGIN_CHALLENGE"
prompt_type = 4
end
@@ -1837,7 +1814,7 @@ post "/login" do |env|
env.response.status_code = 400
next templated "error"
end
- when "text"
+ else # "text"
answer = Digest::MD5.hexdigest(answer.downcase.strip)
found_valid_captcha = false
@@ -1862,8 +1839,8 @@ post "/login" do |env|
sid = Base64.urlsafe_encode(Random::Secure.random_bytes(32))
user, sid = create_user(sid, email, password)
user_array = user.to_a
+ user_array[4] = user_array[4].to_json # User preferences
- user_array[4] = user_array[4].to_json
args = arg_array(user_array)
PG_DB.exec("INSERT INTO users VALUES (#{args})", args: user_array)
@@ -2090,10 +2067,6 @@ post "/preferences" do |env|
end
config.default_user_preferences.feed_menu = admin_feed_menu
- top_enabled = env.params.body["top_enabled"]?.try &.as(String)
- top_enabled ||= "off"
- config.top_enabled = top_enabled == "on"
-
captcha_enabled = env.params.body["captcha_enabled"]?.try &.as(String)
captcha_enabled ||= "off"
config.captcha_enabled = captcha_enabled == "on"
@@ -2248,6 +2221,10 @@ post "/watch_ajax" do |env|
end
when "action_mark_unwatched"
PG_DB.exec("UPDATE users SET watched = array_remove(watched, $1) WHERE email = $2", id, user.email)
+ else
+ error_message = {"error" => "Unsupported action #{action}"}.to_json
+ env.response.status_code = 400
+ next error_message
end
if redirect
@@ -2311,8 +2288,7 @@ get "/modify_notifications" do |env|
end
headers = cookies.add_request_headers(headers)
- match = html.body.match(/'XSRF_TOKEN': "(?<session_token>[A-Za-z0-9\_\-\=]+)"/)
- if match
+ if match = html.body.match(/'XSRF_TOKEN': "(?<session_token>[^"]+)"/)
session_token = match["session_token"]
else
next env.redirect referer
@@ -2402,6 +2378,10 @@ post "/subscription_ajax" do |env|
end
when "action_remove_subscriptions"
PG_DB.exec("UPDATE users SET feed_needs_update = true, subscriptions = array_remove(subscriptions, $1) WHERE email = $2", channel_id, email)
+ else
+ error_message = {"error" => "Unsupported action #{action}"}.to_json
+ env.response.status_code = 400
+ next error_message
end
if redirect
@@ -2447,20 +2427,39 @@ get "/subscription_manager" do |env|
end
subscriptions = PG_DB.query_all("SELECT * FROM channels WHERE id = ANY(#{values})", as: InvidiousChannel)
-
subscriptions.sort_by! { |channel| channel.author.downcase }
if action_takeout
- host_url = make_host_url(config, Kemal.config)
-
if format == "json"
env.response.content_type = "application/json"
env.response.headers["content-disposition"] = "attachment"
- next {
- "subscriptions" => user.subscriptions,
- "watch_history" => user.watched,
- "preferences" => user.preferences,
- }.to_json
+ playlists = PG_DB.query_all("SELECT * FROM playlists WHERE author = $1 AND id LIKE 'IV%' ORDER BY created", user.email, as: InvidiousPlaylist)
+
+ next JSON.build do |json|
+ json.object do
+ json.field "subscriptions", user.subscriptions
+ json.field "watch_history", user.watched
+ json.field "preferences", user.preferences
+ json.field "playlists" do
+ json.array do
+ playlists.each do |playlist|
+ json.object do
+ json.field "title", playlist.title
+ json.field "description", html_to_content(playlist.description_html)
+ json.field "privacy", playlist.privacy.to_s
+ json.field "videos" do
+ json.array do
+ PG_DB.query_all("SELECT id FROM playlist_videos WHERE plid = $1 ORDER BY array_position($2, index) LIMIT 500", playlist.id, playlist.index, as: String).each do |video_id|
+ json.string video_id
+ end
+ end
+ end
+ end
+ end
+ end
+ end
+ end
+ end
else
env.response.content_type = "application/xml"
env.response.headers["content-disposition"] = "attachment"
@@ -2478,7 +2477,7 @@ get "/subscription_manager" do |env|
if format == "newpipe"
xmlUrl = "https://www.youtube.com/feeds/videos.xml?channel_id=#{channel.id}"
else
- xmlUrl = "#{host_url}/feed/channel/#{channel.id}"
+ xmlUrl = "#{HOST_URL}/feed/channel/#{channel.id}"
end
xml.element("outline", text: channel.author, title: channel.author,
@@ -2520,42 +2519,13 @@ post "/data_control" do |env|
if user
user = user.as(User)
- spawn do
- # Since import can take a while, if we're not done after 20 seconds
- # push out content to prevent timeout.
-
- # Interesting to note is that Chrome will try to render before the content has finished loading,
- # which is why we include a loading icon. Firefox and its derivatives will not see this page,
- # instead redirecting immediately once the connection has closed.
-
- # https://stackoverflow.com/q/2091239 is helpful but not directly applicable here.
-
- sleep 20.seconds
- env.response.puts %(<meta http-equiv="refresh" content="0; url=#{referer}">)
- env.response.puts %(<link rel="stylesheet" href="/css/ionicons.min.css?v=#{ASSET_COMMIT}">)
- env.response.puts %(<link rel="stylesheet" href="/css/default.css?v=#{ASSET_COMMIT}">)
- if env.get("preferences").as(Preferences).dark_mode == "dark"
- env.response.puts %(<link rel="stylesheet" href="/css/darktheme.css?v=#{ASSET_COMMIT}">)
- else
- env.response.puts %(<link rel="stylesheet" href="/css/lighttheme.css?v=#{ASSET_COMMIT}">)
- end
- env.response.puts %(<h3><div class="loading"><i class="icon ion-ios-refresh"></i></div></h3>)
- env.response.flush
-
- loop do
- env.response.puts %(<!-- keepalive #{Time.utc.to_unix} -->)
- env.response.flush
-
- sleep (20 + rand(11)).seconds
- end
- end
+ # TODO: Find a way to prevent browser timeout
HTTP::FormData.parse(env.request) do |part|
body = part.body.gets_to_end
- if body.empty?
- next
- end
+ next if body.empty?
+ # TODO: Unify into single import based on content-type
case part.name
when "import_invidious"
body = JSON.parse(body)
@@ -2576,9 +2546,55 @@ post "/data_control" do |env|
end
if body["preferences"]?
- user.preferences = Preferences.from_json(body["preferences"].to_json, user.preferences)
+ user.preferences = Preferences.from_json(body["preferences"].to_json)
PG_DB.exec("UPDATE users SET preferences = $1 WHERE email = $2", user.preferences.to_json, user.email)
end
+
+ if playlists = body["playlists"]?.try &.as_a?
+ playlists.each do |item|
+ title = item["title"]?.try &.as_s?.try &.delete("<>")
+ description = item["description"]?.try &.as_s?.try &.delete("\r")
+ privacy = item["privacy"]?.try &.as_s?.try { |privacy| PlaylistPrivacy.parse? privacy }
+
+ next if !title
+ next if !description
+ next if !privacy
+
+ playlist = create_playlist(PG_DB, title, privacy, user)
+ PG_DB.exec("UPDATE playlists SET description = $1 WHERE id = $2", description, playlist.id)
+
+ videos = item["videos"]?.try &.as_a?.try &.each_with_index do |video_id, idx|
+ raise "Playlist cannot have more than 500 videos" if idx > 500
+
+ video_id = video_id.try &.as_s?
+ next if !video_id
+
+ begin
+ video = get_video(video_id, PG_DB)
+ rescue ex
+ next
+ end
+
+ playlist_video = PlaylistVideo.new({
+ title: video.title,
+ id: video.id,
+ author: video.author,
+ ucid: video.ucid,
+ length_seconds: video.length_seconds,
+ published: video.published,
+ plid: playlist.id,
+ live_now: video.live_now,
+ index: Random::Secure.rand(0_i64..Int64::MAX),
+ })
+
+ video_array = playlist_video.to_a
+ args = arg_array(video_array)
+
+ PG_DB.exec("INSERT INTO playlist_videos VALUES (#{args})", args: video_array)
+ PG_DB.exec("UPDATE playlists SET index = array_append(index, $1), video_count = cardinality(index), updated = $2 WHERE id = $3", playlist_video.index, Time.utc, playlist.id)
+ end
+ end
+ end
when "import_youtube"
subscriptions = XML.parse(body)
user.subscriptions += subscriptions.xpath_nodes(%q(//outline[@type="rss"])).map do |channel|
@@ -2618,7 +2634,7 @@ post "/data_control" do |env|
PG_DB.exec("UPDATE users SET feed_needs_update = true, subscriptions = $1 WHERE email = $2", user.subscriptions, user.email)
when "import_newpipe"
- Zip::Reader.open(IO::Memory.new(body)) do |file|
+ Compress::Zip::Reader.open(IO::Memory.new(body)) do |file|
file.each_entry do |entry|
if entry.filename == "newpipe.db"
tempfile = File.tempfile(".db")
@@ -2642,6 +2658,7 @@ post "/data_control" do |env|
end
end
end
+ else nil # Ignore
end
end
end
@@ -2983,6 +3000,10 @@ post "/token_ajax" do |env|
case action
when .starts_with? "action_revoke_token"
PG_DB.exec("DELETE FROM session_ids * WHERE id = $1 AND email = $2", session, user.email)
+ else
+ error_message = {"error" => "Unsupported action #{action}"}.to_json
+ env.response.status_code = 400
+ next error_message
end
if redirect
@@ -2997,12 +3018,7 @@ end
get "/feed/top" do |env|
locale = LOCALES[env.get("preferences").as(Preferences).locale]?
-
- if config.top_enabled
- templated "top"
- else
- env.redirect "/"
- end
+ env.redirect "/"
end
get "/feed/popular" do |env|
@@ -3125,12 +3141,10 @@ get "/feed/channel/:ucid" do |env|
next error_message
end
- rss = YT_POOL.client &.get("/feeds/videos.xml?channel_id=#{channel.ucid}").body
- rss = XML.parse_html(rss)
+ response = YT_POOL.client &.get("/feeds/videos.xml?channel_id=#{channel.ucid}")
+ rss = XML.parse_html(response.body)
- videos = [] of SearchVideo
-
- rss.xpath_nodes("//feed/entry").each do |entry|
+ videos = rss.xpath_nodes("//feed/entry").map do |entry|
video_id = entry.xpath_node("videoid").not_nil!.content
title = entry.xpath_node("title").not_nil!.content
@@ -3142,41 +3156,39 @@ get "/feed/channel/:ucid" do |env|
description_html = entry.xpath_node("group/description").not_nil!.to_s
views = entry.xpath_node("group/community/statistics").not_nil!.["views"].to_i64
- videos << SearchVideo.new(
- title: title,
- id: video_id,
- author: author,
- ucid: ucid,
- published: published,
- views: views,
- description_html: description_html,
- length_seconds: 0,
- live_now: false,
- paid: false,
- premium: false,
- premiere_timestamp: nil
- )
+ SearchVideo.new({
+ title: title,
+ id: video_id,
+ author: author,
+ ucid: ucid,
+ published: published,
+ views: views,
+ description_html: description_html,
+ length_seconds: 0,
+ live_now: false,
+ paid: false,
+ premium: false,
+ premiere_timestamp: nil,
+ })
end
- host_url = make_host_url(config, Kemal.config)
-
XML.build(indent: " ", encoding: "UTF-8") do |xml|
xml.element("feed", "xmlns:yt": "http://www.youtube.com/xml/schemas/2015",
"xmlns:media": "http://search.yahoo.com/mrss/", xmlns: "http://www.w3.org/2005/Atom",
"xml:lang": "en-US") do
- xml.element("link", rel: "self", href: "#{host_url}#{env.request.resource}")
+ xml.element("link", rel: "self", href: "#{HOST_URL}#{env.request.resource}")
xml.element("id") { xml.text "yt:channel:#{channel.ucid}" }
xml.element("yt:channelId") { xml.text channel.ucid }
xml.element("title") { xml.text channel.author }
- xml.element("link", rel: "alternate", href: "#{host_url}/channel/#{channel.ucid}")
+ xml.element("link", rel: "alternate", href: "#{HOST_URL}/channel/#{channel.ucid}")
xml.element("author") do
xml.element("name") { xml.text channel.author }
- xml.element("uri") { xml.text "#{host_url}/channel/#{channel.ucid}" }
+ xml.element("uri") { xml.text "#{HOST_URL}/channel/#{channel.ucid}" }
end
videos.each do |video|
- video.to_xml(host_url, channel.auto_generated, params, xml)
+ video.to_xml(channel.auto_generated, params, xml)
end
end
end
@@ -3210,19 +3222,18 @@ get "/feed/private" do |env|
params = HTTP::Params.parse(env.params.query["params"]? || "")
videos, notifications = get_subscription_feed(PG_DB, user, max_results, page)
- host_url = make_host_url(config, Kemal.config)
XML.build(indent: " ", encoding: "UTF-8") do |xml|
xml.element("feed", "xmlns:yt": "http://www.youtube.com/xml/schemas/2015",
"xmlns:media": "http://search.yahoo.com/mrss/", xmlns: "http://www.w3.org/2005/Atom",
"xml:lang": "en-US") do
- xml.element("link", "type": "text/html", rel: "alternate", href: "#{host_url}/feed/subscriptions")
+ xml.element("link", "type": "text/html", rel: "alternate", href: "#{HOST_URL}/feed/subscriptions")
xml.element("link", "type": "application/atom+xml", rel: "self",
- href: "#{host_url}#{env.request.resource}")
+ href: "#{HOST_URL}#{env.request.resource}")
xml.element("title") { xml.text translate(locale, "Invidious Private Feed for `x`", user.email) }
(notifications + videos).each do |video|
- video.to_xml(locale, host_url, params, xml)
+ video.to_xml(locale, params, xml)
end
end
end
@@ -3236,8 +3247,6 @@ get "/feed/playlist/:plid" do |env|
plid = env.params.url["plid"]
params = HTTP::Params.parse(env.params.query["params"]? || "")
-
- host_url = make_host_url(config, Kemal.config)
path = env.request.path
if plid.starts_with? "IV"
@@ -3248,18 +3257,18 @@ get "/feed/playlist/:plid" do |env|
xml.element("feed", "xmlns:yt": "http://www.youtube.com/xml/schemas/2015",
"xmlns:media": "http://search.yahoo.com/mrss/", xmlns: "http://www.w3.org/2005/Atom",
"xml:lang": "en-US") do
- xml.element("link", rel: "self", href: "#{host_url}#{env.request.resource}")
+ xml.element("link", rel: "self", href: "#{HOST_URL}#{env.request.resource}")
xml.element("id") { xml.text "iv:playlist:#{plid}" }
xml.element("iv:playlistId") { xml.text plid }
xml.element("title") { xml.text playlist.title }
- xml.element("link", rel: "alternate", href: "#{host_url}/playlist?list=#{plid}")
+ xml.element("link", rel: "alternate", href: "#{HOST_URL}/playlist?list=#{plid}")
xml.element("author") do
xml.element("name") { xml.text playlist.author }
end
videos.each do |video|
- video.to_xml(host_url, false, xml)
+ video.to_xml(false, xml)
end
end
end
@@ -3278,7 +3287,8 @@ get "/feed/playlist/:plid" do |env|
when "url", "href"
full_path = URI.parse(node[attribute.name]).full_path
query_string_opt = full_path.starts_with?("/watch?v=") ? "&#{params}" : ""
- node[attribute.name] = "#{host_url}#{full_path}#{query_string_opt}"
+ node[attribute.name] = "#{HOST_URL}#{full_path}#{query_string_opt}"
+ else nil # Skip
end
end
end
@@ -3286,7 +3296,7 @@ get "/feed/playlist/:plid" do |env|
document = document.to_xml(options: XML::SaveOptions::NO_DECL)
document.scan(/<uri>(?<url>[^<]+)<\/uri>/).each do |match|
- content = "#{host_url}#{URI.parse(match["url"]).full_path}"
+ content = "#{HOST_URL}#{URI.parse(match["url"]).full_path}"
document = document.gsub(match[0], "<uri>#{content}</uri>")
end
@@ -3389,18 +3399,18 @@ post "/feed/webhook/:token" do |env|
}.to_json
PG_DB.exec("NOTIFY notifications, E'#{payload}'")
- video = ChannelVideo.new(
- id: id,
- title: video.title,
- published: published,
- updated: updated,
- ucid: video.ucid,
- author: author,
- length_seconds: video.length_seconds,
- live_now: video.live_now,
+ video = ChannelVideo.new({
+ id: id,
+ title: video.title,
+ published: published,
+ updated: updated,
+ ucid: video.ucid,
+ author: author,
+ length_seconds: video.length_seconds,
+ live_now: video.live_now,
premiere_timestamp: video.premiere_timestamp,
- views: video.views,
- )
+ views: video.views,
+ })
PG_DB.query_all("UPDATE users SET feed_needs_update = true, notifications = array_append(notifications, $1) \
WHERE updated < $2 AND $3 = ANY(subscriptions) AND $1 <> ALL(notifications)",
@@ -3465,14 +3475,12 @@ get "/c/:user" do |env|
user = env.params.url["user"]
response = YT_POOL.client &.get("/c/#{user}")
- document = XML.parse_html(response.body)
+ html = XML.parse_html(response.body)
- anchor = document.xpath_node(%q(//a[contains(@class,"branded-page-header-title-link")]))
- if !anchor
- next env.redirect "/"
- end
+ ucid = html.xpath_node(%q(//link[@rel="canonical"])).try &.["href"].split("/")[-1]
+ next env.redirect "/" if !ucid
- env.redirect anchor["href"]
+ env.redirect "/channel/#{ucid}"
end
# Legacy endpoint for /user/:username
@@ -3562,14 +3570,14 @@ get "/channel/:ucid" do |env|
item.author
end
end
- items = items.select { |item| item.is_a?(SearchPlaylist) }.map { |item| item.as(SearchPlaylist) }
+ items = items.select(&.is_a?(SearchPlaylist)).map(&.as(SearchPlaylist))
items.each { |item| item.author = "" }
else
sort_options = {"newest", "oldest", "popular"}
sort_by ||= "newest"
- items, count = get_60_videos(channel.ucid, channel.author, page, channel.auto_generated, sort_by)
- items.select! { |item| !item.paid }
+ count, items = get_60_videos(channel.ucid, channel.author, page, channel.auto_generated, sort_by)
+ items.reject! &.paid
env.set "search", "channel:#{channel.ucid} "
end
@@ -3664,7 +3672,7 @@ get "/channel/:ucid/community" do |env|
end
begin
- items = JSON.parse(fetch_channel_community(ucid, continuation, locale, config, Kemal.config, "json", thin_mode))
+ items = JSON.parse(fetch_channel_community(ucid, continuation, locale, "json", thin_mode))
rescue ex
env.response.status_code = 500
error_message = ex.message
@@ -3717,7 +3725,6 @@ get "/api/v1/storyboards/:id" do |env|
end
storyboards = video.storyboards
-
width = env.params.query["width"]?
height = env.params.query["height"]?
@@ -3725,7 +3732,7 @@ get "/api/v1/storyboards/:id" do |env|
response = JSON.build do |json|
json.object do
json.field "storyboards" do
- generate_storyboards(json, id, storyboards, config, Kemal.config)
+ generate_storyboards(json, id, storyboards)
end
end
end
@@ -3755,8 +3762,7 @@ get "/api/v1/storyboards/:id" do |env|
end_time = storyboard[:interval].milliseconds
storyboard[:storyboard_count].times do |i|
- host_url = make_host_url(config, Kemal.config)
- url = storyboard[:url].gsub("$M", i).gsub("https://i9.ytimg.com", host_url)
+ url = storyboard[:url].gsub("$M", i).gsub("https://i9.ytimg.com", HOST_URL)
storyboard[:storyboard_height].times do |j|
storyboard[:storyboard_width].times do |k|
@@ -4038,7 +4044,7 @@ get "/api/v1/annotations/:id" do |env|
cache_annotation(PG_DB, id, annotations)
end
- when "youtube"
+ else # "youtube"
response = YT_POOL.client &.get("/annotations_invideo?video_id=#{id}")
if response.status_code != 200
@@ -4079,7 +4085,7 @@ get "/api/v1/videos/:id" do |env|
next error_message
end
- video.to_json(locale, config, Kemal.config, decrypt_function)
+ video.to_json(locale)
end
get "/api/v1/trending" do |env|
@@ -4101,7 +4107,7 @@ get "/api/v1/trending" do |env|
videos = JSON.build do |json|
json.array do
trending.each do |video|
- video.to_json(locale, config, Kemal.config, json)
+ video.to_json(locale, json)
end
end
end
@@ -4117,7 +4123,7 @@ get "/api/v1/popular" do |env|
JSON.build do |json|
json.array do
popular_videos.each do |video|
- video.to_json(locale, config, Kemal.config, json)
+ video.to_json(locale, json)
end
end
end
@@ -4127,41 +4133,7 @@ get "/api/v1/top" do |env|
locale = LOCALES[env.get("preferences").as(Preferences).locale]?
env.response.content_type = "application/json"
-
- if !config.top_enabled
- error_message = {"error" => "Administrator has disabled this endpoint."}.to_json
- env.response.status_code = 400
- next error_message
- end
-
- JSON.build do |json|
- json.array do
- top_videos.each do |video|
- # Top videos have much more information than provided below (adaptiveFormats, etc)
- # but can be very out of date, so we only provide a subset here
-
- json.object do
- json.field "title", video.title
- json.field "videoId", video.id
- json.field "videoThumbnails" do
- generate_thumbnails(json, video.id, config, Kemal.config)
- end
-
- json.field "lengthSeconds", video.length_seconds
- json.field "viewCount", video.views
-
- json.field "author", video.author
- json.field "authorId", video.ucid
- json.field "authorUrl", "/channel/#{video.ucid}"
- json.field "published", video.published.to_unix
- json.field "publishedText", translate(locale, "`x` ago", recode_date(video.published, locale))
-
- json.field "description", html_to_content(video.description_html)
- json.field "descriptionHtml", video.description_html
- end
- end
- end
- end
+ "[]"
end
get "/api/v1/channels/:ucid" do |env|
@@ -4192,7 +4164,7 @@ get "/api/v1/channels/:ucid" do |env|
count = 0
else
begin
- videos, count = get_60_videos(channel.ucid, channel.author, page, channel.auto_generated, sort_by)
+ count, videos = get_60_videos(channel.ucid, channel.author, page, channel.auto_generated, sort_by)
rescue ex
error_message = {"error" => ex.message}.to_json
env.response.status_code = 500
@@ -4238,7 +4210,7 @@ get "/api/v1/channels/:ucid" do |env|
qualities.each do |quality|
json.object do
- json.field "url", channel.author_thumbnail.gsub(/=\d+/, "=s#{quality}")
+ json.field "url", channel.author_thumbnail.gsub(/=s\d+/, "=s#{quality}")
json.field "width", quality
json.field "height", quality
end
@@ -4261,7 +4233,7 @@ get "/api/v1/channels/:ucid" do |env|
json.field "latestVideos" do
json.array do
videos.each do |video|
- video.to_json(locale, config, Kemal.config, json)
+ video.to_json(locale, json)
end
end
end
@@ -4322,7 +4294,7 @@ end
end
begin
- videos, count = get_60_videos(channel.ucid, channel.author, page, channel.auto_generated, sort_by)
+ count, videos = get_60_videos(channel.ucid, channel.author, page, channel.auto_generated, sort_by)
rescue ex
error_message = {"error" => ex.message}.to_json
env.response.status_code = 500
@@ -4332,7 +4304,7 @@ end
JSON.build do |json|
json.array do
videos.each do |video|
- video.to_json(locale, config, Kemal.config, json)
+ video.to_json(locale, json)
end
end
end
@@ -4358,7 +4330,7 @@ end
JSON.build do |json|
json.array do
videos.each do |video|
- video.to_json(locale, config, Kemal.config, json)
+ video.to_json(locale, json)
end
end
end
@@ -4373,9 +4345,9 @@ end
ucid = env.params.url["ucid"]
continuation = env.params.query["continuation"]?
- sort_by = env.params.query["sort"]?.try &.downcase
- sort_by ||= env.params.query["sort_by"]?.try &.downcase
- sort_by ||= "last"
+ sort_by = env.params.query["sort"]?.try &.downcase ||
+ env.params.query["sort_by"]?.try &.downcase ||
+ "last"
begin
channel = get_about_info(ucid, locale)
@@ -4397,9 +4369,7 @@ end
json.field "playlists" do
json.array do
items.each do |item|
- if item.is_a?(SearchPlaylist)
- item.to_json(locale, config, Kemal.config, json)
- end
+ item.to_json(locale, json) if item.is_a?(SearchPlaylist)
end
end
end
@@ -4428,7 +4398,7 @@ end
# sort_by = env.params.query["sort_by"]?.try &.downcase
begin
- fetch_channel_community(ucid, continuation, locale, config, Kemal.config, format, thin_mode)
+ fetch_channel_community(ucid, continuation, locale, format, thin_mode)
rescue ex
env.response.status_code = 400
error_message = {"error" => ex.message}.to_json
@@ -4454,7 +4424,7 @@ get "/api/v1/channels/search/:ucid" do |env|
JSON.build do |json|
json.array do
search_results.each do |item|
- item.to_json(locale, config, Kemal.config, json)
+ item.to_json(locale, json)
end
end
end
@@ -4499,7 +4469,7 @@ get "/api/v1/search" do |env|
JSON.build do |json|
json.array do
search_results.each do |item|
- item.to_json(locale, config, Kemal.config, json)
+ item.to_json(locale, json)
end
end
end
@@ -4515,10 +4485,8 @@ get "/api/v1/search/suggestions" do |env|
query ||= ""
begin
- client = QUIC::Client.new("suggestqueries.google.com")
- client.family = CONFIG.force_resolve || Socket::Family::INET
- client.family = Socket::Family::INET if client.family == Socket::Family::UNSPEC
- response = client.get("/complete/search?hl=en&gl=#{region}&client=youtube&ds=yt&q=#{URI.encode_www_form(query)}&callback=suggestCallback").body
+ headers = HTTP::Headers{":authority" => "suggestqueries.google.com"}
+ response = YT_POOL.client &.get("/complete/search?hl=en&gl=#{region}&client=youtube&ds=yt&q=#{URI.encode_www_form(query)}&callback=suggestCallback", headers).body
body = response[35..-2]
body = JSON.parse(body).as_a
@@ -4578,7 +4546,7 @@ end
next error_message
end
- response = playlist.to_json(offset, locale, config, Kemal.config, continuation: continuation)
+ response = playlist.to_json(offset, locale, continuation: continuation)
if format == "html"
response = JSON.parse(response)
@@ -4642,7 +4610,7 @@ get "/api/v1/mixes/:rdid" do |env|
json.field "videoThumbnails" do
json.array do
- generate_thumbnails(json, video.id, config, Kemal.config)
+ generate_thumbnails(json, video.id)
end
end
@@ -4677,7 +4645,7 @@ get "/api/v1/auth/notifications" do |env|
topics = env.params.query["topics"]?.try &.split(",").uniq.first(1000)
topics ||= [] of String
- create_notification_stream(env, config, Kemal.config, decrypt_function, topics, connection_channel)
+ create_notification_stream(env, topics, connection_channel)
end
post "/api/v1/auth/notifications" do |env|
@@ -4686,7 +4654,7 @@ post "/api/v1/auth/notifications" do |env|
topics = env.params.body["topics"]?.try &.split(",").uniq.first(1000)
topics ||= [] of String
- create_notification_stream(env, config, Kemal.config, decrypt_function, topics, connection_channel)
+ create_notification_stream(env, topics, connection_channel)
end
get "/api/v1/auth/preferences" do |env|
@@ -4700,7 +4668,7 @@ post "/api/v1/auth/preferences" do |env|
user = env.get("user").as(User)
begin
- preferences = Preferences.from_json(env.request.body || "{}", user.preferences)
+ preferences = Preferences.from_json(env.request.body || "{}")
rescue
preferences = user.preferences
end
@@ -4730,7 +4698,7 @@ get "/api/v1/auth/feed" do |env|
json.field "notifications" do
json.array do
notifications.each do |video|
- video.to_json(locale, config, Kemal.config, json)
+ video.to_json(locale, json)
end
end
end
@@ -4738,7 +4706,7 @@ get "/api/v1/auth/feed" do |env|
json.field "videos" do
json.array do
videos.each do |video|
- video.to_json(locale, config, Kemal.config, json)
+ video.to_json(locale, json)
end
end
end
@@ -4810,7 +4778,7 @@ get "/api/v1/auth/playlists" do |env|
JSON.build do |json|
json.array do
playlists.each do |playlist|
- playlist.to_json(0, locale, config, Kemal.config, json)
+ playlist.to_json(0, locale, json)
end
end
end
@@ -4841,10 +4809,8 @@ post "/api/v1/auth/playlists" do |env|
next error_message
end
- host_url = make_host_url(config, Kemal.config)
-
playlist = create_playlist(PG_DB, title, privacy, user)
- env.response.headers["Location"] = "#{host_url}/api/v1/auth/playlists/#{playlist.id}"
+ env.response.headers["Location"] = "#{HOST_URL}/api/v1/auth/playlists/#{playlist.id}"
env.response.status_code = 201
{
"title" => title,
@@ -4956,17 +4922,17 @@ post "/api/v1/auth/playlists/:plid/videos" do |env|
next error_message
end
- playlist_video = PlaylistVideo.new(
- title: video.title,
- id: video.id,
- author: video.author,
- ucid: video.ucid,
+ playlist_video = PlaylistVideo.new({
+ title: video.title,
+ id: video.id,
+ author: video.author,
+ ucid: video.ucid,
length_seconds: video.length_seconds,
- published: video.published,
- plid: plid,
- live_now: video.live_now,
- index: Random::Secure.rand(0_i64..Int64::MAX)
- )
+ published: video.published,
+ plid: plid,
+ live_now: video.live_now,
+ index: Random::Secure.rand(0_i64..Int64::MAX),
+ })
video_array = playlist_video.to_a
args = arg_array(video_array)
@@ -4974,11 +4940,9 @@ post "/api/v1/auth/playlists/:plid/videos" do |env|
PG_DB.exec("INSERT INTO playlist_videos VALUES (#{args})", args: video_array)
PG_DB.exec("UPDATE playlists SET index = array_append(index, $1), video_count = video_count + 1, updated = $2 WHERE id = $3", playlist_video.index, Time.utc, plid)
- host_url = make_host_url(config, Kemal.config)
-
- env.response.headers["Location"] = "#{host_url}/api/v1/auth/playlists/#{plid}/videos/#{playlist_video.index.to_u64.to_s(16).upcase}"
+ env.response.headers["Location"] = "#{HOST_URL}/api/v1/auth/playlists/#{plid}/videos/#{playlist_video.index.to_u64.to_s(16).upcase}"
env.response.status_code = 201
- playlist_video.to_json(locale, config, Kemal.config, index: playlist.index.size)
+ playlist_video.to_json(locale, index: playlist.index.size)
end
delete "/api/v1/auth/playlists/:plid/videos/:index" do |env|
@@ -5156,7 +5120,7 @@ get "/api/manifest/dash/id/:id" do |env|
next
end
- if dashmpd = video.player_response["streamingData"]?.try &.["dashManifestUrl"]?.try &.as_s
+ if dashmpd = video.dash_manifest_url
manifest = YT_POOL.client &.get(URI.parse(dashmpd).full_path).body
manifest = manifest.gsub(/<BaseURL>[^<]+<\/BaseURL>/) do |baseurl|
@@ -5173,16 +5137,16 @@ get "/api/manifest/dash/id/:id" do |env|
next manifest
end
- adaptive_fmts = video.adaptive_fmts(decrypt_function)
+ adaptive_fmts = video.adaptive_fmts
if local
adaptive_fmts.each do |fmt|
- fmt["url"] = URI.parse(fmt["url"]).full_path
+ fmt["url"] = JSON::Any.new(URI.parse(fmt["url"].as_s).full_path)
end
end
- audio_streams = video.audio_streams(adaptive_fmts)
- video_streams = video.video_streams(adaptive_fmts).sort_by { |stream| {stream["size"].split("x")[0].to_i, stream["fps"].to_i} }.reverse
+ audio_streams = video.audio_streams
+ video_streams = video.video_streams.sort_by { |stream| {stream["width"].as_i, stream["fps"].as_i} }.reverse
XML.build(indent: " ", encoding: "UTF-8") do |xml|
xml.element("MPD", "xmlns": "urn:mpeg:dash:schema:mpd:2011",
@@ -5192,24 +5156,22 @@ get "/api/manifest/dash/id/:id" do |env|
i = 0
{"audio/mp4", "audio/webm"}.each do |mime_type|
- mime_streams = audio_streams.select { |stream| stream["type"].starts_with? mime_type }
- if mime_streams.empty?
- next
- end
+ mime_streams = audio_streams.select { |stream| stream["mimeType"].as_s.starts_with? mime_type }
+ next if mime_streams.empty?
xml.element("AdaptationSet", id: i, mimeType: mime_type, startWithSAP: 1, subsegmentAlignment: true) do
mime_streams.each do |fmt|
- codecs = fmt["type"].split("codecs=")[1].strip('"')
- bandwidth = fmt["bitrate"].to_i * 1000
- itag = fmt["itag"]
- url = fmt["url"]
+ codecs = fmt["mimeType"].as_s.split("codecs=")[1].strip('"')
+ bandwidth = fmt["bitrate"].as_i
+ itag = fmt["itag"].as_i
+ url = fmt["url"].as_s
xml.element("Representation", id: fmt["itag"], codecs: codecs, bandwidth: bandwidth) do
xml.element("AudioChannelConfiguration", schemeIdUri: "urn:mpeg:dash:23003:3:audio_channel_configuration:2011",
value: "2")
xml.element("BaseURL") { xml.text url }
- xml.element("SegmentBase", indexRange: fmt["index"]) do
- xml.element("Initialization", range: fmt["init"])
+ xml.element("SegmentBase", indexRange: "#{fmt["indexRange"]["start"]}-#{fmt["indexRange"]["end"]}") do
+ xml.element("Initialization", range: "#{fmt["initRange"]["start"]}-#{fmt["initRange"]["end"]}")
end
end
end
@@ -5218,21 +5180,24 @@ get "/api/manifest/dash/id/:id" do |env|
i += 1
end
+ potential_heights = {4320, 2160, 1440, 1080, 720, 480, 360, 240, 144}
+
{"video/mp4", "video/webm"}.each do |mime_type|
- mime_streams = video_streams.select { |stream| stream["type"].starts_with? mime_type }
+ mime_streams = video_streams.select { |stream| stream["mimeType"].as_s.starts_with? mime_type }
next if mime_streams.empty?
heights = [] of Int32
xml.element("AdaptationSet", id: i, mimeType: mime_type, startWithSAP: 1, subsegmentAlignment: true, scanType: "progressive") do
mime_streams.each do |fmt|
- codecs = fmt["type"].split("codecs=")[1].strip('"')
- bandwidth = fmt["bitrate"]
- itag = fmt["itag"]
- url = fmt["url"]
- width, height = fmt["size"].split("x").map { |i| i.to_i }
+ codecs = fmt["mimeType"].as_s.split("codecs=")[1].strip('"')
+ bandwidth = fmt["bitrate"].as_i
+ itag = fmt["itag"].as_i
+ url = fmt["url"].as_s
+ width = fmt["width"].as_i
+ height = fmt["height"].as_i
# Resolutions reported by YouTube player (may not accurately reflect source)
- height = [4320, 2160, 1440, 1080, 720, 480, 360, 240, 144].sort_by { |i| (height - i).abs }[0]
+ height = potential_heights.min_by { |i| (height - i).abs }
next if unique_res && heights.includes? height
heights << height
@@ -5240,8 +5205,8 @@ get "/api/manifest/dash/id/:id" do |env|
startWithSAP: "1", maxPlayoutRate: "1",
bandwidth: bandwidth, frameRate: fmt["fps"]) do
xml.element("BaseURL") { xml.text url }
- xml.element("SegmentBase", indexRange: fmt["index"]) do
- xml.element("Initialization", range: fmt["init"])
+ xml.element("SegmentBase", indexRange: "#{fmt["indexRange"]["start"]}-#{fmt["indexRange"]["end"]}") do
+ xml.element("Initialization", range: "#{fmt["initRange"]["start"]}-#{fmt["initRange"]["end"]}")
end
end
end
@@ -5255,10 +5220,10 @@ get "/api/manifest/dash/id/:id" do |env|
end
get "/api/manifest/hls_variant/*" do |env|
- manifest = YT_POOL.client &.get(env.request.path)
+ response = YT_POOL.client &.get(env.request.path)
- if manifest.status_code != 200
- env.response.status_code = manifest.status_code
+ if response.status_code != 200
+ env.response.status_code = response.status_code
next
end
@@ -5267,12 +5232,10 @@ get "/api/manifest/hls_variant/*" do |env|
env.response.content_type = "application/x-mpegURL"
env.response.headers.add("Access-Control-Allow-Origin", "*")
- host_url = make_host_url(config, Kemal.config)
-
- manifest = manifest.body
+ manifest = response.body
if local
- manifest = manifest.gsub("https://www.youtube.com", host_url)
+ manifest = manifest.gsub("https://www.youtube.com", HOST_URL)
manifest = manifest.gsub("index.m3u8", "index.m3u8?local=true")
end
@@ -5280,10 +5243,10 @@ get "/api/manifest/hls_variant/*" do |env|
end
get "/api/manifest/hls_playlist/*" do |env|
- manifest = YT_POOL.client &.get(env.request.path)
+ response = YT_POOL.client &.get(env.request.path)
- if manifest.status_code != 200
- env.response.status_code = manifest.status_code
+ if response.status_code != 200
+ env.response.status_code = response.status_code
next
end
@@ -5292,9 +5255,7 @@ get "/api/manifest/hls_playlist/*" do |env|
env.response.content_type = "application/x-mpegURL"
env.response.headers.add("Access-Control-Allow-Origin", "*")
- host_url = make_host_url(config, Kemal.config)
-
- manifest = manifest.body
+ manifest = response.body
if local
manifest = manifest.gsub(/^https:\/\/r\d---.{11}\.c\.youtube\.com[^\n]*/m) do |match|
@@ -5329,7 +5290,7 @@ get "/api/manifest/hls_playlist/*" do |env|
raw_params["local"] = "true"
- "#{host_url}/videoplayback?#{raw_params}"
+ "#{HOST_URL}/videoplayback?#{raw_params}"
end
end
@@ -5355,7 +5316,7 @@ get "/latest_version" do |env|
end
id ||= env.params.query["id"]?
- itag ||= env.params.query["itag"]?
+ itag ||= env.params.query["itag"]?.try &.to_i
region = env.params.query["region"]?
@@ -5370,26 +5331,16 @@ get "/latest_version" do |env|
video = get_video(id, PG_DB, region: region)
- fmt_stream = video.fmt_stream(decrypt_function)
- adaptive_fmts = video.adaptive_fmts(decrypt_function)
+ fmt = video.fmt_stream.find(nil) { |f| f["itag"].as_i == itag } || video.adaptive_fmts.find(nil) { |f| f["itag"].as_i == itag }
+ url = fmt.try &.["url"]?.try &.as_s
- urls = (fmt_stream + adaptive_fmts).select { |fmt| fmt["itag"] == itag }
- if urls.empty?
+ if !url
env.response.status_code = 404
next
- elsif urls.size > 1
- env.response.status_code = 409
- next
end
- url = urls[0]["url"]
- if local
- url = URI.parse(url).full_path.not_nil!
- end
-
- if title
- url += "&title=#{title}"
- end
+ url = URI.parse(url).full_path.not_nil! if local
+ url = "#{url}&title=#{title}" if title
env.redirect url
end
@@ -5482,8 +5433,8 @@ get "/videoplayback" do |env|
end
client = make_client(URI.parse(host), region)
-
response = HTTP::Client::Response.new(500)
+ error = ""
5.times do
begin
response = client.head(url, headers)
@@ -5508,12 +5459,14 @@ get "/videoplayback" do |env|
host = "https://r#{fvip}---#{mn}.googlevideo.com"
client = make_client(URI.parse(host), region)
rescue ex
+ error = ex.message
end
end
if response.status_code >= 400
env.response.status_code = response.status_code
- next
+ env.response.content_type = "text/plain"
+ next error
end
if url.includes? "&file=seg.ts"
@@ -5644,11 +5597,9 @@ get "/videoplayback" do |env|
end
get "/ggpht/*" do |env|
- host = "https://yt3.ggpht.com"
- client = make_client(URI.parse(host))
url = env.request.path.lchop("/ggpht")
- headers = HTTP::Headers.new
+ headers = HTTP::Headers{":authority" => "yt3.ggpht.com"}
REQUEST_HEADERS_WHITELIST.each do |header|
if env.request.headers[header]?
headers[header] = env.request.headers[header]
@@ -5656,7 +5607,7 @@ get "/ggpht/*" do |env|
end
begin
- client.get(url, headers) do |response|
+ YT_POOL.client &.get(url, headers) do |response|
env.response.status_code = response.status_code
response.headers.each do |key, value|
if !RESPONSE_HEADERS_BLACKLIST.includes?(key.downcase)
@@ -5689,16 +5640,16 @@ get "/sb/:id/:storyboard/:index" do |env|
storyboard = env.params.url["storyboard"]
index = env.params.url["index"]
+ url = "/sb/#{id}/#{storyboard}/#{index}?#{env.params.query}"
+
+ headers = HTTP::Headers.new
+
if storyboard.starts_with? "storyboard_live"
- host = "https://i.ytimg.com"
+ headers[":authority"] = "i.ytimg.com"
else
- host = "https://i9.ytimg.com"
+ headers[":authority"] = "i9.ytimg.com"
end
- client = make_client(URI.parse(host))
-
- url = "/sb/#{id}/#{storyboard}/#{index}?#{env.params.query}"
- headers = HTTP::Headers.new
REQUEST_HEADERS_WHITELIST.each do |header|
if env.request.headers[header]?
headers[header] = env.request.headers[header]
@@ -5706,7 +5657,7 @@ get "/sb/:id/:storyboard/:index" do |env|
end
begin
- client.get(url, headers) do |response|
+ YT_POOL.client &.get(url, headers) do |response|
env.response.status_code = response.status_code
response.headers.each do |key, value|
if !RESPONSE_HEADERS_BLACKLIST.includes?(key.downcase)
@@ -5714,6 +5665,7 @@ get "/sb/:id/:storyboard/:index" do |env|
end
end
+ env.response.headers["Connection"] = "close"
env.response.headers["Access-Control-Allow-Origin"] = "*"
if response.status_code >= 300
@@ -5731,11 +5683,9 @@ get "/s_p/:id/:name" do |env|
id = env.params.url["id"]
name = env.params.url["name"]
- host = "https://i9.ytimg.com"
- client = make_client(URI.parse(host))
url = env.request.resource
- headers = HTTP::Headers.new
+ headers = HTTP::Headers{":authority" => "i9.ytimg.com"}
REQUEST_HEADERS_WHITELIST.each do |header|
if env.request.headers[header]?
headers[header] = env.request.headers[header]
@@ -5743,7 +5693,7 @@ get "/s_p/:id/:name" do |env|
end
begin
- client.get(url, headers) do |response|
+ YT_POOL.client &.get(url, headers) do |response|
env.response.status_code = response.status_code
response.headers.each do |key, value|
if !RESPONSE_HEADERS_BLACKLIST.includes?(key.downcase)
@@ -5798,9 +5748,11 @@ get "/vi/:id/:name" do |env|
id = env.params.url["id"]
name = env.params.url["name"]
+ headers = HTTP::Headers{":authority" => "i.ytimg.com"}
+
if name == "maxres.jpg"
- build_thumbnails(id, config, Kemal.config).each do |thumb|
- if YT_IMG_POOL.client &.head("/vi/#{id}/#{thumb[:url]}.jpg").status_code == 200
+ build_thumbnails(id).each do |thumb|
+ if YT_POOL.client &.head("/vi/#{id}/#{thumb[:url]}.jpg", headers).status_code == 200
name = thumb[:url] + ".jpg"
break
end
@@ -5808,7 +5760,6 @@ get "/vi/:id/:name" do |env|
end
url = "/vi/#{id}/#{name}"
- headers = HTTP::Headers.new
REQUEST_HEADERS_WHITELIST.each do |header|
if env.request.headers[header]?
headers[header] = env.request.headers[header]
@@ -5816,7 +5767,7 @@ get "/vi/:id/:name" do |env|
end
begin
- YT_IMG_POOL.client &.get(url, headers) do |response|
+ YT_POOL.client &.get(url, headers) do |response|
env.response.status_code = response.status_code
response.headers.each do |key, value|
if !RESPONSE_HEADERS_BLACKLIST.includes?(key.downcase)
@@ -5838,8 +5789,8 @@ get "/vi/:id/:name" do |env|
end
get "/Captcha" do |env|
- client = make_client(LOGIN_URL)
- response = client.get(env.request.resource)
+ 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
@@ -5904,7 +5855,7 @@ end
error 500 do |env|
error_message = <<-END_HTML
Looks like you've found a bug in Invidious. Feel free to open a new issue
- <a href="https://github.com/omarroth/invidious/issues">here</a>
+ <a href="https://github.com/iv-org/invidious/issues">here</a>
or send an email to
<a href="mailto:#{CONFIG.admin_email}">#{CONFIG.admin_email}</a>.
END_HTML