diff --git a/src/Podsync/Controllers/FeedController.cs b/src/Podsync/Controllers/FeedController.cs index 87a03e6..bd634f9 100644 --- a/src/Podsync/Controllers/FeedController.cs +++ b/src/Podsync/Controllers/FeedController.cs @@ -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 Extensions = new Dictionary + { + ["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()) diff --git a/src/Podsync/Helpers/Extensions.cs b/src/Podsync/Helpers/Extensions.cs index 90c99ed..7e175ad 100644 --- a/src/Podsync/Helpers/Extensions.cs +++ b/src/Podsync/Helpers/Extensions.cs @@ -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; } diff --git a/src/Podsync/Services/Builder/CompositeRssBuilder.cs b/src/Podsync/Services/Builder/CompositeRssBuilder.cs index 77856a2..19add99 100644 --- a/src/Podsync/Services/Builder/CompositeRssBuilder.cs +++ b/src/Podsync/Services/Builder/CompositeRssBuilder.cs @@ -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 _builders; public CompositeRssBuilder(IServiceProvider serviceProvider, IStorageService storageService) : base(storageService) { - _youTubeBuilder = serviceProvider.CreateInstance(); + // Find all RSS builders (all implementations of IRssBuilder), create instances and make dictionary for fast search by Provider type + var buildTypes = serviceProvider.FindAllImplementationsOf(Assembly.GetEntryAssembly()).Where(x => x != typeof(CompositeRssBuilder)); + var builders = buildTypes.Select(builderType => (IRssBuilder)serviceProvider.CreateInstance(builderType)).ToDictionary(builder => builder.Provider); + + _builders = new ReadOnlyDictionary(builders); } public override Provider Provider @@ -21,11 +30,12 @@ namespace Podsync.Services.Builder get { throw new NotSupportedException(); } } - public override Task Query(Uri baseUrl, string feedId, FeedMetadata feed) + public override Task 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"); diff --git a/src/Podsync/Services/Builder/IRssBuilder.cs b/src/Podsync/Services/Builder/IRssBuilder.cs index deb6004..7d09f4d 100644 --- a/src/Podsync/Services/Builder/IRssBuilder.cs +++ b/src/Podsync/Services/Builder/IRssBuilder.cs @@ -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 Query(Uri baseUrl, string feedId); + Provider Provider { get; } + + Task Query(string feedId); + + Task Query(FeedMetadata metadata); } } \ No newline at end of file diff --git a/src/Podsync/Services/Builder/RssBuilderBase.cs b/src/Podsync/Services/Builder/RssBuilderBase.cs index a3279d3..ec82e0d 100644 --- a/src/Podsync/Services/Builder/RssBuilderBase.cs +++ b/src/Podsync/Services/Builder/RssBuilderBase.cs @@ -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 Query(Uri baseUrl, string feedId) + public async Task Query(string feedId) { var metadata = await _storageService.Load(feedId); - return await Query(baseUrl, feedId, metadata); + return await Query(metadata); } - public abstract Task Query(Uri baseUrl, string feedId, FeedMetadata metadata); + public abstract Task Query(FeedMetadata metadata); } } \ No newline at end of file diff --git a/src/Podsync/Services/Builder/VimeoRssBuilder.cs b/src/Podsync/Services/Builder/VimeoRssBuilder.cs new file mode 100644 index 0000000..d9ea5cc --- /dev/null +++ b/src/Podsync/Services/Builder/VimeoRssBuilder.cs @@ -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 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