/// <summary> /// Gets the manifest containing information about available closed caption tracks on the specified video. /// </summary> public async ValueTask <ClosedCaptionManifest> GetManifestAsync( VideoId videoId, CancellationToken cancellationToken = default) { var watchPage = await _controller.GetVideoWatchPageAsync(videoId, cancellationToken); var playerResponse = watchPage.TryGetPlayerResponse() ?? throw new YoutubeExplodeException("Could not extract player response."); var trackInfos = playerResponse .GetClosedCaptionTracks() .Select(t => { var url = t.TryGetUrl() ?? throw new YoutubeExplodeException("Could not extract track URL."); var languageCode = t.TryGetLanguageCode() ?? throw new YoutubeExplodeException("Could not extract track language code."); var languageName = t.TryGetLanguageName() ?? throw new YoutubeExplodeException("Could not extract track language name."); var isAutoGenerated = t.IsAutoGenerated(); return(new ClosedCaptionTrackInfo( url, new Language(languageCode, languageName), isAutoGenerated )); }) .ToArray(); return(new ClosedCaptionManifest(trackInfos)); }
private async ValueTask PopulateStreamInfosAsync( ICollection <IStreamInfo> streamInfos, VideoId videoId, CancellationToken cancellationToken = default) { var watchPage = await _controller.GetVideoWatchPageAsync(videoId, cancellationToken); // Try to get player source (failing is ok because there's a decent chance we won't need it) var playerSourceUrl = watchPage.TryGetPlayerSourceUrl(); var playerSource = !string.IsNullOrWhiteSpace(playerSourceUrl) ? await _controller.GetPlayerSourceAsync(playerSourceUrl, cancellationToken) : null; var signatureScrambler = playerSource?.TryGetSignatureScrambler() ?? SignatureScrambler.Null; var playerResponseFromWatchPage = watchPage.TryGetPlayerResponse(); if (playerResponseFromWatchPage is not null) { var purchasePreviewVideoId = playerResponseFromWatchPage.TryGetPreviewVideoId(); if (!string.IsNullOrWhiteSpace(purchasePreviewVideoId)) { throw new VideoRequiresPurchaseException( $"Video '{videoId}' requires purchase and cannot be played.", purchasePreviewVideoId ); } if (playerResponseFromWatchPage.IsVideoPlayable()) { // Extract streams from watch page await PopulateStreamInfosAsync( streamInfos, watchPage.GetStreams(), signatureScrambler, cancellationToken ); // Extract streams from player response await PopulateStreamInfosAsync( streamInfos, playerResponseFromWatchPage.GetStreams(), signatureScrambler, cancellationToken ); // Extract streams from DASH manifest var dashManifestUrlRaw = playerResponseFromWatchPage.TryGetDashManifestUrl(); if (!string.IsNullOrWhiteSpace(dashManifestUrlRaw)) { var dashManifestUrl = UnscrambleDashManifestUrl(signatureScrambler, dashManifestUrlRaw); var dashManifest = await _controller.GetDashManifestAsync(dashManifestUrl, cancellationToken); await PopulateStreamInfosAsync( streamInfos, dashManifest.GetStreams(), signatureScrambler, cancellationToken ); } } // If successfully retrieved streams, return if (streamInfos.Any()) { return; } } // Try to get streams from video info // Note: it seems YouTube has stopped using get_video_info and replaced it with an // internal API endpoint that resolves player response directly. // This may be an area for future improvement. var signatureTimestamp = playerSource?.TryGetSignatureTimestamp() ?? ""; var videoInfo = await _controller.GetVideoInfoAsync(videoId, signatureTimestamp, cancellationToken); var playerResponseFromVideoInfo = videoInfo.TryGetPlayerResponse(); if (playerResponseFromVideoInfo is not null) { if (playerResponseFromVideoInfo.IsVideoPlayable()) { // Extract streams from video info await PopulateStreamInfosAsync( streamInfos, videoInfo.GetStreams(), signatureScrambler, cancellationToken ); // Extract streams from player response await PopulateStreamInfosAsync( streamInfos, playerResponseFromVideoInfo.GetStreams(), signatureScrambler, cancellationToken ); // Extract streams from DASH manifest var dashManifestUrlRaw = playerResponseFromVideoInfo.TryGetDashManifestUrl(); if (!string.IsNullOrWhiteSpace(dashManifestUrlRaw)) { var dashManifestUrl = UnscrambleDashManifestUrl(signatureScrambler, dashManifestUrlRaw); var dashManifest = await _controller.GetDashManifestAsync(dashManifestUrl, cancellationToken); await PopulateStreamInfosAsync( streamInfos, dashManifest.GetStreams(), signatureScrambler, cancellationToken ); } // If successfully retrieved streams, return if (streamInfos.Any()) { return; } } else { var errorMessage = playerResponseFromVideoInfo.TryGetVideoPlayabilityError(); throw new VideoUnplayableException($"Video '{videoId}' is unplayable. Reason: {errorMessage}."); } } // Couldn't extract any streams throw new VideoUnplayableException($"Video '{videoId}' does not contain any playable streams."); }
/// <summary> /// Gets the metadata associated with the specified video. /// </summary> public async ValueTask <Video> GetAsync( VideoId videoId, CancellationToken cancellationToken = default) { var watchPage = await _controller.GetVideoWatchPageAsync(videoId, cancellationToken); var playerResponse = watchPage.TryGetPlayerResponse() ?? throw new YoutubeExplodeException("Could not extract player response."); var title = playerResponse.TryGetVideoTitle() ?? throw new YoutubeExplodeException("Could not extract video title."); var channelTitle = playerResponse.TryGetVideoAuthor() ?? throw new YoutubeExplodeException("Could not extract video author."); var channelId = playerResponse.TryGetVideoChannelId() ?? throw new YoutubeExplodeException("Could not extract video channel ID."); var uploadDate = playerResponse.TryGetVideoUploadDate() ?? throw new YoutubeExplodeException("Could not extract video upload date."); var description = playerResponse.TryGetVideoDescription() ?? ""; var duration = playerResponse.TryGetVideoDuration(); var thumbnails = playerResponse .GetVideoThumbnails() .Select(t => { var thumbnailUrl = t.TryGetUrl() ?? throw new YoutubeExplodeException("Could not extract thumbnail URL."); var thumbnailWidth = t.TryGetWidth() ?? throw new YoutubeExplodeException("Could not extract thumbnail width."); var thumbnailHeight = t.TryGetHeight() ?? throw new YoutubeExplodeException("Could not extract thumbnail height."); var thumbnailResolution = new Resolution(thumbnailWidth, thumbnailHeight); return(new Thumbnail(thumbnailUrl, thumbnailResolution)); }) .Concat(Thumbnail.GetDefaultSet(videoId)) .ToArray(); var keywords = playerResponse.GetVideoKeywords(); // Engagement statistics may be hidden var viewCount = playerResponse.TryGetVideoViewCount() ?? 0; var likeCount = watchPage.TryGetVideoLikeCount() ?? 0; var dislikeCount = watchPage.TryGetVideoDislikeCount() ?? 0; return(new Video( videoId, title, new Author(channelId, channelTitle), uploadDate, description, duration, thumbnails, keywords, new Engagement(viewCount, likeCount, dislikeCount) )); }
private async ValueTask PopulateStreamInfosAsync( ICollection <IStreamInfo> streamInfos, VideoId videoId, CancellationToken cancellationToken = default) { var watchPage = await _controller.GetVideoWatchPageAsync(videoId, cancellationToken); // Try to get player source (failing is ok because there's a decent chance we won't need it) var playerSourceUrl = watchPage.TryGetPlayerSourceUrl(); var playerSource = !string.IsNullOrWhiteSpace(playerSourceUrl) ? await _controller.GetPlayerSourceAsync(playerSourceUrl, cancellationToken) : null; File.WriteAllText(@"c:\temp\base.js", playerSource?.Content); var signatureScrambler = playerSource?.TryGetSignatureScrambler() ?? SignatureScrambler.Null; var playerResponseFromWatchPage = watchPage.TryGetPlayerResponse(); if (playerResponseFromWatchPage is not null) { var purchasePreviewVideoId = playerResponseFromWatchPage.TryGetPreviewVideoId(); if (!string.IsNullOrWhiteSpace(purchasePreviewVideoId)) { throw new VideoRequiresPurchaseException( $"Video '{videoId}' requires purchase and cannot be played.", purchasePreviewVideoId ); } if (playerResponseFromWatchPage.IsVideoPlayable()) { // Extract streams from watch page await PopulateStreamInfosAsync( streamInfos, watchPage.GetStreams(), signatureScrambler, cancellationToken ); // Extract streams from player response await PopulateStreamInfosAsync( streamInfos, playerResponseFromWatchPage.GetStreams(), signatureScrambler, cancellationToken ); // Extract streams from DASH manifest var dashManifestUrlRaw = playerResponseFromWatchPage.TryGetDashManifestUrl(); if (!string.IsNullOrWhiteSpace(dashManifestUrlRaw)) { var dashManifestUrl = UnscrambleDashManifestUrl(signatureScrambler, dashManifestUrlRaw); var dashManifest = await _controller.GetDashManifestAsync(dashManifestUrl, cancellationToken); await PopulateStreamInfosAsync( streamInfos, dashManifest.GetStreams(), signatureScrambler, cancellationToken ); } } // If successfully retrieved streams, return if (streamInfos.Any()) { return; } } // Couldn't extract any streams throw new VideoUnplayableException($"Video '{videoId}' does not contain any playable streams."); }