adding consumer, audio features injector

This commit is contained in:
andy 2021-10-13 23:19:47 +01:00
parent b5956ef4a0
commit 9dfad73397
20 changed files with 438 additions and 111 deletions

View File

@ -27,6 +27,7 @@ namespace Selector.CLI
{ {
public const string Key = "Watcher"; public const string Key = "Watcher";
public bool Enabled { get; set; } = true;
public List<WatcherInstanceOptions> Instances { get; set; } = new(); public List<WatcherInstanceOptions> Instances { get; set; } = new();
} }
@ -39,6 +40,7 @@ namespace Selector.CLI
public string RefreshKey { get; set; } public string RefreshKey { get; set; }
public int PollPeriod { get; set; } = 5000; public int PollPeriod { get; set; } = 5000;
public WatcherType Type { get; set; } = WatcherType.Player; public WatcherType Type { get; set; } = WatcherType.Player;
public List<Consumers> Consumers { get; set; } = default;
#nullable enable #nullable enable
public string? PlaylistUri { get; set; } public string? PlaylistUri { get; set; }
public string? WatcherCollection { get; set; } public string? WatcherCollection { get; set; }
@ -49,4 +51,9 @@ namespace Selector.CLI
{ {
Player, Playlist Player, Playlist
} }
enum Consumers
{
AudioFeatures
}
} }

View File

@ -20,14 +20,20 @@ namespace Selector.CLI
=> Host.CreateDefaultBuilder(args) => Host.CreateDefaultBuilder(args)
.ConfigureServices((context, services) => { .ConfigureServices((context, services) => {
Console.WriteLine("~~~ Selector CLI ~~~");
Console.WriteLine("");
Console.WriteLine("Configuring...");
// CONFIG // CONFIG
services.Configure<RootOptions>(options => { services.Configure<RootOptions>(options => {
context.Configuration.GetSection(RootOptions.Key).Bind(options); context.Configuration.GetSection(RootOptions.Key).Bind(options);
context.Configuration.GetSection($"{RootOptions.Key}:{WatcherOptions.Key}").Bind(options.WatcherOptions); context.Configuration.GetSection($"{RootOptions.Key}:{WatcherOptions.Key}").Bind(options.WatcherOptions);
}); });
Console.WriteLine("Adding Services...");
// SERVICES // SERVICES
services.AddSingleton<IWatcherFactory, WatcherFactory>(); services.AddSingleton<IWatcherFactory, WatcherFactory>();
services.AddSingleton<IConsumerFactory, AudioFeatureInjectorFactory>();
services.AddSingleton<IWatcherCollectionFactory, WatcherCollectionFactory>(); services.AddSingleton<IWatcherCollectionFactory, WatcherCollectionFactory>();
// For generating spotify clients // For generating spotify clients
services.AddSingleton<IRefreshTokenFactoryProvider, RefreshTokenFactoryProvider>(); services.AddSingleton<IRefreshTokenFactoryProvider, RefreshTokenFactoryProvider>();
@ -35,15 +41,25 @@ namespace Selector.CLI
switch(context.Configuration.GetValue<EqualityChecker>("selector:equality")) switch(context.Configuration.GetValue<EqualityChecker>("selector:equality"))
{ {
case EqualityChecker.Uri: case EqualityChecker.Uri:
Console.WriteLine("Using Uri Equality");
services.AddTransient<IEqual, UriEqual>(); services.AddTransient<IEqual, UriEqual>();
break; break;
case EqualityChecker.String: case EqualityChecker.String:
Console.WriteLine("Using String Equality");
services.AddTransient<IEqual, StringEqual>(); services.AddTransient<IEqual, StringEqual>();
break; break;
} }
// HOSTED SERVICES // HOSTED SERVICES
services.AddHostedService<WatcherService>(); if(context.Configuration
.GetSection($"{RootOptions.Key}:{WatcherOptions.Key}")
.Get<WatcherOptions>()
.Enabled)
{
Console.WriteLine("Adding Watcher Service");
services.AddHostedService<WatcherService>();
}
}) })
.ConfigureLogging((context, builder) => { .ConfigureLogging((context, builder) => {
builder.ClearProviders(); builder.ClearProviders();

View File

@ -4,7 +4,8 @@
"commandName": "Project", "commandName": "Project",
"environmentVariables": { "environmentVariables": {
"DOTNET_ENVIRONMENT": "Development" "DOTNET_ENVIRONMENT": "Development"
} },
"nativeDebugging": true
} }
} }
} }

View File

