/// <summary> /// Gets video by ID /// </summary> public async Task <Video> GetVideoAsync(string videoId) { videoId.GuardNotNull(nameof(videoId)); if (!ValidateVideoId(videoId)) { throw new ArgumentException("Invalid Youtube video ID", nameof(videoId)); } // Get player context var context = await GetPlayerContextAsync(videoId).ConfigureAwait(false); // Get video info var request = $"{YoutubeHost}/get_video_info?video_id={videoId}&sts={context.Sts}&el=info&ps=default&hl=en"; var response = await _httpService.GetStringAsync(request).ConfigureAwait(false); var videoDic = UrlHelper.GetDictionaryFromUrlQuery(response); // Check error code if (videoDic.ContainsKey("errorcode")) { var errorCode = videoDic.Get("errorcode").ParseInt(); var errorReason = videoDic.Get("reason"); throw new VideoNotAvailableException(videoId, errorCode, errorReason); } // Check if video requires purchase if (videoDic.GetOrDefault("requires_purchase") == "1") { var previewVideoId = videoDic.Get("ypc_vid"); throw new VideoRequiresPurchaseException(videoId, previewVideoId); } // Parse metadata var title = videoDic.Get("title"); var duration = TimeSpan.FromSeconds(videoDic.Get("length_seconds").ParseDouble()); var viewCount = videoDic.Get("view_count").ParseLong(); var keywords = videoDic.Get("keywords").Split(","); var isListed = videoDic.GetOrDefault("is_listed") == "1"; // unlisted videos don't have this var isRatingAllowed = videoDic.Get("allow_ratings") == "1"; var isMuted = videoDic.Get("muted") == "1"; var isEmbeddingAllowed = videoDic.Get("allow_embed") == "1"; // Prepare stream info collections var muxedStreamInfos = new List <MuxedStreamInfo>(); var audioStreamInfos = new List <AudioStreamInfo>(); var videoStreamInfos = new List <VideoStreamInfo>(); // Resolve muxed streams var muxedStreamInfosEncoded = videoDic.GetOrDefault("url_encoded_fmt_stream_map"); if (muxedStreamInfosEncoded.IsNotBlank()) { await ResolveMuxedStreamInfosAsync(context, muxedStreamInfosEncoded, muxedStreamInfos) .ConfigureAwait(false); } // Resolve adaptive streams var adaptiveStreamInfosEncoded = videoDic.GetOrDefault("adaptive_fmts"); if (adaptiveStreamInfosEncoded.IsNotBlank()) { await ResolveAdaptiveStreamInfosAsync(context, adaptiveStreamInfosEncoded, audioStreamInfos, videoStreamInfos) .ConfigureAwait(false); } // Resolve dash streams var dashManifestUrl = videoDic.GetOrDefault("dashmpd"); if (dashManifestUrl.IsNotBlank()) { await ResolveDashStreamInfosAsync(context, dashManifestUrl, audioStreamInfos, videoStreamInfos) .ConfigureAwait(false); } // Finalize stream info collections muxedStreamInfos = muxedStreamInfos.Distinct(s => s.Itag).OrderByDescending(s => s.VideoQuality).ToList(); audioStreamInfos = audioStreamInfos.Distinct(s => s.Itag).OrderByDescending(s => s.Bitrate).ToList(); videoStreamInfos = videoStreamInfos.Distinct(s => s.Itag).OrderByDescending(s => s.VideoQuality).ToList(); // Parse closed caption tracks var closedCaptionTrackInfos = new List <ClosedCaptionTrackInfo>(); var closedCaptionTrackInfosEncoded = videoDic.GetOrDefault("caption_tracks"); if (closedCaptionTrackInfosEncoded.IsNotBlank()) { ParseClosedCaptionTrackInfos(closedCaptionTrackInfosEncoded, closedCaptionTrackInfos); } // Get metadata extension request = $"{YoutubeHost}/get_video_metadata?video_id={videoId}"; response = await _httpService.GetStringAsync(request).ConfigureAwait(false); var videoXml = XElement.Parse(response).StripNamespaces().ElementStrict("html_content"); // Parse metadata extension var description = videoXml.ElementStrict("video_info").ElementStrict("description").Value; var likeCount = (long)videoXml.ElementStrict("video_info").ElementStrict("likes_count_unformatted"); var dislikeCount = (long)videoXml.ElementStrict("video_info").ElementStrict("dislikes_count_unformatted"); // Parse author info var authorId = videoXml.ElementStrict("user_info").ElementStrict("channel_external_id").Value; var authorName = videoXml.ElementStrict("user_info").ElementStrict("username").Value; var authorTitle = videoXml.ElementStrict("user_info").ElementStrict("channel_title").Value; var authorIsPaid = videoXml.ElementStrict("user_info").ElementStrict("channel_paid").Value == "1"; var authorLogoUrl = videoXml.ElementStrict("user_info").ElementStrict("channel_logo_url").Value; var authorBannerUrl = videoXml.ElementStrict("user_info").ElementStrict("channel_banner_url").Value; // Concat metadata var author = new Channel(authorId, authorName, authorTitle, authorIsPaid, authorLogoUrl, authorBannerUrl); var thumbnails = new VideoThumbnails(videoId); var status = new VideoStatus(isListed, isRatingAllowed, isMuted, isEmbeddingAllowed); var statistics = new Statistics(viewCount, likeCount, dislikeCount); return(new Video(videoId, author, title, description, thumbnails, duration, keywords, status, statistics, muxedStreamInfos, audioStreamInfos, videoStreamInfos, closedCaptionTrackInfos)); }
/// <summary> /// Gets playlist by ID, truncating resulting video list at given number of pages (1 page ≤ 200 videos) /// </summary> public async Task <Playlist> GetPlaylistAsync(string playlistId, int maxPages) { playlistId.GuardNotNull(nameof(playlistId)); maxPages.GuardPositive(nameof(maxPages)); if (!ValidatePlaylistId(playlistId)) { throw new ArgumentException("Invalid Youtube playlist ID", nameof(playlistId)); } // Get all videos across pages var pagesDone = 0; var offset = 0; XElement playlistXml; var videoIds = new HashSet <string>(); var videos = new List <PlaylistVideo>(); do { // Get manifest var request = $"{YoutubeHost}/list_ajax?style=xml&action_get_list=1&list={playlistId}&index={offset}"; var response = await _httpService.GetStringAsync(request).ConfigureAwait(false); playlistXml = XElement.Parse(response).StripNamespaces(); // Parse videos var total = 0; var delta = 0; foreach (var videoXml in playlistXml.Elements("video")) { // Basic info var videoId = videoXml.ElementStrict("encrypted_id").Value; var videoTitle = videoXml.ElementStrict("title").Value; var videoThumbnails = new VideoThumbnails(videoId); var videoDuration = TimeSpan.FromSeconds((double)videoXml.ElementStrict("length_seconds")); var videoDescription = videoXml.ElementStrict("description").Value; // Keywords var videoKeywordsJoined = videoXml.ElementStrict("keywords").Value; var videoKeywords = Regex .Matches(videoKeywordsJoined, @"(?<=(^|\s)(?<q>""?))([^""]|(""""))*?(?=\<q>(?=\s|$))") .Cast <Match>() .Select(m => m.Value) .Where(s => s.IsNotBlank()) .ToArray(); // Statistics // The inner text is already formatted so we have to parse it manually var videoViewCount = Regex.Replace(videoXml.ElementStrict("views").Value, @"\D", "").ParseLong(); var videoLikeCount = Regex.Replace(videoXml.ElementStrict("likes").Value, @"\D", "").ParseLong(); var videoDislikeCount = Regex.Replace(videoXml.ElementStrict("dislikes").Value, @"\D", "").ParseLong(); var videoStatistics = new Statistics(videoViewCount, videoLikeCount, videoDislikeCount); // Video var video = new PlaylistVideo(videoId, videoTitle, videoDescription, videoThumbnails, videoDuration, videoKeywords, videoStatistics); // Add to list if not already there if (videoIds.Add(video.Id)) { videos.Add(video); delta++; } total++; } // Break if the videos started repeating if (delta <= 0) { break; } // Prepare for next page pagesDone++; offset += total; } while (pagesDone < maxPages); // Basic info var title = playlistXml.ElementStrict("title").Value; var author = playlistXml.Element("author")?.Value ?? ""; // system playlists don't have an author var description = playlistXml.ElementStrict("description").Value; // Statistics var viewCount = (long?)playlistXml.Element("views") ?? 0; // watchlater does not have views var likeCount = (long?)playlistXml.Element("likes") ?? 0; // system playlists don't have likes var dislikeCount = (long?)playlistXml.Element("dislikes") ?? 0; // system playlists don't have dislikes var statistics = new Statistics(viewCount, likeCount, dislikeCount); return(new Playlist(playlistId, title, author, description, statistics, videos)); }