mirror of
https://github.com/mxpv/podsync.git
synced 2024-05-11 05:55:04 +00:00
Merge branch 'vimeo'
This commit is contained in:
@@ -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())
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
/*
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 = "/" });
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -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>
|
||||
@@ -9,6 +9,7 @@
|
||||
},
|
||||
"Podsync": {
|
||||
"YouTubeApiKey": "",
|
||||
"VimeoApiKey": "",
|
||||
"RedisConnectionString": "localhost",
|
||||
"BaseUrl": "",
|
||||
"PatreonClientId": "",
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user