Generate download links inside controller action

This commit is contained in:
Maksym Pavlenko
2017-01-06 09:44:19 -08:00
parent c9124cd25e
commit 001cdf4e0a
13 changed files with 87 additions and 81 deletions
+18 -2
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,6 +23,12 @@ namespace Podsync.Controllers
[HandleException]
public class FeedController : Controller
{
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));
private readonly IRssBuilder _rssBuilder;
@@ -94,18 +101,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())
@@ -30,12 +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)
{
IRssBuilder builder;
if (_builders.TryGetValue(feed.Provider, out builder))
{
return builder.Query(baseUrl, feedId, feed);
return builder.Query(feed);
}
throw new NotSupportedException("Not supported provider");
+2 -2
View File
@@ -10,8 +10,8 @@ namespace Podsync.Services.Builder
{
Provider Provider { get; }
Task<Rss> Query(Uri baseUrl, string feedId);
Task<Rss> Query(string feedId);
Task<Rss> Query(Uri baseUrl, string feedId, FeedMetadata metadata);
Task<Rss> Query(FeedMetadata metadata);
}
}
@@ -17,13 +17,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);
}
}
@@ -21,7 +21,7 @@ namespace Podsync.Services.Builder
public override Provider Provider { get; } = Provider.Vimeo;
public override async Task<Rss> Query(Uri baseUrl, string feedId, FeedMetadata metadata)
public override async Task<Rss> Query(FeedMetadata metadata)
{
var linkType = metadata.LinkType;
@@ -94,17 +94,15 @@ namespace Podsync.Services.Builder
{
return new Item
{
Id = video.Id,
Title = video.Title,
Description = video.Description,
PubDate = video.CreatedAt,
Link = video.Link,
Duration = video.Duration,
Content = new MediaContent
{
Length = video.Size,
MediaType = "video/mp4",
Url = null
}
FileSize = video.Size,
ContentType = "video/mp4",
Author = video.Author
};
}
@@ -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));
var rss = new Rss
{
@@ -102,46 +100,36 @@ namespace Podsync.Services.Builder
};
}
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");
}
}
}
+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; }
}
}
@@ -4,6 +4,8 @@ namespace Podsync.Services.Videos.Vimeo
{
public class Video
{
public string Id { get; set; }
public string Title { get; set; }
public string Description { get; set; }
@@ -17,5 +19,7 @@ namespace Podsync.Services.Videos.Vimeo
public long Size { get; set; }
public TimeSpan Duration { get; set; }
public string Author { get; set; }
}
}
@@ -101,15 +101,21 @@ namespace Podsync.Services.Videos.Vimeo
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
Size = size,
Author = v.user.name
};
output.Add(video);
@@ -38,7 +38,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);
@@ -52,12 +52,12 @@ namespace Podsync.Tests.Services.Builder
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.NotNull(item.Content);
Assert.True(item.Content.Length > 0);
Assert.NotNull(item.Content.MediaType);
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)
};