@ -17,6 +17,7 @@ namespace Selector.CLI
private const string ConfigInstanceKey = "localconfig"; private const string ConfigInstanceKey = "localconfig";
private readonly ILogger<WatcherService> Logger; private readonly ILogger<WatcherService> Logger;
private readonly ILoggerFactory LoggerFactory;
private readonly RootOptions Config; private readonly RootOptions Config;
private readonly IWatcherFactory WatcherFactory; private readonly IWatcherFactory WatcherFactory;
private readonly IWatcherCollectionFactory WatcherCollectionFactory; private readonly IWatcherCollectionFactory WatcherCollectionFactory;
@ -28,10 +29,11 @@ namespace Selector.CLI
IWatcherFactory watcherFactory, IWatcherFactory watcherFactory,
IWatcherCollectionFactory watcherCollectionFactory, IWatcherCollectionFactory watcherCollectionFactory,
IRefreshTokenFactoryProvider spotifyFactory, IRefreshTokenFactoryProvider spotifyFactory,
ILogger<WatcherService> logger, ILoggerFactory loggerFactory,
IOptions<RootOptions> config IOptions<RootOptions> config
) { ) {
Logger = logger; Logger = loggerFactory.CreateLogger<WatcherService>();
LoggerFactory = loggerFactory;
Config = config.Value; Config = config.Value;
WatcherFactory = watcherFactory; WatcherFactory = watcherFactory;
WatcherCollectionFactory = watcherCollectionFactory; WatcherCollectionFactory = watcherCollectionFactory;
@ -42,7 +44,7 @@ namespace Selector.CLI
public async Task StartAsync(CancellationToken cancellationToken) public async Task StartAsync(CancellationToken cancellationToken)
{ {
Logger.LogInformation("Starting up"); Logger.LogInformation("Starting watcher service...");
Logger.LogInformation("Loading config instances..."); Logger.LogInformation("Loading config instances...");
var watcherIndices = await InitialiseConfigInstances(); var watcherIndices = await InitialiseConfigInstances();
@ -60,11 +62,11 @@ namespace Selector.CLI
var logMsg = new StringBuilder(); var logMsg = new StringBuilder();
if (!string.IsNullOrWhiteSpace(watcherOption.Name)) if (!string.IsNullOrWhiteSpace(watcherOption.Name))
{ {
logMsg.Append($"Creating {watcherOption.Name} watcher [{watcherOption.Type}]"); logMsg.Append($"Creating [{watcherOption.Name}] watcher [{watcherOption.Type}]");
} }
else else
{ {
logMsg.Append($"Creating new {watcherOption.Type} watcher"); logMsg.Append($"Creating new [{watcherOption.Type}] watcher");
} }
if (!string.IsNullOrWhiteSpace(watcherOption.PlaylistUri)) logMsg.Append($" [{ watcherOption.PlaylistUri}]"); if (!string.IsNullOrWhiteSpace(watcherOption.PlaylistUri)) logMsg.Append($" [{ watcherOption.PlaylistUri}]");
@ -92,7 +94,19 @@ namespace Selector.CLI
break; break;
} }
watcherCollection.Add(watcher); List<IConsumer> consumers = new();
foreach(var consumer in watcherOption.Consumers)
{
switch(consumer)
{
case Consumers.AudioFeatures:
var factory = new AudioFeatureInjectorFactory(LoggerFactory);
consumers.Add(await factory.Get(spotifyFactory));
break;
}
}
watcherCollection.Add(watcher, consumers);
} }
return indices; return indices;
@ -104,6 +118,7 @@ namespace Selector.CLI
{ {
try try
{ {
Logger.LogInformation($"Starting watcher collection [{index}]");
Watchers[index].Start(); Watchers[index].Start();
} }
catch (KeyNotFoundException) catch (KeyNotFoundException)
@ -119,7 +134,7 @@ namespace Selector.CLI
foreach((var key, var watcher) in Watchers) foreach((var key, var watcher) in Watchers)
{ {
Logger.LogInformation($"Stopping watcher collection: {key}"); Logger.LogInformation($"Stopping watcher collection [{key}]");
watcher.Stop(); watcher.Stop();
} }

View File

@ -6,16 +6,17 @@
"Watcher": { "Watcher": {
"Instances": [ "Instances": [
{ {
"name": "player watcher", "name": "Player Watcher",
"type": "player", "type": "player",
"pollperiod": 1000 "pollperiod": 2000,
"consumers": [ "audiofeatures" ]
} }
] ]
} }
}, },
"Logging": { "Logging": {
"LogLevel": { "LogLevel": {
"Default": "Information" "Default": "Trace"
} }
} }
} }

View File

@ -16,6 +16,10 @@
name="logfile" name="logfile"
fileName=".\selector.log" fileName=".\selector.log"
layout="${format}" /> layout="${format}" />
<target xsi:type="File"
name="tracefile"
fileName=".\selector.trace.log"
layout="${format}" />
<target xsi:type="Console" <target xsi:type="Console"
name="logconsole" name="logconsole"
layout="${format}" /> layout="${format}" />
@ -23,6 +27,9 @@
<!-- rules to map from logger name to target --> <!-- rules to map from logger name to target -->
<rules> <rules>
<logger name="Selector.*" minlevel="Trace" writeTo="logfile,logconsole" /> <logger name="*" minlevel="Debug" writeTo="logfile" />
<logger name="*" minlevel="Trace" writeTo="tracefile" />
<logger name="Selector.*" minlevel="Debug" writeTo="logconsole" />
</rules> </rules>
</nlog> </nlog>

View File

@ -0,0 +1,103 @@
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using SpotifyAPI.Web;
namespace Selector
{
public class AudioFeatureInjector : IConsumer
{
private readonly IPlayerWatcher Watcher;
private readonly ITracksClient TrackClient;
private readonly ILogger<AudioFeatureInjector> Logger;
public CancellationToken CancelToken { get; set; }
public AnalysedTrackTimeline Timeline { get; set; } = new();
public AudioFeatureInjector(
IPlayerWatcher watcher,
ITracksClient trackClient,
ILogger<AudioFeatureInjector> logger = null,
CancellationToken token = default
){
Watcher = watcher;
TrackClient = trackClient;
Logger = logger ?? NullLogger<AudioFeatureInjector>.Instance;
CancelToken = token;
}
public void Callback(object sender, ListeningChangeEventArgs e)
{
if (e.Current is null) return;
Task.Run(() => { return AsyncCallback(e); }, CancelToken);
}
private async Task AsyncCallback(ListeningChangeEventArgs e)
{
if (e.Current.Item is FullTrack track)
{
Logger.LogTrace("Making Spotify call");
var audioFeatures = await TrackClient.GetAudioFeatures(track.Id);
Logger.LogDebug($"Adding audio features [{track.DisplayString()}]: [{audioFeatures.DisplayString()}]");
Timeline.Add(AnalysedTrack.From(track, audioFeatures));
}
else if (e.Current.Item is FullEpisode episode)
{
Logger.LogDebug($"Ignoring podcast episdoe [{episode.DisplayString()}]");
}
else
{
Logger.LogError($"Unknown item pulled from API [{e.Current.Item}]");
}
}
public void Subscribe(IWatcher watch = null)
{
var watcher = watch ?? Watcher ?? throw new ArgumentNullException("No watcher provided");
if (watcher is IPlayerWatcher watcherCast)
{
watcherCast.ItemChange += Callback;
}
else
{
throw new ArgumentException("Provided watcher is not a PlayerWatcher");
}
}
public void Unsubscribe(IWatcher watch = null)
{
var watcher = watch ?? Watcher ?? throw new ArgumentNullException("No watcher provided");
if (watcher is IPlayerWatcher watcherCast)
{
watcherCast.ItemChange -= Callback;
}
else
{
throw new ArgumentException("Provided watcher is not a PlayerWatcher");
}
}
}
public class AnalysedTrack {
public FullTrack Track { get; set; }
public TrackAudioFeatures Features { get; set; }
public static AnalysedTrack From(FullTrack track, TrackAudioFeatures features)
{
return new AnalysedTrack()
{
Track = track,
Features = features
};
}
}
}

View File

@ -0,0 +1,32 @@
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using SpotifyAPI.Web;
namespace Selector
{
public class AudioFeatureInjectorFactory: IConsumerFactory {
private readonly ILoggerFactory LoggerFactory;
public AudioFeatureInjectorFactory(ILoggerFactory loggerFactory)
{
LoggerFactory = loggerFactory;
}
public async Task<IConsumer> Get(ISpotifyConfigFactory spotifyFactory, IPlayerWatcher watcher = null)
{
var config = await spotifyFactory.GetConfig();
var client = new SpotifyClient(config);
return new AudioFeatureInjector(
watcher,
client.Tracks,
LoggerFactory.CreateLogger<AudioFeatureInjector>()
);
}
}
}

View File

@ -0,0 +1,12 @@
using System;
using System.Threading.Tasks;
namespace Selector
{
public interface IConsumer
{
public void Callback(object sender, ListeningChangeEventArgs e);
public void Subscribe(IWatcher watch = null);
public void Unsubscribe(IWatcher watch = null);
}
}

View File

@ -0,0 +1,10 @@
using System;
using System.Threading.Tasks;
namespace Selector
{
public interface IConsumerFactory
{
public Task<IConsumer> Get(ISpotifyConfigFactory spotifyFactory, IPlayerWatcher watcher);
}
}

View File

