adding general cards, scrobble density, upgrading nuget packages

This commit is contained in:
andy 2022-06-02 00:53:57 +01:00
parent 97659389af
commit c8170415b9
12 changed files with 158 additions and 34 deletions

View File

@ -8,7 +8,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="6.0.4"> <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="6.0.5">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
@ -18,8 +18,8 @@
<PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" 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" Version="6.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="6.0.0" /> <PackageReference Include="Microsoft.Extensions.Logging.Console" Version="6.0.0" />
<PackageReference Include="NLog" Version="4.7.15" /> <PackageReference Include="NLog" Version="5.0.0" />
<PackageReference Include="NLog.Extensions.Logging" Version="1.7.4" /> <PackageReference Include="NLog.Extensions.Logging" Version="5.0.0" />
<PackageReference Include="Quartz" Version="3.4.0" /> <PackageReference Include="Quartz" Version="3.4.0" />
<PackageReference Include="Quartz.Extensions.Hosting" Version="3.4.0" /> <PackageReference Include="Quartz.Extensions.Hosting" Version="3.4.0" />
<PackageReference Include="SpotifyAPI.Web" Version="6.2.2" /> <PackageReference Include="SpotifyAPI.Web" Version="6.2.2" />

View File

@ -12,20 +12,20 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Identity" Version="2.2.0" /> <PackageReference Include="Microsoft.AspNetCore.Identity" Version="2.2.0" />
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="6.0.4" /> <PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="6.0.5" />
<PackageReference Include="Microsoft.AspNetCore.Identity.UI" Version="6.0.4" /> <PackageReference Include="Microsoft.AspNetCore.Identity.UI" Version="6.0.5" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="6.0.4" /> <PackageReference Include="Microsoft.EntityFrameworkCore" Version="6.0.5" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="6.0.4"> <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="6.0.5">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
</PackageReference> </PackageReference>
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="6.0.4"> <PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="6.0.5">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="6.0.4" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="6.0.5" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="6.0.0" /> <PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="6.0.0" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="6.0.3" /> <PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="6.0.4" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@ -8,11 +8,11 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="FluentAssertions" Version="6.6.0" /> <PackageReference Include="FluentAssertions" Version="6.7.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.1.0" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.2.0" />
<PackageReference Include="Moq" Version="4.17.2" /> <PackageReference Include="Moq" Version="4.18.1" />
<PackageReference Include="xunit" Version="2.4.1" /> <PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3"> <PackageReference Include="xunit.runner.visualstudio" Version="2.4.5">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
</PackageReference> </PackageReference>

View File

@ -12,6 +12,9 @@ using StackExchange.Redis;
using Selector.Cache; using Selector.Cache;
using Selector.Model; using Selector.Model;
using Selector.Model.Extensions; using Selector.Model.Extensions;
using Selector.Web.NowPlaying;
using Microsoft.Extensions.Options;
using System.Collections.Generic;
namespace Selector.Web.Hubs namespace Selector.Web.Hubs
{ {
@ -20,6 +23,7 @@ namespace Selector.Web.Hubs
public Task OnNewPlaying(CurrentlyPlayingDTO context); public Task OnNewPlaying(CurrentlyPlayingDTO context);
public Task OnNewAudioFeature(TrackAudioFeatures features); public Task OnNewAudioFeature(TrackAudioFeatures features);
public Task OnNewPlayCount(PlayCount playCount); public Task OnNewPlayCount(PlayCount playCount);
public Task OnNewCard(Card card);
} }
public class NowPlayingHub: Hub<INowPlayingHubClient> public class NowPlayingHub: Hub<INowPlayingHubClient>
@ -30,11 +34,14 @@ namespace Selector.Web.Hubs
private readonly ApplicationDbContext Db; private readonly ApplicationDbContext Db;
private readonly IScrobbleRepository ScrobbleRepository; private readonly IScrobbleRepository ScrobbleRepository;
private readonly IOptions<NowPlayingOptions> nowOptions;
public NowPlayingHub( public NowPlayingHub(
IDatabaseAsync cache, IDatabaseAsync cache,
AudioFeaturePuller featurePuller, AudioFeaturePuller featurePuller,
ApplicationDbContext db, ApplicationDbContext db,
IScrobbleRepository scrobbleRepository, IScrobbleRepository scrobbleRepository,
IOptions<NowPlayingOptions> options,
PlayCountPuller playCountPuller = null PlayCountPuller playCountPuller = null
) )
{ {
@ -43,6 +50,7 @@ namespace Selector.Web.Hubs
PlayCountPuller = playCountPuller; PlayCountPuller = playCountPuller;
Db = db; Db = db;
ScrobbleRepository = scrobbleRepository; ScrobbleRepository = scrobbleRepository;
nowOptions = options;
} }
public async Task OnConnected() public async Task OnConnected()
@ -108,5 +116,51 @@ namespace Selector.Web.Hubs
} }
} }
} }
public async Task SendFacts(string track, string artist, string album, string albumArtist)
{
var user = Db.Users
.AsNoTracking()
.Where(u => u.Id == Context.UserIdentifier)
.SingleOrDefault()
?? throw new SqlNullValueException("No user returned");
if (user.ScrobbleSavingEnabled())
{
var artistScrobbles = ScrobbleRepository.GetAll(userId: user.Id, artistName: artist, from: GetMaximumWindow()).ToArray();
var artistDensity = artistScrobbles.Density(DateTime.UtcNow - nowOptions.Value.ArtistDensityWindow, DateTime.UtcNow);
if (artistDensity > nowOptions.Value.ArtistDensityThreshold)
{
await Clients.Caller.OnNewCard(new()
{
Content = $"You're on a {artist} binge! {artistDensity} plays/day recently"
});
}
var albumDensity = artistScrobbles.Where(s => s.AlbumName.Equals(album, StringComparison.InvariantCultureIgnoreCase)).Density(DateTime.UtcNow - nowOptions.Value.AlbumDensityWindow, DateTime.UtcNow);
if (albumDensity > nowOptions.Value.AlbumDensityThreshold)
{
await Clients.Caller.OnNewCard(new()
{
Content = $"You're on a {album} binge! {albumDensity} plays/day recently"
});
}
var trackDensity = artistScrobbles.Where(s => s.TrackName.Equals(track, StringComparison.InvariantCultureIgnoreCase)).Density(DateTime.UtcNow - nowOptions.Value.TrackDensityWindow, DateTime.UtcNow);
if (albumDensity > nowOptions.Value.TrackDensityThreshold)
{
await Clients.Caller.OnNewCard(new()
{
Content = $"You're on a {track} binge! {trackDensity} plays/day recently"
});
}
}
}
private DateTime GetMaximumWindow() => GetMaximumWindow(new TimeSpan[] { nowOptions.Value.ArtistDensityWindow, nowOptions.Value.AlbumDensityWindow, nowOptions.Value.TrackDensityWindow });
private DateTime GetMaximumWindow(IEnumerable<TimeSpan> windows) => windows.Select(w => DateTime.UtcNow - w).Min();
} }
} }

