mirror of https://github.com/iv-org/invidious.git
commit
caca445234
566
src/invidious.cr
566
src/invidious.cr
|
@ -189,42 +189,14 @@ get "/watch" do |env|
|
||||||
end
|
end
|
||||||
|
|
||||||
preferences = user.preferences
|
preferences = user.preferences
|
||||||
subscriptions = user.subscriptions.as(Array(String))
|
subscriptions = user.subscriptions
|
||||||
end
|
end
|
||||||
subscriptions ||= [] of String
|
subscriptions ||= [] of String
|
||||||
|
|
||||||
autoplay = env.params.query["autoplay"]?.try &.to_i?
|
autoplay, video_loop, video_start, video_end, listen = process_video_params(env.params.query, preferences)
|
||||||
video_loop = env.params.query["loop"]?.try &.to_i?
|
if listen
|
||||||
|
|
||||||
if preferences
|
|
||||||
autoplay ||= preferences.autoplay.to_unsafe
|
|
||||||
video_loop ||= preferences.video_loop.to_unsafe
|
|
||||||
end
|
|
||||||
|
|
||||||
autoplay ||= 0
|
|
||||||
video_loop ||= 0
|
|
||||||
|
|
||||||
autoplay = autoplay == 1
|
|
||||||
video_loop = video_loop == 1
|
|
||||||
|
|
||||||
if env.params.query["start"]?
|
|
||||||
video_start = decode_time(env.params.query["start"])
|
|
||||||
end
|
|
||||||
if env.params.query["t"]?
|
|
||||||
video_start = decode_time(env.params.query["t"])
|
|
||||||
end
|
|
||||||
video_start ||= 0
|
|
||||||
|
|
||||||
if env.params.query["end"]?
|
|
||||||
video_end = decode_time(env.params.query["end"])
|
|
||||||
end
|
|
||||||
video_end ||= -1
|
|
||||||
|
|
||||||
if env.params.query["listen"]? && (env.params.query["listen"] == "true" || env.params.query["listen"] == "1")
|
|
||||||
listen = true
|
|
||||||
env.params.query.delete_all("listen")
|
env.params.query.delete_all("listen")
|
||||||
end
|
end
|
||||||
listen ||= false
|
|
||||||
|
|
||||||
begin
|
begin
|
||||||
video = get_video(id, PG_DB)
|
video = get_video(id, PG_DB)
|
||||||
|
@ -234,58 +206,27 @@ get "/watch" do |env|
|
||||||
next templated "error"
|
next templated "error"
|
||||||
end
|
end
|
||||||
|
|
||||||
fmt_stream = [] of HTTP::Params
|
fmt_stream = video.fmt_stream(decrypt_function)
|
||||||
video.info["url_encoded_fmt_stream_map"].split(",") do |string|
|
adaptive_fmts = video.adaptive_fmts(decrypt_function)
|
||||||
if !string.empty?
|
audio_streams = video.audio_streams(adaptive_fmts)
|
||||||
fmt_stream << HTTP::Params.parse(string)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
fmt_stream.each { |s| s.add("label", "#{s["quality"]} - #{s["type"].split(";")[0].split("/")[1]}") }
|
captions = video.captions
|
||||||
fmt_stream = fmt_stream.uniq { |s| s["label"] }
|
|
||||||
|
|
||||||
adaptive_fmts = [] of HTTP::Params
|
video.description = fill_links(video.description, "https", "www.youtube.com")
|
||||||
if video.info.has_key?("adaptive_fmts")
|
video.description = add_alt_links(video.description)
|
||||||
video.info["adaptive_fmts"].split(",") do |string|
|
description = video.short_description
|
||||||
adaptive_fmts << HTTP::Params.parse(string)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
if fmt_stream[0]? && fmt_stream[0]["s"]?
|
host_url = make_host_url(Kemal.config.ssl || CONFIG.https_only, env.request.headers["Host"])
|
||||||
adaptive_fmts.each do |fmt|
|
host_params = env.request.query_params
|
||||||
fmt["url"] += "&signature=" + decrypt_signature(fmt["s"], decrypt_function)
|
host_params.delete_all("v")
|
||||||
end
|
|
||||||
|
|
||||||
fmt_stream.each do |fmt|
|
|
||||||
fmt["url"] += "&signature=" + decrypt_signature(fmt["s"], decrypt_function)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
audio_streams = adaptive_fmts.compact_map { |s| s["type"].starts_with?("audio") ? s : nil }
|
|
||||||
audio_streams.sort_by! { |s| s["bitrate"].to_i }.reverse!
|
|
||||||
audio_streams.each do |stream|
|
|
||||||
stream["bitrate"] = (stream["bitrate"].to_f64/1000).to_i.to_s
|
|
||||||
end
|
|
||||||
|
|
||||||
player_response = JSON.parse(video.info["player_response"])
|
|
||||||
if player_response["captions"]?
|
|
||||||
captions = player_response["captions"]["playerCaptionsTracklistRenderer"]["captionTracks"]?.try &.as_a
|
|
||||||
end
|
|
||||||
captions ||= [] of JSON::Any
|
|
||||||
|
|
||||||
if video.info["hlsvp"]?
|
if video.info["hlsvp"]?
|
||||||
hlsvp = video.info["hlsvp"]
|
hlsvp = video.info["hlsvp"]
|
||||||
|
hlsvp = hlsvp.gsub("https://manifest.googlevideo.com", host_url)
|
||||||
if Kemal.config.ssl || CONFIG.https_only
|
|
||||||
scheme = "https://"
|
|
||||||
else
|
|
||||||
scheme = "http://"
|
|
||||||
end
|
end
|
||||||
host = env.request.headers["Host"]
|
|
||||||
url = "#{scheme}#{host}"
|
|
||||||
|
|
||||||
hlsvp = hlsvp.gsub("https://manifest.googlevideo.com", url)
|
# TODO: Find highest resolution thumbnail automatically
|
||||||
end
|
thumbnail = "https://i.ytimg.com/vi/#{video.id}/mqdefault.jpg"
|
||||||
|
|
||||||
rvs = [] of Hash(String, String)
|
rvs = [] of Hash(String, String)
|
||||||
if video.info.has_key?("rvs")
|
if video.info.has_key?("rvs")
|
||||||
|
@ -295,14 +236,8 @@ get "/watch" do |env|
|
||||||
end
|
end
|
||||||
|
|
||||||
rating = video.info["avg_rating"].to_f64
|
rating = video.info["avg_rating"].to_f64
|
||||||
|
|
||||||
engagement = ((video.dislikes.to_f + video.likes.to_f)/video.views * 100)
|
engagement = ((video.dislikes.to_f + video.likes.to_f)/video.views * 100)
|
||||||
|
|
||||||
if video.likes > 0 || video.dislikes > 0
|
|
||||||
calculated_rating = (video.likes.to_f/(video.likes.to_f + video.dislikes.to_f) * 4 + 1)
|
|
||||||
end
|
|
||||||
calculated_rating ||= 0.0
|
|
||||||
|
|
||||||
if video.info["ad_slots"]?
|
if video.info["ad_slots"]?
|
||||||
ad_slots = video.info["ad_slots"].split(",")
|
ad_slots = video.info["ad_slots"].split(",")
|
||||||
ad_slots = ad_slots.join(", ")
|
ad_slots = ad_slots.join(", ")
|
||||||
|
@ -326,32 +261,6 @@ get "/watch" do |env|
|
||||||
k2 = k2.join(", ")
|
k2 = k2.join(", ")
|
||||||
end
|
end
|
||||||
|
|
||||||
video.description = fill_links(video.description, "https", "www.youtube.com")
|
|
||||||
video.description = add_alt_links(video.description)
|
|
||||||
|
|
||||||
description = video.description.gsub("<br>", " ")
|
|
||||||
description = description.gsub("<br/>", " ")
|
|
||||||
description = XML.parse_html(description).content[0..200].gsub('"', """).gsub("\n", " ").strip(" ")
|
|
||||||
if description.empty?
|
|
||||||
description = " "
|
|
||||||
end
|
|
||||||
|
|
||||||
if Kemal.config.ssl || CONFIG.https_only
|
|
||||||
scheme = "https://"
|
|
||||||
else
|
|
||||||
scheme = "http://"
|
|
||||||
end
|
|
||||||
host = env.request.headers["Host"]
|
|
||||||
host_url = "#{scheme}#{host}"
|
|
||||||
host_params = env.request.query_params
|
|
||||||
host_params.delete_all("v")
|
|
||||||
|
|
||||||
if fmt_stream.select { |x| x["label"].starts_with? "hd720" }.size != 0
|
|
||||||
thumbnail = "https://i.ytimg.com/vi/#{video.id}/maxresdefault.jpg"
|
|
||||||
else
|
|
||||||
thumbnail = "https://i.ytimg.com/vi/#{video.id}/hqdefault.jpg"
|
|
||||||
end
|
|
||||||
|
|
||||||
templated "watch"
|
templated "watch"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -366,44 +275,7 @@ get "/embed/:id" do |env|
|
||||||
next env.redirect "/"
|
next env.redirect "/"
|
||||||
end
|
end
|
||||||
|
|
||||||
if env.params.query["start"]?
|
autoplay, video_loop, video_start, video_end, listen, raw, quality, autoplay, controls = process_video_params(env.params.query, nil)
|
||||||
video_start = decode_time(env.params.query["start"])
|
|
||||||
end
|
|
||||||
|
|
||||||
if env.params.query["t"]?
|
|
||||||
video_start = decode_time(env.params.query["t"])
|
|
||||||
end
|
|
||||||
video_start ||= 0
|
|
||||||
|
|
||||||
if env.params.query["end"]?
|
|
||||||
video_end = decode_time(env.params.query["end"])
|
|
||||||
end
|
|
||||||
video_end ||= -1
|
|
||||||
|
|
||||||
if env.params.query["listen"]? && (env.params.query["listen"] == "true" || env.params.query["listen"] == "1")
|
|
||||||
listen = true
|
|
||||||
env.params.query.delete_all("listen")
|
|
||||||
end
|
|
||||||
listen ||= false
|
|
||||||
|
|
||||||
raw = env.params.query["raw"]?.try &.to_i?
|
|
||||||
raw ||= 0
|
|
||||||
raw = raw == 1
|
|
||||||
|
|
||||||
quality = env.params.query["quality"]?
|
|
||||||
quality ||= "hd720"
|
|
||||||
|
|
||||||
autoplay = env.params.query["autoplay"]?.try &.to_i?
|
|
||||||
autoplay ||= 0
|
|
||||||
autoplay = autoplay == 1
|
|
||||||
|
|
||||||
controls = env.params.query["controls"]?.try &.to_i?
|
|
||||||
controls ||= 1
|
|
||||||
controls = controls == 1
|
|
||||||
|
|
||||||
video_loop = env.params.query["loop"]?.try &.to_i?
|
|
||||||
video_loop ||= 0
|
|
||||||
video_loop = video_loop == 1
|
|
||||||
|
|
||||||
begin
|
begin
|
||||||
video = get_video(id, PG_DB)
|
video = get_video(id, PG_DB)
|
||||||
|
@ -412,58 +284,27 @@ get "/embed/:id" do |env|
|
||||||
next templated "error"
|
next templated "error"
|
||||||
end
|
end
|
||||||
|
|
||||||
player_response = JSON.parse(video.info["player_response"])
|
fmt_stream = video.fmt_stream(decrypt_function)
|
||||||
if player_response["captions"]?
|
adaptive_fmts = video.adaptive_fmts(decrypt_function)
|
||||||
captions = player_response["captions"]["playerCaptionsTracklistRenderer"]["captionTracks"]?.try &.as_a
|
audio_streams = video.audio_streams(adaptive_fmts)
|
||||||
end
|
|
||||||
captions ||= [] of JSON::Any
|
captions = video.captions
|
||||||
|
|
||||||
|
video.description = fill_links(video.description, "https", "www.youtube.com")
|
||||||
|
video.description = add_alt_links(video.description)
|
||||||
|
description = video.short_description
|
||||||
|
|
||||||
|
host_url = make_host_url(Kemal.config.ssl || CONFIG.https_only, env.request.headers["Host"])
|
||||||
|
host_params = env.request.query_params
|
||||||
|
host_params.delete_all("v")
|
||||||
|
|
||||||
if video.info["hlsvp"]?
|
if video.info["hlsvp"]?
|
||||||
hlsvp = video.info["hlsvp"]
|
hlsvp = video.info["hlsvp"]
|
||||||
|
hlsvp = hlsvp.gsub("https://manifest.googlevideo.com", host_url)
|
||||||
if Kemal.config.ssl || CONFIG.https_only
|
|
||||||
scheme = "https://"
|
|
||||||
else
|
|
||||||
scheme = "http://"
|
|
||||||
end
|
|
||||||
host = env.request.headers["Host"]
|
|
||||||
url = "#{scheme}#{host}"
|
|
||||||
|
|
||||||
hlsvp = hlsvp.gsub("https://manifest.googlevideo.com", url)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
fmt_stream = [] of HTTP::Params
|
# TODO: Find highest resolution thumbnail automatically
|
||||||
video.info["url_encoded_fmt_stream_map"].split(",") do |string|
|
thumbnail = "https://i.ytimg.com/vi/#{video.id}/mqdefault.jpg"
|
||||||
if !string.empty?
|
|
||||||
fmt_stream << HTTP::Params.parse(string)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
fmt_stream.each { |s| s.add("label", "#{s["quality"]} - #{s["type"].split(";")[0].split("/")[1]}") }
|
|
||||||
fmt_stream = fmt_stream.uniq { |s| s["label"] }
|
|
||||||
|
|
||||||
adaptive_fmts = [] of HTTP::Params
|
|
||||||
if video.info.has_key?("adaptive_fmts")
|
|
||||||
video.info["adaptive_fmts"].split(",") do |string|
|
|
||||||
adaptive_fmts << HTTP::Params.parse(string)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
if adaptive_fmts[0]? && adaptive_fmts[0]["s"]?
|
|
||||||
adaptive_fmts.each do |fmt|
|
|
||||||
fmt["url"] += "&signature=" + decrypt_signature(fmt["s"], decrypt_function)
|
|
||||||
end
|
|
||||||
|
|
||||||
fmt_stream.each do |fmt|
|
|
||||||
fmt["url"] += "&signature=" + decrypt_signature(fmt["s"], decrypt_function)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
audio_streams = adaptive_fmts.compact_map { |s| s["type"].starts_with?("audio") ? s : nil }
|
|
||||||
audio_streams.sort_by! { |s| s["bitrate"].to_i }.reverse!
|
|
||||||
audio_streams.each do |stream|
|
|
||||||
stream["bitrate"] = (stream["bitrate"].to_f64/1000).to_i.to_s
|
|
||||||
end
|
|
||||||
|
|
||||||
if raw
|
if raw
|
||||||
url = fmt_stream[0]["url"]
|
url = fmt_stream[0]["url"]
|
||||||
|
@ -477,32 +318,6 @@ get "/embed/:id" do |env|
|
||||||
next env.redirect url
|
next env.redirect url
|
||||||
end
|
end
|
||||||
|
|
||||||
video.description = fill_links(video.description, "https", "www.youtube.com")
|
|
||||||
video.description = add_alt_links(video.description)
|
|
||||||
|
|
||||||
description = video.description.gsub("<br>", " ")
|
|
||||||
description = description.gsub("<br/>", " ")
|
|
||||||
description = XML.parse_html(description).content[0..200].gsub('"', """).gsub("\n", " ").strip(" ")
|
|
||||||
if description.empty?
|
|
||||||
description = " "
|
|
||||||
end
|
|
||||||
|
|
||||||
if Kemal.config.ssl || CONFIG.https_only
|
|
||||||
scheme = "https://"
|
|
||||||
else
|
|
||||||
scheme = "http://"
|
|
||||||
end
|
|
||||||
host = env.request.headers["Host"]
|
|
||||||
host_url = "#{scheme}#{host}"
|
|
||||||
host_params = env.request.query_params
|
|
||||||
host_params.delete_all("v")
|
|
||||||
|
|
||||||
if fmt_stream.select { |x| x["label"].starts_with? "hd720" }.size != 0
|
|
||||||
thumbnail = "https://i.ytimg.com/vi/#{video.id}/maxresdefault.jpg"
|
|
||||||
else
|
|
||||||
thumbnail = "https://i.ytimg.com/vi/#{video.id}/hqdefault.jpg"
|
|
||||||
end
|
|
||||||
|
|
||||||
rendered "embed"
|
rendered "embed"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -510,24 +325,25 @@ end
|
||||||
|
|
||||||
get "/results" do |env|
|
get "/results" do |env|
|
||||||
search_query = env.params.query["search_query"]?
|
search_query = env.params.query["search_query"]?
|
||||||
|
page = env.params.query["page"]?.try &.to_i?
|
||||||
|
page ||= 1
|
||||||
|
|
||||||
if search_query
|
if search_query
|
||||||
env.redirect "/search?q=#{URI.escape(search_query)}"
|
env.redirect "/search?q=#{URI.escape(search_query)}&page=#{page}"
|
||||||
else
|
else
|
||||||
env.redirect "/"
|
env.redirect "/"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
get "/search" do |env|
|
get "/search" do |env|
|
||||||
if env.params.query["q"]?
|
query = env.params.query["q"]?
|
||||||
query = env.params.query["q"]
|
query ||= ""
|
||||||
else
|
|
||||||
next env.redirect "/"
|
|
||||||
end
|
|
||||||
|
|
||||||
page = env.params.query["page"]?.try &.to_i?
|
page = env.params.query["page"]?.try &.to_i?
|
||||||
page ||= 1
|
page ||= 1
|
||||||
|
|
||||||
videos = search(query, page)
|
search_params = build_search_params(sort_by: "relevance", content_type: "video")
|
||||||
|
videos = search(query, page, search_params)
|
||||||
|
|
||||||
templated "search"
|
templated "search"
|
||||||
end
|
end
|
||||||
|
@ -930,11 +746,8 @@ post "/preferences" do |env|
|
||||||
env.redirect referer
|
env.redirect referer
|
||||||
end
|
end
|
||||||
|
|
||||||
# Function that is useful if you have multiple channels that don't have
|
# /modify_notifications
|
||||||
# the bell dinged. Request parameters are fairly self-explanatory,
|
# will "ding" all subscriptions.
|
||||||
# receive_all_updates = true and receive_post_updates = true will ding all
|
|
||||||
# channels. Calling /modify_notifications without any arguments will
|
|
||||||
# request all notifications from all channels.
|
|
||||||
# /modify_notifications?receive_all_updates=false&receive_no_updates=false
|
# /modify_notifications?receive_all_updates=false&receive_no_updates=false
|
||||||
# will "unding" all subscriptions.
|
# will "unding" all subscriptions.
|
||||||
get "/modify_notifications" do |env|
|
get "/modify_notifications" do |env|
|
||||||
|
@ -1022,14 +835,7 @@ get "/subscription_manager" do |env|
|
||||||
subscriptions.sort_by! { |channel| channel.author.downcase }
|
subscriptions.sort_by! { |channel| channel.author.downcase }
|
||||||
|
|
||||||
if action_takeout
|
if action_takeout
|
||||||
if Kemal.config.ssl || CONFIG.https_only
|
host_url = make_host_url(Kemal.config.ssl || CONFIG.https_only, env.request.headers["Host"])
|
||||||
scheme = "https://"
|
|
||||||
else
|
|
||||||
scheme = "http://"
|
|
||||||
end
|
|
||||||
host = env.request.headers["Host"]
|
|
||||||
|
|
||||||
url = "#{scheme}#{host}"
|
|
||||||
|
|
||||||
if format == "json"
|
if format == "json"
|
||||||
env.response.content_type = "application/json"
|
env.response.content_type = "application/json"
|
||||||
|
@ -1056,7 +862,7 @@ get "/subscription_manager" do |env|
|
||||||
if format == "newpipe"
|
if format == "newpipe"
|
||||||
xmlUrl = "https://www.youtube.com/feeds/videos.xml?channel_id=#{channel.id}"
|
xmlUrl = "https://www.youtube.com/feeds/videos.xml?channel_id=#{channel.id}"
|
||||||
else
|
else
|
||||||
xmlUrl = "#{url}/feed/channel/#{channel.id}"
|
xmlUrl = "#{host_url}/feed/channel/#{channel.id}"
|
||||||
end
|
end
|
||||||
|
|
||||||
xml.element("outline", text: channel.author, title: channel.author,
|
xml.element("outline", text: channel.author, title: channel.author,
|
||||||
|
@ -1444,26 +1250,21 @@ get "/feed/channel/:ucid" do |env|
|
||||||
end
|
end
|
||||||
document = XML.parse_html(content_html)
|
document = XML.parse_html(content_html)
|
||||||
|
|
||||||
if Kemal.config.ssl || CONFIG.https_only
|
host_url = make_host_url(Kemal.config.ssl || CONFIG.https_only, env.request.headers["Host"])
|
||||||
scheme = "https://"
|
|
||||||
else
|
|
||||||
scheme = "http://"
|
|
||||||
end
|
|
||||||
host = env.request.headers["Host"]
|
|
||||||
path = env.request.path
|
path = env.request.path
|
||||||
|
|
||||||
feed = XML.build(indent: " ", encoding: "UTF-8") do |xml|
|
feed = XML.build(indent: " ", encoding: "UTF-8") do |xml|
|
||||||
xml.element("feed", "xmlns:yt": "http://www.youtube.com/xml/schemas/2015",
|
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") do
|
"xmlns:media": "http://search.yahoo.com/mrss/", xmlns: "http://www.w3.org/2005/Atom") do
|
||||||
xml.element("link", rel: "self", href: "#{scheme}#{host}#{path}")
|
xml.element("link", rel: "self", href: "#{host_url}#{path}")
|
||||||
xml.element("id") { xml.text "yt:channel:#{ucid}" }
|
xml.element("id") { xml.text "yt:channel:#{ucid}" }
|
||||||
xml.element("yt:channelId") { xml.text ucid }
|
xml.element("yt:channelId") { xml.text ucid }
|
||||||
xml.element("title") { xml.text channel.author }
|
xml.element("title") { xml.text channel.author }
|
||||||
xml.element("link", rel: "alternate", href: "#{scheme}#{host}/channel/#{ucid}")
|
xml.element("link", rel: "alternate", href: "#{host_url}/channel/#{ucid}")
|
||||||
|
|
||||||
xml.element("author") do
|
xml.element("author") do
|
||||||
xml.element("name") { xml.text channel.author }
|
xml.element("name") { xml.text channel.author }
|
||||||
xml.element("uri") { xml.text "#{scheme}#{host}/channel/#{ucid}" }
|
xml.element("uri") { xml.text "#{host_url}/channel/#{ucid}" }
|
||||||
end
|
end
|
||||||
|
|
||||||
document.xpath_nodes(%q(//li[contains(@class, "feed-item-container")])).each do |node|
|
document.xpath_nodes(%q(//li[contains(@class, "feed-item-container")])).each do |node|
|
||||||
|
@ -1502,11 +1303,11 @@ get "/feed/channel/:ucid" do |env|
|
||||||
xml.element("yt:videoId") { xml.text video_id }
|
xml.element("yt:videoId") { xml.text video_id }
|
||||||
xml.element("yt:channelId") { xml.text ucid }
|
xml.element("yt:channelId") { xml.text ucid }
|
||||||
xml.element("title") { xml.text title }
|
xml.element("title") { xml.text title }
|
||||||
xml.element("link", rel: "alternate", href: "#{scheme}#{host}/watch?v=#{video_id}")
|
xml.element("link", rel: "alternate", href: "#{host_url}/watch?v=#{video_id}")
|
||||||
|
|
||||||
xml.element("author") do
|
xml.element("author") do
|
||||||
xml.element("name") { xml.text channel.author }
|
xml.element("name") { xml.text channel.author }
|
||||||
xml.element("uri") { xml.text "#{scheme}#{host}/channel/#{ucid}" }
|
xml.element("uri") { xml.text "#{host_url}/channel/#{ucid}" }
|
||||||
end
|
end
|
||||||
|
|
||||||
xml.element("published") { xml.text published.to_s("%Y-%m-%dT%H:%M:%S%:z") }
|
xml.element("published") { xml.text published.to_s("%Y-%m-%dT%H:%M:%S%:z") }
|
||||||
|
@ -1585,25 +1386,19 @@ get "/feed/private" do |env|
|
||||||
videos.sort_by! { |video| video.author }.reverse!
|
videos.sort_by! { |video| video.author }.reverse!
|
||||||
end
|
end
|
||||||
|
|
||||||
if Kemal.config.ssl || CONFIG.https_only
|
|
||||||
scheme = "https://"
|
|
||||||
else
|
|
||||||
scheme = "http://"
|
|
||||||
end
|
|
||||||
|
|
||||||
if !limit
|
if !limit
|
||||||
videos = videos[0..max_results]
|
videos = videos[0..max_results]
|
||||||
end
|
end
|
||||||
|
|
||||||
host = env.request.headers["Host"]
|
host_url = make_host_url(Kemal.config.ssl || CONFIG.https_only, env.request.headers["Host"])
|
||||||
path = env.request.path
|
path = env.request.path
|
||||||
query = env.request.query.not_nil!
|
query = env.request.query.not_nil!
|
||||||
|
|
||||||
feed = XML.build(indent: " ", encoding: "UTF-8") do |xml|
|
feed = XML.build(indent: " ", encoding: "UTF-8") do |xml|
|
||||||
xml.element("feed", xmlns: "http://www.w3.org/2005/Atom", "xmlns:media": "http://search.yahoo.com/mrss/",
|
xml.element("feed", xmlns: "http://www.w3.org/2005/Atom", "xmlns:media": "http://search.yahoo.com/mrss/",
|
||||||
"xml:lang": "en-US") do
|
"xml:lang": "en-US") do
|
||||||
xml.element("link", "type": "text/html", rel: "alternate", href: "#{scheme}#{host}/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: "#{scheme}#{host}#{path}?#{query}")
|
xml.element("link", "type": "application/atom+xml", rel: "self", href: "#{host_url}#{path}?#{query}")
|
||||||
xml.element("title") { xml.text "Invidious Private Feed for #{user.email}" }
|
xml.element("title") { xml.text "Invidious Private Feed for #{user.email}" }
|
||||||
|
|
||||||
videos.each do |video|
|
videos.each do |video|
|
||||||
|
@ -1612,11 +1407,11 @@ get "/feed/private" do |env|
|
||||||
xml.element("yt:videoId") { xml.text video.id }
|
xml.element("yt:videoId") { xml.text video.id }
|
||||||
xml.element("yt:channelId") { xml.text video.ucid }
|
xml.element("yt:channelId") { xml.text video.ucid }
|
||||||
xml.element("title") { xml.text video.title }
|
xml.element("title") { xml.text video.title }
|
||||||
xml.element("link", rel: "alternate", href: "#{scheme}#{host}/watch?v=#{video.id}")
|
xml.element("link", rel: "alternate", href: "#{host_url}/watch?v=#{video.id}")
|
||||||
|
|
||||||
xml.element("author") do
|
xml.element("author") do
|
||||||
xml.element("name") { xml.text video.author }
|
xml.element("name") { xml.text video.author }
|
||||||
xml.element("uri") { xml.text "#{scheme}#{host}/channel/#{video.ucid}" }
|
xml.element("uri") { xml.text "#{host_url}/channel/#{video.ucid}" }
|
||||||
end
|
end
|
||||||
|
|
||||||
xml.element("published") { xml.text video.published.to_s("%Y-%m-%dT%H:%M:%S%:z") }
|
xml.element("published") { xml.text video.published.to_s("%Y-%m-%dT%H:%M:%S%:z") }
|
||||||
|
@ -1732,16 +1527,7 @@ get "/api/v1/captions/:id" do |env|
|
||||||
halt env, status_code: 403
|
halt env, status_code: 403
|
||||||
end
|
end
|
||||||
|
|
||||||
player_response = JSON.parse(video.info["player_response"])
|
captions = video.captions
|
||||||
if !player_response["captions"]?
|
|
||||||
env.response.content_type = "application/json"
|
|
||||||
next {
|
|
||||||
"captions" => [] of String,
|
|
||||||
}.to_json
|
|
||||||
end
|
|
||||||
|
|
||||||
tracks = player_response["captions"]["playerCaptionsTracklistRenderer"]["captionTracks"]?.try &.as_a
|
|
||||||
tracks ||= [] of JSON::Any
|
|
||||||
|
|
||||||
label = env.params.query["label"]?
|
label = env.params.query["label"]?
|
||||||
if !label
|
if !label
|
||||||
|
@ -1751,7 +1537,7 @@ get "/api/v1/captions/:id" do |env|
|
||||||
json.object do
|
json.object do
|
||||||
json.field "captions" do
|
json.field "captions" do
|
||||||
json.array do
|
json.array do
|
||||||
tracks.each do |caption|
|
captions.each do |caption|
|
||||||
json.object do
|
json.object do
|
||||||
json.field "label", caption["name"]["simpleText"]
|
json.field "label", caption["name"]["simpleText"]
|
||||||
json.field "languageCode", caption["languageCode"]
|
json.field "languageCode", caption["languageCode"]
|
||||||
|
@ -1765,27 +1551,27 @@ get "/api/v1/captions/:id" do |env|
|
||||||
next response
|
next response
|
||||||
end
|
end
|
||||||
|
|
||||||
track = tracks.select { |tracks| tracks["name"]["simpleText"] == label }
|
caption = captions.select { |caption| caption["name"]["simpleText"] == label }
|
||||||
|
|
||||||
env.response.content_type = "text/vtt"
|
env.response.content_type = "text/vtt"
|
||||||
if track.empty?
|
if caption.empty?
|
||||||
halt env, status_code: 403
|
halt env, status_code: 403
|
||||||
else
|
else
|
||||||
track = track[0]
|
caption = caption[0]
|
||||||
end
|
end
|
||||||
|
|
||||||
track_xml = client.get(track["baseUrl"].as_s).body
|
caption_xml = client.get(caption["baseUrl"].as_s).body
|
||||||
track_xml = XML.parse(track_xml)
|
caption_xml = XML.parse(caption_xml)
|
||||||
|
|
||||||
webvtt = <<-END_VTT
|
webvtt = <<-END_VTT
|
||||||
WEBVTT
|
WEBVTT
|
||||||
Kind: captions
|
Kind: captions
|
||||||
Language: #{track["languageCode"]}
|
Language: #{caption["languageCode"]}
|
||||||
|
|
||||||
|
|
||||||
END_VTT
|
END_VTT
|
||||||
|
|
||||||
track_xml.xpath_nodes("//transcript/text").each do |node|
|
caption_xml.xpath_nodes("//transcript/text").each do |node|
|
||||||
start_time = node["start"].to_f.seconds
|
start_time = node["start"].to_f.seconds
|
||||||
duration = node["dur"]?.try &.to_f.seconds
|
duration = node["dur"]?.try &.to_f.seconds
|
||||||
duration ||= start_time
|
duration ||= start_time
|
||||||
|
@ -1889,30 +1675,30 @@ get "/api/v1/comments/:id" do |env|
|
||||||
|
|
||||||
json.field "comments" do
|
json.field "comments" do
|
||||||
json.array do
|
json.array do
|
||||||
contents.as_a.each do |item|
|
contents.as_a.each do |node|
|
||||||
json.object do
|
json.object do
|
||||||
if !response["commentRepliesContinuation"]?
|
if !response["commentRepliesContinuation"]?
|
||||||
item = item["commentThreadRenderer"]
|
node = node["commentThreadRenderer"]
|
||||||
end
|
end
|
||||||
|
|
||||||
if item["replies"]?
|
if node["replies"]?
|
||||||
item_replies = item["replies"]["commentRepliesRenderer"]
|
node_replies = node["replies"]["commentRepliesRenderer"]
|
||||||
end
|
end
|
||||||
|
|
||||||
if !response["commentRepliesContinuation"]?
|
if !response["commentRepliesContinuation"]?
|
||||||
item_comment = item["comment"]["commentRenderer"]
|
node_comment = node["comment"]["commentRenderer"]
|
||||||
else
|
else
|
||||||
item_comment = item["commentRenderer"]
|
node_comment = node["commentRenderer"]
|
||||||
end
|
end
|
||||||
|
|
||||||
content_text = item_comment["contentText"]["simpleText"]?.try &.as_s.rchop('\ufeff')
|
content_text = node_comment["contentText"]["simpleText"]?.try &.as_s.rchop('\ufeff')
|
||||||
content_text ||= item_comment["contentText"]["runs"].as_a.map { |comment| comment["text"] }
|
content_text ||= node_comment["contentText"]["runs"].as_a.map { |comment| comment["text"] }
|
||||||
.join("").rchop('\ufeff')
|
.join("").rchop('\ufeff')
|
||||||
|
|
||||||
json.field "author", item_comment["authorText"]["simpleText"]
|
json.field "author", node_comment["authorText"]["simpleText"]
|
||||||
json.field "authorThumbnails" do
|
json.field "authorThumbnails" do
|
||||||
json.array do
|
json.array do
|
||||||
item_comment["authorThumbnail"]["thumbnails"].as_a.each do |thumbnail|
|
node_comment["authorThumbnail"]["thumbnails"].as_a.each do |thumbnail|
|
||||||
json.object do
|
json.object do
|
||||||
json.field "url", thumbnail["url"]
|
json.field "url", thumbnail["url"]
|
||||||
json.field "width", thumbnail["width"]
|
json.field "width", thumbnail["width"]
|
||||||
|
@ -1921,19 +1707,19 @@ get "/api/v1/comments/:id" do |env|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
json.field "authorId", item_comment["authorEndpoint"]["browseEndpoint"]["browseId"]
|
json.field "authorId", node_comment["authorEndpoint"]["browseEndpoint"]["browseId"]
|
||||||
json.field "authorUrl", item_comment["authorEndpoint"]["browseEndpoint"]["canonicalBaseUrl"]
|
json.field "authorUrl", node_comment["authorEndpoint"]["browseEndpoint"]["canonicalBaseUrl"]
|
||||||
json.field "content", content_text
|
json.field "content", content_text
|
||||||
json.field "published", item_comment["publishedTimeText"]["runs"][0]["text"]
|
json.field "published", node_comment["publishedTimeText"]["runs"][0]["text"]
|
||||||
json.field "likeCount", item_comment["likeCount"]
|
json.field "likeCount", node_comment["likeCount"]
|
||||||
json.field "commentId", item_comment["commentId"]
|
json.field "commentId", node_comment["commentId"]
|
||||||
|
|
||||||
if item_replies && !response["commentRepliesContinuation"]?
|
if node_replies && !response["commentRepliesContinuation"]?
|
||||||
reply_count = item_replies["moreText"]["simpleText"].as_s.match(/View all (?<count>\d+) replies/)
|
reply_count = node_replies["moreText"]["simpleText"].as_s.match(/View all (?<count>\d+) replies/)
|
||||||
.try &.["count"].to_i?
|
.try &.["count"].to_i?
|
||||||
reply_count ||= 1
|
reply_count ||= 1
|
||||||
|
|
||||||
continuation = item_replies["continuations"].as_a[0]["nextContinuationData"]["continuation"].as_s
|
continuation = node_replies["continuations"].as_a[0]["nextContinuationData"]["continuation"].as_s
|
||||||
|
|
||||||
json.field "replies" do
|
json.field "replies" do
|
||||||
json.object do
|
json.object do
|
||||||
|
@ -1996,35 +1782,10 @@ get "/api/v1/videos/:id" do |env|
|
||||||
halt env, status_code: 403
|
halt env, status_code: 403
|
||||||
end
|
end
|
||||||
|
|
||||||
adaptive_fmts = [] of HTTP::Params
|
fmt_stream = video.fmt_stream(decrypt_function)
|
||||||
if video.info.has_key?("adaptive_fmts")
|
adaptive_fmts = video.adaptive_fmts(decrypt_function)
|
||||||
video.info["adaptive_fmts"].split(",") do |string|
|
|
||||||
adaptive_fmts << HTTP::Params.parse(string)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
fmt_stream = [] of HTTP::Params
|
captions = video.captions
|
||||||
video.info["url_encoded_fmt_stream_map"].split(",") do |string|
|
|
||||||
if !string.empty?
|
|
||||||
fmt_stream << HTTP::Params.parse(string)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
if adaptive_fmts[0]? && adaptive_fmts[0]["s"]?
|
|
||||||
adaptive_fmts.each do |fmt|
|
|
||||||
fmt["url"] += "&signature=" + decrypt_signature(fmt["s"], decrypt_function)
|
|
||||||
end
|
|
||||||
|
|
||||||
fmt_stream.each do |fmt|
|
|
||||||
fmt["url"] += "&signature=" + decrypt_signature(fmt["s"], decrypt_function)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
player_response = JSON.parse(video.info["player_response"])
|
|
||||||
if player_response["captions"]?
|
|
||||||
captions = player_response["captions"]["playerCaptionsTracklistRenderer"]["captionTracks"]?.try &.as_a
|
|
||||||
end
|
|
||||||
captions ||= [] of JSON::Any
|
|
||||||
|
|
||||||
env.response.content_type = "application/json"
|
env.response.content_type = "application/json"
|
||||||
video_info = JSON.build do |json|
|
video_info = JSON.build do |json|
|
||||||
|
@ -2567,11 +2328,8 @@ get "/api/v1/channels/:ucid/videos" do |env|
|
||||||
end
|
end
|
||||||
|
|
||||||
get "/api/v1/search" do |env|
|
get "/api/v1/search" do |env|
|
||||||
if env.params.query["q"]?
|
query = env.params.query["q"]?
|
||||||
query = env.params.query["q"]
|
query ||= ""
|
||||||
else
|
|
||||||
next env.redirect "/"
|
|
||||||
end
|
|
||||||
|
|
||||||
page = env.params.query["page"]?.try &.to_i?
|
page = env.params.query["page"]?.try &.to_i?
|
||||||
page ||= 1
|
page ||= 1
|
||||||
|
@ -2591,10 +2349,11 @@ get "/api/v1/search" do |env|
|
||||||
# TODO: Support other content types
|
# TODO: Support other content types
|
||||||
content_type = "video"
|
content_type = "video"
|
||||||
|
|
||||||
|
env.response.content_type = "application/json"
|
||||||
|
|
||||||
begin
|
begin
|
||||||
search_params = build_search_params(sort_by, date, content_type, duration, features)
|
search_params = build_search_params(sort_by, date, content_type, duration, features)
|
||||||
rescue ex
|
rescue ex
|
||||||
env.response.content_type = "application/json"
|
|
||||||
next JSON.build do |json|
|
next JSON.build do |json|
|
||||||
json.object do
|
json.object do
|
||||||
json.field "error", ex.message
|
json.field "error", ex.message
|
||||||
|
@ -2602,67 +2361,16 @@ get "/api/v1/search" do |env|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
client = make_client(YT_URL)
|
response = JSON.build do |json|
|
||||||
html = client.get("/results?q=#{URI.escape(query)}&page=#{page}&sp=#{search_params}&disable_polymer=1").body
|
|
||||||
html = XML.parse_html(html)
|
|
||||||
|
|
||||||
results = JSON.build do |json|
|
|
||||||
json.array do
|
json.array do
|
||||||
html.xpath_nodes(%q(//ol[@class="item-section"]/li)).each do |node|
|
search_results = search(query, page, search_params)
|
||||||
anchor = node.xpath_node(%q(.//h3[contains(@class,"yt-lockup-title")]/a))
|
search_results.each do |video|
|
||||||
if !anchor
|
|
||||||
next
|
|
||||||
end
|
|
||||||
|
|
||||||
if anchor["href"].starts_with? "https://www.googleadservices.com"
|
|
||||||
next
|
|
||||||
end
|
|
||||||
|
|
||||||
title = anchor.content.strip
|
|
||||||
video_id = anchor["href"].lchop("/watch?v=")
|
|
||||||
|
|
||||||
anchor = node.xpath_node(%q(.//div[contains(@class, "yt-lockup-byline")]/a)).not_nil!
|
|
||||||
author = anchor.content
|
|
||||||
author_url = anchor["href"]
|
|
||||||
|
|
||||||
published = node.xpath_node(%q(.//div[contains(@class,"yt-lockup-meta")]/ul/li[1]))
|
|
||||||
if !published
|
|
||||||
next
|
|
||||||
end
|
|
||||||
published = published.content
|
|
||||||
if published.ends_with? "watching"
|
|
||||||
next
|
|
||||||
end
|
|
||||||
published = decode_date(published).epoch
|
|
||||||
|
|
||||||
view_count = node.xpath_node(%q(.//div[contains(@class,"yt-lockup-meta")]/ul/li[2])).not_nil!
|
|
||||||
puts view_count
|
|
||||||
view_count = view_count.content.rchop(" views")
|
|
||||||
if view_count == "No"
|
|
||||||
view_count = 0
|
|
||||||
else
|
|
||||||
view_count = view_count.delete(",").to_i64
|
|
||||||
end
|
|
||||||
|
|
||||||
descriptionHtml = node.xpath_node(%q(.//div[contains(@class, "yt-lockup-description")]))
|
|
||||||
if !descriptionHtml
|
|
||||||
description = ""
|
|
||||||
descriptionHtml = ""
|
|
||||||
else
|
|
||||||
descriptionHtml = descriptionHtml.to_s
|
|
||||||
description = descriptionHtml.gsub("<br>", "\n")
|
|
||||||
description = description.gsub("<br/>", "\n")
|
|
||||||
description = XML.parse_html(description).content.strip("\n ")
|
|
||||||
end
|
|
||||||
|
|
||||||
length_seconds = decode_length_seconds(node.xpath_node(%q(.//span[@class="video-time"])).not_nil!.content)
|
|
||||||
|
|
||||||
json.object do
|
json.object do
|
||||||
json.field "title", title
|
json.field "title", video.title
|
||||||
json.field "videoId", video_id
|
json.field "videoId", video.id
|
||||||
|
|
||||||
json.field "author", author
|
json.field "author", video.author
|
||||||
json.field "authorUrl", author_url
|
json.field "authorUrl", "/channel/#{video.ucid}"
|
||||||
|
|
||||||
json.field "videoThumbnails" do
|
json.field "videoThumbnails" do
|
||||||
json.object do
|
json.object do
|
||||||
|
@ -2673,7 +2381,7 @@ get "/api/v1/search" do |env|
|
||||||
qualities.each do |quality|
|
qualities.each do |quality|
|
||||||
json.field quality[:name] do
|
json.field quality[:name] do
|
||||||
json.object do
|
json.object do
|
||||||
json.field "url", "https://i.ytimg.com/vi/#{video_id}/#{quality["url"]}.jpg"
|
json.field "url", "https://i.ytimg.com/vi/#{video.id}/#{quality["url"]}.jpg"
|
||||||
json.field "width", quality[:width]
|
json.field "width", quality[:width]
|
||||||
json.field "height", quality[:height]
|
json.field "height", quality[:height]
|
||||||
end
|
end
|
||||||
|
@ -2682,19 +2390,18 @@ get "/api/v1/search" do |env|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
json.field "description", description
|
json.field "description", video.description
|
||||||
json.field "descriptionHtml", descriptionHtml
|
json.field "descriptionHtml", video.description_html
|
||||||
|
|
||||||
json.field "viewCount", view_count
|
json.field "viewCount", video.view_count
|
||||||
json.field "published", published
|
json.field "published", video.published.epoch
|
||||||
json.field "lengthSeconds", length_seconds
|
json.field "lengthSeconds", video.length_seconds
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
env.response.content_type = "application/json"
|
response
|
||||||
results
|
|
||||||
end
|
end
|
||||||
|
|
||||||
get "/api/manifest/dash/id/:id" do |env|
|
get "/api/manifest/dash/id/:id" do |env|
|
||||||
|
@ -2733,40 +2440,34 @@ get "/api/manifest/dash/id/:id" do |env|
|
||||||
next manifest
|
next manifest
|
||||||
end
|
end
|
||||||
|
|
||||||
adaptive_fmts = [] of HTTP::Params
|
adaptive_fmts = video.adaptive_fmts(decrypt_function)
|
||||||
if video.info.has_key?("adaptive_fmts")
|
|
||||||
video.info["adaptive_fmts"].split(",") do |string|
|
|
||||||
adaptive_fmts << HTTP::Params.parse(string)
|
|
||||||
end
|
|
||||||
else
|
|
||||||
halt env, status_code: 403
|
|
||||||
end
|
|
||||||
|
|
||||||
if local
|
if local
|
||||||
adaptive_fmts.each do |fmt|
|
adaptive_fmts.each do |fmt|
|
||||||
if Kemal.config.ssl || CONFIG.https_only
|
if Kemal.config.ssl || CONFIG.https_only
|
||||||
scheme = "https://"
|
scheme = "https://"
|
||||||
|
else
|
||||||
|
scheme = "http://"
|
||||||
end
|
end
|
||||||
scheme ||= "http://"
|
|
||||||
|
|
||||||
fmt["url"] = scheme + env.request.headers["Host"] + URI.parse(fmt["url"]).full_path
|
fmt["url"] = scheme + env.request.headers["Host"] + URI.parse(fmt["url"]).full_path
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
if adaptive_fmts[0]? && adaptive_fmts[0]["s"]?
|
|
||||||
adaptive_fmts.each do |fmt|
|
|
||||||
fmt["url"] += "&signature=" + decrypt_signature(fmt["s"], decrypt_function)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
video_streams = adaptive_fmts.compact_map { |s| s["type"].starts_with?("video/mp4") ? s : nil }
|
video_streams = adaptive_fmts.compact_map { |s| s["type"].starts_with?("video/mp4") ? s : nil }
|
||||||
|
|
||||||
audio_streams = adaptive_fmts.compact_map { |s| s["type"].starts_with?("audio/mp4") ? s : nil }
|
audio_streams = adaptive_fmts.compact_map { |s| s["type"].starts_with?("audio/mp4") ? s : nil }
|
||||||
|
|
||||||
audio_streams.sort_by! { |s| s["bitrate"].to_i }.reverse!
|
audio_streams.sort_by! { |s| s["bitrate"].to_i }.reverse!
|
||||||
audio_streams.each do |fmt|
|
audio_streams.each do |fmt|
|
||||||
fmt["bitrate"] = (fmt["bitrate"].to_f64/1000).to_i.to_s
|
fmt["bitrate"] = (fmt["bitrate"].to_f64/1000).to_i.to_s
|
||||||
end
|
end
|
||||||
|
|
||||||
|
(audio_streams + video_streams).each do |fmt|
|
||||||
|
fmt["url"] = fmt["url"].gsub('?', '/')
|
||||||
|
fmt["url"] = fmt["url"].gsub('&', '/')
|
||||||
|
fmt["url"] = fmt["url"].gsub('=', '/')
|
||||||
|
end
|
||||||
|
|
||||||
manifest = XML.build(indent: " ", encoding: "UTF-8") do |xml|
|
manifest = XML.build(indent: " ", encoding: "UTF-8") do |xml|
|
||||||
xml.element("MPD", "xmlns:xsi": "http://www.w3.org/2001/XMLSchema-instance", "xmlns": "urn:mpeg:DASH:schema:MPD:2011",
|
xml.element("MPD", "xmlns:xsi": "http://www.w3.org/2001/XMLSchema-instance", "xmlns": "urn:mpeg:DASH:schema:MPD:2011",
|
||||||
"xmlns:yt": "http://youtube.com/yt/2012/10/10", "xsi:schemaLocation": "urn:mpeg:DASH:schema:MPD:2011 DASH-MPD.xsd",
|
"xmlns:yt": "http://youtube.com/yt/2012/10/10", "xsi:schemaLocation": "urn:mpeg:DASH:schema:MPD:2011 DASH-MPD.xsd",
|
||||||
|
@ -2782,9 +2483,6 @@ get "/api/manifest/dash/id/:id" do |env|
|
||||||
bandwidth = fmt["bitrate"]
|
bandwidth = fmt["bitrate"]
|
||||||
itag = fmt["itag"]
|
itag = fmt["itag"]
|
||||||
url = fmt["url"]
|
url = fmt["url"]
|
||||||
url = url.gsub("?", "/")
|
|
||||||
url = url.gsub("&", "/")
|
|
||||||
url = url.gsub("=", "/")
|
|
||||||
|
|
||||||
xml.element("Representation", id: fmt["itag"], codecs: codecs, bandwidth: bandwidth) do
|
xml.element("Representation", id: fmt["itag"], codecs: codecs, bandwidth: bandwidth) do
|
||||||
xml.element("AudioChannelConfiguration", schemeIdUri: "urn:mpeg:dash:23003:3:audio_channel_configuration:2011",
|
xml.element("AudioChannelConfiguration", schemeIdUri: "urn:mpeg:dash:23003:3:audio_channel_configuration:2011",
|
||||||
|
@ -2805,9 +2503,6 @@ get "/api/manifest/dash/id/:id" do |env|
|
||||||
bandwidth = fmt["bitrate"]
|
bandwidth = fmt["bitrate"]
|
||||||
itag = fmt["itag"]
|
itag = fmt["itag"]
|
||||||
url = fmt["url"]
|
url = fmt["url"]
|
||||||
url = url.gsub("?", "/")
|
|
||||||
url = url.gsub("&", "/")
|
|
||||||
url = url.gsub("=", "/")
|
|
||||||
height, width = fmt["size"].split("x")
|
height, width = fmt["size"].split("x")
|
||||||
|
|
||||||
xml.element("Representation", id: itag, codecs: codecs, width: width, startWithSAP: "1", maxPlayoutRate: "1",
|
xml.element("Representation", id: itag, codecs: codecs, width: width, startWithSAP: "1", maxPlayoutRate: "1",
|
||||||
|
@ -2833,23 +2528,15 @@ get "/api/manifest/hls_variant/*" do |env|
|
||||||
manifest = client.get(env.request.path)
|
manifest = client.get(env.request.path)
|
||||||
|
|
||||||
if manifest.status_code != 200
|
if manifest.status_code != 200
|
||||||
halt env, status_code: 403
|
halt env, status_code: manifest.status_code
|
||||||
end
|
end
|
||||||
|
|
||||||
manifest = manifest.body
|
|
||||||
|
|
||||||
if Kemal.config.ssl || CONFIG.https_only
|
|
||||||
scheme = "https://"
|
|
||||||
else
|
|
||||||
scheme = "http://"
|
|
||||||
end
|
|
||||||
host = env.request.headers["Host"]
|
|
||||||
|
|
||||||
url = "#{scheme}#{host}"
|
|
||||||
|
|
||||||
env.response.content_type = "application/x-mpegURL"
|
env.response.content_type = "application/x-mpegURL"
|
||||||
env.response.headers.add("Access-Control-Allow-Origin", "*")
|
env.response.headers.add("Access-Control-Allow-Origin", "*")
|
||||||
manifest.gsub("https://www.youtube.com", url)
|
|
||||||
|
host_url = make_host_url(Kemal.config.ssl || CONFIG.https_only, env.request.headers["Host"])
|
||||||
|
manifest = manifest.body
|
||||||
|
manifest.gsub("https://www.youtube.com", host_url)
|
||||||
end
|
end
|
||||||
|
|
||||||
get "/api/manifest/hls_playlist/*" do |env|
|
get "/api/manifest/hls_playlist/*" do |env|
|
||||||
|
@ -2857,20 +2544,13 @@ get "/api/manifest/hls_playlist/*" do |env|
|
||||||
manifest = client.get(env.request.path)
|
manifest = client.get(env.request.path)
|
||||||
|
|
||||||
if manifest.status_code != 200
|
if manifest.status_code != 200
|
||||||
halt env, status_code: 403
|
halt env, status_code: manifest.status_code
|
||||||
end
|
end
|
||||||
|
|
||||||
if Kemal.config.ssl || CONFIG.https_only
|
host_url = make_host_url(Kemal.config.ssl || CONFIG.https_only, env.request.headers["Host"])
|
||||||
scheme = "https://"
|
|
||||||
else
|
|
||||||
scheme = "http://"
|
|
||||||
end
|
|
||||||
host = env.request.headers["Host"]
|
|
||||||
|
|
||||||
url = "#{scheme}#{host}"
|
manifest = manifest.body.gsub("https://www.youtube.com", host_url)
|
||||||
|
manifest = manifest.gsub(/https:\/\/r\d---.{11}\.c\.youtube\.com/, host_url)
|
||||||
manifest = manifest.body.gsub("https://www.youtube.com", url)
|
|
||||||
manifest = manifest.gsub(/https:\/\/r\d---.{11}\.c\.youtube\.com/, url)
|
|
||||||
fvip = manifest.match(/hls_chunk_host\/r(?<fvip>\d)---/).not_nil!["fvip"]
|
fvip = manifest.match(/hls_chunk_host\/r(?<fvip>\d)---/).not_nil!["fvip"]
|
||||||
manifest = manifest.gsub("seg.ts", "seg.ts/fvip/#{fvip}")
|
manifest = manifest.gsub("seg.ts", "seg.ts/fvip/#{fvip}")
|
||||||
|
|
||||||
|
|
|
@ -127,3 +127,13 @@ def arg_array(array, start = 1)
|
||||||
|
|
||||||
return args
|
return args
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def make_host_url(ssl, host)
|
||||||
|
if ssl
|
||||||
|
scheme = "https://"
|
||||||
|
else
|
||||||
|
scheme = "http://"
|
||||||
|
end
|
||||||
|
|
||||||
|
return "#{scheme}#{host}"
|
||||||
|
end
|
||||||
|
|
|
@ -1,28 +1,92 @@
|
||||||
|
class SearchVideo
|
||||||
|
add_mapping({
|
||||||
|
title: String,
|
||||||
|
id: String,
|
||||||
|
author: String,
|
||||||
|
ucid: String,
|
||||||
|
published: Time,
|
||||||
|
view_count: Int64,
|
||||||
|
description: String,
|
||||||
|
description_html: String,
|
||||||
|
length_seconds: Int32,
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
def search(query, page = 1, search_params = build_search_params(content_type: "video"))
|
def search(query, page = 1, search_params = build_search_params(content_type: "video"))
|
||||||
client = make_client(YT_URL)
|
client = make_client(YT_URL)
|
||||||
html = client.get("/results?q=#{URI.escape(query)}&page=#{page}&sp=#{search_params}").body
|
html = client.get("/results?q=#{URI.escape(query)}&page=#{page}&sp=#{search_params}").body
|
||||||
|
if html.empty?
|
||||||
|
return [] of SearchVideo
|
||||||
|
end
|
||||||
|
|
||||||
html = XML.parse_html(html)
|
html = XML.parse_html(html)
|
||||||
|
videos = [] of SearchVideo
|
||||||
|
|
||||||
videos = [] of ChannelVideo
|
html.xpath_nodes(%q(//ol[@class="item-section"]/li)).each do |node|
|
||||||
|
anchor = node.xpath_node(%q(.//h3[contains(@class,"yt-lockup-title")]/a))
|
||||||
html.xpath_nodes(%q(//ol[@class="item-section"]/li)).each do |item|
|
if !anchor
|
||||||
root = item.xpath_node(%q(div[contains(@class,"yt-lockup-video")]/div))
|
|
||||||
if !root
|
|
||||||
next
|
next
|
||||||
end
|
end
|
||||||
|
|
||||||
id = root.xpath_node(%q(.//div[contains(@class,"yt-lockup-thumbnail")]/a/@href)).not_nil!.content.lchop("/watch?v=")
|
if anchor["href"].starts_with? "https://www.googleadservices.com"
|
||||||
|
next
|
||||||
|
end
|
||||||
|
|
||||||
title = root.xpath_node(%q(.//div[@class="yt-lockup-content"]/h3/a)).not_nil!.content
|
title = anchor.content.strip
|
||||||
|
id = anchor["href"].lchop("/watch?v=")
|
||||||
|
|
||||||
author = root.xpath_node(%q(.//div[@class="yt-lockup-content"]/div/a)).not_nil!
|
anchor = node.xpath_node(%q(.//div[contains(@class, "yt-lockup-byline")]/a)).not_nil!
|
||||||
ucid = author["href"].rpartition("/")[-1]
|
author = anchor.content
|
||||||
author = author.content
|
author_url = anchor["href"]
|
||||||
|
ucid = author_url.split("/")[-1]
|
||||||
|
|
||||||
published = root.xpath_node(%q(.//ul[@class="yt-lockup-meta-info"]/li[1])).not_nil!.content
|
metadata = node.xpath_nodes(%q(.//div[contains(@class,"yt-lockup-meta")]/ul/li))
|
||||||
published = decode_date(published)
|
if metadata.size == 0
|
||||||
|
next
|
||||||
|
elsif metadata.size == 1
|
||||||
|
view_count = metadata[0].content.rchop(" watching").delete(",").to_i64
|
||||||
|
published = Time.now
|
||||||
|
else
|
||||||
|
published = decode_date(metadata[0].content)
|
||||||
|
|
||||||
|
view_count = metadata[1].content.rchop(" views")
|
||||||
|
if view_count == "No"
|
||||||
|
view_count = 0_i64
|
||||||
|
else
|
||||||
|
view_count = view_count.delete(",").to_i64
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
description_html = node.xpath_node(%q(.//div[contains(@class, "yt-lockup-description")]))
|
||||||
|
if !description_html
|
||||||
|
description = ""
|
||||||
|
description_html = ""
|
||||||
|
else
|
||||||
|
description_html = description_html.to_s
|
||||||
|
description = description_html.gsub("<br>", "\n")
|
||||||
|
description = description.gsub("<br/>", "\n")
|
||||||
|
description = XML.parse_html(description).content.strip("\n ")
|
||||||
|
end
|
||||||
|
|
||||||
|
length_seconds = node.xpath_node(%q(.//span[@class="video-time"]))
|
||||||
|
if length_seconds
|
||||||
|
length_seconds = decode_length_seconds(length_seconds.content)
|
||||||
|
else
|
||||||
|
length_seconds = -1
|
||||||
|
end
|
||||||
|
|
||||||
|
video = SearchVideo.new(
|
||||||
|
title,
|
||||||
|
id,
|
||||||
|
author,
|
||||||
|
ucid,
|
||||||
|
published,
|
||||||
|
view_count,
|
||||||
|
description,
|
||||||
|
description_html,
|
||||||
|
length_seconds,
|
||||||
|
)
|
||||||
|
|
||||||
video = ChannelVideo.new(id, title, published, Time.now, ucid, author)
|
|
||||||
videos << video
|
videos << video
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -5,6 +5,75 @@ class Video
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def fmt_stream(decrypt_function)
|
||||||
|
streams = [] of HTTP::Params
|
||||||
|
self.info["url_encoded_fmt_stream_map"].split(",") do |string|
|
||||||
|
if !string.empty?
|
||||||
|
streams << HTTP::Params.parse(string)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
streams.each { |s| s.add("label", "#{s["quality"]} - #{s["type"].split(";")[0].split("/")[1]}") }
|
||||||
|
streams = streams.uniq { |s| s["label"] }
|
||||||
|
|
||||||
|
if streams[0]? && streams[0]["s"]?
|
||||||
|
streams.each do |fmt|
|
||||||
|
fmt["url"] += "&signature=" + decrypt_signature(fmt["s"], decrypt_function)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return streams
|
||||||
|
end
|
||||||
|
|
||||||
|
def adaptive_fmts(decrypt_function)
|
||||||
|
adaptive_fmts = [] of HTTP::Params
|
||||||
|
if self.info.has_key?("adaptive_fmts")
|
||||||
|
self.info["adaptive_fmts"].split(",") do |string|
|
||||||
|
adaptive_fmts << HTTP::Params.parse(string)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if adaptive_fmts[0]? && adaptive_fmts[0]["s"]?
|
||||||
|
adaptive_fmts.each do |fmt|
|
||||||
|
fmt["url"] += "&signature=" + decrypt_signature(fmt["s"], decrypt_function)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return adaptive_fmts
|
||||||
|
end
|
||||||
|
|
||||||
|
def audio_streams(adaptive_fmts)
|
||||||
|
audio_streams = adaptive_fmts.compact_map { |s| s["type"].starts_with?("audio") ? s : nil }
|
||||||
|
audio_streams.sort_by! { |s| s["bitrate"].to_i }.reverse!
|
||||||
|
audio_streams.each do |stream|
|
||||||
|
stream["bitrate"] = (stream["bitrate"].to_f64/1000).to_i.to_s
|
||||||
|
end
|
||||||
|
|
||||||
|
return audio_streams
|
||||||
|
end
|
||||||
|
|
||||||
|
def captions
|
||||||
|
player_response = JSON.parse(self.info["player_response"])
|
||||||
|
|
||||||
|
if player_response["captions"]?
|
||||||
|
captions = player_response["captions"]["playerCaptionsTracklistRenderer"]["captionTracks"]?.try &.as_a
|
||||||
|
end
|
||||||
|
captions ||= [] of JSON::Any
|
||||||
|
|
||||||
|
return captions
|
||||||
|
end
|
||||||
|
|
||||||
|
def short_description
|
||||||
|
description = self.description.gsub("<br>", " ")
|
||||||
|
description = description.gsub("<br/>", " ")
|
||||||
|
description = XML.parse_html(description).content[0..200].gsub('"', """).gsub("\n", " ").strip(" ")
|
||||||
|
if description.empty?
|
||||||
|
description = " "
|
||||||
|
end
|
||||||
|
|
||||||
|
return description
|
||||||
|
end
|
||||||
|
|
||||||
add_mapping({
|
add_mapping({
|
||||||
id: String,
|
id: String,
|
||||||
info: {
|
info: {
|
||||||
|
@ -221,3 +290,53 @@ def itag_to_metadata(itag : String)
|
||||||
|
|
||||||
return formats[itag]
|
return formats[itag]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def process_video_params(query, preferences)
|
||||||
|
autoplay = query["autoplay"]?.try &.to_i?
|
||||||
|
video_loop = query["loop"]?.try &.to_i?
|
||||||
|
|
||||||
|
if preferences
|
||||||
|
autoplay ||= preferences.autoplay.to_unsafe
|
||||||
|
video_loop ||= preferences.video_loop.to_unsafe
|
||||||
|
end
|
||||||
|
autoplay ||= 0
|
||||||
|
autoplay = autoplay == 1
|
||||||
|
|
||||||
|
video_loop ||= 0
|
||||||
|
video_loop = video_loop == 1
|
||||||
|
|
||||||
|
if query["start"]?
|
||||||
|
video_start = decode_time(query["start"])
|
||||||
|
end
|
||||||
|
if query["t"]?
|
||||||
|
video_start = decode_time(query["t"])
|
||||||
|
end
|
||||||
|
video_start ||= 0
|
||||||
|
|
||||||
|
if query["end"]?
|
||||||
|
video_end = decode_time(query["end"])
|
||||||
|
end
|
||||||
|
video_end ||= -1
|
||||||
|
|
||||||
|
if query["listen"]? && (query["listen"] == "true" || query["listen"] == "1")
|
||||||
|
listen = true
|
||||||
|
end
|
||||||
|
listen ||= false
|
||||||
|
|
||||||
|
raw = query["raw"]?.try &.to_i?
|
||||||
|
raw ||= 0
|
||||||
|
raw = raw == 1
|
||||||
|
|
||||||
|
quality = query["quality"]?
|
||||||
|
quality ||= "hd720"
|
||||||
|
|
||||||
|
autoplay = query["autoplay"]?.try &.to_i?
|
||||||
|
autoplay ||= 0
|
||||||
|
autoplay = autoplay == 1
|
||||||
|
|
||||||
|
controls = query["controls"]?.try &.to_i?
|
||||||
|
controls ||= 1
|
||||||
|
controls = controls == 1
|
||||||
|
|
||||||
|
return autoplay, video_loop, video_start, video_end, listen, raw, quality, autoplay, controls
|
||||||
|
end
|
||||||
|
|
|
@ -39,7 +39,7 @@
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
||||||
<div class="pure-g">
|
<div class="pure-g h-box">
|
||||||
<div class="pure-u-1 pure-u-md-1-5">
|
<div class="pure-u-1 pure-u-md-1-5">
|
||||||
<% if page > 2 %>
|
<% if page > 2 %>
|
||||||
<a href="/channel/<%= ucid %>?page=<%= page - 1 %>">Previous page</a>
|
<a href="/channel/<%= ucid %>?page=<%= page - 1 %>">Previous page</a>
|
||||||
|
|
|
@ -1,17 +0,0 @@
|
||||||
<div class="pure-u-1 pure-u-md-1-4">
|
|
||||||
<div class="h-box">
|
|
||||||
<a style="width:100%;" href="/watch?v=<%= video.id %>">
|
|
||||||
<% if env.get?("user") && env.get("user").as(User).preferences.thin_mode %>
|
|
||||||
<% else %>
|
|
||||||
<img style="width:100%;" src="https://i.ytimg.com/vi/<%= video.id %>/mqdefault.jpg"/>
|
|
||||||
<% end %>
|
|
||||||
<p><%= video.title %></p>
|
|
||||||
</a>
|
|
||||||
<p>
|
|
||||||
<b><a style="width:100%;" href="/channel/<%= video.ucid %>"><%= video.author %></a></b>
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
<h5>Shared <%= video.published.to_s("%B %-d, %Y at %r UTC") %></h5>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
|
@ -10,8 +10,9 @@
|
||||||
<p>
|
<p>
|
||||||
<b><a style="width:100%;" href="/channel/<%= video.ucid %>"><%= video.author %></a></b>
|
<b><a style="width:100%;" href="/channel/<%= video.ucid %>"><%= video.author %></a></b>
|
||||||
</p>
|
</p>
|
||||||
<p>
|
|
||||||
|
<% if Time.now - video.published > 1.minute %>
|
||||||
<h5>Shared <%= recode_date(video.published) %> ago</h5>
|
<h5>Shared <%= recode_date(video.published) %> ago</h5>
|
||||||
</p>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
|
@ -20,7 +20,7 @@
|
||||||
<div class="pure-g">
|
<div class="pure-g">
|
||||||
<div class="pure-u-1 pure-u-md-1-5"></div>
|
<div class="pure-u-1 pure-u-md-1-5"></div>
|
||||||
<div class="pure-u-1 pure-u-md-3-5">
|
<div class="pure-u-1 pure-u-md-3-5">
|
||||||
<div class="pure-g navbar">
|
<div class="pure-g navbar h-box">
|
||||||
<div class="pure-u-1 pure-u-md-4-24">
|
<div class="pure-u-1 pure-u-md-4-24">
|
||||||
<a href="/" class="index-link pure-menu-heading">Invidious</a>
|
<a href="/" class="index-link pure-menu-heading">Invidious</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<% content_for "header" do %>
|
<% content_for "header" do %>
|
||||||
<title><%= query.size > 30 ? query[0,30].rstrip(".") + "..." : query %> - Invidious</title>
|
<title><%= query.not_nil!.size > 30 ? query.not_nil![0,30].rstrip(".") + "..." : query.not_nil! %> - Invidious</title>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
||||||
<% videos.each_slice(4) do |slice| %>
|
<% videos.each_slice(4) do |slice| %>
|
||||||
|
@ -10,7 +10,7 @@
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
||||||
<div class="pure-g">
|
<div class="pure-g h-box">
|
||||||
<div class="pure-u-1 pure-u-md-1-5">
|
<div class="pure-u-1 pure-u-md-1-5">
|
||||||
<% if page > 2 %>
|
<% if page > 2 %>
|
||||||
<a href="/search?q=<%= query %>&page=<%= page - 1 %>">Previous page</a>
|
<a href="/search?q=<%= query %>&page=<%= page - 1 %>">Previous page</a>
|
||||||
|
|
|
@ -19,7 +19,7 @@
|
||||||
<% notifications.each_slice(4) do |slice| %>
|
<% notifications.each_slice(4) do |slice| %>
|
||||||
<div class="pure-g">
|
<div class="pure-g">
|
||||||
<% slice.each do |video| %>
|
<% slice.each do |video| %>
|
||||||
<%= rendered "components/subscription_video" %>
|
<%= rendered "components/video" %>
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
@ -29,7 +29,7 @@
|
||||||
<% videos.each_slice(4) do |slice| %>
|
<% videos.each_slice(4) do |slice| %>
|
||||||
<div class="pure-g">
|
<div class="pure-g">
|
||||||
<% slice.each do |video| %>
|
<% slice.each do |video| %>
|
||||||
<%= rendered "components/subscription_video" %>
|
<%= rendered "components/video" %>
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
Loading…
Reference in New Issue