mirror of
https://github.com/mxpv/podsync.git
synced 2024-05-11 05:55:04 +00:00
Add Patreon integration
This commit is contained in:
@@ -23,6 +23,7 @@ namespace Podsync.Controllers
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Main video download endpoint, don't forget to reflect any changes in LinkService.Download
|
// Main video download endpoint, don't forget to reflect any changes in LinkService.Download
|
||||||
|
[HttpGet]
|
||||||
[Route("{feedId}/{videoId}/")]
|
[Route("{feedId}/{videoId}/")]
|
||||||
public async Task<IActionResult> Download(string feedId, string videoId)
|
public async Task<IActionResult> Download(string feedId, string videoId)
|
||||||
{
|
{
|
||||||
@@ -31,7 +32,7 @@ namespace Podsync.Controllers
|
|||||||
var url = _linkService.Make(new LinkInfo
|
var url = _linkService.Make(new LinkInfo
|
||||||
{
|
{
|
||||||
Provider = metadata.Provider,
|
Provider = metadata.Provider,
|
||||||
LinkType = metadata.LinkType,
|
LinkType = LinkType.Video,
|
||||||
Id = videoId
|
Id = videoId
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@@ -50,6 +50,12 @@ namespace Podsync.Controllers
|
|||||||
PageSize = request.PageSize ?? DefaultPageSize
|
PageSize = request.PageSize ?? DefaultPageSize
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (!User.EnablePatreonFeatures())
|
||||||
|
{
|
||||||
|
feed.Quality = ResolveType.VideoHigh;
|
||||||
|
feed.PageSize = DefaultPageSize;
|
||||||
|
}
|
||||||
|
|
||||||
return _storageService.Save(feed);
|
return _storageService.Save(feed);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
46
src/Podsync/Helpers/UserExtensions.cs
Normal file
46
src/Podsync/Helpers/UserExtensions.cs
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
using System.Linq;
|
||||||
|
using System.Security.Claims;
|
||||||
|
using Podsync.Services.Patreon;
|
||||||
|
|
||||||
|
namespace Podsync.Helpers
|
||||||
|
{
|
||||||
|
public static class UserExtensions
|
||||||
|
{
|
||||||
|
private const string OwnerId = "2822191";
|
||||||
|
|
||||||
|
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(PatreonConstants.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 :(";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string GetClaim(this ClaimsPrincipal claimsPrincipal, string type)
|
||||||
|
{
|
||||||
|
return claimsPrincipal.Claims.FirstOrDefault(x => x.Type == type)?.Value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -21,11 +21,11 @@ namespace Podsync.Services.Builder
|
|||||||
get { throw new NotSupportedException(); }
|
get { throw new NotSupportedException(); }
|
||||||
}
|
}
|
||||||
|
|
||||||
public override Task<Rss> Query(FeedMetadata feed)
|
public override Task<Rss> Query(string feedId, FeedMetadata feed)
|
||||||
{
|
{
|
||||||
if (feed.Provider == Provider.YouTube)
|
if (feed.Provider == Provider.YouTube)
|
||||||
{
|
{
|
||||||
return _youTubeBuilder.Query(feed);
|
return _youTubeBuilder.Query(feedId, feed);
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new NotSupportedException("Not supported provider");
|
throw new NotSupportedException("Not supported provider");
|
||||||
|
@@ -8,6 +8,6 @@ namespace Podsync.Services.Builder
|
|||||||
{
|
{
|
||||||
Task<Rss> Query(string feedId);
|
Task<Rss> Query(string feedId);
|
||||||
|
|
||||||
Task<Rss> Query(FeedMetadata feed);
|
Task<Rss> Query(string feedId, FeedMetadata feed);
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -20,9 +20,9 @@ namespace Podsync.Services.Builder
|
|||||||
{
|
{
|
||||||
var metadata = await _storageService.Load(feedId);
|
var metadata = await _storageService.Load(feedId);
|
||||||
|
|
||||||
return await Query(metadata);
|
return await Query(feedId, metadata);
|
||||||
}
|
}
|
||||||
|
|
||||||
public abstract Task<Rss> Query(FeedMetadata metadata);
|
public abstract Task<Rss> Query(string feedId, FeedMetadata metadata);
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -24,7 +24,7 @@ namespace Podsync.Services.Builder
|
|||||||
|
|
||||||
public override Provider Provider { get; } = Provider.YouTube;
|
public override Provider Provider { get; } = Provider.YouTube;
|
||||||
|
|
||||||
public override async Task<Rss> Query(FeedMetadata metadata)
|
public override async Task<Rss> Query(string feedId, FeedMetadata metadata)
|
||||||
{
|
{
|
||||||
if (metadata.Provider != Provider.YouTube)
|
if (metadata.Provider != Provider.YouTube)
|
||||||
{
|
{
|
||||||
@@ -57,7 +57,7 @@ namespace Podsync.Services.Builder
|
|||||||
// Get video descriptions
|
// Get video descriptions
|
||||||
var videos = await _youTube.GetVideos(new VideoQuery { Id = string.Join(",", ids) });
|
var videos = await _youTube.GetVideos(new VideoQuery { Id = string.Join(",", ids) });
|
||||||
|
|
||||||
channel.Items = videos.Select(x => MakeItem(x, metadata));
|
channel.Items = videos.Select(x => MakeItem(x, feedId, metadata));
|
||||||
|
|
||||||
var rss = new Rss
|
var rss = new Rss
|
||||||
{
|
{
|
||||||
@@ -103,7 +103,7 @@ namespace Podsync.Services.Builder
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private Item MakeItem(Video video, FeedMetadata feed)
|
private Item MakeItem(Video video, string feedId, FeedMetadata feed)
|
||||||
{
|
{
|
||||||
return new Item
|
return new Item
|
||||||
{
|
{
|
||||||
@@ -116,7 +116,7 @@ namespace Podsync.Services.Builder
|
|||||||
{
|
{
|
||||||
Length = video.Size,
|
Length = video.Size,
|
||||||
MediaType = SelectMediaType(feed.Quality),
|
MediaType = SelectMediaType(feed.Quality),
|
||||||
Url = _linkService.Download(feed.Id, video.VideoId)
|
Url = _linkService.Download(feedId, video.VideoId)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
17
src/Podsync/Services/Patreon/Contracts/Pledge.cs
Normal file
17
src/Podsync/Services/Patreon/Contracts/Pledge.cs
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
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; }
|
||||||
|
}
|
||||||
|
}
|
23
src/Podsync/Services/Patreon/Contracts/User.cs
Normal file
23
src/Podsync/Services/Patreon/Contracts/User.cs
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
|
namespace Podsync.Services.Patreon.Contracts
|
||||||
|
{
|
||||||
|
public class User
|
||||||
|
{
|
||||||
|
public User()
|
||||||
|
{
|
||||||
|
Pledges = Enumerable.Empty<Pledge>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public string Id { get; set; }
|
||||||
|
|
||||||
|
public string Email { get; set; }
|
||||||
|
|
||||||
|
public string Name { get; set; }
|
||||||
|
|
||||||
|
public string Url { get; set; }
|
||||||
|
|
||||||
|
public IEnumerable<Pledge> Pledges { get; set; }
|
||||||
|
}
|
||||||
|
}
|
10
src/Podsync/Services/Patreon/IPatreonApi.cs
Normal file
10
src/Podsync/Services/Patreon/IPatreonApi.cs
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
using System;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Podsync.Services.Patreon
|
||||||
|
{
|
||||||
|
public interface IPatreonApi : IDisposable
|
||||||
|
{
|
||||||
|
Task<dynamic> FetchProfile(Tokens tokens);
|
||||||
|
}
|
||||||
|
}
|
46
src/Podsync/Services/Patreon/PatreonApi.cs
Normal file
46
src/Podsync/Services/Patreon/PatreonApi.cs
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
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<dynamic> FetchProfile(Tokens tokens)
|
||||||
|
{
|
||||||
|
return Query("current_user", tokens);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
_client.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<dynamic> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
13
src/Podsync/Services/Patreon/PatreonConstants.cs
Normal file
13
src/Podsync/Services/Patreon/PatreonConstants.cs
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
72
src/Podsync/Services/Patreon/PatreonExtensions.cs
Normal file
72
src/Podsync/Services/Patreon/PatreonExtensions.cs
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
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<User> 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<Pledge> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
9
src/Podsync/Services/Patreon/Tokens.cs
Normal file
9
src/Podsync/Services/Patreon/Tokens.cs
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
namespace Podsync.Services.Patreon
|
||||||
|
{
|
||||||
|
public struct Tokens
|
||||||
|
{
|
||||||
|
public string AccessToken { get; set; }
|
||||||
|
|
||||||
|
public string RefreshToken { get; set; }
|
||||||
|
}
|
||||||
|
}
|
@@ -1,4 +1,6 @@
|
|||||||
namespace Podsync.Services
|
using Podsync.Services.Patreon;
|
||||||
|
|
||||||
|
namespace Podsync.Services
|
||||||
{
|
{
|
||||||
public class PodsyncConfiguration
|
public class PodsyncConfiguration
|
||||||
{
|
{
|
||||||
@@ -7,5 +9,11 @@
|
|||||||
public string RedisConnectionString { get; set; }
|
public string RedisConnectionString { get; set; }
|
||||||
|
|
||||||
public string BaseUrl { get; set; }
|
public string BaseUrl { get; set; }
|
||||||
|
|
||||||
|
public string PatreonClientId { get; set; }
|
||||||
|
|
||||||
|
public string PatreonSecret { get; set; }
|
||||||
|
|
||||||
|
public Tokens CreatorTokens { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -1,11 +1,18 @@
|
|||||||
using Microsoft.AspNetCore.Builder;
|
using System.Linq;
|
||||||
|
using System.Security.Claims;
|
||||||
|
using Microsoft.AspNetCore.Authentication.Cookies;
|
||||||
|
using Microsoft.AspNetCore.Authentication.OAuth;
|
||||||
|
using Microsoft.AspNetCore.Builder;
|
||||||
using Microsoft.AspNetCore.Hosting;
|
using Microsoft.AspNetCore.Hosting;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Microsoft.AspNetCore.Http.Authentication;
|
||||||
using Microsoft.Extensions.Configuration;
|
using Microsoft.Extensions.Configuration;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Podsync.Services;
|
using Podsync.Services;
|
||||||
using Podsync.Services.Builder;
|
using Podsync.Services.Builder;
|
||||||
using Podsync.Services.Links;
|
using Podsync.Services.Links;
|
||||||
|
using Podsync.Services.Patreon;
|
||||||
using Podsync.Services.Resolver;
|
using Podsync.Services.Resolver;
|
||||||
using Podsync.Services.Storage;
|
using Podsync.Services.Storage;
|
||||||
using Podsync.Services.Videos.YouTube;
|
using Podsync.Services.Videos.YouTube;
|
||||||
@@ -43,8 +50,12 @@ namespace Podsync
|
|||||||
services.AddSingleton<IResolverService, YtdlWrapper>();
|
services.AddSingleton<IResolverService, YtdlWrapper>();
|
||||||
services.AddSingleton<IStorageService, RedisStorage>();
|
services.AddSingleton<IStorageService, RedisStorage>();
|
||||||
services.AddSingleton<IRssBuilder, CompositeRssBuilder>();
|
services.AddSingleton<IRssBuilder, CompositeRssBuilder>();
|
||||||
|
services.AddSingleton<IPatreonApi, PatreonApi>();
|
||||||
|
|
||||||
// Add framework services.
|
// Add authentication services
|
||||||
|
services.AddAuthentication(config => config.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme);
|
||||||
|
|
||||||
|
// Add framework services
|
||||||
services.AddMvc();
|
services.AddMvc();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,6 +73,77 @@ namespace Podsync
|
|||||||
|
|
||||||
app.UseStaticFiles();
|
app.UseStaticFiles();
|
||||||
|
|
||||||
|
app.UseCookieAuthentication(new CookieAuthenticationOptions
|
||||||
|
{
|
||||||
|
AutomaticAuthenticate = true,
|
||||||
|
AutomaticChallenge = true,
|
||||||
|
LoginPath = new PathString("/login"),
|
||||||
|
LogoutPath = new PathString("/logout")
|
||||||
|
});
|
||||||
|
|
||||||
|
// Patreon authentication
|
||||||
|
app.UseOAuthAuthentication(new OAuthOptions
|
||||||
|
{
|
||||||
|
AuthenticationScheme = PatreonConstants.AuthenticationScheme,
|
||||||
|
|
||||||
|
ClientId = Configuration[$"Podsync:{nameof(PodsyncConfiguration.PatreonClientId)}"],
|
||||||
|
ClientSecret = Configuration[$"Podsync:{nameof(PodsyncConfiguration.PatreonSecret)}"],
|
||||||
|
|
||||||
|
CallbackPath = new PathString("/oauth-patreon"),
|
||||||
|
|
||||||
|
AuthorizationEndpoint = PatreonConstants.AuthorizationEndpoint,
|
||||||
|
TokenEndpoint = PatreonConstants.TokenEndpoint,
|
||||||
|
|
||||||
|
SaveTokens = true,
|
||||||
|
|
||||||
|
Scope = { "users", "pledges-to-me", "my-campaign" },
|
||||||
|
|
||||||
|
Events = new OAuthEvents
|
||||||
|
{
|
||||||
|
OnCreatingTicket = async context =>
|
||||||
|
{
|
||||||
|
var patreonApi = app.ApplicationServices.GetService<IPatreonApi>();
|
||||||
|
|
||||||
|
var tokens = new Tokens
|
||||||
|
{
|
||||||
|
AccessToken = context.AccessToken,
|
||||||
|
RefreshToken = context.RefreshToken
|
||||||
|
};
|
||||||
|
|
||||||
|
var user = await patreonApi.FetchUserAndPledges(tokens);
|
||||||
|
|
||||||
|
context.Identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, user.Id));
|
||||||
|
context.Identity.AddClaim(new Claim(ClaimTypes.Name, user.Name));
|
||||||
|
context.Identity.AddClaim(new Claim(ClaimTypes.Email, user.Email));
|
||||||
|
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()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.Map("/login", builder =>
|
||||||
|
{
|
||||||
|
builder.Run(async context =>
|
||||||
|
{
|
||||||
|
// Return a challenge to invoke the Patreon authentication scheme
|
||||||
|
await context.Authentication.ChallengeAsync(PatreonConstants.AuthenticationScheme, new AuthenticationProperties { RedirectUri = "/" });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
app.Map("/logout", builder =>
|
||||||
|
{
|
||||||
|
builder.Run(async context =>
|
||||||
|
{
|
||||||
|
// Sign the user out of the authentication middleware (i.e. it will clear the Auth cookie)
|
||||||
|
await context.Authentication.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
|
||||||
|
|
||||||
|
// Redirect the user to the home page after signing out
|
||||||
|
context.Response.Redirect("/");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
app.UseMvc(routes =>
|
app.UseMvc(routes =>
|
||||||
{
|
{
|
||||||
routes.MapRoute("default", "{controller=Home}/{action=Index}/{id?}");
|
routes.MapRoute("default", "{controller=Home}/{action=Index}/{id?}");
|
||||||
|
@@ -1,4 +1,8 @@
|
|||||||
|
@using Podsync.Helpers
|
||||||
|
@{
|
||||||
|
var enableFeatures = User.EnablePatreonFeatures();
|
||||||
|
}
|
||||||
|
|
||||||
<div class="title">
|
<div class="title">
|
||||||
<h1>Podsync</h1>
|
<h1>Podsync</h1>
|
||||||
|
|
||||||
@@ -8,16 +12,22 @@
|
|||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
<div class="login-block">
|
<div class="login-block">
|
||||||
<h5>Login to unlock Patreon features <i class="fa fa-question-circle" aria-hidden="true"></i></h5>
|
@if (User.Identity.IsAuthenticated)
|
||||||
<a href="#">
|
{
|
||||||
<i class="fa fa-facebook" aria-hidden="true"></i>
|
<p>
|
||||||
</a>
|
Yo, <i class="fa fa-user-circle" aria-hidden="true"></i> @User.GetName() ( <a href="~/logout"><i class="fa fa-sign-out" aria-hidden="true"></i>logout</a>)
|
||||||
<a href="#">
|
</p>
|
||||||
<i class="fa fa-twitter" aria-hidden="true"></i>
|
}
|
||||||
</a>
|
else
|
||||||
<a href="#">
|
{
|
||||||
<i class="fa fa-google-plus" aria-hidden="true"></i>
|
<h5>
|
||||||
</a>
|
<a href="~/login">
|
||||||
|
Login with Patreon to unlock features
|
||||||
|
<i class="fa fa-question-circle master-tooltip" aria-hidden="true" title="We have added advanced features for those who supported development. Login with your Patreon account to unlock them.">
|
||||||
|
</i>
|
||||||
|
</a>
|
||||||
|
</h5>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -34,11 +44,22 @@
|
|||||||
|
|
||||||
<div class="controls">
|
<div class="controls">
|
||||||
<p>
|
<p>
|
||||||
<i id="control-icon" class="fa fa-question-circle" aria-hidden="true"></i>
|
@if (enableFeatures)
|
||||||
<span id="control-panel" class="locked">
|
{
|
||||||
|
<i class="fa fa-wrench" aria-hidden="true"></i>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<i class="fa fa-question-circle master-tooltip"
|
||||||
|
aria-hidden="true"
|
||||||
|
title="This features are available for patrons only. You may support us and unlock this features"></i>
|
||||||
|
}
|
||||||
|
|
||||||
|
<span id="control-panel" class="@(!enableFeatures ? "locked" : "")">
|
||||||
Selected format <a class="selected-option" id="video-format">video</a> <a id="audio-format">audio</a>,
|
Selected format <a class="selected-option" id="video-format">video</a> <a id="audio-format">audio</a>,
|
||||||
quality <a class="selected-option" id="best-quality">best</a> <a id="worst-quality">worst</a>,
|
quality <a class="selected-option" id="best-quality">best</a> <a id="worst-quality">worst</a>,
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<span class="locked" style="text-decoration: line-through">
|
<span class="locked" style="text-decoration: line-through">
|
||||||
<i class="fa fa-question-circle master-tooltip" title="Still under development. We will keep you updated via Patreon's news feed" aria-hidden="true"></i>
|
<i class="fa fa-question-circle master-tooltip" title="Still under development. We will keep you updated via Patreon's news feed" aria-hidden="true"></i>
|
||||||
episode count <a class="selected-option">50</a> <a>100</a> <a>150</a>
|
episode count <a class="selected-option">50</a> <a>100</a> <a>150</a>
|
||||||
|
@@ -48,8 +48,7 @@
|
|||||||
<script src="~/js/site.min.js" asp-append-version="true">
|
<script src="~/js/site.min.js" asp-append-version="true">
|
||||||
</script>
|
</script>
|
||||||
</environment>
|
</environment>
|
||||||
|
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
|
(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
|
||||||
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
|
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
|
||||||
|
@@ -10,6 +10,9 @@
|
|||||||
|
|
||||||
"Podsync": {
|
"Podsync": {
|
||||||
"YouTubeApiKey": "",
|
"YouTubeApiKey": "",
|
||||||
"RedisConnectionString": "localhost"
|
"RedisConnectionString": "localhost",
|
||||||
|
"BaseUrl": "",
|
||||||
|
"PatreonClientId": "",
|
||||||
|
"PatreonSecret": ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -2,6 +2,8 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"Google.Apis.YouTube.v3": "1.16.0.582",
|
"Google.Apis.YouTube.v3": "1.16.0.582",
|
||||||
"Hashids.net": "1.2.2",
|
"Hashids.net": "1.2.2",
|
||||||
|
"Microsoft.AspNetCore.Authentication.Cookies": "1.0.0",
|
||||||
|
"Microsoft.AspNetCore.Authentication.OAuth": "1.0.0",
|
||||||
"Microsoft.AspNetCore.Diagnostics": "1.0.0",
|
"Microsoft.AspNetCore.Diagnostics": "1.0.0",
|
||||||
"Microsoft.AspNetCore.Mvc": "1.0.0",
|
"Microsoft.AspNetCore.Mvc": "1.0.0",
|
||||||
"Microsoft.AspNetCore.Razor.Tools": {
|
"Microsoft.AspNetCore.Razor.Tools": {
|
||||||
|
@@ -64,12 +64,13 @@ a {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.login-block {
|
.login-block {
|
||||||
color: #EB8C11;
|
color: #e18712;
|
||||||
font-size: 1.3em;
|
font-size: 1em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.login-block a {
|
.login-block a {
|
||||||
margin-right: 0.5em;
|
font-size: 1.2em;
|
||||||
|
font-weight: normal;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Main block + input */
|
/* Main block + input */
|
||||||
|
BIN
src/Podsync/wwwroot/img/patreon_logo.png
Normal file
BIN
src/Podsync/wwwroot/img/patreon_logo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 3.4 KiB |
@@ -77,23 +77,6 @@ $(function () {
|
|||||||
Control panel
|
Control panel
|
||||||
*/
|
*/
|
||||||
|
|
||||||
function lockControls(lock) {
|
|
||||||
if (lock) {
|
|
||||||
$('#control-panel').addClass('locked');
|
|
||||||
$('#control-icon')
|
|
||||||
.removeClass('fa-wrench')
|
|
||||||
.addClass('fa-question-circle master-tooltip')
|
|
||||||
.attr('title', 'This features are available for patrons only (please, login with your Patreon account). You may support us and unlock this features');
|
|
||||||
} else {
|
|
||||||
$('#control-panel')
|
|
||||||
.removeClass('locked');
|
|
||||||
$('#control-icon')
|
|
||||||
.removeClass('fa-question-circle master-tooltip')
|
|
||||||
.addClass('fa-wrench')
|
|
||||||
.removeAttr('title');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function isLocked() {
|
function isLocked() {
|
||||||
return $('#control-panel').hasClass('locked');
|
return $('#control-panel').hasClass('locked');
|
||||||
}
|
}
|
||||||
@@ -118,6 +101,17 @@ $(function () {
|
|||||||
$('#best-quality, #worst-quality').toggleClass('selected-option');
|
$('#best-quality, #worst-quality').toggleClass('selected-option');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getQuality() {
|
||||||
|
var isAudio = $('#audio-format').hasClass('selected-option');
|
||||||
|
var isWorst = $('#worst-quality').hasClass('selected-option');
|
||||||
|
|
||||||
|
if (isAudio) {
|
||||||
|
return isWorst ? 'AudioLow' : 'AudioHigh';
|
||||||
|
} else {
|
||||||
|
return isWorst ? 'VideoLow' : 'VideoHigh';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* Modal */
|
/* Modal */
|
||||||
|
|
||||||
function closeModal() {
|
function closeModal() {
|
||||||
@@ -164,7 +158,7 @@ $(function () {
|
|||||||
|
|
||||||
$('#get-link').click(function(e) {
|
$('#get-link').click(function(e) {
|
||||||
var url = $('#url-input').val();
|
var url = $('#url-input').val();
|
||||||
createFeed({ url: url, quality: 'VideoHigh' }, displayLink);
|
createFeed({ url: url, quality: getQuality() }, displayLink);
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -192,6 +186,4 @@ $(function () {
|
|||||||
}
|
}
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
});
|
});
|
||||||
|
|
||||||
lockControls(true);
|
|
||||||
});
|
});
|
24
test/Podsync.Tests/Services/Patreon/PatreonApiTests.cs
Normal file
24
test/Podsync.Tests/Services/Patreon/PatreonApiTests.cs
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
using System.Threading.Tasks;
|
||||||
|
using Podsync.Services.Patreon;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Podsync.Tests.Services.Patreon
|
||||||
|
{
|
||||||
|
public class PatreonApiTests : TestBase
|
||||||
|
{
|
||||||
|
private readonly IPatreonApi _api = new PatreonApi();
|
||||||
|
|
||||||
|
private Tokens Tokens => Configuration.CreatorTokens;
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task FetchProfileTest()
|
||||||
|
{
|
||||||
|
var user = await _api.FetchUserAndPledges(Tokens);
|
||||||
|
|
||||||
|
Assert.Equal("2822191", user.Id);
|
||||||
|
Assert.Equal("pavlenko.maksym@gmail.com", user.Email);
|
||||||
|
Assert.Equal("https://www.patreon.com/podsync", user.Url);
|
||||||
|
Assert.Equal("Max", user.Name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Reference in New Issue
Block a user