added play count puller, play count rendering in UI
This commit is contained in:
parent
e9f593862e
commit
ab058c769f
@ -55,6 +55,7 @@ namespace Selector.CLI
|
||||
Console.WriteLine("> Adding Last.fm credentials...");
|
||||
|
||||
services.AddLastFm(config.LastfmClient, config.LastfmSecret);
|
||||
services.AddCachingLastFm();
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@ -30,7 +30,7 @@ namespace Selector.Cache
|
||||
|
||||
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)
|
||||
|
@ -15,7 +15,7 @@ namespace Selector.Cache
|
||||
private readonly IPlayerWatcher Watcher;
|
||||
private readonly IDatabaseAsync Db;
|
||||
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; }
|
||||
|
||||
@ -35,7 +35,7 @@ namespace Selector.Cache
|
||||
{
|
||||
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)
|
||||
@ -57,6 +57,7 @@ namespace Selector.Cache
|
||||
if (watcher is IPlayerWatcher watcherCast)
|
||||
{
|
||||
watcherCast.ItemChange += Callback;
|
||||
watcherCast.PlayingChange += Callback;
|
||||
}
|
||||
else
|
||||
{
|
||||
@ -71,6 +72,7 @@ namespace Selector.Cache
|
||||
if (watcher is IPlayerWatcher watcherCast)
|
||||
{
|
||||
watcherCast.ItemChange -= Callback;
|
||||
watcherCast.PlayingChange -= Callback;
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@ -37,7 +37,7 @@ namespace Selector.Cache
|
||||
|
||||
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)
|
||||
|
@ -34,7 +34,7 @@ namespace Selector.Cache
|
||||
{
|
||||
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)
|
||||
|
@ -42,5 +42,10 @@ namespace Selector.Cache.Extensions
|
||||
{
|
||||
services.AddSingleton<AudioFeaturePuller>();
|
||||
}
|
||||
|
||||
public static void AddCachingLastFm(this IServiceCollection services)
|
||||
{
|
||||
services.AddSingleton<PlayCountPuller>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
174
Selector.Cache/Services/PlayCountPuller.cs
Normal file
174
Selector.Cache/Services/PlayCountPuller.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
@ -18,18 +18,26 @@ namespace Selector.Web.Hubs
|
||||
{
|
||||
public Task OnNewPlaying(CurrentlyPlayingDTO context);
|
||||
public Task OnNewAudioFeature(TrackAudioFeatures features);
|
||||
public Task OnNewPlayCount(PlayCount playCount);
|
||||
}
|
||||
|
||||
public class NowPlayingHub: Hub<INowPlayingHubClient>
|
||||
{
|
||||
private readonly IDatabaseAsync Cache;
|
||||
private readonly AudioFeaturePuller AudioFeaturePuller;
|
||||
private readonly PlayCountPuller PlayCountPuller;
|
||||
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;
|
||||
AudioFeaturePuller = puller;
|
||||
AudioFeaturePuller = featurePuller;
|
||||
PlayCountPuller = playCountPuller;
|
||||
Db = db;
|
||||
}
|
||||
|
||||
@ -69,5 +77,27 @@ namespace Selector.Web.Hubs
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -36,6 +36,8 @@ namespace Selector.Web
|
||||
/// Spotify callback for authentication
|
||||
/// </summary>
|
||||
public string SpotifyCallback { get; set; }
|
||||
public string LastfmClient { get; set; }
|
||||
public string LastfmSecret { get; set; }
|
||||
|
||||
public RedisOptions RedisOptions { get; set; } = new();
|
||||
|
||||
|
@ -13,6 +13,7 @@
|
||||
<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-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>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -99,6 +99,8 @@ namespace Selector.Web
|
||||
services.AddSpotify();
|
||||
services.AddCachingSpotify();
|
||||
|
||||
ConfigureLastFm(config, services);
|
||||
|
||||
services.AddCacheHubProxy();
|
||||
}
|
||||
|
||||
@ -131,5 +133,20 @@ namespace Selector.Web
|
||||
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...");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -10,12 +10,22 @@ export interface nowPlayingProxy {
|
||||
export interface NowPlayingHubClient {
|
||||
OnNewPlaying: (context: CurrentlyPlayingDTO) => void;
|
||||
OnNewAudioFeature: (features: TrackAudioFeatures) => void;
|
||||
OnNewPlayCount: (playCount: PlayCount) => void;
|
||||
}
|
||||
|
||||
export interface NowPlayingHub {
|
||||
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 {
|
||||
context: CurrentlyPlayingContextDTO;
|
||||
username: string;
|
||||
|
14
Selector.Web/scripts/Now/LastFm.ts
Normal file
14
Selector.Web/scripts/Now/LastFm.ts
Normal 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>
|
||||
`
|
||||
}
|
@ -1,8 +1,9 @@
|
||||
import * as signalR from "@microsoft/signalr";
|
||||
import * as Vue from "vue";
|
||||
import { TrackAudioFeatures, CurrentlyPlayingDTO } from "./HubInterfaces";
|
||||
import { TrackAudioFeatures, PlayCount, CurrentlyPlayingDTO } from "./HubInterfaces";
|
||||
import NowPlayingCard from "./Now/NowPlayingCard";
|
||||
import { AudioFeatureCard, AudioFeatureChartCard, PopularityCard, SpotifyLogoLink } from "./Now/Spotify";
|
||||
import { PlayCountCard } from "./Now/LastFm";
|
||||
import BaseInfoCard from "./Now/BaseInfoCard";
|
||||
|
||||
const connection = new signalR.HubConnectionBuilder()
|
||||
@ -22,6 +23,7 @@ interface InfoCard {
|
||||
interface NowPlaying {
|
||||
currentlyPlaying?: CurrentlyPlayingDTO,
|
||||
trackFeatures?: TrackAudioFeatures,
|
||||
playCount?: PlayCount,
|
||||
cards: InfoCard[]
|
||||
}
|
||||
|
||||
@ -30,6 +32,7 @@ const app = Vue.createApp({
|
||||
return {
|
||||
currentlyPlaying: undefined,
|
||||
trackFeatures: undefined,
|
||||
playCount: undefined,
|
||||
cards: []
|
||||
} as NowPlaying
|
||||
},
|
||||
@ -39,11 +42,18 @@ const app = Vue.createApp({
|
||||
console.log(context);
|
||||
this.currentlyPlaying = context;
|
||||
this.trackFeatures = null;
|
||||
this.playCount = null;
|
||||
this.cards = [];
|
||||
|
||||
if(context.track !== null && context.track !== undefined)
|
||||
{
|
||||
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);
|
||||
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("popularity", PopularityCard);
|
||||
app.component("spotify-logo", SpotifyLogoLink);
|
||||
app.component("play-count-card", PlayCountCard);
|
||||
const vm = app.mount('#app');
|
@ -37,7 +37,7 @@ namespace Selector
|
||||
{
|
||||
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)
|
||||
|
@ -52,7 +52,7 @@ namespace Selector
|
||||
{
|
||||
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)
|
||||
@ -62,7 +62,7 @@ namespace Selector
|
||||
Logger.LogTrace("Making Last.fm call");
|
||||
|
||||
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);
|
||||
// TODO: Null checking on credentials
|
||||
var userInfo = UserClient.GetInfoAsync(Credentials.Username);
|
||||
|
@ -29,6 +29,18 @@ namespace Selector.Extensions
|
||||
var lastAuth = new LastAuth(client, secret);
|
||||
services.AddSingleton(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)
|
||||
|
@ -2,7 +2,7 @@ using System;
|
||||
|
||||
using SpotifyAPI.Web;
|
||||
|
||||
namespace Selector.Cache {
|
||||
namespace Selector {
|
||||
|
||||
public class CurrentlyPlayingDTO {
|
||||
public CurrentlyPlayingContextDTO Context { get; set; }
|
Loading…
Reference in New Issue
Block a user