/// <inheritdoc /> public async Task <IReadOnlyList <ClosedCaptionTrackInfo> > GetVideoClosedCaptionTrackInfosAsync(string videoId) { videoId.GuardNotNull(nameof(videoId)); if (!ValidateVideoId(videoId)) { throw new ArgumentException($"Invalid YouTube video ID [{videoId}].", nameof(videoId)); } // Get parser var parser = await GetPlayerResponseParserAsync(videoId); // Parse closed caption track infos var closedCaptionTrackInfos = new List <ClosedCaptionTrackInfo>(); foreach (var closedCaptionTrackInfoParser in parser.GetClosedCaptionTrackInfos()) { // Parse info var url = closedCaptionTrackInfoParser.ParseUrl(); var isAutoGenerated = closedCaptionTrackInfoParser.ParseIsAutoGenerated(); // Parse language var code = closedCaptionTrackInfoParser.ParseLanguageCode(); var name = closedCaptionTrackInfoParser.ParseLanguageName(); var language = new Language(code, name); // Enforce format to the one we know how to parse url = UrlEx.SetQueryParameter(url, "format", "3"); var closedCaptionTrackInfo = new ClosedCaptionTrackInfo(url, language, isAutoGenerated); closedCaptionTrackInfos.Add(closedCaptionTrackInfo); } return(closedCaptionTrackInfos); }
private async Task <IReadOnlyDictionary <string, string> > GetVideoInfoAsync(string videoId, string sts = "") { // Get video info with 'el=embedded' var raw = await GetVideoInfoRawAsync(videoId, "embedded", sts).ConfigureAwait(false); var videoInfo = UrlEx.SplitQuery(raw); // If there is no error - return if (!videoInfo.ContainsKey("errorcode")) { return(videoInfo); } // Get video info with 'el=detailpage' raw = await GetVideoInfoRawAsync(videoId, "detailpage", sts).ConfigureAwait(false); videoInfo = UrlEx.SplitQuery(raw); // If there is no error - return if (!videoInfo.ContainsKey("errorcode")) { return(videoInfo); } // If there is error - throw var errorCode = videoInfo["errorcode"].ParseInt(); var errorReason = videoInfo["reason"]; throw new VideoUnavailableException(videoId, errorCode, errorReason); }
private IDictionary <string, string> GetVideoInfoAsync(string videoId, string sts = "") { // Get video info var raw = GetVideoInfoRawAsync(videoId, "embedded", sts); //.ConfigureAwait(false); var videoInfo = UrlEx.SplitQuery(raw); // If can't be embedded - try another value of el if (videoInfo.ContainsKey("errorcode")) { var errorReason = videoInfo["reason"]; if (errorReason.Contains("&feature=player_embedded")) { raw = GetVideoInfoRawAsync(videoId, "detailpage", sts); //.ConfigureAwait(false); videoInfo = UrlEx.SplitQuery(raw); } } // Check error if (videoInfo.ContainsKey("errorcode")) { var errorCode = videoInfo["errorcode"].ParseInt(); var errorReason = videoInfo["reason"]; //throw new VideoUnavailableException(videoId, errorCode, errorReason); return(null); } return(videoInfo); }
public void PluginRawPageUrl_PluginId_ReturnsUrlEncodedPathToPlugin(string pluginId, string expectedPluginUrl) { // Arrange & Act var actualPluginUrl = UrlEx.PluginRawPageUrl(pluginId); // Assert Assert.AreEqual(expectedPluginUrl, actualPluginUrl); }
public string ParseDescription() { var buffer = new StringBuilder(); var descriptionNode = _root.QuerySelector("p#eow-description"); var childNodes = descriptionNode.ChildNodes; foreach (var childNode in childNodes) { // If it's a text node - display text content if (childNode.NodeType == NodeType.Text) { buffer.Append(childNode.TextContent); } // If it's an anchor node - perform some special transformation else if (childNode is IHtmlAnchorElement anchorNode) { // If the link appears shortened - get full link if (anchorNode.TextContent.EndsWith("...", StringComparison.OrdinalIgnoreCase)) { // Get href var href = anchorNode.GetAttribute("href"); // If it's a relative link that goes through YouTube redirect - extract the actual link if (href.StartsWith("/redirect", StringComparison.OrdinalIgnoreCase)) { // Get query parameters var queryParams = UrlEx.SplitQuery(anchorNode.Search); // Get the actual href href = queryParams["q"]; } // If it's a relative link - prepend YouTube's host else if (href.StartsWith("/", StringComparison.OrdinalIgnoreCase)) { // Prepend host to the link to make it absolute href = "https://youtube.com" + anchorNode.GetAttribute("href"); } buffer.Append(href); } // Otherwise - just use its inner text else { buffer.Append(anchorNode.TextContent); } } // If it's a break row node - append new line else if (childNode is IHtmlBreakRowElement) { buffer.AppendLine(); } } return(buffer.ToString()); }
/// <inheritdoc /> public List <ClosedCaptionTrackInfo> GetVideoClosedCaptionTrackInfosAsync(string videoId) { videoId.GuardNotNull(nameof(videoId)); if (!ValidateVideoId(videoId)) { throw new ArgumentException($"Invalid YouTube video ID [{videoId}].", nameof(videoId)); } // Get video info var videoInfo = GetVideoInfoAsync(videoId); //.ConfigureAwait(false); if (videoInfo == null) { return new List <ClosedCaptionTrackInfo>() { } } ; // Extract captions metadata var playerResponseRaw = videoInfo["player_response"]; var playerResponseJson = JToken.Parse(playerResponseRaw); var captionTracksJson = playerResponseJson.SelectToken("$..captionTracks").EmptyIfNull(); // Parse closed caption tracks var closedCaptionTrackInfos = new List <ClosedCaptionTrackInfo>(); foreach (var captionTrackJson in captionTracksJson) { // Extract values var code = captionTrackJson["languageCode"].Value <string>(); var name = captionTrackJson["name"]["simpleText"].Value <string>(); var language = new Language(code, name); var isAuto = captionTrackJson["vssId"].Value <string>() .StartsWith("a.", StringComparison.OrdinalIgnoreCase); var url = captionTrackJson["baseUrl"].Value <string>(); // Enforce format url = UrlEx.SetQueryParameter(url, "format", "3"); var closedCaptionTrackInfo = new ClosedCaptionTrackInfo(url, language, isAuto); closedCaptionTrackInfos.Add(closedCaptionTrackInfo); } return(closedCaptionTrackInfos); } }
private async Task <IReadOnlyDictionary <string, string> > GetVideoInfoAsync(string videoId, string sts = "") { var raw = await GetVideoInfoRawAsync(videoId, "embedded", sts).ConfigureAwait(false); var videoInfo = UrlEx.SplitQuery(raw); // Check if there is an error if (videoInfo.ContainsKey("errorcode")) { var errorCode = videoInfo["errorcode"].ParseInt(); var errorReason = videoInfo["reason"]; throw new VideoUnavailableException(videoId, errorCode, errorReason); } return(videoInfo); }
public string ParseDescription() { var buffer = new StringBuilder(); var descriptionNode = _root.QuerySelector("p#eow-description"); var childNodes = descriptionNode.ChildNodes; foreach (var childNode in childNodes) { if (childNode.NodeType == NodeType.Text) { buffer.Append(childNode.TextContent); } else if (childNode is IHtmlAnchorElement anchorNode) { // If it uses YouTube redirect - get the actual link if (anchorNode.PathName.Equals("/redirect", StringComparison.OrdinalIgnoreCase)) { // Get query parameters var queryParams = UrlEx.SplitQuery(anchorNode.Search); // Get the actual href var actualHref = queryParams["q"].UrlDecode(); buffer.Append(actualHref); } else { buffer.Append(anchorNode.TextContent); } } else if (childNode is IHtmlBreakRowElement) { buffer.AppendLine(); } } return(buffer.ToString()); }
/// <inheritdoc /> public async Task <MediaStreamInfoSet> GetVideoMediaStreamInfosAsync(string videoId) { videoId.GuardNotNull(nameof(videoId)); if (!ValidateVideoId(videoId)) { throw new ArgumentException($"Invalid YouTube video ID [{videoId}].", nameof(videoId)); } // Get player context var playerContext = await GetVideoPlayerContextAsync(videoId).ConfigureAwait(false); // Get video info var videoInfo = await GetVideoInfoAsync(videoId, playerContext.Sts).ConfigureAwait(false); // Check if requires purchase if (videoInfo.ContainsKey("ypc_vid")) { var previewVideoId = videoInfo["ypc_vid"]; throw new VideoRequiresPurchaseException(videoId, previewVideoId); } // Prepare stream info collections var muxedStreamInfoMap = new Dictionary <int, MuxedStreamInfo>(); var audioStreamInfoMap = new Dictionary <int, AudioStreamInfo>(); var videoStreamInfoMap = new Dictionary <int, VideoStreamInfo>(); // Resolve muxed streams var muxedStreamInfosEncoded = videoInfo.GetOrDefault("url_encoded_fmt_stream_map"); if (muxedStreamInfosEncoded.IsNotBlank()) { foreach (var streamEncoded in muxedStreamInfosEncoded.Split(",")) { var streamInfoDic = UrlEx.SplitQuery(streamEncoded); // Extract values var itag = streamInfoDic["itag"].ParseInt(); var url = streamInfoDic["url"]; var sig = streamInfoDic.GetOrDefault("s"); #if RELEASE if (!MediaStreamInfo.IsKnown(itag)) { continue; } #endif // Decipher signature if needed if (sig.IsNotBlank()) { var playerSource = await GetVideoPlayerSourceAsync(playerContext.SourceUrl).ConfigureAwait(false); sig = playerSource.Decipher(sig); url = UrlEx.SetQueryParameter(url, "signature", sig); } // Probe stream and get content length var contentLength = await _httpClient.GetContentLengthAsync(url, false).ConfigureAwait(false) ?? -1; // If probe failed, the stream is gone or faulty if (contentLength < 0) { continue; } var streamInfo = new MuxedStreamInfo(itag, url, contentLength); muxedStreamInfoMap[itag] = streamInfo; } } // Resolve adaptive streams var adaptiveStreamInfosEncoded = videoInfo.GetOrDefault("adaptive_fmts"); if (adaptiveStreamInfosEncoded.IsNotBlank()) { foreach (var streamEncoded in adaptiveStreamInfosEncoded.Split(",")) { var streamInfoDic = UrlEx.SplitQuery(streamEncoded); // Extract values var itag = streamInfoDic["itag"].ParseInt(); var url = streamInfoDic["url"]; var sig = streamInfoDic.GetOrDefault("s"); var contentLength = streamInfoDic["clen"].ParseLong(); var bitrate = streamInfoDic["bitrate"].ParseLong(); #if RELEASE if (!MediaStreamInfo.IsKnown(itag)) { continue; } #endif // Decipher signature if needed if (sig.IsNotBlank()) { var playerSource = await GetVideoPlayerSourceAsync(playerContext.SourceUrl).ConfigureAwait(false); sig = playerSource.Decipher(sig); url = UrlEx.SetQueryParameter(url, "signature", sig); } // Check if audio var isAudio = streamInfoDic["type"].Contains("audio/"); // If audio stream if (isAudio) { var streamInfo = new AudioStreamInfo(itag, url, contentLength, bitrate); audioStreamInfoMap[itag] = streamInfo; } // If video stream else { // Parse additional data var size = streamInfoDic["size"]; var width = size.SubstringUntil("x").ParseInt(); var height = size.SubstringAfter("x").ParseInt(); var resolution = new VideoResolution(width, height); var framerate = streamInfoDic["fps"].ParseInt(); var qualityLabel = streamInfoDic["quality_label"]; var streamInfo = new VideoStreamInfo(itag, url, contentLength, bitrate, resolution, framerate, qualityLabel); videoStreamInfoMap[itag] = streamInfo; } } } // Resolve dash streams var dashManifestUrl = videoInfo.GetOrDefault("dashmpd"); if (dashManifestUrl.IsNotBlank()) { // Parse signature var sig = Regex.Match(dashManifestUrl, @"/s/(.*?)(?:/|$)").Groups[1].Value; // Decipher signature if needed if (sig.IsNotBlank()) { var playerSource = await GetVideoPlayerSourceAsync(playerContext.SourceUrl).ConfigureAwait(false); sig = playerSource.Decipher(sig); dashManifestUrl = UrlEx.SetRouteParameter(dashManifestUrl, "signature", sig); } // Get the manifest var response = await _httpClient.GetStringAsync(dashManifestUrl).ConfigureAwait(false); var dashManifestXml = XElement.Parse(response).StripNamespaces(); var streamsXml = dashManifestXml.Descendants("Representation"); // Parse streams foreach (var streamXml in streamsXml) { // Skip partial streams if (streamXml.Descendants("Initialization").FirstOrDefault()?.Attribute("sourceURL")?.Value .Contains("sq/") == true) { continue; } // Extract values var itag = (int)streamXml.Attribute("id"); var url = (string)streamXml.Element("BaseURL"); var bitrate = (long)streamXml.Attribute("bandwidth"); #if RELEASE if (!MediaStreamInfo.IsKnown(itag)) { continue; } #endif // Parse content length var contentLength = Regex.Match(url, @"clen[/=](\d+)").Groups[1].Value.ParseLong(); // Check if audio stream var isAudio = streamXml.Element("AudioChannelConfiguration") != null; // If audio stream if (isAudio) { var streamInfo = new AudioStreamInfo(itag, url, contentLength, bitrate); audioStreamInfoMap[itag] = streamInfo; } // If video stream else { // Parse additional data var width = (int)streamXml.Attribute("width"); var height = (int)streamXml.Attribute("height"); var resolution = new VideoResolution(width, height); var framerate = (int)streamXml.Attribute("frameRate"); var streamInfo = new VideoStreamInfo(itag, url, contentLength, bitrate, resolution, framerate); videoStreamInfoMap[itag] = streamInfo; } } } // Get the raw HLS stream playlist (*.m3u8) var hlsLiveStreamUrl = videoInfo.GetOrDefault("hlsvp"); // Finalize stream info collections var muxedStreamInfos = muxedStreamInfoMap.Values.OrderByDescending(s => s.VideoQuality).ToArray(); var audioStreamInfos = audioStreamInfoMap.Values.OrderByDescending(s => s.Bitrate).ToArray(); var videoStreamInfos = videoStreamInfoMap.Values.OrderByDescending(s => s.VideoQuality).ToArray(); return(new MediaStreamInfoSet(muxedStreamInfos, audioStreamInfos, videoStreamInfos, hlsLiveStreamUrl)); }
/// <inheritdoc /> public async Task <MediaStreamInfoSet> GetVideoMediaStreamInfosAsync(string videoId) { videoId.GuardNotNull(nameof(videoId)); if (!ValidateVideoId(videoId)) { throw new ArgumentException($"Invalid YouTube video ID [{videoId}].", nameof(videoId)); } // Get player context var playerContext = await GetVideoPlayerContextAsync(videoId).ConfigureAwait(false); // Get parser var parser = await GetVideoInfoParserAsync(videoId, playerContext.Sts).ConfigureAwait(false); // Check if video requires purchase var previewVideoId = parser.ParsePreviewVideoId(); if (previewVideoId.IsNotBlank()) { throw new VideoRequiresPurchaseException(videoId, previewVideoId); } // Prepare stream info maps var muxedStreamInfoMap = new Dictionary <int, MuxedStreamInfo>(); var audioStreamInfoMap = new Dictionary <int, AudioStreamInfo>(); var videoStreamInfoMap = new Dictionary <int, VideoStreamInfo>(); // Parse muxed stream infos foreach (var muxedStreamInfoParser in parser.GetMuxedStreamInfos()) { // Extract itag var itag = muxedStreamInfoParser.ParseItag(); #if RELEASE // Skip unknown itags if (!ItagHelper.IsKnown(itag)) { continue; } #endif // Extract URL var url = muxedStreamInfoParser.ParseUrl(); // Decipher signature if needed var signature = muxedStreamInfoParser.ParseSignature(); if (signature.IsNotBlank()) { var playerSource = await GetVideoPlayerSourceAsync(playerContext.SourceUrl).ConfigureAwait(false); signature = playerSource.Decipher(signature); url = UrlEx.SetQueryParameter(url, "signature", signature); } // Probe stream and get content length var contentLength = await _httpClient.GetContentLengthAsync(url, false).ConfigureAwait(false) ?? -1; // If probe failed or content length is 0, it means the stream is gone or faulty if (contentLength <= 0) { continue; } var streamInfo = new MuxedStreamInfo(itag, url, contentLength); muxedStreamInfoMap[itag] = streamInfo; } // Parse adaptive stream infos foreach (var adaptiveStreamInfoParser in parser.GetAdaptiveStreamInfos()) { // Extract info var itag = adaptiveStreamInfoParser.ParseItag(); #if RELEASE // Skip unknown itags if (!ItagHelper.IsKnown(itag)) { continue; } #endif // Extract content length var contentLength = adaptiveStreamInfoParser.ParseContentLength(); // If content length is 0, it means that the stream is gone or faulty if (contentLength <= 0) { continue; } // Extract URL var url = adaptiveStreamInfoParser.ParseUrl(); // Decipher signature if needed var signature = adaptiveStreamInfoParser.ParseSignature(); if (signature.IsNotBlank()) { var playerSource = await GetVideoPlayerSourceAsync(playerContext.SourceUrl).ConfigureAwait(false); signature = playerSource.Decipher(signature); url = UrlEx.SetQueryParameter(url, "signature", signature); } // Extract bitrate var bitrate = adaptiveStreamInfoParser.ParseBitrate(); // If audio-only if (adaptiveStreamInfoParser.ParseIsAudioOnly()) { var streamInfo = new AudioStreamInfo(itag, url, contentLength, bitrate); audioStreamInfoMap[itag] = streamInfo; } // If video-only else { // Extract info var width = adaptiveStreamInfoParser.ParseWidth(); var height = adaptiveStreamInfoParser.ParseHeight(); var framerate = adaptiveStreamInfoParser.ParseFramerate(); var qualityLabel = adaptiveStreamInfoParser.ParseQualityLabel(); var resolution = new VideoResolution(width, height); var streamInfo = new VideoStreamInfo(itag, url, contentLength, bitrate, resolution, framerate, qualityLabel); videoStreamInfoMap[itag] = streamInfo; } } // Parse dash manifest var dashManifestUrl = parser.ParseDashManifestUrl(); if (dashManifestUrl.IsNotBlank()) { // Parse signature var signature = Regex.Match(dashManifestUrl, @"/s/(.*?)(?:/|$)").Groups[1].Value; // Decipher signature if needed if (signature.IsNotBlank()) { var playerSource = await GetVideoPlayerSourceAsync(playerContext.SourceUrl).ConfigureAwait(false); signature = playerSource.Decipher(signature); dashManifestUrl = UrlEx.SetRouteParameter(dashManifestUrl, "signature", signature); } // Get the dash manifest parser var dashManifestParser = await GetDashManifestParserAsync(dashManifestUrl).ConfigureAwait(false); // Parse dash stream infos foreach (var dashStreamInfoParser in dashManifestParser.GetStreamInfos()) { // Extract itag var itag = dashStreamInfoParser.ParseItag(); #if RELEASE // Skip unknown itags if (!ItagHelper.IsKnown(itag)) { continue; } #endif // Extract info var url = dashStreamInfoParser.ParseUrl(); var contentLength = dashStreamInfoParser.ParseContentLength(); var bitrate = dashStreamInfoParser.ParseBitrate(); // If audio-only if (dashStreamInfoParser.ParseIsAudioOnly()) { var streamInfo = new AudioStreamInfo(itag, url, contentLength, bitrate); audioStreamInfoMap[itag] = streamInfo; } // If video-only else { // Parse additional data var width = dashStreamInfoParser.ParseWidth(); var height = dashStreamInfoParser.ParseHeight(); var framerate = dashStreamInfoParser.ParseFramerate(); var resolution = new VideoResolution(width, height); var streamInfo = new VideoStreamInfo(itag, url, contentLength, bitrate, resolution, framerate); videoStreamInfoMap[itag] = streamInfo; } } } // Get the raw HLS stream playlist (*.m3u8) var hlsPlaylistUrl = parser.ParseHlsPlaylistUrl(); // Finalize stream info collections var muxedStreamInfos = muxedStreamInfoMap.Values.OrderByDescending(s => s.VideoQuality).ToArray(); var audioStreamInfos = audioStreamInfoMap.Values.OrderByDescending(s => s.Bitrate).ToArray(); var videoStreamInfos = videoStreamInfoMap.Values.OrderByDescending(s => s.VideoQuality).ToArray(); return(new MediaStreamInfoSet(muxedStreamInfos, audioStreamInfos, videoStreamInfos, hlsPlaylistUrl)); }
private async Task <IReadOnlyDictionary <string, string> > GetVideoInfoAsync(string videoId, string el, string sts) { var raw = await GetVideoInfoRawAsync(videoId, el, sts).ConfigureAwait(false); return(UrlEx.SplitQuery(raw)); }