@ -5,20 +5,23 @@ using System.Linq;
using SpotifyAPI.Web; using SpotifyAPI.Web;
namespace Selector.Helpers namespace Selector
{ {
public static class SpotifyExtensions public static class SpotifyExtensions
{ {
public static string ToString(this FullTrack track) => $"{track.Name} - {track.Album.Name} - {track.Artists}"; public static string DisplayString(this FullTrack track) => $"{track.Name} / {track.Album.Name} / {track.Artists.DisplayString()}";
public static string ToString(this SimpleAlbum album) => $"{album.Name} - {album.Artists}"; public static string DisplayString(this SimpleAlbum album) => $"{album.Name} / {album.Artists.DisplayString()}";
public static string ToString(this SimpleArtist artist) => $"{artist.Name}"; public static string DisplayString(this SimpleArtist artist) => artist.Name;
public static string ToString(this FullEpisode ep) => $"{ep.Name} - {ep.Show}"; public static string DisplayString(this FullEpisode ep) => $"{ep.Name} / {ep.Show.DisplayString()}";
public static string ToString(this SimpleShow show) => $"{show.Name} - {show.Publisher}"; public static string DisplayString(this SimpleShow show) => $"{show.Name} / {show.Publisher}";
public static string ToString(this CurrentlyPlayingContext context) => $"{context.IsPlaying}, {context.Item}, {context.Device}";
public static string ToString(this Device device) => $"{device.Id}: {device.Name} {device.VolumePercent}%";
public static string ToString(this IEnumerable<SimpleArtist> artists) => string.Join("/", artists.Select(a => a.Name)); public static string DisplayString(this CurrentlyPlayingContext currentPlaying) => $"{currentPlaying.IsPlaying}, {currentPlaying.Item}, {currentPlaying.Device.DisplayString()}";
public static string DisplayString(this Context context) => $"{context.Type}, {context.Uri}";
public static string DisplayString(this Device device) => $"{device.Name} ({device.Id}) {device.VolumePercent}%";
public static string DisplayString(this TrackAudioFeatures feature) => $"Acou. {feature.Acousticness}, Dance {feature.Danceability}, Energy {feature.Energy}, Instru. {feature.Instrumentalness}, Key {feature.Key}, Live {feature.Liveness}, Loud {feature.Loudness}, Mode {feature.Mode}, Speech {feature.Speechiness}, Tempo {feature.Tempo}, Valence {feature.Valence}";
public static string DisplayString(this IEnumerable<SimpleArtist> artists) => string.Join(", ", artists.Select(a => a.DisplayString()));
} }
} }

View File

@ -0,0 +1,50 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using SpotifyAPI.Web;
namespace Selector
{
public class AnalysedTrackTimeline
: BaseTimeline<AnalysedTrack>, ITrackTimeline<AnalysedTrack>
{
public IEqual EqualityChecker { get; set; }
public AnalysedTrack Get(FullTrack track)
=> GetAll(track)
.LastOrDefault();
public IEnumerable<AnalysedTrack> GetAll(FullTrack track)
=> GetAllTimelineItems(track)
.Select(t => t.Item);
public IEnumerable<TimelineItem<AnalysedTrack>> GetAllTimelineItems(FullTrack track)
=> Recent
.Where(i => EqualityChecker.IsEqual(i.Item.Track, track));
public AnalysedTrack Get(SimpleAlbum album)
=> GetAll(album)
.LastOrDefault();
public IEnumerable<AnalysedTrack> GetAll(SimpleAlbum album)
=> GetAllTimelineItems(album)
.Select(t => t.Item);
public IEnumerable<TimelineItem<AnalysedTrack>> GetAllTimelineItems(SimpleAlbum album)
=> Recent
.Where(i => EqualityChecker.IsEqual(i.Item.Track.Album, album));
public AnalysedTrack Get(SimpleArtist artist)
=> GetAll(artist)
.LastOrDefault();
public IEnumerable<AnalysedTrack> GetAll(SimpleArtist artist)
=> GetAllTimelineItems(artist)
.Select(t => t.Item);
public IEnumerable<TimelineItem<AnalysedTrack>> GetAllTimelineItems(SimpleArtist artist)
=> Recent
.Where(i => EqualityChecker.IsEqual(i.Item.Track.Artists[0], artist));
}
}

View File

@ -0,0 +1,61 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace Selector
{
public abstract class BaseTimeline<T> : ITimeline<T>, IEnumerable<TimelineItem<T>> where T : class
{
protected List<TimelineItem<T>> Recent = new();
public int Count { get => Recent.Count; }
public void Clear() => Recent.Clear();
public bool SortOnBackDate { get; set; } = true;
private int? max = 1000;
public int? MaxSize
{
get => max;
set => max = value is null ? value : Math.Max(1, (int) value);
}
public virtual void Add(T item) => Add(item, DateTime.UtcNow);
public virtual void Add(T item, DateTime timestamp)
{
Recent.Add(TimelineItem<T>.From(item, timestamp));
if (timestamp < Recent.Last().Time && SortOnBackDate)
{
Sort();
}
CheckSize();
}
public void Sort()
{
Recent = Recent.OrderBy(i => i.Time).ToList();
}
protected void CheckSize()
{
if (MaxSize is int maxSize && Count > maxSize)
{
Recent.RemoveRange(0, Count - maxSize);
}
}
public T Get()
=> Recent.Last().Item;
public T Get(DateTime at)
=> GetTimelineItem(at).Item;
public TimelineItem<T> GetTimelineItem(DateTime at)
=> Recent
.Where(i => i.Time <= at).LastOrDefault();
public IEnumerator<TimelineItem<T>> GetEnumerator() => Recent.GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}
}

View File

@ -0,0 +1,21 @@
using System;
using System.Collections.Generic;
using SpotifyAPI.Web;
namespace Selector
{
public interface ITrackTimeline<T>: ITimeline<T>
{
public T Get(FullTrack track);
public IEnumerable<T> GetAll(FullTrack track);
public IEnumerable<TimelineItem<T>> GetAllTimelineItems(FullTrack track);
public T Get(SimpleAlbum album);
public IEnumerable<T> GetAll(SimpleAlbum album);
public IEnumerable<TimelineItem<T>> GetAllTimelineItems(SimpleAlbum album);
public T Get(SimpleArtist artist);
public IEnumerable<T> GetAll(SimpleArtist artist);
public IEnumerable<TimelineItem<T>> GetAllTimelineItems(SimpleArtist artist);
}
}

View File

@ -7,67 +7,11 @@ using SpotifyAPI.Web;
namespace Selector namespace Selector
{ {
public class PlayerTimeline public class PlayerTimeline
: ITimeline<CurrentlyPlayingContext>, : BaseTimeline<CurrentlyPlayingContext>, ITrackTimeline<CurrentlyPlayingContext>
IEnumerable<TimelineItem<CurrentlyPlayingContext>>
{ {
private List<TimelineItem<CurrentlyPlayingContext>> recentlyPlayed = new();
public IEqual EqualityChecker { get; set; } public IEqual EqualityChecker { get; set; }
public bool SortOnBackDate { get; set; } = true;
public int Count { get => recentlyPlayed.Count; }
private int? max = 1000; public override void Add(CurrentlyPlayingContext item) => Add(item, DateHelper.FromUnixMilli(item.Timestamp));
public int? MaxSize {
get => max;
set {
if(value is null)
{
max = value;
}
else
{
max = Math.Max(1, (int) value);
}
}
}
public void Add(CurrentlyPlayingContext item) => Add(item, DateHelper.FromUnixMilli(item.Timestamp));
public void Add(CurrentlyPlayingContext item, DateTime timestamp)
{
recentlyPlayed.Add(TimelineItem<CurrentlyPlayingContext>.From(item, timestamp));
if (timestamp < recentlyPlayed.Last().Time && SortOnBackDate)
{
Sort();
}
CheckSize();
}
public void Sort()
{
recentlyPlayed = recentlyPlayed
.OrderBy(i => i.Time)
.ToList();
}
private void CheckSize()
{
if (MaxSize is int maxSize && Count > maxSize) {
recentlyPlayed.RemoveRange(0, Count - maxSize);
}
}
public void Clear() => recentlyPlayed.Clear();
public CurrentlyPlayingContext Get()
=> recentlyPlayed.Last().Item;
public CurrentlyPlayingContext Get(DateTime at)
=> GetTimelineItem(at)?.Item;
public TimelineItem<CurrentlyPlayingContext> GetTimelineItem(DateTime at)
=> recentlyPlayed
.Where(i => i.Time <= at).LastOrDefault();
public CurrentlyPlayingContext Get(FullTrack track) public CurrentlyPlayingContext Get(FullTrack track)
=> GetAll(track) => GetAll(track)
@ -77,8 +21,8 @@ namespace Selector
=> GetAllTimelineItems(track) => GetAllTimelineItems(track)
.Select(t => t.Item); .Select(t => t.Item);
private IEnumerable<TimelineItem<CurrentlyPlayingContext>> GetAllTimelineItems(FullTrack track) public IEnumerable<TimelineItem<CurrentlyPlayingContext>> GetAllTimelineItems(FullTrack track)
=> recentlyPlayed => Recent
.Where(i => i.Item.Item is FullTrack iterTrack .Where(i => i.Item.Item is FullTrack iterTrack
&& EqualityChecker.IsEqual(iterTrack, track)); && EqualityChecker.IsEqual(iterTrack, track));
@ -90,8 +34,8 @@ namespace Selector
=> GetAllTimelineItems(ep) => GetAllTimelineItems(ep)
.Select(t => t.Item); .Select(t => t.Item);
private IEnumerable<TimelineItem<CurrentlyPlayingContext>> GetAllTimelineItems(FullEpisode ep) public IEnumerable<TimelineItem<CurrentlyPlayingContext>> GetAllTimelineItems(FullEpisode ep)
=> recentlyPlayed => Recent
.Where(i => i.Item.Item is FullEpisode iterEp .Where(i => i.Item.Item is FullEpisode iterEp
&& EqualityChecker.IsEqual(iterEp, ep)); && EqualityChecker.IsEqual(iterEp, ep));
@ -103,8 +47,8 @@ namespace Selector
=> GetAllTimelineItems(album) => GetAllTimelineItems(album)
.Select(t => t.Item); .Select(t => t.Item);
private IEnumerable<TimelineItem<CurrentlyPlayingContext>> GetAllTimelineItems(SimpleAlbum album) public IEnumerable<TimelineItem<CurrentlyPlayingContext>> GetAllTimelineItems(SimpleAlbum album)
=> recentlyPlayed => Recent
.Where(i => i.Item.Item is FullTrack iterTrack .Where(i => i.Item.Item is FullTrack iterTrack
&& EqualityChecker.IsEqual(iterTrack.Album, album)); && EqualityChecker.IsEqual(iterTrack.Album, album));
@ -116,8 +60,8 @@ namespace Selector
=> GetAllTimelineItems(artist) => GetAllTimelineItems(artist)
.Select(t => t.Item); .Select(t => t.Item);
private IEnumerable<TimelineItem<CurrentlyPlayingContext>> GetAllTimelineItems(SimpleArtist artist) public IEnumerable<TimelineItem<CurrentlyPlayingContext>> GetAllTimelineItems(SimpleArtist artist)
=> recentlyPlayed => Recent
.Where(i => i.Item.Item is FullTrack iterTrack .Where(i => i.Item.Item is FullTrack iterTrack
&& EqualityChecker.IsEqual(iterTrack.Artists[0], artist)); && EqualityChecker.IsEqual(iterTrack.Artists[0], artist));
@ -129,8 +73,8 @@ namespace Selector
=> GetAllTimelineItems(device) => GetAllTimelineItems(device)
.Select(t => t.Item); .Select(t => t.Item);
private IEnumerable<TimelineItem<CurrentlyPlayingContext>> GetAllTimelineItems(Device device) public IEnumerable<TimelineItem<CurrentlyPlayingContext>> GetAllTimelineItems(Device device)
=> recentlyPlayed => Recent
.Where(i => EqualityChecker.IsEqual(i.Item.Device, device)); .Where(i => EqualityChecker.IsEqual(i.Item.Device, device));
public CurrentlyPlayingContext Get(Context context) public CurrentlyPlayingContext Get(Context context)
@ -141,11 +85,8 @@ namespace Selector
=> GetAllTimelineItems(context) => GetAllTimelineItems(context)
.Select(t => t.Item); .Select(t => t.Item);
private IEnumerable<TimelineItem<CurrentlyPlayingContext>> GetAllTimelineItems(Context context) public IEnumerable<TimelineItem<CurrentlyPlayingContext>> GetAllTimelineItems(Context context)
=> recentlyPlayed => Recent
.Where(i => EqualityChecker.IsEqual(i.Item.Context, context)); .Where(i => EqualityChecker.IsEqual(i.Item.Context, context));
public IEnumerator<TimelineItem<CurrentlyPlayingContext>> GetEnumerator() => recentlyPlayed.GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
} }
} }

