/// <summary> /// Apply the decrypted signature to the stream manifest. /// </summary> public static void ApplySignature(ObscuredContainer streamContainer, string js) { int numSignaturesFound = 0; foreach (KeyValuePair <string, object> s in streamContainer) { // Iterate over each stream and sign the URLs ObscuredContainer stream = (ObscuredContainer)s.Value; string URL = stream.GetValue <string>("url"); if (URL.Contains("signature=")) { // Sometimes, signature is provided directly by YT, so we can skip the whole signature descrambling numSignaturesFound++; continue; } // Get, decode and save signature string cipheredSignature = stream.GetValue <string>("s"); string signature = Cipher.DecodeSignature(js, cipheredSignature); stream["url"] = URL + "&signature=" + signature; /*YouTube.Log(string.Format ( * "Descrambled Signature for ITag={0} \n \t s={1} \n \t signature={2}", * stream.GetValue<string>("itag"), * cipheredSignature, * signature));*/ } if (numSignaturesFound > 0) { CSTube.Log(string.Format("{0} out of {1} URLs already contained a signature!", numSignaturesFound, streamContainer.Count)); } }
/// <summary> /// Read the YouTube player configuration (args and assets) from the JSON data embedded into the HTML page. /// It serves as the primary source of obtaining the stream manifest data. /// </summary> internal static YTPlayerConfig getYTPlayerConfig(string watchHTML) { string configRaw = Helpers.DoRegex(extractYTPlayerConfig, watchHTML, 1); if (string.IsNullOrEmpty(configRaw)) { CSTube.Log("ERROR: Video is unavailable!"); return(new YTPlayerConfig()); } // Get config as JSON structure JObject obj = Helpers.TryParseJObject(configRaw); JToken argsToken = obj.GetValue("args"); JToken assetsToken = obj.GetValue("assets"); // Create config and read it from JSON YTPlayerConfig config = new YTPlayerConfig(); config.args = ObscuredContainer.FromJSONRecursive(argsToken, 10); config.assets = ObscuredContainer.FromJSONRecursive(assetsToken, 10); if (config.args == null || config.assets == null) { CSTube.Log("ERROR: Player Config JSON is invalid!"); } return(config); }
/// <summary> /// Extracts the raw transform plan as a list of havascript functions from the specified code. /// </summary> private static string[] getTransformPlan(string js) { string name = Regex.Escape(getInitialFunctionName(js)); string pattern = name + @"=function\([a-z]\)\{[a-z]=[a-z]\.split\(""""\);(.*?);return [a-z].join\(""""\)\};"; string[] plan = Helpers.DoRegex(pattern, js, 1).Split(';'); CSTube.Log("Transform Plan " + name + ":" + string.Join(" || ", plan)); return(plan); }
/// <summary> /// Creates a new Youtube video inspector and automatically fetches video information asynchronously. /// </summary> public static async Task <Video> Fetch(string url, bool log = true) { CSTube.SetLogState(log); Video video = new Video(url); await video.FetchInformation(); CSTube.SetLogState(true); return(video); }
/// <summary> /// Extracts the transform object from the specified funcVar in the specified javascript code. /// Returns a list of function definitions inside the object. /// Format: {ID}:{FunctionDef} /// </summary> private static string[] getTransformObject(string js, string funcVar) { string pattern = "var " + Regex.Escape(funcVar) + @"=\{(.*?)\};"; string[] obj = Helpers.DoRegex(pattern, js, 1, RegexOptions.Singleline) .Replace("\n", " ") .Split(new string[] { ", " }, StringSplitOptions.RemoveEmptyEntries) .Select(s => s.Trim()) .ToArray(); CSTube.Log("Transform Object " + funcVar + ": " + string.Join(" || ", obj)); return(obj); }
/// <summary> /// Write the media stream to the specified path. /// </summary> public void Download(string path) { CSTube.Log(string.Format("Downloading File to {0}", path)); Directory.CreateDirectory(Path.GetDirectoryName(path)); HttpWebRequest req = (HttpWebRequest)HttpWebRequest.Create(URL); using (HttpWebResponse response = (HttpWebResponse)req.GetResponse()) { CSTube.Log(string.Format("Total file size in bytes: {0}", response.Headers["content-length"])); using (System.IO.Stream input = response.GetResponseStream()) using (System.IO.Stream output = File.OpenWrite(path)) input.CopyToAsync(output).Wait(); } }
/// <summary> /// Reads all HTML from the specified URL synchronously. /// </summary> public static async Task <string> ReadHTML(string URL) { string result; WebRequest req = WebRequest.Create(URL); req.Method = "GET"; try { using (StreamReader reader = new StreamReader(req.GetResponse().GetResponseStream())) result = await reader.ReadToEndAsync(); } catch (WebException e) { CSTube.Log("ERROR: Request Timeout, check your internet connection!"); return(null); } return(result); }
/// <summary> /// Fetches the information about this video (including available streams and captions). /// Conains two synchronous HTTP fetches and several descrambling and signing algorithms. /// </summary> public async Task FetchInformation() { CSTube.Log("Fetching information of video " + videoID); // Page HTML watchHTML = await Helpers.ReadHTML(watchURL); if (string.IsNullOrEmpty(watchHTML)) { return; } // PlayerConfig JSON playerConfig = Extract.getYTPlayerConfig(watchHTML); if (playerConfig.args == null || playerConfig.assets == null) { return; } // JavaScript Source string jsURL = Extract.getJSURL(playerConfig); jsSRC = await Helpers.ReadHTML(jsURL); // VideoInfo Raw Data //string videoInfoURL = Extract.getVideoInfoURL(videoID, watchURL, watchHTML); //videoInfoRAW = Helpers.ReadHTML(videoInfoURL); CSTube.Log("Finished downloading information! Continuing to parse!"); // Parse Raw video info data //System.Collections.Specialized.NameValueCollection p = HttpUtility.ParseQueryString(videoInfoRAW); //videoInfo = ObscuredContainer.FromDictionary(p.AllKeys.ToDictionary(k => k, k => (object)p[k])); // Get all stream formats this video has (progressive and adaptive) List <string> streamMaps = new List <string>(); streamMaps.Add("url_encoded_fmt_stream_map"); if (playerConfig.args.ContainsKey("adaptive_fmts")) { streamMaps.Add("adaptive_fmts"); } foreach (string streamFormat in streamMaps) { // Descramble stream data in player args ObscuredContainer streamBundle = Mixins.ApplyDescrambler(playerConfig.args, streamFormat); // If required, apply signature to the stream URLs Mixins.ApplySignature(streamBundle, jsSRC); // Write stream data into Stream objects foreach (object streamData in streamBundle.Values) { formatStreams.Add(new Stream((ObscuredContainer)streamData)); } } // Try to read out captionTracks if existant ObscuredContainer captionTrackBundle = Helpers.TryGetAttribute <ObscuredContainer>(playerConfig.args, "player_response/captions/playerCaptionsTracklistRenderer/captionTracks"); if (captionTrackBundle != null) { // Write caption tracks into Caption objects foreach (object captionTrack in captionTrackBundle.Values) { captionTracks.Add(new Caption((ObscuredContainer)captionTrack)); } } videoDataAvailable = true; // Log success! CSTube.Log(string.Format( "Finished parsing video data! Found {0} video streams and {1} caption tracks for video '{2}'!", formatStreams.Count, captionTracks.Count, title )); CSTube.Log(string.Format("Video Streams: \n \t " + string.Join(" \n \t ", formatStreams.Select(s => s.ToString())) )); if (captionTracks.Count > 0) { CSTube.Log(string.Format("Caption Tracks: \n \t " + string.Join(" \n \t ", captionTracks.Select(s => s.ToString())) )); } }