caching working, caching audio feature injector, added timeline to event

This commit is contained in:
andy 2021-10-29 22:35:34 +01:00
parent b0467c3df9
commit a3510c05ed
14 changed files with 191 additions and 29 deletions

View File

@ -71,7 +71,7 @@ namespace Selector.CLI
enum Consumers enum Consumers
{ {
AudioFeatures, CacheWriter, Publisher AudioFeatures, AudioFeaturesCache, CacheWriter, Publisher
} }
class DatabaseOptions { class DatabaseOptions {

View File

@ -114,6 +114,11 @@ namespace Selector.CLI
consumers.Add(await featureInjector.Get(spotifyFactory)); consumers.Add(await featureInjector.Get(spotifyFactory));
break; break;
case Consumers.AudioFeaturesCache:
var featureInjectorCache = new CachingAudioFeatureInjectorFactory(LoggerFactory, Cache);
consumers.Add(await featureInjectorCache.Get(spotifyFactory));
break;
case Consumers.CacheWriter: case Consumers.CacheWriter:
var cacheWriter = new CacheWriterFactory(Cache, LoggerFactory); var cacheWriter = new CacheWriterFactory(Cache, LoggerFactory);
consumers.Add(await cacheWriter.Get()); consumers.Add(await cacheWriter.Get());

View File

@ -9,7 +9,7 @@
"name": "Player Watcher", "name": "Player Watcher",
"type": "player", "type": "player",
"pollperiod": 2000, "pollperiod": 2000,
"consumers": [ "audiofeatures", "cachewriter" ] "consumers": [ "audiofeaturescache", "cachewriter", "publisher" ]
} }
] ]
}, },

View File

@ -0,0 +1,48 @@
using System;
using System.Collections.Generic;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using SpotifyAPI.Web;
using StackExchange.Redis;
namespace Selector.Cache
{
public class CachingAudioFeatureInjector : AudioFeatureInjector
{
private readonly IDatabaseAsync Db;
public TimeSpan CacheExpiry { get; set; } = TimeSpan.FromDays(1);
public CachingAudioFeatureInjector(
IPlayerWatcher watcher,
IDatabaseAsync db,
ITracksClient trackClient,
ILogger<CachingAudioFeatureInjector> logger = null,
CancellationToken token = default
) : base(watcher, trackClient, logger, token) {
Db = db;
NewFeature += CacheCallback;
}
public void CacheCallback(object sender, AnalysedTrack e)
{
Task.Run(() => { return AsyncCacheCallback(e); }, CancelToken);
}
public async Task AsyncCacheCallback(AnalysedTrack e)
{
var payload = JsonSerializer.Serialize(e);
Logger.LogTrace($"Caching current for [{e.Track.DisplayString()}]");
var resp = await Db.StringSetAsync(Key.AudioFeature(e.Track.Id), payload, expiry: CacheExpiry);
Logger.LogDebug($"Cached audio feature for [{e.Track.DisplayString()}], {(resp ? "value set" : "value NOT set")}");
}
}
}

View File

@ -5,7 +5,7 @@ using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Logging.Abstractions;
using SpotifyAPI.Web;
using StackExchange.Redis; using StackExchange.Redis;
namespace Selector.Cache namespace Selector.Cache
@ -39,8 +39,14 @@ namespace Selector.Cache
public async Task AsyncCallback(ListeningChangeEventArgs e) public async Task AsyncCallback(ListeningChangeEventArgs e)
{ {
var payload = JsonSerializer.Serialize(e); var payload = JsonSerializer.Serialize((CurrentlyPlayingDTO) e);
await Db.StringSetAsync(Key.CurrentlyPlaying(e.Username), payload);
Logger.LogTrace($"Caching current for [{e.Username}]");
var resp = await Db.StringSetAsync(Key.CurrentlyPlaying(e.Username), payload);
Logger.LogDebug($"Cached current for [{e.Username}], {(resp ? "value set" : "value NOT set")}");
} }
public void Subscribe(IWatcher watch = null) public void Subscribe(IWatcher watch = null)

View File

@ -0,0 +1,43 @@
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using SpotifyAPI.Web;
using StackExchange.Redis;
namespace Selector.Cache
{
public interface ICachingAudioFeatureInjectorFactory
{
public Task<IConsumer> Get(ISpotifyConfigFactory spotifyFactory, IPlayerWatcher watcher);
}
public class CachingAudioFeatureInjectorFactory: ICachingAudioFeatureInjectorFactory {
private readonly ILoggerFactory LoggerFactory;
private readonly IDatabaseAsync Db;
public CachingAudioFeatureInjectorFactory(
ILoggerFactory loggerFactory,
IDatabaseAsync db
) {
LoggerFactory = loggerFactory;
Db = db;
}
public async Task<IConsumer> Get(ISpotifyConfigFactory spotifyFactory, IPlayerWatcher watcher = null)
{
var config = await spotifyFactory.GetConfig();
var client = new SpotifyClient(config);
return new CachingAudioFeatureInjector(
watcher,
Db,
client.Tracks,
LoggerFactory.CreateLogger<CachingAudioFeatureInjector>()
);
}
}
}

View File

@ -5,7 +5,7 @@ using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Logging.Abstractions;
using SpotifyAPI.Web;
using StackExchange.Redis; using StackExchange.Redis;
namespace Selector.Cache namespace Selector.Cache
@ -39,8 +39,13 @@ namespace Selector.Cache
public async Task AsyncCallback(ListeningChangeEventArgs e) public async Task AsyncCallback(ListeningChangeEventArgs e)
{ {
var payload = JsonSerializer.Serialize(e); var payload = JsonSerializer.Serialize((CurrentlyPlayingDTO) e);
await Subscriber.PublishAsync(Key.CurrentlyPlaying(e.Username), payload);
Logger.LogTrace($"Publishing current for [{e.Username}]");
var receivers = await Subscriber.PublishAsync(Key.CurrentlyPlaying(e.Username), payload);
Logger.LogDebug($"Published current for [{e.Username}], {receivers} receivers");
} }
public void Subscribe(IWatcher watch = null) public void Subscribe(IWatcher watch = null)

40
Selector.Cache/DTO.cs Normal file
View File

@ -0,0 +1,40 @@
using System;
using SpotifyAPI.Web;
namespace Selector.Cache {
public class CurrentlyPlayingDTO {
public CurrentlyPlayingContext Context { get; set; }
public string Username { get; set; }
public FullTrack Track { get; set; }
public FullEpisode Episode { get; set; }
public static explicit operator CurrentlyPlayingDTO(ListeningChangeEventArgs e)
{
if(e.Current.Item is FullTrack track)
{
return new()
{
Context = e.Current,
Username = e.Username,
Track = track
};
}
else if (e.Current.Item is FullEpisode episode)
{
return new()
{
Context = e.Current,
Username = e.Username,
Episode = episode
};
}
else
{
throw new ArgumentException("Unknown item item");
}
}
}
}

View File

@ -7,8 +7,11 @@ namespace Selector.Cache
public class Key public class Key
{ {
public const string CurrentlyPlayingName = "CurrentlyPlaying"; public const string CurrentlyPlayingName = "CurrentlyPlaying";
public const string TrackName = "Track";
public const string AudioFeatureName = "AudioFeature";
public static string CurrentlyPlaying(string user) => Namespace(new[] { user, CurrentlyPlayingName }); public static string CurrentlyPlaying(string user) => Namespace(new[] { user, CurrentlyPlayingName });
public static string AudioFeature(string trackId) => Namespace(new[] { TrackName, trackId, AudioFeatureName });
public static string Namespace(string[] args) => string.Join(":", args); public static string Namespace(string[] args) => string.Join(":", args);
} }

View File

@ -11,9 +11,11 @@ namespace Selector
{ {
public class AudioFeatureInjector : IConsumer public class AudioFeatureInjector : IConsumer
{ {
private readonly IPlayerWatcher Watcher; protected readonly IPlayerWatcher Watcher;
private readonly ITracksClient TrackClient; protected readonly ITracksClient TrackClient;
private readonly ILogger<AudioFeatureInjector> Logger; protected readonly ILogger<AudioFeatureInjector> Logger;
protected event EventHandler<AnalysedTrack> NewFeature;
public CancellationToken CancelToken { get; set; } public CancellationToken CancelToken { get; set; }
@ -47,7 +49,10 @@ namespace Selector
var audioFeatures = await TrackClient.GetAudioFeatures(track.Id); var audioFeatures = await TrackClient.GetAudioFeatures(track.Id);
Logger.LogDebug($"Adding audio features [{track.DisplayString()}]: [{audioFeatures.DisplayString()}]"); Logger.LogDebug($"Adding audio features [{track.DisplayString()}]: [{audioFeatures.DisplayString()}]");
Timeline.Add(AnalysedTrack.From(track, audioFeatures), DateHelper.FromUnixMilli(e.Current.Timestamp)); var analysedTrack = AnalysedTrack.From(track, audioFeatures);
Timeline.Add(analysedTrack, DateHelper.FromUnixMilli(e.Current.Timestamp));
OnNewFeature(analysedTrack);
} }
catch (APIUnauthorizedException ex) catch (APIUnauthorizedException ex)
{ {
@ -102,6 +107,11 @@ namespace Selector
throw new ArgumentException("Provided watcher is not a PlayerWatcher"); throw new ArgumentException("Provided watcher is not a PlayerWatcher");
} }
} }
protected virtual void OnNewFeature(AnalysedTrack args)
{
NewFeature?.Invoke(this, args);
}
} }
public class AnalysedTrack { public class AnalysedTrack {

View File

@ -4,16 +4,18 @@ using SpotifyAPI.Web;
namespace Selector namespace Selector
{ {
public class ListeningChangeEventArgs: EventArgs { public class ListeningChangeEventArgs: EventArgs {
public CurrentlyPlayingContext Previous; public CurrentlyPlayingContext Previous { get; set; }
public CurrentlyPlayingContext Current; public CurrentlyPlayingContext Current { get; set; }
public string Username; public string Username { get; set; }
PlayerTimeline Timeline { get; set; }
public static ListeningChangeEventArgs From(CurrentlyPlayingContext previous, CurrentlyPlayingContext current, string username = null) public static ListeningChangeEventArgs From(CurrentlyPlayingContext previous, CurrentlyPlayingContext current, PlayerTimeline timeline, string username = null)
{ {
return new ListeningChangeEventArgs() return new ListeningChangeEventArgs()
{ {
Previous = previous, Previous = previous,
Current = current, Current = current,
Timeline = timeline,
Username = username Username = username
}; };
} }

View File

@ -74,14 +74,14 @@ namespace Selector
&& (Live.Item is FullTrack || Live.Item is FullEpisode)) && (Live.Item is FullTrack || Live.Item is FullEpisode))
{ {
Logger.LogDebug($"Playback started: {Live.DisplayString()}"); Logger.LogDebug($"Playback started: {Live.DisplayString()}");
OnPlayingChange(ListeningChangeEventArgs.From(previous, Live, Username)); OnPlayingChange(ListeningChangeEventArgs.From(previous, Live, Past, Username));
} }
// STOPPED PLAYBACK // STOPPED PLAYBACK
else if((previous.Item is FullTrack || previous.Item is FullEpisode) else if((previous.Item is FullTrack || previous.Item is FullEpisode)
&& Live is null) && Live is null)
{ {
Logger.LogDebug($"Playback stopped: {previous.DisplayString()}"); Logger.LogDebug($"Playback stopped: {previous.DisplayString()}");
OnPlayingChange(ListeningChangeEventArgs.From(previous, Live, Username)); OnPlayingChange(ListeningChangeEventArgs.From(previous, Live, Past, Username));
} }
// CONTINUING PLAYBACK // CONTINUING PLAYBACK
else { else {
@ -92,17 +92,17 @@ namespace Selector
{ {
if(!eq.IsEqual(previousTrack, currentTrack)) { if(!eq.IsEqual(previousTrack, currentTrack)) {
Logger.LogDebug($"Track changed: {previousTrack.DisplayString()} -> {currentTrack.DisplayString()}"); Logger.LogDebug($"Track changed: {previousTrack.DisplayString()} -> {currentTrack.DisplayString()}");
OnItemChange(ListeningChangeEventArgs.From(previous, Live, Username)); OnItemChange(ListeningChangeEventArgs.From(previous, Live, Past, Username));
} }
if(!eq.IsEqual(previousTrack.Album, currentTrack.Album)) { if(!eq.IsEqual(previousTrack.Album, currentTrack.Album)) {
Logger.LogDebug($"Album changed: {previousTrack.Album.DisplayString()} -> {currentTrack.Album.DisplayString()}"); Logger.LogDebug($"Album changed: {previousTrack.Album.DisplayString()} -> {currentTrack.Album.DisplayString()}");
OnAlbumChange(ListeningChangeEventArgs.From(previous, Live, Username)); OnAlbumChange(ListeningChangeEventArgs.From(previous, Live, Past, Username));
} }
if(!eq.IsEqual(previousTrack.Artists[0], currentTrack.Artists[0])) { if(!eq.IsEqual(previousTrack.Artists[0], currentTrack.Artists[0])) {
Logger.LogDebug($"Artist changed: {previousTrack.Artists.DisplayString()} -> {currentTrack.Artists.DisplayString()}"); Logger.LogDebug($"Artist changed: {previousTrack.Artists.DisplayString()} -> {currentTrack.Artists.DisplayString()}");
OnArtistChange(ListeningChangeEventArgs.From(previous, Live, Username)); OnArtistChange(ListeningChangeEventArgs.From(previous, Live, Past, Username));
} }
} }
// CHANGED CONTENT // CHANGED CONTENT
@ -110,8 +110,8 @@ namespace Selector
|| (previous.Item is FullEpisode && Live.Item is FullTrack)) || (previous.Item is FullEpisode && Live.Item is FullTrack))
{ {
Logger.LogDebug($"Media type changed: {previous.Item}, {previous.Item}"); Logger.LogDebug($"Media type changed: {previous.Item}, {previous.Item}");
OnContentChange(ListeningChangeEventArgs.From(previous, Live, Username)); OnContentChange(ListeningChangeEventArgs.From(previous, Live, Past, Username));
OnItemChange(ListeningChangeEventArgs.From(previous, Live, Username)); OnItemChange(ListeningChangeEventArgs.From(previous, Live, Past, Username));
} }
// PODCASTS // PODCASTS
else if(previous.Item is FullEpisode previousEp else if(previous.Item is FullEpisode previousEp
@ -119,7 +119,7 @@ namespace Selector
{ {
if(!eq.IsEqual(previousEp, currentEp)) { if(!eq.IsEqual(previousEp, currentEp)) {
Logger.LogDebug($"Podcast changed: {previousEp.DisplayString()} -> {currentEp.DisplayString()}"); Logger.LogDebug($"Podcast changed: {previousEp.DisplayString()} -> {currentEp.DisplayString()}");
OnItemChange(ListeningChangeEventArgs.From(previous, Live, Username)); OnItemChange(ListeningChangeEventArgs.From(previous, Live, Past, Username));
} }
} }
else { else {
@ -129,25 +129,25 @@ namespace Selector
// CONTEXT // CONTEXT
if(!eq.IsEqual(previous.Context, Live.Context)) { if(!eq.IsEqual(previous.Context, Live.Context)) {
Logger.LogDebug($"Context changed: {previous.Context.DisplayString()} -> {Live.Context.DisplayString()}"); Logger.LogDebug($"Context changed: {previous.Context.DisplayString()} -> {Live.Context.DisplayString()}");
OnContextChange(ListeningChangeEventArgs.From(previous, Live, Username)); OnContextChange(ListeningChangeEventArgs.From(previous, Live, Past, Username));
} }
// DEVICE // DEVICE
if(!eq.IsEqual(previous?.Device, Live?.Device)) { if(!eq.IsEqual(previous?.Device, Live?.Device)) {
Logger.LogDebug($"Device changed: {previous?.Device.DisplayString()} -> {Live?.Device.DisplayString()}"); Logger.LogDebug($"Device changed: {previous?.Device.DisplayString()} -> {Live?.Device.DisplayString()}");
OnDeviceChange(ListeningChangeEventArgs.From(previous, Live, Username)); OnDeviceChange(ListeningChangeEventArgs.From(previous, Live, Past, Username));
} }
// IS PLAYING // IS PLAYING
if(previous.IsPlaying != Live.IsPlaying) { if(previous.IsPlaying != Live.IsPlaying) {
Logger.LogDebug($"Playing state changed: {previous.IsPlaying} -> {Live.IsPlaying}"); Logger.LogDebug($"Playing state changed: {previous.IsPlaying} -> {Live.IsPlaying}");
OnPlayingChange(ListeningChangeEventArgs.From(previous, Live, Username)); OnPlayingChange(ListeningChangeEventArgs.From(previous, Live, Past, Username));
} }
// VOLUME // VOLUME
if(previous.Device.VolumePercent != Live.Device.VolumePercent) { if(previous.Device.VolumePercent != Live.Device.VolumePercent) {
Logger.LogDebug($"Volume changed: {previous.Device.VolumePercent}% -> {Live.Device.VolumePercent}%"); Logger.LogDebug($"Volume changed: {previous.Device.VolumePercent}% -> {Live.Device.VolumePercent}%");
OnVolumeChange(ListeningChangeEventArgs.From(previous, Live, Username)); OnVolumeChange(ListeningChangeEventArgs.From(previous, Live, Past, Username));
} }
} }
} }