added play count puller, play count rendering in UI

This commit is contained in:
andy 2021-11-30 20:38:26 +00:00
parent e9f593862e
commit ab058c769f
19 changed files with 297 additions and 12 deletions

View File

@ -55,6 +55,7 @@ namespace Selector.CLI
Console.WriteLine("> Adding Last.fm credentials..."); Console.WriteLine("> Adding Last.fm credentials...");
services.AddLastFm(config.LastfmClient, config.LastfmSecret); services.AddLastFm(config.LastfmClient, config.LastfmSecret);
services.AddCachingLastFm();
} }
else else
{ {

View File

@ -30,7 +30,7 @@ namespace Selector.Cache
public void CacheCallback(object sender, AnalysedTrack e) public void CacheCallback(object sender, AnalysedTrack e)
{ {
Task.Run(() => { return AsyncCacheCallback(e); }, CancelToken); Task.Run(async () => { await AsyncCacheCallback(e); }, CancelToken);
} }
public async Task AsyncCacheCallback(AnalysedTrack e) public async Task AsyncCacheCallback(AnalysedTrack e)

View File

@ -15,7 +15,7 @@ namespace Selector.Cache
private readonly IPlayerWatcher Watcher; private readonly IPlayerWatcher Watcher;
private readonly IDatabaseAsync Db; private readonly IDatabaseAsync Db;
private readonly ILogger<CacheWriter> Logger; private readonly ILogger<CacheWriter> Logger;
public TimeSpan CacheExpiry { get; set; } = TimeSpan.FromMinutes(10); public TimeSpan CacheExpiry { get; set; } = TimeSpan.FromMinutes(20);
public CancellationToken CancelToken { get; set; } public CancellationToken CancelToken { get; set; }
@ -35,7 +35,7 @@ namespace Selector.Cache
{ {
if (e.Current is null) return; if (e.Current is null) return;
Task.Run(() => { return AsyncCallback(e); }, CancelToken); Task.Run(async () => { await AsyncCallback(e); }, CancelToken);
} }
public async Task AsyncCallback(ListeningChangeEventArgs e) public async Task AsyncCallback(ListeningChangeEventArgs e)
@ -57,6 +57,7 @@ namespace Selector.Cache
if (watcher is IPlayerWatcher watcherCast) if (watcher is IPlayerWatcher watcherCast)
{ {
watcherCast.ItemChange += Callback; watcherCast.ItemChange += Callback;
watcherCast.PlayingChange += Callback;
} }
else else
{ {
@ -71,6 +72,7 @@ namespace Selector.Cache
if (watcher is IPlayerWatcher watcherCast) if (watcher is IPlayerWatcher watcherCast)
{ {
watcherCast.ItemChange -= Callback; watcherCast.ItemChange -= Callback;
watcherCast.PlayingChange -= Callback;
} }
else else
{ {

View File

@ -37,7 +37,7 @@ namespace Selector.Cache
public void CacheCallback(object sender, PlayCount e) public void CacheCallback(object sender, PlayCount e)
{ {
Task.Run(() => { return AsyncCacheCallback(e); }, CancelToken); Task.Run(async () => { await AsyncCacheCallback(e); }, CancelToken);
} }
public async Task AsyncCacheCallback(PlayCount e) public async Task AsyncCacheCallback(PlayCount e)

View File

@ -34,7 +34,7 @@ namespace Selector.Cache
{ {
if (e.Current is null) return; if (e.Current is null) return;
Task.Run(() => { return AsyncCallback(e); }, CancelToken); Task.Run(async () => { await AsyncCallback(e); }, CancelToken);
} }
public async Task AsyncCallback(ListeningChangeEventArgs e) public async Task AsyncCallback(ListeningChangeEventArgs e)

View File

@ -42,5 +42,10 @@ namespace Selector.Cache.Extensions
{ {
services.AddSingleton<AudioFeaturePuller>(); services.AddSingleton<AudioFeaturePuller>();
} }
public static void AddCachingLastFm(this IServiceCollection services)
{
services.AddSingleton<PlayCountPuller>();
}
} }
} }

View File

@ -0,0 +1,174 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using IF.Lastfm.Core.Api;
using IF.Lastfm.Core.Api.Helpers;
using IF.Lastfm.Core.Objects;
using StackExchange.Redis;
namespace Selector.Cache
{
public class PlayCountPuller
{
private readonly IDatabaseAsync Cache;
private readonly ILogger<PlayCountPuller> Logger;
protected readonly ITrackApi TrackClient;
protected readonly IAlbumApi AlbumClient;
protected readonly IArtistApi ArtistClient;
protected readonly IUserApi UserClient;
public PlayCountPuller(
IDatabaseAsync cache,
ILogger<PlayCountPuller> logger,
ITrackApi trackClient,
IAlbumApi albumClient,
IArtistApi artistClient,
IUserApi userClient
)
{
Cache = cache;
Logger = logger;
TrackClient = trackClient;
AlbumClient = albumClient;
ArtistClient = artistClient;
UserClient = userClient;
}
public async Task<PlayCount> Get(string username, string track, string artist, string album, string albumArtist)
{
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 cacheTasks = new Task[] { trackCache, albumCache, artistCache, userCache };
await Task.WhenAll(cacheTasks);
PlayCount playCount = new()
{
Username = username
};
Task<LastResponse<LastTrack>> trackHttp = null;
Task<LastResponse<LastAlbum>> albumHttp = null;
Task<LastResponse<LastArtist>> artistHttp = null;
Task<LastResponse<LastUser>> userHttp = null;
if (trackCache.IsCompletedSuccessfully)
{
if(trackCache.Result == RedisValue.Null)
{
trackHttp = TrackClient.GetInfoAsync(track, artist, username);
}
else
{
playCount.Track = (int) trackCache.Result;
}
}
else
{
trackHttp = TrackClient.GetInfoAsync(track, artist, username);
}
if (albumCache.IsCompletedSuccessfully)
{
if (albumCache.Result == RedisValue.Null)
{
albumHttp = AlbumClient.GetInfoAsync(albumArtist, album, username: username);
}
else
{
playCount.Album = (int)albumCache.Result;
}
}
else
{
albumHttp = AlbumClient.GetInfoAsync(albumArtist, album, username: username);
}
if (artistCache.IsCompletedSuccessfully)
{
if (artistCache.Result == RedisValue.Null)
{
artistHttp = ArtistClient.GetInfoAsync(artist);
}
else
{
playCount.Artist = (int)artistCache.Result;
}
}
else
{
artistHttp = ArtistClient.GetInfoAsync(artist);
}
if (userCache.IsCompletedSuccessfully)
{
if (userCache.Result == RedisValue.Null)
{
userHttp = UserClient.GetInfoAsync(username);
}
else
{
playCount.User = (int)userCache.Result;
}
}
else
{
userHttp = UserClient.GetInfoAsync(username);
}
await Task.WhenAll(new Task[] {trackHttp, albumHttp, artistHttp, userHttp}.Where(t => t is not null));
if (trackHttp is not null && trackHttp.IsCompletedSuccessfully)
{
if (trackHttp.Result.Success)
{
playCount.Track = trackHttp.Result.Content.UserPlayCount;
}
else
{
Logger.LogDebug($"Track info error [{username}] [{trackHttp.Result.Status}]");
}
}
if (albumHttp is not null && albumHttp.IsCompletedSuccessfully)
{
if (albumHttp.Result.Success)
{
playCount.Album = albumHttp.Result.Content.UserPlayCount;
}
else
{
Logger.LogDebug($"Album info error [{username}] [{albumHttp.Result.Status}]");
}
}
//TODO: Add artist count
if (userHttp is not null && userHttp.IsCompletedSuccessfully)
{
if (userHttp.Result.Success)
{
playCount.User = userHttp.Result.Content.Playcount;
}
else
{
Logger.LogDebug($"User info error [{username}] [{userHttp.Result.Status}]");
}
}
return playCount;
}
}
}

View File

@ -18,18 +18,26 @@ 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 class NowPlayingHub: Hub<INowPlayingHubClient> public class NowPlayingHub: Hub<INowPlayingHubClient>
{ {
private readonly IDatabaseAsync Cache; private readonly IDatabaseAsync Cache;
private readonly AudioFeaturePuller AudioFeaturePuller; private readonly AudioFeaturePuller AudioFeaturePuller;
private readonly PlayCountPuller PlayCountPuller;
private readonly ApplicationDbContext Db; private readonly ApplicationDbContext Db;
public NowPlayingHub(IDatabaseAsync cache, AudioFeaturePuller puller, ApplicationDbContext db) public NowPlayingHub(
IDatabaseAsync cache,
AudioFeaturePuller featurePuller,
ApplicationDbContext db,
PlayCountPuller playCountPuller = null
)
{ {
Cache = cache; Cache = cache;
AudioFeaturePuller = puller; AudioFeaturePuller = featurePuller;
PlayCountPuller = playCountPuller;
Db = db; Db = db;
} }
@ -69,5 +77,27 @@ namespace Selector.Web.Hubs
await Clients.Caller.OnNewAudioFeature(feature); await Clients.Caller.OnNewAudioFeature(feature);
} }
} }
public async Task SendPlayCount(string track, string artist, string album, string albumArtist)
{
if(PlayCountPuller is not null)
{
var user = Db.Users
.AsNoTracking()
.Where(u => u.Id == Context.UserIdentifier)
.SingleOrDefault()
?? throw new SqlNullValueException("No user returned");
if(!string.IsNullOrWhiteSpace(user.LastFmUsername))
{
var playCount = await PlayCountPuller.Get(user.LastFmUsername, track, artist, album, albumArtist);
if (playCount is not null)
{
await Clients.Caller.OnNewPlayCount(playCount);
}
}
}
}
} }
} }

View File

@ -36,6 +36,8 @@ namespace Selector.Web
/// Spotify callback for authentication /// Spotify callback for authentication
/// </summary> /// </summary>
public string SpotifyCallback { get; set; } public string SpotifyCallback { get; set; }
public string LastfmClient { get; set; }
public string LastfmSecret { get; set; }
public RedisOptions RedisOptions { get; set; } = new(); public RedisOptions RedisOptions { get; set; } = new();

View File

@ -13,6 +13,7 @@
<popularity :track="currentlyPlaying.track" v-if="currentlyPlaying !== null && currentlyPlaying !== undefined && currentlyPlaying.track != null && currentlyPlaying.track != undefined" ></popularity> <popularity :track="currentlyPlaying.track" v-if="currentlyPlaying !== null && currentlyPlaying !== undefined && currentlyPlaying.track != null && currentlyPlaying.track != undefined" ></popularity>
<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" 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.html"></info-card>
</div> </div>
</div> </div>

View File

@ -99,6 +99,8 @@ namespace Selector.Web
services.AddSpotify(); services.AddSpotify();
services.AddCachingSpotify(); services.AddCachingSpotify();
ConfigureLastFm(config, services);
services.AddCacheHubProxy(); services.AddCacheHubProxy();
} }
@ -131,5 +133,20 @@ namespace Selector.Web
endpoints.MapHub<NowPlayingHub>("/hub"); endpoints.MapHub<NowPlayingHub>("/hub");
}); });
} }
public static void ConfigureLastFm(RootOptions config, IServiceCollection services)
{
if (config.LastfmClient is not null)
{
Console.WriteLine("> Adding Last.fm credentials...");
services.AddLastFm(config.LastfmClient, config.LastfmSecret);
services.AddCachingLastFm();
}
else
{
Console.WriteLine("> No Last.fm credentials, skipping init...");
}
}
} }
} }

View File

@ -10,12 +10,22 @@ export interface nowPlayingProxy {
export interface NowPlayingHubClient { export interface NowPlayingHubClient {
OnNewPlaying: (context: CurrentlyPlayingDTO) => void; OnNewPlaying: (context: CurrentlyPlayingDTO) => void;
OnNewAudioFeature: (features: TrackAudioFeatures) => void; OnNewAudioFeature: (features: TrackAudioFeatures) => void;
OnNewPlayCount: (playCount: PlayCount) => void;
} }
export interface NowPlayingHub { export interface NowPlayingHub {
SendNewPlaying(context: CurrentlyPlayingDTO): void; SendNewPlaying(context: CurrentlyPlayingDTO): void;
} }
export interface PlayCount {
track: number | null;
album: number | null;
artist: number | null;
user: number | null;
username: string;
listeningEvent: ListeningChangeEventArgs;
}
export interface CurrentlyPlayingDTO { export interface CurrentlyPlayingDTO {
context: CurrentlyPlayingContextDTO; context: CurrentlyPlayingContextDTO;
username: string; username: string;

View File

@ -0,0 +1,14 @@
import * as Vue from "vue";
export let PlayCountCard: Vue.Component = {
props: ['count'],
template:
`
<div class="card info-card">
<h5 v-if="count.track != null && count.track != undefined" >Track: {{ count.track.toLocaleString() }}</h5>
<h5 v-if="count.album != null && count.album != undefined" >Album: {{ count.album.toLocaleString() }}</h5>
<h5 v-if="count.artist != null && count.artist != undefined" >Artist: {{ count.artist.toLocaleString() }}</h5>
<h5 v-if="count.user != null && count.user != undefined" >User: {{ count.user.toLocaleString() }}</h5>
</div>
`
}

View File

@ -1,8 +1,9 @@
import * as signalR from "@microsoft/signalr"; import * as signalR from "@microsoft/signalr";
import * as Vue from "vue"; import * as Vue from "vue";
import { TrackAudioFeatures, CurrentlyPlayingDTO } from "./HubInterfaces"; import { TrackAudioFeatures, PlayCount, CurrentlyPlayingDTO } from "./HubInterfaces";
import NowPlayingCard from "./Now/NowPlayingCard"; import NowPlayingCard from "./Now/NowPlayingCard";
import { AudioFeatureCard, AudioFeatureChartCard, PopularityCard, SpotifyLogoLink } from "./Now/Spotify"; import { AudioFeatureCard, AudioFeatureChartCard, PopularityCard, SpotifyLogoLink } from "./Now/Spotify";
import { PlayCountCard } from "./Now/LastFm";
import BaseInfoCard from "./Now/BaseInfoCard"; import BaseInfoCard from "./Now/BaseInfoCard";
const connection = new signalR.HubConnectionBuilder() const connection = new signalR.HubConnectionBuilder()
@ -22,6 +23,7 @@ interface InfoCard {
interface NowPlaying { interface NowPlaying {
currentlyPlaying?: CurrentlyPlayingDTO, currentlyPlaying?: CurrentlyPlayingDTO,
trackFeatures?: TrackAudioFeatures, trackFeatures?: TrackAudioFeatures,
playCount?: PlayCount,
cards: InfoCard[] cards: InfoCard[]
} }
@ -30,6 +32,7 @@ const app = Vue.createApp({
return { return {
currentlyPlaying: undefined, currentlyPlaying: undefined,
trackFeatures: undefined, trackFeatures: undefined,
playCount: undefined,
cards: [] cards: []
} as NowPlaying } as NowPlaying
}, },
@ -39,11 +42,18 @@ const app = Vue.createApp({
console.log(context); console.log(context);
this.currentlyPlaying = context; this.currentlyPlaying = context;
this.trackFeatures = null; this.trackFeatures = null;
this.playCount = null;
this.cards = []; this.cards = [];
if(context.track !== null && context.track !== undefined) if(context.track !== null && context.track !== undefined)
{ {
connection.invoke("SendAudioFeatures", context.track.id); connection.invoke("SendAudioFeatures", context.track.id);
connection.invoke("SendPlayCount",
context.track.name,
context.track.artists[0].name,
context.track.album.name,
context.track.album.artists[0].name
);
} }
}); });
@ -52,6 +62,12 @@ const app = Vue.createApp({
console.log(feature); console.log(feature);
this.trackFeatures = feature; this.trackFeatures = feature;
}); });
connection.on("OnNewPlayCount", (count: PlayCount) => {
console.log(count);
this.playCount = count;
});
} }
}); });
@ -61,4 +77,5 @@ app.component("audio-feature-chart-card", AudioFeatureChartCard);
app.component("info-card", BaseInfoCard); app.component("info-card", BaseInfoCard);
app.component("popularity", PopularityCard); app.component("popularity", PopularityCard);
app.component("spotify-logo", SpotifyLogoLink); app.component("spotify-logo", SpotifyLogoLink);
app.component("play-count-card", PlayCountCard);
const vm = app.mount('#app'); const vm = app.mount('#app');