View File

@ -5,17 +5,29 @@ using System.Text;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
namespace Selector namespace Selector
{ {
public abstract class BaseWatcher: IWatcher public abstract class BaseWatcher: IWatcher
{ {
protected readonly ILogger<BaseWatcher> Logger;
public BaseWatcher(ILogger<BaseWatcher> logger = null)
{
Logger = logger ?? NullLogger<BaseWatcher>.Instance;
}
public abstract Task WatchOne(CancellationToken token); public abstract Task WatchOne(CancellationToken token);
public async Task Watch(CancellationToken cancelToken) public async Task Watch(CancellationToken cancelToken)
{ {
Logger.LogDebug("Starting watcher");
while (true) { while (true) {
cancelToken.ThrowIfCancellationRequested(); cancelToken.ThrowIfCancellationRequested();
await WatchOne(cancelToken); await WatchOne(cancelToken);
Logger.LogTrace($"Finished watch one, delaying {PollPeriod}ms...");
await Task.Delay(PollPeriod, cancelToken); await Task.Delay(PollPeriod, cancelToken);
} }
} }

View File

@ -11,7 +11,7 @@ namespace Selector
{ {
public class PlayerWatcher: BaseWatcher, IPlayerWatcher public class PlayerWatcher: BaseWatcher, IPlayerWatcher
{ {
private readonly ILogger<PlayerWatcher> Logger; new private readonly ILogger<PlayerWatcher> Logger;
private readonly IPlayerClient spotifyClient; private readonly IPlayerClient spotifyClient;
private readonly IEqual eq; private readonly IEqual eq;
@ -26,12 +26,13 @@ namespace Selector
public event EventHandler<ListeningChangeEventArgs> PlayingChange; public event EventHandler<ListeningChangeEventArgs> PlayingChange;
public CurrentlyPlayingContext Live { get; private set; } public CurrentlyPlayingContext Live { get; private set; }
public PlayerTimeline Past { get; private set; } public PlayerTimeline Past { get; set; }
public PlayerWatcher(IPlayerClient spotifyClient, public PlayerWatcher(IPlayerClient spotifyClient,
IEqual equalityChecker, IEqual equalityChecker,
ILogger<PlayerWatcher> logger = null, ILogger<PlayerWatcher> logger = null,
int pollPeriod = 3000) { int pollPeriod = 3000
) : base(logger) {
this.spotifyClient = spotifyClient; this.spotifyClient = spotifyClient;
eq = equalityChecker; eq = equalityChecker;
@ -44,7 +45,9 @@ namespace Selector
token.ThrowIfCancellationRequested(); token.ThrowIfCancellationRequested();
try{ try{
Logger.LogTrace("Making Spotify call");
var polledCurrent = await spotifyClient.GetCurrentPlayback(); var polledCurrent = await spotifyClient.GetCurrentPlayback();
Logger.LogTrace($"Received Spotify call [{polledCurrent?.DisplayString()}]");
if (polledCurrent != null) StoreCurrentPlaying(polledCurrent); if (polledCurrent != null) StoreCurrentPlaying(polledCurrent);
@ -70,14 +73,14 @@ namespace Selector
if(previous is null if(previous is null
&& (Live.Item is FullTrack || Live.Item is FullEpisode)) && (Live.Item is FullTrack || Live.Item is FullEpisode))
{ {
Logger.LogDebug($"Playback started: {Live}"); Logger.LogDebug($"Playback started: {Live.DisplayString()}");
OnPlayingChange(ListeningChangeEventArgs.From(previous, Live)); OnPlayingChange(ListeningChangeEventArgs.From(previous, Live));
} }
// 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}"); Logger.LogDebug($"Playback stopped: {previous.DisplayString()}");
OnPlayingChange(ListeningChangeEventArgs.From(previous, Live)); OnPlayingChange(ListeningChangeEventArgs.From(previous, Live));
} }
// CONTINUING PLAYBACK // CONTINUING PLAYBACK
@ -88,17 +91,17 @@ namespace Selector
&& Live.Item is FullTrack currentTrack) && Live.Item is FullTrack currentTrack)
{ {
if(!eq.IsEqual(previousTrack, currentTrack)) { if(!eq.IsEqual(previousTrack, currentTrack)) {
Logger.LogDebug($"Track changed: {previousTrack} -> {currentTrack}"); Logger.LogDebug($"Track changed: {previousTrack.DisplayString()} -> {currentTrack.DisplayString()}");
OnItemChange(ListeningChangeEventArgs.From(previous, Live)); OnItemChange(ListeningChangeEventArgs.From(previous, Live));
} }
if(!eq.IsEqual(previousTrack.Album, currentTrack.Album)) { if(!eq.IsEqual(previousTrack.Album, currentTrack.Album)) {
Logger.LogDebug($"Album changed: {previousTrack.Album} -> {currentTrack.Album}"); Logger.LogDebug($"Album changed: {previousTrack.Album.DisplayString()} -> {currentTrack.Album.DisplayString()}");
OnAlbumChange(ListeningChangeEventArgs.From(previous, Live)); OnAlbumChange(ListeningChangeEventArgs.From(previous, Live));
} }
if(!eq.IsEqual(previousTrack.Artists[0], currentTrack.Artists[0])) { if(!eq.IsEqual(previousTrack.Artists[0], currentTrack.Artists[0])) {
Logger.LogDebug($"Artist changed: {previousTrack.Artists[0]} -> {currentTrack.Artists[0]}"); Logger.LogDebug($"Artist changed: {previousTrack.Artists.DisplayString()} -> {currentTrack.Artists.DisplayString()}");
OnArtistChange(ListeningChangeEventArgs.From(previous, Live)); OnArtistChange(ListeningChangeEventArgs.From(previous, Live));
} }
} }
@ -115,7 +118,7 @@ namespace Selector
&& Live.Item is FullEpisode currentEp) && Live.Item is FullEpisode currentEp)
{ {
if(!eq.IsEqual(previousEp, currentEp)) { if(!eq.IsEqual(previousEp, currentEp)) {
Logger.LogDebug($"Podcast changed: {previousEp} -> {currentEp}"); Logger.LogDebug($"Podcast changed: {previousEp.DisplayString()} -> {currentEp.DisplayString()}");
OnItemChange(ListeningChangeEventArgs.From(previous, Live)); OnItemChange(ListeningChangeEventArgs.From(previous, Live));
} }
} }
@ -125,13 +128,13 @@ namespace Selector
// CONTEXT // CONTEXT
if(!eq.IsEqual(previous.Context, Live.Context)) { if(!eq.IsEqual(previous.Context, Live.Context)) {
Logger.LogDebug($"Context changed: {previous.Context} -> {Live.Context}"); Logger.LogDebug($"Context changed: {previous.Context.DisplayString()} -> {Live.Context.DisplayString()}");
OnContextChange(ListeningChangeEventArgs.From(previous, Live)); OnContextChange(ListeningChangeEventArgs.From(previous, Live));
} }
// DEVICE // DEVICE
if(!eq.IsEqual(previous?.Device, Live?.Device)) { if(!eq.IsEqual(previous?.Device, Live?.Device)) {
Logger.LogDebug($"Device changed: {previous?.Device} -> {Live?.Device}"); Logger.LogDebug($"Device changed: {previous?.Device.DisplayString()} -> {Live?.Device.DisplayString()}");
OnDeviceChange(ListeningChangeEventArgs.From(previous, Live)); OnDeviceChange(ListeningChangeEventArgs.From(previous, Live));
} }

View File

@ -8,6 +8,7 @@ namespace Selector
{ {
public bool IsRunning { get; } public bool IsRunning { get; }
public void Add(IWatcher watcher); public void Add(IWatcher watcher);
public void Add(IWatcher watcher, List<IConsumer> consumers);
public void Start(); public void Start();
public void Stop(); public void Stop();

View File

@ -34,7 +34,12 @@ namespace Selector
public void Add(IWatcher watcher) public void Add(IWatcher watcher)
{ {
var context = WatcherContext.From(watcher); Add(watcher, default);
}
public void Add(IWatcher watcher, List<IConsumer> consumers)
{
var context = WatcherContext.From(watcher, consumers);
if (IsRunning) context.Start(); if (IsRunning) context.Start();
Watchers.Add(context); Watchers.Add(context);
@ -45,7 +50,7 @@ namespace Selector
public void Start() public void Start()
{ {
Logger.LogDebug($"Starting {Count} watchers"); Logger.LogDebug($"Starting {Count} watcher(s)");
foreach(var watcher in Watchers) foreach(var watcher in Watchers)
{ {
watcher.Start(); watcher.Start();
@ -55,7 +60,7 @@ namespace Selector
public void Stop() public void Stop()
{ {
Logger.LogDebug($"Stopping {Count} watchers"); Logger.LogDebug($"Stopping {Count} watcher(s)");
foreach (var watcher in Watchers) foreach (var watcher in Watchers)
{ {
watcher.Stop(); watcher.Stop();

View File

@ -1,5 +1,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using System.Text; using System.Text;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
@ -9,6 +10,7 @@ namespace Selector
public class WatcherContext: IDisposable public class WatcherContext: IDisposable
{ {
public IWatcher Watcher { get; set; } public IWatcher Watcher { get; set; }
private List<IConsumer> Consumers { get; set; } = new();
public bool IsRunning { get; private set; } public bool IsRunning { get; private set; }
public Task Task { get; set; } public Task Task { get; set; }
public CancellationTokenSource TokenSource { get; set; } public CancellationTokenSource TokenSource { get; set; }
@ -18,11 +20,30 @@ namespace Selector
Watcher = watcher; Watcher = watcher;
} }
public WatcherContext(IWatcher watcher, List<IConsumer> consumers)
{
Watcher = watcher;
Consumers = consumers ?? new();
}
public static WatcherContext From(IWatcher watcher) public static WatcherContext From(IWatcher watcher)
{ {
return new(watcher); return new(watcher);
} }
public static WatcherContext From(IWatcher watcher, List<IConsumer> consumers)
{
return new(watcher, consumers);
}
public void AddConsumer(IConsumer consumer)
{
if (IsRunning)
consumer.Subscribe(Watcher);
Consumers.Add(consumer);
}
public void Start() public void Start()
{ {
if (IsRunning) if (IsRunning)
@ -30,6 +51,9 @@ namespace Selector
IsRunning = true; IsRunning = true;
TokenSource = new(); TokenSource = new();
Consumers.ForEach(c => c.Subscribe(Watcher));
Task = Watcher.Watch(TokenSource.Token); Task = Watcher.Watch(TokenSource.Token);
Task.ContinueWith(t => Task.ContinueWith(t =>
{ {
@ -39,6 +63,8 @@ namespace Selector
public void Stop() public void Stop()
{ {
Consumers.ForEach(c => c.Unsubscribe(Watcher));
TokenSource.Cancel(); TokenSource.Cancel();
IsRunning = false; IsRunning = false;
} }