Last active
August 29, 2015 14:23
-
-
Save RickDB/d2ed54937a5f821e709a to your computer and use it in GitHub Desktop.
OV - Youtube fixes #002
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using Google.Apis.Auth.OAuth2; | |
using Google.Apis.Json; | |
using Google.Apis.Services; | |
using Google.Apis.Util.Store; | |
using Google.Apis.YouTube.v3; | |
using System; | |
using System.Collections.Generic; | |
using System.ComponentModel; | |
using System.Globalization; | |
using System.Linq; | |
using System.Threading; | |
using System.Threading.Tasks; | |
namespace OnlineVideos.Sites | |
{ | |
public class YouTubeV3Util : SiteUtilBase, IFilter | |
{ | |
#region Helper classes | |
private class YouTubeUserdataStore : IDataStore | |
{ | |
const string PREFIX = "YouTubeV3apiStore."; | |
public Task StoreAsync<T>(string key, T value) | |
{ | |
var serialized = NewtonsoftJsonSerializer.Instance.Serialize(value); | |
OnlineVideoSettings.Instance.UserStore.SetValue(PREFIX + key, serialized, true); | |
return TaskEx.Delay(0); | |
} | |
public Task DeleteAsync<T>(string key) | |
{ | |
OnlineVideoSettings.Instance.UserStore.SetValue(PREFIX + key, null); | |
return TaskEx.Delay(0); | |
} | |
public Task<T> GetAsync<T>(string key) | |
{ | |
TaskCompletionSource<T> tcs = new TaskCompletionSource<T>(); | |
var serialized = OnlineVideoSettings.Instance.UserStore.GetValue(PREFIX + key, true); | |
if (!string.IsNullOrWhiteSpace(serialized)) | |
{ | |
try | |
{ | |
tcs.SetResult(NewtonsoftJsonSerializer.Instance.Deserialize<T>(serialized)); | |
} | |
catch (Exception ex) | |
{ | |
tcs.SetException(ex); | |
} | |
} | |
else | |
{ | |
tcs.SetResult(default(T)); | |
} | |
return tcs.Task; | |
} | |
public Task ClearAsync() | |
{ | |
return TaskEx.Delay(0); | |
} | |
} | |
private class YouTubeVideo : VideoInfo | |
{ | |
internal string ChannelId { get; set; } | |
internal string ChannelTitle { get; set; } | |
internal string PlaylistItemId { get; set; } | |
} | |
private class YouTubeCategory : RssLink | |
{ | |
internal enum CategoryKind { Other, Channel, Playlist, GuideCategory, VideoCategory }; | |
internal CategoryKind Kind { get; set; } | |
internal string Id { get; set; } | |
internal bool IsMine { get; set; } | |
} | |
#endregion | |
public enum VideoQuality { Low, Medium, High, HD, FullHD }; | |
public enum VideoFormat { flv, mp4, webm }; | |
const string CLIENT = @"eyJpbnN0YWxsZWQiOnsiYXV0aF91cmkiOiJodHRwczovL2FjY291bnRzLmdvb2dsZS5jb20vby9vYXV0aDIvYXV0aCIsImNsaWVudF9zZWNyZXQiOiJ4cG52b05vNFB6N3lJUXdiVmdIQUdBcl8iLCJ0b2tlbl91cmkiOiJodHRwczovL2FjY291bnRzLmdvb2dsZS5jb20vby9vYXV0aDIvdG9rZW4iLCJjbGllbnRfZW1haWwiOiIiLCJyZWRpcmVjdF91cmlzIjpbInVybjppZXRmOndnOm9hdXRoOjIuMDpvb2IiLCJvb2IiXSwiY2xpZW50X3g1MDlfY2VydF91cmwiOiIiLCJjbGllbnRfaWQiOiI5MjUzNzY1MjgyODAtMm9xdWkydnEwbHE2YjVtZjRzNTNodWNqNnRrb2JxazcuYXBwcy5nb29nbGV1c2VyY29udGVudC5jb20iLCJhdXRoX3Byb3ZpZGVyX3g1MDlfY2VydF91cmwiOiJodHRwczovL3d3dy5nb29nbGVhcGlzLmNvbS9vYXV0aDIvdjEvY2VydHMifX0="; | |
[Category("OnlineVideosConfiguration"), Description("Add some dynamic categories found at startup to the list of configured ones.")] | |
bool useDynamicCategories = true; | |
[Category("OnlineVideosUserConfiguration"), LocalizableDisplayName("Preferred Format"), Description("Prefer this format when there are more than one for the desired quality.")] | |
VideoFormat preferredFormat = VideoFormat.mp4; | |
[Category("OnlineVideosUserConfiguration"), LocalizableDisplayName("Undesired Format"), Description("Try to avoid this format when there are more than one for the desired quality.")] | |
VideoFormat undesiredFormat = VideoFormat.webm; | |
[Category("OnlineVideosUserConfiguration"), LocalizableDisplayName("Video Quality", TranslationFieldName = "VideoQuality"), Description("Defines the maximum quality for the video to be played.")] | |
VideoQuality videoQuality = VideoQuality.High; | |
[Category("OnlineVideosUserConfiguration"), LocalizableDisplayName("Videos per Page"), Description("Defines the default number of videos to display per page.")] | |
int pageSize = 26; | |
[Category("OnlineVideosUserConfiguration"), LocalizableDisplayName("Enable Login"), Description("Will popup a browser on first use to select your YouTube account.")] | |
bool enableLogin = false; | |
string hl = CultureInfo.CurrentUICulture.TwoLetterISOLanguageName; | |
string regionCode = RegionInfo.CurrentRegion.TwoLetterISORegionName; | |
YouTubeService service; | |
Func<List<VideoInfo>> nextPageVideosQuery; | |
SearchResource.ListRequest.OrderEnum currentSearchOrder = SearchResource.ListRequest.OrderEnum.Relevance; | |
string currentVideosTitle; | |
string userFavoritesPlaylistId; | |
public override int DiscoverDynamicCategories() | |
{ | |
if (useDynamicCategories) | |
{ | |
Settings.Categories = new BindingList<Category>(); | |
var guideCatgeory = new Category() { Name = "YouTube Guide", HasSubCategories = true }; | |
guideCatgeory.Other = (Func<List<Category>>)(() => QueryGuideCategories(guideCatgeory)); | |
Settings.Categories.Add(guideCatgeory); | |
var videoCategory = new Category() { Name = "Video Categories", HasSubCategories = true }; | |
videoCategory.Other = (Func<List<Category>>)(() => QueryVideoCategories(videoCategory)); | |
Settings.Categories.Add(videoCategory); | |
if (enableLogin) | |
{ | |
try | |
{ | |
QueryUserChannel().ForEach(c => Settings.Categories.Add(c)); | |
} | |
catch (Exception ex) | |
{ | |
throw new OnlineVideosException(ex.Message); | |
} | |
} | |
Settings.DynamicCategoriesDiscovered = true; | |
} | |
return Settings.Categories.Count; | |
} | |
public override int DiscoverSubCategories(Category parentCategory) | |
{ | |
var method = parentCategory.Other as Func<List<Category>>; | |
if (method != null) | |
{ | |
parentCategory.SubCategories = method.Invoke(); | |
parentCategory.SubCategoriesDiscovered = true; | |
return parentCategory.SubCategories.Count; | |
} | |
return 0; | |
} | |
public override int DiscoverNextPageCategories(NextPageCategory category) | |
{ | |
var method = category.Other as Func<List<Category>>; | |
if (method != null) | |
{ | |
var newCategories = method.Invoke(); | |
category.ParentCategory.SubCategories.Remove(category); | |
category.ParentCategory.SubCategories.AddRange(newCategories); | |
return newCategories.Count; | |
} | |
return 0; | |
} | |
public override string GetCurrentVideosTitle() | |
{ | |
return currentVideosTitle; | |
} | |
public override List<VideoInfo> GetVideos(Category category) | |
{ | |
currentVideosTitle = null; // use default title for videos retrieved via this method (which is the Category Name) | |
base.HasNextPage = false; | |
nextPageVideosQuery = null; | |
var method = category.Other as Func<List<VideoInfo>>; | |
if (method != null) | |
{ | |
return method.Invoke(); | |
} | |
return new List<VideoInfo>(); | |
} | |
public override List<String> GetMultipleVideoUrls(VideoInfo video, bool inPlaylist = false) | |
{ | |
video.PlaybackOptions = Hoster.HosterFactory.GetHoster("Youtube").GetPlaybackOptions(video.VideoUrl); | |
if (video.PlaybackOptions != null && video.PlaybackOptions.Count > 0) | |
{ | |
if (video.PlaybackOptions.Count == 1) | |
{ | |
// nothing to chose from, only one option available | |
return new List<string>() { video.PlaybackOptions.First().Value }; | |
} | |
else | |
{ | |
KeyValuePair<string, string> foundQuality = default(KeyValuePair<string, string>); | |
switch (videoQuality) | |
{ | |
case VideoQuality.Low: //use first available option | |
foundQuality = video.PlaybackOptions.First(); break; | |
case VideoQuality.Medium: //first above 320 that is not 3D | |
foundQuality = video.PlaybackOptions.FirstOrDefault(q => !q.Key.Contains("320") && !q.Key.Contains("3D")); break; | |
case VideoQuality.High: //highest below the HD formats that is not 3D | |
foundQuality = video.PlaybackOptions.LastOrDefault(q => !q.Key.Contains("1920") && !q.Key.Contains("1280") && !q.Key.Contains("3D")); break; | |
case VideoQuality.HD: //first below full HD that is not 3D | |
foundQuality = video.PlaybackOptions.LastOrDefault(q => !q.Key.Contains("1920") && !q.Key.Contains("3D")); break; | |
case VideoQuality.FullHD: //use highest available quality that is not 3D | |
foundQuality = video.PlaybackOptions.Last(q => !q.Key.Contains("3D")); break; | |
} | |
if (!string.IsNullOrEmpty(foundQuality.Key)) | |
{ | |
string resolution = foundQuality.Key.Substring(0, foundQuality.Key.IndexOf('|')); | |
// try to find one that has the same resolution and the preferred format | |
var bestMatch = video.PlaybackOptions.FirstOrDefault(q => q.Key.Contains(resolution) && !q.Key.Contains("3D") && q.Key.Contains(preferredFormat.ToString())); | |
// try to find one that has the same resolution and not the undesired format | |
if (string.IsNullOrEmpty(bestMatch.Key)) bestMatch = video.PlaybackOptions.FirstOrDefault(q => q.Key.Contains(resolution) && !q.Key.Contains("3D") && !q.Key.Contains(undesiredFormat.ToString())); | |
if (!string.IsNullOrEmpty(bestMatch.Key)) foundQuality = bestMatch; | |
} | |
// fallback when no match was found -> use first choice | |
if (string.IsNullOrEmpty(foundQuality.Key)) foundQuality = video.PlaybackOptions.First(); | |
if (inPlaylist) video.PlaybackOptions = null; | |
return new List<string>() { foundQuality.Value }; | |
} | |
} | |
return null; // no playback options | |
} | |
#region Search | |
public override bool CanSearch { get { return true; } } | |
Dictionary<string, string> cachedSearchCategories = null; | |
public override Dictionary<string, string> GetSearchableCategories() | |
{ | |
if (cachedSearchCategories == null) | |
QueryVideoCategories(null); | |
return cachedSearchCategories; | |
} | |
public override List<SearchResultItem> Search(string query, string category = null) | |
{ | |
base.HasNextPage = false; | |
nextPageVideosQuery = null; | |
return QuerySearchVideos(query, "videos", null, category, null).ConvertAll(v => (SearchResultItem)v); | |
} | |
#endregion | |
#region Paging | |
public override List<VideoInfo> GetNextPageVideos() | |
{ | |
var method = nextPageVideosQuery; | |
base.HasNextPage = false; | |
nextPageVideosQuery = null; | |
if (method != null) | |
{ | |
return method.Invoke(); | |
} | |
return new List<VideoInfo>(); | |
} | |
#endregion | |
#region IFilter Members | |
public List<VideoInfo> FilterVideos(Category category, int maxResults, string orderBy, string timeFrame) | |
{ | |
return (category.Other as Func<List<VideoInfo>>).Invoke(); | |
} | |
public List<VideoInfo> FilterSearchResults(string query, int maxResults, string orderBy, string timeFrame) | |
{ | |
return FilterSearchResults(query, null, maxResults, orderBy, timeFrame); | |
} | |
public List<VideoInfo> FilterSearchResults(string query, string category, int maxResults, string orderBy, string timeFrame) | |
{ | |
Enum.TryParse<SearchResource.ListRequest.OrderEnum>(orderBy, out currentSearchOrder); | |
return QuerySearchVideos(query, "videos", null, category, null); | |
} | |
public List<int> GetResultSteps() { return new List<int>() { 10, 20, 30, 40, 50 }; } | |
public Dictionary<string, string> GetOrderByOptions() { return Enum.GetNames(typeof(SearchResource.ListRequest.OrderEnum)).ToDictionary(o => o); } | |
public Dictionary<string, string> GetTimeFrameOptions() { return new Dictionary<string, string>(); } | |
#endregion | |
#region Context Menu | |
public override List<ContextMenuEntry> GetContextMenuEntries(Category selectedCategory, VideoInfo selectedItem) | |
{ | |
List<ContextMenuEntry> result = new List<ContextMenuEntry>(); | |
var ytVideo = selectedItem as YouTubeVideo; | |
var ytCategory = selectedCategory as YouTubeCategory; | |
if (selectedItem == null && ytCategory != null) | |
{ | |
if (ytCategory.Kind == YouTubeCategory.CategoryKind.Playlist && ytCategory.IsMine && !string.IsNullOrEmpty(ytCategory.Id)) | |
{ | |
result.Add(new ContextMenuEntry() { DisplayText = Translation.Instance.DeletePlaylist, Action = ContextMenuEntry.UIAction.Execute }); | |
} | |
} | |
if (ytVideo != null) | |
{ | |
result.Add(new ContextMenuEntry() { DisplayText = Translation.Instance.RelatedVideos, Action = ContextMenuEntry.UIAction.Execute }); | |
if (!string.IsNullOrEmpty(ytVideo.ChannelTitle) && !string.IsNullOrEmpty(ytVideo.ChannelId)) | |
{ | |
result.Add(new ContextMenuEntry() { DisplayText = Translation.Instance.UploadsBy + " [" + ytVideo.ChannelTitle + "]", Action = ContextMenuEntry.UIAction.Execute }); | |
result.Add(new ContextMenuEntry() { DisplayText = Translation.Instance.Playlists + " [" + ytVideo.ChannelTitle + "]", Action = ContextMenuEntry.UIAction.Execute }); | |
} | |
if (!string.IsNullOrEmpty(userFavoritesPlaylistId)) | |
{ | |
if (ytCategory != null && ytCategory.Kind == YouTubeCategory.CategoryKind.Playlist && ytCategory.Id == userFavoritesPlaylistId && !string.IsNullOrEmpty(ytVideo.PlaylistItemId)) | |
{ | |
result.Add(new ContextMenuEntry() { DisplayText = Translation.Instance.RemoveFromFavorites + " (" + Settings.Name + ")", Action = ContextMenuEntry.UIAction.Execute }); | |
} | |
else | |
{ | |
result.Add(new ContextMenuEntry() { DisplayText = Translation.Instance.AddToFavourites + " (" + Settings.Name + ")", Action = ContextMenuEntry.UIAction.Execute }); | |
} | |
} | |
if (ytCategory != null && ytCategory.Kind == YouTubeCategory.CategoryKind.Playlist && ytCategory.IsMine && !string.IsNullOrEmpty(ytCategory.Id) && !string.IsNullOrEmpty(ytVideo.PlaylistItemId)) | |
{ | |
result.Add(new ContextMenuEntry() { DisplayText = Translation.Instance.RemoveFromPlaylist, Action = ContextMenuEntry.UIAction.Execute }); | |
} | |
if (enableLogin) | |
{ | |
var plCtx = new ContextMenuEntry() { DisplayText = Translation.Instance.AddToPlaylist, Action = ContextMenuEntry.UIAction.ShowList }; | |
plCtx.SubEntries.Add(new ContextMenuEntry() { DisplayText = Translation.Instance.CreateNewPlaylist, Action = ContextMenuEntry.UIAction.GetText }); | |
foreach (var pl in QueryChannelPlaylists(new YouTubeCategory() { IsMine = true }, null)) | |
{ | |
if (pl is YouTubeCategory) | |
plCtx.SubEntries.Add(new ContextMenuEntry() { DisplayText = pl.Name, Other = (pl as YouTubeCategory).Id }); | |
} | |
result.Add(plCtx); | |
} | |
} | |
return result; | |
} | |
public override ContextMenuExecutionResult ExecuteContextMenuEntry(Category selectedCategory, VideoInfo selectedItem, ContextMenuEntry choice) | |
{ | |
ContextMenuExecutionResult result = new ContextMenuExecutionResult(); | |
try | |
{ | |
if (choice.DisplayText == Translation.Instance.AddToFavourites + " (" + Settings.Name + ")") | |
{ | |
var query = Service.PlaylistItems.Insert( | |
new Google.Apis.YouTube.v3.Data.PlaylistItem() | |
{ | |
Snippet = new Google.Apis.YouTube.v3.Data.PlaylistItemSnippet() | |
{ | |
Title = selectedItem.Title, | |
PlaylistId = userFavoritesPlaylistId, | |
ResourceId = new Google.Apis.YouTube.v3.Data.ResourceId() | |
{ | |
VideoId = selectedItem.VideoUrl, | |
Kind = "youtube#video" | |
} | |
} | |
}, | |
"snippet"); | |
var response = query.Execute(); | |
result.ExecutionResultMessage = string.Format("{0} {1}", Translation.Instance.Success, Translation.Instance.AddingToFavorites); | |
} | |
else if (choice.DisplayText == Translation.Instance.RemoveFromFavorites + " (" + Settings.Name + ")") | |
{ | |
var query = Service.PlaylistItems.Delete((selectedItem as YouTubeVideo).PlaylistItemId); | |
var response = query.Execute(); | |
result.RefreshCurrentItems = true; | |
} | |
else if (choice.DisplayText == Translation.Instance.RelatedVideos) | |
{ | |
base.HasNextPage = false; | |
nextPageVideosQuery = null; | |
currentVideosTitle = Translation.Instance.RelatedVideos + " [" + selectedItem.Title + "]"; | |
result.ResultItems = QuerySearchVideos(null, "videos", null, null, (selectedItem as YouTubeVideo).VideoUrl).ConvertAll<SearchResultItem>(v => v as SearchResultItem); | |
} | |
else if (choice.DisplayText.StartsWith(Translation.Instance.UploadsBy)) | |
{ | |
base.HasNextPage = false; | |
nextPageVideosQuery = null; | |
currentVideosTitle = Translation.Instance.UploadsBy + " [" + (selectedItem as YouTubeVideo).ChannelTitle + "]"; | |
result.ResultItems = QuerySearchVideos(null, "videos", (selectedItem as YouTubeVideo).ChannelId, null, null).ConvertAll<SearchResultItem>(v => v as SearchResultItem); | |
} | |
else if (choice.DisplayText.StartsWith(Translation.Instance.Playlists)) | |
{ | |
var parentCategory = new YouTubeCategory() { Name = Translation.Instance.Playlists + " [" + (selectedItem as YouTubeVideo).ChannelTitle + "]" }; | |
parentCategory.SubCategories = QueryChannelPlaylists(parentCategory, (selectedItem as YouTubeVideo).ChannelId); | |
result.ResultItems = parentCategory.SubCategories.ConvertAll<SearchResultItem>(v => v as SearchResultItem); | |
} | |
else if (choice.DisplayText == Translation.Instance.RemoveFromPlaylist) | |
{ | |
var query = Service.PlaylistItems.Delete((selectedItem as YouTubeVideo).PlaylistItemId); | |
var response = query.Execute(); | |
result.RefreshCurrentItems = true; | |
if ((selectedCategory as YouTubeCategory).EstimatedVideoCount != null) (selectedCategory as YouTubeCategory).EstimatedVideoCount--; | |
} | |
else if (choice.DisplayText == Translation.Instance.DeletePlaylist) | |
{ | |
var query = Service.Playlists.Delete((selectedCategory as YouTubeCategory).Id); | |
var response = query.Execute(); | |
selectedCategory.ParentCategory.SubCategoriesDiscovered = false; | |
result.RefreshCurrentItems = true; | |
} | |
else if (choice.ParentEntry != null && choice.ParentEntry.DisplayText == Translation.Instance.AddToPlaylist) | |
{ | |
if (choice.Other == null) | |
{ | |
// create new playlist first | |
var query = Service.Playlists.Insert( | |
new Google.Apis.YouTube.v3.Data.Playlist() | |
{ | |
Snippet = new Google.Apis.YouTube.v3.Data.PlaylistSnippet() { Title = choice.UserInputText } | |
}, | |
"snippet"); | |
var response = query.Execute(); | |
choice.Other = response.Id; | |
} | |
var queryItem = Service.PlaylistItems.Insert( | |
new Google.Apis.YouTube.v3.Data.PlaylistItem() | |
{ | |
Snippet = new Google.Apis.YouTube.v3.Data.PlaylistItemSnippet() | |
{ | |
Title = selectedItem.Title, | |
PlaylistId = choice.Other as string, | |
ResourceId = new Google.Apis.YouTube.v3.Data.ResourceId() | |
{ | |
VideoId = selectedItem.VideoUrl, | |
Kind = "youtube#video" | |
} | |
} | |
}, | |
"snippet"); | |
var responseItem = queryItem.Execute(); | |
// force re-discovery of dynamic subcategories for my playlists category (as either a new catgeory was added or the count changed) | |
var playlistsCategory = Settings.Categories.FirstOrDefault(c => (c is YouTubeCategory) && (c as YouTubeCategory).IsMine && c.Name.EndsWith(Translation.Instance.Playlists)); | |
if (playlistsCategory != null) playlistsCategory.SubCategoriesDiscovered = false; | |
} | |
} | |
catch (Google.GoogleApiException apiEx) | |
{ | |
throw new OnlineVideosException(string.Format("{0} {1}", apiEx.HttpStatusCode, apiEx.Message)); | |
} | |
catch (Exception ex) | |
{ | |
throw new OnlineVideosException(ex.Message); | |
} | |
return result; | |
} | |
#endregion | |
#region YouTube service wrapper methods | |
/// <summary> | |
/// Gets a (cached) instance of the <see cref="YouTubeService"/> used to query the API. | |
/// When authorization is enabled, upon first creation a user token will be retrieved using a browser popup. | |
/// </summary> | |
YouTubeService Service | |
{ | |
get | |
{ | |
if (service == null) | |
{ | |
UserCredential credential = null; | |
if (enableLogin) | |
{ | |
using (var stream = new System.IO.MemoryStream(Convert.FromBase64String(CLIENT))) | |
{ | |
credential = GoogleWebAuthorizationBroker.AuthorizeAsync( | |
GoogleClientSecrets.Load(stream).Secrets, | |
new[] { YouTubeService.Scope.Youtube }, | |
"user", | |
CancellationToken.None, | |
new YouTubeUserdataStore() | |
).Result; | |
} | |
} | |
service = new YouTubeService(new BaseClientService.Initializer() | |
{ | |
ApiKey = "AIzaSyDzL_VrmG4Q2K4unBafZEoOv3UCAUTB7e4", | |
ApplicationName = "OnlineVideos", | |
HttpClientInitializer = credential, | |
}); | |
} | |
return service; | |
} | |
} | |
/// <summary>Returns a list of categories for the authenticated user (Watch Later, Watch History, Subscriptions, Playlists)</summary> | |
List<Category> QueryUserChannel() | |
{ | |
var query = Service.Channels.List("snippet, contentDetails"); | |
query.Mine = true; | |
var response = query.Execute(); | |
var userChannel = response.Items.FirstOrDefault(); | |
var results = new List<Category>(); | |
if (userChannel != null) | |
{ | |
var userName = userChannel.Snippet.Title; | |
if (!string.IsNullOrWhiteSpace(userName)) | |
{ | |
userFavoritesPlaylistId = userChannel.ContentDetails.RelatedPlaylists.Favorites; | |
results.Add(new Category() { Name = string.Format("{0}'s {1}", userName, "Watch Later"), Thumb = userChannel.Snippet.Thumbnails.High.Url, Other = (Func<List<VideoInfo>>)(() => QueryPlaylistVideos(userChannel.ContentDetails.RelatedPlaylists.WatchLater)) }); | |
results.Add(new Category() { Name = string.Format("{0}'s {1}", userName, "Watch History"), Thumb = userChannel.Snippet.Thumbnails.High.Url, Other = (Func<List<VideoInfo>>)(() => QueryPlaylistVideos(userChannel.ContentDetails.RelatedPlaylists.WatchHistory)) }); | |
var subscriptionsCategory = new Category() { Name = string.Format("{0}'s {1}", userName, Translation.Instance.Subscriptions), Thumb = userChannel.Snippet.Thumbnails.High.Url, HasSubCategories = true }; | |
subscriptionsCategory.Other = (Func<List<Category>>)(() => QueryMySubscriptions(subscriptionsCategory)); | |
results.Add(subscriptionsCategory); | |
var playlistsCategory = new YouTubeCategory() { Name = string.Format("{0}'s {1}", userName, Translation.Instance.Playlists), Thumb = userChannel.Snippet.Thumbnails.High.Url, HasSubCategories = true, IsMine = true }; | |
playlistsCategory.Other = (Func<List<Category>>)(() => QueryChannelPlaylists(playlistsCategory, null)); | |
results.Add(playlistsCategory); | |
} | |
} | |
return results; | |
} | |
/// <summary>Returns a list of categories that can be associated with YouTube channels.</summary> | |
/// <remarks> | |
/// A guide category identifies a category that YouTube algorithmically assigns based on a channel's content or other indicators, such as the channel's popularity. | |
/// The list is similar to video categories, with the difference being that a video's uploader can assign a video category but only YouTube can assign a channel category. | |
/// </remarks> | |
List<Category> QueryGuideCategories(Category parentCategory) | |
{ | |
var query = Service.GuideCategories.List("snippet"); | |
query.RegionCode = regionCode; | |
query.Hl = hl; | |
var response = query.Execute(); | |
var results = new List<Category>(); | |
foreach (var item in response.Items) | |
{ | |
var category = new YouTubeCategory() { Name = item.Snippet.Title, HasSubCategories = true, ParentCategory = parentCategory, Kind = YouTubeCategory.CategoryKind.GuideCategory, Id = item.Id }; | |
category.Other = (Func<List<Category>>)(() => QueryChannelsForGuideCategory(category, item.Id)); | |
results.Add(category); | |
} | |
return results; | |
} | |
/// <summary>Returns a list of categories that can be associated with YouTube videos.</summary> | |
List<Category> QueryVideoCategories(Category parentCategory) | |
{ | |
var query = Service.VideoCategories.List("snippet"); | |
query.RegionCode = regionCode; | |
query.Hl = hl; | |
var response = query.Execute(); | |
var results = new List<Category>(); | |
cachedSearchCategories = new Dictionary<string, string>(); | |
foreach (var item in response.Items) | |
{ | |
if (item.Snippet.Assignable == true) | |
{ | |
var category = new YouTubeCategory() { Name = item.Snippet.Title, ParentCategory = parentCategory, Kind = YouTubeCategory.CategoryKind.VideoCategory, Id = item.Id }; | |
category.Other = (Func<List<VideoInfo>>)(() => QueryCategoryVideos(item.Id)); | |
results.Add(category); | |
cachedSearchCategories.Add(item.Snippet.Title, item.Id); | |
} | |
} | |
return results; | |
} | |
/// <summary>Returns a list of channels for the given guide category.</summary> | |
/// <param name="guideCategoryId">The guide category to use as filter in the query.</param> | |
List<Category> QueryChannelsForGuideCategory(Category parentCategory, string guideCategoryId, string pageToken = null) | |
{ | |
var query = Service.Channels.List("snippet, statistics"); | |
query.CategoryId = guideCategoryId; | |
query.Hl = hl; | |
query.MaxResults = pageSize; | |
query.PageToken = pageToken; | |
var response = query.Execute(); | |
var results = new List<Category>(); | |
foreach (var item in response.Items) | |
{ | |
var category = new YouTubeCategory() | |
{ | |
Name = item.Snippet.Localized.Title, | |
Description = item.Snippet.Localized.Description, | |
Thumb = item.Snippet.Thumbnails != null ? item.Snippet.Thumbnails.High.Url : null, | |
EstimatedVideoCount = (uint)(item.Statistics.VideoCount ?? 0), | |
HasSubCategories = true, | |
ParentCategory = parentCategory, | |
Kind = YouTubeCategory.CategoryKind.Channel, | |
Id = item.Id | |
}; | |
category.Other = (Func<List<Category>>)(() => QueryChannelPlaylists(category, item.Id)); | |
results.Add(category); | |
} | |
if (!string.IsNullOrEmpty(response.NextPageToken)) | |
{ | |
results.Add(new NextPageCategory() { ParentCategory = parentCategory, Other = (Func<List<Category>>)(() => QueryChannelsForGuideCategory(parentCategory, guideCategoryId, response.NextPageToken)) }); | |
} | |
return results; | |
} | |
/// <summary>Returns a list of playlists for the given channel.</summary> | |
/// <param name="channelId">The channel to use as filter in the query.</param> | |
List<Category> QueryChannelPlaylists(YouTubeCategory parentCategory, string channelId, string pageToken = null) | |
{ | |
var query = Service.Playlists.List("snippet, contentDetails"); | |
if (string.IsNullOrEmpty(channelId)) | |
query.Mine = true; | |
else | |
query.ChannelId = channelId; | |
query.Hl = hl; | |
query.MaxResults = pageSize; | |
query.PageToken = pageToken; | |
var response = query.Execute(); | |
var results = new List<Category>(); | |
if (!string.IsNullOrEmpty(channelId) && pageToken == null && parentCategory.EstimatedVideoCount > 0) | |
{ | |
var resultsUploaded = new List<Category>(); | |
// before all playlists add a category that will list all uploads of the channel | |
results.Add(new YouTubeCategory() | |
{ | |
Name = string.Format("{0} {1}", Translation.Instance.UploadsBy, parentCategory.Name), | |
Thumb = parentCategory.Thumb, | |
EstimatedVideoCount = parentCategory.EstimatedVideoCount, | |
ParentCategory = parentCategory, | |
Kind = YouTubeCategory.CategoryKind.Channel, | |
Id = channelId, | |
Other = (Func<List<VideoInfo>>)(() => QuerySearchVideos(null, "videos", parentCategory.Id, null, null, true, null)) | |
}); | |
} | |
foreach (var item in response.Items) | |
{ | |
if ((long)(item.ContentDetails.ItemCount ?? 0) > 0 || parentCategory.IsMine) // hide empty playlists when not listing the authenticated user's | |
results.Add(new YouTubeCategory() | |
{ | |
Name = item.Snippet.Localized.Title, | |
Description = item.Snippet.Localized.Description, | |
Thumb = item.Snippet.Thumbnails != null ? item.Snippet.Thumbnails.High.Url : null, | |
EstimatedVideoCount = (uint)(item.ContentDetails.ItemCount ?? 0), | |
ParentCategory = parentCategory, | |
Kind = YouTubeCategory.CategoryKind.Playlist, | |
Id = item.Id, | |
IsMine = parentCategory.IsMine, | |
Other = (Func<List<VideoInfo>>)(() => QueryPlaylistVideos(item.Id)) | |
}); | |
} | |
if (!string.IsNullOrEmpty(response.NextPageToken)) | |
{ | |
results.Add(new NextPageCategory() { ParentCategory = parentCategory, Other = (Func<List<Category>>)(() => QueryChannelPlaylists(parentCategory, channelId, response.NextPageToken)) }); | |
} | |
return results; | |
} | |
/// <summary>Returns a list of the authenticated user's subscriptions (channels).</summary> | |
List<Category> QueryMySubscriptions(Category parentCategory, string pageToken = null) | |
{ | |
var query = Service.Subscriptions.List("snippet, contentDetails"); | |
query.Mine = true; | |
query.MaxResults = pageSize; | |
query.PageToken = pageToken; | |
var response = query.Execute(); | |
var results = new List<Category>(); | |
// before all channels add a category that will list all uploads | |
results.Add(new YouTubeCategory() | |
{ | |
Name = "Latest Videos", | |
Thumb = parentCategory.Thumb, | |
ParentCategory = parentCategory, | |
Kind = YouTubeCategory.CategoryKind.Other, | |
Other = (Func<List<VideoInfo>>)(() => QueryNewestSubscriptionVideos()) | |
}); | |
foreach (var item in response.Items) | |
{ | |
var category = new YouTubeCategory() | |
{ | |
Name = item.Snippet.Title, | |
Description = item.Snippet.Description, | |
Thumb = item.Snippet.Thumbnails != null ? item.Snippet.Thumbnails.High.Url : null, | |
EstimatedVideoCount = (uint)(item.ContentDetails.TotalItemCount ?? 0), | |
ParentCategory = parentCategory, | |
HasSubCategories = true, | |
Kind = YouTubeCategory.CategoryKind.Channel, | |
Id = item.Snippet.ResourceId.ChannelId, | |
IsMine = true | |
}; | |
category.Other = (Func<List<Category>>)(() => QueryChannelPlaylists(category, item.Snippet.ResourceId.ChannelId)); | |
results.Add(category); | |
} | |
if (!string.IsNullOrEmpty(response.NextPageToken)) | |
{ | |
results.Add(new NextPageCategory() { ParentCategory = parentCategory, Other = (Func<List<Category>>)(() => QueryMySubscriptions(parentCategory, response.NextPageToken)) }); | |
} | |
return results; | |
} | |
List<VideoInfo> QueryNewestSubscriptionVideos(string pageToken = null) | |
{ | |
var query = Service.Activities.List("snippet, contentDetails"); | |
query.Home = true; | |
query.RegionCode = regionCode; | |
query.MaxResults = pageSize; | |
query.PageToken = pageToken; | |
var response = query.Execute(); | |
var results = response.Items.Where(i => i.Snippet.Type == "upload").Select(i => new YouTubeVideo() | |
{ | |
Title = i.Snippet.Title, | |
Description = i.Snippet.Description, | |
Thumb = i.Snippet.Thumbnails != null ? i.Snippet.Thumbnails.High.Url : null, | |
Airdate = i.Snippet.PublishedAt != null ? i.Snippet.PublishedAt.Value.ToString("g", OnlineVideoSettings.Instance.Locale) : i.Snippet.PublishedAtRaw, | |
VideoUrl = i.ContentDetails.Upload.VideoId, | |
ChannelId = i.Snippet.ChannelId, | |
ChannelTitle = i.Snippet.ChannelTitle, | |
}).ToList<VideoInfo>(); | |
if (!string.IsNullOrEmpty(response.NextPageToken)) | |
{ | |
base.HasNextPage = true; | |
nextPageVideosQuery = () => QueryNewestSubscriptionVideos(response.NextPageToken); | |
} | |
return results; | |
} | |
/// <summary>Returns a list of most popular videos for the given category.</summary> | |
/// <param name="videoCategoryId">The category to use use as filter in the query.</param> | |
List<VideoInfo> QueryCategoryVideos(string videoCategoryId, string pageToken = null) | |
{ | |
var query = Service.Videos.List("snippet, contentDetails"); | |
query.Chart = VideosResource.ListRequest.ChartEnum.MostPopular; | |
query.VideoCategoryId = videoCategoryId; | |
query.RegionCode = regionCode; | |
query.Hl = hl; | |
query.MaxResults = pageSize; | |
query.PageToken = pageToken; | |
var response = query.Execute(); | |
var results = response.Items.Select(i => new YouTubeVideo() | |
{ | |
Title = i.Snippet.Localized.Title, | |
Description = i.Snippet.Localized.Description, | |
Thumb = i.Snippet.Thumbnails != null ? i.Snippet.Thumbnails.High.Url : null, | |
Airdate = i.Snippet.PublishedAt != null ? i.Snippet.PublishedAt.Value.ToString("g", OnlineVideoSettings.Instance.Locale) : i.Snippet.PublishedAtRaw, | |
Length = System.Xml.XmlConvert.ToTimeSpan(i.ContentDetails.Duration).ToString(), | |
VideoUrl = i.Id, | |
ChannelId = i.Snippet.ChannelId, | |
ChannelTitle = i.Snippet.ChannelTitle | |
}).ToList<VideoInfo>(); | |
if (!string.IsNullOrEmpty(response.NextPageToken)) | |
{ | |
base.HasNextPage = true; | |
nextPageVideosQuery = () => QueryCategoryVideos(videoCategoryId, response.NextPageToken); | |
} | |
return results; | |
} | |
/// <summary>Returns a list of videos for the given playlist.</summary> | |
/// <param name="playlistId">The playlist to use as a filter in the query.</param> | |
List<VideoInfo> QueryPlaylistVideos(string playlistId, string pageToken = null) | |
{ | |
var query = Service.PlaylistItems.List("snippet"); | |
query.PlaylistId = playlistId; | |
query.MaxResults = pageSize; | |
query.PageToken = pageToken; | |
var response = query.Execute(); | |
var results = response.Items.Where(i => i.Snippet.ResourceId.Kind == "youtube#video").Select(i => new YouTubeVideo() | |
{ | |
Title = i.Snippet.Title, | |
Description = i.Snippet.Description, | |
Thumb = i.Snippet.Thumbnails != null ? i.Snippet.Thumbnails.High.Url : null, | |
Airdate = i.Snippet.PublishedAt != null ? i.Snippet.PublishedAt.Value.ToString("g", OnlineVideoSettings.Instance.Locale) : i.Snippet.PublishedAtRaw, | |
VideoUrl = i.Snippet.ResourceId.VideoId, | |
ChannelId = i.Snippet.ChannelId, | |
ChannelTitle = i.Snippet.ChannelTitle, | |
PlaylistItemId = i.Id, | |
}).ToList<VideoInfo>(); | |
if (!string.IsNullOrEmpty(response.NextPageToken)) | |
{ | |
base.HasNextPage = true; | |
nextPageVideosQuery = () => QueryPlaylistVideos(playlistId, response.NextPageToken); | |
} | |
return results; | |
} | |
/// <summary>Returns a list of videos for the given search string.</summary> | |
/// <param name="queryString">The search string to use as as filter in the query.</param> | |
/// <param name="channelId">The channel id to use as filter in the query.</param> | |
List<VideoInfo> QuerySearchVideos(string queryString, string searchType, string channelId, string categoryId, string relatedToVideoId, bool sortbyDate = false, string pageToken = null) | |
{ | |
var query = Service.Search.List("snippet"); | |
if (!string.IsNullOrEmpty(channelId)) | |
query.ChannelId = channelId; | |
if (!string.IsNullOrEmpty(queryString)) | |
query.Q = queryString; | |
if (!string.IsNullOrEmpty(categoryId)) | |
query.VideoCategoryId = categoryId; | |
if (!string.IsNullOrEmpty(relatedToVideoId)) | |
query.RelatedToVideoId = relatedToVideoId; | |
if (sortbyDate) | |
{ | |
query.Order = SearchResource.ListRequest.OrderEnum.Date; | |
} | |
else | |
{ | |
query.Order = currentSearchOrder; | |
} | |
query.Type = searchType; | |
query.MaxResults = pageSize; | |
query.PageToken = pageToken; | |
var response = query.Execute(); | |
// Collect video IDs for duration lookup | |
string videoIDs = ""; | |
foreach (var item in response.Items) | |
{ | |
if (string.IsNullOrEmpty(videoIDs)) | |
{ | |
videoIDs = item.Id.VideoId; | |
} | |
else | |
{ | |
videoIDs = videoIDs + "," + item.Id.VideoId; | |
} | |
} | |
Dictionary<string, string> videoDurations = QueryVideoInfoDuration(videoIDs); | |
//Log.Error("Duration results found: " + videoDurations.Count.ToString()); | |
var results = response.Items.Select(i => new YouTubeVideo() | |
{ | |
Title = i.Snippet.Title, | |
Description = i.Snippet.Description, | |
Thumb = i.Snippet.Thumbnails != null ? i.Snippet.Thumbnails.High.Url : null, | |
Airdate = i.Snippet.PublishedAt != null ? i.Snippet.PublishedAt.Value.ToString("g", OnlineVideoSettings.Instance.Locale) : i.Snippet.PublishedAtRaw, | |
VideoUrl = i.Id.VideoId, | |
ChannelId = i.Snippet.ChannelId, | |
ChannelTitle = i.Snippet.ChannelTitle, | |
Length = videoDurations.FirstOrDefault(x => x.Key == i.Id.VideoId).Value | |
}).ToList<VideoInfo>(); | |
if (!string.IsNullOrEmpty(response.NextPageToken)) | |
{ | |
base.HasNextPage = true; | |
nextPageVideosQuery = () => QuerySearchVideos(queryString, searchType, channelId, categoryId, relatedToVideoId, sortbyDate, response.NextPageToken); | |
} | |
return results; | |
} | |
Dictionary<string, string> QueryVideoInfoDuration(string videoIDs) | |
{ | |
Dictionary<string, string> videoDurations = new Dictionary<string, string>(); | |
//Log.Error("IDs to query: " + videoIDs); | |
var query = Service.Videos.List("snippet, contentDetails"); | |
query.Id = videoIDs; | |
var response = query.Execute(); | |
foreach(var item in response.Items) | |
{ | |
string duration = System.Xml.XmlConvert.ToTimeSpan(item.ContentDetails.Duration).ToString(); | |
//Trim if no hours found (00:) | |
if (duration.StartsWith("00:")) | |
{ | |
duration = duration.Substring(3); | |
} | |
videoDurations.Add(item.Id, duration); | |
//Log.Error(string.Format("ID: {0} Duration: {1}", item.Id, duration)); | |
} | |
return videoDurations; | |
} | |
#endregion | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment