diff --git a/Selector.Model/Extensions/ServiceExtensions.cs b/Selector.Model/Extensions/ServiceExtensions.cs index 3b44395..5374a0c 100644 --- a/Selector.Model/Extensions/ServiceExtensions.cs +++ b/Selector.Model/Extensions/ServiceExtensions.cs @@ -1,6 +1,6 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.Extensions.DependencyInjection; - +using Selector.Cache; using Selector.Model.Authorisation; namespace Selector.Model.Extensions @@ -21,5 +21,12 @@ namespace Selector.Model.Extensions services.AddScoped(); services.AddSingleton(); } + + public static IServiceCollection AddDBPlayCountPuller(this IServiceCollection services) + { + services.AddTransient(); + + return services; + } } } diff --git a/Selector.Model/Scrobble/DBPlayCountPuller.cs b/Selector.Model/Scrobble/DBPlayCountPuller.cs new file mode 100644 index 0000000..802326c --- /dev/null +++ b/Selector.Model/Scrobble/DBPlayCountPuller.cs @@ -0,0 +1,70 @@ +using System; +using System.Diagnostics; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.Extensions.Options; +using Selector.Model; + +namespace Selector.Cache +{ + public class DBPlayCountPuller + { + protected readonly IScrobbleRepository ScrobbleRepository; + private readonly IOptions nowOptions; + + public DBPlayCountPuller( + IOptions options, + IScrobbleRepository scrobbleRepository + ) + { + ScrobbleRepository = scrobbleRepository; + nowOptions = options; + } + + public Task Get(string username, string track, string artist, string album, string albumArtist) + { + if (string.IsNullOrWhiteSpace(username)) throw new ArgumentNullException("No username provided"); + + var userScrobbleCount = ScrobbleRepository.Count(username: username); + + var artistScrobbles = ScrobbleRepository.GetAll(username: username, artistName: artist).ToArray(); + var albumScrobbles = artistScrobbles.Where( + s => s.AlbumName.Equals(album, StringComparison.CurrentCultureIgnoreCase)).ToArray(); + var trackScrobbles = artistScrobbles.Where( + s => s.TrackName.Equals(track, StringComparison.CurrentCultureIgnoreCase)).ToArray(); + + var postCalc = artistScrobbles.Resample(nowOptions.Value.ArtistResampleWindow).Select(s => s.Value).Sum(); + //var postCalc = playCount.ArtistCountData.Select(s => s.Value).Sum(); + Debug.Assert(postCalc == artistScrobbles.Count()); + + PlayCount playCount = new() + { + Username = username, + Artist = artistScrobbles.Count(), + Album = albumScrobbles.Count(), + Track = trackScrobbles.Count(), + User = userScrobbleCount, + + ArtistCountData = artistScrobbles + .Resample(nowOptions.Value.ArtistResampleWindow) + //.ResampleByMonth() + .CumulativeSum() + .ToArray(), + + AlbumCountData = albumScrobbles + .Resample(nowOptions.Value.AlbumResampleWindow) + //.ResampleByMonth() + .CumulativeSum() + .ToArray(), + + TrackCountData = trackScrobbles + .Resample(nowOptions.Value.TrackResampleWindow) + //.ResampleByMonth() + .CumulativeSum() + .ToArray() + }; + + return Task.FromResult(playCount); + } + } +} diff --git a/Selector.Model/Scrobble/IScrobbleRepository.cs b/Selector.Model/Scrobble/IScrobbleRepository.cs index d9cbfab..a59994d 100644 --- a/Selector.Model/Scrobble/IScrobbleRepository.cs +++ b/Selector.Model/Scrobble/IScrobbleRepository.cs @@ -17,5 +17,6 @@ namespace Selector.Model public void RemoveRange(IEnumerable scrobbles); void Update(UserScrobble item); Task Save(); + int Count(string include = null, string userId = null, string username = null, string trackName = null, string albumName = null, string artistName = null, DateTime? from = null, DateTime? to = null); } } diff --git a/Selector.Model/Scrobble/ScrobbleRepository.cs b/Selector.Model/Scrobble/ScrobbleRepository.cs index 7663efb..77dcb9f 100644 --- a/Selector.Model/Scrobble/ScrobbleRepository.cs +++ b/Selector.Model/Scrobble/ScrobbleRepository.cs @@ -39,7 +39,7 @@ namespace Selector.Model return scrobbles.FirstOrDefault(); } - public IEnumerable GetAll(string include = null, string userId = null, string username = null, string trackName = null, string albumName = null, string artistName = null, DateTime? from = null, DateTime? to = null) + private IQueryable GetAllQueryable(string include = null, string userId = null, string username = null, string trackName = null, string albumName = null, string artistName = null, DateTime? from = null, DateTime? to = null) { var scrobbles = db.Scrobble.AsQueryable(); @@ -55,7 +55,8 @@ namespace Selector.Model if (!string.IsNullOrWhiteSpace(username)) { - var user = db.Users.AsNoTracking().Where(u => u.UserName.Equals(username, StringComparison.InvariantCultureIgnoreCase)).FirstOrDefault(); + var normalUsername = username.ToUpperInvariant(); + var user = db.Users.AsNoTracking().Where(u => u.NormalizedUserName == normalUsername).FirstOrDefault(); if (user is not null) { scrobbles = scrobbles.Where(s => s.UserId == user.Id); @@ -91,9 +92,12 @@ namespace Selector.Model scrobbles = scrobbles.Where(u => u.Timestamp < to.Value); } - return scrobbles.AsEnumerable(); + return scrobbles; } + public IEnumerable GetAll(string include = null, string userId = null, string username = null, string trackName = null, string albumName = null, string artistName = null, DateTime? from = null, DateTime? to = null) + => GetAllQueryable(include: include, userId: userId, username: username, trackName: trackName, albumName: albumName, artistName: artistName, from: from, to: to).AsEnumerable(); + public void Remove(int key) { Remove(Find(key)); @@ -118,5 +122,8 @@ namespace Selector.Model { return db.SaveChangesAsync(); } + + public int Count(string include = null, string userId = null, string username = null, string trackName = null, string albumName = null, string artistName = null, DateTime? from = null, DateTime? to = null) + => GetAllQueryable(include: include, userId: userId, username: username, trackName: trackName, albumName: albumName, artistName: artistName, from: from, to: to).Count(); } } diff --git a/Selector.Web/Hubs/NowPlayingHub.cs b/Selector.Web/Hubs/NowPlayingHub.cs index c5234d1..1f5ba26 100644 --- a/Selector.Web/Hubs/NowPlayingHub.cs +++ b/Selector.Web/Hubs/NowPlayingHub.cs @@ -30,6 +30,7 @@ namespace Selector.Web.Hubs private readonly IDatabaseAsync Cache; private readonly AudioFeaturePuller AudioFeaturePuller; private readonly PlayCountPuller PlayCountPuller; + private readonly DBPlayCountPuller DBPlayCountPuller; private readonly ApplicationDbContext Db; private readonly IScrobbleRepository ScrobbleRepository; @@ -41,12 +42,14 @@ namespace Selector.Web.Hubs ApplicationDbContext db, IScrobbleRepository scrobbleRepository, IOptions options, + DBPlayCountPuller dbPlayCountPuller, PlayCountPuller playCountPuller = null ) { Cache = cache; AudioFeaturePuller = featurePuller; PlayCountPuller = playCountPuller; + DBPlayCountPuller = dbPlayCountPuller; Db = db; ScrobbleRepository = scrobbleRepository; nowOptions = options; @@ -103,36 +106,16 @@ namespace Selector.Web.Hubs if (user.LastFmConnected()) { - var playCount = await PlayCountPuller.Get(user.LastFmUsername, track, artist, album, albumArtist); + PlayCount playCount; if (user.ScrobbleSavingEnabled()) { - var artistScrobbles = ScrobbleRepository.GetAll(userId: user.Id, artistName: artist).ToArray(); - - playCount.Artist = artistScrobbles.Length; - - playCount.ArtistCountData = artistScrobbles - //.Resample(nowOptions.Value.ArtistResampleWindow) - .ResampleByMonth() - .ToArray(); - - var postCalc = playCount.ArtistCountData.Select(s => s.Value).Sum(); - Debug.Assert(postCalc == artistScrobbles.Count()); - - playCount.AlbumCountData = artistScrobbles - .Where(s => s.AlbumName.Equals(album, StringComparison.CurrentCultureIgnoreCase)) - //.Resample(nowOptions.Value.AlbumResampleWindow) - .ResampleByMonth() - .ToArray(); - - playCount.TrackCountData = artistScrobbles - .Where(s => s.TrackName.Equals(track, StringComparison.CurrentCultureIgnoreCase)) - //.Resample(nowOptions.Value.TrackResampleWindow) - .ResampleByMonth() - .ToArray(); + playCount = await DBPlayCountPuller.Get(user.UserName, track, artist, album, albumArtist); + } + else + { + playCount = await PlayCountPuller.Get(user.LastFmUsername, track, artist, album, albumArtist); } - - await Clients.Caller.OnNewPlayCount(playCount); } diff --git a/Selector.Web/Options.cs b/Selector.Web/Options.cs index 49ba879..80fe477 100644 --- a/Selector.Web/Options.cs +++ b/Selector.Web/Options.cs @@ -53,22 +53,4 @@ namespace Selector.Web public bool Enabled { get; set; } = false; public string ConnectionString { get; set; } } - - public class NowPlayingOptions - { - public const string Key = "Now"; - - public TimeSpan ArtistResampleWindow { get; set; } = TimeSpan.FromDays(30); - public TimeSpan AlbumResampleWindow { get; set; } = TimeSpan.FromDays(30); - public TimeSpan TrackResampleWindow { get; set; } = TimeSpan.FromDays(30); - - public TimeSpan ArtistDensityWindow { get; set; } = TimeSpan.FromDays(10); - public decimal ArtistDensityThreshold { get; set; } = 5; - - public TimeSpan AlbumDensityWindow { get; set; } = TimeSpan.FromDays(10); - public decimal AlbumDensityThreshold { get; set; } = 5; - - public TimeSpan TrackDensityWindow { get; set; } = TimeSpan.FromDays(10); - public decimal TrackDensityThreshold { get; set; } = 5; - } } diff --git a/Selector.Web/Startup.cs b/Selector.Web/Startup.cs index 63265ba..6e90e09 100644 --- a/Selector.Web/Startup.cs +++ b/Selector.Web/Startup.cs @@ -54,6 +54,7 @@ namespace Selector.Web services.AddDbContext(options => options.UseNpgsql(Configuration.GetConnectionString("Default")) ); + services.AddDBPlayCountPuller(); services.AddTransient(); services.AddIdentity() diff --git a/Selector/Options.cs b/Selector/Options.cs new file mode 100644 index 0000000..5bd0279 --- /dev/null +++ b/Selector/Options.cs @@ -0,0 +1,22 @@ +using System; +namespace Selector +{ + public class NowPlayingOptions + { + public const string Key = "Now"; + + public TimeSpan ArtistResampleWindow { get; set; } = TimeSpan.FromDays(30); + public TimeSpan AlbumResampleWindow { get; set; } = TimeSpan.FromDays(30); + public TimeSpan TrackResampleWindow { get; set; } = TimeSpan.FromDays(30); + + public TimeSpan ArtistDensityWindow { get; set; } = TimeSpan.FromDays(10); + public decimal ArtistDensityThreshold { get; set; } = 5; + + public TimeSpan AlbumDensityWindow { get; set; } = TimeSpan.FromDays(10); + public decimal AlbumDensityThreshold { get; set; } = 5; + + public TimeSpan TrackDensityWindow { get; set; } = TimeSpan.FromDays(10); + public decimal TrackDensityThreshold { get; set; } = 5; + } +} + diff --git a/Selector/Scrobble/Resampler.cs b/Selector/Scrobble/Resampler.cs index d469f28..bfbc60d 100644 --- a/Selector/Scrobble/Resampler.cs +++ b/Selector/Scrobble/Resampler.cs @@ -26,25 +26,37 @@ namespace Selector var earliest = sortedScrobbles.First().Timestamp; var latest = sortedScrobbles.Last().Timestamp; - for (var counter = earliest; counter <= latest; counter += window) + var enumeratorExhausted = false; + + for (var windowStart = earliest; windowStart <= latest; windowStart += window) { - var windowEnd = counter + window; + var windowEnd = windowStart + window; var count = 0; + var windowOverran = false; - if (sortedScrobblesIter.Current is not null) + while(!windowOverran && !enumeratorExhausted) { - count++; - } - - while (sortedScrobblesIter.MoveNext() && counter <= sortedScrobblesIter.Current.Timestamp && sortedScrobblesIter.Current.Timestamp < windowEnd) - { - count++; + if (windowStart <= sortedScrobblesIter.Current.Timestamp) + { + if(sortedScrobblesIter.Current.Timestamp < windowEnd) + { + count++; + if (!sortedScrobblesIter.MoveNext()) + { + enumeratorExhausted = true; + } + } + else + { + windowOverran = true; + } + } } yield return new CountSample() { - TimeStamp = counter + (window / 2), + TimeStamp = windowStart + (window / 2), Value = count }; } @@ -92,6 +104,21 @@ namespace Selector }; } } + + public static IEnumerable CumulativeSum(this IEnumerable samples) + { + var sum = 0; + foreach(var sample in samples) + { + sum += sample.Value; + + yield return new CountSample + { + TimeStamp = sample.TimeStamp, + Value = sum + }; + } + } } } diff --git a/Selector/Watcher/BaseWatcher.cs b/Selector/Watcher/BaseWatcher.cs index f1ed7d5..03dea9a 100644 --- a/Selector/Watcher/BaseWatcher.cs +++ b/Selector/Watcher/BaseWatcher.cs @@ -1,7 +1,5 @@ using System; -using System.Collections.Generic; -using System.Linq.Expressions; -using System.Text; +using System.Diagnostics; using System.Threading; using System.Threading.Tasks; @@ -15,10 +13,12 @@ namespace Selector protected readonly ILogger Logger; public string Id { get; set; } public string SpotifyUsername { get; set; } + private Stopwatch ExecutionTimer { get; set; } public BaseWatcher(ILogger logger = null) { Logger = logger ?? NullLogger.Instance; + ExecutionTimer = new Stopwatch(); } public abstract Task WatchOne(CancellationToken token); @@ -28,6 +28,9 @@ namespace Selector { Logger.LogDebug("Starting watcher"); while (true) { + + ExecutionTimer.Start(); + cancelToken.ThrowIfCancellationRequested(); try @@ -38,9 +41,13 @@ namespace Selector { Logger.LogError(ex, "Exception occured while conducting single poll operation"); } - - Logger.LogTrace($"Finished watch one, delaying {PollPeriod}ms..."); - await Task.Delay(PollPeriod, cancelToken); + + ExecutionTimer.Stop(); + var waitTime = decimal.ToInt32(Math.Max(0, PollPeriod - ExecutionTimer.ElapsedMilliseconds)); + ExecutionTimer.Reset(); + + Logger.LogTrace($"Finished watch one, delaying \"{PollPeriod}\"ms (\"{waitTime}\"ms)..."); + await Task.Delay(waitTime, cancelToken); } }