diff --git a/Selector.Net/BaseNode.cs b/Selector.Net/BaseNode.cs new file mode 100644 index 0000000..a3d14cf --- /dev/null +++ b/Selector.Net/BaseNode.cs @@ -0,0 +1,9 @@ +using System; + +namespace Selector.Net; + +public class BaseNode : INode +{ + public T Id { get; set; } +} + diff --git a/Selector.Net/BaseSinkSource.cs b/Selector.Net/BaseSinkSource.cs new file mode 100644 index 0000000..b48cb6b --- /dev/null +++ b/Selector.Net/BaseSinkSource.cs @@ -0,0 +1,19 @@ +using System; + +namespace Selector.Net; + +public abstract class BaseSinkSource : BaseSingleSink, ISource +{ + protected Emit EmitHandler { get; set; } + + public void ReceiveHandler(Emit handler) + { + EmitHandler = handler; + } + + protected void Emit(object obj) + { + EmitHandler(this, obj); + } +} + diff --git a/Selector.Net/Graph.cs b/Selector.Net/Graph.cs new file mode 100644 index 0000000..db0129f --- /dev/null +++ b/Selector.Net/Graph.cs @@ -0,0 +1,135 @@ +using System; +using QuikGraph; + +namespace Selector.Net +{ + public class Graph : IGraph + { + protected AdjacencyGraph, SEdge>> graph { get; set; } + private readonly object graphLock = new object(); + + public Graph() + { + graph = new(); + } + + public IEnumerable> Nodes => graph.Vertices; + + public Task AddEdge(INode from, INode to) + { + lock(graphLock) + { + graph.AddVerticesAndEdge(new SEdge>(from, to)); + } + + if (from is ISource fromSource) + { + fromSource.ReceiveHandler(SourceHandler); + } + + if (to is ISource toSource) + { + toSource.ReceiveHandler(SourceHandler); + } + + return Task.CompletedTask; + } + + public Task AddNode(INode node) + { + lock (graphLock) + { + graph.AddVertex(node); + } + + if (node is ISource source) + { + source.ReceiveHandler(SourceHandler); + } + + return Task.CompletedTask; + } + + private async void SourceHandler(ISource sender, object obj, + IEnumerable nodeWhitelist = null, IEnumerable nodeBlacklist = null) + { + if(nodeWhitelist is not null && nodeBlacklist is not null && nodeWhitelist.Any() && nodeBlacklist.Any()) + { + throw new ArgumentException("Cannot provide whitelist and blacklist, at most one"); + } + + if (graph.TryGetOutEdges(sender, out var edges)) + { + foreach (var edge in edges) + { + if (edge.Target is ISink sink) + { + if(nodeWhitelist is not null && nodeWhitelist.Any()) + { + if(nodeWhitelist.Contains(sink.Id)) + { + await sink.Consume(obj); + } + } + else if (nodeBlacklist is not null && nodeBlacklist.Any()) + { + if (!nodeBlacklist.Contains(sink.Id)) + { + await sink.Consume(obj); + } + } + else + { + await sink.Consume(obj); + } + } + } + } + } + + public IEnumerable> GetSinks() + { + foreach (var node in graph.Vertices) + { + if (node is ISink sink) + { + yield return sink; + } + } + } + + public IEnumerable> GetSinks() + { + foreach (var node in graph.Vertices) + { + if (node is ISink sink) + { + yield return sink; + } + } + } + + public IEnumerable> GetSources() + { + foreach (var node in graph.Vertices) + { + if (node is ISource source) + { + yield return source; + } + } + } + + public async Task Sink(string topic, object obj) + { + foreach (var sink in GetSinks()) + { + if (sink.Topics.Contains(topic)) + { + await sink.Consume(obj); + } + } + } + } +} + diff --git a/Selector.Net/Interfaces/IGraph.cs b/Selector.Net/Interfaces/IGraph.cs new file mode 100644 index 0000000..2062d16 --- /dev/null +++ b/Selector.Net/Interfaces/IGraph.cs @@ -0,0 +1,21 @@ +using System; +using QuikGraph; + +namespace Selector.Net +{ + public interface IGraph + { + IEnumerable> Nodes { get; } + + Task AddEdge(INode from, INode to); + Task AddNode(INode node); + + Task Sink(string topic, object obj); + + IEnumerable> GetSources(); + + IEnumerable> GetSinks(); + IEnumerable> GetSinks(); + } +} + diff --git a/Selector.Net/Interfaces/INode.cs b/Selector.Net/Interfaces/INode.cs new file mode 100644 index 0000000..1b6b333 --- /dev/null +++ b/Selector.Net/Interfaces/INode.cs @@ -0,0 +1,10 @@ +using System; + +namespace Selector.Net +{ + public interface INode + { + public T Id { get; set; } + } +} + diff --git a/Selector.Net/Interfaces/ISink.cs b/Selector.Net/Interfaces/ISink.cs new file mode 100644 index 0000000..1a9ac62 --- /dev/null +++ b/Selector.Net/Interfaces/ISink.cs @@ -0,0 +1,25 @@ +using System; +namespace Selector.Net +{ + public interface ISink : INode + { + IEnumerable Topics { get; set; } + + Task Consume(object obj); + } + + /// + /// Not a node, just callback handler + /// + /// + public interface ITypeSink + { + Task ConsumeType(TObj obj); + } + + public interface ISink : ISink, ITypeSink + { + //Task ConsumeType(TObj obj); + } +} + diff --git a/Selector.Net/Interfaces/ISource.cs b/Selector.Net/Interfaces/ISource.cs new file mode 100644 index 0000000..34f56fc --- /dev/null +++ b/Selector.Net/Interfaces/ISource.cs @@ -0,0 +1,17 @@ +using System; +namespace Selector.Net +{ + public delegate void Emit(ISource sender, object obj, IEnumerable nodeWhitelist = null, IEnumerable nodeBlacklist = null); + public delegate void Emit(ISource sender, object obj, IEnumerable nodeWhitelist = null, IEnumerable nodeBlacklist = null); + + public interface ISource : INode + { + void ReceiveHandler(Emit handler); + } + + public interface ISource : INode + { + void ReceiveHandler(Emit handler); + } +} + diff --git a/Selector.Net/Playlist/Added.cs b/Selector.Net/Playlist/Added.cs new file mode 100644 index 0000000..5dd8b37 --- /dev/null +++ b/Selector.Net/Playlist/Added.cs @@ -0,0 +1,56 @@ +using System; +using SpotifyAPI.Web; + +namespace Selector.Net.Playlist; + +public enum AddedType +{ + Since, Before +} + + +public class Added : BaseSinkSource +{ + public DateTime Threshold { get; set; } + public bool IncludeNull { get; set; } + + public AddedType Operator { get; set; } = AddedType.Since; + + private IEnumerable> Filter(IEnumerable> tracks) + { + return tracks.Where(t => + { + if (t.AddedAt is not null) + { + switch (Operator) + { + case AddedType.Since: + return t.AddedAt.Value > Threshold; + case AddedType.Before: + return t.AddedAt.Value < Threshold; + } + } + else + { + if (IncludeNull) + { + return true; + } + } + + return false; + }); + } + + public override Task ConsumeType(PlaylistChangeEventArgs obj) + { + //obj.CurrentTracks = Filter(obj.CurrentTracks); + obj.AddedTracks = Filter(obj.AddedTracks); + obj.RemovedTracks = Filter(obj.RemovedTracks); + + Emit(obj); + + return Task.CompletedTask; + } +} + diff --git a/Selector.Net/Playlist/Aggregator.cs b/Selector.Net/Playlist/Aggregator.cs new file mode 100644 index 0000000..489500d --- /dev/null +++ b/Selector.Net/Playlist/Aggregator.cs @@ -0,0 +1,60 @@ +using System; +using System.Linq; +using Microsoft.Extensions.Logging; +using SpotifyAPI.Web; + +namespace Selector.Net.Playlist +{ + public class AggregatorConfig + { + public string PlaylistId { get; set; } + } + + public class Aggregator: BaseSingleSink + { + private readonly ILogger> Logger; + private readonly ISpotifyClient SpotifyClient; + private readonly ICurrentItemListResolver ItemResolver; + private readonly AggregatorConfig Config; + + public Aggregator(ISpotifyClient spotifyClient, ICurrentItemListResolver itemResolver, AggregatorConfig config, ILogger> logger) + { + Logger = logger; + SpotifyClient = spotifyClient; + ItemResolver = itemResolver; + Config = config; + } + + public override async Task ConsumeType(PlaylistChangeEventArgs obj) + { + try + { + var addedTracks = obj.AddedTracks.ToList(); + var removedTracks = obj.AddedTracks.ToList(); + var removedTrackURIs = obj.AddedTracks.ToList(); + + var currentTracks = await ItemResolver.GetCurrentItems().ConfigureAwait(false); + + currentTracks = currentTracks.Where(t => addedTracks); + currentTracks = currentTracks.Concat(addedTracks); + + } + catch (APIUnauthorizedException e) + { + Logger.LogDebug("Unauthorised error: [{message}] (should be refreshed and retried?)", e.Message); + //throw e; + } + catch (APITooManyRequestsException e) + { + Logger.LogDebug("Too many requests error: [{message}]", e.Message); + // throw e; + } + catch (APIException e) + { + Logger.LogDebug("API error: [{message}]", e.Message); + // throw e; + } + } + } +} + diff --git a/Selector.Net/Playlist/Applier.cs b/Selector.Net/Playlist/Applier.cs new file mode 100644 index 0000000..1dd62d9 --- /dev/null +++ b/Selector.Net/Playlist/Applier.cs @@ -0,0 +1,54 @@ +using System; +using Microsoft.Extensions.Logging; +using SpotifyAPI.Web; + +namespace Selector.Net.Playlist +{ + public class ApplierConfig { + public string PlaylistId { get; set; } + } + + public class Applier: BaseSingleSink + { + private readonly ILogger> Logger; + private readonly ISpotifyClient SpotifyClient; + private readonly ApplierConfig Config; + + private FullPlaylist Playlist { get; set; } + private IEnumerable> Items { get; set; } + + public Applier(ISpotifyClient spotifyClient, ApplierConfig config, ILogger> logger) + { + Logger = logger; + SpotifyClient = spotifyClient; + Config = config; + } + + public override async Task ConsumeType(PlaylistChangeEventArgs obj) + { + try + { + var tracks = obj.CurrentTracks.ToList(); + var trackUris = tracks.Select(t => t.GetUri()).ToList(); + + await SpotifyClient.Playlists.ReplaceItems(Config.PlaylistId, new PlaylistReplaceItemsRequest(trackUris)); + } + catch (APIUnauthorizedException e) + { + Logger.LogDebug("Unauthorised error: [{message}] (should be refreshed and retried?)", e.Message); + //throw e; + } + catch (APITooManyRequestsException e) + { + Logger.LogDebug("Too many requests error: [{message}]", e.Message); + // throw e; + } + catch (APIException e) + { + Logger.LogDebug("API error: [{message}]", e.Message); + // throw e; + } + } + } +} + diff --git a/Selector.Net/Playlist/CurrentTrackResolver.cs b/Selector.Net/Playlist/CurrentTrackResolver.cs new file mode 100644 index 0000000..3049455 --- /dev/null +++ b/Selector.Net/Playlist/CurrentTrackResolver.cs @@ -0,0 +1,11 @@ +using System; +using SpotifyAPI.Web; + +namespace Selector.Net.Playlist +{ + public interface ICurrentItemListResolver + { + Task>> GetCurrentItems(); + } +} + diff --git a/Selector.Net/Playlist/ItemFilter.cs b/Selector.Net/Playlist/ItemFilter.cs new file mode 100644 index 0000000..0a3b81c --- /dev/null +++ b/Selector.Net/Playlist/ItemFilter.cs @@ -0,0 +1,26 @@ +using System; +using SpotifyAPI.Web; + +namespace Selector.Net.Playlist; + +public class ItemFilter : BaseSinkSource +{ + public Func, bool> Func { get; set; } + + public ItemFilter(Func, bool> func) + { + Func = func; + } + + public override Task ConsumeType(PlaylistChangeEventArgs obj) + { + //obj.CurrentTracks = obj.CurrentTracks.Where(Func); + obj.AddedTracks = obj.AddedTracks.Where(Func); + obj.RemovedTracks = obj.RemovedTracks.Where(Func); + + Emit(obj); + + return Task.CompletedTask; + } +} + diff --git a/Selector.Net/Playlist/ItemMutator.cs b/Selector.Net/Playlist/ItemMutator.cs new file mode 100644 index 0000000..059d63b --- /dev/null +++ b/Selector.Net/Playlist/ItemMutator.cs @@ -0,0 +1,26 @@ +using System; +using SpotifyAPI.Web; + +namespace Selector.Net.Playlist; + +public class ItemMutator : BaseSinkSource +{ + public Func, PlaylistTrack> Func { get; set; } + + public ItemMutator(Func, PlaylistTrack> func) + { + Func = func; + } + + public override Task ConsumeType(PlaylistChangeEventArgs obj) + { + //obj.CurrentTracks = obj.CurrentTracks.Select(Func); + obj.AddedTracks = obj.AddedTracks.Select(Func); + obj.RemovedTracks = obj.RemovedTracks.Select(Func); + + Emit(obj); + + return Task.CompletedTask; + } +} + diff --git a/Selector.Net/Playlist/PlaylistFilter.cs b/Selector.Net/Playlist/PlaylistFilter.cs new file mode 100644 index 0000000..9893729 --- /dev/null +++ b/Selector.Net/Playlist/PlaylistFilter.cs @@ -0,0 +1,68 @@ +using System; + +namespace Selector.Net.Playlist +{ + public class PlaylistFilterConfig + { + public IEnumerable NameWhiteList { get; set; } + public IEnumerable NameBlackList { get; set; } + public IEnumerable UriWhiteList { get; set; } + public IEnumerable UriBlackList { get; set; } + } + + public class PlaylistFilter : BaseSinkSource + { + public IEnumerable NameWhiteList { get; set; } + public IEnumerable NameBlackList { get; set; } + public IEnumerable UriWhiteList { get; set; } + public IEnumerable UriBlackList { get; set; } + + public PlaylistFilter() { } + + public PlaylistFilter(PlaylistFilterConfig config) + { + NameWhiteList = config.NameWhiteList; + NameBlackList = config.NameBlackList; + UriWhiteList = config.UriWhiteList; + UriBlackList = config.UriBlackList; + } + + public override Task ConsumeType(PlaylistChangeEventArgs obj) + { + if (NameWhiteList is not null && NameWhiteList.Any()) + { + if (NameWhiteList.Contains(obj.Current.Name)) + { + Emit(obj); + } + } + + if (NameBlackList is not null && NameBlackList.Any()) + { + if (!NameBlackList.Contains(obj.Current.Name)) + { + Emit(obj); + } + } + + if (UriWhiteList is not null && UriWhiteList.Any()) + { + if (UriWhiteList.Contains(obj.Current.Name)) + { + Emit(obj); + } + } + + if (UriBlackList is not null && UriBlackList.Any()) + { + if (!UriBlackList.Contains(obj.Current.Name)) + { + Emit(obj); + } + } + + return Task.CompletedTask; + } + } +} + diff --git a/Selector.Net/Playlist/TypeFilter.cs b/Selector.Net/Playlist/TypeFilter.cs new file mode 100644 index 0000000..d82b228 --- /dev/null +++ b/Selector.Net/Playlist/TypeFilter.cs @@ -0,0 +1,40 @@ +using System; +using SpotifyAPI.Web; + +namespace Selector.Net.Playlist; + +public enum PlayableItemType +{ + Track, Episode +} + +public class TypeFilter : BaseSinkSource>> +{ + public PlayableItemType FilterType { get; set; } + + public TypeFilter(PlayableItemType filterType) + { + FilterType = filterType; + } + + public override Task ConsumeType(IEnumerable> tracks) + { + switch (FilterType) + { + case PlayableItemType.Track: + + Emit(tracks.Where(i => i.Track is FullTrack)); + + break; + case PlayableItemType.Episode: + + Emit(tracks.Where(i => i.Track is FullEpisode)); + + break; + } + + + return Task.CompletedTask; + } +} + diff --git a/Selector.Net/PlaylistGraph.cs b/Selector.Net/PlaylistGraph.cs new file mode 100644 index 0000000..3a6aede --- /dev/null +++ b/Selector.Net/PlaylistGraph.cs @@ -0,0 +1,18 @@ +using System; +namespace Selector.Net.Playlist; + +public class PlaylistGraph +{ + private IGraph graph { get; set; } + + public PlaylistGraph(PlaylistFilterConfig filterConfig) + { + graph = new Graph(); + + var entryFilter = new PlaylistFilter(filterConfig) + { + Topics = new[] { "track-entry" } + }; + } +} + diff --git a/Selector.Net/Repeater.cs b/Selector.Net/Repeater.cs new file mode 100644 index 0000000..6e3c9b5 --- /dev/null +++ b/Selector.Net/Repeater.cs @@ -0,0 +1,18 @@ +using System; + +namespace Selector.Net; + +public class Repeater: BaseSource, ISink +{ + public IEnumerable Topics { get; set; } + + private Type[] _types = Array.Empty(); + public IEnumerable Types => _types; + + public Task Consume(object obj) + { + Emit(obj); + + return Task.CompletedTask; + } +} diff --git a/Selector.Net/Selector.Net.csproj b/Selector.Net/Selector.Net.csproj new file mode 100644 index 0000000..9537d11 --- /dev/null +++ b/Selector.Net/Selector.Net.csproj @@ -0,0 +1,32 @@ + + + + net6.0 + enable + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Selector.Net/Sink/BaseMultiSink.cs b/Selector.Net/Sink/BaseMultiSink.cs new file mode 100644 index 0000000..7baf6c2 --- /dev/null +++ b/Selector.Net/Sink/BaseMultiSink.cs @@ -0,0 +1,30 @@ +using System; + +namespace Selector.Net; + +//public abstract class BaseMultiSink: BaseNode, ISink +//{ +// public IEnumerable Topics { get; } + +// public IDictionary> Callbacks { get; set; } + + +// public Task Consume(object obj) +// { +// var objType = obj.GetType(); + +// if(Callbacks.ContainsKey(objType)) +// { +// var callback = Callbacks[objType]; + +// var objCast = (TObj)obj; + +// return ConsumeType(objCast); +// } +// else +// { +// throw new ArgumentException("Not of acceptable payload type"); +// } +// } +//} + diff --git a/Selector.Net/Sink/BaseSingleSink.cs b/Selector.Net/Sink/BaseSingleSink.cs new file mode 100644 index 0000000..c285dfb --- /dev/null +++ b/Selector.Net/Sink/BaseSingleSink.cs @@ -0,0 +1,27 @@ +using System; + +namespace Selector.Net; + +public abstract class BaseSingleSink: BaseNode, ISink +{ + public IEnumerable Topics { get; set; } + + public Task Consume(object obj) + { + var type = GetType(); + var genericArgs = type.GetGenericArguments(); + var objType = obj.GetType(); + + if (objType.IsAssignableTo(genericArgs[1])) + { + var objCast = (TObj)obj; + + return ConsumeType(objCast); + } + + return Task.CompletedTask; + } + + public abstract Task ConsumeType(TObj obj); +} + diff --git a/Selector.Net/Sink/DebugSink.cs b/Selector.Net/Sink/DebugSink.cs new file mode 100644 index 0000000..1d971d3 --- /dev/null +++ b/Selector.Net/Sink/DebugSink.cs @@ -0,0 +1,28 @@ +using System; +using Microsoft.Extensions.Logging; + +namespace Selector.Net; + +public class DebugSink: ISink +{ + public IEnumerable Topics { get; set; } + public TNodeId Id { get; set; } + + public ILogger> Logger { get; set; } + + public Task Consume(object obj) + { + var type = obj.GetType(); + + if (Logger is not null) + { + Logger.LogDebug("{} Received, {}", type, obj); + }else + { + Console.WriteLine("{0} Received, {1}", type, obj); + } + + return Task.CompletedTask; + } +} + diff --git a/Selector.Net/Sink/EmptySink.cs b/Selector.Net/Sink/EmptySink.cs new file mode 100644 index 0000000..f45d22e --- /dev/null +++ b/Selector.Net/Sink/EmptySink.cs @@ -0,0 +1,12 @@ +using System; + +namespace Selector.Net; + +public class EmptySink: BaseSingleSink +{ + public override Task ConsumeType(TObj obj) + { + return Task.CompletedTask; + } +} + diff --git a/Selector.Net/Source/BaseSource.cs b/Selector.Net/Source/BaseSource.cs new file mode 100644 index 0000000..619b9d6 --- /dev/null +++ b/Selector.Net/Source/BaseSource.cs @@ -0,0 +1,19 @@ +using System; + +namespace Selector.Net; + +public abstract class BaseSource: BaseNode, ISource +{ + protected Emit EmitHandler { get; set; } + + public void ReceiveHandler(Emit handler) + { + EmitHandler = handler; + } + + protected void Emit(object obj) + { + EmitHandler(this, obj); + } +} + diff --git a/Selector.Net/Source/TriggerSource.cs b/Selector.Net/Source/TriggerSource.cs new file mode 100644 index 0000000..0f77e68 --- /dev/null +++ b/Selector.Net/Source/TriggerSource.cs @@ -0,0 +1,10 @@ +namespace Selector.Net; + +public class TriggerSource : BaseSource +{ + public void Trigger(T obj) + { + Emit(obj); + } +} + diff --git a/Selector.Tests/Net/Graph.cs b/Selector.Tests/Net/Graph.cs new file mode 100644 index 0000000..70647d6 --- /dev/null +++ b/Selector.Tests/Net/Graph.cs @@ -0,0 +1,127 @@ +using System; +using System.Collections.Generic; +using QuikGraph; +using Selector.Net; +using Xunit; +using FluentAssertions; +using Moq; + +namespace Selector.Tests.Net +{ + public class GraphTests + { + [Fact] + public void Network() + { + var net = new Graph(); + + var trigger = new TriggerSource() + { + Id = "trigger" + }; + + var sink = new EmptySink() + { + Id = "sink" + }; + + net.AddEdge( + trigger, + sink + ); + + trigger.Trigger("payload"); + } + + [Fact] + public void SourceReceivesPayload() + { + var net = new Graph(); + + var trigger = new TriggerSource() + { + Id = "trigger" + }; + + var sink = new Mock>(); + + net.AddEdge( + trigger, + sink.Object + ); + + var payload = "payload"; + + trigger.Trigger(payload); + + sink.Verify(a => a.Consume(payload), Times.Once()); + sink.VerifyNoOtherCalls(); + } + + [Fact] + public void SourceReceivesGraphPayload() + { + var net = new Graph(); + + var sink = new Mock>(); + sink.Setup(a => a.Topics).Returns(new[] {"topic1"}); + + net.AddNode( + sink.Object + ); + + var payload = "payload"; + + net.Sink("topic1", payload); + + sink.Verify(a => a.Consume(payload), Times.Once()); + sink.Verify(a => a.Topics, Times.Once()); + sink.VerifyNoOtherCalls(); + } + + [Fact] + public void SourceReceivesRepeatedPayload() + { + var net = new Graph(); + + var trigger = new TriggerSource() + { + Id = "trigger" + }; + + var repeater = new Repeater(); + var repeater2 = new Repeater(); + var repeater3 = new Repeater(); + + var sink = new Mock>(); + + net.AddEdge( + trigger, + repeater + ); + + net.AddEdge( + repeater, + repeater2 + ); + + net.AddEdge( + repeater2, + repeater3 + ); + + net.AddEdge( + repeater3, + sink.Object + ); + + var payload = "payload"; + + trigger.Trigger(payload); + + sink.Verify(a => a.Consume(payload), Times.Once()); + sink.VerifyNoOtherCalls(); + } + } +} + diff --git a/Selector.Tests/Selector.Tests.csproj b/Selector.Tests/Selector.Tests.csproj index 8d71bca..465b817 100644 --- a/Selector.Tests/Selector.Tests.csproj +++ b/Selector.Tests/Selector.Tests.csproj @@ -24,6 +24,13 @@ + + + + + + + diff --git a/Selector.Tests/Watcher/PlayerWatcher.cs b/Selector.Tests/Watcher/PlayerWatcher.cs index 9a4ed7b..23e553c 100644 --- a/Selector.Tests/Watcher/PlayerWatcher.cs +++ b/Selector.Tests/Watcher/PlayerWatcher.cs @@ -9,233 +9,232 @@ using System.Threading; using System.Threading.Tasks; using Xunit.Sdk; -namespace Selector.Tests +namespace Selector.Tests; + +public class PlayerWatcherTests { - public class PlayerWatcherTests + public static IEnumerable NowPlayingData => + new List { - public static IEnumerable NowPlayingData => - new List - { - new object[] { new List(){ - Helper.CurrentPlayback(Helper.FullTrack("track1", "album1", "artist1")), - Helper.CurrentPlayback(Helper.FullTrack("track2", "album2", "artist2")), - Helper.CurrentPlayback(Helper.FullTrack("track3", "album3", "artist3")), - } - } - }; - - [Theory] - [MemberData(nameof(NowPlayingData))] - public async void NowPlaying(List playing) - { - var playingQueue = new Queue(playing); - - var spotMock = new Mock(); - var eq = new UriEqual(); - - spotMock.Setup(s => s.GetCurrentPlayback().Result).Returns(playingQueue.Dequeue); - - var watcher = new PlayerWatcher(spotMock.Object, eq); - - for (var i = 0; i < playing.Count; i++) - { - await watcher.WatchOne(); - watcher.Live.Should().Be(playing[i]); + new object[] { new List(){ + Helper.CurrentPlayback(Helper.FullTrack("track1", "album1", "artist1")), + Helper.CurrentPlayback(Helper.FullTrack("track2", "album2", "artist2")), + Helper.CurrentPlayback(Helper.FullTrack("track3", "album3", "artist3")), } } + }; - public static IEnumerable EventsData => - new List + [Theory] + [MemberData(nameof(NowPlayingData))] + public async void NowPlaying(List playing) + { + var playingQueue = new Queue(playing); + + var spotMock = new Mock(); + var eq = new UriEqual(); + + spotMock.Setup(s => s.GetCurrentPlayback().Result).Returns(playingQueue.Dequeue); + + var watcher = new PlayerWatcher(spotMock.Object, eq); + + for (var i = 0; i < playing.Count; i++) { - // NO CHANGING - new object[] { new List(){ - Helper.CurrentPlayback(Helper.FullTrack("nochange", "album1", "artist1"), isPlaying: true, context: "context1"), - Helper.CurrentPlayback(Helper.FullTrack("nochange", "album1", "artist1"), isPlaying: true, context: "context1"), - Helper.CurrentPlayback(Helper.FullTrack("nochange", "album1", "artist1"), isPlaying: true, context: "context1"), - }, - // to raise - new List(){ "ItemChange", "ContextChange", "PlayingChange", "DeviceChange", "VolumeChange" }, - // to not raise - new List(){ "AlbumChange", "ArtistChange" } - }, - // TRACK CHANGE - new object[] { new List(){ - Helper.CurrentPlayback(Helper.FullTrack("trackchange1", "album1", "artist1")), - Helper.CurrentPlayback(Helper.FullTrack("trackchange2", "album1", "artist1")) - }, - // to raise - new List(){ "ContextChange", "PlayingChange", "ItemChange", "DeviceChange", "VolumeChange" }, - // to not raise - new List(){ "AlbumChange", "ArtistChange" } - }, - // ALBUM CHANGE - new object[] { new List(){ - Helper.CurrentPlayback(Helper.FullTrack("albumchange", "album1", "artist1")), - Helper.CurrentPlayback(Helper.FullTrack("albumchange", "album2", "artist1")) - }, - // to raise - new List(){ "ContextChange", "PlayingChange", "ItemChange", "AlbumChange", "DeviceChange", "VolumeChange" }, - // to not raise - new List(){ "ArtistChange" } - }, - // ARTIST CHANGE - new object[] { new List(){ - Helper.CurrentPlayback(Helper.FullTrack("artistchange", "album1", "artist1")), - Helper.CurrentPlayback(Helper.FullTrack("artistchange", "album1", "artist2")) - }, - // to raise - new List(){ "ContextChange", "PlayingChange", "ItemChange", "ArtistChange", "DeviceChange", "VolumeChange" }, - // to not raise - new List(){ "AlbumChange" } - }, - // CONTEXT CHANGE - new object[] { new List(){ - Helper.CurrentPlayback(Helper.FullTrack("contextchange", "album1", "artist1"), context: "context1"), - Helper.CurrentPlayback(Helper.FullTrack("contextchange", "album1", "artist1"), context: "context2") - }, - // to raise - new List(){ "PlayingChange", "ItemChange", "ContextChange", "DeviceChange", "VolumeChange" }, - // to not raise - new List(){ "AlbumChange", "ArtistChange" } - }, - // PLAYING CHANGE - new object[] { new List(){ - Helper.CurrentPlayback(Helper.FullTrack("playingchange1", "album1", "artist1"), isPlaying: true, context: "context1"), - Helper.CurrentPlayback(Helper.FullTrack("playingchange1", "album1", "artist1"), isPlaying: false, context: "context1") - }, - // to raise - new List(){ "ContextChange", "ItemChange", "PlayingChange", "DeviceChange", "VolumeChange" }, - // to not raise - new List(){ "AlbumChange", "ArtistChange" } - }, - // PLAYING CHANGE - new object[] { new List(){ - Helper.CurrentPlayback(Helper.FullTrack("playingchange2", "album1", "artist1"), isPlaying: false, context: "context1"), - Helper.CurrentPlayback(Helper.FullTrack("playingchange2", "album1", "artist1"), isPlaying: true, context: "context1") - }, - // to raise - new List(){ "ContextChange", "ItemChange", "PlayingChange", "DeviceChange", "VolumeChange" }, - // to not raise - new List(){ "AlbumChange", "ArtistChange" } - }, - // CONTENT CHANGE - new object[] { new List(){ - Helper.CurrentPlayback(Helper.FullTrack("contentchange1", "album1", "artist1"), isPlaying: true, context: "context1"), - Helper.CurrentPlayback(Helper.FullEpisode("contentchange1", "show1", "pub1"), isPlaying: true, context: "context2") - }, - // to raise - new List(){ "PlayingChange", "ContentChange", "ContextChange", "ItemChange", "DeviceChange", "VolumeChange" }, - // to not raise - new List(){ "AlbumChange", "ArtistChange" } - }, - // CONTENT CHANGE - new object[] { new List(){ - Helper.CurrentPlayback(Helper.FullEpisode("contentchange1", "show1", "pub1"), isPlaying: true, context: "context2"), - Helper.CurrentPlayback(Helper.FullTrack("contentchange1", "album1", "artist1"), isPlaying: true, context: "context1") - }, - // to raise - new List(){ "PlayingChange", "ContentChange", "ContextChange", "ItemChange", "DeviceChange", "VolumeChange" }, - // to not raise - new List(){ "AlbumChange", "ArtistChange" } - }, - // DEVICE CHANGE - new object[] { new List(){ - Helper.CurrentPlayback(Helper.FullTrack("devicechange", "album1", "artist1"), device: Helper.Device("dev1")), - Helper.CurrentPlayback(Helper.FullTrack("devicechange", "album1", "artist1"), device: Helper.Device("dev2")) - }, - // to raise - new List(){ "ContextChange", "PlayingChange", "ItemChange", "VolumeChange", "DeviceChange" }, - // to not raise - new List(){ "AlbumChange", "ArtistChange", "ContentChange" } - }, - // VOLUME CHANGE - new object[] { new List(){ - Helper.CurrentPlayback(Helper.FullTrack("volumechange", "album1", "artist1"), device: Helper.Device("dev1", volume: 50)), - Helper.CurrentPlayback(Helper.FullTrack("volumechange", "album1", "artist1"), device: Helper.Device("dev1", volume: 60)) - }, - // to raise - new List(){ "ContextChange", "PlayingChange", "ItemChange", "VolumeChange", "DeviceChange" }, - // to not raise - new List(){ "AlbumChange", "ArtistChange", "ContentChange" } - }, - // // STARTED PLAYBACK - // new object[] { new List(){ - // null, - // Helper.CurrentPlayback(Helper.FullTrack("track1", "album1", "artist1"), isPlaying: true, context: "context1") - // }, - // // to raise - // new List(){ "PlayingChange" }, - // // to not raise - // new List(){ "AlbumChange", "ArtistChange", "ContentChange", "ContextChange", "ItemChange", "DeviceChange", "VolumeChange" } - // }, - // // STARTED PLAYBACK - // new object[] { new List(){ - // Helper.CurrentPlayback(Helper.FullTrack("track1", "album1", "artist1"), isPlaying: true, context: "context1"), - // null - // }, - // // to raise - // new List(){ "PlayingChange" }, - // // to not raise - // new List(){ "AlbumChange", "ArtistChange", "ContentChange", "ContextChange", "ItemChange", "DeviceChange", "VolumeChange" } - // } - }; - - [Theory] - [MemberData(nameof(EventsData))] - public async void Events(List playing, List toRaise, List toNotRaise) - { - var playingQueue = new Queue(playing); - - var spotMock = new Mock(); - var eq = new UriEqual(); - - spotMock.Setup( - s => s.GetCurrentPlayback().Result - ).Returns(playingQueue.Dequeue); - - var watcher = new PlayerWatcher(spotMock.Object, eq); - using var monitoredWatcher = watcher.Monitor(); - - for (var i = 0; i < playing.Count; i++) - { - await watcher.WatchOne(); - } - - toRaise.ForEach(r => monitoredWatcher.Should().Raise(r).WithSender(watcher)); - toNotRaise.ForEach(r => monitoredWatcher.Should().NotRaise(r)); + await watcher.WatchOne(); + watcher.Live.Should().Be(playing[i]); } - - [Theory] - [InlineData(1000, 3500, 4)] - [InlineData(500, 3800, 8)] - [InlineData(100, 250, 3)] - public async void Watch(int pollPeriod, int execTime, int numberOfCalls) - { - var spotMock = new Mock(); - var eq = new UriEqual(); - var watch = new PlayerWatcher(spotMock.Object, eq) - { - PollPeriod = pollPeriod - }; - - var tokenSource = new CancellationTokenSource(); - var task = watch.Watch(tokenSource.Token); - - await Task.Delay(execTime); - tokenSource.Cancel(); - - spotMock.Verify(s => s.GetCurrentPlayback(), Times.Exactly(numberOfCalls)); - } - - // [Fact] - // public async void Auth() - // { - // var spot = new SpotifyClient(""); - // var eq = new UriEqual(); - // var watch = new PlayerWatcher(spot.Player, eq); - - // var token = new CancellationTokenSource(); - // await watch.Watch(token.Token); - // } } + + public static IEnumerable EventsData => + new List + { + // NO CHANGING + new object[] { new List(){ + Helper.CurrentPlayback(Helper.FullTrack("nochange", "album1", "artist1"), isPlaying: true, context: "context1"), + Helper.CurrentPlayback(Helper.FullTrack("nochange", "album1", "artist1"), isPlaying: true, context: "context1"), + Helper.CurrentPlayback(Helper.FullTrack("nochange", "album1", "artist1"), isPlaying: true, context: "context1"), + }, + // to raise + new List(){ "ItemChange", "ContextChange", "PlayingChange", "DeviceChange", "VolumeChange" }, + // to not raise + new List(){ "AlbumChange", "ArtistChange" } + }, + // TRACK CHANGE + new object[] { new List(){ + Helper.CurrentPlayback(Helper.FullTrack("trackchange1", "album1", "artist1")), + Helper.CurrentPlayback(Helper.FullTrack("trackchange2", "album1", "artist1")) + }, + // to raise + new List(){ "ContextChange", "PlayingChange", "ItemChange", "DeviceChange", "VolumeChange" }, + // to not raise + new List(){ "AlbumChange", "ArtistChange" } + }, + // ALBUM CHANGE + new object[] { new List(){ + Helper.CurrentPlayback(Helper.FullTrack("albumchange", "album1", "artist1")), + Helper.CurrentPlayback(Helper.FullTrack("albumchange", "album2", "artist1")) + }, + // to raise + new List(){ "ContextChange", "PlayingChange", "ItemChange", "AlbumChange", "DeviceChange", "VolumeChange" }, + // to not raise + new List(){ "ArtistChange" } + }, + // ARTIST CHANGE + new object[] { new List(){ + Helper.CurrentPlayback(Helper.FullTrack("artistchange", "album1", "artist1")), + Helper.CurrentPlayback(Helper.FullTrack("artistchange", "album1", "artist2")) + }, + // to raise + new List(){ "ContextChange", "PlayingChange", "ItemChange", "ArtistChange", "DeviceChange", "VolumeChange" }, + // to not raise + new List(){ "AlbumChange" } + }, + // CONTEXT CHANGE + new object[] { new List(){ + Helper.CurrentPlayback(Helper.FullTrack("contextchange", "album1", "artist1"), context: "context1"), + Helper.CurrentPlayback(Helper.FullTrack("contextchange", "album1", "artist1"), context: "context2") + }, + // to raise + new List(){ "PlayingChange", "ItemChange", "ContextChange", "DeviceChange", "VolumeChange" }, + // to not raise + new List(){ "AlbumChange", "ArtistChange" } + }, + // PLAYING CHANGE + new object[] { new List(){ + Helper.CurrentPlayback(Helper.FullTrack("playingchange1", "album1", "artist1"), isPlaying: true, context: "context1"), + Helper.CurrentPlayback(Helper.FullTrack("playingchange1", "album1", "artist1"), isPlaying: false, context: "context1") + }, + // to raise + new List(){ "ContextChange", "ItemChange", "PlayingChange", "DeviceChange", "VolumeChange" }, + // to not raise + new List(){ "AlbumChange", "ArtistChange" } + }, + // PLAYING CHANGE + new object[] { new List(){ + Helper.CurrentPlayback(Helper.FullTrack("playingchange2", "album1", "artist1"), isPlaying: false, context: "context1"), + Helper.CurrentPlayback(Helper.FullTrack("playingchange2", "album1", "artist1"), isPlaying: true, context: "context1") + }, + // to raise + new List(){ "ContextChange", "ItemChange", "PlayingChange", "DeviceChange", "VolumeChange" }, + // to not raise + new List(){ "AlbumChange", "ArtistChange" } + }, + // CONTENT CHANGE + new object[] { new List(){ + Helper.CurrentPlayback(Helper.FullTrack("contentchange1", "album1", "artist1"), isPlaying: true, context: "context1"), + Helper.CurrentPlayback(Helper.FullEpisode("contentchange1", "show1", "pub1"), isPlaying: true, context: "context2") + }, + // to raise + new List(){ "PlayingChange", "ContentChange", "ContextChange", "ItemChange", "DeviceChange", "VolumeChange" }, + // to not raise + new List(){ "AlbumChange", "ArtistChange" } + }, + // CONTENT CHANGE + new object[] { new List(){ + Helper.CurrentPlayback(Helper.FullEpisode("contentchange1", "show1", "pub1"), isPlaying: true, context: "context2"), + Helper.CurrentPlayback(Helper.FullTrack("contentchange1", "album1", "artist1"), isPlaying: true, context: "context1") + }, + // to raise + new List(){ "PlayingChange", "ContentChange", "ContextChange", "ItemChange", "DeviceChange", "VolumeChange" }, + // to not raise + new List(){ "AlbumChange", "ArtistChange" } + }, + // DEVICE CHANGE + new object[] { new List(){ + Helper.CurrentPlayback(Helper.FullTrack("devicechange", "album1", "artist1"), device: Helper.Device("dev1")), + Helper.CurrentPlayback(Helper.FullTrack("devicechange", "album1", "artist1"), device: Helper.Device("dev2")) + }, + // to raise + new List(){ "ContextChange", "PlayingChange", "ItemChange", "VolumeChange", "DeviceChange" }, + // to not raise + new List(){ "AlbumChange", "ArtistChange", "ContentChange" } + }, + // VOLUME CHANGE + new object[] { new List(){ + Helper.CurrentPlayback(Helper.FullTrack("volumechange", "album1", "artist1"), device: Helper.Device("dev1", volume: 50)), + Helper.CurrentPlayback(Helper.FullTrack("volumechange", "album1", "artist1"), device: Helper.Device("dev1", volume: 60)) + }, + // to raise + new List(){ "ContextChange", "PlayingChange", "ItemChange", "VolumeChange", "DeviceChange" }, + // to not raise + new List(){ "AlbumChange", "ArtistChange", "ContentChange" } + }, + // // STARTED PLAYBACK + // new object[] { new List(){ + // null, + // Helper.CurrentPlayback(Helper.FullTrack("track1", "album1", "artist1"), isPlaying: true, context: "context1") + // }, + // // to raise + // new List(){ "PlayingChange" }, + // // to not raise + // new List(){ "AlbumChange", "ArtistChange", "ContentChange", "ContextChange", "ItemChange", "DeviceChange", "VolumeChange" } + // }, + // // STARTED PLAYBACK + // new object[] { new List(){ + // Helper.CurrentPlayback(Helper.FullTrack("track1", "album1", "artist1"), isPlaying: true, context: "context1"), + // null + // }, + // // to raise + // new List(){ "PlayingChange" }, + // // to not raise + // new List(){ "AlbumChange", "ArtistChange", "ContentChange", "ContextChange", "ItemChange", "DeviceChange", "VolumeChange" } + // } + }; + + [Theory] + [MemberData(nameof(EventsData))] + public async void Events(List playing, List toRaise, List toNotRaise) + { + var playingQueue = new Queue(playing); + + var spotMock = new Mock(); + var eq = new UriEqual(); + + spotMock.Setup( + s => s.GetCurrentPlayback().Result + ).Returns(playingQueue.Dequeue); + + var watcher = new PlayerWatcher(spotMock.Object, eq); + using var monitoredWatcher = watcher.Monitor(); + + for (var i = 0; i < playing.Count; i++) + { + await watcher.WatchOne(); + } + + toRaise.ForEach(r => monitoredWatcher.Should().Raise(r).WithSender(watcher)); + toNotRaise.ForEach(r => monitoredWatcher.Should().NotRaise(r)); + } + + [Theory] + [InlineData(1000, 3500, 4)] + [InlineData(500, 3800, 8)] + [InlineData(100, 250, 3)] + public async void Watch(int pollPeriod, int execTime, int numberOfCalls) + { + var spotMock = new Mock(); + var eq = new UriEqual(); + var watch = new PlayerWatcher(spotMock.Object, eq) + { + PollPeriod = pollPeriod + }; + + var tokenSource = new CancellationTokenSource(); + var task = watch.Watch(tokenSource.Token); + + await Task.Delay(execTime); + tokenSource.Cancel(); + + spotMock.Verify(s => s.GetCurrentPlayback(), Times.Exactly(numberOfCalls)); + } + + // [Fact] + // public async void Auth() + // { + // var spot = new SpotifyClient(""); + // var eq = new UriEqual(); + // var watch = new PlayerWatcher(spot.Player, eq); + + // var token = new CancellationTokenSource(); + // await watch.Watch(token.Token); + // } } diff --git a/Selector.sln b/Selector.sln index d79642b..b6d5fdd 100644 --- a/Selector.sln +++ b/Selector.sln @@ -24,6 +24,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Selector.Cache", "Selector. EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Selector.Event", "Selector.Event\Selector.Event.csproj", "{C2FF1673-CB1A-43B7-A814-07BB3CB3A0D6}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Selector.Net", "Selector.Net\Selector.Net.csproj", "{825F16A4-DB67-4CC4-A4D0-8326D297E227}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -58,6 +60,10 @@ Global {C2FF1673-CB1A-43B7-A814-07BB3CB3A0D6}.Debug|Any CPU.Build.0 = Debug|Any CPU {C2FF1673-CB1A-43B7-A814-07BB3CB3A0D6}.Release|Any CPU.ActiveCfg = Release|Any CPU {C2FF1673-CB1A-43B7-A814-07BB3CB3A0D6}.Release|Any CPU.Build.0 = Release|Any CPU + {825F16A4-DB67-4CC4-A4D0-8326D297E227}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {825F16A4-DB67-4CC4-A4D0-8326D297E227}.Debug|Any CPU.Build.0 = Debug|Any CPU + {825F16A4-DB67-4CC4-A4D0-8326D297E227}.Release|Any CPU.ActiveCfg = Release|Any CPU + {825F16A4-DB67-4CC4-A4D0-8326D297E227}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE