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
{
AudioFeatures, CacheWriter, Publisher
AudioFeatures, AudioFeaturesCache, CacheWriter, Publisher
}
class DatabaseOptions {

View File

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

View File

@ -9,7 +9,7 @@
"name": "Player Watcher",
"type": "player",
"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 Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using SpotifyAPI.Web;
using StackExchange.Redis;
namespace Selector.Cache
@ -33,14 +33,20 @@ namespace Selector.Cache
public void Callback(object sender, ListeningChangeEventArgs e)
{
if (e.Current is null) return;
Task.Run(() => { return AsyncCallback(e); }, CancelToken);
}
public async Task AsyncCallback(ListeningChangeEventArgs e)
{
var payload = JsonSerializer.Serialize(e);
await Db.StringSetAsync(Key.CurrentlyPlaying(e.Username), payload);
var payload = JsonSerializer.Serialize((CurrentlyPlayingDTO) e);
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)

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 Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using SpotifyAPI.Web;
using StackExchange.Redis;
namespace Selector.Cache
@ -39,8 +39,13 @@ namespace Selector.Cache
public async Task AsyncCallback(ListeningChangeEventArgs e)
{
var payload = JsonSerializer.Serialize(e);
await Subscriber.PublishAsync(Key.CurrentlyPlaying(e.Username), payload);
var payload = JsonSerializer.Serialize((CurrentlyPlayingDTO) e);
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)

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 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 AudioFeature(string trackId) => Namespace(new[] { TrackName, trackId, AudioFeatureName });
public static string Namespace(string[] args) => string.Join(":", args);
}

View File

@ -11,9 +11,11 @@ namespace Selector
{
public class AudioFeatureInjector : IConsumer
{
private readonly IPlayerWatcher Watcher;
private readonly ITracksClient TrackClient;
private readonly ILogger<AudioFeatureInjector> Logger;
protected readonly IPlayerWatcher Watcher;
protected readonly ITracksClient TrackClient;
protected readonly ILogger<AudioFeatureInjector> Logger;
protected event EventHandler<AnalysedTrack> NewFeature;
public CancellationToken CancelToken { get; set; }
@ -47,7 +49,10 @@ namespace Selector
var audioFeatures = await TrackClient.GetAudioFeatures(track.Id);
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)
{
@ -102,6 +107,11 @@ namespace Selector
throw new ArgumentException("Provided watcher is not a PlayerWatcher");
}
}
protected virtual void OnNewFeature(AnalysedTrack args)
{
NewFeature?.Invoke(this, args);
}
}
public class AnalysedTrack {

View File

@ -4,16 +4,18 @@ using SpotifyAPI.Web;
namespace Selector
{
public class ListeningChangeEventArgs: EventArgs {
public CurrentlyPlayingContext Previous;
public CurrentlyPlayingContext Current;
public string Username;
public CurrentlyPlayingContext Previous { get; set; }
public CurrentlyPlayingContext Current { get; set; }
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()
{
Previous = previous,
Current = current,
Timeline = timeline,
Username = username
};
}

View File

@ -74,14 +74,14 @@ namespace Selector
&& (Live.Item is FullTrack || Live.Item is FullEpisode))
{
Logger.LogDebug($"Playback started: {Live.DisplayString()}");
OnPlayingChange(ListeningChangeEventArgs.From(previous, Live, Username));
OnPlayingChange(ListeningChangeEventArgs.From(previous, Live, Past, Username));
}
// STOPPED PLAYBACK
else if((previous.Item is FullTrack || previous.Item is FullEpisode)
&& Live is null)
{
Logger.LogDebug($"Playback stopped: {previous.DisplayString()}");
OnPlayingChange(ListeningChangeEventArgs.From(previous, Live, Username));
OnPlayingChange(ListeningChangeEventArgs.From(previous, Live, Past, Username));
}
// CONTINUING PLAYBACK
else {
@ -92,17 +92,17 @@ namespace Selector
{
if(!eq.IsEqual(previousTrack, currentTrack)) {
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)) {
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])) {
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
@ -110,8 +110,8 @@ namespace Selector
|| (previous.Item is FullEpisode && Live.Item is FullTrack))
{
Logger.LogDebug($"Media type changed: {previous.Item}, {previous.Item}");
OnContentChange(ListeningChangeEventArgs.From(previous, Live, Username));
OnItemChange(ListeningChangeEventArgs.From(previous, Live, Username));
OnContentChange(ListeningChangeEventArgs.From(previous, Live, Past, Username));
OnItemChange(ListeningChangeEventArgs.From(previous, Live, Past, Username));
}
// PODCASTS
else if(previous.Item is FullEpisode previousEp
@ -119,7 +119,7 @@ namespace Selector
{
if(!eq.IsEqual(previousEp, currentEp)) {
Logger.LogDebug($"Podcast changed: {previousEp.DisplayString()} -> {currentEp.DisplayString()}");
OnItemChange(ListeningChangeEventArgs.From(previous, Live, Username));
OnItemChange(ListeningChangeEventArgs.From(previous, Live, Past, Username));
}
}
else {
@ -129,25 +129,25 @@ namespace Selector
// CONTEXT
if(!eq.IsEqual(previous.Context, Live.Context)) {
Logger.LogDebug($"Context changed: {previous.Context.DisplayString()} -> {Live.Context.DisplayString()}");
OnContextChange(ListeningChangeEventArgs.From(previous, Live, Username));
OnContextChange(ListeningChangeEventArgs.From(previous, Live, Past, Username));
}
// DEVICE
if(!eq.IsEqual(previous?.Device, Live?.Device)) {
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
if(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
if(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));
}
}
}