View File

@ -37,7 +37,7 @@ namespace Selector
{ {
if (e.Current is null) return; if (e.Current is null) return;
Task.Run(() => { return AsyncCallback(e); }, CancelToken); Task.Run(async () => { await AsyncCallback(e); }, CancelToken);
} }
public async Task AsyncCallback(ListeningChangeEventArgs e) public async Task AsyncCallback(ListeningChangeEventArgs e)

View File

@ -52,7 +52,7 @@ namespace Selector
{ {
if (e.Current is null) return; if (e.Current is null) return;
Task.Run(() => { return AsyncCallback(e); }, CancelToken); Task.Run(async () => { await AsyncCallback(e); }, CancelToken);
} }
public async Task AsyncCallback(ListeningChangeEventArgs e) public async Task AsyncCallback(ListeningChangeEventArgs e)
@ -62,7 +62,7 @@ namespace Selector
Logger.LogTrace("Making Last.fm call"); Logger.LogTrace("Making Last.fm call");
var trackInfo = TrackClient.GetInfoAsync(track.Name, track.Artists[0].Name, username: Credentials?.Username); var trackInfo = TrackClient.GetInfoAsync(track.Name, track.Artists[0].Name, username: Credentials?.Username);
var albumInfo = AlbumClient.GetInfoAsync(track.Album.Name, track.Album.Artists[0].Name, username: Credentials?.Username); var albumInfo = AlbumClient.GetInfoAsync(track.Album.Artists[0].Name, track.Album.Name, username: Credentials?.Username);
var artistInfo = ArtistClient.GetInfoAsync(track.Artists[0].Name); var artistInfo = ArtistClient.GetInfoAsync(track.Artists[0].Name);
// TODO: Null checking on credentials // TODO: Null checking on credentials
var userInfo = UserClient.GetInfoAsync(Credentials.Username); var userInfo = UserClient.GetInfoAsync(Credentials.Username);

View File

@ -29,6 +29,18 @@ namespace Selector.Extensions
var lastAuth = new LastAuth(client, secret); var lastAuth = new LastAuth(client, secret);
services.AddSingleton(lastAuth); services.AddSingleton(lastAuth);
services.AddTransient(sp => new LastfmClient(sp.GetService<LastAuth>())); services.AddTransient(sp => new LastfmClient(sp.GetService<LastAuth>()));
services.AddTransient<ITrackApi>(sp => sp.GetService<LastfmClient>().Track);
services.AddTransient<IAlbumApi>(sp => sp.GetService<LastfmClient>().Album);
services.AddTransient<IArtistApi>(sp => sp.GetService<LastfmClient>().Artist);
services.AddTransient<IUserApi>(sp => sp.GetService<LastfmClient>().User);
services.AddTransient<IChartApi>(sp => sp.GetService<LastfmClient>().Chart);
services.AddTransient<ILibraryApi>(sp => sp.GetService<LastfmClient>().Library);
services.AddTransient<ITagApi>(sp => sp.GetService<LastfmClient>().Tag);
} }
public static void AddWatcher(this IServiceCollection services) public static void AddWatcher(this IServiceCollection services)

View File

@ -2,7 +2,7 @@ using System;
using SpotifyAPI.Web; using SpotifyAPI.Web;
namespace Selector.Cache { namespace Selector {
public class CurrentlyPlayingDTO { public class CurrentlyPlayingDTO {
public CurrentlyPlayingContextDTO Context { get; set; } public CurrentlyPlayingContextDTO Context { get; set; }