diff --git a/Selector.CLI/DbWatcherService.cs b/Selector.CLI/DbWatcherService.cs new file mode 100644 index 0000000..1288a9d --- /dev/null +++ b/Selector.CLI/DbWatcherService.cs @@ -0,0 +1,170 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +using Selector.Cache; +using Selector.Model; +using IF.Lastfm.Core.Api; +using StackExchange.Redis; + +namespace Selector.CLI +{ + class DbWatcherService : IHostedService + { + private const int PollPeriod = 1000; + + private readonly ILogger 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 Dictionary Watchers { get; set; } = new(); + + public DbWatcherService( + IWatcherFactory watcherFactory, + IWatcherCollectionFactory watcherCollectionFactory, + IRefreshTokenFactoryProvider spotifyFactory, + ILoggerFactory loggerFactory, + IServiceProvider serviceProvider, + IOptions config, + LastAuth lastAuth = null, + IDatabaseAsync cache = null, + ISubscriber subscriber = null + ) { + Logger = loggerFactory.CreateLogger(); + LoggerFactory = loggerFactory; + Config = config.Value; + WatcherFactory = watcherFactory; + WatcherCollectionFactory = watcherCollectionFactory; + SpotifyFactory = spotifyFactory; + LastAuth = lastAuth; + ServiceProvider = serviceProvider; + Cache = cache; + Subscriber = subscriber; + } + + public async Task StartAsync(CancellationToken cancellationToken) + { + Logger.LogInformation("Starting database watcher service..."); + + var watcherIndices = await InitInstances(); + + Logger.LogInformation($"Starting {watcherIndices.Count()} affected watcher collection(s)..."); + StartWatcherCollections(watcherIndices); + } + + private async Task> InitInstances() + { + using var scope = ServiceProvider.CreateScope(); + var db = scope.ServiceProvider.GetService(); + + var indices = new HashSet(); + + foreach (var dbWatcher in db.Watcher.Include(w => w.User)) + { + Logger.LogInformation($"Creating new [{dbWatcher.Type}] watcher"); + + var watcherCollectionIdx = dbWatcher.UserId; + indices.Add(watcherCollectionIdx); + + if (!Watchers.ContainsKey(watcherCollectionIdx)) + Watchers[watcherCollectionIdx] = WatcherCollectionFactory.Get(); + + var watcherCollection = Watchers[watcherCollectionIdx]; + + Logger.LogDebug("Getting Spotify factory"); + var spotifyFactory = await SpotifyFactory.GetFactory(dbWatcher.User.SpotifyRefreshToken); + + IWatcher watcher = null; + List consumers = new(); + + switch (dbWatcher.Type) + { + case WatcherType.Player: + watcher = await WatcherFactory.Get(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()); + + 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()); + } + else + { + Logger.LogDebug($"[{dbWatcher.User.UserName}] No Last.fm username, skipping play counter"); + } + + break; + + case WatcherType.Playlist: + throw new NotImplementedException("Playlist watchers not implemented"); + // break; + } + + watcherCollection.Add(watcher, consumers); + } + + return indices; + } + + private void StartWatcherCollections(IEnumerable indices) + { + foreach (var index in indices) + { + try + { + Logger.LogInformation($"Starting watcher collection [{index}]"); + Watchers[index].Start(); + } + catch (KeyNotFoundException) + { + Logger.LogError($"Unable to retrieve watcher collection [{index}] when starting"); + } + } + } + + public Task StopAsync(CancellationToken cancellationToken) + { + Logger.LogInformation("Shutting down"); + + foreach((var key, var watcher) in Watchers) + { + Logger.LogInformation($"Stopping watcher collection [{key}]"); + watcher.Stop(); + } + + return Task.CompletedTask; + } + } +} diff --git a/Selector.CLI/WatcherService.cs b/Selector.CLI/LocalWatcherService.cs similarity index 93% rename from Selector.CLI/WatcherService.cs rename to Selector.CLI/LocalWatcherService.cs index a403430..e883ce7 100644 --- a/Selector.CLI/WatcherService.cs +++ b/Selector.CLI/LocalWatcherService.cs @@ -16,11 +16,11 @@ using StackExchange.Redis; namespace Selector.CLI { - class WatcherService : IHostedService + class LocalWatcherService : IHostedService { private const string ConfigInstanceKey = "localconfig"; - private readonly ILogger Logger; + private readonly ILogger Logger; private readonly ILoggerFactory LoggerFactory; private readonly RootOptions Config; private readonly IWatcherFactory WatcherFactory; @@ -33,7 +33,7 @@ namespace Selector.CLI private Dictionary Watchers { get; set; } = new(); - public WatcherService( + public LocalWatcherService( IWatcherFactory watcherFactory, IWatcherCollectionFactory watcherCollectionFactory, IRefreshTokenFactoryProvider spotifyFactory, @@ -43,7 +43,7 @@ namespace Selector.CLI IDatabaseAsync cache = null, ISubscriber subscriber = null ) { - Logger = loggerFactory.CreateLogger(); + Logger = loggerFactory.CreateLogger(); LoggerFactory = loggerFactory; Config = config.Value; WatcherFactory = watcherFactory; @@ -56,16 +56,15 @@ namespace Selector.CLI public async Task StartAsync(CancellationToken cancellationToken) { - Logger.LogInformation("Starting watcher service..."); + Logger.LogInformation("Starting local watcher service..."); - Logger.LogInformation("Loading config instances..."); - var watcherIndices = await InitialiseConfigInstances(); + var watcherIndices = await InitInstances(); Logger.LogInformation($"Starting {watcherIndices.Count()} affected watcher collection(s)..."); StartWatcherCollections(watcherIndices); } - private async Task> InitialiseConfigInstances() + private async Task> InitInstances() { var indices = new HashSet(); @@ -143,7 +142,7 @@ namespace Selector.CLI } else { - Logger.LogError("No Last.fm usernmae provided, skipping play counter"); + Logger.LogError("No Last.fm username provided, skipping play counter"); } break; } diff --git a/Selector.CLI/Options.cs b/Selector.CLI/Options.cs index 2b25719..2f941df 100644 --- a/Selector.CLI/Options.cs +++ b/Selector.CLI/Options.cs @@ -52,6 +52,7 @@ namespace Selector.CLI public const string Key = "Watcher"; public bool Enabled { get; set; } = true; + public bool LocalEnabled { get; set; } = true; public List Instances { get; set; } = new(); } diff --git a/Selector.CLI/Program.cs b/Selector.CLI/Program.cs index 75c677a..b0dab88 100644 --- a/Selector.CLI/Program.cs +++ b/Selector.CLI/Program.cs @@ -134,8 +134,17 @@ namespace Selector.CLI // HOSTED SERVICES if (config.WatcherOptions.Enabled) { - Console.WriteLine("> Adding Watcher Service"); - services.AddHostedService(); + if(config.WatcherOptions.LocalEnabled) + { + Console.WriteLine("> Adding Local Watcher Service"); + services.AddHostedService(); + } + + if(config.DatabaseOptions.Enabled) + { + Console.WriteLine("> Adding Db Watcher Service"); + services.AddHostedService(); + } } } diff --git a/Selector.CLI/appsettings.json b/Selector.CLI/appsettings.json index fab32e5..fa587ea 100644 --- a/Selector.CLI/appsettings.json +++ b/Selector.CLI/appsettings.json @@ -4,6 +4,7 @@ "ClientSecret": "", "Equality": "uri", "Watcher": { + "localenabled": false, "Instances": [ { "type": "player", @@ -14,7 +15,7 @@ ] }, "Database": { - "enabled": false + "enabled": true }, "Redis": { "enabled": true diff --git a/Selector.Cache/Selector.Cache.csproj b/Selector.Cache/Selector.Cache.csproj index e1f56d5..be32231 100644 --- a/Selector.Cache/Selector.Cache.csproj +++ b/Selector.Cache/Selector.Cache.csproj @@ -7,7 +7,7 @@ - + diff --git a/Selector.Tests/Selector.Tests.csproj b/Selector.Tests/Selector.Tests.csproj index 8621833..fb5ea0a 100644 --- a/Selector.Tests/Selector.Tests.csproj +++ b/Selector.Tests/Selector.Tests.csproj @@ -9,14 +9,14 @@ - + runtime; build; native; contentfiles; analyzers; buildtransitive all - + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/Selector.Web/Selector.Web.csproj b/Selector.Web/Selector.Web.csproj index 75e6f8e..5c45605 100644 --- a/Selector.Web/Selector.Web.csproj +++ b/Selector.Web/Selector.Web.csproj @@ -3,6 +3,7 @@ net6.0 latest + true diff --git a/Selector/Selector.csproj b/Selector/Selector.csproj index 0e4a40e..7595f2c 100644 --- a/Selector/Selector.csproj +++ b/Selector/Selector.csproj @@ -7,7 +7,7 @@ - + diff --git a/Selector/Watcher/PlayerWatcher.cs b/Selector/Watcher/PlayerWatcher.cs index 4853d0d..b5f2830 100644 --- a/Selector/Watcher/PlayerWatcher.cs +++ b/Selector/Watcher/PlayerWatcher.cs @@ -152,7 +152,7 @@ namespace Selector OnVolumeChange(ListeningChangeEventArgs.From(previous, Live, Past, id: Id, username: SpotifyUsername)); } } - } + } } catch(APIUnauthorizedException e) { @@ -162,12 +162,13 @@ namespace Selector catch(APITooManyRequestsException e) { Logger.LogDebug($"Too many requests error: [{e.Message}]"); - throw e; + await Task.Delay(e.RetryAfter); + // throw e; } catch(APIException e) { Logger.LogDebug($"API error: [{e.Message}]"); - throw e; + // throw e; } }