Implement YouTube API client

This commit is contained in:
Maksym Pavlenko
2016-10-05 18:34:27 -04:00
parent 3565c8dbab
commit e78fa82a49
17 changed files with 552 additions and 18 deletions
@@ -0,0 +1,7 @@
namespace Podsync.Services
{
public class PodsyncConfiguration
{
public string YouTubeApiKey { get; set; }
}
}
@@ -0,0 +1,6 @@
namespace Podsync.Services.Videos.YouTube
{
public class Channel : YouTubeItem
{
}
}
@@ -0,0 +1,11 @@
namespace Podsync.Services.Videos.YouTube
{
public struct ChannelQuery
{
public string ChannelId { get; set; }
public string Username { get; set; }
public uint? Count { get; set; }
}
}
@@ -0,0 +1,54 @@
using System.Collections.Generic;
using System.Threading.Tasks;
namespace Podsync.Services.Videos.YouTube
{
public interface IYouTubeClient
{
/// <summary>
/// Returns a collection of zero or more channel resources that match the request criteria
/// Cost: 5 (quota cost of 1 + id: 0, snippet: 2, contentDetails: 2)
/// See https://developers.google.com/youtube/v3/docs/channels/list
/// </summary>
/// <param name="query"></param>
/// <returns></returns>
Task<IEnumerable<Channel>> GetChannels(ChannelQuery query);
/// <summary>
/// Returns a collection of playlists that match the API request parameters
/// Cost: 3 (quota cost of 1 + id: 0, snippet: 2)
/// See https://developers.google.com/youtube/v3/docs/playlists/list
/// </summary>
/// <param name="query"></param>
/// <returns></returns>
Task<IEnumerable<Playlist>> GetPlaylists(PlaylistQuery query);
/// <summary>
/// Returns a list of videos that match the API request parameters
/// Cost: 5 (quota cost of 1 + id: 0, snippet: 2, contentDetails: 2)
/// See https://developers.google.com/youtube/v3/docs/videos/list
/// </summary>
/// <param name="query"></param>
/// <returns></returns>
Task<IEnumerable<Video>> GetVideos(VideoQuery query);
/// <summary>
/// Returns a collection of playlist items that match the API request parameters.
/// You can retrieve all of the playlist items in a specified playlist or
/// retrieve one or more playlist items by their unique IDs.
/// Cost: 3 (quota cost of 1 + id: 0, snippet: 2)
/// See https://developers.google.com/youtube/v3/docs/playlistItems/list
/// </summary>
/// <param name="query"></param>
/// <returns></returns>
Task<IEnumerable<Video>> GetPlaylistItems(PlaylistItemsQuery query);
/// <summary>
/// Optimized version of GetPlaylistItems to query video IDs only
/// Cost: 3 (quota cost of 1 + id: 0, snippet: 2)
/// </summary>
/// <param name="query"></param>
/// <returns></returns>
Task<IEnumerable<string>> GetPlaylistItemIds(PlaylistItemsQuery query);
}
}
@@ -0,0 +1,6 @@
namespace Podsync.Services.Videos.YouTube
{
public class Playlist : YouTubeItem
{
}
}
@@ -0,0 +1,24 @@
namespace Podsync.Services.Videos.YouTube
{
public struct PlaylistItemsQuery
{
/// <summary>
/// The id parameter specifies a comma-separated list of one or more unique playlist item IDs
/// </summary>
public string Id { get; set; }
/// <summary>
/// The playlistId parameter specifies the unique ID of the playlist for which you want to retrieve playlist items.
/// Note that even though this is an optional parameter, every request to retrieve playlist items must specify
/// a value for either the id parameter or the playlistId parameter.
/// </summary>
public string PlaylistId { get; set; }
/// <summary>
/// The videoId parameter specifies that the request should return only the playlist items that contain the specified video
/// </summary>
public string VideoId { get; set; }
public uint? Count { get; set; }
}
}
@@ -0,0 +1,11 @@
namespace Podsync.Services.Videos.YouTube
{
public struct PlaylistQuery
{
public string PlaylistId { get; set; }
public string ChannelId { get; set; }
public uint? Count { get; set; }
}
}
@@ -0,0 +1,11 @@
using System;
namespace Podsync.Services.Videos.YouTube
{
public class Video : YouTubeItem
{
public TimeSpan Duration { get; set; }
public long Size { get; set; }
}
}
@@ -0,0 +1,9 @@
namespace Podsync.Services.Videos.YouTube
{
public struct VideoQuery
{
public string Id { get; set; }
public uint? Count { get; set; }
}
}
@@ -0,0 +1,211 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using System.Xml;
using Google.Apis.Services;
using Google.Apis.YouTube.v3;
using Google.Apis.YouTube.v3.Data;
using Microsoft.Extensions.Options;
using Podsync.Services.Links;
namespace Podsync.Services.Videos.YouTube
{
public sealed class YouTubeClient : IYouTubeClient, IDisposable
{
private const int MaxResults = 50;
private readonly ILinkService _linkService;
private readonly YouTubeService _youtube;
public YouTubeClient(ILinkService linkService, IOptions<PodsyncConfiguration> configuration)
{
_linkService = linkService;
_youtube = new YouTubeService(new BaseClientService.Initializer
{
ApplicationName = "Podsync",
ApiKey = configuration.Value.YouTubeApiKey
});
}
public async Task<IEnumerable<Channel>> GetChannels(ChannelQuery query)
{
var request = _youtube.Channels.List("id,snippet,contentDetails");
request.MaxResults = query.Count ?? MaxResults;
request.Id = query.ChannelId;
request.ForUsername = query.Username;
var response = await request.ExecuteAsync();
return response.Items.Select(ConvertChannel);
}
public async Task<IEnumerable<Playlist>> GetPlaylists(PlaylistQuery query)
{
var request = _youtube.Playlists.List("id,snippet");
request.MaxResults = query.Count ?? MaxResults;
request.Id = query.PlaylistId;
request.ChannelId = query.ChannelId;
var response = await request.ExecuteAsync();
return response.Items.Select(ConvertPlaylist);
}
public async Task<IEnumerable<Video>> GetVideos(VideoQuery query)
{
var request = _youtube.Videos.List("id,snippet,contentDetails");
request.MaxResults = query.Count ?? MaxResults;
request.Id = query.Id;
var response = await request.ExecuteAsync();
return response.Items.Select(ConvertVideo);
}
public async Task<IEnumerable<Video>> GetPlaylistItems(PlaylistItemsQuery query)
{
var response = await QueryPlaylistItems(query);
return response.Items.Select(ConvertPlaylistItem);
}
public async Task<IEnumerable<string>> GetPlaylistItemIds(PlaylistItemsQuery query)
{
var response = await QueryPlaylistItems(query);
return response.Items.Select(x => x.Snippet.ResourceId.VideoId);
}
public void Dispose()
{
_youtube.Dispose();
}
private static long GetVideoSize(string definition, TimeSpan duration)
{
// Video size information requires 1 additional call for each video (1 feed = 50 videos = 50 calls),
// which is too expensive, so get approximated size depending on duration and definition params
var totalSeconds = (long)duration.TotalSeconds;
const long hdBytesPerSecond = 350000;
const long ldBytesPerSecond = 100000;
return totalSeconds * (definition == "hd" ? hdBytesPerSecond : ldBytesPerSecond);
}
private Task<PlaylistItemListResponse> QueryPlaylistItems(PlaylistItemsQuery query)
{
var request = _youtube.PlaylistItems.List("id,snippet");
request.MaxResults = query.Count ?? MaxResults;
request.Id = query.Id;
request.PlaylistId = query.PlaylistId;
request.VideoId = query.VideoId;
return request.ExecuteAsync();
}
private Video ConvertVideo(Google.Apis.YouTube.v3.Data.Video item)
{
var snippet = item.Snippet;
var details = item.ContentDetails;
var link = _linkService.Make(new LinkInfo
{
Id = item.Id,
LinkType = LinkType.Video,
Provider = Provider.YouTube
});
var duration = XmlConvert.ToTimeSpan(details.Duration);
var size = GetVideoSize(details.Definition, duration);
return new Video
{
VideoId = item.Id,
ChannelId = snippet.ChannelId,
Title = snippet.Title,
Description = snippet.Description,
PublishedAt = snippet.PublishedAt ?? DateTime.MinValue,
Link = link,
Duration = duration,
Size = size,
};
}
private Video ConvertPlaylistItem(PlaylistItem item)
{
var snippet = item.Snippet;
var link = _linkService.Make(new LinkInfo
{
Id = item.Id,
LinkType = LinkType.Video,
Provider = Provider.YouTube
});
return new Video
{
VideoId = snippet.ResourceId.VideoId,
ChannelId = snippet.ChannelId,
PlaylistId = snippet.PlaylistId,
Link = link,
Title = snippet.Title,
Description = snippet.Description,
PublishedAt = snippet.PublishedAt ?? DateTime.MinValue,
Thumbnail = new Uri(snippet.Thumbnails.Default__.Url)
};
}
private Playlist ConvertPlaylist(Google.Apis.YouTube.v3.Data.Playlist item)
{
var link = _linkService.Make(new LinkInfo
{
Id = item.Id,
LinkType = LinkType.Playlist,
Provider = Provider.YouTube
});
var snippet = item.Snippet;
return new Playlist
{
PlaylistId = item.Id,
ChannelId = snippet.ChannelId,
Link = link,
Title = $"{snippet.ChannelTitle}: {snippet.Title}",
Description = snippet.Description,
PublishedAt = snippet.PublishedAt ?? DateTime.MinValue,
Thumbnail = new Uri(snippet.Thumbnails.Default__.Url)
};
}
private Channel ConvertChannel(Google.Apis.YouTube.v3.Data.Channel item)
{
var link = _linkService.Make(new LinkInfo
{
Id = item.Id,
LinkType = LinkType.Channel,
Provider = Provider.YouTube
});
var snippet = item.Snippet;
return new Channel
{
ChannelId = item.Id,
PlaylistId = item.ContentDetails.RelatedPlaylists.Uploads,
Link = link,
Title = snippet.Title,
Description = snippet.Description,
PublishedAt = snippet.PublishedAt ?? DateTime.MinValue,
Thumbnail = new Uri(snippet.Thumbnails.Default__.Url)
};
}
}
}
@@ -0,0 +1,23 @@
using System;
namespace Podsync.Services.Videos.YouTube
{
public class YouTubeItem
{
public string VideoId { get; set; }
public string ChannelId { get; set; }
public string PlaylistId { get; set; }
public Uri Link { get; set; }
public Uri Thumbnail { get; set; }
public string Title { get; set; }
public string Description { get; set; }
public DateTime PublishedAt { get; set; }
}
}
+12 -3
View File
@@ -3,7 +3,9 @@ using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Podsync.Services;
using Podsync.Services.Links;
using Podsync.Services.Videos.YouTube;
namespace Podsync
{
@@ -16,6 +18,12 @@ namespace Podsync
.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
.AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true)
.AddEnvironmentVariables();
if (env.IsDevelopment())
{
builder.AddUserSecrets();
}
Configuration = builder.Build();
}
@@ -24,8 +32,11 @@ namespace Podsync
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.Configure<PodsyncConfiguration>(Configuration.GetSection("Podsync"));
// Register core services
services.AddSingleton<ILinkService, LinkService>();
services.AddSingleton<IYouTubeClient, YouTubeClient>();
// Add framework services.
services.AddMvc();
@@ -51,9 +62,7 @@ namespace Podsync
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
routes.MapRoute("default", "{controller=Home}/{action=Index}/{id?}");
});
}
}
+12 -8
View File
@@ -1,10 +1,14 @@
{
"Logging": {
"IncludeScopes": false,
"LogLevel": {
"Default": "Debug",
"System": "Information",
"Microsoft": "Information"
}
}
"Logging": {
"IncludeScopes": false,
"LogLevel": {
"Default": "Debug",
"System": "Information",
"Microsoft": "Information"
}
},
"Podsync": {
"YouTubeApiKey": ""
}
}
+11 -7
View File
@@ -1,9 +1,6 @@
{
"dependencies": {
"Microsoft.NETCore.App": {
"version": "1.0.0",
"type": "platform"
},
"Google.Apis.YouTube.v3": "1.16.0.582",
"Microsoft.AspNetCore.Diagnostics": "1.0.0",
"Microsoft.AspNetCore.Mvc": "1.0.0",
"Microsoft.AspNetCore.Razor.Tools": {
@@ -13,14 +10,19 @@
"Microsoft.AspNetCore.Server.IISIntegration": "1.0.0",
"Microsoft.AspNetCore.Server.Kestrel": "1.0.0",
"Microsoft.AspNetCore.StaticFiles": "1.0.0",
"Microsoft.AspNetCore.WebUtilities": "1.0.0",
"Microsoft.Extensions.Configuration.EnvironmentVariables": "1.0.0",
"Microsoft.Extensions.Configuration.Json": "1.0.0",
"Microsoft.Extensions.Configuration.UserSecrets": "1.0.0",
"Microsoft.Extensions.Logging": "1.0.0",
"Microsoft.Extensions.Logging.Console": "1.0.0",
"Microsoft.Extensions.Logging.Debug": "1.0.0",
"Microsoft.Extensions.Options.ConfigurationExtensions": "1.0.0",
"Microsoft.VisualStudio.Web.BrowserLink.Loader": "14.0.0",
"Microsoft.AspNetCore.WebUtilities": "1.0.0"
"Microsoft.NETCore.App": {
"version": "1.0.0",
"type": "platform"
},
"Microsoft.VisualStudio.Web.BrowserLink.Loader": "14.0.0"
},
"tools": {
@@ -64,5 +66,7 @@
"scripts": {
"prepublish": [ "bower install", "dotnet bundle" ],
"postpublish": [ "dotnet publish-iis --publish-folder %publish:OutputPath% --framework %publish:FullTargetFramework%" ]
}
},
"userSecretsId": "aspnet-Podsync-20161004104901"
}
@@ -0,0 +1,112 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using System.Linq;
using Podsync.Services.Links;
using Podsync.Services.Videos.YouTube;
using Xunit;
namespace Podsync.Tests.Services.Videos.YouTube
{
public class YouTubeClientTests : TestBase, IDisposable
{
private readonly YouTubeClient _client;
public YouTubeClientTests()
{
_client = new YouTubeClient(new LinkService(), Options);
}
[Fact]
public async Task GetChannelTest()
{
var query = new ChannelQuery
{
ChannelId = "UC0JB7TSe49lg56u6qH8y_MQ"
};
var list = await _client.GetChannels(query);
var channel = list.Single();
Assert.Equal(query.ChannelId, channel.ChannelId);
Assert.Equal("GDC", channel.Title);
Assert.False(string.IsNullOrEmpty(channel.Description));
Assert.NotEqual(DateTime.MinValue, channel.PublishedAt);
Assert.NotNull(channel.Thumbnail);
}
[Fact]
public async Task GetPlaylistsTest()
{
var query = new PlaylistQuery
{
PlaylistId = "PL2e4mYbwSTbbiX2uwspn0xiYb8_P_cTAr",
Count = 1
};
var list = await _client.GetPlaylists(query);
var playlist = list.Single();
Assert.Equal("PL2e4mYbwSTbbiX2uwspn0xiYb8_P_cTAr", playlist.PlaylistId);
Assert.Equal("UC0JB7TSe49lg56u6qH8y_MQ", playlist.ChannelId);
Assert.Equal("GDC: Postmortems", playlist.Title);
Assert.Equal(new Uri("https://youtube.com/playlist?list=PL2e4mYbwSTbbiX2uwspn0xiYb8_P_cTAr"), playlist.Link);
Assert.NotNull(playlist.Thumbnail);
Assert.Equal(new DateTime(2015, 06, 29, 16, 58, 34), playlist.PublishedAt);
}
[Fact]
public async Task GetVideosTest()
{
var query = new VideoQuery
{
Id = "OlYH4gDi0Sk,kkcKnWrCZ7k"
};
var response = await _client.GetVideos(query);
var list = response as IList<Video> ?? response.ToList();
Assert.Equal(2, list.Count);
var last = list.Last();
Assert.Equal("kkcKnWrCZ7k", last.VideoId);
Assert.Equal("UCS-aAvZsVegeMDfpdCPjWnA", last.ChannelId);
Assert.Equal("Deus Ex: Mankind Divided - Full Soundtrack OST", last.Title);
Assert.False(string.IsNullOrEmpty(last.Description));
Assert.Equal(new TimeSpan(0, 1, 57, 28), last.Duration);
Assert.Equal(new DateTime(2016, 8, 24, 16, 50, 34), last.PublishedAt);
Assert.Equal(new Uri("https://youtube.com/watch?v=kkcKnWrCZ7k"), last.Link);
Assert.True(last.Size > 0);
}
[Fact]
public async Task GetPlaylistItemsTest()
{
var query = new PlaylistItemsQuery
{
PlaylistId = "PLWG9qzvMcoJGw9AS0XgLfrwDR2kcAMLhs",
Count = 10
};
var response = await _client.GetPlaylistItems(query);
var list = response as IList<Video> ?? response.ToList();
Assert.Equal(4, list.Count);
foreach (var video in list)
{
Assert.False(string.IsNullOrEmpty(video.VideoId));
Assert.False(string.IsNullOrEmpty(video.ChannelId));
Assert.False(string.IsNullOrEmpty(video.PlaylistId));
Assert.False(string.IsNullOrEmpty(video.Title));
}
}
public void Dispose()
{
_client.Dispose();
}
}
}
+29
View File
@@ -0,0 +1,29 @@
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Options;
using Podsync.Services;
namespace Podsync.Tests
{
public abstract class TestBase
{
private const string UserSecretsId = "aspnet-Podsync-20161004104901";
protected TestBase()
{
var configurationRoot = new ConfigurationBuilder()
.AddUserSecrets(UserSecretsId)
.Build();
var podsyncSection = configurationRoot.GetSection("Podsync");
var configuration = new PodsyncConfiguration();
podsyncSection.Bind(configuration);
Options = new OptionsWrapper<PodsyncConfiguration>(configuration);
}
protected IOptions<PodsyncConfiguration> Options { get; }
protected PodsyncConfiguration Configuration => Options.Value;
}
}
+3
View File
@@ -4,10 +4,13 @@
"dependencies": {
"dotnet-test-xunit": "2.2.0-preview2-build1029",
"Microsoft.Extensions.Configuration.UserSecrets": "1.0.0",
"Microsoft.Extensions.Options.ConfigurationExtensions": "1.0.0",
"Microsoft.NETCore.App": {
"version": "1.0.0",
"type": "platform"
},
"Moq": "4.6.38-alpha",
"Podsync": "1.0.0-*",
"xunit": "2.2.0-beta2-build3300"
},