public async Task ExecuteAsync() { context.OnProgresStateChanged(YoutubeStage.StartingDownload); if (context.VideoInfo == null) throw new InvalidOperationException("Cant extract audio when no VideoInfo is selected."); HttpResponseMessage response; var rpf = new RetryableProcessFailed("Video Downloader") {Tag=context}; retry:try { response = await _httpClient.GetAsync(context.VideoInfo.DownloadUrl); } catch (Exception e) { rpf.Defaultize(e); context.OnDownloadFailed(rpf); if (rpf.ShouldRetry) goto retry; return; } if (!response.IsSuccessStatusCode) throw new Exception(); context.VideoPath = new FileInfo(context.VideoPath?.FullName ?? context.VideoSaveableFilename); using (var downloadStream = await response.Content.ReadAsStreamAsync()) using (var fileStream = File.Open(context.VideoPath.FullName, FileMode.Create, FileAccess.Write)) { var buffer = new byte[0x4000]; //16KB buffer var cancelRequest = false; int bytes; double bytesDownloaded = 0; while (!cancelRequest && (bytes = await downloadStream.ReadAsync(buffer, 0, buffer.Length)) > 0) { await fileStream.WriteAsync(buffer, 0, bytes); bytesDownloaded += bytes; var e = context.OnProgresStateChanged(YoutubeStage.Downloading, ((bytesDownloaded / downloadStream.Length) * 100)); if (e.Cancel) cancelRequest = true; } } context.OnProgresStateChanged(YoutubeStage.DownloadFinished); }
/// <summary> /// Occurs when the downlaod progress of the video file has changed. /// </summary> /// <summary> /// Starts the video download. /// </summary> /// <exception cref="IOException">The video file could not be saved.</exception> /// <exception cref="WebException">An error occured while downloading the video.</exception> public override void Execute() { context.OnProgresStateChanged(YoutubeStage.StartingDownload); if (context.VideoInfo==null) throw new InvalidOperationException("Cant extract audio when no VideoInfo is selected."); HttpWebRequest request; var rpf = new RetryableProcessFailed("Video Downloader") { Tag = context }; retry:try { request = (HttpWebRequest)WebRequest.Create(context.VideoInfo.DownloadUrl); } catch (Exception e) { rpf.Defaultize(e); context.OnDownloadFailed(rpf); if (rpf.ShouldRetry) goto retry; return; } if (context.BytesToDownload.HasValue) request.AddRange(0, context.BytesToDownload.Value - 1); context.VideoPath = new FileInfo(context.VideoPath?.FullName ?? context.VideoSaveableFilename); using (var response = request.GetResponse()) using (var source = response.GetResponseStream()) using (var target = File.Open(context.VideoPath.FullName, FileMode.Create, FileAccess.Write)) { var buffer = new byte[1024]; var cancel = false; int bytes; var copiedBytes = 0; while (!cancel && (bytes = source.Read(buffer, 0, buffer.Length)) > 0) { target.Write(buffer, 0, bytes); copiedBytes += bytes; var e = context.OnProgresStateChanged(YoutubeStage.Downloading, (copiedBytes * 1.0 / response.ContentLength) * 100f); if (e.Cancel) cancel = true; } } context.OnProgresStateChanged(YoutubeStage.DownloadFinished); }
/// <summary> /// Gets a list of <see cref="VideoInfo" />s for the specified URL. /// </summary> /// <param name="context">The context, must contain a url to the video in the Url property</param> /// <param name="decryptSignature"> /// A value indicating whether the video signatures should be decrypted or not. Decrypting /// consists of a HTTP request for each <see cref="VideoInfo" />, so you may want to set /// this to false and call <see cref="DecryptDownloadUrl" /> on your selected /// <see /// cref="VideoInfo" /> /// later. /// </param> /// <returns>A list of <see cref="VideoInfo" />s that can be used to download the video.</returns> /// <exception cref="VideoNotAvailableException">The video is not available.</exception> /// <exception cref="WebException"> /// An error occurred while downloading the YouTube page html. /// </exception> /// <exception cref="YoutubeParseException">The Youtube page could not be parsed.</exception> public static IEnumerable<VideoInfo> GetDownloadUrls(YoutubeContext context, bool decryptSignature = true) { if (context == null) throw new ArgumentNullException(nameof(context)); if (context.Url == null) throw new ArgumentNullException(nameof(context.Url)); context.OnProgresStateChanged(YoutubeStage.ProcessingUrls); string ytb; var isYoutubeUrl = TryNormalizeYoutubeUrl(context.Url, out ytb); context.Url = ytb; if (!isYoutubeUrl) throw new ArgumentException("URL is not a valid youtube URL!"); _retry: try { var rpf = new RetryableProcessFailed("ParseHtml5Version") {Tag = context.Url}; _redownload: var json = LoadJson(context.Url); var videoTitle = GetVideoTitle(json); var n = 0; var downloadUrls = ExtractDownloadUrls(json); var infos = GetVideoInfos(downloadUrls, videoTitle, context.Url).ToArray(); try { var htmlPlayerVersion = GetHtml5PlayerVersion(json); foreach (var info in infos) { info.HtmlPlayerVersion = htmlPlayerVersion; if (decryptSignature && info.RequiresDecryption) DecryptDownloadUrl(info); } return infos; } catch (Exception e) { rpf.Defaultize(e); context.OnDownloadFailed(rpf); Console.WriteLine(e); if (rpf.ShouldRetry) goto _redownload; return null; } } catch (Exception ex) when (ex.Message == "Result cannot be called on a failed Match.") { goto _retry; } catch (Exception ex) { if (ex is WebException || ex is VideoNotAvailableException) throw; ThrowYoutubeParseException(ex, context.Url); } //Message return null; // Will never happen, but the compiler requires it }
/// <summary> /// Extracts the playlist from the url, wether its on a playlist page or a side playlist when playing a video. /// Will return the urls of the songs /// </summary> public static async Task<List<string>> ExtractPlaylistAsync(string url) { url = NormalizeYoutubePlaylistUrl(url); if (string.IsNullOrEmpty(url)) throw new ArgumentNullException(nameof(url)); string pageSource; var rpf = new RetryableProcessFailed("LoadUrls") {Tag = url}; retry: try { pageSource = await _httpClient.DownloadStringTaskAsync(url); } catch (Exception e) { rpf.Defaultize(e); //TODO FailedDownload?.Invoke(rpf); if (rpf.ShouldRetry) goto retry; return null; } var doc = new HtmlDocument(); doc.LoadHtml(pageSource); return _extractPlaylistUrls(url, doc); }
private static async Task<JObject> LoadJsonAsync(string url) { string pageSource; var rpf = new RetryableProcessFailed("LoadUrls") {Tag = url}; retry: try { pageSource = await _httpClient.DownloadStringTaskAsync(url); } catch (Exception e) { rpf.Defaultize(e); //TODO FailedDownload?.Invoke(rpf); if (rpf.ShouldRetry) goto retry; return null; } if (IsVideoUnavailable(pageSource)) throw new VideoNotAvailableException(); var dataRegex = new Regex(@"ytplayer\.config\s*=\s*(\{.+?\});", RegexOptions.Multiline); var extractedJson = dataRegex.Match(pageSource).Result("$1"); return JObject.Parse(extractedJson); }
private static JObject LoadJson(string url) { string pageSource; var rpf = new RetryableProcessFailed("LoadUrls") {Tag = url}; var timeout = 1500u; retry: try { pageSource = HttpHelper.DownloadString(url, timeout); } catch (Exception e) { rpf.Defaultize(e); //TODO FailedDownload?.Invoke(rpf); if (rpf.ShouldRetry) { timeout += 500; goto retry; } return null; } if (IsVideoUnavailable(pageSource)) throw new VideoNotAvailableException(); var dataRegex = new Regex(@"ytplayer\.config\s*=\s*(\{.+?\});", RegexOptions.Multiline); var extractedJson = dataRegex.Match(pageSource).Result("$1"); return JObject.Parse(extractedJson); }
/// <summary> /// Gets a list of <see cref="VideoInfo" />s for the specified URL. /// </summary> /// <param name="context">The context, must contain a url to the video in the Url property</param> /// <param name="decryptSignature"> /// A value indicating whether the video signatures should be decrypted or not. Decrypting /// consists of a HTTP request for each <see cref="VideoInfo" />, so you may want to set /// this to false and call <see cref="DecryptDownloadUrl" /> on your selected /// <see /// cref="VideoInfo" /> /// later. /// </param> /// <returns>A list of <see cref="VideoInfo" />s that can be used to download the video.</returns> /// <exception cref="VideoNotAvailableException">The video is not available.</exception> /// <exception cref="WebException"> /// An error occurred while downloading the YouTube page html. /// </exception> /// <exception cref="YoutubeParseException">The Youtube page could not be parsed.</exception> public static async Task<IEnumerable<VideoInfo>> GetDownloadUrlsAsync(YoutubeContext context, bool decryptSignature = true) { if (context == null) throw new ArgumentNullException(nameof(context)); if (context.Url == null) throw new ArgumentNullException(nameof(context.Url)); context.OnProgresStateChanged(YoutubeStage.ProcessingUrls); string ytb; var isYoutubeUrl = TryNormalizeYoutubeUrl(context.Url, out ytb); context.Url = ytb; if (!isYoutubeUrl) throw new ArgumentException("URL is not a valid youtube URL!"); try { var rpf = new RetryableProcessFailed("ParseHtml5Version") {Tag = context.Url }; _redownload: var json = await LoadJsonAsync(context.Url); var videoTitle = GetVideoTitle(json); int n = 0; var downloadUrls = ExtractDownloadUrls(json); var infos = GetVideoInfos(downloadUrls, videoTitle, context.Url); string htmlPlayerVersion; try { htmlPlayerVersion = GetHtml5PlayerVersion(json); } catch (Exception e) { rpf.Defaultize(e); context.OnDownloadFailed(rpf); if (rpf.ShouldRetry) goto _redownload; return null; } foreach (var info in infos) { info.HtmlPlayerVersion = htmlPlayerVersion; if (decryptSignature && info.RequiresDecryption) await DecryptDownloadUrlAsync(info); } return infos; } catch (Exception ex) { if (ex is WebException || ex is VideoNotAvailableException) throw; ThrowYoutubeParseException(ex, context.Url); } return null; // Will never happen, but the compiler requires it }
internal void OnDownloadFailed(RetryableProcessFailed e) { DownloadFailed?.Invoke(this, e); }
public static async Task<string> DecipherWithVersionAsync(VideoInfo vidinfo, string cipher, string cipherVersion) { var jsUrl = $"http://s.ytimg.com/yts/jsbin/player-{cipherVersion}.js"; string js; var rpf = new RetryableProcessFailed("LoadUrls") {Tag = vidinfo}; var timeout = 1500u; retry: try { js = await HttpHelper.DownloadStringAsync(jsUrl, timeout); } catch (Exception e) { rpf.Defaultize(e); if (rpf.ShouldRetry && rpf.NumberOfTries <= 10) { timeout += 500; goto retry; } return null; } //Find "C" in this: var A = B.sig||C (B.s) var functNamePattern = @"\.sig\s*\|\|([a-zA-Z0-9\$]+)\("; //Regex Formed To Find Word or DollarSign var funcName = Regex.Match(js, functNamePattern).Groups[1].Value; if (funcName.Contains("$")) funcName = "\\" + funcName; //Due To Dollar Sign Introduction, Need To Escape var funcBodyPattern = @"(?<brace>{([^{}]| ?(brace))*})"; //Match nested angle braces var funcPattern = @"var " + @funcName + @"=function\(\w+\)\{.*?\};"; //Escape funcName string var funcBody = Regex.Match(js, funcPattern).Value; //Entire sig function var lines = funcBody.Split(';'); //Each line in sig function string idReverse = "", idSlice = "", idCharSwap = ""; //Hold name for each cipher method var functionIdentifier = ""; var operations = ""; foreach (var line in lines.Skip(1).Take(lines.Length - 2)) //Matches the funcBody with each cipher method. Only runs till all three are defined. { if (!string.IsNullOrEmpty(idReverse) && !string.IsNullOrEmpty(idSlice) && !string.IsNullOrEmpty(idCharSwap)) break; //Break loop if all three cipher methods are defined functionIdentifier = GetFunctionFromLine(line); var reReverse = $@"{functionIdentifier}:\bfunction\b\(\w+\)"; //Regex for reverse (one parameter) var reSlice = $@"{functionIdentifier}:\bfunction\b\([a],b\).(\breturn\b)?.?\w+\."; //Regex for slice (return or not) var reSwap = $@"{functionIdentifier}:\bfunction\b\(\w+\,\w\).\bvar\b.\bc=a\b"; //Regex for the char swap. if (Regex.Match(js, reReverse).Success) idReverse = functionIdentifier; //If def matched the regex for reverse then the current function is a defined as the reverse if (Regex.Match(js, reSlice).Success) idSlice = functionIdentifier; //If def matched the regex for slice then the current function is defined as the slice. if (Regex.Match(js, reSwap).Success) idCharSwap = functionIdentifier; //If def matched the regex for charSwap then the current function is defined as swap. } foreach (var line in lines.Skip(1).Take(lines.Length - 2)) { Match m; functionIdentifier = GetFunctionFromLine(line); if ((m = Regex.Match(line, @"\(\w+,(?<index>\d+)\)")).Success && functionIdentifier == idCharSwap) operations += "w" + m.Groups["index"].Value + " "; //operation is a swap (w) if ((m = Regex.Match(line, @"\(\w+,(?<index>\d+)\)")).Success && functionIdentifier == idSlice) operations += "s" + m.Groups["index"].Value + " "; //operation is a slice if (functionIdentifier == idReverse) //No regex required for reverse (reverse method has no parameters) operations += "r "; //operation is a reverse } operations = operations.Trim(); return DecipherWithOperations(cipher, operations); }