diff --git a/src/invidious.cr b/src/invidious.cr index 01759a62..715bbd89 100644 --- a/src/invidious.cr +++ b/src/invidious.cr @@ -670,144 +670,6 @@ get "/signout" do |env| env.redirect referer end -get "/redirect" do |env| - if env.params.query["q"]? - env.redirect env.params.query["q"] - else - env.redirect "/" - end -end - -get "/api/manifest/dash/id/:id" do |env| - env.response.headers.add("Access-Control-Allow-Origin", "*") - env.response.content_type = "application/dash+xml" - - local = env.params.query["local"]?.try &.== "true" - id = env.params.url["id"] - - client = make_client(YT_URL) - begin - video = get_video(id, client, PG_DB) - rescue ex - halt env, status_code: 403 - end - - if video.info["dashmpd"]? - manifest = client.get(video.info["dashmpd"]).body - - manifest = manifest.gsub(/[^<]+<\/BaseURL>/) do |baseurl| - url = baseurl.lchop("") - url = url.rchop("") - - if local - if Kemal.config.ssl - scheme = "https://" - end - scheme ||= "http://" - - url = scheme + env.request.headers["Host"] + URI.parse(url).full_path - end - - "#{url}" - end - - next manifest - end - - 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 - else - halt env, status_code: 403 - end - - if local - adaptive_fmts.each do |fmt| - if Kemal.config.ssl - scheme = "https://" - end - scheme ||= "http://" - - fmt["url"] = scheme + env.request.headers["Host"] + URI.parse(fmt["url"]).full_path - end - end - - if adaptive_fmts[0]? && adaptive_fmts[0]["s"]? - adaptive_fmts.each do |fmt| - fmt["url"] += "&signature=" + decrypt_signature(fmt["s"]) - end - end - - 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.sort_by! { |s| s["bitrate"].to_i }.reverse! - audio_streams.each do |fmt| - fmt["bitrate"] = (fmt["bitrate"].to_f64/1000).to_i.to_s - end - - 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", - "xmlns:yt": "http://youtube.com/yt/2012/10/10", "xsi:schemaLocation": "urn:mpeg:DASH:schema:MPD:2011 DASH-MPD.xsd", - minBufferTime: "PT1.5S", profiles: "urn:mpeg:dash:profile:isoff-main:2011", type: "static", - mediaPresentationDuration: "PT#{video.info["length_seconds"]}S") do - xml.element("Period") do - xml.element("AdaptationSet", id: 0, mimeType: "audio/mp4", subsegmentAlignment: true) do - xml.element("Role", schemeIdUri: "urn:mpeg:DASH:role:2011", value: "main") - audio_streams.each do |fmt| - mimetype, codecs = fmt["type"].split(";") - codecs = codecs[9..-2] - fmt_type = mimetype.split("/")[0] - bandwidth = fmt["bitrate"] - itag = fmt["itag"] - 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("AudioChannelConfiguration", schemeIdUri: "urn:mpeg:dash:23003:3:audio_channel_configuration:2011", value: "2") - xml.element("BaseURL") { xml.text url } - xml.element("SegmentBase", indexRange: fmt["init"]) do - xml.element("Initialization", range: fmt["index"]) - end - end - end - end - - xml.element("AdaptationSet", id: 1, mimeType: "video/mp4", subsegmentAlignment: true) do - xml.element("Role", schemeIdUri: "urn:mpeg:DASH:role:2011", value: "main") - video_streams.each do |fmt| - mimetype, codecs = fmt["type"].split(";") - codecs = codecs[9..-2] - bandwidth = fmt["bitrate"] - itag = fmt["itag"] - url = fmt["url"] - url = url.gsub("?", "/") - url = url.gsub("&", "/") - url = url.gsub("=", "/") - height, width = fmt["size"].split("x") - - xml.element("Representation", id: itag, codecs: codecs, width: width, startWithSAP: "1", maxPlayoutRate: "1", - height: height, bandwidth: bandwidth, frameRate: fmt["fps"]) do - xml.element("BaseURL") { xml.text url } - xml.element("SegmentBase", indexRange: fmt["init"]) do - xml.element("Initialization", range: fmt["index"]) - end - end - end - end - end - end - end - - manifest = manifest.gsub(%(), %()) - manifest = manifest.gsub(%(), %()) - manifest -end - # Get subscriptions for authorized user get "/feed/subscriptions" do |env| authorized = env.get? "authorized" @@ -979,6 +841,54 @@ get "/subscription_ajax" do |env| env.redirect referer end +get "/user/:user" do |env| + user = env.params.url["user"] + env.redirect "/channel/#{user}" +end + +get "/channel/:ucid" do |env| + authorized = env.get? "authorized" + if authorized + sid = env.get("sid").as(String) + + subscriptions = PG_DB.query_one?("SELECT subscriptions FROM users WHERE id = $1", sid, as: Array(String)) + end + subscriptions ||= [] of String + + ucid = env.params.url["ucid"] + + page = env.params.query["page"]?.try &.to_i + page ||= 1 + + client = make_client(YT_URL) + + if !ucid.starts_with? "UC" + rss = client.get("/feeds/videos.xml?user=#{ucid}").body + rss = XML.parse_html(rss) + + ucid = rss.xpath_node("//feed/channelid").not_nil!.content + env.redirect "/channel/#{ucid}" + end + + url = produce_playlist_url(ucid, (page - 1) * 100) + response = client.get(url) + + json = JSON.parse(response.body) + document = XML.parse_html(json["content_html"].as_s) + author = document.xpath_node(%q(//div[@class="pl-video-owner"]/a)).not_nil!.content + + videos = [] of ChannelVideo + document.xpath_nodes(%q(//a[contains(@class,"pl-video-title-link")])).each do |item| + href = URI.parse(item["href"]) + id = HTTP::Params.parse(href.query.not_nil!)["v"] + title = item.content + + videos << ChannelVideo.new(id, title, Time.now, Time.now, ucid, author) + end + + templated "channel" +end + get "/modify_theme" do |env| referer = env.request.headers["referer"]? referer ||= "/" @@ -993,6 +903,150 @@ get "/modify_theme" do |env| env.redirect referer end +get "/redirect" do |env| + if env.params.query["q"]? + env.redirect env.params.query["q"] + else + env.redirect "/" + end +end + +get "/api/manifest/dash/id/:id" do |env| + env.response.headers.add("Access-Control-Allow-Origin", "*") + env.response.content_type = "application/dash+xml" + + local = env.params.query["local"]?.try &.== "true" + id = env.params.url["id"] + + client = make_client(YT_URL) + begin + video = get_video(id, client, PG_DB) + rescue ex + halt env, status_code: 403 + end + + if video.info["dashmpd"]? + manifest = client.get(video.info["dashmpd"]).body + + manifest = manifest.gsub(/[^<]+<\/BaseURL>/) do |baseurl| + url = baseurl.lchop("") + url = url.rchop("") + + if local + if Kemal.config.ssl + scheme = "https://" + end + scheme ||= "http://" + + url = scheme + env.request.headers["Host"] + URI.parse(url).full_path + end + + "#{url}" + end + + next manifest + end + + 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 + else + halt env, status_code: 403 + end + + if local + adaptive_fmts.each do |fmt| + if Kemal.config.ssl + scheme = "https://" + end + scheme ||= "http://" + + fmt["url"] = scheme + env.request.headers["Host"] + URI.parse(fmt["url"]).full_path + end + end + + if adaptive_fmts[0]? && adaptive_fmts[0]["s"]? + adaptive_fmts.each do |fmt| + fmt["url"] += "&signature=" + decrypt_signature(fmt["s"]) + end + end + + 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.sort_by! { |s| s["bitrate"].to_i }.reverse! + audio_streams.each do |fmt| + fmt["bitrate"] = (fmt["bitrate"].to_f64/1000).to_i.to_s + end + + 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", + "xmlns:yt": "http://youtube.com/yt/2012/10/10", "xsi:schemaLocation": "urn:mpeg:DASH:schema:MPD:2011 DASH-MPD.xsd", + minBufferTime: "PT1.5S", profiles: "urn:mpeg:dash:profile:isoff-main:2011", type: "static", + mediaPresentationDuration: "PT#{video.info["length_seconds"]}S") do + xml.element("Period") do + xml.element("AdaptationSet", id: 0, mimeType: "audio/mp4", subsegmentAlignment: true) do + xml.element("Role", schemeIdUri: "urn:mpeg:DASH:role:2011", value: "main") + audio_streams.each do |fmt| + mimetype, codecs = fmt["type"].split(";") + codecs = codecs[9..-2] + fmt_type = mimetype.split("/")[0] + bandwidth = fmt["bitrate"] + itag = fmt["itag"] + 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("AudioChannelConfiguration", schemeIdUri: "urn:mpeg:dash:23003:3:audio_channel_configuration:2011", value: "2") + xml.element("BaseURL") { xml.text url } + xml.element("SegmentBase", indexRange: fmt["init"]) do + xml.element("Initialization", range: fmt["index"]) + end + end + end + end + + xml.element("AdaptationSet", id: 1, mimeType: "video/mp4", subsegmentAlignment: true) do + xml.element("Role", schemeIdUri: "urn:mpeg:DASH:role:2011", value: "main") + video_streams.each do |fmt| + mimetype, codecs = fmt["type"].split(";") + codecs = codecs[9..-2] + bandwidth = fmt["bitrate"] + itag = fmt["itag"] + url = fmt["url"] + url = url.gsub("?", "/") + url = url.gsub("&", "/") + url = url.gsub("=", "/") + height, width = fmt["size"].split("x") + + xml.element("Representation", id: itag, codecs: codecs, width: width, startWithSAP: "1", maxPlayoutRate: "1", + height: height, bandwidth: bandwidth, frameRate: fmt["fps"]) do + xml.element("BaseURL") { xml.text url } + xml.element("SegmentBase", indexRange: fmt["init"]) do + xml.element("Initialization", range: fmt["index"]) + end + end + end + end + end + end + end + + manifest = manifest.gsub(%(), %()) + manifest = manifest.gsub(%(), %()) + manifest +end + +options "/videoplayback*" do |env| + env.response.headers["Access-Control-Allow-Origin"] = "*" + env.response.headers["Access-Control-Allow-Methods"] = "GET" + env.response.headers["Access-Control-Allow-Headers"] = "Content-Type, range" +end + get "/videoplayback*" do |env| path = env.request.path if path != "/videoplayback" @@ -1066,60 +1120,6 @@ get "/videoplayback*" do |env| end end -get "/user/:user" do |env| - user = env.params.url["user"] - env.redirect "/channel/#{user}" -end - -get "/channel/:ucid" do |env| - authorized = env.get? "authorized" - if authorized - sid = env.get("sid").as(String) - - subscriptions = PG_DB.query_one?("SELECT subscriptions FROM users WHERE id = $1", sid, as: Array(String)) - end - subscriptions ||= [] of String - - ucid = env.params.url["ucid"] - - page = env.params.query["page"]?.try &.to_i - page ||= 1 - - client = make_client(YT_URL) - - if !ucid.starts_with? "UC" - rss = client.get("/feeds/videos.xml?user=#{ucid}").body - rss = XML.parse_html(rss) - - ucid = rss.xpath_node("//feed/channelid").not_nil!.content - env.redirect "/channel/#{ucid}" - end - - url = produce_playlist_url(ucid, (page - 1) * 100) - response = client.get(url) - - json = JSON.parse(response.body) - document = XML.parse_html(json["content_html"].as_s) - author = document.xpath_node(%q(//div[@class="pl-video-owner"]/a)).not_nil!.content - - videos = [] of ChannelVideo - document.xpath_nodes(%q(//a[contains(@class,"pl-video-title-link")])).each do |item| - href = URI.parse(item["href"]) - id = HTTP::Params.parse(href.query.not_nil!)["v"] - title = item.content - - videos << ChannelVideo.new(id, title, Time.now, Time.now, ucid, author) - end - - templated "channel" -end - -options "/videoplayback*" do |env| - env.response.headers["Access-Control-Allow-Origin"] = "*" - env.response.headers["Access-Control-Allow-Methods"] = "GET" - env.response.headers["Access-Control-Allow-Headers"] = "Content-Type, range" -end - error 404 do |env| error_message = "404 Page not found" templated "error"