From a87856723d2383139e139c09270ce19230d2a8c7 Mon Sep 17 00:00:00 2001 From: Azi Hassan Date: Thu, 5 Sep 2024 02:52:25 +0100 Subject: [PATCH] [fix/ISSUE-83] Add support for downloading HLS streams --- source/app.d | 9 ++++- source/cache.d | 1 + source/downloaders.d | 86 +++++++++++++++++++++++++++++++++++++++-- source/helpers.d | 75 ++++++++++++++++++++++++++++++------ source/parsers.d | 92 ++++++++++++++++++++++++++++++++------------ tests/index.m3u8 | 14 +++++++ 6 files changed, 234 insertions(+), 43 deletions(-) create mode 100644 tests/index.m3u8 diff --git a/source/app.d b/source/app.d index f95af78..8f7ca48 100644 --- a/source/app.d +++ b/source/app.d @@ -1,7 +1,7 @@ import std.stdio : writef, stdout, writeln; import std.algorithm : each; import std.conv : to; -import std.string : format; +import std.string : format, endsWith; import std.file : getcwd, write, getSize; import std.net.curl : get; import std.path : buildPath; @@ -157,7 +157,12 @@ void handleURL(string url, int itag, StdoutLogger logger, bool displayFormats, b logger.display("Downloading ", url, " to ", filename); Downloader downloader; - if(parallel) + if(link.endsWith(".m3u8")) + { + logger.display("Using M3u8Downloader"); + downloader = new M3u8Downloader(logger, youtubeFormat, !noProgress); + } + else if(parallel) { logger.display("Using ParallelDownloader"); downloader = new ParallelDownloader(logger, parser.getID(), parser.getTitle(), youtubeFormat, !noProgress); diff --git a/source/cache.d b/source/cache.d index 9803bf6..4273658 100644 --- a/source/cache.d +++ b/source/cache.d @@ -34,6 +34,7 @@ struct Cache curl.set(CurlOption.url, url); curl.set(CurlOption.encoding, "deflate, gzip"); curl.set(CurlOption.followlocation, true); + curl.set(CurlOption.useragent, "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.5 Safari/605.1.15,gzip(gfe)"); curl.onReceive = (ubyte[] chunk) { result ~= chunk.map!(to!(const(char))).to!string; diff --git a/source/downloaders.d b/source/downloaders.d index e902783..f654ff2 100644 --- a/source/downloaders.d +++ b/source/downloaders.d @@ -1,11 +1,11 @@ import std.stdio : writef, writeln, File; import std.parallelism : defaultPoolThreads, taskPool, totalCPUs; -import std.algorithm : each, sort, sum, map, min; +import std.algorithm : each, sort, sum, map, min, filter; import std.conv : to; -import std.string : startsWith, indexOf, format, split; +import std.string : startsWith, indexOf, format, split, lineSplitter; import std.file : append, exists, read, remove, getSize; import std.range : iota; -import std.net.curl : Curl, CurlOption, HTTP; +import std.net.curl : Curl, CurlOption, HTTP, get; import std.math : ceil; import helpers : getContentLength, sanitizePath, StdoutLogger, formatSuccess, formatTitle; @@ -59,7 +59,8 @@ class RegularDownloader : Downloader auto file = File(destination, "ab"); curl.set(CurlOption.url, url); - curl.set(CurlOption.useragent, "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:95.0) Gecko/20100101 Firefox/95.0"); + //curl.set(CurlOption.useragent, "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:95.0) Gecko/20100101 Firefox/95.0"); + curl.set(CurlOption.useragent, "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.5 Safari/605.1.15,gzip(gfe)"); curl.set(CurlOption.referer, referer); curl.set(CurlOption.followlocation, true); curl.set(CurlOption.failonerror, true); @@ -149,6 +150,7 @@ class ParallelDownloader : Downloader //request range length limit above which youtube starts throttling downloads //https://github.com/azihassan/youtube-d/issues/65#issuecomment-2094993192 + public immutable LENGTH_THROTTLING_LIMIT = 10.0 * 1024.0 * 1024.0; this(StdoutLogger logger, string id, string title, YoutubeFormat youtubeFormat, bool progress = true) @@ -274,3 +276,79 @@ class ChunkedDownloader : ParallelDownloader } } +//https://rr3---sn-p5h-jhoy.googlevideo.com/videoplayback/id/c303be7a57ea6f28/itag/91/source/youtube/expire/1724813705/ei/KT3OZryRFbDCmLAP0ZW0sA4/ip/102.49.55.161/requiressl/yes/ratebypass/yes/pfa/1/sgoap/clen%3D4659962%3Bdur%3D764.075%3Bgir%3Dyes%3Bitag%3D139%3Blmt%3D1724731671440441/sgovp/clen%3D8272591%3Bdur%3D763.929%3Bgir%3Dyes%3Bitag%3D160%3Blmt%3D1724731663052618/rqh/1/hls_chunk_host/rr3---sn-p5h-jhoy.googlevideo.com/xpc/EgVo2aDSNQ%3D%3D/mh/vz/mm/31,29/mn/sn-p5h-jhoy,sn-apn7en7e/ms/au,rdu/mv/m/mvi/3/pl/17/initcwndbps/391250/spc/Mv1m9jMugnpWvFVyQljKM1pZQINUP9nMutRavY8GPMP3mBAHQ9x5lGAO2u3KZYw/vprv/1/playlist_type/CLEAN/txp/6309224/mt/1724791751/fvip/3/keepalive/yes/sparams/expire,ei,ip,id,itag,source,requiressl,ratebypass,pfa,sgoap,sgovp,rqh,xpc,spc,vprv,playlist_type/sig/AJfQdSswRQIgS6erfF7F7NN8ScQJC33JIBqa3FkM9Gk7lNq0gd64a-MCIQDOBo0dLp_vVWa1lvNHVBc8mstehsyJPV3qs1bpOzS8Wg%3D%3D/lsparams/hls_chunk_host,mh,mm,mn,ms,mv,mvi,pl,initcwndbps/lsig/AGtxev0wRgIhAOk3G0guZupTB9f04t3hunhQ0zZqIT2gwXLdsywduCIeAiEA9Fz-TI3Ix8dHL9eplJ4nu7NHnSi4o4TRYBBjRgpkJss%3D/playlist/index.m3u8/govp/slices%3D0-44728/goap/slices%3D0-62899/begin/0/len/5005/gosq/0/file/seg.ts +class M3u8Downloader : Downloader +{ + private StdoutLogger logger; + private int delegate(ulong length, ulong currentLength) onProgress; + private bool progress; + private YoutubeFormat youtubeFormat; + + this(StdoutLogger logger, YoutubeFormat youtubeFormat, bool progress = true) + { + this.logger = logger; + this.onProgress = onProgress; + this.youtubeFormat = youtubeFormat; + this.progress = progress; + } + + override public void download(string destination, string url, string referer) + { + logger.display("Length = ", youtubeFormat.length); + logger.display("progress = ", progress); + logger.display("youtubeFormat = ", youtubeFormat); + this.onProgress = (ulong _, ulong __) { + if(youtubeFormat.length == 0) + { + logger.display("youtubeFormat.length == 0"); + return 0; + } + ulong current = destination.getSize(); + auto percentage = 100.0 * (cast(float)(current) / youtubeFormat.length); + writef!"\r[%.2f %%] %.2f / %.2f MB"(percentage, current / 1024.0 / 1024.0, youtubeFormat.length / 1024.0 / 1024.0); + return 0; + }; + + string playlist = url.get().idup; + foreach(segment; playlist.lineSplitter.filter!(line => line[0] != '#')) + { + downloadSegment(destination, segment, referer); + } + } + + private void downloadSegment(string destination, string url, string referer) + { + auto http = HTTP(url); + + //http.verbose(logger.verbose); + + auto curl = http.handle(); + if(destination.exists() && destination.getSize() == youtubeFormat.length) + { + logger.display("Done !".formatSuccess()); + return; + } + + auto file = File(destination, "ab"); + curl.set(CurlOption.url, url); + curl.set(CurlOption.useragent, "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.5 Safari/605.1.15,gzip(gfe)"); + curl.set(CurlOption.referer, referer); + curl.set(CurlOption.followlocation, true); + curl.set(CurlOption.failonerror, true); + curl.set(CurlOption.connecttimeout, 60 * 3); + curl.set(CurlOption.nosignal, true); + + curl.onReceive = (ubyte[] data) { + file.rawWrite(data); + return data.length; + }; + + if(progress) + { + curl.onProgress = (size_t total, size_t current, size_t _, size_t __) { + return onProgress(total, current); + }; + } + auto result = curl.perform(); + } +} diff --git a/source/helpers.d b/source/helpers.d index e1e029a..5b071e6 100644 --- a/source/helpers.d +++ b/source/helpers.d @@ -1,44 +1,38 @@ import std.logger; import std.stdio : writeln, writefln, File, stdout; -import std.regex : ctRegex, matchFirst, escaper, regex, Captures; -import std.algorithm : filter; +import std.regex : ctRegex, matchAll, matchFirst, escaper, regex, Captures; +import std.algorithm : filter, map, canFind, sum; import std.conv : to; import std.net.curl : HTTP; -import std.string : split, indexOf, startsWith, endsWith; +import std.string : split, indexOf, startsWith, endsWith, strip; import std.format : formattedRead; +import std.range : chunks; import parsers : YoutubeFormat, AudioVisual; ulong getContentLength(string url, YoutubeFormat youtubeFormat) { - writeln("url = ", url); - writeln("youtubeFormat = ", youtubeFormat.length); if(youtubeFormat.length != 0) { - writeln("return ", youtubeFormat.length); return youtubeFormat.length; } - writeln("queryString = "); string[string] queryString = url.parseQueryString(); - writeln("queryString = ", queryString); if("range" in queryString && !queryString["range"].endsWith("-")) { string[] limits = queryString["range"].split("-"); - writeln("return ", limits); return limits[1].to!ulong - limits[0].to!ulong; } - writeln("Sending head request"); auto http = HTTP(url); http.method = HTTP.Method.head; - http.addRequestHeader("User-Agent", "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:95.0) Gecko/20100101 Firefox/95.0"); + //http.addRequestHeader("User-Agent", "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:95.0) Gecko/20100101 Firefox/95.0"); + http.addRequestHeader("User-Agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.5 Safari/605.1.15,gzip(gfe)"); http.perform(); if(http.statusLine.code >= 400) { throw new Exception("Failed with status " ~ http.statusLine.code.to!string); } - writeln("return ", http.responseHeaders["content-length"]); return http.responseHeaders["content-length"].to!ulong; } @@ -298,3 +292,60 @@ unittest assert("https://www.youtube.com/s/player/0c96dfd3/player_ias.vflset/ar_EG/base.js".parseBaseJSKey() == "0c96dfd3"); assert("www.youtube.com/s/player/0c96dfd3/player_ias.vflset/ar_EG/base.js".parseBaseJSKey() == "0c96dfd3"); } + +YoutubeFormat[] parseM3u8Formats(string m3u8) +{ + YoutubeFormat[] formats; + string[] lines = m3u8.strip().split("\n"); + size_t firstChunkIndex; + do + { + firstChunkIndex++; + } + while(!lines[firstChunkIndex].startsWith("#EXT-X-STREAM-INF:")); + + foreach(videoInfo; lines[firstChunkIndex .. $].chunks(2)) + { + string streamInfo = videoInfo[0]["#EXT-X-STREAM-INF:".length .. $]; + string url = videoInfo[1]; + + string codecs = videoInfo[0].matchOrFail!`CODECS="(.+)"`; + string resolution = videoInfo[0].matchOrFail!`RESOLUTION=(\d+x\d+)`; + + YoutubeFormat format; + format.itag = url.matchOrFail!`\/itag\/(\d+)\/`.to!int; + format.length = url.matchAll(ctRegex!`clen%3D(\d+)`).map!(capture => capture[1].to!ulong).sum(); + format.quality = resolution.split("x")[1] ~ "p"; + if(codecs.canFind("mp4a")) + { + format.audioVisual ~= AudioVisual.AUDIO; + format.mimetype = "audio/mp4"; + } + if(codecs.canFind("avc")) + { + format.audioVisual ~= AudioVisual.VIDEO; + format.mimetype = "video/mp4"; + } + + formats ~= format; + } + return formats; +} + +unittest +{ + writeln("Should parse HLS m3u8 formats".formatTitle()); + scope(success) writeln("OK\n".formatSuccess()); + string m3u8 = "tests/index.m3u8".readText(); + YoutubeFormat[] expected = [ + YoutubeFormat(91, 4659962 + 8272591, "144p", "video/mp4", [AudioVisual.AUDIO, AudioVisual.VIDEO]), + YoutubeFormat(92, 4659962 + 17875980, "240p", "audio/mp4", [AudioVisual.AUDIO]), + YoutubeFormat(93, 12365022 + 33446485, "360p", "video/mp4", [AudioVisual.VIDEO]), + YoutubeFormat(94, 12365022 + 62573407, "480p", "video/mp4", [AudioVisual.AUDIO, AudioVisual.VIDEO]), + YoutubeFormat(95, 12365022 + 124753691, "720p", "video/mp4", [AudioVisual.AUDIO, AudioVisual.VIDEO]), + YoutubeFormat(96, 12365022 + 239812801, "1080p", "video/mp4", [AudioVisual.AUDIO, AudioVisual.VIDEO]) + ]; + + YoutubeFormat[] actual = m3u8.parseM3u8Formats(); + assert(expected == actual); +} diff --git a/source/parsers.d b/source/parsers.d index 1dbdf36..79593c5 100644 --- a/source/parsers.d +++ b/source/parsers.d @@ -6,12 +6,12 @@ import std.typecons : tuple, Tuple; import std.conv : to; import std.array : replace; import std.file : readText; -import std.string : indexOf, format, lastIndexOf, split, strip, toStringz, startsWith; +import std.string : indexOf, format, lastIndexOf, split, strip, toStringz, startsWith, lineSplitter; import std.regex : ctRegex, matchFirst, escaper; import std.algorithm : canFind, filter, reverse, map; import std.format : formattedRead; -import helpers : parseQueryString, matchOrFail, StdoutLogger, formatTitle, formatSuccess, formatError, formatWarning; +import helpers : parseQueryString, matchOrFail, StdoutLogger, formatTitle, formatSuccess, formatError, formatWarning, parseM3u8Formats; import html; import duktape; @@ -21,6 +21,7 @@ abstract class YoutubeVideoURLExtractor protected string html; protected Document parser; protected StdoutLogger logger; + protected string baseJS; abstract public string getURL(int itag, bool attemptDethrottle = false); abstract public ulong findExpirationTimestamp(int itag); @@ -56,7 +57,7 @@ abstract class YoutubeVideoURLExtractor public YoutubeFormat getFormat(int itag) { - YoutubeFormat[] formats = getFormats("formats") ~ getFormats("adaptiveFormats"); + YoutubeFormat[] formats = getFormats("formats") ~ getFormats("adaptiveFormats") ~ getM3u8Formats("hlsManifestUrl"); auto match = formats.filter!(format => format.itag == itag); if(match.empty) { @@ -67,7 +68,7 @@ abstract class YoutubeVideoURLExtractor public YoutubeFormat[] getFormats() { - return getFormats("formats") ~ getFormats("adaptiveFormats"); + return getFormats("formats") ~ getFormats("adaptiveFormats") ~ getM3u8Formats("hlsManifestUrl"); } private YoutubeFormat[] getFormats(string formatKey) @@ -109,11 +110,38 @@ abstract class YoutubeVideoURLExtractor } return formats; } + + private YoutubeFormat[] getM3u8Formats(string formatKey) + { + string streamingData = html.matchOrFail!`"streamingData":(.*?),"player`; + auto json = streamingData.parseJSON(); + if(formatKey !in json) + { + return []; + } + string manifest = json[formatKey].str.get().idup; + return manifest.parseM3u8Formats(); + } + + protected string solveThrottlingChallenge(string url) + { + string[string] queryString = url.parseQueryString(); + if(baseJS == "" || "n" !in queryString) + { + return url; + } + + string n = queryString["n"]; + logger.displayVerbose("Found n : ", n); + auto solver = ThrottlingAlgorithm(baseJS, logger); + string solvedN = solver.solve(n); + logger.displayVerbose("Solved n : ", solvedN); + return url.replace("&n=" ~ n, "&n=" ~ solvedN); + } } class SimpleYoutubeVideoURLExtractor : YoutubeVideoURLExtractor { - private string baseJS; this(string html, StdoutLogger logger) { this.html = html; @@ -129,28 +157,46 @@ class SimpleYoutubeVideoURLExtractor : YoutubeVideoURLExtractor } override string getURL(int itag, bool attemptDethrottle = false) + { + if(html.canFind(`"itag":` ~ itag.to!string)) + { + return getRegularURL(itag, attemptDethrottle); + } + return getHLSURL(itag, attemptDethrottle); + } + + private string getRegularURL(int itag, bool attemptDethrottle) { string url = html .matchOrFail(`"itag":` ~ itag.to!string ~ `,"url":"(.*?)"`) .replace(`\u0026`, "&"); - - string[string] queryString = url.parseQueryString(); - if(baseJS == "" || !attemptDethrottle || "n" !in queryString) + if(!attemptDethrottle) { return url; } + return solveThrottlingChallenge(url); + } - string n = queryString["n"]; - logger.displayVerbose("Found n : ", n); - auto solver = ThrottlingAlgorithm(baseJS, logger); - string solvedN = solver.solve(n); - logger.displayVerbose("Solved n : ", solvedN); - return url.replace("&n=" ~ n, "&n=" ~ solvedN); + private string getHLSURL(int itag, bool attemptDethrottle) + { + string formats = parseHLSManifestURL(html).get().idup; + foreach(formatURL; formats.lineSplitter.filter!(line => line[0] != '#')) + { + if(formatURL.canFind("/itag/" ~ itag.to!string ~ "/")) + { + return formatURL; + } + } + throw new Exception("Failed to find format " ~ itag.to!string); } override ulong findExpirationTimestamp(int itag) { string videoURL = getURL(itag); + if(videoURL.canFind("/expire/")) + { + return videoURL.matchOrFail!`\/expire\/(\d+)`.to!ulong; + } string[string] params = videoURL.parseQueryString(); return params["expire"].to!ulong; } @@ -271,8 +317,6 @@ unittest class AdvancedYoutubeVideoURLExtractor : YoutubeVideoURLExtractor { - private string baseJS; - this(string html, string baseJS, StdoutLogger logger) { this.html = html; @@ -290,18 +334,11 @@ class AdvancedYoutubeVideoURLExtractor : YoutubeVideoURLExtractor string sig = algorithm.decrypt(params["s"]); string url = params["url"].decodeComponent() ~ "&" ~ params["sp"] ~ "=" ~ sig; - string[string] urlParams = url.parseQueryString(); - if("n" !in urlParams || !attemptDethrottle) + if(!attemptDethrottle) { return url; } - - string n = urlParams["n"]; - logger.displayVerbose("Found n : ", n); - auto solver = ThrottlingAlgorithm(baseJS, logger); - string solvedN = solver.solve(n); - logger.displayVerbose("Solved n : ", solvedN); - return url.replace("&n=" ~ n, "&n=" ~ solvedN); + return solveThrottlingChallenge(url); } override ulong findExpirationTimestamp(int itag) @@ -438,6 +475,11 @@ string parseBaseJSURL(string html) return "https://www.youtube.com" ~ html.matchOrFail!`jsUrl"*:\s*"(.*?)"`; } +string parseHLSManifestURL(string html) +{ + return html.matchOrFail!`"hlsManifestUrl":"(.*?)"`; +} + unittest { writeln("Should parse base.js URL".formatTitle()); diff --git a/tests/index.m3u8 b/tests/index.m3u8 new file mode 100644 index 0000000..92a2d82 --- /dev/null +++ b/tests/index.m3u8 @@ -0,0 +1,14 @@ +#EXTM3U +#EXT-X-INDEPENDENT-SEGMENTS +#EXT-X-STREAM-INF:BANDWIDTH=180310,CODECS="mp4a.40.5,avc1.4D400C",RESOLUTION=256x144,FRAME-RATE=30,VIDEO-RANGE=SDR,CLOSED-CAPTIONS=NONE +https://manifest.googlevideo.com/api/manifest/hls_playlist/expire/1724813705/ei/KT3OZryRFbDCmLAP0ZW0sA4/ip/102.49.55.161/id/c303be7a57ea6f28/itag/91/source/youtube/requiressl/yes/ratebypass/yes/pfa/1/sgoap/clen%3D4659962%3Bdur%3D764.075%3Bgir%3Dyes%3Bitag%3D139%3Blmt%3D1724731671440441/sgovp/clen%3D8272591%3Bdur%3D763.929%3Bgir%3Dyes%3Bitag%3D160%3Blmt%3D1724731663052618/rqh/1/hls_chunk_host/rr3---sn-p5h-jhoy.googlevideo.com/xpc/EgVo2aDSNQ%3D%3D/mh/vz/mm/31,29/mn/sn-p5h-jhoy,sn-apn7en7e/ms/au,rdu/mv/m/mvi/3/pl/17/initcwndbps/391250/spc/Mv1m9jMugnpWvFVyQljKM1pZQINUP9nMutRavY8GPMP3mBAHQ9x5lGAO2u3KZYw/vprv/1/playlist_type/CLEAN/dover/11/txp/6309224/mt/1724791751/fvip/3/keepalive/yes/sparams/expire,ei,ip,id,itag,source,requiressl,ratebypass,pfa,sgoap,sgovp,rqh,xpc,spc,vprv,playlist_type/sig/AJfQdSswRQIgS6erfF7F7NN8ScQJC33JIBqa3FkM9Gk7lNq0gd64a-MCIQDOBo0dLp_vVWa1lvNHVBc8mstehsyJPV3qs1bpOzS8Wg%3D%3D/lsparams/hls_chunk_host,mh,mm,mn,ms,mv,mvi,pl,initcwndbps/lsig/AGtxev0wRgIhAOk3G0guZupTB9f04t3hunhQ0zZqIT2gwXLdsywduCIeAiEA9Fz-TI3Ix8dHL9eplJ4nu7NHnSi4o4TRYBBjRgpkJss%3D/playlist/index.m3u8 +#EXT-X-STREAM-INF:BANDWIDTH=337802,CODECS="mp4a.40.5",RESOLUTION=426x240,FRAME-RATE=30,VIDEO-RANGE=SDR,CLOSED-CAPTIONS=NONE +https://manifest.googlevideo.com/api/manifest/hls_playlist/expire/1724813705/ei/KT3OZryRFbDCmLAP0ZW0sA4/ip/102.49.55.161/id/c303be7a57ea6f28/itag/92/source/youtube/requiressl/yes/ratebypass/yes/pfa/1/sgoap/clen%3D4659962%3Bdur%3D764.075%3Bgir%3Dyes%3Bitag%3D139%3Blmt%3D1724731671440441/sgovp/clen%3D17875980%3Bdur%3D763.929%3Bgir%3Dyes%3Bitag%3D133%3Blmt%3D1724731663773844/rqh/1/hls_chunk_host/rr3---sn-p5h-jhoy.googlevideo.com/xpc/EgVo2aDSNQ%3D%3D/mh/vz/mm/31,29/mn/sn-p5h-jhoy,sn-apn7en7e/ms/au,rdu/mv/m/mvi/3/pl/17/initcwndbps/391250/spc/Mv1m9jMugnpWvFVyQljKM1pZQINUP9nMutRavY8GPMP3mBAHQ9x5lGAO2u3KZYw/vprv/1/playlist_type/CLEAN/dover/11/txp/6309224/mt/1724791751/fvip/3/keepalive/yes/sparams/expire,ei,ip,id,itag,source,requiressl,ratebypass,pfa,sgoap,sgovp,rqh,xpc,spc,vprv,playlist_type/sig/AJfQdSswRQIgafzewRQAPsTZpK_oDWDMkjFkoFQHP60PfV8RRZHGanECIQCSnYTkzt3pYp3bkNnxQgC4G9DMU556gIgqucOVzPnEzA%3D%3D/lsparams/hls_chunk_host,mh,mm,mn,ms,mv,mvi,pl,initcwndbps/lsig/AGtxev0wRQIgIMNatrwQA-P_cjEFc0-TiOovkV7UIv4qPMvqJdR8PRECIQC_p7ODi-5T98bykuwUnHsAh13IyurXnnQMta5_ho6gvw%3D%3D/playlist/index.m3u8 +#EXT-X-STREAM-INF:BANDWIDTH=667103,CODECS="avc1.4D401E",RESOLUTION=640x360,FRAME-RATE=30,VIDEO-RANGE=SDR,CLOSED-CAPTIONS=NONE +https://manifest.googlevideo.com/api/manifest/hls_playlist/expire/1724813705/ei/KT3OZryRFbDCmLAP0ZW0sA4/ip/102.49.55.161/id/c303be7a57ea6f28/itag/93/source/youtube/requiressl/yes/ratebypass/yes/pfa/1/sgoap/clen%3D12365022%3Bdur%3D763.982%3Bgir%3Dyes%3Bitag%3D140%3Blmt%3D1724731682752651/sgovp/clen%3D33446485%3Bdur%3D763.929%3Bgir%3Dyes%3Bitag%3D134%3Blmt%3D1724731664119996/rqh/1/hls_chunk_host/rr3---sn-p5h-jhoy.googlevideo.com/xpc/EgVo2aDSNQ%3D%3D/mh/vz/mm/31,29/mn/sn-p5h-jhoy,sn-apn7en7e/ms/au,rdu/mv/m/mvi/3/pl/17/initcwndbps/391250/spc/Mv1m9jMugnpWvFVyQljKM1pZQINUP9nMutRavY8GPMP3mBAHQ9x5lGAO2u3KZYw/vprv/1/playlist_type/CLEAN/dover/11/txp/6309224/mt/1724791751/fvip/3/keepalive/yes/sparams/expire,ei,ip,id,itag,source,requiressl,ratebypass,pfa,sgoap,sgovp,rqh,xpc,spc,vprv,playlist_type/sig/AJfQdSswRQIgWzseZ1S-vbmNrkyd95KJw__RECdfWxJvf9vtCf3KMOwCIQDDlhUgoFePmLEKUPjjbZFXeW9o2YlRUbRFn6E42seCCA%3D%3D/lsparams/hls_chunk_host,mh,mm,mn,ms,mv,mvi,pl,initcwndbps/lsig/AGtxev0wRAIgDJp_oKNBpU3Ae22VKREJklhO0TBpWIHxbTXLtCz88BECIDbgaIyqJ2wupndyOIomxjQzQU4Q-n6PKe7gRNrOdl-5/playlist/index.m3u8 +#EXT-X-STREAM-INF:BANDWIDTH=1065530,CODECS="mp4a.40.2,avc1.4D401F",RESOLUTION=854x480,FRAME-RATE=30,VIDEO-RANGE=SDR,CLOSED-CAPTIONS=NONE +https://manifest.googlevideo.com/api/manifest/hls_playlist/expire/1724813705/ei/KT3OZryRFbDCmLAP0ZW0sA4/ip/102.49.55.161/id/c303be7a57ea6f28/itag/94/source/youtube/requiressl/yes/ratebypass/yes/pfa/1/sgoap/clen%3D12365022%3Bdur%3D763.982%3Bgir%3Dyes%3Bitag%3D140%3Blmt%3D1724731682752651/sgovp/clen%3D62573407%3Bdur%3D763.929%3Bgir%3Dyes%3Bitag%3D135%3Blmt%3D1724731664153589/rqh/1/hls_chunk_host/rr3---sn-p5h-jhoy.googlevideo.com/xpc/EgVo2aDSNQ%3D%3D/mh/vz/mm/31,29/mn/sn-p5h-jhoy,sn-apn7en7e/ms/au,rdu/mv/m/mvi/3/pl/17/initcwndbps/391250/spc/Mv1m9jMugnpWvFVyQljKM1pZQINUP9nMutRavY8GPMP3mBAHQ9x5lGAO2u3KZYw/vprv/1/playlist_type/CLEAN/dover/11/txp/6309224/mt/1724791751/fvip/3/keepalive/yes/sparams/expire,ei,ip,id,itag,source,requiressl,ratebypass,pfa,sgoap,sgovp,rqh,xpc,spc,vprv,playlist_type/sig/AJfQdSswRQIgQlVN62QOHUncK05NPbI4s600ng0eLAAiMR7ztJfStMgCIQCgg_qT6Lonm9hZ4amSJ8r99Y3Vqrkb5TIQ3M5ns5KPMg%3D%3D/lsparams/hls_chunk_host,mh,mm,mn,ms,mv,mvi,pl,initcwndbps/lsig/AGtxev0wRAIgBhdSr8P836P5pmTyYEqIsnDAV1jYTsh6W7K5VlpHFHYCIDqpy6_MUv1F_VWU1e_7GautKLITcpOAOZrY1pejSULH/playlist/index.m3u8 +#EXT-X-STREAM-INF:BANDWIDTH=1878186,CODECS="mp4a.40.2,avc1.64001F",RESOLUTION=1280x720,FRAME-RATE=30,VIDEO-RANGE=SDR,CLOSED-CAPTIONS=NONE +https://manifest.googlevideo.com/api/manifest/hls_playlist/expire/1724813705/ei/KT3OZryRFbDCmLAP0ZW0sA4/ip/102.49.55.161/id/c303be7a57ea6f28/itag/95/source/youtube/requiressl/yes/ratebypass/yes/pfa/1/sgoap/clen%3D12365022%3Bdur%3D763.982%3Bgir%3Dyes%3Bitag%3D140%3Blmt%3D1724731682752651/sgovp/clen%3D124753691%3Bdur%3D763.929%3Bgir%3Dyes%3Bitag%3D136%3Blmt%3D1724731665362215/rqh/1/hls_chunk_host/rr3---sn-p5h-jhoy.googlevideo.com/xpc/EgVo2aDSNQ%3D%3D/mh/vz/mm/31,29/mn/sn-p5h-jhoy,sn-apn7en7e/ms/au,rdu/mv/m/mvi/3/pl/17/initcwndbps/391250/spc/Mv1m9jMugnpWvFVyQljKM1pZQINUP9nMutRavY8GPMP3mBAHQ9x5lGAO2u3KZYw/vprv/1/playlist_type/CLEAN/dover/11/txp/6309224/mt/1724791751/fvip/3/keepalive/yes/sparams/expire,ei,ip,id,itag,source,requiressl,ratebypass,pfa,sgoap,sgovp,rqh,xpc,spc,vprv,playlist_type/sig/AJfQdSswRgIhAMa0AaZTL5nXnfvp1VDJ4gn-ylIf27qrkkPPCfKcQdhWAiEA8kT6k6FMEcLDZyajuXU4Xjv5Hgq1UaoUO0cY9-Lg9-o%3D/lsparams/hls_chunk_host,mh,mm,mn,ms,mv,mvi,pl,initcwndbps/lsig/AGtxev0wRQIgNkG32KEZ70LGhdiGo7c6yUoFsSeY4bwK5aEfKEtR4HYCIQDDYgZn1fzkygu-mlUkuG7jobmyRGNmBGjtBgVGwn6xLg%3D%3D/playlist/index.m3u8 +#EXT-X-STREAM-INF:BANDWIDTH=3303829,CODECS="mp4a.40.2,avc1.640028",RESOLUTION=1920x1080,FRAME-RATE=30,VIDEO-RANGE=SDR,CLOSED-CAPTIONS=NONE +https://manifest.googlevideo.com/api/manifest/hls_playlist/expire/1724813705/ei/KT3OZryRFbDCmLAP0ZW0sA4/ip/102.49.55.161/id/c303be7a57ea6f28/itag/96/source/youtube/requiressl/yes/ratebypass/yes/pfa/1/sgoap/clen%3D12365022%3Bdur%3D763.982%3Bgir%3Dyes%3Bitag%3D140%3Blmt%3D1724731682752651/sgovp/clen%3D239812801%3Bdur%3D763.929%3Bgir%3Dyes%3Bitag%3D137%3Blmt%3D1724731668686890/rqh/1/hls_chunk_host/rr3---sn-p5h-jhoy.googlevideo.com/xpc/EgVo2aDSNQ%3D%3D/mh/vz/mm/31,29/mn/sn-p5h-jhoy,sn-apn7en7e/ms/au,rdu/mv/m/mvi/3/pl/17/initcwndbps/391250/spc/Mv1m9jMugnpWvFVyQljKM1pZQINUP9nMutRavY8GPMP3mBAHQ9x5lGAO2u3KZYw/vprv/1/playlist_type/CLEAN/dover/11/txp/6309224/mt/1724791751/fvip/3/keepalive/yes/sparams/expire,ei,ip,id,itag,source,requiressl,ratebypass,pfa,sgoap,sgovp,rqh,xpc,spc,vprv,playlist_type/sig/AJfQdSswRAIgCNm9TIJF81RAJYu1R-iO2sO1Kaot68NTChhl5IG8zUYCIGbVhQ9g5EL3N7YXGUzixiuGjAa7kr4Wu53mWKI64qS8/lsparams/hls_chunk_host,mh,mm,mn,ms,mv,mvi,pl,initcwndbps/lsig/AGtxev0wRQIgOZThILm66IQqLiz4n5OS5q8IofgfGch1vnVcDlebuUMCIQD-xahK9PpTY1k40zs-3obT6UbXrxK0ri3aSAwLgGn59g%3D%3D/playlist/index.m3u8