working without redis, player watcher event tweaking, graceful exception handling

This commit is contained in:
andy 2021-12-04 12:49:09 +00:00
parent 35eee0f068
commit a401280edf
20 changed files with 167 additions and 124 deletions

View File

@ -44,12 +44,14 @@ namespace Selector.CLI
IAudioFeatureInjectorFactory audioFeatureInjectorFactory,
IPlayCounterFactory playCounterFactory,
IPublisherFactory publisherFactory,
ICacheWriterFactory cacheWriterFactory,
ILogger<DbWatcherService> logger,
IServiceProvider serviceProvider
) {
IServiceProvider serviceProvider,
IPublisherFactory publisherFactory = null,
ICacheWriterFactory cacheWriterFactory = null
)
{
Logger = logger;
ServiceProvider = serviceProvider;
@ -105,8 +107,8 @@ namespace Selector.CLI
watcher = await WatcherFactory.Get<PlayerWatcher>(spotifyFactory, id: dbWatcher.UserId, pollPeriod: PollPeriod);
consumers.Add(await AudioFeatureInjectorFactory.Get(spotifyFactory));
consumers.Add(await CacheWriterFactory.Get());
consumers.Add(await PublisherFactory.Get());
if (CacheWriterFactory is not null) consumers.Add(await CacheWriterFactory.Get());
if (PublisherFactory is not null) consumers.Add(await PublisherFactory.Get());
if (!string.IsNullOrWhiteSpace(dbWatcher.User.LastFmUsername))
{

View File

@ -123,7 +123,7 @@ namespace Selector.CLI
case Consumers.PlayCounter:
if(!string.IsNullOrWhiteSpace(watcherOption.LastFmUsername))
{
consumers.Add(await ServiceProvider.GetService<PlayCounterCachingFactory>().Get(creds: new() { Username = watcherOption.LastFmUsername }));
consumers.Add(await ServiceProvider.GetService<PlayCounterFactory>().Get(creds: new() { Username = watcherOption.LastFmUsername }));
}
else
{

View File

@ -55,7 +55,12 @@ namespace Selector.CLI
Console.WriteLine("> Adding Last.fm credentials...");
services.AddLastFm(config.LastfmClient, config.LastfmSecret);
services.AddCachingLastFm();
if(config.RedisOptions.Enabled)
{
Console.WriteLine("> Adding caching Last.fm consumers...");
services.AddCachingLastFm();
}
}
else
{
@ -101,11 +106,19 @@ namespace Selector.CLI
Console.WriteLine("> Adding Services...");
// SERVICES
services.AddConsumerFactories();
services.AddCachingConsumerFactories();
if (config.RedisOptions.Enabled)
{
Console.WriteLine("> Adding caching consumers...");
services.AddCachingConsumerFactories();
}
services.AddWatcher();
services.AddSpotify();
services.AddCachingSpotify();
if (config.RedisOptions.Enabled) {
Console.WriteLine("> Adding caching Spotify consumers...");
services.AddCachingSpotify();
}
ConfigureLastFm(config, services);
ConfigureDb(config, services);
@ -140,6 +153,8 @@ namespace Selector.CLI
static IHostBuilder CreateHostBuilder(string[] args, Action<HostBuilderContext, IServiceCollection> BuildServices, Action<HostBuilderContext, ILoggingBuilder> BuildLogs)
=> Host.CreateDefaultBuilder(args)
.UseWindowsService()
.UseSystemd()
.ConfigureServices((context, services) => BuildServices(context, services))
.ConfigureLogging((context, builder) => BuildLogs(context, builder));
}

View File

@ -10,6 +10,8 @@
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="6.0.0" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="6.0.0" />
<PackageReference Include="Microsoft.Extensions.Hosting.Systemd" Version="6.0.0" />
<PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="6.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="6.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="6.0.0" />
<PackageReference Include="NLog" Version="4.7.12" />
@ -26,7 +28,10 @@
</ItemGroup>
<ItemGroup>
<None Update="appsettings.Development.json">
<None Update="appsettings.Development.json" Condition="Exists('appsettings.Development.json')">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="appsettings.Release.json" Condition="Exists('appsettings.Release.json')">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="appsettings.json">

View File

@ -57,7 +57,6 @@ namespace Selector.Cache
if (watcher is IPlayerWatcher watcherCast)
{
watcherCast.ItemChange += Callback;
watcherCast.PlayingChange += Callback;
}
else
{
@ -72,7 +71,6 @@ namespace Selector.Cache
if (watcher is IPlayerWatcher watcherCast)
{
watcherCast.ItemChange -= Callback;
watcherCast.PlayingChange -= Callback;
}
else
{

View File

@ -27,8 +27,8 @@ namespace Selector.Cache.Extensions
public static void AddCachingConsumerFactories(this IServiceCollection services)
{
services.AddTransient<IAudioFeatureInjectorFactory, AudioFeatureInjectorFactory>();
services.AddTransient<AudioFeatureInjectorFactory>();
services.AddTransient<IAudioFeatureInjectorFactory, CachingAudioFeatureInjectorFactory>();
services.AddTransient<CachingAudioFeatureInjectorFactory>();
services.AddTransient<IPlayCounterFactory, PlayCounterCachingFactory>();
services.AddTransient<PlayCounterCachingFactory>();

View File

@ -13,7 +13,7 @@ namespace Selector.Cache
public AudioFeaturePuller(
IRefreshTokenFactoryProvider spotifyFactory,
IDatabaseAsync cache
IDatabaseAsync cache = null
)
{
SpotifyFactory = spotifyFactory;
@ -24,8 +24,8 @@ namespace Selector.Cache
{
if(string.IsNullOrWhiteSpace(trackId)) throw new ArgumentNullException("No track Id provided");
var track = await Cache.StringGetAsync(Key.AudioFeature(trackId));
if (track == RedisValue.Null)
var track = await Cache?.StringGetAsync(Key.AudioFeature(trackId));
if (Cache is null || track == RedisValue.Null)
{
if(!string.IsNullOrWhiteSpace(refreshToken))
{

View File

@ -23,13 +23,13 @@ namespace Selector.Cache
protected readonly IUserApi UserClient;
public PlayCountPuller(
IDatabaseAsync cache,
ILogger<PlayCountPuller> logger,
ITrackApi trackClient,
IAlbumApi albumClient,
IArtistApi artistClient,
IUserApi userClient
IUserApi userClient,
IDatabaseAsync cache = null
)
{
Cache = cache;
@ -45,14 +45,14 @@ namespace Selector.Cache
{
if (string.IsNullOrWhiteSpace(username)) throw new ArgumentNullException("No username provided");
var trackCache = Cache.StringGetAsync(Key.TrackPlayCount(track, artist));
var albumCache = Cache.StringGetAsync(Key.AlbumPlayCount(album, albumArtist));
var artistCache = Cache.StringGetAsync(Key.ArtistPlayCount(artist));
var userCache = Cache.StringGetAsync(Key.UserPlayCount(username));
var trackCache = Cache?.StringGetAsync(Key.TrackPlayCount(track, artist));
var albumCache = Cache?.StringGetAsync(Key.AlbumPlayCount(album, albumArtist));
var artistCache = Cache?.StringGetAsync(Key.ArtistPlayCount(artist));
var userCache = Cache?.StringGetAsync(Key.UserPlayCount(username));
var cacheTasks = new Task[] { trackCache, albumCache, artistCache, userCache };
await Task.WhenAll(cacheTasks);
await Task.WhenAll(cacheTasks.Where(t => t is not null));
PlayCount playCount = new()
{
@ -64,64 +64,36 @@ namespace Selector.Cache
Task<LastResponse<LastArtist>> artistHttp = null;
Task<LastResponse<LastUser>> userHttp = null;
if (trackCache.IsCompletedSuccessfully)
if (trackCache is not null && trackCache.IsCompletedSuccessfully && trackCache.Result != RedisValue.Null)
{
if(trackCache.Result == RedisValue.Null)
{
trackHttp = TrackClient.GetInfoAsync(track, artist, username);
}
else
{
playCount.Track = (int) trackCache.Result;
}
playCount.Track = (int) trackCache.Result;
}
else
{
trackHttp = TrackClient.GetInfoAsync(track, artist, username);
}
if (albumCache.IsCompletedSuccessfully)
if (albumCache is not null && albumCache.IsCompletedSuccessfully && albumCache.Result != RedisValue.Null)
{
if (albumCache.Result == RedisValue.Null)
{
albumHttp = AlbumClient.GetInfoAsync(albumArtist, album, username: username);
}
else
{
playCount.Album = (int)albumCache.Result;
}
playCount.Album = (int) albumCache.Result;
}
else
{
albumHttp = AlbumClient.GetInfoAsync(albumArtist, album, username: username);
}
if (artistCache.IsCompletedSuccessfully)
if (artistCache is not null && artistCache.IsCompletedSuccessfully && artistCache.Result != RedisValue.Null)
{
if (artistCache.Result == RedisValue.Null)
{
artistHttp = ArtistClient.GetInfoAsync(artist);
}
else
{
playCount.Artist = (int)artistCache.Result;
}
playCount.Artist = (int) artistCache.Result;
}
else
{
artistHttp = ArtistClient.GetInfoAsync(artist);
}
if (userCache.IsCompletedSuccessfully)
if (userCache is not null && userCache.IsCompletedSuccessfully && userCache.Result != RedisValue.Null)
{
if (userCache.Result == RedisValue.Null)
{
userHttp = UserClient.GetInfoAsync(username);
}
else
{
playCount.User = (int)userCache.Result;
}
playCount.User = (int) userCache.Result;
}
else
{

View File

@ -64,11 +64,28 @@ namespace Selector.Model
public class DesignTimeDbContextFactory : IDesignTimeDbContextFactory<ApplicationDbContext>
{
private static string GetPath(string env) => $"{@Directory.GetCurrentDirectory()}/../Selector.Web/appsettings.{env}.json";
public ApplicationDbContext CreateDbContext(string[] args)
{
string configFile;
if(File.Exists(GetPath("Development")))
{
configFile = GetPath("Development");
}
else if(File.Exists(GetPath("Release")))
{
configFile = GetPath("Release");
}
else
{
throw new FileNotFoundException("No config file available to load a connection string from");
}
IConfigurationRoot configuration = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile(@Directory.GetCurrentDirectory() + "/../Selector.Web/appsettings.Development.json")
.AddJsonFile(configFile)
.Build();
var builder = new DbContextOptionsBuilder<ApplicationDbContext>();

View File

@ -22,7 +22,7 @@
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="6.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="6.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="6.0.0" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="6.0.0" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="6.0.1" />
</ItemGroup>
<ItemGroup>

View File

@ -85,7 +85,7 @@ namespace Selector.Web.Areas.Identity.Pages.Account
var result = await _signInManager.PasswordSignInAsync(Input.Username, Input.Password, Input.RememberMe, lockoutOnFailure: false);
if (result.Succeeded)
{
_logger.LogInformation("User logged in.");
_logger.LogInformation($"[{Input.Username}] logged in.");
return LocalRedirect(returnUrl);
}
if (result.RequiresTwoFactor)
@ -94,7 +94,7 @@ namespace Selector.Web.Areas.Identity.Pages.Account
}
if (result.IsLockedOut)
{
_logger.LogWarning("User account locked out.");
_logger.LogWarning($"[{Input.Username}] locked out.");
return RedirectToPage("./Lockout");
}
else

View File

@ -18,19 +18,10 @@ namespace Selector.Web.Pages
public class NowModel : PageModel
{
private readonly ILogger<NowModel> Logger;
private readonly INowPlayingMappingFactory MappingFactory;
private readonly CacheHubProxy HubProxy;
private readonly UserManager<ApplicationUser> UserManager;
public NowModel(ILogger<NowModel> logger,
INowPlayingMappingFactory mappingFactory,
CacheHubProxy hubProxy,
UserManager<ApplicationUser> userManager)
public NowModel(ILogger<NowModel> logger)
{
Logger = logger;
MappingFactory = mappingFactory;
HubProxy = hubProxy;
UserManager = userManager;
}
public void OnGet()

View File

@ -34,7 +34,10 @@
</ItemGroup>
<ItemGroup>
<None Update="appsettings.Development.json">
<None Update="appsettings.Development.json" Condition="Exists('appsettings.Development.json')">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="appsettings.Release.json" Condition="Exists('appsettings.Release.json')">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="appsettings.json">

View File

@ -97,11 +97,14 @@ namespace Selector.Web
services.AddRedisServices(config.RedisOptions.ConnectionString);
services.AddSpotify();
services.AddCachingSpotify();
if (config.RedisOptions.Enabled)
{
Console.WriteLine("> Adding caching Spotify consumers...");
services.AddCachingSpotify();
services.AddCacheHubProxy();
}
ConfigureLastFm(config, services);
services.AddCacheHubProxy();
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
@ -141,7 +144,12 @@ namespace Selector.Web
Console.WriteLine("> Adding Last.fm credentials...");
services.AddLastFm(config.LastfmClient, config.LastfmSecret);
services.AddCachingLastFm();
if (config.RedisOptions.Enabled)
{
Console.WriteLine("> Adding caching Last.fm consumers...");
services.AddCachingLastFm();
}
}
else
{

View File

@ -22,6 +22,7 @@ namespace Selector
}
public abstract Task WatchOne(CancellationToken token);
public abstract Task Reset();
public async Task Watch(CancellationToken cancelToken)
{

View File

@ -13,7 +13,7 @@ namespace Selector
public class WatcherCollection: IWatcherCollection, IDisposable, IEnumerable<WatcherContext>
{
private readonly ILogger<WatcherCollection> Logger;
public bool IsRunning { get; private set; } = true;
public bool IsRunning { get; private set; } = false;
private List<WatcherContext> Watchers { get; set; } = new();
public WatcherCollection(ILogger<WatcherCollection> logger = null)

View File

@ -58,10 +58,24 @@ namespace Selector
Consumers.ForEach(c => c.Subscribe(Watcher));
Reset();
}
private void Reset()
{
if(Task is not null && !Task.IsCompleted)
{
TokenSource.Cancel();
}
TokenSource = new();
Task = Watcher.Watch(TokenSource.Token);
Task.ContinueWith(t =>
{
if (t.Exception != null) throw t.Exception;
Watcher.Reset();
Reset();
}, TaskContinuationOptions.OnlyOnFaulted);
}

View File

@ -8,6 +8,7 @@ namespace Selector
/// <summary>
/// Track or episode changes
/// </summary>
public event EventHandler<ListeningChangeEventArgs> NetworkPoll;
public event EventHandler<ListeningChangeEventArgs> ItemChange;
public event EventHandler<ListeningChangeEventArgs> AlbumChange;
public event EventHandler<ListeningChangeEventArgs> ArtistChange;

View File

@ -18,6 +18,8 @@ namespace Selector
/// <returns></returns>
public Task Watch(CancellationToken cancelToken);
public Task Reset();
/// <summary>
/// Time interval in ms between polls from Watch()
/// </summary>

View File

@ -15,6 +15,7 @@ namespace Selector
private readonly IPlayerClient spotifyClient;
private readonly IEqual eq;
public event EventHandler<ListeningChangeEventArgs> NetworkPoll;
public event EventHandler<ListeningChangeEventArgs> ItemChange;
public event EventHandler<ListeningChangeEventArgs> AlbumChange;
public event EventHandler<ListeningChangeEventArgs> ArtistChange;
@ -26,6 +27,7 @@ namespace Selector
public event EventHandler<ListeningChangeEventArgs> PlayingChange;
public CurrentlyPlayingContext Live { get; private set; }
private CurrentlyPlayingContext Previous { get; set; }
public PlayerTimeline Past { get; set; } = new();
public PlayerWatcher(IPlayerClient spotifyClient,
@ -40,6 +42,15 @@ namespace Selector
PollPeriod = pollPeriod;
}
public override Task Reset()
{
Previous = null;
Live = null;
Past = new();
return Task.CompletedTask;
}
public override async Task WatchOne(CancellationToken token = default)
{
token.ThrowIfCancellationRequested();
@ -52,104 +63,100 @@ namespace Selector
if (polledCurrent != null) StoreCurrentPlaying(polledCurrent);
// swap new item into live and bump existing down to previous
CurrentlyPlayingContext previous;
if(Live is null) {
Live = polledCurrent;
previous = polledCurrent;
}
else {
previous = Live;
Live = polledCurrent;
}
Previous = Live;
Live = polledCurrent;
OnNetworkPoll(GetEvent());
// NOT PLAYING
if(previous is null && Live is null)
if(Previous is null && Live is null)
{
// Console.WriteLine("not playing");
}
else
{
// STARTED PLAYBACK
if(previous is null
&& (Live.Item is FullTrack || Live.Item is FullEpisode))
if(Previous is null && Live is not null)
{
Logger.LogDebug($"Playback started: {Live.DisplayString()}");
OnPlayingChange(ListeningChangeEventArgs.From(previous, Live, Past, id: Id, username: SpotifyUsername));
OnItemChange(ListeningChangeEventArgs.From(previous, Live, Past, id: Id, username: SpotifyUsername));
OnPlayingChange(GetEvent());
OnItemChange(GetEvent());
OnContextChange(GetEvent());
}
// STOPPED PLAYBACK
else if((previous.Item is FullTrack || previous.Item is FullEpisode)
&& Live is null)
else if(Previous is not null && Live is null)
{
Logger.LogDebug($"Playback stopped: {previous.DisplayString()}");
OnPlayingChange(ListeningChangeEventArgs.From(previous, Live, Past, id: Id, username: SpotifyUsername));
OnItemChange(ListeningChangeEventArgs.From(previous, Live, Past, id: Id, username: SpotifyUsername));
Logger.LogDebug($"Playback stopped: {Previous.DisplayString()}");
OnPlayingChange(GetEvent());
OnItemChange(GetEvent());
OnContextChange(GetEvent());
}
// CONTINUING PLAYBACK
else {
// MUSIC
if(previous.Item is FullTrack previousTrack
if(Previous.Item is FullTrack previousTrack
&& Live.Item is FullTrack currentTrack)
{
if(!eq.IsEqual(previousTrack, currentTrack)) {
Logger.LogDebug($"Track changed: {previousTrack.DisplayString()} -> {currentTrack.DisplayString()}");
OnItemChange(ListeningChangeEventArgs.From(previous, Live, Past, id: Id, username: SpotifyUsername));
OnItemChange(GetEvent());
}
if(!eq.IsEqual(previousTrack.Album, currentTrack.Album)) {
Logger.LogDebug($"Album changed: {previousTrack.Album.DisplayString()} -> {currentTrack.Album.DisplayString()}");
OnAlbumChange(ListeningChangeEventArgs.From(previous, Live, Past, id: Id, username: SpotifyUsername));
OnAlbumChange(GetEvent());
}
if(!eq.IsEqual(previousTrack.Artists[0], currentTrack.Artists[0])) {
Logger.LogDebug($"Artist changed: {previousTrack.Artists.DisplayString()} -> {currentTrack.Artists.DisplayString()}");
OnArtistChange(ListeningChangeEventArgs.From(previous, Live, Past, id: Id, username: SpotifyUsername));
OnArtistChange(GetEvent());
}
}
// CHANGED CONTENT
else if((previous.Item is FullTrack && Live.Item is FullEpisode)
|| (previous.Item is FullEpisode && Live.Item is FullTrack))
// CHANGED CONTENT TYPE
else if((Previous.Item is FullTrack && Live.Item is FullEpisode)
|| (Previous.Item is FullEpisode && Live.Item is FullTrack))
{
Logger.LogDebug($"Media type changed: {previous.Item}, {previous.Item}");
OnContentChange(ListeningChangeEventArgs.From(previous, Live, Past, id: Id, username: SpotifyUsername));
OnItemChange(ListeningChangeEventArgs.From(previous, Live, Past, id: Id, username: SpotifyUsername));
Logger.LogDebug($"Media type changed: {Previous.Item}, {Previous.Item}");
OnContentChange(GetEvent());
OnItemChange(GetEvent());
}
// PODCASTS
else if(previous.Item is FullEpisode previousEp
else if(Previous.Item is FullEpisode previousEp
&& Live.Item is FullEpisode currentEp)
{
if(!eq.IsEqual(previousEp, currentEp)) {
Logger.LogDebug($"Podcast changed: {previousEp.DisplayString()} -> {currentEp.DisplayString()}");
OnItemChange(ListeningChangeEventArgs.From(previous, Live, Past, id: Id, username: SpotifyUsername));
OnItemChange(GetEvent());
}
}
else {
Logger.LogError($"Unknown combination of previous and current playing contexts, [{Previous.DisplayString()}] [{Live.DisplayString()}]");
throw new NotSupportedException("Unknown item combination");
}
// CONTEXT
if(!eq.IsEqual(previous.Context, Live.Context)) {
Logger.LogDebug($"Context changed: {previous.Context.DisplayString()} -> {Live.Context.DisplayString()}");
OnContextChange(ListeningChangeEventArgs.From(previous, Live, Past, id: Id, username: SpotifyUsername));
if(!eq.IsEqual(Previous.Context, Live.Context)) {
Logger.LogDebug($"Context changed: {Previous.Context.DisplayString()} -> {Live.Context.DisplayString()}");
OnContextChange(GetEvent());
}
// DEVICE
if(!eq.IsEqual(previous?.Device, Live?.Device)) {
Logger.LogDebug($"Device changed: {previous?.Device.DisplayString()} -> {Live?.Device.DisplayString()}");
OnDeviceChange(ListeningChangeEventArgs.From(previous, Live, Past, id: Id, username: SpotifyUsername));
if(!eq.IsEqual(Previous?.Device, Live?.Device)) {
Logger.LogDebug($"Device changed: {Previous?.Device.DisplayString()} -> {Live?.Device.DisplayString()}");
OnDeviceChange(GetEvent());
}
// IS PLAYING
if(previous.IsPlaying != Live.IsPlaying) {
Logger.LogDebug($"Playing state changed: {previous.IsPlaying} -> {Live.IsPlaying}");
OnPlayingChange(ListeningChangeEventArgs.From(previous, Live, Past, id: Id, username: SpotifyUsername));
if(Previous.IsPlaying != Live.IsPlaying) {
Logger.LogDebug($"Playing state changed: {Previous.IsPlaying} -> {Live.IsPlaying}");
OnPlayingChange(GetEvent());
}
// VOLUME
if(previous.Device.VolumePercent != Live.Device.VolumePercent) {
Logger.LogDebug($"Volume changed: {previous.Device.VolumePercent}% -> {Live.Device.VolumePercent}%");
OnVolumeChange(ListeningChangeEventArgs.From(previous, Live, Past, id: Id, username: SpotifyUsername));
if(Previous.Device.VolumePercent != Live.Device.VolumePercent) {
Logger.LogDebug($"Volume changed: {Previous.Device.VolumePercent}% -> {Live.Device.VolumePercent}%");
OnVolumeChange(GetEvent());
}
}
}
@ -172,6 +179,8 @@ namespace Selector
}
}
private ListeningChangeEventArgs GetEvent() => ListeningChangeEventArgs.From(Previous, Live, Past, id: Id, username: SpotifyUsername);
/// <summary>
/// Store currently playing in last plays. Determine whether new list or appending required
/// </summary>
@ -182,6 +191,11 @@ namespace Selector
}
#region Event Firers
protected virtual void OnNetworkPoll(ListeningChangeEventArgs args)
{
NetworkPoll?.Invoke(this, args);
}
protected virtual void OnItemChange(ListeningChangeEventArgs args)
{
ItemChange?.Invoke(this, args);