diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f4f7a0c..60fd8d5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,4 +1,4 @@ -name: dotnet package +name: ci on: [push] diff --git a/README.md b/README.md index f30c5a0..025f0eb 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,5 @@ # Selector +![ci](https://github.com/sarsoo/Selector/actions/workflows/ci.yml/badge.svg) + Investigating a Spotify listening agent \ No newline at end of file diff --git a/Selector.Tests/Equality.cs b/Selector.Tests/Equality.cs index 90c1305..5f691bc 100644 --- a/Selector.Tests/Equality.cs +++ b/Selector.Tests/Equality.cs @@ -140,6 +140,31 @@ namespace Selector.Tests var eq = new UriEquality(); eq.Artist(artist1, artist2).Should().Be(shouldEqual); } + + public static IEnumerable EpisodeData => + new List + { + // SAME + new object[] { + Helper.FullEpisode("1"), + Helper.FullEpisode("1"), + true + }, + // DIFFERENT + new object[] { + Helper.FullEpisode("1"), + Helper.FullEpisode("2"), + false + } + }; + + [Theory] + [MemberData(nameof(EpisodeData))] + public void EpisodeEquality(FullEpisode episode1, FullEpisode episode2, bool shouldEqual) + { + var eq = new UriEquality(); + eq.Episode(episode1, episode2).Should().Be(shouldEqual); + } } public class StringEqualityTests @@ -273,5 +298,42 @@ namespace Selector.Tests var eq = new StringEquality(); eq.Artist(artist1, artist2).Should().Be(shouldEqual); } + + public static IEnumerable EpisodeData => + new List + { + // SAME + new object[] { + Helper.FullEpisode("1", "1", "1"), + Helper.FullEpisode("1", "1", "1"), + true + }, + // DIFFERENT PUBLISHER + new object[] { + Helper.FullEpisode("1", "1", "1"), + Helper.FullEpisode("1", "1", "2"), + false + }, + // DIFFERENT SHOW + new object[] { + Helper.FullEpisode("1", "1", "1"), + Helper.FullEpisode("1", "2", "1"), + false + }, + // DIFFERENT EPISODE + new object[] { + Helper.FullEpisode("1", "1", "1"), + Helper.FullEpisode("2", "1", "1"), + false + }, + }; + + [Theory] + [MemberData(nameof(EpisodeData))] + public void EpisodeEquality(FullEpisode episode1, FullEpisode episode2, bool shouldEqual) + { + var eq = new StringEquality(); + eq.Episode(episode1, episode2).Should().Be(shouldEqual); + } } } diff --git a/Selector.Tests/Helper.cs b/Selector.Tests/Helper.cs index 90fa018..bb658a5 100644 --- a/Selector.Tests/Helper.cs +++ b/Selector.Tests/Helper.cs @@ -26,6 +26,16 @@ namespace Selector.Tests return FullTrack(name, album, new List() { artist }); } + public static FullEpisode FullEpisode(string name, string show = null, string publisher = null) + { + return new FullEpisode() + { + Name = name, + Uri = name, + Show = SimpleShow(show ?? name, publisher: publisher) + }; + } + public static SimpleAlbum SimpleAlbum(string name, List artists) { return new SimpleAlbum() @@ -50,6 +60,16 @@ namespace Selector.Tests }; } + public static SimpleShow SimpleShow(string name, string publisher = null) + { + return new SimpleShow() + { + Name = name, + Publisher = publisher ?? name, + Uri = name + }; + } + public static CurrentlyPlaying CurrentlyPlaying(FullTrack track, bool isPlaying = true, string context = null) { return new CurrentlyPlaying() @@ -60,6 +80,16 @@ namespace Selector.Tests }; } + public static CurrentlyPlaying CurrentlyPlaying(FullEpisode episode, bool isPlaying = true, string context = null) + { + return new CurrentlyPlaying() + { + Context = Context(context ?? episode.Uri), + IsPlaying = isPlaying, + Item = episode + }; + } + public static Context Context(string uri) { return new Context() diff --git a/Selector.Tests/PlayerWatcher.cs b/Selector.Tests/PlayerWatcher.cs index 93fbb89..8edc8a1 100644 --- a/Selector.Tests/PlayerWatcher.cs +++ b/Selector.Tests/PlayerWatcher.cs @@ -5,7 +5,7 @@ using Moq; using FluentAssertions; using SpotifyAPI.Web; -using Selector; +using System.Threading; namespace Selector.Tests { @@ -29,12 +29,11 @@ namespace Selector.Tests 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); + var watcher = new PlayerWatcher(spotMock.Object, eq); for(var i = 0; i < playing.Count; i++) { @@ -56,7 +55,7 @@ namespace Selector.Tests // to raise new List(){ }, // to not raise - new List(){ "TrackChange", "AlbumChange", "ArtistChange", "ContextChange", "PlayingChange" } + new List(){ "ItemChange", "AlbumChange", "ArtistChange", "ContextChange", "PlayingChange" } }, // TRACK CHANGE new object[] { new List(){ @@ -64,7 +63,7 @@ namespace Selector.Tests Helper.CurrentlyPlaying(Helper.FullTrack("track2", "album1", "artist1")) }, // to raise - new List(){ "TrackChange" }, + new List(){ "ItemChange" }, // to not raise new List(){ "AlbumChange", "ArtistChange" } }, @@ -74,7 +73,7 @@ namespace Selector.Tests Helper.CurrentlyPlaying(Helper.FullTrack("track1", "album2", "artist1")) }, // to raise - new List(){ "TrackChange", "AlbumChange" }, + new List(){ "ItemChange", "AlbumChange" }, // to not raise new List(){ "ArtistChange" } }, @@ -84,7 +83,7 @@ namespace Selector.Tests Helper.CurrentlyPlaying(Helper.FullTrack("track1", "album1", "artist2")) }, // to raise - new List(){ "TrackChange", "AlbumChange", "ArtistChange" }, + new List(){ "ItemChange", "AlbumChange", "ArtistChange" }, // to not raise new List(){ } }, @@ -96,7 +95,7 @@ namespace Selector.Tests // to raise new List(){ "ContextChange" }, // to not raise - new List(){ "TrackChange", "AlbumChange", "ArtistChange" } + new List(){ "ItemChange", "AlbumChange", "ArtistChange" } }, // PLAYING CHANGE new object[] { new List(){ @@ -106,7 +105,37 @@ namespace Selector.Tests // to raise new List(){ "PlayingChange" }, // to not raise - new List(){ "TrackChange", "AlbumChange", "ArtistChange", "ContextChange" } + new List(){ "ItemChange", "AlbumChange", "ArtistChange", "ContextChange" } + }, + // PLAYING CHANGE + new object[] { new List(){ + Helper.CurrentlyPlaying(Helper.FullTrack("track1", "album1", "artist1"), isPlaying: false, context: "context1"), + Helper.CurrentlyPlaying(Helper.FullTrack("track1", "album1", "artist1"), isPlaying: true, context: "context1") + }, + // to raise + new List(){ "PlayingChange" }, + // to not raise + new List(){ "ItemChange", "AlbumChange", "ArtistChange", "ContextChange" } + }, + // CONTENT CHANGE + new object[] { new List(){ + Helper.CurrentlyPlaying(Helper.FullTrack("track1", "album1", "artist1"), isPlaying: true, context: "context1"), + Helper.CurrentlyPlaying(Helper.FullEpisode("ep1", "show1", "pub1"), isPlaying: true, context: "context2") + }, + // to raise + new List(){ "ContentChange", "ContextChange", "ItemChange" }, + // to not raise + new List(){ "AlbumChange", "ArtistChange", "PlayingChange" } + }, + // CONTENT CHANGE + new object[] { new List(){ + Helper.CurrentlyPlaying(Helper.FullEpisode("ep1", "show1", "pub1"), isPlaying: true, context: "context2"), + Helper.CurrentlyPlaying(Helper.FullTrack("track1", "album1", "artist1"), isPlaying: true, context: "context1") + }, + // to raise + new List(){ "ContentChange", "ContextChange", "ItemChange" }, + // to not raise + new List(){ "AlbumChange", "ArtistChange", "PlayingChange" } } }; @@ -117,12 +146,11 @@ namespace Selector.Tests 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); + var watcher = new PlayerWatcher(spotMock.Object, eq); using var monitoredWatcher = watcher.Monitor(); for(var i = 0; i < playing.Count; i++) @@ -137,5 +165,16 @@ namespace Selector.Tests monitoredWatcher.Should().NotRaise(notRraise); } } + + // [Fact] + // public async void Auth() + // { + // var spot = new SpotifyClient(""); + // var eq = new UriEquality(); + // var watch = new PlayerWatcher(spot.Player, eq); + + // var token = new CancellationTokenSource(); + // await watch.Watch(token.Token); + // } } } diff --git a/Selector/Equality/IEqualityChecker.cs b/Selector/Equality/IEqualityChecker.cs index 6829c99..f1abcbb 100644 --- a/Selector/Equality/IEqualityChecker.cs +++ b/Selector/Equality/IEqualityChecker.cs @@ -7,11 +7,13 @@ namespace Selector { 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 Show(FullShow show1, FullShow show2); 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 Show(SimpleShow show1, SimpleShow show2); public bool Artist(SimpleArtist artist1, SimpleArtist artist2); public bool Context(Context context1, Context context2); diff --git a/Selector/Equality/StringEquality.cs b/Selector/Equality/StringEquality.cs index 9a7a401..d0b8227 100644 --- a/Selector/Equality/StringEquality.cs +++ b/Selector/Equality/StringEquality.cs @@ -18,7 +18,8 @@ namespace Selector { new public bool Episode(FullEpisode ep1, FullEpisode ep2) { - return ep1.Uri == ep2.Uri; + return ep1.Uri == ep2.Uri + && Show(ep1.Show, ep2.Show); } new public bool Album(FullAlbum album1, FullAlbum album2) @@ -27,6 +28,12 @@ namespace Selector { && Enumerable.SequenceEqual(album1.Artists.Select(a => a.Name), album2.Artists.Select(a => a.Name)); } + new public bool Show(FullShow show1, FullShow show2) + { + return show1.Name == show2.Name + && show1.Publisher == show2.Publisher; + } + new public bool Artist(FullArtist artist1, FullArtist artist2) { return artist1.Name == artist2.Name; @@ -48,6 +55,12 @@ namespace Selector { return album1.Name == album2.Name && Enumerable.SequenceEqual(album1.Artists.Select(a => a.Name), album2.Artists.Select(a => a.Name)); } + + new public bool Show(SimpleShow show1, SimpleShow show2) + { + return show1.Name == show2.Name + && show1.Publisher == show2.Publisher; + } new public bool Artist(SimpleArtist artist1, SimpleArtist artist2) { diff --git a/Selector/Equality/UriEquality.cs b/Selector/Equality/UriEquality.cs index 051ba44..d5472f2 100644 --- a/Selector/Equality/UriEquality.cs +++ b/Selector/Equality/UriEquality.cs @@ -24,6 +24,11 @@ namespace Selector { return album1.Uri == album2.Uri && Enumerable.SequenceEqual(album1.Artists.Select(a => a.Uri), album2.Artists.Select(a => a.Uri)); } + + public bool Show(FullShow show1, FullShow show2) + { + return show1.Uri == show2.Uri; + } public bool Artist(FullArtist artist1, FullArtist artist2) { return artist1.Uri == artist2.Uri; @@ -45,6 +50,12 @@ namespace Selector { return album1.Uri == album2.Uri && Enumerable.SequenceEqual(album1.Artists.Select(a => a.Uri), album2.Artists.Select(a => a.Uri)); } + + public bool Show(SimpleShow show1, SimpleShow show2) + { + return show1.Uri == show2.Uri; + } + public bool Artist(SimpleArtist artist1, SimpleArtist artist2) { return artist1.Uri == artist2.Uri; @@ -54,6 +65,7 @@ namespace Selector { { return context1.Uri == context2.Uri; } + public bool Device(Device device1, Device device2) { return device1.Id == device2.Id; diff --git a/Selector/Watcher/Interfaces/IPlayerWatcher.cs b/Selector/Watcher/Interfaces/IPlayerWatcher.cs index d0f9082..46c38fd 100644 --- a/Selector/Watcher/Interfaces/IPlayerWatcher.cs +++ b/Selector/Watcher/Interfaces/IPlayerWatcher.cs @@ -6,10 +6,11 @@ namespace Selector { public interface IPlayerWatcher: IWatcher { - public event EventHandler TrackChange; + public event EventHandler ItemChange; public event EventHandler AlbumChange; public event EventHandler ArtistChange; public event EventHandler ContextChange; + public event EventHandler ContentChange; // public event EventHandler VolumeChange; // public event EventHandler DeviceChange; diff --git a/Selector/Watcher/PlayerWatcher.cs b/Selector/Watcher/PlayerWatcher.cs index 59e9ddc..66f6e8b 100644 --- a/Selector/Watcher/PlayerWatcher.cs +++ b/Selector/Watcher/PlayerWatcher.cs @@ -9,13 +9,13 @@ namespace Selector public class PlayerWatcher: IPlayerWatcher { private readonly IPlayerClient spotifyClient; - private IScheduler sleepScheduler; private IEqualityChecker equalityChecker; - public event EventHandler TrackChange; + public event EventHandler ItemChange; public event EventHandler AlbumChange; public event EventHandler ArtistChange; public event EventHandler ContextChange; + public event EventHandler ContentChange; // public event EventHandler VolumeChange; // public event EventHandler DeviceChange; @@ -24,80 +24,157 @@ namespace Selector private CurrentlyPlaying live { get; set; } private List> lastPlays { get; set; } - public PlayerWatcher(IPlayerClient spotifyClient, IScheduler sleepScheduler, IEqualityChecker equalityChecker) { + private int _pollPeriod; + public int PollPeriod { + get => _pollPeriod; + set => _pollPeriod = Math.Max(0, value); + } + + public PlayerWatcher(IPlayerClient spotifyClient, + IEqualityChecker equalityChecker, + int pollPeriod = 3000) { + this.spotifyClient = spotifyClient; - this.sleepScheduler = sleepScheduler; this.equalityChecker = equalityChecker; + this.PollPeriod = pollPeriod; 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; + var polledCurrent = await spotifyClient.GetCurrentlyPlaying(new PlayerCurrentlyPlayingRequest()); - if(!equalityChecker.Track(existingItem, currentItem, true)) { - OnTrackChange(new ListeningChangeEventArgs(){ - Previous = existing, - Current = live - }); + if (polledCurrent != null) StoreCurrentPlaying(polledCurrent); + + CurrentlyPlaying previous; + if(live is null) { + live = polledCurrent; + previous = polledCurrent; + } + else { + previous = live; + live = polledCurrent; } - if(!equalityChecker.Album(existingItem.Album, currentItem.Album)) { - OnAlbumChange(new ListeningChangeEventArgs(){ - Previous = existing, - Current = live - }); + // NOT PLAYING + if(previous is null && live is null) + { + // Console.WriteLine("not playing"); } + else + { + // STARTED PLAYBACK + if(previous is null && (live.Item is FullTrack || live.Item is FullEpisode)) + { + // Console.WriteLine("started playing"); - if(!equalityChecker.Artist(existingItem.Artists[0], currentItem.Artists[0])) { - OnArtistChange(new ListeningChangeEventArgs(){ - Previous = existing, - Current = live - }); - } + } + // STOPPED PLAYBACK + else if((previous.Item is FullTrack || previous.Item is FullEpisode) && live is null) + { + // Console.WriteLine("stopped playing"); - if(!equalityChecker.Context(existing.Context, live.Context)) { - OnContextChange(new ListeningChangeEventArgs(){ - Previous = existing, - Current = live - }); - } + } + else { - if(existing.IsPlaying != live.IsPlaying) { - OnPlayingChange(new ListeningChangeEventArgs(){ - Previous = existing, - Current = live - }); - } + // MUSIC + if(previous.Item is FullTrack && live.Item is FullTrack) + { + var previousItem = (FullTrack) previous.Item; + var currentItem = (FullTrack) live.Item; + + if(!equalityChecker.Track(previousItem, currentItem, true)) { + OnItemChange(new ListeningChangeEventArgs(){ + Previous = previous, + Current = live + }); + } + + if(!equalityChecker.Album(previousItem.Album, currentItem.Album)) { + OnAlbumChange(new ListeningChangeEventArgs(){ + Previous = previous, + Current = live + }); + } + + if(!equalityChecker.Artist(previousItem.Artists[0], currentItem.Artists[0])) { + OnArtistChange(new ListeningChangeEventArgs(){ + Previous = previous, + Current = live + }); + } + } + // CHANGED CONTENT + else if(previous.Item is FullTrack && live.Item is FullEpisode + || previous.Item is FullEpisode && live.Item is FullTrack) + { + OnContentChange(new ListeningChangeEventArgs(){ + Previous = previous, + Current = live + }); + OnItemChange(new ListeningChangeEventArgs(){ + Previous = previous, + Current = live + }); + } + // PODCASTS + else if(previous.Item is FullEpisode && live.Item is FullEpisode) + { + var previousItem = (FullEpisode) previous.Item; + var currentItem = (FullEpisode) live.Item; + + if(!equalityChecker.Episode(previousItem, currentItem)) { + OnItemChange(new ListeningChangeEventArgs(){ + Previous = previous, + Current = live + }); + } + } + else { + throw new NotSupportedException("Unknown item combination"); + } + + // CONTEXT + if(!equalityChecker.Context(previous.Context, live.Context)) { + OnContextChange(new ListeningChangeEventArgs(){ + Previous = previous, + Current = live + }); + } + + // IS PLAYING + if(previous.IsPlaying != live.IsPlaying) { + OnPlayingChange(new ListeningChangeEventArgs(){ + Previous = previous, + Current = live + }); + } + } + } } - catch(InvalidCastException) + catch(APIUnauthorizedException e) { - var existingItem = (FullEpisode) existing.Item; - - throw new NotImplementedException("Podcasts not implemented"); + throw e; + } + catch(APITooManyRequestsException e) + { + throw e; + } + catch(APIException e) + { + throw e; } } - public Task Watch(CancellationToken cancelToken) + public async Task Watch(CancellationToken cancelToken) { - return Task.CompletedTask; + while (!cancelToken.IsCancellationRequested) + { + await WatchOne(); + await Task.Delay(PollPeriod); + } } public CurrentlyPlaying NowPlaying() @@ -156,9 +233,9 @@ namespace Selector } } - protected virtual void OnTrackChange(ListeningChangeEventArgs args) + protected virtual void OnItemChange(ListeningChangeEventArgs args) { - TrackChange?.Invoke(this, args); + ItemChange?.Invoke(this, args); } protected virtual void OnAlbumChange(ListeningChangeEventArgs args) @@ -176,6 +253,10 @@ namespace Selector ContextChange?.Invoke(this, args); } + protected virtual void OnContentChange(ListeningChangeEventArgs args) + { + ContentChange?.Invoke(this, args); + } // protected virtual void OnVolumeChange(ListeningChangeEventArgs args) // {