View File

@ -0,0 +1,9 @@
using System;
namespace Selector.Web.NowPlaying
{
public class Card
{
public string Content { get; set; }
}
}

View File

@ -1,4 +1,5 @@
using System.Collections.Generic; using System;
using System.Collections.Generic;
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
namespace Selector.Web namespace Selector.Web
@ -8,6 +9,7 @@ namespace Selector.Web
{ {
config.GetSection(RootOptions.Key).Bind(options); config.GetSection(RootOptions.Key).Bind(options);
config.GetSection(FormatKeys(new[] { RootOptions.Key, RedisOptions.Key })).Bind(options.RedisOptions); config.GetSection(FormatKeys(new[] { RootOptions.Key, RedisOptions.Key })).Bind(options.RedisOptions);
config.GetSection(FormatKeys(new[] { RootOptions.Key, NowPlayingOptions.Key })).Bind(options.NowOptions);
} }
public static RootOptions ConfigureOptions(IConfiguration config) public static RootOptions ConfigureOptions(IConfiguration config)
@ -40,6 +42,7 @@ namespace Selector.Web
public string LastfmSecret { get; set; } public string LastfmSecret { get; set; }
public RedisOptions RedisOptions { get; set; } = new(); public RedisOptions RedisOptions { get; set; } = new();
public NowPlayingOptions NowOptions { get; set; } = new();
} }
@ -50,4 +53,18 @@ 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 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;
}
} }

View File

@ -14,7 +14,7 @@
<audio-feature-card :feature="trackFeatures" v-if="trackFeatures !== null && trackFeatures !== undefined" /></audio-feature-card> <audio-feature-card :feature="trackFeatures" v-if="trackFeatures !== null && trackFeatures !== undefined" /></audio-feature-card>
<audio-feature-chart-card :feature="trackFeatures" v-if="trackFeatures !== null && trackFeatures !== undefined" /></audio-feature-chart-card> <audio-feature-chart-card :feature="trackFeatures" v-if="trackFeatures !== null && trackFeatures !== undefined" /></audio-feature-chart-card>
<play-count-card :count="playCount" :track="lastfmTrack" :username="playCount.username" v-if="playCount !== null && playCount !== undefined" /></play-count-card> <play-count-card :count="playCount" :track="lastfmTrack" :username="playCount.username" v-if="playCount !== null && playCount !== undefined" /></play-count-card>
<info-card v-for="card in cards" :html="card.html"></info-card> <info-card v-for="card in cards" :html="card.content"></info-card>
</div> </div>
</div> </div>

View File

@ -1,4 +1,4 @@
{ {
"iisSettings": { "iisSettings": {
"windowsAuthentication": false, "windowsAuthentication": false,
"anonymousAuthentication": true, "anonymousAuthentication": true,
@ -17,7 +17,6 @@
}, },
"Selector.Web": { "Selector.Web": {
"commandName": "Project", "commandName": "Project",
"dotnetRunMessages": "true",
"launchBrowser": true, "launchBrowser": true,
"applicationUrl": "https://localhost:5001;http://localhost:5000", "applicationUrl": "https://localhost:5001;http://localhost:5000",
"environmentVariables": { "environmentVariables": {
@ -25,4 +24,4 @@
} }
} }
} }
} }

