diff --git a/Podsync.sln b/Podsync.sln deleted file mode 100644 index 8d733f4..0000000 --- a/Podsync.sln +++ /dev/null @@ -1,62 +0,0 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 15 -VisualStudioVersion = 15.0.26403.7 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{3EF2294C-C807-46F3-B3F4-98B7F30688D0}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{15355284-4B88-4EF3-B580-20907F343E43}" - ProjectSection(SolutionItems) = preProject - .gitignore = .gitignore - docker-compose.yml = docker-compose.yml - EndProjectSection -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Podsync", "src\Podsync\Podsync.csproj", "{C9283472-E95E-4517-AE3B-3E276B9851E4}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{D94416F2-31EA-4D38-A3D0-BB74180687D4}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Podsync.Tests", "test\Podsync.Tests\Podsync.Tests.csproj", "{81ABAF5F-4CB6-42A6-8EAA-19EB9BAF66AA}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Debug|x64 = Debug|x64 - Debug|x86 = Debug|x86 - Release|Any CPU = Release|Any CPU - Release|x64 = Release|x64 - Release|x86 = Release|x86 - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {C9283472-E95E-4517-AE3B-3E276B9851E4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {C9283472-E95E-4517-AE3B-3E276B9851E4}.Debug|Any CPU.Build.0 = Debug|Any CPU - {C9283472-E95E-4517-AE3B-3E276B9851E4}.Debug|x64.ActiveCfg = Debug|Any CPU - {C9283472-E95E-4517-AE3B-3E276B9851E4}.Debug|x64.Build.0 = Debug|Any CPU - {C9283472-E95E-4517-AE3B-3E276B9851E4}.Debug|x86.ActiveCfg = Debug|Any CPU - {C9283472-E95E-4517-AE3B-3E276B9851E4}.Debug|x86.Build.0 = Debug|Any CPU - {C9283472-E95E-4517-AE3B-3E276B9851E4}.Release|Any CPU.ActiveCfg = Release|Any CPU - {C9283472-E95E-4517-AE3B-3E276B9851E4}.Release|Any CPU.Build.0 = Release|Any CPU - {C9283472-E95E-4517-AE3B-3E276B9851E4}.Release|x64.ActiveCfg = Release|Any CPU - {C9283472-E95E-4517-AE3B-3E276B9851E4}.Release|x64.Build.0 = Release|Any CPU - {C9283472-E95E-4517-AE3B-3E276B9851E4}.Release|x86.ActiveCfg = Release|Any CPU - {C9283472-E95E-4517-AE3B-3E276B9851E4}.Release|x86.Build.0 = Release|Any CPU - {81ABAF5F-4CB6-42A6-8EAA-19EB9BAF66AA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {81ABAF5F-4CB6-42A6-8EAA-19EB9BAF66AA}.Debug|Any CPU.Build.0 = Debug|Any CPU - {81ABAF5F-4CB6-42A6-8EAA-19EB9BAF66AA}.Debug|x64.ActiveCfg = Debug|Any CPU - {81ABAF5F-4CB6-42A6-8EAA-19EB9BAF66AA}.Debug|x64.Build.0 = Debug|Any CPU - {81ABAF5F-4CB6-42A6-8EAA-19EB9BAF66AA}.Debug|x86.ActiveCfg = Debug|Any CPU - {81ABAF5F-4CB6-42A6-8EAA-19EB9BAF66AA}.Debug|x86.Build.0 = Debug|Any CPU - {81ABAF5F-4CB6-42A6-8EAA-19EB9BAF66AA}.Release|Any CPU.ActiveCfg = Release|Any CPU - {81ABAF5F-4CB6-42A6-8EAA-19EB9BAF66AA}.Release|Any CPU.Build.0 = Release|Any CPU - {81ABAF5F-4CB6-42A6-8EAA-19EB9BAF66AA}.Release|x64.ActiveCfg = Release|Any CPU - {81ABAF5F-4CB6-42A6-8EAA-19EB9BAF66AA}.Release|x64.Build.0 = Release|Any CPU - {81ABAF5F-4CB6-42A6-8EAA-19EB9BAF66AA}.Release|x86.ActiveCfg = Release|Any CPU - {81ABAF5F-4CB6-42A6-8EAA-19EB9BAF66AA}.Release|x86.Build.0 = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(NestedProjects) = preSolution - {C9283472-E95E-4517-AE3B-3E276B9851E4} = {3EF2294C-C807-46F3-B3F4-98B7F30688D0} - {81ABAF5F-4CB6-42A6-8EAA-19EB9BAF66AA} = {D94416F2-31EA-4D38-A3D0-BB74180687D4} - EndGlobalSection -EndGlobal diff --git a/README.md b/README.md deleted file mode 100644 index ee4a9fd..0000000 --- a/README.md +++ /dev/null @@ -1,8 +0,0 @@ -# Podsync -Turn YouTube channels into podcast feeds - -## Redis configuration notes -- [Set overcommit_memory=1 on CoreOS](https://gist.github.com/PyYoshi/c7b271c8ba80ea350376) -- [Set somaxconn=1024 on CoreOS](https://gist.github.com/PyYoshi/55c6953b7e8267d81daf) -- [Tweak Redis](https://easyengine.io/tutorials/redis/) -- [Performance tips for Redis Cache Server](https://www.techandme.se/performance-tips-for-redis-cache-server/) diff --git a/src/Podsync/.bowerrc b/src/Podsync/.bowerrc deleted file mode 100644 index 6406626..0000000 --- a/src/Podsync/.bowerrc +++ /dev/null @@ -1,3 +0,0 @@ -{ - "directory": "wwwroot/lib" -} diff --git a/src/Podsync/Controllers/FeedController.cs b/src/Podsync/Controllers/FeedController.cs deleted file mode 100644 index f12e3c9..0000000 --- a/src/Podsync/Controllers/FeedController.cs +++ /dev/null @@ -1,104 +0,0 @@ -using System; -using System.Collections.Generic; -using System.ComponentModel.DataAnnotations; -using System.Linq; -using System.Security.Claims; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Mvc; -using Podsync.Helpers; -using Podsync.Services; -using Podsync.Services.Links; -using Podsync.Services.Rss; -using Podsync.Services.Rss.Contracts; -using Podsync.Services.Storage; - -namespace Podsync.Controllers -{ - [Route("feed")] - [ServiceFilter(typeof(HandleExceptionAttribute), IsReusable = true)] - public class FeedController : Controller - { - private static readonly IDictionary Extensions = new Dictionary - { - ["video/mp4"] = "mp4", - ["audio/mp4"] = "m4a" - }; - - private readonly ILinkService _linkService; - private readonly IFeedService _feedService; - - public FeedController(IRssBuilder rssBuilder, ILinkService linkService, IFeedService feedService) - { - _linkService = linkService; - _feedService = feedService; - } - - [HttpPost] - [Route("create")] - [ValidateModelState] - public async Task Create([FromBody] CreateFeedRequest request) - { - var linkInfo = _linkService.Parse(new Uri(request.Url)); - - var feed = new FeedMetadata - { - Provider = linkInfo.Provider, - LinkType = linkInfo.LinkType, - Id = linkInfo.Id, - Quality = request.Quality ?? Constants.DefaultFormat, - PageSize = request.PageSize ?? Constants.DefaultPageSize - }; - - // Check if user eligible for Patreon features - var allowFeatures = User.EnablePatreonFeatures(); - if (allowFeatures) - { - feed.PatreonId = User.GetClaim(ClaimTypes.NameIdentifier); - } - else - { - feed.Quality = Constants.DefaultFormat; - feed.PageSize = Constants.DefaultPageSize; - } - - var feedId = await _feedService.Create(feed); - var url = _linkService.Feed(Request.GetBaseUrl(), feedId); - - return url; - } - - [HttpGet] - [Route("~/{feedId:length(4, 6)}")] - [ValidateModelState] - public async Task Feed([Required] string feedId) - { - try - { - var body = await _feedService.Get(feedId, FixLinks); - return Content(body, "application/rss+xml; charset=UTF-8"); - } - catch (KeyNotFoundException) - { - return NotFound($"ERROR: No feed with id {feedId}"); - } - } - - [NonAction] - private void FixLinks(string feedId, Feed feed) - { - var selfHost = Request.GetBaseUrl(); - - // Set atom link to this feed - // See https://validator.w3.org/feed/docs/warning/MissingAtomSelfLink.html - var selfLink = new Uri(selfHost, Request.Path); - feed.Channels.ForEach(x => x.AtomLink = selfLink); - - // No magic here, just make download links to DownloadController.Download - feed.Channels.SelectMany(x => x.Items).ForEach(item => - { - var ext = Extensions[item.ContentType]; - item.DownloadLink = new Uri(selfHost, $"download/{feedId}/{item.Id}.{ext}"); - }); - } - } -} \ No newline at end of file diff --git a/src/Podsync/Controllers/HomeController.cs b/src/Podsync/Controllers/HomeController.cs deleted file mode 100644 index 8281577..0000000 --- a/src/Podsync/Controllers/HomeController.cs +++ /dev/null @@ -1,12 +0,0 @@ -using Microsoft.AspNetCore.Mvc; - -namespace Podsync.Controllers -{ - public class HomeController : Controller - { - public IActionResult Index() - { - return View(); - } - } -} diff --git a/src/Podsync/Controllers/StatusController.cs b/src/Podsync/Controllers/StatusController.cs deleted file mode 100644 index 31c0d6b..0000000 --- a/src/Podsync/Controllers/StatusController.cs +++ /dev/null @@ -1,45 +0,0 @@ -using System; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Mvc; -using Podsync.Services.Resolver; -using Podsync.Services.Storage; - -namespace Podsync.Controllers -{ - public class StatusController : Controller - { - private const string ErrorStatus = "ERROR"; - - private readonly IStorageService _storageService; - private readonly IResolverService _resolverService; - - public StatusController(IStorageService storageService, IResolverService resolverService) - { - _storageService = storageService; - _resolverService = resolverService; - } - - [HttpGet] - [Route("~/status")] - public async Task Index() - { - var storageStatus = ErrorStatus; - - try - { - var time = await _storageService.Ping(); - storageStatus = time.ToString(); - } - catch (Exception) - { - // Nothing to do - } - - var resolverStatus = _resolverService.Version ?? ErrorStatus; - - return $"Path: {Request.Path}\r\n" + - $"Redis: {storageStatus}\r\n" + - $"Resolve: {resolverStatus}"; - } - } -} \ No newline at end of file diff --git a/src/Podsync/Dockerfile b/src/Podsync/Dockerfile deleted file mode 100644 index 881213c..0000000 --- a/src/Podsync/Dockerfile +++ /dev/null @@ -1,13 +0,0 @@ -FROM microsoft/aspnetcore-build AS build -WORKDIR /workspace -COPY . . -RUN dotnet restore -RUN dotnet publish --configuration release --output ./bin/ - -FROM microsoft/aspnetcore:1.1 -ENV ASPNETCORE_URLS http://*:8080 -ENV ASPNETCORE_ENVIRONMENT Production -WORKDIR /app -COPY --from=build /workspace/bin/ . -EXPOSE 8080 -ENTRYPOINT ["dotnet", "Podsync.dll"] \ No newline at end of file diff --git a/src/Podsync/Helpers/EnumerableExtensions.cs b/src/Podsync/Helpers/EnumerableExtensions.cs deleted file mode 100644 index ac1233b..0000000 --- a/src/Podsync/Helpers/EnumerableExtensions.cs +++ /dev/null @@ -1,76 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; - -namespace Podsync.Helpers -{ - internal static class EnumerableExtensions - { - public static IEnumerable> Chunk(this IEnumerable source, int chunkSize) - { - if (chunkSize < 1) - { - throw new ArgumentException("Chunk size must be positive", nameof(chunkSize)); - } - - while (source.Any()) - { - yield return source.Take(chunkSize); - source = source.Skip(chunkSize); - } - } - - public static bool SafeAny(this IEnumerable source) - { - if (source == null) - { - return false; - } - - if (source.Any()) - { - return true; - } - - return false; - } - - public static void ForEach(this IEnumerable source, Action action) - { - if (source == null) - { - return; - } - - foreach (var item in source) - { - action(item); - } - } - - public static void AddTo(this IEnumerable collection, List target) - { - target.AddRange(collection); - } - - public static void SafeForEach(this IEnumerable source, Action action) - { - if (source == null) - { - return; - } - - foreach (var item in source) - { - try - { - action(item); - } - catch - { - // Nothing to do - } - } - } - } -} \ No newline at end of file diff --git a/src/Podsync/Helpers/Extensions.cs b/src/Podsync/Helpers/Extensions.cs deleted file mode 100644 index 46a17f6..0000000 --- a/src/Podsync/Helpers/Extensions.cs +++ /dev/null @@ -1,82 +0,0 @@ -using System; -using System.Linq; -using System.Security.Claims; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc; -using Podsync.Services; -using Podsync.Services.Resolver; - -namespace Podsync.Helpers -{ - public static class Extensions - { - /// - /// Generates a fully qualified URL to the specified content by using the specified content path. - /// Converts a virtual (relative) path to an application absolute path. - /// See http://stackoverflow.com/questions/30755827/getting-absolute-urls-using-asp-net-core-mvc-6 - /// - /// - /// - /// - public static string AbsoluteContent(this IUrlHelper url, string contentPath) - { - var request = url.ActionContext.HttpContext.Request; - var baseUri = new Uri($"{request.Scheme}://{request.Host.Value}"); - var fullUri = new Uri(baseUri, url.Content(contentPath)); - - return fullUri.ToString(); - } - - public static Uri GetBaseUrl(this HttpRequest request) - { - return new Uri($"{request.Scheme}://{request.Host}"); - } - - private const string OwnerId = "2822191"; - - /// - /// Check if user eligible for advanced features - /// - /// - /// - public static bool EnablePatreonFeatures(this ClaimsPrincipal user) - { - if (!user.Identity.IsAuthenticated) - { - return false; - } - - if (user.GetClaim(ClaimTypes.NameIdentifier) == OwnerId) - { - return true; - } - - const int MinAmountCents = 100; - - int amount; - if (int.TryParse(user.GetClaim(Constants.Patreon.AmountDonated), out amount)) - { - return amount >= MinAmountCents; - } - - return false; - } - - public static string GetName(this ClaimsPrincipal claimsPrincipal) - { - return claimsPrincipal.GetClaim(ClaimTypes.Name) - ?? claimsPrincipal.GetClaim(ClaimTypes.Email) - ?? "noname :("; - } - - public static string GetClaim(this ClaimsPrincipal claimsPrincipal, string type) - { - return claimsPrincipal.Claims.FirstOrDefault(x => x.Type == type)?.Value; - } - - public static bool IsAudio(this ResolveFormat format) - { - return format == ResolveFormat.AudioHigh || format == ResolveFormat.AudioLow; - } - } -} \ No newline at end of file diff --git a/src/Podsync/Helpers/HandleExceptionAttribute.cs b/src/Podsync/Helpers/HandleExceptionAttribute.cs deleted file mode 100644 index 81b04fe..0000000 --- a/src/Podsync/Helpers/HandleExceptionAttribute.cs +++ /dev/null @@ -1,34 +0,0 @@ -using System; -using System.Net; -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Mvc.Filters; -using Microsoft.Extensions.Logging; -using Podsync.Services; - -namespace Podsync.Helpers -{ - public class HandleExceptionAttribute : ExceptionFilterAttribute - { - private readonly ILogger _logger; - - public HandleExceptionAttribute(ILogger logger) - { - _logger = logger; - } - - public override void OnException(ExceptionContext context) - { - var exception = context.Exception; - if (exception is ArgumentNullException || exception is ArgumentException) - { - context.Result = new BadRequestObjectResult(exception.Message); - } - else - { - context.Result = new StatusCodeResult((int)HttpStatusCode.InternalServerError); - - _logger.LogCritical(Constants.Events.UnhandledError, context.Exception, "Unhandled exception"); - } - } - } -} \ No newline at end of file diff --git a/src/Podsync/Helpers/ServiceProviderExtensions.cs b/src/Podsync/Helpers/ServiceProviderExtensions.cs deleted file mode 100644 index 82eb002..0000000 --- a/src/Podsync/Helpers/ServiceProviderExtensions.cs +++ /dev/null @@ -1,73 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Reflection; -using Microsoft.Extensions.DependencyInjection; - -namespace Podsync.Helpers -{ - internal static class ServiceProviderExtensions - { - public static T CreateInstance(this IServiceProvider serviceProvider) - { - return serviceProvider.CreateInstance(typeof(T)); - } - - public static T CreateInstance(this IServiceProvider serviceProvider, Type implementation) - { - return (T)serviceProvider.CreateInstance(implementation); - } - - public static object CreateInstance(this IServiceProvider serviceProvider, Type type) - { - return ActivatorUtilities.CreateInstance(serviceProvider, type); - } - - public static IEnumerable 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 FindAllImplementationsOf(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 GetLoadableTypes(Assembly assembly) - { - try - { - return assembly.GetTypes(); - } - catch (ReflectionTypeLoadException e) - { - return e.Types.Where(x => x != null); - } - } - } -} \ No newline at end of file diff --git a/src/Podsync/Helpers/ValidateModelStateAttribute.cs b/src/Podsync/Helpers/ValidateModelStateAttribute.cs deleted file mode 100644 index f755e40..0000000 --- a/src/Podsync/Helpers/ValidateModelStateAttribute.cs +++ /dev/null @@ -1,28 +0,0 @@ -using System.Linq; -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Mvc.Filters; - -namespace Podsync.Helpers -{ - /// - /// Validate the model state prior to action execution. - /// See http://www.strathweb.com/2015/06/action-filters-service-filters-type-filters-asp-net-5-mvc-6/ - /// - public class ValidateModelStateAttribute : ActionFilterAttribute - { - private readonly bool _validateNotNull; - - public ValidateModelStateAttribute(bool validateNotNull = true) - { - _validateNotNull = validateNotNull; - } - - public override void OnActionExecuting(ActionExecutingContext context) - { - if (!context.ModelState.IsValid || _validateNotNull && context.ActionArguments.Any(p => p.Value == null)) - { - context.Result = new BadRequestObjectResult(context.ModelState); - } - } - } -} \ No newline at end of file diff --git a/src/Podsync/Podsync.csproj b/src/Podsync/Podsync.csproj deleted file mode 100644 index 1ea8dde..0000000 --- a/src/Podsync/Podsync.csproj +++ /dev/null @@ -1,90 +0,0 @@ - - - netcoreapp1.1 - true - Podsync - Exe - Podsync - aspnet-Podsync-20161004104901 - - - - - PreserveNewest - - - - - 1.25.0.760 - - - - - 1.1.2 - - - 1.1.2 - - - 1.1.2 - - - 1.1.2 - - - 1.1.3 - - - 1.1.2 - - - 1.1.2 - - - 1.1.2 - - - 1.1.2 - - - 1.1.2 - - - 1.1.2 - - - 1.1.2 - - - 1.1.2 - - - 1.1.2 - - - 1.1.2 - - - 1.1.2 - - - 1.1.1 - - - - 1.2.3 - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/src/Podsync/Program.cs b/src/Podsync/Program.cs deleted file mode 100644 index d9b3cb5..0000000 --- a/src/Podsync/Program.cs +++ /dev/null @@ -1,20 +0,0 @@ -using System.IO; -using Microsoft.AspNetCore.Hosting; - -namespace Podsync -{ - public class Program - { - public static void Main(string[] args) - { - var host = new WebHostBuilder() - .UseKestrel() - .UseContentRoot(Directory.GetCurrentDirectory()) - .UseIISIntegration() - .UseStartup() - .Build(); - - host.Run(); - } - } -} diff --git a/src/Podsync/Properties/launchSettings.json b/src/Podsync/Properties/launchSettings.json deleted file mode 100644 index 73787a2..0000000 --- a/src/Podsync/Properties/launchSettings.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "iisSettings": { - "windowsAuthentication": false, - "anonymousAuthentication": true, - "iisExpress": { - "applicationUrl": "http://localhost:56247/", - "sslPort": 0 - } - }, - "profiles": { - "IIS Express": { - "commandName": "IISExpress", - "launchBrowser": true, - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development" - } - }, - "Podsync": { - "commandName": "Project", - "launchBrowser": true, - "launchUrl": "http://localhost:5000", - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development" - } - } - } -} \ No newline at end of file diff --git a/src/Podsync/Services/Constants.cs b/src/Podsync/Services/Constants.cs deleted file mode 100644 index 6467b09..0000000 --- a/src/Podsync/Services/Constants.cs +++ /dev/null @@ -1,38 +0,0 @@ -using Podsync.Services.Resolver; - -namespace Podsync.Services -{ - public static class Constants - { - public const int DefaultPageSize = 50; - - public const ResolveFormat DefaultFormat = ResolveFormat.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); - } - - public static class Events - { - public const int RssError = 1; - - public const int YtdlError = 2; - - public const int UnhandledError = 3; - } - - public static class Cache - { - public const string VideosPrefix = "video_urls"; - - public const string FeedsPrefix = "feeds"; - } - } -} \ No newline at end of file diff --git a/src/Podsync/Services/CreateFeedRequest.cs b/src/Podsync/Services/CreateFeedRequest.cs deleted file mode 100644 index 058b2dc..0000000 --- a/src/Podsync/Services/CreateFeedRequest.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System.ComponentModel.DataAnnotations; -using Podsync.Services.Resolver; - -namespace Podsync.Services -{ - public class CreateFeedRequest - { - [Required] - [Url] - public string Url { get; set; } - - public ResolveFormat? Quality { get; set; } - - [Range(50, 150)] - public int? PageSize { get; set; } - } -} \ No newline at end of file diff --git a/src/Podsync/Services/Links/ILinkService.cs b/src/Podsync/Services/Links/ILinkService.cs deleted file mode 100644 index 06f9e7a..0000000 --- a/src/Podsync/Services/Links/ILinkService.cs +++ /dev/null @@ -1,13 +0,0 @@ -using System; - -namespace Podsync.Services.Links -{ - public interface ILinkService - { - LinkInfo Parse(Uri link); - - Uri Make(LinkInfo info); - - Uri Feed(Uri baseUrl, string feedId); - } -} \ No newline at end of file diff --git a/src/Podsync/Services/Links/LinkInfo.cs b/src/Podsync/Services/Links/LinkInfo.cs deleted file mode 100644 index 8f5e5d3..0000000 --- a/src/Podsync/Services/Links/LinkInfo.cs +++ /dev/null @@ -1,11 +0,0 @@ -namespace Podsync.Services.Links -{ - public struct LinkInfo - { - public string Id { get; set; } - - public LinkType LinkType { get; set; } - - public Provider Provider { get; set; } - } -} \ No newline at end of file diff --git a/src/Podsync/Services/Links/LinkService.cs b/src/Podsync/Services/Links/LinkService.cs deleted file mode 100644 index 45f7493..0000000 --- a/src/Podsync/Services/Links/LinkService.cs +++ /dev/null @@ -1,169 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Text.RegularExpressions; -using Microsoft.AspNetCore.WebUtilities; -using Microsoft.Extensions.Primitives; - -namespace Podsync.Services.Links -{ - public class LinkService : ILinkService - { - private static readonly IDictionary> LinkFormats = new Dictionary> - { - [Provider.YouTube] = new Dictionary - { - [LinkType.Video] = "https://youtube.com/watch?v={0}", - [LinkType.Channel] = "https://youtube.com/channel/{0}", - [LinkType.Playlist] = "https://youtube.com/playlist?list={0}" - }, - - [Provider.Vimeo] = new Dictionary - { - [LinkType.Channel] = "https://vimeo.com/channels/{0}", - [LinkType.Group] = "https://vimeo.com/groups/{0}", - [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 - https://www.youtube.com/channel/UCrlakW-ewUT8sOod6Wmzyow/videos - */ - - private static readonly Regex YouTubeRegex = new Regex(@"^(?:https?://)?(?:www\.|m\.)?(?:youtube.com/)(?user|channel|playlist|watch)/?(?[-\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/)(?groups|channels)?/?(?\w+)", RegexOptions.Compiled); - - public LinkInfo Parse(Uri link) - { - if (link == null) - { - throw new ArgumentNullException(nameof(link), "Link can't be null"); - } - - var provider = Provider.Unknown; - var linkType = LinkType.Unknown; - - var id = string.Empty; - - // YouTube - var match = YouTubeRegex.Match(link.AbsoluteUri); - if (match.Success) - { - provider = Provider.YouTube; - - var type = match.Groups["type"]?.ToString(); - if (type == "user") - { - // https://www.youtube.com/user/fxigr1 - - 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 - { - // Vimeo - match = VimeoRegex.Match(link.AbsoluteUri); - if (match.Success) - { - provider = Provider.Vimeo; - id = match.Groups["id"]?.ToString(); - - var type = match.Groups["type"]?.ToString(); - if (type == "groups") - { - // https://vimeo.com/groups/109 - - linkType = LinkType.Group; - } - else if (type == "channels") - { - // https://vimeo.com/channels/staffpicks - - linkType = LinkType.Channel; - } - else - { - // https://vimeo.com/awhitelabelproduct - - linkType = LinkType.User; - } - } - } - - if (string.IsNullOrWhiteSpace(id) || linkType == LinkType.Unknown || provider == Provider.Unknown) - { - throw new ArgumentException("Not supported provider or link format"); - } - - return new LinkInfo - { - Id = id, - LinkType = linkType, - Provider = provider - }; - } - - public Uri Make(LinkInfo info) - { - if (info.Id == null) - { - throw new ArgumentNullException(nameof(info.Id), "Id can't be empty"); - } - - try - { - var format = LinkFormats[info.Provider][info.LinkType]; - return new Uri(string.Format(format, info.Id)); - } - catch (KeyNotFoundException ex) - { - throw new ArgumentException("Unsupported provider or link type", nameof(info), ex); - } - } - - public Uri Feed(Uri baseUrl, string feedId) - { - return new Uri(baseUrl, feedId); - } - } -} \ No newline at end of file diff --git a/src/Podsync/Services/Links/LinkType.cs b/src/Podsync/Services/Links/LinkType.cs deleted file mode 100644 index c6e14a4..0000000 --- a/src/Podsync/Services/Links/LinkType.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace Podsync.Services.Links -{ - public enum LinkType - { - Unknown = 0, - Video, - Playlist, - User, - Channel, - Group - } -} \ No newline at end of file diff --git a/src/Podsync/Services/Links/Provider.cs b/src/Podsync/Services/Links/Provider.cs deleted file mode 100644 index 4d4f3b6..0000000 --- a/src/Podsync/Services/Links/Provider.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace Podsync.Services.Links -{ - public enum Provider - { - Unknown = 0, - YouTube, - Vimeo, - } -} \ No newline at end of file diff --git a/src/Podsync/Services/Patreon/Contracts/Pledge.cs b/src/Podsync/Services/Patreon/Contracts/Pledge.cs deleted file mode 100644 index 79433d8..0000000 --- a/src/Podsync/Services/Patreon/Contracts/Pledge.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System; - -namespace Podsync.Services.Patreon.Contracts -{ - public class Pledge - { - public string Id { get; set; } - - public DateTime CreatedAt { get; set; } - - public DateTime DeclinedSince { get; set; } - - public int AmountCents { get; set; } - - public int PledgeCapCents { get; set; } - } -} \ No newline at end of file diff --git a/src/Podsync/Services/Patreon/Contracts/User.cs b/src/Podsync/Services/Patreon/Contracts/User.cs deleted file mode 100644 index 0ffdc9c..0000000 --- a/src/Podsync/Services/Patreon/Contracts/User.cs +++ /dev/null @@ -1,23 +0,0 @@ -using System.Collections.Generic; -using System.Linq; - -namespace Podsync.Services.Patreon.Contracts -{ - public class User - { - public User() - { - Pledges = Enumerable.Empty(); - } - - public string Id { get; set; } - - public string Email { get; set; } - - public string Name { get; set; } - - public string Url { get; set; } - - public IEnumerable Pledges { get; set; } - } -} \ No newline at end of file diff --git a/src/Podsync/Services/Patreon/IPatreonApi.cs b/src/Podsync/Services/Patreon/IPatreonApi.cs deleted file mode 100644 index bbdd221..0000000 --- a/src/Podsync/Services/Patreon/IPatreonApi.cs +++ /dev/null @@ -1,10 +0,0 @@ -using System; -using System.Threading.Tasks; - -namespace Podsync.Services.Patreon -{ - public interface IPatreonApi : IDisposable - { - Task FetchProfile(Tokens tokens); - } -} \ No newline at end of file diff --git a/src/Podsync/Services/Patreon/PatreonApi.cs b/src/Podsync/Services/Patreon/PatreonApi.cs deleted file mode 100644 index a297d42..0000000 --- a/src/Podsync/Services/Patreon/PatreonApi.cs +++ /dev/null @@ -1,46 +0,0 @@ -using System; -using System.Net.Http; -using System.Net.Http.Headers; -using System.Threading.Tasks; -using Newtonsoft.Json.Linq; - -namespace Podsync.Services.Patreon -{ - public sealed class PatreonApi : IPatreonApi - { - private readonly HttpClient _client; - - public PatreonApi() - { - _client = new HttpClient - { - BaseAddress = new Uri("https://api.patreon.com/oauth2/api/") - }; - - _client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer"); - } - - public Task FetchProfile(Tokens tokens) - { - return Query("current_user", tokens); - } - - public void Dispose() - { - _client.Dispose(); - } - - private async Task Query(string path, Tokens tokens) - { - var request = new HttpRequestMessage(HttpMethod.Get, path); - request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", tokens.AccessToken); - - var response = await _client.SendAsync(request); - response.EnsureSuccessStatusCode(); - - var json = await response.Content.ReadAsStringAsync(); - - return JObject.Parse(json); - } - } -} \ No newline at end of file diff --git a/src/Podsync/Services/Patreon/PatreonExtensions.cs b/src/Podsync/Services/Patreon/PatreonExtensions.cs deleted file mode 100644 index 12011e3..0000000 --- a/src/Podsync/Services/Patreon/PatreonExtensions.cs +++ /dev/null @@ -1,72 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using Podsync.Services.Patreon.Contracts; - -namespace Podsync.Services.Patreon -{ - public static class PatreonExtensions - { - public static async Task FetchUserAndPledges(this IPatreonApi api, Tokens tokens) - { - var resp = await api.FetchProfile(tokens); - - dynamic userAttrs = resp.data.attributes; - - var user = new User - { - Id = resp.data.id, - Email = userAttrs.email, - Name = userAttrs.first_name ?? userAttrs.full_name, - Url = userAttrs.url, - Pledges = ParsePledges(resp) - }; - - return user; - } - - private static IEnumerable ParsePledges(dynamic resp) - { - dynamic pledges = resp.data.relationships.pledges.data; - - foreach (var pledge in pledges) - { - var id = pledge.id; - var type = pledge.type; - - foreach (var include in resp.included) - { - if (include.id == id && include.type == type) - { - dynamic attrs = include.attributes; - - yield return new Pledge - { - Id = include.id, - CreatedAt = ParseDate(attrs.created_at), - DeclinedSince = ParseDate(attrs.declined_since), - AmountCents = attrs.amount_cents, - PledgeCapCents = attrs.pledge_cap_cents - }; - - break; - } - } - } - } - - private static DateTime ParseDate(object obj) - { - var date = obj?.ToString(); - - if (string.IsNullOrWhiteSpace(date)) - { - return DateTime.MinValue; - } - - var dateTime = DateTime.Parse(date); - - return DateTime.SpecifyKind(dateTime, DateTimeKind.Utc); - } - } -} \ No newline at end of file diff --git a/src/Podsync/Services/Patreon/Tokens.cs b/src/Podsync/Services/Patreon/Tokens.cs deleted file mode 100644 index 20c60c5..0000000 --- a/src/Podsync/Services/Patreon/Tokens.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace Podsync.Services.Patreon -{ - public struct Tokens - { - public string AccessToken { get; set; } - - public string RefreshToken { get; set; } - } -} \ No newline at end of file diff --git a/src/Podsync/Services/PodsyncConfiguration.cs b/src/Podsync/Services/PodsyncConfiguration.cs deleted file mode 100644 index cd81bdd..0000000 --- a/src/Podsync/Services/PodsyncConfiguration.cs +++ /dev/null @@ -1,21 +0,0 @@ -using Podsync.Services.Patreon; - -namespace Podsync.Services -{ - public class PodsyncConfiguration - { - public string YouTubeApiKey { get; set; } - - public string VimeoApiKey { get; set; } - - public string RedisConnectionString { get; set; } - - public string PatreonClientId { get; set; } - - public string PatreonSecret { get; set; } - - public Tokens CreatorTokens { get; set; } - - public string RemoteResolverUrl { get; set; } - } -} \ No newline at end of file diff --git a/src/Podsync/Services/Resolver/CachedResolver.cs b/src/Podsync/Services/Resolver/CachedResolver.cs deleted file mode 100644 index bc38dc5..0000000 --- a/src/Podsync/Services/Resolver/CachedResolver.cs +++ /dev/null @@ -1,39 +0,0 @@ -using System; -using System.Threading.Tasks; -using Podsync.Services.Storage; - -namespace Podsync.Services.Resolver -{ - public abstract class CachedResolver : IResolverService - { - private readonly TimeSpan UrlExpiration = TimeSpan.FromHours(3); - private readonly IStorageService _storageService; - - protected CachedResolver(IStorageService storageService) - { - _storageService = storageService; - } - - public abstract string Version { get; } - - public async Task Resolve(Uri videoUrl, ResolveFormat format) - { - var id = videoUrl.GetHashCode().ToString(); - - // Check if this video URL was resolved within last 3 hours - var value = await _storageService.GetCached(Constants.Cache.VideosPrefix, id); - if (!string.IsNullOrWhiteSpace(value)) - { - return new Uri(value); - } - - // Resolve and save to cache - var uri = await ResolveInternal(videoUrl, format); - await _storageService.Cache(Constants.Cache.VideosPrefix, id, uri.ToString(), UrlExpiration); - - return uri; - } - - protected abstract Task ResolveInternal(Uri videoUrl, ResolveFormat format); - } -} \ No newline at end of file diff --git a/src/Podsync/Services/Resolver/IResolverService.cs b/src/Podsync/Services/Resolver/IResolverService.cs deleted file mode 100644 index c45094c..0000000 --- a/src/Podsync/Services/Resolver/IResolverService.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System; -using System.Threading.Tasks; - -namespace Podsync.Services.Resolver -{ - public interface IResolverService - { - string Version { get; } - - Task Resolve(Uri videoUrl, ResolveFormat format = ResolveFormat.VideoHigh); - } -} \ No newline at end of file diff --git a/src/Podsync/Services/Resolver/RemoteResolver.cs b/src/Podsync/Services/Resolver/RemoteResolver.cs deleted file mode 100644 index 56280a2..0000000 --- a/src/Podsync/Services/Resolver/RemoteResolver.cs +++ /dev/null @@ -1,42 +0,0 @@ -using System; -using System.Net.Http; -using System.Threading.Tasks; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; -using Podsync.Services.Storage; - -namespace Podsync.Services.Resolver -{ - public class RemoteResolver : CachedResolver - { - private readonly ILogger _logger; - private readonly HttpClient _client = new HttpClient(); - - public RemoteResolver(IStorageService storageService, IOptions options, ILogger logger) : base(storageService) - { - _logger = logger; - _client.BaseAddress = new Uri(options.Value.RemoteResolverUrl); - - _logger.LogInformation($"Remote resolver URL: {_client.BaseAddress}"); - } - - public override string Version { get; } = "Remote"; - - protected override async Task ResolveInternal(Uri videoUrl, ResolveFormat format) - { - using(var response = await _client.GetAsync($"/resolve?url={videoUrl}&quality={format}")) - { - using(response.Content) - { - var body = await response.Content.ReadAsStringAsync(); - if (!response.IsSuccessStatusCode) - { - throw new HttpRequestException(body); - } - - return new Uri(body); - } - } - } - } -} \ No newline at end of file diff --git a/src/Podsync/Services/Resolver/ResolveFormat.cs b/src/Podsync/Services/Resolver/ResolveFormat.cs deleted file mode 100644 index cfaf76c..0000000 --- a/src/Podsync/Services/Resolver/ResolveFormat.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace Podsync.Services.Resolver -{ - public enum ResolveFormat - { - VideoHigh = 0, - VideoLow, - AudioHigh, - AudioLow - } -} \ No newline at end of file diff --git a/src/Podsync/Services/Rss/Builders/CompositeRssBuilder.cs b/src/Podsync/Services/Rss/Builders/CompositeRssBuilder.cs deleted file mode 100644 index 49a473b..0000000 --- a/src/Podsync/Services/Rss/Builders/CompositeRssBuilder.cs +++ /dev/null @@ -1,54 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Collections.ObjectModel; -using System.Linq; -using System.Reflection; -using System.Threading.Tasks; -using Microsoft.Extensions.Logging; -using Podsync.Helpers; -using Podsync.Services.Links; -using Podsync.Services.Storage; - -namespace Podsync.Services.Rss.Builders -{ - // ReSharper disable once ClassNeverInstantiated.Global - public class CompositeRssBuilder : RssBuilderBase - { - private readonly IDictionary _builders; - private readonly ILogger _logger; - - public CompositeRssBuilder(IServiceProvider serviceProvider, IStorageService storageService, ILogger logger) : base(storageService) - { - _logger = logger; - - // 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); - - _logger.LogInformation($"Found {builders.Count} RSS builders"); - _builders = new ReadOnlyDictionary(builders); - } - - public override Provider Provider => throw new NotSupportedException(); - - public override Task Query(FeedMetadata feed) - { - try - { - IRssBuilder builder; - if (_builders.TryGetValue(feed.Provider, out builder)) - { - return builder.Query(feed); - } - - throw new NotSupportedException("Not supported provider"); - } - catch (Exception ex) - { - _logger.LogError(Constants.Events.RssError, ex, "Failed to query RSS feed (id: {ID})", feed.Id); - - throw; - } - } - } -} \ No newline at end of file diff --git a/src/Podsync/Services/Rss/Builders/RssBuilderBase.cs b/src/Podsync/Services/Rss/Builders/RssBuilderBase.cs deleted file mode 100644 index b6289f7..0000000 --- a/src/Podsync/Services/Rss/Builders/RssBuilderBase.cs +++ /dev/null @@ -1,28 +0,0 @@ -using System.Threading.Tasks; -using Podsync.Services.Links; -using Podsync.Services.Rss.Contracts; -using Podsync.Services.Storage; - -namespace Podsync.Services.Rss.Builders -{ - public abstract class RssBuilderBase : IRssBuilder - { - private readonly IStorageService _storageService; - - protected RssBuilderBase(IStorageService storageService) - { - _storageService = storageService; - } - - public abstract Provider Provider { get; } - - public async Task Query(string feedId) - { - var metadata = await _storageService.Load(feedId); - - return await Query(metadata); - } - - public abstract Task Query(FeedMetadata metadata); - } -} \ No newline at end of file diff --git a/src/Podsync/Services/Rss/Builders/VimeoRssBuilder.cs b/src/Podsync/Services/Rss/Builders/VimeoRssBuilder.cs deleted file mode 100644 index 6e33548..0000000 --- a/src/Podsync/Services/Rss/Builders/VimeoRssBuilder.cs +++ /dev/null @@ -1,114 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Podsync.Services.Links; -using Podsync.Services.Rss.Contracts; -using Podsync.Services.Storage; -using Podsync.Services.Videos.Vimeo; - -namespace Podsync.Services.Rss.Builders -{ - // 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 Feed - { - 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