diff --git a/Selector.CLI/Options.cs b/Selector.CLI/Options.cs index 3b46f9c..6149600 100644 --- a/Selector.CLI/Options.cs +++ b/Selector.CLI/Options.cs @@ -27,6 +27,7 @@ namespace Selector.CLI { public const string Key = "Watcher"; + public bool Enabled { get; set; } = true; public List Instances { get; set; } = new(); } @@ -39,6 +40,7 @@ namespace Selector.CLI public string RefreshKey { get; set; } public int PollPeriod { get; set; } = 5000; public WatcherType Type { get; set; } = WatcherType.Player; + public List Consumers { get; set; } = default; #nullable enable public string? PlaylistUri { get; set; } public string? WatcherCollection { get; set; } @@ -49,4 +51,9 @@ namespace Selector.CLI { Player, Playlist } + + enum Consumers + { + AudioFeatures + } } diff --git a/Selector.CLI/Program.cs b/Selector.CLI/Program.cs index 9096967..fa74e73 100644 --- a/Selector.CLI/Program.cs +++ b/Selector.CLI/Program.cs @@ -20,14 +20,20 @@ namespace Selector.CLI => Host.CreateDefaultBuilder(args) .ConfigureServices((context, services) => { + Console.WriteLine("~~~ Selector CLI ~~~"); + Console.WriteLine(""); + + Console.WriteLine("Configuring..."); // CONFIG services.Configure(options => { context.Configuration.GetSection(RootOptions.Key).Bind(options); context.Configuration.GetSection($"{RootOptions.Key}:{WatcherOptions.Key}").Bind(options.WatcherOptions); }); + Console.WriteLine("Adding Services..."); // SERVICES services.AddSingleton(); + services.AddSingleton(); services.AddSingleton(); // For generating spotify clients services.AddSingleton(); @@ -35,15 +41,25 @@ namespace Selector.CLI switch(context.Configuration.GetValue("selector:equality")) { case EqualityChecker.Uri: + Console.WriteLine("Using Uri Equality"); services.AddTransient(); break; case EqualityChecker.String: + Console.WriteLine("Using String Equality"); services.AddTransient(); break; } // HOSTED SERVICES - services.AddHostedService(); + if(context.Configuration + .GetSection($"{RootOptions.Key}:{WatcherOptions.Key}") + .Get() + .Enabled) + { + Console.WriteLine("Adding Watcher Service"); + services.AddHostedService(); + } + }) .ConfigureLogging((context, builder) => { builder.ClearProviders(); diff --git a/Selector.CLI/Properties/launchSettings.json b/Selector.CLI/Properties/launchSettings.json index a4dd153..171b13a 100644 --- a/Selector.CLI/Properties/launchSettings.json +++ b/Selector.CLI/Properties/launchSettings.json @@ -4,7 +4,8 @@ "commandName": "Project", "environmentVariables": { "DOTNET_ENVIRONMENT": "Development" - } + }, + "nativeDebugging": true } } } \ No newline at end of file diff --git a/Selector.CLI/WatcherService.cs b/Selector.CLI/WatcherService.cs index 01c39c4..c5f3aee 100644 --- a/Selector.CLI/WatcherService.cs +++ b/Selector.CLI/WatcherService.cs @@ -17,6 +17,7 @@ namespace Selector.CLI private const string ConfigInstanceKey = "localconfig"; private readonly ILogger Logger; + private readonly ILoggerFactory LoggerFactory; private readonly RootOptions Config; private readonly IWatcherFactory WatcherFactory; private readonly IWatcherCollectionFactory WatcherCollectionFactory; @@ -28,10 +29,11 @@ namespace Selector.CLI IWatcherFactory watcherFactory, IWatcherCollectionFactory watcherCollectionFactory, IRefreshTokenFactoryProvider spotifyFactory, - ILogger logger, + ILoggerFactory loggerFactory, IOptions config ) { - Logger = logger; + Logger = loggerFactory.CreateLogger(); + LoggerFactory = loggerFactory; Config = config.Value; WatcherFactory = watcherFactory; WatcherCollectionFactory = watcherCollectionFactory; @@ -42,7 +44,7 @@ namespace Selector.CLI public async Task StartAsync(CancellationToken cancellationToken) { - Logger.LogInformation("Starting up"); + Logger.LogInformation("Starting watcher service..."); Logger.LogInformation("Loading config instances..."); var watcherIndices = await InitialiseConfigInstances(); @@ -60,11 +62,11 @@ namespace Selector.CLI var logMsg = new StringBuilder(); if (!string.IsNullOrWhiteSpace(watcherOption.Name)) { - logMsg.Append($"Creating {watcherOption.Name} watcher [{watcherOption.Type}]"); + logMsg.Append($"Creating [{watcherOption.Name}] watcher [{watcherOption.Type}]"); } else { - logMsg.Append($"Creating new {watcherOption.Type} watcher"); + logMsg.Append($"Creating new [{watcherOption.Type}] watcher"); } if (!string.IsNullOrWhiteSpace(watcherOption.PlaylistUri)) logMsg.Append($" [{ watcherOption.PlaylistUri}]"); @@ -92,7 +94,19 @@ namespace Selector.CLI break; } - watcherCollection.Add(watcher); + List 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; @@ -104,6 +118,7 @@ namespace Selector.CLI { try { + Logger.LogInformation($"Starting watcher collection [{index}]"); Watchers[index].Start(); } catch (KeyNotFoundException) @@ -119,7 +134,7 @@ namespace Selector.CLI foreach((var key, var watcher) in Watchers) { - Logger.LogInformation($"Stopping watcher collection: {key}"); + Logger.LogInformation($"Stopping watcher collection [{key}]"); watcher.Stop(); } diff --git a/Selector.CLI/appsettings.json b/Selector.CLI/appsettings.json index 471b077..618455f 100644 --- a/Selector.CLI/appsettings.json +++ b/Selector.CLI/appsettings.json @@ -6,16 +6,17 @@ "Watcher": { "Instances": [ { - "name": "player watcher", + "name": "Player Watcher", "type": "player", - "pollperiod": 1000 + "pollperiod": 2000, + "consumers": [ "audiofeatures" ] } ] } }, "Logging": { "LogLevel": { - "Default": "Information" + "Default": "Trace" } } } \ No newline at end of file diff --git a/Selector.CLI/nlog.config b/Selector.CLI/nlog.config index 808429a..9bc7668 100644 --- a/Selector.CLI/nlog.config +++ b/Selector.CLI/nlog.config @@ -16,6 +16,10 @@ name="logfile" fileName=".\selector.log" layout="${format}" /> + @@ -23,6 +27,9 @@ - + + + + \ No newline at end of file diff --git a/Selector/Consumers/AudioFeatureInjector.cs b/Selector/Consumers/AudioFeatureInjector.cs new file mode 100644 index 0000000..9b079b6 --- /dev/null +++ b/Selector/Consumers/AudioFeatureInjector.cs @@ -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 Logger; + + public CancellationToken CancelToken { get; set; } + + public AnalysedTrackTimeline Timeline { get; set; } = new(); + + public AudioFeatureInjector( + IPlayerWatcher watcher, + ITracksClient trackClient, + ILogger logger = null, + CancellationToken token = default + ){ + Watcher = watcher; + TrackClient = trackClient; + Logger = logger ?? NullLogger.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 + }; + } + } +} diff --git a/Selector/Consumers/AudioFeatureInjectorFactory.cs b/Selector/Consumers/AudioFeatureInjectorFactory.cs new file mode 100644 index 0000000..38579b7 --- /dev/null +++ b/Selector/Consumers/AudioFeatureInjectorFactory.cs @@ -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 Get(ISpotifyConfigFactory spotifyFactory, IPlayerWatcher watcher = null) + { + var config = await spotifyFactory.GetConfig(); + var client = new SpotifyClient(config); + + return new AudioFeatureInjector( + watcher, + client.Tracks, + LoggerFactory.CreateLogger() + ); + } + } +} diff --git a/Selector/Consumers/IConsumer.cs b/Selector/Consumers/IConsumer.cs new file mode 100644 index 0000000..5fe62d4 --- /dev/null +++ b/Selector/Consumers/IConsumer.cs @@ -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); + } +} diff --git a/Selector/Consumers/IConsumerFactory.cs b/Selector/Consumers/IConsumerFactory.cs new file mode 100644 index 0000000..f0e527e --- /dev/null +++ b/Selector/Consumers/IConsumerFactory.cs @@ -0,0 +1,10 @@ +using System; +using System.Threading.Tasks; + +namespace Selector +{ + public interface IConsumerFactory + { + public Task Get(ISpotifyConfigFactory spotifyFactory, IPlayerWatcher watcher); + } +} diff --git a/Selector/Helpers/SpotifyExtensions.cs b/Selector/Helpers/SpotifyExtensions.cs index 77750aa..76b954d 100644 --- a/Selector/Helpers/SpotifyExtensions.cs +++ b/Selector/Helpers/SpotifyExtensions.cs @@ -5,20 +5,23 @@ using System.Linq; using SpotifyAPI.Web; -namespace Selector.Helpers +namespace Selector { public static class SpotifyExtensions { - public static string ToString(this FullTrack track) => $"{track.Name} - {track.Album.Name} - {track.Artists}"; - public static string ToString(this SimpleAlbum album) => $"{album.Name} - {album.Artists}"; - public static string ToString(this SimpleArtist artist) => $"{artist.Name}"; + public static string DisplayString(this FullTrack track) => $"{track.Name} / {track.Album.Name} / {track.Artists.DisplayString()}"; + public static string DisplayString(this SimpleAlbum album) => $"{album.Name} / {album.Artists.DisplayString()}"; + public static string DisplayString(this SimpleArtist artist) => artist.Name; - public static string ToString(this FullEpisode ep) => $"{ep.Name} - {ep.Show}"; - public static string ToString(this SimpleShow show) => $"{show.Name} - {show.Publisher}"; + public static string DisplayString(this FullEpisode ep) => $"{ep.Name} / {ep.Show.DisplayString()}"; + 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 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 artists) => string.Join(", ", artists.Select(a => a.DisplayString())); } } diff --git a/Selector/Timeline/AnalysedTrackTimeline.cs b/Selector/Timeline/AnalysedTrackTimeline.cs new file mode 100644 index 0000000..362865e --- /dev/null +++ b/Selector/Timeline/AnalysedTrackTimeline.cs @@ -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, ITrackTimeline + { + public IEqual EqualityChecker { get; set; } + + public AnalysedTrack Get(FullTrack track) + => GetAll(track) + .LastOrDefault(); + + public IEnumerable GetAll(FullTrack track) + => GetAllTimelineItems(track) + .Select(t => t.Item); + + public IEnumerable> GetAllTimelineItems(FullTrack track) + => Recent + .Where(i => EqualityChecker.IsEqual(i.Item.Track, track)); + + public AnalysedTrack Get(SimpleAlbum album) + => GetAll(album) + .LastOrDefault(); + + public IEnumerable GetAll(SimpleAlbum album) + => GetAllTimelineItems(album) + .Select(t => t.Item); + + public IEnumerable> GetAllTimelineItems(SimpleAlbum album) + => Recent + .Where(i => EqualityChecker.IsEqual(i.Item.Track.Album, album)); + + public AnalysedTrack Get(SimpleArtist artist) + => GetAll(artist) + .LastOrDefault(); + + public IEnumerable GetAll(SimpleArtist artist) + => GetAllTimelineItems(artist) + .Select(t => t.Item); + + public IEnumerable> GetAllTimelineItems(SimpleArtist artist) + => Recent + .Where(i => EqualityChecker.IsEqual(i.Item.Track.Artists[0], artist)); + } +} diff --git a/Selector/Timeline/BaseTimeline.cs b/Selector/Timeline/BaseTimeline.cs new file mode 100644 index 0000000..efa2b10 --- /dev/null +++ b/Selector/Timeline/BaseTimeline.cs @@ -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 : ITimeline, IEnumerable> where T : class + { + protected List> 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.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 GetTimelineItem(DateTime at) + => Recent + .Where(i => i.Time <= at).LastOrDefault(); + + public IEnumerator> GetEnumerator() => Recent.GetEnumerator(); + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + } +} diff --git a/Selector/Timeline/Interfaces/ITrackTimeline.cs b/Selector/Timeline/Interfaces/ITrackTimeline.cs new file mode 100644 index 0000000..54eeb9e --- /dev/null +++ b/Selector/Timeline/Interfaces/ITrackTimeline.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; +using SpotifyAPI.Web; + +namespace Selector +{ + public interface ITrackTimeline: ITimeline + { + public T Get(FullTrack track); + public IEnumerable GetAll(FullTrack track); + public IEnumerable> GetAllTimelineItems(FullTrack track); + + public T Get(SimpleAlbum album); + public IEnumerable GetAll(SimpleAlbum album); + public IEnumerable> GetAllTimelineItems(SimpleAlbum album); + + public T Get(SimpleArtist artist); + public IEnumerable GetAll(SimpleArtist artist); + public IEnumerable> GetAllTimelineItems(SimpleArtist artist); + } +} diff --git a/Selector/Timeline/PlayerTimeline.cs b/Selector/Timeline/PlayerTimeline.cs index 8eff0d1..94e494e 100644 --- a/Selector/Timeline/PlayerTimeline.cs +++ b/Selector/Timeline/PlayerTimeline.cs @@ -7,67 +7,11 @@ using SpotifyAPI.Web; namespace Selector { public class PlayerTimeline - : ITimeline, - IEnumerable> + : BaseTimeline, ITrackTimeline { - - private List> recentlyPlayed = new(); public IEqual EqualityChecker { get; set; } - public bool SortOnBackDate { get; set; } = true; - public int Count { get => recentlyPlayed.Count; } - private int? max = 1000; - 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.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 GetTimelineItem(DateTime at) - => recentlyPlayed - .Where(i => i.Time <= at).LastOrDefault(); + public override void Add(CurrentlyPlayingContext item) => Add(item, DateHelper.FromUnixMilli(item.Timestamp)); public CurrentlyPlayingContext Get(FullTrack track) => GetAll(track) @@ -77,8 +21,8 @@ namespace Selector => GetAllTimelineItems(track) .Select(t => t.Item); - private IEnumerable> GetAllTimelineItems(FullTrack track) - => recentlyPlayed + public IEnumerable> GetAllTimelineItems(FullTrack track) + => Recent .Where(i => i.Item.Item is FullTrack iterTrack && EqualityChecker.IsEqual(iterTrack, track)); @@ -90,8 +34,8 @@ namespace Selector => GetAllTimelineItems(ep) .Select(t => t.Item); - private IEnumerable> GetAllTimelineItems(FullEpisode ep) - => recentlyPlayed + public IEnumerable> GetAllTimelineItems(FullEpisode ep) + => Recent .Where(i => i.Item.Item is FullEpisode iterEp && EqualityChecker.IsEqual(iterEp, ep)); @@ -103,8 +47,8 @@ namespace Selector => GetAllTimelineItems(album) .Select(t => t.Item); - private IEnumerable> GetAllTimelineItems(SimpleAlbum album) - => recentlyPlayed + public IEnumerable> GetAllTimelineItems(SimpleAlbum album) + => Recent .Where(i => i.Item.Item is FullTrack iterTrack && EqualityChecker.IsEqual(iterTrack.Album, album)); @@ -116,8 +60,8 @@ namespace Selector => GetAllTimelineItems(artist) .Select(t => t.Item); - private IEnumerable> GetAllTimelineItems(SimpleArtist artist) - => recentlyPlayed + public IEnumerable> GetAllTimelineItems(SimpleArtist artist) + => Recent .Where(i => i.Item.Item is FullTrack iterTrack && EqualityChecker.IsEqual(iterTrack.Artists[0], artist)); @@ -129,8 +73,8 @@ namespace Selector => GetAllTimelineItems(device) .Select(t => t.Item); - private IEnumerable> GetAllTimelineItems(Device device) - => recentlyPlayed + public IEnumerable> GetAllTimelineItems(Device device) + => Recent .Where(i => EqualityChecker.IsEqual(i.Item.Device, device)); public CurrentlyPlayingContext Get(Context context) @@ -141,11 +85,8 @@ namespace Selector => GetAllTimelineItems(context) .Select(t => t.Item); - private IEnumerable> GetAllTimelineItems(Context context) - => recentlyPlayed + public IEnumerable> GetAllTimelineItems(Context context) + => Recent .Where(i => EqualityChecker.IsEqual(i.Item.Context, context)); - - public IEnumerator> GetEnumerator() => recentlyPlayed.GetEnumerator(); - IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); } } diff --git a/Selector/Watcher/BaseWatcher.cs b/Selector/Watcher/BaseWatcher.cs index 76fbf57..32a3168 100644 --- a/Selector/Watcher/BaseWatcher.cs +++ b/Selector/Watcher/BaseWatcher.cs @@ -5,17 +5,29 @@ using System.Text; using System.Threading; using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; + namespace Selector { public abstract class BaseWatcher: IWatcher { + protected readonly ILogger Logger; + + public BaseWatcher(ILogger logger = null) + { + Logger = logger ?? NullLogger.Instance; + } + public abstract Task WatchOne(CancellationToken token); public async Task Watch(CancellationToken cancelToken) { + Logger.LogDebug("Starting watcher"); while (true) { cancelToken.ThrowIfCancellationRequested(); await WatchOne(cancelToken); + Logger.LogTrace($"Finished watch one, delaying {PollPeriod}ms..."); await Task.Delay(PollPeriod, cancelToken); } } diff --git a/Selector/Watcher/PlayerWatcher.cs b/Selector/Watcher/PlayerWatcher.cs index 52992a0..deee447 100644 --- a/Selector/Watcher/PlayerWatcher.cs +++ b/Selector/Watcher/PlayerWatcher.cs @@ -11,7 +11,7 @@ namespace Selector { public class PlayerWatcher: BaseWatcher, IPlayerWatcher { - private readonly ILogger Logger; + new private readonly ILogger Logger; private readonly IPlayerClient spotifyClient; private readonly IEqual eq; @@ -26,12 +26,13 @@ namespace Selector public event EventHandler PlayingChange; public CurrentlyPlayingContext Live { get; private set; } - public PlayerTimeline Past { get; private set; } + public PlayerTimeline Past { get; set; } public PlayerWatcher(IPlayerClient spotifyClient, IEqual equalityChecker, ILogger logger = null, - int pollPeriod = 3000) { + int pollPeriod = 3000 + ) : base(logger) { this.spotifyClient = spotifyClient; eq = equalityChecker; @@ -44,7 +45,9 @@ namespace Selector token.ThrowIfCancellationRequested(); try{ + Logger.LogTrace("Making Spotify call"); var polledCurrent = await spotifyClient.GetCurrentPlayback(); + Logger.LogTrace($"Received Spotify call [{polledCurrent?.DisplayString()}]"); if (polledCurrent != null) StoreCurrentPlaying(polledCurrent); @@ -70,14 +73,14 @@ namespace Selector if(previous is null && (Live.Item is FullTrack || Live.Item is FullEpisode)) { - Logger.LogDebug($"Playback started: {Live}"); + Logger.LogDebug($"Playback started: {Live.DisplayString()}"); OnPlayingChange(ListeningChangeEventArgs.From(previous, Live)); } // STOPPED PLAYBACK else if((previous.Item is FullTrack || previous.Item is FullEpisode) && Live is null) { - Logger.LogDebug($"Playback stopped: {previous}"); + Logger.LogDebug($"Playback stopped: {previous.DisplayString()}"); OnPlayingChange(ListeningChangeEventArgs.From(previous, Live)); } // CONTINUING PLAYBACK @@ -88,17 +91,17 @@ namespace Selector && Live.Item is FullTrack 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)); } 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)); } 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)); } } @@ -115,7 +118,7 @@ namespace Selector && Live.Item is FullEpisode 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)); } } @@ -125,13 +128,13 @@ namespace Selector // 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)); } // 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)); } diff --git a/Selector/Watcher/WatcherCollection/IWatcherCollection.cs b/Selector/Watcher/WatcherCollection/IWatcherCollection.cs index c5a3899..084a440 100644 --- a/Selector/Watcher/WatcherCollection/IWatcherCollection.cs +++ b/Selector/Watcher/WatcherCollection/IWatcherCollection.cs @@ -8,7 +8,8 @@ namespace Selector { public bool IsRunning { get; } public void Add(IWatcher watcher); - + public void Add(IWatcher watcher, List consumers); + public void Start(); public void Stop(); } diff --git a/Selector/Watcher/WatcherCollection/WatcherCollection.cs b/Selector/Watcher/WatcherCollection/WatcherCollection.cs index b43da4d..973a95b 100644 --- a/Selector/Watcher/WatcherCollection/WatcherCollection.cs +++ b/Selector/Watcher/WatcherCollection/WatcherCollection.cs @@ -34,9 +34,14 @@ namespace Selector public void Add(IWatcher watcher) { - var context = WatcherContext.From(watcher); + Add(watcher, default); + } + + public void Add(IWatcher watcher, List consumers) + { + var context = WatcherContext.From(watcher, consumers); if (IsRunning) context.Start(); - + Watchers.Add(context); } @@ -45,7 +50,7 @@ namespace Selector public void Start() { - Logger.LogDebug($"Starting {Count} watchers"); + Logger.LogDebug($"Starting {Count} watcher(s)"); foreach(var watcher in Watchers) { watcher.Start(); @@ -55,7 +60,7 @@ namespace Selector public void Stop() { - Logger.LogDebug($"Stopping {Count} watchers"); + Logger.LogDebug($"Stopping {Count} watcher(s)"); foreach (var watcher in Watchers) { watcher.Stop(); diff --git a/Selector/Watcher/WatcherCollection/WatcherContext.cs b/Selector/Watcher/WatcherCollection/WatcherContext.cs index 4f6387e..878e6cd 100644 --- a/Selector/Watcher/WatcherCollection/WatcherContext.cs +++ b/Selector/Watcher/WatcherCollection/WatcherContext.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Text; using System.Threading; using System.Threading.Tasks; @@ -9,6 +10,7 @@ namespace Selector public class WatcherContext: IDisposable { public IWatcher Watcher { get; set; } + private List Consumers { get; set; } = new(); public bool IsRunning { get; private set; } public Task Task { get; set; } public CancellationTokenSource TokenSource { get; set; } @@ -18,11 +20,30 @@ namespace Selector Watcher = watcher; } + public WatcherContext(IWatcher watcher, List consumers) + { + Watcher = watcher; + Consumers = consumers ?? new(); + } + public static WatcherContext From(IWatcher watcher) { return new(watcher); } + public static WatcherContext From(IWatcher watcher, List consumers) + { + return new(watcher, consumers); + } + + public void AddConsumer(IConsumer consumer) + { + if (IsRunning) + consumer.Subscribe(Watcher); + + Consumers.Add(consumer); + } + public void Start() { if (IsRunning) @@ -30,6 +51,9 @@ namespace Selector IsRunning = true; TokenSource = new(); + + Consumers.ForEach(c => c.Subscribe(Watcher)); + Task = Watcher.Watch(TokenSource.Token); Task.ContinueWith(t => { @@ -39,6 +63,8 @@ namespace Selector public void Stop() { + Consumers.ForEach(c => c.Unsubscribe(Watcher)); + TokenSource.Cancel(); IsRunning = false; }