View File

@ -14,13 +14,13 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="6.0.4" /> <PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="6.0.5" />
<PackageReference Include="Microsoft.AspNetCore.Identity.UI" Version="6.0.4" /> <PackageReference Include="Microsoft.AspNetCore.Identity.UI" Version="6.0.5" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" Version="6.0.4" /> <PackageReference Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" Version="6.0.5" />
<PackageReference Include="Microsoft.AspNetCore.StaticFiles" Version="2.2.0" /> <PackageReference Include="Microsoft.AspNetCore.StaticFiles" Version="2.2.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="6.0.4"> <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="6.0.5">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
</PackageReference> </PackageReference>
@ -28,16 +28,20 @@
<PackageReference Include="Microsoft.Extensions.Hosting.Systemd" 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.Hosting.WindowsServices" Version="6.0.0" />
<PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="6.0.3" /> <PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="6.0.5" />
<PackageReference Include="NLog" Version="4.7.15" /> <PackageReference Include="NLog" Version="5.0.0" />
<PackageReference Include="NLog.Extensions.Logging" Version="1.7.4" /> <PackageReference Include="NLog.Extensions.Logging" Version="5.0.0" />
<PackageReference Include="NLog.Web.AspNetCore" Version="4.14.0" /> <PackageReference Include="NLog.Web.AspNetCore" Version="5.0.0" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Folder Include="wwwroot\" /> <Folder Include="wwwroot\" />
<Folder Include="NowPlaying\" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<None Remove="NowPlaying\" />
</ItemGroup>
<ItemGroup> <ItemGroup>
<None Update="appsettings.Development.json" Condition="Exists('appsettings.Development.json')"> <None Update="appsettings.Development.json" Condition="Exists('appsettings.Development.json')">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>

View File

@ -1011,9 +1011,9 @@
} }
}, },
"node_modules/eventsource": { "node_modules/eventsource": {
"version": "1.1.0", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/eventsource/-/eventsource-1.1.0.tgz", "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-1.1.1.tgz",
"integrity": "sha512-VSJjT5oCNrFvCS6igjzPAt5hBzQ2qPBFIbJ03zLI9SE0mxwZpMw6BfJrbFHm1a141AavMEB8JHmBhWAd66PfCg==", "integrity": "sha512-qV5ZC0h7jYIAOhArFJgSfdyz6rALJyb270714o7ZtNnw2WSJ+eexhKtE0O8LYPRsHZHf2osHKZBxGPvm3kPkCA==",
"dependencies": { "dependencies": {
"original": "^1.0.0" "original": "^1.0.0"
}, },
@ -3613,9 +3613,9 @@
"dev": true "dev": true
}, },
"eventsource": { "eventsource": {
"version": "1.1.0", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/eventsource/-/eventsource-1.1.0.tgz", "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-1.1.1.tgz",
"integrity": "sha512-VSJjT5oCNrFvCS6igjzPAt5hBzQ2qPBFIbJ03zLI9SE0mxwZpMw6BfJrbFHm1a141AavMEB8JHmBhWAd66PfCg==", "integrity": "sha512-qV5ZC0h7jYIAOhArFJgSfdyz6rALJyb270714o7ZtNnw2WSJ+eexhKtE0O8LYPRsHZHf2osHKZBxGPvm3kPkCA==",
"requires": { "requires": {
"original": "^1.0.0" "original": "^1.0.0"
} }

View File

@ -17,7 +17,7 @@ connection.start()
.catch(err => console.error(err)); .catch(err => console.error(err));
interface InfoCard { interface InfoCard {
html: string Content: string
} }
interface NowPlaying { interface NowPlaying {
@ -64,6 +64,12 @@ const app = Vue.createApp({
context.track.album.name, context.track.album.name,
context.track.album.artists[0].name context.track.album.artists[0].name
); );
connection.invoke("SendFacts",
context.track.name,
context.track.artists[0].name,
context.track.album.name,
context.track.album.artists[0].name
);
} }
}); });
@ -78,6 +84,12 @@ const app = Vue.createApp({
console.log(count); console.log(count);
this.playCount = count; this.playCount = count;
}); });
connection.on("OnNewCard", (card: InfoCard) => {
console.log(card);
this.cards.push(card);
});
} }
}); });

View File

@ -0,0 +1,29 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace Selector
{
public static class PlayDensity
{
public static decimal Density(this IEnumerable<Scrobble> scrobbles, DateTime from, DateTime to)
{
var filteredScrobbles = scrobbles.Where(s => s.Timestamp > from && s.Timestamp < to);
var dayDelta = (decimal) (to - from).Days;
return filteredScrobbles.Count() / dayDelta;
}
public static decimal Density(this IEnumerable<Scrobble> scrobbles)
{
var minDate = scrobbles.Select(s => s.Timestamp).Min();
var maxDate = scrobbles.Select(s => s.Timestamp).Max();
var dayDelta = (decimal) (maxDate - minDate).Days;
return scrobbles.Count() / dayDelta;
}
}
}