more DI for consumer factories, caching play counter, service extensions

This commit is contained in:
andy 2021-11-29 21:04:15 +00:00
parent 975ee772dd
commit e41d525b0b
21 changed files with 288 additions and 135 deletions

View File

@ -25,38 +25,45 @@ namespace Selector.CLI
private readonly ILogger<LocalWatcherService> Logger;
private readonly ILoggerFactory LoggerFactory;
private readonly IServiceProvider ServiceProvider;
private readonly RootOptions Config;
private readonly IWatcherFactory WatcherFactory;
private readonly IWatcherCollectionFactory WatcherCollectionFactory;
private readonly IRefreshTokenFactoryProvider SpotifyFactory;
private readonly LastAuth LastAuth;
private readonly IDatabaseAsync Cache;
private readonly ISubscriber Subscriber;
private readonly IAudioFeatureInjectorFactory AudioFeatureInjectorFactory;
private readonly IPlayCounterFactory PlayCounterFactory;
private readonly IPublisherFactory PublisherFactory;
private readonly ICacheWriterFactory CacheWriterFactory;
private Dictionary<string, IWatcherCollection> Watchers { get; set; } = new();
public DbWatcherService(
IWatcherFactory watcherFactory,
IWatcherCollectionFactory watcherCollectionFactory,
IRefreshTokenFactoryProvider spotifyFactory,
IAudioFeatureInjectorFactory audioFeatureInjectorFactory,
IPlayCounterFactory playCounterFactory,
IPublisherFactory publisherFactory,
ICacheWriterFactory cacheWriterFactory,
ILoggerFactory loggerFactory,
IServiceProvider serviceProvider,
IOptions<RootOptions> config,
LastAuth lastAuth = null,
IDatabaseAsync cache = null,
ISubscriber subscriber = null
IServiceProvider serviceProvider
) {
Logger = loggerFactory.CreateLogger<LocalWatcherService>();
LoggerFactory = loggerFactory;
Config = config.Value;
ServiceProvider = serviceProvider;
WatcherFactory = watcherFactory;
WatcherCollectionFactory = watcherCollectionFactory;
SpotifyFactory = spotifyFactory;
LastAuth = lastAuth;
ServiceProvider = serviceProvider;
Cache = cache;
Subscriber = subscriber;
AudioFeatureInjectorFactory = audioFeatureInjectorFactory;
PlayCounterFactory = playCounterFactory;
PublisherFactory = publisherFactory;
CacheWriterFactory = cacheWriterFactory;
}
public async Task StartAsync(CancellationToken cancellationToken)
@ -99,26 +106,13 @@ namespace Selector.CLI
case WatcherType.Player:
watcher = await WatcherFactory.Get<PlayerWatcher>(spotifyFactory, id: dbWatcher.UserId, pollPeriod: PollPeriod);
var featureInjector = new AudioFeatureInjectorFactory(LoggerFactory);
consumers.Add(await featureInjector.Get(spotifyFactory));
var featureInjectorCache = new CachingAudioFeatureInjectorFactory(LoggerFactory, Cache);
consumers.Add(await featureInjectorCache.Get(spotifyFactory));
var cacheWriter = new CacheWriterFactory(Cache, LoggerFactory);
consumers.Add(await cacheWriter.Get());
var pub = new PublisherFactory(Subscriber, LoggerFactory);
consumers.Add(await pub.Get());
consumers.Add(await AudioFeatureInjectorFactory.Get(spotifyFactory));
consumers.Add(await CacheWriterFactory.Get());
consumers.Add(await PublisherFactory.Get());
if (!string.IsNullOrWhiteSpace(dbWatcher.User.LastFmUsername))
{
if (LastAuth is null) throw new ArgumentNullException("No Last Auth Injected");
var client = new LastfmClient(LastAuth);
var playCount = new PlayCounterFactory(LoggerFactory, client: client, creds: new() { Username = dbWatcher.User.LastFmUsername });
consumers.Add(await playCount.Get());
consumers.Add(await PlayCounterFactory.Get(creds: new() { Username = dbWatcher.User.LastFmUsername }));
}
else
{

View File

@ -4,15 +4,13 @@ using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using IF.Lastfm.Core.Api;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Selector.Cache;
using StackExchange.Redis;
namespace Selector.CLI
{
@ -21,15 +19,12 @@ namespace Selector.CLI
private const string ConfigInstanceKey = "localconfig";
private readonly ILogger<LocalWatcherService> Logger;
private readonly ILoggerFactory LoggerFactory;
private readonly RootOptions Config;
private readonly IWatcherFactory WatcherFactory;
private readonly IWatcherCollectionFactory WatcherCollectionFactory;
private readonly IRefreshTokenFactoryProvider SpotifyFactory;
private readonly LastAuth LastAuth;
private readonly IDatabaseAsync Cache;
private readonly ISubscriber Subscriber;
private readonly IServiceProvider ServiceProvider;
private Dictionary<string, IWatcherCollection> Watchers { get; set; } = new();
@ -37,21 +32,20 @@ namespace Selector.CLI
IWatcherFactory watcherFactory,
IWatcherCollectionFactory watcherCollectionFactory,
IRefreshTokenFactoryProvider spotifyFactory,
ILoggerFactory loggerFactory,
IOptions<RootOptions> config,
LastAuth lastAuth = null,
IDatabaseAsync cache = null,
ISubscriber subscriber = null
IServiceProvider serviceProvider,
ILogger<LocalWatcherService> logger,
IOptions<RootOptions> config
) {
Logger = loggerFactory.CreateLogger<LocalWatcherService>();
LoggerFactory = loggerFactory;
Logger = logger;
Config = config.Value;
WatcherFactory = watcherFactory;
WatcherCollectionFactory = watcherCollectionFactory;
SpotifyFactory = spotifyFactory;
LastAuth = lastAuth;
Cache = cache;
Subscriber = subscriber;
ServiceProvider = serviceProvider;
}
public async Task StartAsync(CancellationToken cancellationToken)
@ -111,34 +105,25 @@ namespace Selector.CLI
switch(consumer)
{
case Consumers.AudioFeatures:
var featureInjector = new AudioFeatureInjectorFactory(LoggerFactory);
consumers.Add(await featureInjector.Get(spotifyFactory));
consumers.Add(await ServiceProvider.GetService<AudioFeatureInjectorFactory>().Get(spotifyFactory));
break;
case Consumers.AudioFeaturesCache:
var featureInjectorCache = new CachingAudioFeatureInjectorFactory(LoggerFactory, Cache);
consumers.Add(await featureInjectorCache.Get(spotifyFactory));
consumers.Add(await ServiceProvider.GetService<CachingAudioFeatureInjectorFactory>().Get(spotifyFactory));
break;
case Consumers.CacheWriter:
var cacheWriter = new CacheWriterFactory(Cache, LoggerFactory);
consumers.Add(await cacheWriter.Get());
consumers.Add(await ServiceProvider.GetService<CacheWriterFactory>().Get());
break;
case Consumers.Publisher:
var pub = new PublisherFactory(Subscriber, LoggerFactory);
consumers.Add(await pub.Get());
consumers.Add(await ServiceProvider.GetService<PublisherFactory>().Get());
break;
case Consumers.PlayCounter:
if(!string.IsNullOrWhiteSpace(watcherOption.LastFmUsername))
{
if(LastAuth is null) throw new ArgumentNullException("No Last Auth Injected");
var client = new LastfmClient(LastAuth);
var playCount = new PlayCounterFactory(LoggerFactory, client: client, creds: new(){ Username = watcherOption.LastFmUsername });
consumers.Add(await playCount.Get());
consumers.Add(await ServiceProvider.GetService<PlayCounterCachingFactory>().Get(creds: new() { Username = watcherOption.LastFmUsername }));
}
else
{

View File

@ -22,7 +22,7 @@ namespace Selector.CLI
public static string FormatKeys(string[] args) => string.Join(":", args);
}
class RootOptions
public class RootOptions
{
public const string Key = "Selector";
@ -42,12 +42,12 @@ namespace Selector.CLI
public EqualityChecker Equality { get; set; } = EqualityChecker.Uri;
}
enum EqualityChecker
public enum EqualityChecker
{
Uri, String
}
class WatcherOptions
public class WatcherOptions
{
public const string Key = "Watcher";
@ -56,7 +56,7 @@ namespace Selector.CLI
public List<WatcherInstanceOptions> Instances { get; set; } = new();
}
class WatcherInstanceOptions
public class WatcherInstanceOptions
{
public const string Key = "Instances";
@ -73,19 +73,19 @@ namespace Selector.CLI
#nullable disable
}
enum Consumers
public enum Consumers
{
AudioFeatures, AudioFeaturesCache, CacheWriter, Publisher, PlayCounter
}
class DatabaseOptions {
public class DatabaseOptions {
public const string Key = "Database";
public bool Enabled { get; set; } = false;
public string ConnectionString { get; set; }
}
class RedisOptions
public class RedisOptions
{
public const string Key = "Redis";

View File

@ -11,12 +11,12 @@ using NLog.Extensions.Logging;
using Selector.Model;
using Selector.Cache;
using Selector.Cache.Extensions;
using IF.Lastfm.Core.Api;
using StackExchange.Redis;
namespace Selector.CLI
{
class Program
public static class Program
{
public static async Task Main(string[] args)
{
@ -55,7 +55,8 @@ namespace Selector.CLI
Console.WriteLine("> Adding Last.fm credentials...");
var lastAuth = new LastAuth(config.LastfmClient, config.LastfmSecret);
services.AddSingleton<LastAuth>(lastAuth);
services.AddSingleton(lastAuth);
services.AddTransient(sp => new LastfmClient(sp.GetService<LastAuth>()));
}
else
{
@ -74,25 +75,6 @@ namespace Selector.CLI
}
}
public static void ConfigureRedis(RootOptions config, IServiceCollection services)
{
if (config.RedisOptions.Enabled)
{
Console.WriteLine("> Configuring Redis...");
if(string.IsNullOrWhiteSpace(config.RedisOptions.ConnectionString))
{
Console.WriteLine("> No Redis configuration string provided, exiting...");
Environment.Exit(1);
}
var connMulti = ConnectionMultiplexer.Connect(config.RedisOptions.ConnectionString);
services.AddSingleton(connMulti);
services.AddTransient<IDatabaseAsync>(services => services.GetService<ConnectionMultiplexer>().GetDatabase());
services.AddTransient<ISubscriber>(services => services.GetService<ConnectionMultiplexer>().GetSubscriber());
}
}
public static void ConfigureEqual(RootOptions config, IServiceCollection services)
{
switch (config.Equality)
@ -119,8 +101,9 @@ namespace Selector.CLI
Console.WriteLine("> Adding Services...");
// SERVICES
services.AddCachingConsumerFactories();
services.AddSingleton<IWatcherFactory, WatcherFactory>();
services.AddSingleton<IAudioFeatureInjectorFactory, AudioFeatureInjectorFactory>();
services.AddSingleton<IWatcherCollectionFactory, WatcherCollectionFactory>();
// For generating spotify clients
//services.AddSingleton<IRefreshTokenFactoryProvider, RefreshTokenFactoryProvider>();
@ -128,7 +111,10 @@ namespace Selector.CLI
ConfigureLastFm(config, services);
ConfigureDb(config, services);
ConfigureRedis(config, services);
if (config.RedisOptions.Enabled)
services.AddRedisServices(config.RedisOptions.ConnectionString);
ConfigureEqual(config, services);
// HOSTED SERVICES

View File

@ -9,12 +9,7 @@ using StackExchange.Redis;
namespace Selector.Cache
{
public interface ICachingAudioFeatureInjectorFactory
{
public Task<IConsumer> Get(ISpotifyConfigFactory spotifyFactory, IPlayerWatcher watcher);
}
public class CachingAudioFeatureInjectorFactory: ICachingAudioFeatureInjectorFactory {
public class CachingAudioFeatureInjectorFactory: IAudioFeatureInjectorFactory {
private readonly ILoggerFactory LoggerFactory;
private readonly IDatabaseAsync Db;

View File

@ -0,0 +1,52 @@
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using StackExchange.Redis;
using IF.Lastfm.Core.Api;
namespace Selector.Cache
{
public class PlayCounterCachingFactory: IPlayCounterFactory
{
private readonly ILoggerFactory LoggerFactory;
private readonly IDatabaseAsync Cache;
private readonly LastfmClient Client;
private readonly LastFmCredentials Creds;
public PlayCounterCachingFactory(
ILoggerFactory loggerFactory,
IDatabaseAsync cache,
LastfmClient client = null,
LastFmCredentials creds = null)
{
LoggerFactory = loggerFactory;
Cache = cache;
Client = client;
Creds = creds;
}
public async Task<IConsumer> Get(LastfmClient fmClient = null, LastFmCredentials creds = null, IPlayerWatcher watcher = null)
{
var client = fmClient ?? Client;
if (client is null)
{
throw new ArgumentNullException("No Last.fm client provided");
}
return new PlayCounterCaching(
watcher,
client.Track,
client.Album,
client.Artist,
client.User,
Cache,
credentials: creds ?? Creds,
logger: LoggerFactory.CreateLogger<PlayCounterCaching>()
);
}
}
}

View File

@ -0,0 +1,61 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using IF.Lastfm.Core.Api;
using StackExchange.Redis;
using SpotifyAPI.Web;
namespace Selector.Cache
{
public class PlayCounterCaching: PlayCounter
{
private readonly IDatabaseAsync Db;
public TimeSpan CacheExpiry { get; set; } = TimeSpan.FromDays(14);
public PlayCounterCaching(
IPlayerWatcher watcher,
ITrackApi trackClient,
IAlbumApi albumClient,
IArtistApi artistClient,
IUserApi userClient,
IDatabaseAsync db,
LastFmCredentials credentials = null,
ILogger<PlayCounterCaching> logger = null,
CancellationToken token = default
) : base(watcher, trackClient, albumClient, artistClient, userClient, credentials, logger, token)
{
Db = db;
NewPlayCount += CacheCallback;
}
public void CacheCallback(object sender, PlayCount e)
{
Task.Run(() => { return AsyncCacheCallback(e); }, CancelToken);
}
public async Task AsyncCacheCallback(PlayCount e)
{
var track = e.ListeningEvent.Current.Item as FullTrack;
Logger.LogTrace($"Caching play count for [{track.DisplayString()}]");
var tasks = new Task[]
{
Db.StringSetAsync(Key.TrackPlayCount(track.Name, track.Artists[0].Name), e.Track, expiry: CacheExpiry),
Db.StringSetAsync(Key.AlbumPlayCount(track.Album.Name, track.Album.Artists[0].Name), e.Album, expiry: CacheExpiry),
Db.StringSetAsync(Key.ArtistPlayCount(track.Artists[0].Name), e.Artist, expiry: CacheExpiry),
Db.StringSetAsync(Key.UserPlayCount(e.Username), e.User, expiry: CacheExpiry),
};
await Task.WhenAll(tasks);
Logger.LogDebug($"Cached audio feature for [{track.DisplayString()}]");
}
}
}

View File

@ -0,0 +1,37 @@
using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.Extensions.DependencyInjection;
using StackExchange.Redis;
namespace Selector.Cache.Extensions
{
public static class ServiceExtensions
{
public static void AddRedisServices(this IServiceCollection services, string connectionStr)
{
Console.WriteLine("> Configuring Redis...");
if (string.IsNullOrWhiteSpace(connectionStr))
{
Console.WriteLine("> No Redis configuration string provided, exiting...");
Environment.Exit(1);
}
var connMulti = ConnectionMultiplexer.Connect(connectionStr);
services.AddSingleton(connMulti);
services.AddTransient<IDatabaseAsync>(services => services.GetService<ConnectionMultiplexer>().GetDatabase());
services.AddTransient<ISubscriber>(services => services.GetService<ConnectionMultiplexer>().GetSubscriber());
}
public static void AddCachingConsumerFactories(this IServiceCollection services)
{
services.AddTransient<IAudioFeatureInjectorFactory, AudioFeatureInjectorFactory>();
services.AddTransient<IPlayCounterFactory, PlayCounterCachingFactory>();
services.AddTransient<ICacheWriterFactory, CacheWriterFactory>();
services.AddTransient<IPublisherFactory, PublisherFactory>();
}
}
}

View File

@ -11,6 +11,7 @@ namespace Selector.Cache
public const string TrackName = "Track";
public const string AlbumName = "Album";
public const string ArtistName = "Artist";
public const string UserName = "User";
public const string AudioFeatureName = "AudioFeature";
public const string PlayCountName = "PlayCount";
@ -30,6 +31,7 @@ namespace Selector.Cache
public static string TrackPlayCount(string name, string artist) => Namespace(TrackName, artist, name, PlayCountName);
public static string AlbumPlayCount(string name, string artist) => Namespace(AlbumName, artist, name, PlayCountName);
public static string ArtistPlayCount(string name) => Namespace(ArtistName, name, PlayCountName);
public static string UserPlayCount(string username) => Namespace(UserName, username, PlayCountName);
public static string WatcherReserved(int id) => Namespace(WatcherName, id.ToString(), ReservedName);

View File

@ -0,0 +1,30 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.Extensions.DependencyInjection;
using Selector.Model.Authorisation;
namespace Selector.Model.Extensions
{
public static class ServiceExtensions
{
public static void AddAuthorisationHandlers(this IServiceCollection services)
{
services.AddAuthorization(options =>
{
options.FallbackPolicy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.Build();
});
services.AddScoped<IAuthorizationHandler, WatcherIsOwnerAuthHandler>();
services.AddSingleton<IAuthorizationHandler, WatcherIsAdminAuthHandler>();
services.AddScoped<IAuthorizationHandler, UserIsSelfAuthHandler>();
services.AddSingleton<IAuthorizationHandler, UserIsAdminAuthHandler>();
}
}
}

View File

@ -13,13 +13,12 @@ using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Authorization;
using Microsoft.EntityFrameworkCore;
using StackExchange.Redis;
using Selector.Web.Service;
using Selector.Web.Hubs;
using Selector.Model;
using Selector.Model.Authorisation;
using Selector.Model.Extensions;
using Selector.Cache;
using Selector.Cache.Extensions;
namespace Selector.Web
{
@ -93,35 +92,10 @@ namespace Selector.Web
options.SlidingExpiration = true;
});
// AUTH
services.AddAuthorization(options =>
{
options.FallbackPolicy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.Build();
});
services.AddScoped<IAuthorizationHandler, WatcherIsOwnerAuthHandler>();
services.AddSingleton<IAuthorizationHandler, WatcherIsAdminAuthHandler>();
services.AddAuthorisationHandlers();
services.AddScoped<IAuthorizationHandler, UserIsSelfAuthHandler>();
services.AddSingleton<IAuthorizationHandler, UserIsAdminAuthHandler>();
// REDIS
if (config.RedisOptions.Enabled)
{
Console.WriteLine("> Configuring Redis...");
if (string.IsNullOrWhiteSpace(config.RedisOptions.ConnectionString))
{
Console.WriteLine("> No Redis configuration string provided, exiting...");
Environment.Exit(1);
}
var connMulti = ConnectionMultiplexer.Connect(config.RedisOptions.ConnectionString);
services.AddSingleton(connMulti);
services.AddTransient<IDatabaseAsync>(services => services.GetService<ConnectionMultiplexer>().GetDatabase());
services.AddTransient<ISubscriber>(services => services.GetService<ConnectionMultiplexer>().GetSubscriber());
}
services.AddRedisServices(config.RedisOptions.ConnectionString);
services.AddSingleton<IRefreshTokenFactoryProvider, CachingRefreshTokenFactoryProvider>();
services.AddSingleton<AudioFeaturePuller>();

View File

@ -10,7 +10,7 @@ namespace Selector
{
public interface IAudioFeatureInjectorFactory
{
public Task<IConsumer> Get(ISpotifyConfigFactory spotifyFactory, IPlayerWatcher watcher);
public Task<IConsumer> Get(ISpotifyConfigFactory spotifyFactory, IPlayerWatcher watcher = null);
}
public class AudioFeatureInjectorFactory: IAudioFeatureInjectorFactory {

View File

@ -123,13 +123,19 @@ namespace Selector
Logger.LogDebug($"Adding Last.fm data [{e.Id}/{e.SpotifyUsername}/{Credentials.Username}] [{track.DisplayString()}], track: {trackCount}, album: {albumCount}, artist: {artistCount}, user: {userCount}");
OnNewPlayCount(new()
PlayCount playCount = new()
{
Track = trackCount,
Album = albumCount,
Artist = artistCount,
User = userCount,
});
ListeningEvent = e
};
if (!string.IsNullOrWhiteSpace(Credentials.Username))
playCount.Username = Credentials.Username;
OnNewPlayCount(playCount);
}
else if (e.Current.Item is FullEpisode episode)
{
@ -181,6 +187,8 @@ namespace Selector
public int? Album { get; set; }
public int? Artist { get; set; }
public int? User { get; set; }
public string Username { get; set; }
public ListeningChangeEventArgs ListeningEvent { get; set; }
}
public class LastFmCredentials

View File

@ -0,0 +1,16 @@
using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.Extensions.DependencyInjection;
namespace Selector.Extensions
{
public static class ServiceExtensions
{
public static void AddConsumerFactories(this IServiceCollection services)
{
services.AddTransient<IAudioFeatureInjectorFactory, AudioFeatureInjectorFactory>();
services.AddTransient<IPlayCounterFactory, PlayCounterFactory>();
}
}
}

View File

@ -0,0 +1,13 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace Selector
{
public interface IWatcherContext
{
void AddConsumer(IConsumer consumer);
void Start();
void Stop();
}
}

View File

@ -7,7 +7,7 @@ using System.Threading.Tasks;
namespace Selector
{
public class WatcherContext: IDisposable
public class WatcherContext: IDisposable, IWatcherContext
{
public IWatcher Watcher { get; set; }
private List<IConsumer> Consumers { get; set; } = new();