tweaking resampler, db count puller
This commit is contained in:
parent
0b621e00bc
commit
63df0ab0c2
@ -1,6 +1,6 @@
|
|||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Selector.Cache;
|
||||||
using Selector.Model.Authorisation;
|
using Selector.Model.Authorisation;
|
||||||
|
|
||||||
namespace Selector.Model.Extensions
|
namespace Selector.Model.Extensions
|
||||||
@ -21,5 +21,12 @@ namespace Selector.Model.Extensions
|
|||||||
services.AddScoped<IAuthorizationHandler, UserIsSelfAuthHandler>();
|
services.AddScoped<IAuthorizationHandler, UserIsSelfAuthHandler>();
|
||||||
services.AddSingleton<IAuthorizationHandler, UserIsAdminAuthHandler>();
|
services.AddSingleton<IAuthorizationHandler, UserIsAdminAuthHandler>();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static IServiceCollection AddDBPlayCountPuller(this IServiceCollection services)
|
||||||
|
{
|
||||||
|
services.AddTransient<DBPlayCountPuller>();
|
||||||
|
|
||||||
|
return services;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
70
Selector.Model/Scrobble/DBPlayCountPuller.cs
Normal file
70
Selector.Model/Scrobble/DBPlayCountPuller.cs
Normal file
@ -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<NowPlayingOptions> nowOptions;
|
||||||
|
|
||||||
|
public DBPlayCountPuller(
|
||||||
|
IOptions<NowPlayingOptions> options,
|
||||||
|
IScrobbleRepository scrobbleRepository
|
||||||
|
)
|
||||||
|
{
|
||||||
|
ScrobbleRepository = scrobbleRepository;
|
||||||
|
nowOptions = options;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<PlayCount> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -17,5 +17,6 @@ namespace Selector.Model
|
|||||||
public void RemoveRange(IEnumerable<UserScrobble> scrobbles);
|
public void RemoveRange(IEnumerable<UserScrobble> scrobbles);
|
||||||
void Update(UserScrobble item);
|
void Update(UserScrobble item);
|
||||||
Task<int> Save();
|
Task<int> 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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -39,7 +39,7 @@ namespace Selector.Model
|
|||||||
return scrobbles.FirstOrDefault();
|
return scrobbles.FirstOrDefault();
|
||||||
}
|
}
|
||||||
|
|
||||||
public IEnumerable<UserScrobble> 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<UserScrobble> 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();
|
var scrobbles = db.Scrobble.AsQueryable();
|
||||||
|
|
||||||
@ -55,7 +55,8 @@ namespace Selector.Model
|
|||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(username))
|
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)
|
if (user is not null)
|
||||||
{
|
{
|
||||||
scrobbles = scrobbles.Where(s => s.UserId == user.Id);
|
scrobbles = scrobbles.Where(s => s.UserId == user.Id);
|
||||||
@ -91,9 +92,12 @@ namespace Selector.Model
|
|||||||
scrobbles = scrobbles.Where(u => u.Timestamp < to.Value);
|
scrobbles = scrobbles.Where(u => u.Timestamp < to.Value);
|
||||||
}
|
}
|
||||||
|
|
||||||
return scrobbles.AsEnumerable();
|
return scrobbles;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public IEnumerable<UserScrobble> 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)
|
public void Remove(int key)
|
||||||
{
|
{
|
||||||
Remove(Find(key));
|
Remove(Find(key));
|
||||||
@ -118,5 +122,8 @@ namespace Selector.Model
|
|||||||
{
|
{
|
||||||
return db.SaveChangesAsync();
|
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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -30,6 +30,7 @@ namespace Selector.Web.Hubs
|
|||||||
private readonly IDatabaseAsync Cache;
|
private readonly IDatabaseAsync Cache;
|
||||||
private readonly AudioFeaturePuller AudioFeaturePuller;
|
private readonly AudioFeaturePuller AudioFeaturePuller;
|
||||||
private readonly PlayCountPuller PlayCountPuller;
|
private readonly PlayCountPuller PlayCountPuller;
|
||||||
|
private readonly DBPlayCountPuller DBPlayCountPuller;
|
||||||
private readonly ApplicationDbContext Db;
|
private readonly ApplicationDbContext Db;
|
||||||
private readonly IScrobbleRepository ScrobbleRepository;
|
private readonly IScrobbleRepository ScrobbleRepository;
|
||||||
|
|
||||||
@ -41,12 +42,14 @@ namespace Selector.Web.Hubs
|
|||||||
ApplicationDbContext db,
|
ApplicationDbContext db,
|
||||||
IScrobbleRepository scrobbleRepository,
|
IScrobbleRepository scrobbleRepository,
|
||||||
IOptions<NowPlayingOptions> options,
|
IOptions<NowPlayingOptions> options,
|
||||||
|
DBPlayCountPuller dbPlayCountPuller,
|
||||||
PlayCountPuller playCountPuller = null
|
PlayCountPuller playCountPuller = null
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
Cache = cache;
|
Cache = cache;
|
||||||
AudioFeaturePuller = featurePuller;
|
AudioFeaturePuller = featurePuller;
|
||||||
PlayCountPuller = playCountPuller;
|
PlayCountPuller = playCountPuller;
|
||||||
|
DBPlayCountPuller = dbPlayCountPuller;
|
||||||
Db = db;
|
Db = db;
|
||||||
ScrobbleRepository = scrobbleRepository;
|
ScrobbleRepository = scrobbleRepository;
|
||||||
nowOptions = options;
|
nowOptions = options;
|
||||||
@ -103,36 +106,16 @@ namespace Selector.Web.Hubs
|
|||||||
|
|
||||||
if (user.LastFmConnected())
|
if (user.LastFmConnected())
|
||||||
{
|
{
|
||||||
var playCount = await PlayCountPuller.Get(user.LastFmUsername, track, artist, album, albumArtist);
|
PlayCount playCount;
|
||||||
|
|
||||||
if (user.ScrobbleSavingEnabled())
|
if (user.ScrobbleSavingEnabled())
|
||||||
{
|
{
|
||||||
var artistScrobbles = ScrobbleRepository.GetAll(userId: user.Id, artistName: artist).ToArray();
|
playCount = await DBPlayCountPuller.Get(user.UserName, track, artist, album, albumArtist);
|
||||||
|
}
|
||||||
playCount.Artist = artistScrobbles.Length;
|
else
|
||||||
|
{
|
||||||
playCount.ArtistCountData = artistScrobbles
|
playCount = await PlayCountPuller.Get(user.LastFmUsername, track, artist, album, albumArtist);
|
||||||
//.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();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
await Clients.Caller.OnNewPlayCount(playCount);
|
await Clients.Caller.OnNewPlayCount(playCount);
|
||||||
}
|
}
|
||||||
|
@ -53,22 +53,4 @@ namespace Selector.Web
|
|||||||
public bool Enabled { get; set; } = false;
|
public bool Enabled { get; set; } = false;
|
||||||
public string ConnectionString { get; set; }
|
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -54,6 +54,7 @@ namespace Selector.Web
|
|||||||
services.AddDbContext<ApplicationDbContext>(options =>
|
services.AddDbContext<ApplicationDbContext>(options =>
|
||||||
options.UseNpgsql(Configuration.GetConnectionString("Default"))
|
options.UseNpgsql(Configuration.GetConnectionString("Default"))
|
||||||
);
|
);
|
||||||
|
services.AddDBPlayCountPuller();
|
||||||
services.AddTransient<IScrobbleRepository, ScrobbleRepository>();
|
services.AddTransient<IScrobbleRepository, ScrobbleRepository>();
|
||||||
|
|
||||||
services.AddIdentity<ApplicationUser, IdentityRole>()
|
services.AddIdentity<ApplicationUser, IdentityRole>()
|
||||||
|
22
Selector/Options.cs
Normal file
22
Selector/Options.cs
Normal file
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -26,25 +26,37 @@ namespace Selector
|
|||||||
var earliest = sortedScrobbles.First().Timestamp;
|
var earliest = sortedScrobbles.First().Timestamp;
|
||||||
var latest = sortedScrobbles.Last().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 count = 0;
|
||||||
|
var windowOverran = false;
|
||||||
|
|
||||||
if (sortedScrobblesIter.Current is not null)
|
while(!windowOverran && !enumeratorExhausted)
|
||||||
{
|
{
|
||||||
count++;
|
if (windowStart <= sortedScrobblesIter.Current.Timestamp)
|
||||||
}
|
{
|
||||||
|
if(sortedScrobblesIter.Current.Timestamp < windowEnd)
|
||||||
while (sortedScrobblesIter.MoveNext() && counter <= sortedScrobblesIter.Current.Timestamp && sortedScrobblesIter.Current.Timestamp < windowEnd)
|
{
|
||||||
{
|
count++;
|
||||||
count++;
|
if (!sortedScrobblesIter.MoveNext())
|
||||||
|
{
|
||||||
|
enumeratorExhausted = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
windowOverran = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
yield return new CountSample()
|
yield return new CountSample()
|
||||||
{
|
{
|
||||||
TimeStamp = counter + (window / 2),
|
TimeStamp = windowStart + (window / 2),
|
||||||
Value = count
|
Value = count
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -92,6 +104,21 @@ namespace Selector
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static IEnumerable<CountSample> CumulativeSum(this IEnumerable<CountSample> samples)
|
||||||
|
{
|
||||||
|
var sum = 0;
|
||||||
|
foreach(var sample in samples)
|
||||||
|
{
|
||||||
|
sum += sample.Value;
|
||||||
|
|
||||||
|
yield return new CountSample
|
||||||
|
{
|
||||||
|
TimeStamp = sample.TimeStamp,
|
||||||
|
Value = sum
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,7 +1,5 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Diagnostics;
|
||||||
using System.Linq.Expressions;
|
|
||||||
using System.Text;
|
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
@ -15,10 +13,12 @@ namespace Selector
|
|||||||
protected readonly ILogger<BaseWatcher> Logger;
|
protected readonly ILogger<BaseWatcher> Logger;
|
||||||
public string Id { get; set; }
|
public string Id { get; set; }
|
||||||
public string SpotifyUsername { get; set; }
|
public string SpotifyUsername { get; set; }
|
||||||
|
private Stopwatch ExecutionTimer { get; set; }
|
||||||
|
|
||||||
public BaseWatcher(ILogger<BaseWatcher> logger = null)
|
public BaseWatcher(ILogger<BaseWatcher> logger = null)
|
||||||
{
|
{
|
||||||
Logger = logger ?? NullLogger<BaseWatcher>.Instance;
|
Logger = logger ?? NullLogger<BaseWatcher>.Instance;
|
||||||
|
ExecutionTimer = new Stopwatch();
|
||||||
}
|
}
|
||||||
|
|
||||||
public abstract Task WatchOne(CancellationToken token);
|
public abstract Task WatchOne(CancellationToken token);
|
||||||
@ -28,6 +28,9 @@ namespace Selector
|
|||||||
{
|
{
|
||||||
Logger.LogDebug("Starting watcher");
|
Logger.LogDebug("Starting watcher");
|
||||||
while (true) {
|
while (true) {
|
||||||
|
|
||||||
|
ExecutionTimer.Start();
|
||||||
|
|
||||||
cancelToken.ThrowIfCancellationRequested();
|
cancelToken.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
try
|
try
|
||||||
@ -38,9 +41,13 @@ namespace Selector
|
|||||||
{
|
{
|
||||||
Logger.LogError(ex, "Exception occured while conducting single poll operation");
|
Logger.LogError(ex, "Exception occured while conducting single poll operation");
|
||||||
}
|
}
|
||||||
|
|
||||||
Logger.LogTrace($"Finished watch one, delaying {PollPeriod}ms...");
|
ExecutionTimer.Stop();
|
||||||
await Task.Delay(PollPeriod, cancelToken);
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user