Merge branch 'vimeo'

This commit is contained in:
Maksym Pavlenko
2017-01-09 11:47:05 -08:00
33 changed files with 863 additions and 320 deletions
+19 -5
View File
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Security.Claims;
using System.Threading.Tasks;
using System.Xml.Serialization;
@@ -22,7 +23,11 @@ namespace Podsync.Controllers
[HandleException]
public class FeedController : Controller
{
private const int DefaultPageSize = 50;
private static readonly IDictionary<string, string> Extensions = new Dictionary<string, string>
{
["video/mp4"] = "mp4",
["audio/mp4"] = "m4a"
};
private readonly XmlSerializer _serializer = new XmlSerializer(typeof(Rss));
@@ -57,7 +62,7 @@ namespace Podsync.Controllers
LinkType = linkInfo.LinkType,
Id = linkInfo.Id,
Quality = request.Quality ?? ResolveType.VideoHigh,
PageSize = request.PageSize ?? DefaultPageSize
PageSize = request.PageSize ?? Constants.DefaultPageSize
};
// Check if user eligible for Patreon features
@@ -65,7 +70,7 @@ namespace Podsync.Controllers
if (!enablePatreonFeatures)
{
feed.Quality = ResolveType.VideoHigh;
feed.PageSize = DefaultPageSize;
feed.PageSize = Constants.DefaultPageSize;
}
var feedId = await _storageService.Save(feed);
@@ -101,18 +106,27 @@ namespace Podsync.Controllers
try
{
rss = await _rssBuilder.Query(Request.GetBaseUrl(), feedId);
rss = await _rssBuilder.Query(feedId);
}
catch (KeyNotFoundException)
{
return NotFound(feedId);
}
var selfHost = Request.GetBaseUrl();
// Set atom link to this feed
// See https://validator.w3.org/feed/docs/warning/MissingAtomSelfLink.html
var selfLink = new Uri($"{Request.Scheme}://{Request.Host}{Request.Path}");
var selfLink = new Uri(selfHost, Request.Path);
rss.Channels.ForEach(x => x.AtomLink = selfLink);
// No magic here, just make download links to DownloadController.Download
rss.Channels.SelectMany(x => x.Items).ForEach(item =>
{
var ext = Extensions[item.ContentType];
item.DownloadLink = new Uri(selfHost, $"download/{feedId}/{item.Id}.{ext}");
});
// Serialize feed to string
string body;
using (var writer = new Utf8StringWriter())
+2 -2
View File
@@ -3,7 +3,7 @@ using System.Linq;
using System.Security.Claims;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Podsync.Services.Patreon;
using Podsync.Services;
namespace Podsync.Helpers
{
@@ -53,7 +53,7 @@ namespace Podsync.Helpers
const int MinAmountCents = 100;
int amount;
if (int.TryParse(user.GetClaim(PatreonConstants.AmountDonated), out amount))
if (int.TryParse(user.GetClaim(Constants.Patreon.AmountDonated), out amount))
{
return amount >= MinAmountCents;
}
@@ -1,4 +1,8 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Reflection;
using System.Threading.Tasks;
using Podsync.Services.Feed;
using Podsync.Services.Links;
@@ -7,13 +11,18 @@ using Shared;
namespace Podsync.Services.Builder
{
// ReSharper disable once ClassNeverInstantiated.Global
public class CompositeRssBuilder : RssBuilderBase
{
private readonly YouTubeRssBuilder _youTubeBuilder;
private readonly IDictionary<Provider, IRssBuilder> _builders;
public CompositeRssBuilder(IServiceProvider serviceProvider, IStorageService storageService) : base(storageService)
{
_youTubeBuilder = serviceProvider.CreateInstance<YouTubeRssBuilder>();
// Find all RSS builders (all implementations of IRssBuilder), create instances and make dictionary for fast search by Provider type
var buildTypes = serviceProvider.FindAllImplementationsOf<IRssBuilder>(Assembly.GetEntryAssembly()).Where(x => x != typeof(CompositeRssBuilder));
var builders = buildTypes.Select(builderType => (IRssBuilder)serviceProvider.CreateInstance(builderType)).ToDictionary(builder => builder.Provider);
_builders = new ReadOnlyDictionary<Provider, IRssBuilder>(builders);
}
public override Provider Provider
@@ -21,11 +30,12 @@ namespace Podsync.Services.Builder
get { throw new NotSupportedException(); }
}
public override Task<Rss> Query(Uri baseUrl, string feedId, FeedMetadata feed)
public override Task<Rss> Query(FeedMetadata feed)
{
if (feed.Provider == Provider.YouTube)
IRssBuilder builder;
if (_builders.TryGetValue(feed.Provider, out builder))
{
return _youTubeBuilder.Query(baseUrl, feedId, feed);
return builder.Query(feed);
}
throw new NotSupportedException("Not supported provider");
+8 -3
View File
@@ -1,11 +1,16 @@
using System;
using System.Threading.Tasks;
using System.Threading.Tasks;
using Podsync.Services.Feed;
using Podsync.Services.Links;
using Podsync.Services.Storage;
namespace Podsync.Services.Builder
{
public interface IRssBuilder
{
Task<Rss> Query(Uri baseUrl, string feedId);
Provider Provider { get; }
Task<Rss> Query(string feedId);
Task<Rss> Query(FeedMetadata metadata);
}
}
@@ -1,5 +1,4 @@
using System;
using System.Threading.Tasks;
using System.Threading.Tasks;
using Podsync.Services.Feed;
using Podsync.Services.Links;
using Podsync.Services.Storage;
@@ -8,8 +7,6 @@ namespace Podsync.Services.Builder
{
public abstract class RssBuilderBase : IRssBuilder
{
protected static readonly string DefaultItunesCategory = "TV & Film";
private readonly IStorageService _storageService;
protected RssBuilderBase(IStorageService storageService)
@@ -19,13 +16,13 @@ namespace Podsync.Services.Builder
public abstract Provider Provider { get; }
public async Task<Rss> Query(Uri baseUrl, string feedId)
public async Task<Rss> Query(string feedId)
{
var metadata = await _storageService.Load(feedId);
return await Query(baseUrl, feedId, metadata);
return await Query(metadata);
}
public abstract Task<Rss> Query(Uri baseUrl, string feedId, FeedMetadata metadata);
public abstract Task<Rss> Query(FeedMetadata metadata);
}
}
@@ -0,0 +1,114 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Podsync.Services.Feed;
using Podsync.Services.Links;
using Podsync.Services.Storage;
using Podsync.Services.Videos.Vimeo;
namespace Podsync.Services.Builder
{
// ReSharper disable once ClassNeverInstantiated.Global
public class VimeoRssBuilder : RssBuilderBase
{
private readonly IVimeoClient _client;
public VimeoRssBuilder(IStorageService storageService, IVimeoClient client) : base(storageService)
{
_client = client;
}
public override Provider Provider { get; } = Provider.Vimeo;
public override async Task<Rss> Query(FeedMetadata metadata)
{
var linkType = metadata.LinkType;
var id = metadata.Id;
var pageSize = metadata.PageSize;
if (pageSize == 0)
{
pageSize = Constants.DefaultPageSize;
}
Channel channel;
if (linkType == LinkType.Channel)
{
channel = CreateChannel(await _client.Channel(id));
channel.Items = CreateItems(await _client.ChannelVideos(id, pageSize));
}
else if (linkType == LinkType.Group)
{
channel = CreateChannel(await _client.Group(id));
channel.Items = CreateItems(await _client.GroupVideos(id, pageSize));
}
else if (linkType == LinkType.User)
{
channel = CreateChannel(await _client.User(id));
channel.Items = CreateItems(await _client.UserVideos(id, pageSize));
}
else
{
throw new NotSupportedException("URL type is not supported");
}
var rss = new Rss
{
Channels = new[] { channel }
};
return rss;
}
private static Channel CreateChannel(Group group)
{
return new Channel
{
Title = group.Name,
Description = group.Description,
Link = group.Link,
PubDate = group.CreatedAt,
Image = group.Thumbnail,
Thumbnail = group.Thumbnail,
Guid = group.Link.ToString()
};
}
private static Channel CreateChannel(User user)
{
return new Channel
{
Title = user.Name,
Description = user.Bio,
Link = user.Link,
PubDate = user.CreatedAt,
Image = user.Thumbnail,
Thumbnail = user.Thumbnail,
Guid = user.Link.ToString()
};
}
private static Item CreateItem(Video video)
{
return new Item
{
Id = video.Id,
Title = video.Title,
Description = video.Description,
PubDate = video.CreatedAt,
Link = video.Link,
Duration = video.Duration,
FileSize = video.Size,
ContentType = "video/mp4",
Author = video.Author
};
}
private static Item[] CreateItems(IEnumerable<Video> videos)
{
return videos.Select(CreateItem).ToArray();
}
}
}
@@ -13,18 +13,16 @@ namespace Podsync.Services.Builder
{
public class YouTubeRssBuilder : RssBuilderBase
{
private readonly ILinkService _linkService;
private readonly IYouTubeClient _youTube;
public YouTubeRssBuilder(ILinkService linkService, IYouTubeClient youTube, IStorageService storageService) : base(storageService)
public YouTubeRssBuilder(IYouTubeClient youTube, IStorageService storageService) : base(storageService)
{
_linkService = linkService;
_youTube = youTube;
}
public override Provider Provider { get; } = Provider.YouTube;
public override async Task<Rss> Query(Uri baseUrl, string feedId, FeedMetadata metadata)
public override async Task<Rss> Query(FeedMetadata metadata)
{
if (metadata.Provider != Provider.YouTube)
{
@@ -57,7 +55,7 @@ namespace Podsync.Services.Builder
// Get video descriptions
var videos = await _youTube.GetVideos(new VideoQuery { Id = string.Join(",", ids) });
channel.Items = videos.Select(youtubeVideo => MakeItem(youtubeVideo, baseUrl, feedId, metadata));
channel.Items = videos.Select(youtubeVideo => MakeItem(youtubeVideo, metadata)).ToArray();
var rss = new Rss
{
@@ -96,54 +94,42 @@ namespace Podsync.Services.Builder
Title = item.Title,
Description = item.Description,
Link = item.Link,
LastBuildDate = DateTime.Now,
PubDate = item.PublishedAt,
Image = item.Thumbnail,
Thumbnail = item.Thumbnail,
Category = DefaultItunesCategory
};
}
private Item MakeItem(Video video, Uri baseUrl, string feedId, FeedMetadata feed)
private Item MakeItem(Video video, FeedMetadata feed)
{
string contentType;
string extension;
GetMediaInfo(feed.Quality, out contentType, out extension);
var downloadUri = _linkService.Download(baseUrl, feedId, video.VideoId, extension);
string contentType = GetContentType(feed.Quality);
return new Item
{
Id = video.VideoId,
Title = video.Title,
Description = video.Description,
PubDate = video.PublishedAt,
Link = video.Link,
Duration = video.Duration,
Content = new MediaContent
{
Length = video.Size,
MediaType = contentType,
Url = downloadUri
}
FileSize = video.Size,
ContentType = contentType
};
}
private static void GetMediaInfo(ResolveType resolveType, out string contentType, out string extension)
private static string GetContentType(ResolveType resolveType)
{
if (resolveType == ResolveType.VideoHigh || resolveType == ResolveType.VideoLow)
{
contentType = "video/mp4";
extension = ".mp4";
return "video/mp4";
}
else if (resolveType == ResolveType.AudioHigh || resolveType == ResolveType.AudioLow)
if (resolveType == ResolveType.AudioHigh || resolveType == ResolveType.AudioLow)
{
contentType = "audio/mp4";
extension = ".m4a";
}
else
{
throw new ArgumentException("Unsupported resolve type");
return "audio/mp4";
}
throw new ArgumentException("Unsupported resolve type");
}
}
}
+22
View File
@@ -0,0 +1,22 @@
using Podsync.Services.Resolver;
namespace Podsync.Services
{
public static class Constants
{
public const int DefaultPageSize = 50;
public const ResolveType DefaultFormat = ResolveType.VideoHigh;
public static class Patreon
{
public const string AuthenticationScheme = "Patreon";
public const string AuthorizationEndpoint = "https://www.patreon.com/oauth2/authorize";
public const string TokenEndpoint = "https://api.patreon.com/oauth2/token";
public const string AmountDonated = "Patreon/" + nameof(AmountDonated);
}
}
}
+4 -4
View File
@@ -1,6 +1,4 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml;
using System.Xml.Schema;
using System.Xml.Serialization;
@@ -12,10 +10,12 @@ namespace Podsync.Services.Feed
public class Channel : IXmlSerializable
{
private const string PodsyncGeneratorName = "Podsync Generator";
private const string DefaultItunesCategory = "TV & Film";
public Channel()
{
Items = Enumerable.Empty<Item>();
Category = DefaultItunesCategory;
LastBuildDate = DateTime.Now;
}
public string Guid { get; set; }
@@ -40,7 +40,7 @@ namespace Podsync.Services.Feed
public Uri Thumbnail { get; set; }
public IEnumerable<Item> Items { get; set; }
public Item[] Items { get; set; }
public bool Explicit { get; set; }
+20 -8
View File
@@ -1,4 +1,5 @@
using System;
using System.IO;
using System.Xml;
using System.Xml.Schema;
using System.Xml.Serialization;
@@ -22,7 +23,13 @@ namespace Podsync.Services.Feed
public TimeSpan Duration { get; set; }
public MediaContent Content { get; set; }
public string Id { get; set; }
public long FileSize { get; set; }
public Uri DownloadLink { get; set; }
public string ContentType { get; set; }
public XmlSchema GetSchema()
{
@@ -52,17 +59,22 @@ namespace Podsync.Services.Feed
writer.WriteStartElement("guid");
writer.WriteAttributeString("isPermaLink", "true");
writer.WriteString(Link.ToString());
writer.WriteString(Link?.ToString() ?? Id);
writer.WriteEndElement();
/*
<enclosure url="http://podsync.net/download/youtube/yp202t46OIE.mp4" length="48300000" type="video/mp4"/>
*/
if (DownloadLink == null)
{
throw new InvalidDataException("Can't generate RSS item with no download link");
}
writer.WriteStartElement("enclosure");
writer.WriteAttributeString("url", Content.Url.ToString());
writer.WriteAttributeString("length", Content.Length.ToString());
writer.WriteAttributeString("type", Content.MediaType);
writer.WriteAttributeString("url", DownloadLink.ToString());
writer.WriteAttributeString("length", FileSize.ToString());
writer.WriteAttributeString("type", ContentType);
writer.WriteEndElement();
/*
@@ -70,9 +82,9 @@ namespace Podsync.Services.Feed
*/
writer.WriteStartElement("content", Namespaces.Media);
writer.WriteAttributeString("url", Content.Url.ToString());
writer.WriteAttributeString("fileSize", Content.Length.ToString());
writer.WriteAttributeString("type", Content.MediaType);
writer.WriteAttributeString("url", DownloadLink.ToString());
writer.WriteAttributeString("fileSize", FileSize.ToString());
writer.WriteAttributeString("type", ContentType);
writer.WriteEndElement();
/*
-13
View File
@@ -1,13 +0,0 @@
using System;
namespace Podsync.Services.Feed
{
public struct MediaContent
{
public Uri Url { get; set; }
public long Length { get; set; }
public string MediaType { get; set; }
}
}
@@ -8,8 +8,6 @@ namespace Podsync.Services.Links
Uri Make(LinkInfo info);
Uri Download(Uri baseUrl, string feedId, string videoId, string ext);
Uri Feed(Uri baseUrl, string feedId);
}
}
+75 -114
View File
@@ -1,8 +1,8 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using Microsoft.AspNetCore.WebUtilities;
using Shared;
using Microsoft.Extensions.Primitives;
namespace Podsync.Services.Links
{
@@ -14,20 +14,41 @@ namespace Podsync.Services.Links
{
[LinkType.Video] = "https://youtube.com/watch?v={0}",
[LinkType.Channel] = "https://youtube.com/channel/{0}",
[LinkType.Playlist] = "https://youtube.com/playlist?list={0}",
[LinkType.Info] = "https://youtube.com/get_video_info?video_id={0}"
[LinkType.Playlist] = "https://youtube.com/playlist?list={0}"
},
[Provider.Vimeo] = new Dictionary<LinkType, string>
{
[LinkType.Category] = "https://vimeo.com/categories/{0}",
[LinkType.Channel] = "https://vimeo.com/channels/{0}",
[LinkType.Group] = "https://vimeo.com/groups/{0}",
[LinkType.User] = "https://vimeo.com/{0}",
[LinkType.Info] = "https://player.vimeo.com/video/{0}/config"
[LinkType.User] = "https://vimeo.com/user{0}",
[LinkType.Video] = "https://vimeo.com/{0}"
}
};
/*
YouTube users, channels and playlists
Test input:
https://www.youtube.com/playlist?list=PLCB9F975ECF01953C
https://www.youtube.com/channel/UC5XPnUk8Vvv_pWslhwom6Og
https://www.youtube.com/user/fxigr1
*/
private static readonly Regex YouTubeRegex = new Regex(@"^(?:https?://)?(?:www\.)?(?:youtube.com/)(?<type>user|channel|playlist|watch)/?(?<id>\w+)?", RegexOptions.Compiled);
/*
Vimeo groups, channels and users
Test input:
https://vimeo.com/groups/109
http://vimeo.com/groups/109
http://www.vimeo.com/groups/109
https://vimeo.com/groups/109/videos/
https://vimeo.com/channels/staffpicks
https://vimeo.com/channels/staffpicks/146224925
https://vimeo.com/awhitelabelproduct
*/
private static readonly Regex VimeoRegex = new Regex(@"^(?:https?://)?(?:www\.)?(?:vimeo.com/)(?<type>groups|channels)?/?(?<id>\w+)", RegexOptions.Compiled);
public LinkInfo Parse(Uri link)
{
if (link == null)
@@ -40,132 +61,77 @@ namespace Podsync.Services.Links
var id = string.Empty;
var segments = link.Segments
.Select(x => x.TrimEnd('/'))
.Where(x => !string.IsNullOrWhiteSpace(x))
.ToArray();
var host = link.Host.ToLowerInvariant().TrimStart("www.").TrimStart("m.");
if (host == "youtu.be")
// YouTube
var match = YouTubeRegex.Match(link.AbsoluteUri);
if (match.Success)
{
provider = Provider.YouTube;
if (segments.Length == 1)
var type = match.Groups["type"]?.ToString();
if (type == "user")
{
// https://youtu.be/AAAAAAAAA01
// https://www.youtu.be/AAAAAAAAA08
// https://www.youtube.com/user/fxigr1
linkType = LinkType.Video;
id = segments.Single();
id = match.Groups["id"]?.ToString();
linkType = LinkType.User;
}
else if (type == "channel")
{
// https://www.youtube.com/channel/UC5XPnUk8Vvv_pWslhwom6Og
id = match.Groups["id"]?.ToString();
linkType = LinkType.Channel;
}
else if (type == "playlist" || type == "watch")
{
// https://www.youtube.com/playlist?list=PLCB9F975ECF01953C
// https://www.youtube.com/watch?v=otm9NaT9OWU&list=PLCB9F975ECF01953C
var qs = QueryHelpers.ParseQuery(link.Query);
StringValues list;
if (qs.TryGetValue("list", out list))
{
id = list;
}
linkType = LinkType.Playlist;
}
}
else if (host == "youtube.com")
else
{
provider = Provider.YouTube;
var query = QueryHelpers.ParseQuery(link.Query);
if (segments.Length >= 2 && segments[0] == "user")
// Vimeo
match = VimeoRegex.Match(link.AbsoluteUri);
if (match.Success)
{
linkType = LinkType.User;
id = segments[1];
}
else if (segments.Length == 2)
{
if (string.Equals(segments[0], "embed"))
provider = Provider.Vimeo;
id = match.Groups["id"]?.ToString();
var type = match.Groups["type"]?.ToString();
if (type == "groups")
{
linkType = LinkType.Video;
// https://vimeo.com/groups/109
if (string.Equals(segments[1], "watch"))
{
// http://www.youtube.com/embed/watch?feature=player_embedded&v=AAAAAAAAA02
// http://www.youtube.com/embed/watch?v=AAAAAAAAA03
id = query["v"];
}
else if (segments[1].StartsWith("v="))
{
// http://www.youtube.com/embed/v=AAAAAAAAA04
id = segments[1].TrimStart("v=");
}
linkType = LinkType.Group;
}
else if (string.Equals(segments[0], "watch"))
else if (type == "channels")
{
// http://www.youtube.com/watch/jMeC7JFQ6811
linkType = LinkType.Video;
id = segments[1];
}
else if (string.Equals(segments[0], "v"))
{
// http://www.youtube.com/v/jMeC7JFQ6812
// http://www.youtube.com/v/A-AAAAAAA18?fs=1&rel=0
linkType = LinkType.Video;
id = segments[1];
}
else if (string.Equals(segments[0], "channel"))
{
// https://www.youtube.com/channel/UC5XPnUk8Vvv_pWslhwom6Og
// https://vimeo.com/channels/staffpicks
linkType = LinkType.Channel;
id = segments[1];
}
}
else if (segments.Length == 1)
{
if (string.Equals(segments[0], "watch"))
else
{
if (query.ContainsKey("list"))
{
// https://www.youtube.com/watch?v=otm9NaT9OWU&list=PLCB9F975ECF01953C
// https://vimeo.com/awhitelabelproduct
linkType = LinkType.Playlist;
id = query["list"];
}
else
{
// http://www.youtube.com/watch?v=AAAAAAAAA06
// http://www.youtube.com/watch?feature=player_embedded&v=AAAAAAAAA05
linkType = LinkType.Video;
id = query["v"];
}
}
else if (string.Equals(segments[0], "attribution_link"))
{
// http://www.youtube.com/attribution_link?u=/watch?v=jMeC7JFQ6815&feature=share&a=9QlmP1yvjcllp0h3l0NwuA
// http://www.youtube.com/attribution_link?a=fF1CWYwxCQ4&u=/watch?v=jMeC7JFQ6816&feature=em-uploademail
// http://www.youtube.com/attribution_link?a=fF1CWYwxCQ4&feature=em-uploademail&u=/watch?v=jMeC7JFQ6817
string u = query["u"];
var pos = u?.IndexOf("?", StringComparison.OrdinalIgnoreCase) ?? -1;
if (pos != -1)
{
// ReSharper disable once PossibleNullReferenceException
var attrQueryParams = QueryHelpers.ParseQuery(u.Substring(pos));
linkType = LinkType.Video;
id = attrQueryParams["v"];
}
}
else if (string.Equals(segments[0], "playlist"))
{
// https://www.youtube.com/playlist?list=PLCB9F975ECF01953C
linkType = LinkType.Playlist;
id = query["list"];
linkType = LinkType.User;
}
}
}
if (string.IsNullOrWhiteSpace(id) || linkType == LinkType.Unknown || provider == Provider.Unknown)
{
throw new ArgumentException("This provider is not supported");
throw new ArgumentException("Not supported provider or link format");
}
return new LinkInfo
@@ -194,11 +160,6 @@ namespace Podsync.Services.Links
}
}
public Uri Download(Uri baseUrl, string feedId, string videoId, string ext)
{
return new Uri(baseUrl, $"download/{feedId}/{videoId}{ext}");
}
public Uri Feed(Uri baseUrl, string feedId)
{
return new Uri(baseUrl, feedId);
-2
View File
@@ -7,8 +7,6 @@
Playlist,
User,
Channel,
Info,
Category,
Group
}
}
@@ -1,13 +0,0 @@
namespace Podsync.Services.Patreon
{
public static class PatreonConstants
{
public const string AuthenticationScheme = "Patreon";
public const string AuthorizationEndpoint = "https://www.patreon.com/oauth2/authorize";
public const string TokenEndpoint = "https://api.patreon.com/oauth2/token";
public const string AmountDonated = "Patreon/" + nameof(AmountDonated);
}
}
@@ -6,6 +6,8 @@ namespace Podsync.Services
{
public string YouTubeApiKey { get; set; }
public string VimeoApiKey { get; set; }
public string RedisConnectionString { get; set; }
public string PatreonClientId { get; set; }
+9 -2
View File
@@ -3,11 +3,11 @@ using Podsync.Services.Resolver;
namespace Podsync.Services.Storage
{
public struct FeedMetadata
public class FeedMetadata
{
public Provider Provider { get; set; }
public LinkType LinkType { get; set; }
public LinkType Type { get; set; }
public string Id { get; set; }
@@ -16,5 +16,12 @@ namespace Podsync.Services.Storage
public int PageSize { get; set; }
public override string ToString() => $"{Provider} ({LinkType}) {Id}";
// Workaround for backward compatibility
public LinkType LinkType
{
get { return Type; }
set { Type = value; }
}
}
}
+69 -41
View File
@@ -1,13 +1,14 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Linq.Expressions;
using System.Net;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
using HashidsNet;
using Microsoft.Extensions.Options;
using Podsync.Services.Links;
using Podsync.Services.Resolver;
using StackExchange.Redis;
namespace Podsync.Services.Storage
@@ -18,16 +19,6 @@ namespace Podsync.Services.Storage
private const string IdSalt = "65fce519433f4218aa0cee6394225eea";
private const int IdLength = 4;
// Store all fields manually for backward compatibility with existing implementation
private const string ProviderField = "provider";
private const string TypeField = "type";
private const string IdField = "id";
private const string QualityField = "quality";
private const string PageSizeField = "pageSize";
private const ResolveType DefaultQuality = ResolveType.VideoHigh;
private const int DefaultPageSize = 50;
private static readonly IHashids HashIds = new Hashids(IdSalt, IdLength);
private readonly string _cs;
@@ -85,13 +76,21 @@ namespace Podsync.Services.Storage
{
var id = await MakeId();
if (await Db.KeyExistsAsync(id))
{
throw new InvalidOperationException("Failed to generate feed id");
}
await Db.HashSetAsync(id, new[]
{
new HashEntry(ProviderField, metadata.Provider.ToString()),
new HashEntry(TypeField, metadata.LinkType.ToString()),
new HashEntry(IdField, metadata.Id),
new HashEntry(QualityField, metadata.Quality.ToString()),
new HashEntry(PageSizeField, metadata.PageSize),
// V1
new HashEntry(nameof(metadata.Provider), metadata.Provider.ToString()),
new HashEntry(nameof(metadata.Type), metadata.Type.ToString()),
new HashEntry(nameof(metadata.Id), metadata.Id),
// V2
new HashEntry(nameof(metadata.Quality), metadata.Quality.ToString()),
new HashEntry(nameof(metadata.PageSize), metadata.PageSize),
});
await Db.KeyExpireAsync(id, TimeSpan.FromDays(1));
@@ -116,42 +115,71 @@ namespace Podsync.Services.Storage
throw new KeyNotFoundException("Invaid key");
}
var metadata = new FeedMetadata
{
Id = entries.Single(x => x.Name == IdField).Value,
LinkType = ToEnum<LinkType>(entries.Single(x => x.Name == TypeField)),
Provider = ToEnum<Provider>(entries.Single(x => x.Name == ProviderField)),
};
var metadata = new FeedMetadata();
if (entries.Length > 3)
{
metadata.Quality = ToEnum<ResolveType>(entries.Single(x => x.Name == QualityField));
metadata.PageSize = (int)entries.Single(x => x.Name == PageSizeField).Value;
}
else
{
// Set default values
metadata.Quality = DefaultQuality;
metadata.PageSize = DefaultPageSize;
}
// V1
SetProperty(metadata, x => x.Id, entries);
SetProperty(metadata, x => x.Type, entries);
SetProperty(metadata, x => x.Provider, entries);
// V2
SetProperty(metadata, x => x.Quality, entries, Constants.DefaultFormat);
SetProperty(metadata, x => x.PageSize, entries, Constants.DefaultPageSize);
return metadata;
}
public Task ResetCounter()
{
return Db.KeyDeleteAsync(IdKey);
}
public async Task<string> MakeId()
{
var id = await Db.StringIncrementAsync(IdKey);
return HashIds.EncodeLong(id);
}
private static T ToEnum<T>(HashEntry key)
private static void SetProperty<T, P>(T target, Expression<Func<T, P>> memberLamda, HashEntry[] entries)
{
return (T)Enum.Parse(typeof(T), key.Value, true);
SetProperty(target, memberLamda, entries, default(P), true);
}
private static void SetProperty<T, P>(T target, Expression<Func<T, P>> memberLamda, HashEntry[] entries, P fallback, bool throwIfMissing = false)
{
var memberExpression = memberLamda.Body as MemberExpression;
// Get property name via reflection
var entryName = memberExpression?.Member?.Name;
if (string.IsNullOrEmpty(entryName))
{
throw new InvalidOperationException("Wrong property expression");
}
P value;
// RedisValue is value type
if (entries.Any(x => string.Equals(x.Name, entryName, StringComparison.OrdinalIgnoreCase)))
{
var entry = entries.Single(x => string.Equals(x.Name, entryName, StringComparison.OrdinalIgnoreCase));
var propertyType = typeof(P);
if (propertyType.GetTypeInfo().IsEnum)
{
value = (P)Enum.Parse(propertyType, entry.Value);
}
else
{
value = (P)Convert.ChangeType(entry.Value, propertyType);
}
}
else
{
if (throwIfMissing)
{
throw new InvalidDataException("Missing mandatory property");
}
value = fallback;
}
var property = memberExpression.Member as PropertyInfo;
property?.SetValue(target, value);
}
}
}
@@ -0,0 +1,19 @@
using System;
namespace Podsync.Services.Videos.Vimeo
{
public class Group
{
public string Name { get; set; }
public string Description { get; set; }
public Uri Link { get; set; }
public DateTime CreatedAt { get; set; }
public Uri Thumbnail { get; set; }
public string Author { get; set; }
}
}
@@ -0,0 +1,20 @@
using System.Collections.Generic;
using System.Threading.Tasks;
namespace Podsync.Services.Videos.Vimeo
{
public interface IVimeoClient
{
Task<Group> Group(string id);
Task<Group> Channel(string id);
Task<User> User(string id);
Task<IEnumerable<Video>> GroupVideos(string id, int count);
Task<IEnumerable<Video>> UserVideos(string id, int count);
Task<IEnumerable<Video>> ChannelVideos(string id, int count);
}
}
+17
View File
@@ -0,0 +1,17 @@
using System;
namespace Podsync.Services.Videos.Vimeo
{
public class User
{
public string Name { get; set; }
public string Bio { get; set; }
public DateTime CreatedAt { get; set; }
public Uri Link { get; set; }
public Uri Thumbnail { get; set; }
}
}
@@ -0,0 +1,25 @@
using System;
namespace Podsync.Services.Videos.Vimeo
{
public class Video
{
public string Id { get; set; }
public string Title { get; set; }
public string Description { get; set; }
public Uri Link { get; set; }
public Uri Thumbnail { get; set; }
public DateTime CreatedAt { get; set; }
public long Size { get; set; }
public TimeSpan Duration { get; set; }
public string Author { get; set; }
}
}
@@ -0,0 +1,146 @@
using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading.Tasks;
using Microsoft.Extensions.Options;
using Newtonsoft.Json.Linq;
namespace Podsync.Services.Videos.Vimeo
{
// ReSharper disable once ClassNeverInstantiated.Global
public sealed class VimeoClient : IVimeoClient, IDisposable
{
private const int MaxPageSize = 100;
private readonly HttpClient _client = new HttpClient();
public VimeoClient(IOptions<PodsyncConfiguration> configuration)
{
_client.BaseAddress = new Uri("https://api.vimeo.com/");
_client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", configuration.Value.VimeoApiKey);
}
public Task<Group> Group(string id)
{
return QueryGroup($"groups/{id}");
}
public Task<Group> Channel(string id)
{
return QueryGroup($"channels/{id}");
}
public async Task<User> User(string id)
{
dynamic json = await QueryApi($"users/{id}");
return new User
{
Name = json.name,
Bio = json.bio,
Link = new Uri(json.link.ToString()),
Thumbnail = new Uri(json.pictures.sizes[0].link.ToString()),
CreatedAt = DateTime.Parse(json.created_time.ToString()),
};
}
public Task<IEnumerable<Video>> GroupVideos(string id, int count)
{
return QueryVideos($"groups/{id}/videos", count);
}
public Task<IEnumerable<Video>> UserVideos(string id, int count)
{
return QueryVideos($"users/{id}/videos", count);
}
public Task<IEnumerable<Video>> ChannelVideos(string id, int count)
{
return QueryVideos($"channels/{id}/videos", count);
}
public void Dispose()
{
_client.Dispose();
}
private async Task<IEnumerable<Video>> QueryVideos(string path, int count)
{
if (count <= 0)
{
throw new ArgumentException("Invalid item count", nameof(count));
}
var collection = new List<Video>(count);
var pageIndex = 1;
while (count > 0)
{
var pageSize = Math.Min(count, MaxPageSize);
await GetPage(path, pageIndex, pageSize, collection);
count -= pageSize;
pageIndex++;
}
return collection;
}
private async Task GetPage(string path, int pageIndex, int pageSize, List<Video> output)
{
dynamic resp = await QueryApi($"{path}?per_page={pageSize}&page={pageIndex}");
foreach (dynamic v in resp.data)
{
// Approximated file size
var size = Convert.ToInt64(
v.width.ToObject<long>() *
v.height.ToObject<long>() *
v.duration.ToObject<long>() *
0.38848958333);
// Extract id from uri like '/videos/50522981'
var uri = v.uri.ToString();
var id = uri.Split(new[] { '/' }, StringSplitOptions.RemoveEmptyEntries)[1];
var video = new Video
{
Id = id,
Title = v.name,
Description = v.description,
Link = new Uri(v.link?.ToString()),
Thumbnail = new Uri(v.pictures?.sizes[0]?.link?.ToString()),
CreatedAt = DateTime.Parse(v.created_time?.ToString()),
Duration = TimeSpan.FromSeconds(v.duration?.ToObject<int>()),
Size = size,
Author = v.user.name
};
output.Add(video);
}
}
private async Task<Group> QueryGroup(string path)
{
dynamic json = await QueryApi(path);
return new Group
{
Name = json.name,
Description = json.description,
Link = new Uri(json.link?.ToString()),
Thumbnail = new Uri(json.pictures?.sizes[0]?.link?.ToString()),
CreatedAt = DateTime.Parse(json.created_time?.ToString()),
Author = json.user.name,
};
}
private async Task<JObject> QueryApi(string path)
{
var json = await _client.GetStringAsync(path);
return JObject.Parse(json);
}
}
}
+7 -5
View File
@@ -17,6 +17,7 @@ using Podsync.Services.Links;
using Podsync.Services.Patreon;
using Podsync.Services.Resolver;
using Podsync.Services.Storage;
using Podsync.Services.Videos.Vimeo;
using Podsync.Services.Videos.YouTube;
namespace Podsync
@@ -51,6 +52,7 @@ namespace Podsync
// Register core services
services.AddSingleton<ILinkService, LinkService>();
services.AddSingleton<IYouTubeClient, YouTubeClient>();
services.AddSingleton<IVimeoClient, VimeoClient>();
services.AddSingleton<IResolverService, YtdlWrapper>();
services.AddSingleton<IStorageService, RedisStorage>();
services.AddSingleton<IRssBuilder, CompositeRssBuilder>();
@@ -93,15 +95,15 @@ namespace Podsync
// Patreon authentication
app.UseOAuthAuthentication(new OAuthOptions
{
AuthenticationScheme = PatreonConstants.AuthenticationScheme,
AuthenticationScheme = Constants.Patreon.AuthenticationScheme,
ClientId = Configuration[$"Podsync:{nameof(PodsyncConfiguration.PatreonClientId)}"],
ClientSecret = Configuration[$"Podsync:{nameof(PodsyncConfiguration.PatreonSecret)}"],
CallbackPath = new PathString("/oauth-patreon"),
AuthorizationEndpoint = PatreonConstants.AuthorizationEndpoint,
TokenEndpoint = PatreonConstants.TokenEndpoint,
AuthorizationEndpoint = Constants.Patreon.AuthorizationEndpoint,
TokenEndpoint = Constants.Patreon.TokenEndpoint,
SaveTokens = true,
@@ -127,7 +129,7 @@ namespace Podsync
context.Identity.AddClaim(new Claim(ClaimTypes.Uri, user.Url));
var amountCents = user.Pledges.Sum(x => x.AmountCents);
context.Identity.AddClaim(new Claim(PatreonConstants.AmountDonated, amountCents.ToString()));
context.Identity.AddClaim(new Claim(Constants.Patreon.AmountDonated, amountCents.ToString()));
var telemetry = app.ApplicationServices.GetService<TelemetryClient>();
telemetry.TrackEvent("Login", new Dictionary<string, string>
@@ -145,7 +147,7 @@ namespace Podsync
builder.Run(async context =>
{
// Return a challenge to invoke the Patreon authentication scheme
await context.Authentication.ChallengeAsync(PatreonConstants.AuthenticationScheme, new AuthenticationProperties { RedirectUri = "/" });
await context.Authentication.ChallengeAsync(Constants.Patreon.AuthenticationScheme, new AuthenticationProperties { RedirectUri = "/" });
});
});
-14
View File
@@ -1,14 +0,0 @@
@{
ViewData["Title"] = "Error";
}
<h1 class="text-danger">Error.</h1>
<h2 class="text-danger">An error occurred while processing your request.</h2>
<h3>Development Mode</h3>
<p>
Swapping to <strong>Development</strong> environment will display more detailed information about the error that occurred.
</p>
<p>
<strong>Development environment should not be enabled in deployed applications</strong>, as it can result in sensitive information from exceptions being displayed to end users. For local debugging, development environment can be enabled by setting the <strong>ASPNETCORE_ENVIRONMENT</strong> environment variable to <strong>Development</strong>, and restarting the application.
</p>
+1
View File
@@ -9,6 +9,7 @@
},
"Podsync": {
"YouTubeApiKey": "",
"VimeoApiKey": "",
"RedisConnectionString": "localhost",
"BaseUrl": "",
"PatreonClientId": "",
+50
View File
@@ -1,4 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using Microsoft.Extensions.DependencyInjection;
namespace Shared
@@ -19,5 +22,52 @@ namespace Shared
{
return ActivatorUtilities.CreateInstance(serviceProvider, type);
}
public static IEnumerable<Type> FindAllImplementationsOf(this IServiceProvider serviceProvider, Type interfaceType, Assembly assembly)
{
if (!interfaceType.GetTypeInfo().IsInterface)
{
throw new ArgumentException("T should be an interface");
}
return GetLoadableTypes(assembly).Where(type => IsAssignableFrom(interfaceType, type));
}
public static IEnumerable<Type> FindAllImplementationsOf<T>(this IServiceProvider serviceProvider, Assembly assembly)
{
return serviceProvider.FindAllImplementationsOf(typeof(T), assembly);
}
private static bool IsAssignableFrom(Type interfaceType, Type serviceType)
{
var serviceTypeInfo = serviceType.GetTypeInfo();
if (serviceTypeInfo.IsInterface || serviceTypeInfo.IsAbstract)
{
return false;
}
var interfaceTypeInfo = interfaceType.GetTypeInfo();
if (!interfaceTypeInfo.IsGenericType)
{
return interfaceType.IsAssignableFrom(serviceType);
}
return serviceType
.GetInterfaces()
.Where(type => type.GetTypeInfo().IsGenericType)
.Any(type => type.GetGenericTypeDefinition() == interfaceType);
}
private static IEnumerable<Type> GetLoadableTypes(Assembly assembly)
{
try
{
return assembly.GetTypes();
}
catch (ReflectionTypeLoadException e)
{
return e.Types.Where(x => x != null);
}
}
}
}
@@ -0,0 +1,65 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using Moq;
using Podsync.Services.Builder;
using Podsync.Services.Links;
using Podsync.Services.Storage;
using Podsync.Services.Videos.Vimeo;
using Xunit;
namespace Podsync.Tests.Services.Builder
{
public class VimeoRssBuilderTests : TestBase
{
private readonly Mock<IStorageService> _storageService = new Mock<IStorageService>();
private readonly VimeoRssBuilder _builder;
public VimeoRssBuilderTests()
{
_builder = new VimeoRssBuilder(_storageService.Object, new VimeoClient(Options));
}
[Theory]
[InlineData(LinkType.Channel, "staffpicks")]
[InlineData(LinkType.Group, "motion")]
[InlineData(LinkType.User, "motionarray")]
public async Task BuildRssTest(LinkType linkType, string id)
{
var feed = new FeedMetadata
{
Provider = Provider.YouTube,
LinkType = linkType,
Id = id
};
var feedId = DateTime.UtcNow.Ticks.ToString();
_storageService.Setup(x => x.Load(feedId)).ReturnsAsync(feed);
var rss = await _builder.Query(feedId);
Assert.NotEmpty(rss.Channels);
var channel = rss.Channels.Single();
Assert.NotNull(channel.Title);
Assert.NotNull(channel.Description);
Assert.NotNull(channel.Image);
Assert.NotNull(channel.Guid);
Assert.NotEmpty(channel.Items);
foreach (var item in channel.Items)
{
Assert.NotNull(item.Id);
Assert.NotNull(item.Title);
Assert.NotNull(item.Link);
Assert.True(item.Duration.TotalSeconds > 0);
Assert.True(item.FileSize > 0);
Assert.NotNull(item.ContentType);
Assert.NotNull(item.PubDate);
}
}
}
}
@@ -21,7 +21,7 @@ namespace Podsync.Tests.Services.Builder
var linkService = new LinkService();
var client = new YouTubeClient(linkService, Options);
_builder = new YouTubeRssBuilder(linkService, client, _storageService.Object);
_builder = new YouTubeRssBuilder(client, _storageService.Object);
}
[Theory]
@@ -41,7 +41,7 @@ namespace Podsync.Tests.Services.Builder
_storageService.Setup(x => x.Load(feedId)).ReturnsAsync(feed);
var rss = await _builder.Query(new Uri("http://localhost:2020"), feedId);
var rss = await _builder.Query(feedId);
Assert.NotEmpty(rss.Channels);
@@ -58,10 +58,8 @@ namespace Podsync.Tests.Services.Builder
Assert.NotNull(item.Title);
Assert.NotNull(item.Link);
Assert.True(item.Duration.TotalSeconds > 0);
Assert.NotNull(item.Content);
Assert.True(item.Content.Length > 0);
Assert.NotNull(item.Content.MediaType);
Assert.NotNull(item.Content.Url);
Assert.True(item.FileSize > 0);
Assert.NotNull(item.ContentType);
Assert.NotNull(item.PubDate);
}
}
@@ -20,12 +20,9 @@ namespace Podsync.Tests.Services.Feed
Title = "Steve Gillespie - Getting Arrested (Stand up Comedy)",
Link = new Uri("https://youtube.com/watch?v=Jj22gfTnpAI"),
PubDate = DateTime.Parse("Mon, 07 Nov 2016 20:02:26 GMT"),
Content = new MediaContent
{
Url = new Uri("http://podsync.net/download/youtube/Jj22gfTnpAI.mp4"),
Length = 52850000,
MediaType = "video/mp4"
},
DownloadLink = new Uri("http://podsync.net/download/youtube/Jj22gfTnpAI.mp4"),
FileSize = 52850000,
ContentType = "video/mp4",
Duration = new TimeSpan(0, 0, 2, 31)
};
@@ -9,35 +9,38 @@ namespace Podsync.Tests.Services.Links
private readonly ILinkService _linkService = new LinkService();
[Theory]
[InlineData("http://youtu.be/jMeC7JFQ6801", Provider.YouTube, LinkType.Video, "jMeC7JFQ6801")]
[InlineData("http://www.youtube.com/embed/watch?feature=player_embedded&v=jMeC7JFQ6802", Provider.YouTube, LinkType.Video, "jMeC7JFQ6802")]
[InlineData("http://www.youtube.com/embed/watch?v=jMeC7JFQ6803", Provider.YouTube, LinkType.Video, "jMeC7JFQ6803")]
[InlineData("http://www.youtube.com/embed/v=jMeC7JFQ6804", Provider.YouTube, LinkType.Video, "jMeC7JFQ6804")]
[InlineData("http://www.youtube.com/watch?v=jMeC7JFQ6806", Provider.YouTube, LinkType.Video, "jMeC7JFQ6806")]
[InlineData("http://www.youtube.com/watch?v=jMeC7JFQ6807", Provider.YouTube, LinkType.Video, "jMeC7JFQ6807")]
[InlineData("http://www.youtu.be/jMeC7JFQ6808", Provider.YouTube, LinkType.Video, "jMeC7JFQ6808")]
[InlineData("http://youtu.be/jMeC7JFQ6809", Provider.YouTube, LinkType.Video, "jMeC7JFQ6809")]
[InlineData("http://www.youtube.com/watch?feature=player_embedded&v=jMeC7JFQ6805", Provider.YouTube, LinkType.Video, "jMeC7JFQ6805")]
[InlineData("http://www.youtube.com/attribution_link?u=/watch?v=jMeC7JFQ6815&feature=share&a=9QlmP1yvjcllp0h3l0NwuA", Provider.YouTube, LinkType.Video, "jMeC7JFQ6815")]
[InlineData("http://www.youtube.com/attribution_link?a=fF1CWYwxCQ4&u=/watch?v=jMeC7JFQ6816&feature=em-uploademail", Provider.YouTube, LinkType.Video, "jMeC7JFQ6816")]
[InlineData("http://www.youtube.com/attribution_link?a=fF1CWYwxCQ4&feature=em-uploademail&u=/watch?v=jMeC7JFQ6817", Provider.YouTube, LinkType.Video, "jMeC7JFQ6817")]
[InlineData("http://youtube.com/watch?v=jMeC7JFQ6810", Provider.YouTube, LinkType.Video, "jMeC7JFQ6810")]
[InlineData("http://www.youtube.com/watch/jMeC7JFQ6811", Provider.YouTube, LinkType.Video, "jMeC7JFQ6811")]
[InlineData("http://www.youtube.com/v/jMeC7JFQ6812", Provider.YouTube, LinkType.Video, "jMeC7JFQ6812")]
[InlineData("http://WWW.YOUTUBE.COM/v/jMeC7JFQ6812", Provider.YouTube, LinkType.Video, "jMeC7JFQ6812")]
[InlineData("https://www.youtube.com/playlist?list=PLCB9F975ECF01953C", Provider.YouTube, LinkType.Playlist, "PLCB9F975ECF01953C")]
[InlineData("https://www.youtube.com/watch?v=otm9NaT9OWU&list=PLCB9F975ECF01953C", Provider.YouTube, LinkType.Playlist, "PLCB9F975ECF01953C")]
[InlineData("https://www.youtube.com/channel/UC5XPnUk8Vvv_pWslhwom6Og", Provider.YouTube, LinkType.Channel, "UC5XPnUk8Vvv_pWslhwom6Og")]
[InlineData("https://www.youtube.com/user/UC5XPnUk8Vvv_pWslhwom6Og", Provider.YouTube, LinkType.User, "UC5XPnUk8Vvv_pWslhwom6Og")]
[InlineData("https://www.youtube.com/user/ComboBreakerVideo/videos", Provider.YouTube, LinkType.User, "ComboBreakerVideo")]
[InlineData("https://www.youtube.com/user/UC5XPnUk8Vvv_pWslhwom6Og/playlists", Provider.YouTube, LinkType.User, "UC5XPnUk8Vvv_pWslhwom6Og")]
public void ParseLinkTest(string link, Provider provider, LinkType linkType, string id)
[InlineData("https://www.youtube.com/playlist?list=PLCB9F975ECF01953C", LinkType.Playlist, "PLCB9F975ECF01953C")]
[InlineData("https://www.youtube.com/watch?v=otm9NaT9OWU&list=PLCB9F975ECF01953C", LinkType.Playlist, "PLCB9F975ECF01953C")]
[InlineData("https://www.youtube.com/channel/UC5XPnUk8Vvv_pWslhwom6Og", LinkType.Channel, "UC5XPnUk8Vvv_pWslhwom6Og")]
[InlineData("https://www.youtube.com/user/UC5XPnUk8Vvv_pWslhwom6Og", LinkType.User, "UC5XPnUk8Vvv_pWslhwom6Og")]
[InlineData("https://www.youtube.com/user/ComboBreakerVideo/videos", LinkType.User, "ComboBreakerVideo")]
[InlineData("https://www.youtube.com/user/UC5XPnUk8Vvv_pWslhwom6Og/playlists", LinkType.User, "UC5XPnUk8Vvv_pWslhwom6Og")]
[InlineData("https://www.youtube.com/playlist?list=PLP8qlV2aurYqdhyXW9ErqUW9Fw9F_mheM", LinkType.Playlist, "PLP8qlV2aurYqdhyXW9ErqUW9Fw9F_mheM")]
[InlineData("https://www.youtube.com/user/NEMAGIA/videos", LinkType.User, "NEMAGIA")]
public void ParseYoutubeLinks(string link, LinkType linkType, string id)
{
var info = _linkService.Parse(new Uri(link));
Assert.Equal(info.Id, id);
Assert.Equal(info.LinkType, linkType);
Assert.Equal(info.Provider, provider);
Assert.Equal(info.Provider, Provider.YouTube);
}
[Theory]
[InlineData("https://vimeo.com/groups/101", LinkType.Group, "101")]
[InlineData("http://vimeo.com/groups/102", LinkType.Group, "102")]
[InlineData("http://www.vimeo.com/groups/103", LinkType.Group, "103")]
[InlineData("https://vimeo.com/awhitelabelproduct", LinkType.User, "awhitelabelproduct")]
[InlineData("https://vimeo.com/groups/104/videos/", LinkType.Group, "104")]
[InlineData("https://vimeo.com/channels/staffpicks", LinkType.Channel, "staffpicks")]
[InlineData("https://vimeo.com/channels/staffpicks/146224925", LinkType.Channel, "staffpicks")]
public void ParseVimeoLinks(string link, LinkType linkType, string id)
{
var info = _linkService.Parse(new Uri(link));
Assert.Equal(info.Id, id);
Assert.Equal(info.LinkType, linkType);
Assert.Equal(info.Provider, Provider.Vimeo);
}
[Fact]
@@ -51,12 +54,9 @@ namespace Podsync.Tests.Services.Links
[InlineData(Provider.YouTube, LinkType.Channel, "123", "https://youtube.com/channel/123")]
[InlineData(Provider.YouTube, LinkType.Playlist, "213", "https://youtube.com/playlist?list=213")]
[InlineData(Provider.YouTube, LinkType.Video, "321", "https://youtube.com/watch?v=321")]
[InlineData(Provider.YouTube, LinkType.Info, "111", "https://youtube.com/get_video_info?video_id=111")]
[InlineData(Provider.Vimeo, LinkType.Category, "xyz", "https://vimeo.com/categories/xyz")]
[InlineData(Provider.Vimeo, LinkType.Channel, "yzx", "https://vimeo.com/channels/yzx")]
[InlineData(Provider.Vimeo, LinkType.Group, "zyd", "https://vimeo.com/groups/zyd")]
[InlineData(Provider.Vimeo, LinkType.User, "dfz", "https://vimeo.com/dfz")]
[InlineData(Provider.Vimeo, LinkType.Info, "xgd", "https://player.vimeo.com/video/xgd/config")]
[InlineData(Provider.Vimeo, LinkType.User, "123", "https://vimeo.com/user123")]
public void MakeLinkTest(Provider provider, LinkType linkType, string id, string expected)
{
var info = new LinkInfo
@@ -29,17 +29,11 @@ namespace Podsync.Tests.Services.Storage
{
const int idCount = 50;
try
{
var results = new string[idCount];
Parallel.For(0, results.Length, (i, _) => results[i] = _storage.MakeId().GetAwaiter().GetResult());
Assert.Equal(results.Length, results.Distinct().Count());
}
finally
{
_storage.ResetCounter();
}
var results = new string[idCount];
Parallel.For(0, results.Length, (i, _) => results[i] = _storage.MakeId().GetAwaiter().GetResult());
Assert.Equal(results.Length, results.Distinct().Count());
}
[Fact]
@@ -49,7 +43,9 @@ namespace Podsync.Tests.Services.Storage
{
Id = "123",
LinkType = LinkType.Channel,
Provider = Provider.Vimeo
Provider = Provider.Vimeo,
PageSize = 45
};
var id = await _storage.Save(feed);
@@ -61,6 +57,7 @@ namespace Podsync.Tests.Services.Storage
Assert.Equal(feed.Id, loaded.Id);
Assert.Equal(feed.LinkType, loaded.LinkType);
Assert.Equal(feed.Provider, loaded.Provider);
Assert.Equal(45, loaded.PageSize);
}
[Fact]
@@ -0,0 +1,92 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Podsync.Services.Videos.Vimeo;
using Xunit;
// ReSharper disable PossibleMultipleEnumeration
namespace Podsync.Tests.Services.Videos.Vimeo
{
public class VimeoClientTests : TestBase, IDisposable
{
private readonly VimeoClient _client;
public VimeoClientTests()
{
_client = new VimeoClient(Options);
}
[Fact]
public async Task ChannelTest()
{
var channel = await _client.Channel("staffpicks");
Assert.Equal(new Uri("https://vimeo.com/channels/staffpicks"), channel.Link);
Assert.Equal("Vimeo Staff Picks", channel.Name);
Assert.Equal("Vimeo Curation", channel.Author);
Assert.False(string.IsNullOrWhiteSpace(channel.Description));
}
[Fact]
public async Task GroupTest()
{
var group = await _client.Group("motion");
Assert.Equal(new Uri("https://vimeo.com/groups/motion"), group.Link);
Assert.Equal("Motion Graphic Artists", group.Name);
Assert.Equal("Danny Garcia", group.Author);
Assert.False(string.IsNullOrWhiteSpace(group.Description));
}
[Fact]
public async Task UserTest()
{
var user = await _client.User("motionarray");
Assert.Equal("Motion Array", user.Name);
Assert.False(string.IsNullOrWhiteSpace(user.Bio));
Assert.Equal(new Uri("https://vimeo.com/motionarray"), user.Link);
}
[Fact]
public async Task GroupVideosTest()
{
var videos = await _client.GroupVideos("motion", 101);
Assert.Equal(101, videos.Count());
ValidateCollection(videos);
}
[Fact]
public async Task UserVideosTest()
{
var videos = await _client.UserVideos("motionarray", 7);
Assert.Equal(7, videos.Count());
ValidateCollection(videos);
}
[Fact]
public async Task ChannelVideosTest()
{
var videos = await _client.ChannelVideos("staffpicks", 44);
Assert.Equal(44, videos.Count());
ValidateCollection(videos);
}
public void Dispose()
{
_client.Dispose();
}
private void ValidateCollection(IEnumerable<Video> videos)
{
foreach (var video in videos)
{
Assert.False(string.IsNullOrWhiteSpace(video.Title));
Assert.True(video.Duration.TotalSeconds > 1);
Assert.True(video.Size > 0);
Assert.NotNull(video.Link);
}
}
}
}