diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..a91c9cd --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,26 @@ +{ + "version": "0.2.0", + "configurations": [ + { + // Use IntelliSense to find out which attributes exist for C# debugging + // Use hover for the description of the existing attributes + // For further information visit https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md + "name": ".NET Core Launch (console)", + "type": "coreclr", + "request": "launch", + "preLaunchTask": "build", + // If you have changed target frameworks, make sure to update the program path. + "program": "${workspaceFolder}/Selector.Tests/bin/Debug/net5.0/Selector.Tests.dll", + "args": [], + "cwd": "${workspaceFolder}/Selector.Tests", + // For more information about the 'console' field, see https://aka.ms/VSCode-CS-LaunchJson-Console + "console": "internalConsole", + "stopAtEntry": false + }, + { + "name": ".NET Core Attach", + "type": "coreclr", + "request": "attach" + } + ] +} \ No newline at end of file diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 0000000..b55186a --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,42 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "label": "build", + "command": "dotnet", + "type": "process", + "args": [ + "build", + "${workspaceFolder}/Selector.Tests/Selector.Tests.csproj", + "/property:GenerateFullPaths=true", + "/consoleloggerparameters:NoSummary" + ], + "problemMatcher": "$msCompile" + }, + { + "label": "publish", + "command": "dotnet", + "type": "process", + "args": [ + "publish", + "${workspaceFolder}/Selector.Tests/Selector.Tests.csproj", + "/property:GenerateFullPaths=true", + "/consoleloggerparameters:NoSummary" + ], + "problemMatcher": "$msCompile" + }, + { + "label": "watch", + "command": "dotnet", + "type": "process", + "args": [ + "watch", + "run", + "${workspaceFolder}/Selector.Tests/Selector.Tests.csproj", + "/property:GenerateFullPaths=true", + "/consoleloggerparameters:NoSummary" + ], + "problemMatcher": "$msCompile" + } + ] +} \ No newline at end of file diff --git a/Selector.Tests/Equality.cs b/Selector.Tests/Equality.cs index d35f88b..90c1305 100644 --- a/Selector.Tests/Equality.cs +++ b/Selector.Tests/Equality.cs @@ -115,6 +115,31 @@ namespace Selector.Tests var eq = new UriEquality(); eq.Album(album1, album2).Should().Be(shouldEqual); } + + public static IEnumerable ArtistData => + new List + { + // SAME + new object[] { + Helper.SimpleArtist("1"), + Helper.SimpleArtist("1"), + true + }, + // DIFFERENT + new object[] { + Helper.SimpleArtist("1"), + Helper.SimpleArtist("2"), + false + } + }; + + [Theory] + [MemberData(nameof(ArtistData))] + public void ArtistEquality(SimpleArtist artist1, SimpleArtist artist2, bool shouldEqual) + { + var eq = new UriEquality(); + eq.Artist(artist1, artist2).Should().Be(shouldEqual); + } } public class StringEqualityTests @@ -223,5 +248,30 @@ namespace Selector.Tests var eq = new StringEquality(); eq.Album(album1, album2).Should().Be(shouldEqual); } + + public static IEnumerable ArtistData => + new List + { + // SAME + new object[] { + Helper.SimpleArtist("1"), + Helper.SimpleArtist("1"), + true + }, + // DIFFERENT + new object[] { + Helper.SimpleArtist("1"), + Helper.SimpleArtist("2"), + false + } + }; + + [Theory] + [MemberData(nameof(ArtistData))] + public void ArtistEquality(SimpleArtist artist1, SimpleArtist artist2, bool shouldEqual) + { + var eq = new StringEquality(); + eq.Artist(artist1, artist2).Should().Be(shouldEqual); + } } } diff --git a/Selector.Tests/Helper.cs b/Selector.Tests/Helper.cs index 85d0031..90fa018 100644 --- a/Selector.Tests/Helper.cs +++ b/Selector.Tests/Helper.cs @@ -49,5 +49,23 @@ namespace Selector.Tests Uri = name }; } + + public static CurrentlyPlaying CurrentlyPlaying(FullTrack track, bool isPlaying = true, string context = null) + { + return new CurrentlyPlaying() + { + Context = Context(context ?? track.Uri), + IsPlaying = isPlaying, + Item = track + }; + } + + public static Context Context(string uri) + { + return new Context() + { + Uri = uri + }; + } } } diff --git a/Selector.Tests/PlayerWatcher.cs b/Selector.Tests/PlayerWatcher.cs index e314c3d..93fbb89 100644 --- a/Selector.Tests/PlayerWatcher.cs +++ b/Selector.Tests/PlayerWatcher.cs @@ -11,60 +11,131 @@ namespace Selector.Tests { public class PlayerWatcherTests { - //public static IEnumerable TrackData => - //new List - //{ - // new object[] { new List(){ - // new CurrentlyPlaying(){ - // Item = new FullTrack() { + public static IEnumerable NowPlayingData => + new List + { + new object[] { new List(){ + Helper.CurrentlyPlaying(Helper.FullTrack("track1", "album1", "artist1")), + Helper.CurrentlyPlaying(Helper.FullTrack("track2", "album2", "artist2")), + Helper.CurrentlyPlaying(Helper.FullTrack("track3", "album3", "artist3")), + } + } + }; - // } - // } - // }, 1 } - //}; + [Theory] + [MemberData(nameof(NowPlayingData))] + public async void NowPlaying(List playing) + { + var playingQueue = new Queue(playing); - //[Theory] - //[MemberData(nameof(TrackData))] - //public void Test1(List playing) - //{ - // var spotMock = new Mock(); - // // spotMock.Setup(spot => spot.GetCurrentlyPlaying(It.IsAny())).Returns(); - // // var watch = new Watcher(); - //} + var spotMock = new Mock(); + var scheduleMock = new Mock(); + var eq = new UriEquality(); - // [Fact] - // public void Test2() - // { - // var artist = new SimpleArtist(){ - // Name = "test" - // }; - // var track = new FullTrack() { - // Name = "Test", - // Album = new SimpleAlbum() { - // Name = "test", - // Artists = new List(){ - // artist - // } - // }, - // Artists = new List(){ - // artist - // } - // }; + spotMock.Setup(s => s.GetCurrentlyPlaying(It.IsAny()).Result).Returns(playingQueue.Dequeue); - // var track2 = new FullTrack() { - // Name = "Test", - // Album = new SimpleAlbum() { - // Name = "test", - // Artists = new List(){ - // artist - // } - // }, - // Artists = new List(){ - // artist - // } - // }; + var watcher = new PlayerWatcher(spotMock.Object, scheduleMock.Object, eq); - // track.Should().Be(track2); - // } + for(var i = 0; i < playing.Count; i++) + { + await watcher.WatchOne(); + var current = watcher.NowPlaying(); + current.Should().Be(playing[i]); + } + } + + public static IEnumerable EventsData => + new List + { + // NO CHANGING + new object[] { new List(){ + Helper.CurrentlyPlaying(Helper.FullTrack("track1", "album1", "artist1"), isPlaying: true, context: "context1"), + Helper.CurrentlyPlaying(Helper.FullTrack("track1", "album1", "artist1"), isPlaying: true, context: "context1"), + Helper.CurrentlyPlaying(Helper.FullTrack("track1", "album1", "artist1"), isPlaying: true, context: "context1"), + }, + // to raise + new List(){ }, + // to not raise + new List(){ "TrackChange", "AlbumChange", "ArtistChange", "ContextChange", "PlayingChange" } + }, + // TRACK CHANGE + new object[] { new List(){ + Helper.CurrentlyPlaying(Helper.FullTrack("track1", "album1", "artist1")), + Helper.CurrentlyPlaying(Helper.FullTrack("track2", "album1", "artist1")) + }, + // to raise + new List(){ "TrackChange" }, + // to not raise + new List(){ "AlbumChange", "ArtistChange" } + }, + // ALBUM CHANGE + new object[] { new List(){ + Helper.CurrentlyPlaying(Helper.FullTrack("track1", "album1", "artist1")), + Helper.CurrentlyPlaying(Helper.FullTrack("track1", "album2", "artist1")) + }, + // to raise + new List(){ "TrackChange", "AlbumChange" }, + // to not raise + new List(){ "ArtistChange" } + }, + // ARTIST CHANGE + new object[] { new List(){ + Helper.CurrentlyPlaying(Helper.FullTrack("track1", "album1", "artist1")), + Helper.CurrentlyPlaying(Helper.FullTrack("track1", "album1", "artist2")) + }, + // to raise + new List(){ "TrackChange", "AlbumChange", "ArtistChange" }, + // to not raise + new List(){ } + }, + // CONTEXT CHANGE + new object[] { new List(){ + Helper.CurrentlyPlaying(Helper.FullTrack("track1", "album1", "artist1"), context: "context1"), + Helper.CurrentlyPlaying(Helper.FullTrack("track1", "album1", "artist1"), context: "context2") + }, + // to raise + new List(){ "ContextChange" }, + // to not raise + new List(){ "TrackChange", "AlbumChange", "ArtistChange" } + }, + // PLAYING CHANGE + new object[] { new List(){ + Helper.CurrentlyPlaying(Helper.FullTrack("track1", "album1", "artist1"), isPlaying: true, context: "context1"), + Helper.CurrentlyPlaying(Helper.FullTrack("track1", "album1", "artist1"), isPlaying: false, context: "context1") + }, + // to raise + new List(){ "PlayingChange" }, + // to not raise + new List(){ "TrackChange", "AlbumChange", "ArtistChange", "ContextChange" } + } + }; + + [Theory] + [MemberData(nameof(EventsData))] + public async void Events(List playing, List toRaise, List toNotRaise) + { + var playingQueue = new Queue(playing); + + var spotMock = new Mock(); + var scheduleMock = new Mock(); + var eq = new UriEquality(); + + spotMock.Setup(s => s.GetCurrentlyPlaying(It.IsAny()).Result).Returns(playingQueue.Dequeue); + + var watcher = new PlayerWatcher(spotMock.Object, scheduleMock.Object, eq); + using var monitoredWatcher = watcher.Monitor(); + + for(var i = 0; i < playing.Count; i++) + { + await watcher.WatchOne(); + } + + foreach(var raise in toRaise){ + monitoredWatcher.Should().Raise(raise).WithSender(watcher); + } + foreach(var notRraise in toNotRaise){ + monitoredWatcher.Should().NotRaise(notRraise); + } + } } } diff --git a/Selector/Equality/IEqualityChecker.cs b/Selector/Equality/IEqualityChecker.cs index 78c85ce..6829c99 100644 --- a/Selector/Equality/IEqualityChecker.cs +++ b/Selector/Equality/IEqualityChecker.cs @@ -5,10 +5,12 @@ namespace Selector { public interface IEqualityChecker { public bool Track(FullTrack track1, FullTrack track2, bool includingAlbum); + public bool Episode(FullEpisode ep1, FullEpisode ep2); public bool Album(FullAlbum album1, FullAlbum album2); public bool Artist(FullArtist artist1, FullArtist artist2); public bool Track(SimpleTrack track1, SimpleTrack track2); + public bool Episode(SimpleEpisode ep1, SimpleEpisode ep2); public bool Album(SimpleAlbum album1, SimpleAlbum album2); public bool Artist(SimpleArtist artist1, SimpleArtist artist2); diff --git a/Selector/Equality/StringEquality.cs b/Selector/Equality/StringEquality.cs index 456b8c3..9a7a401 100644 --- a/Selector/Equality/StringEquality.cs +++ b/Selector/Equality/StringEquality.cs @@ -15,11 +15,18 @@ namespace Selector { && track1.Name == track2.Name && Enumerable.SequenceEqual(track1.Artists.Select(a => a.Name), track2.Artists.Select(a => a.Name)); } + + new public bool Episode(FullEpisode ep1, FullEpisode ep2) + { + return ep1.Uri == ep2.Uri; + } + new public bool Album(FullAlbum album1, FullAlbum album2) { - return album1.Name == album1.Name + return album1.Name == album2.Name && Enumerable.SequenceEqual(album1.Artists.Select(a => a.Name), album2.Artists.Select(a => a.Name)); } + new public bool Artist(FullArtist artist1, FullArtist artist2) { return artist1.Name == artist2.Name; @@ -30,11 +37,18 @@ namespace Selector { return track1.Name == track2.Name && Enumerable.SequenceEqual(track1.Artists.Select(a => a.Name), track2.Artists.Select(a => a.Name)); } + + new public bool Episode(SimpleEpisode ep1, SimpleEpisode ep2) + { + return ep1.Name == ep2.Name; + } + new public bool Album(SimpleAlbum album1, SimpleAlbum album2) { - return album1.Name == album1.Name + return album1.Name == album2.Name && Enumerable.SequenceEqual(album1.Artists.Select(a => a.Name), album2.Artists.Select(a => a.Name)); } + new public bool Artist(SimpleArtist artist1, SimpleArtist artist2) { return artist1.Name == artist2.Name; diff --git a/Selector/Equality/UriEquality.cs b/Selector/Equality/UriEquality.cs index dd5db27..051ba44 100644 --- a/Selector/Equality/UriEquality.cs +++ b/Selector/Equality/UriEquality.cs @@ -13,6 +13,12 @@ namespace Selector { && track1.Uri == track2.Uri && Enumerable.SequenceEqual(track1.Artists.Select(a => a.Uri), track2.Artists.Select(a => a.Uri)); } + + public bool Episode(FullEpisode ep1, FullEpisode ep2) + { + return ep1.Uri == ep2.Uri; + } + public bool Album(FullAlbum album1, FullAlbum album2) { return album1.Uri == album2.Uri @@ -28,6 +34,12 @@ namespace Selector { return track1.Uri == track2.Uri && Enumerable.SequenceEqual(track1.Artists.Select(a => a.Uri), track2.Artists.Select(a => a.Uri)); } + + public bool Episode(SimpleEpisode ep1, SimpleEpisode ep2) + { + return ep1.Uri == ep2.Uri; + } + public bool Album(SimpleAlbum album1, SimpleAlbum album2) { return album1.Uri == album2.Uri diff --git a/Selector/Watcher/Interfaces/IPlayerWatcher.cs b/Selector/Watcher/Interfaces/IPlayerWatcher.cs index 65c776b..d0f9082 100644 --- a/Selector/Watcher/Interfaces/IPlayerWatcher.cs +++ b/Selector/Watcher/Interfaces/IPlayerWatcher.cs @@ -11,8 +11,9 @@ namespace Selector public event EventHandler ArtistChange; public event EventHandler ContextChange; - public event EventHandler VolumeChange; - public event EventHandler DeviceChange; + // public event EventHandler VolumeChange; + // public event EventHandler DeviceChange; + public event EventHandler PlayingChange; public CurrentlyPlaying NowPlaying(); // recently playing diff --git a/Selector/Watcher/PlayerWatcher.cs b/Selector/Watcher/PlayerWatcher.cs index 3c954df..59e9ddc 100644 --- a/Selector/Watcher/PlayerWatcher.cs +++ b/Selector/Watcher/PlayerWatcher.cs @@ -17,21 +17,82 @@ namespace Selector public event EventHandler ArtistChange; public event EventHandler ContextChange; - public event EventHandler VolumeChange; - public event EventHandler DeviceChange; + // public event EventHandler VolumeChange; + // public event EventHandler DeviceChange; + public event EventHandler PlayingChange; private CurrentlyPlaying live { get; set; } - //public List LastPlays { get; private set; } + private List> lastPlays { get; set; } public PlayerWatcher(IPlayerClient spotifyClient, IScheduler sleepScheduler, IEqualityChecker equalityChecker) { this.spotifyClient = spotifyClient; this.sleepScheduler = sleepScheduler; this.equalityChecker = equalityChecker; + + lastPlays = new List>(); } public async Task WatchOne() { + var polledCurrent = await spotifyClient.GetCurrentlyPlaying(new PlayerCurrentlyPlayingRequest()); + + StoreCurrentPlaying(polledCurrent); + + CurrentlyPlaying existing; + if(live is null) { + live = polledCurrent; + existing = polledCurrent; + } + else { + existing = live; + live = polledCurrent; + } + try{ + var existingItem = (FullTrack) existing.Item; + var currentItem = (FullTrack) live.Item; + + if(!equalityChecker.Track(existingItem, currentItem, true)) { + OnTrackChange(new ListeningChangeEventArgs(){ + Previous = existing, + Current = live + }); + } + + if(!equalityChecker.Album(existingItem.Album, currentItem.Album)) { + OnAlbumChange(new ListeningChangeEventArgs(){ + Previous = existing, + Current = live + }); + } + + if(!equalityChecker.Artist(existingItem.Artists[0], currentItem.Artists[0])) { + OnArtistChange(new ListeningChangeEventArgs(){ + Previous = existing, + Current = live + }); + } + + if(!equalityChecker.Context(existing.Context, live.Context)) { + OnContextChange(new ListeningChangeEventArgs(){ + Previous = existing, + Current = live + }); + } + + if(existing.IsPlaying != live.IsPlaying) { + OnPlayingChange(new ListeningChangeEventArgs(){ + Previous = existing, + Current = live + }); + } + } + catch(InvalidCastException) + { + var existingItem = (FullEpisode) existing.Item; + + throw new NotImplementedException("Podcasts not implemented"); + } } public Task Watch(CancellationToken cancelToken) @@ -41,7 +102,58 @@ namespace Selector public CurrentlyPlaying NowPlaying() { - throw new NotImplementedException(); + return live; + } + + /// + /// Store currently playing in last plays. Determine whether new list or appending required + /// + /// New currently playing to store + private void StoreCurrentPlaying(CurrentlyPlaying current) + { + if(lastPlays.Count > 0) + { + bool matchesMostRecent; + + try { + var castItem = (FullTrack) current.Item; + var castStoredItem = (FullTrack) lastPlays[0][0].Item; + + matchesMostRecent = equalityChecker.Track(castItem, castStoredItem, true); + } + catch(InvalidCastException) + { + var castItem = (FullEpisode) current.Item; + var castStoredItem = (FullEpisode) lastPlays[0][0].Item; + + matchesMostRecent = equalityChecker.Episode(castItem, castStoredItem); + } + + if (matchesMostRecent) + { + lastPlays[0].Add(current); + } + else + { + StoreNewTrack(current); + } + } + else { + StoreNewTrack(current); + } + } + + /// + /// Store currently playing at front of last plays list. Pushes new list to hold same track + /// + /// New currently playing to store + private void StoreNewTrack(CurrentlyPlaying current) + { + if (live != null) { + var newPlayingList = new List(); + newPlayingList.Add(live); + lastPlays.Insert(0, newPlayingList); + } } protected virtual void OnTrackChange(ListeningChangeEventArgs args) @@ -65,14 +177,19 @@ namespace Selector } - protected virtual void OnVolumeChange(ListeningChangeEventArgs args) - { - ArtistChange?.Invoke(this, args); - } + // protected virtual void OnVolumeChange(ListeningChangeEventArgs args) + // { + // ArtistChange?.Invoke(this, args); + // } - protected virtual void OnDeviceChange(ListeningChangeEventArgs args) + // protected virtual void OnDeviceChange(ListeningChangeEventArgs args) + // { + // ContextChange?.Invoke(this, args); + // } + + protected virtual void OnPlayingChange(ListeningChangeEventArgs args) { - ContextChange?.Invoke(this, args); + PlayingChange?.Invoke(this, args); } } }