diff --git a/Dockerfile.CLI b/Dockerfile.CLI
index 285775e..c22ffb6 100644
--- a/Dockerfile.CLI
+++ b/Dockerfile.CLI
@@ -2,6 +2,7 @@ FROM mcr.microsoft.com/dotnet/sdk:9.0 AS base
 
 COPY *.sln .
 COPY Selector/*.csproj ./Selector/
+COPY Selector.AppleMusic/*.csproj ./Selector.AppleMusic/
 COPY Selector.Cache/*.csproj ./Selector.Cache/
 COPY Selector.Data/*.csproj ./Selector.Data/
 COPY Selector.Event/*.csproj ./Selector.Event/
diff --git a/Dockerfile.Web b/Dockerfile.Web
index da548ea..17cadb7 100644
--- a/Dockerfile.Web
+++ b/Dockerfile.Web
@@ -10,6 +10,7 @@ FROM mcr.microsoft.com/dotnet/sdk:9.0 AS base
 
 COPY *.sln .
 COPY Selector/*.csproj ./Selector/
+COPY Selector.AppleMusic/*.csproj ./Selector.AppleMusic/
 COPY Selector.Cache/*.csproj ./Selector.Cache/
 COPY Selector.Data/*.csproj ./Selector.Data/
 COPY Selector.Event/*.csproj ./Selector.Event/
diff --git a/Selector.AppleMusic/AppleMusicApi.cs b/Selector.AppleMusic/AppleMusicApi.cs
index 9a8bf73..a61f26c 100644
--- a/Selector.AppleMusic/AppleMusicApi.cs
+++ b/Selector.AppleMusic/AppleMusicApi.cs
@@ -1,8 +1,14 @@
-namespace Selector.AppleMusic;
+using System.Net;
+using System.Net.Http.Json;
+using Selector.AppleMusic.Exceptions;
+using Selector.AppleMusic.Model;
+
+namespace Selector.AppleMusic;
 
 public class AppleMusicApi(HttpClient client, string developerToken, string userToken)
 {
     private static readonly string _apiBaseUrl = "https://api.music.apple.com/v1";
+    private readonly AppleJsonContext _appleJsonContext = AppleJsonContext.Default;
 
     private async Task<HttpResponseMessage> MakeRequest(HttpMethod httpMethod, string requestUri)
     {
@@ -14,8 +20,32 @@ public class AppleMusicApi(HttpClient client, string developerToken, string user
         return response;
     }
 
-    public async Task GetRecentlyPlayedTracks()
+    private void CheckResponse(HttpResponseMessage response)
     {
-        var response = await MakeRequest(HttpMethod.Get, "/me/recent/played/tracks");
+        if (!response.IsSuccessStatusCode)
+        {
+            if (response.StatusCode == HttpStatusCode.Unauthorized)
+            {
+                throw new UnauthorisedException();
+            }
+            else if (response.StatusCode == HttpStatusCode.Forbidden)
+            {
+                throw new ForbiddenException();
+            }
+            else if (response.StatusCode == HttpStatusCode.TooManyRequests)
+            {
+                throw new RateLimitException();
+            }
+        }
+    }
+
+    public async Task<RecentlyPlayedTracksResponse> GetRecentlyPlayedTracks()
+    {
+        var response = await MakeRequest(HttpMethod.Get, "/me/recent/played/tracks?types=songs");
+
+        CheckResponse(response);
+
+        var parsed = await response.Content.ReadFromJsonAsync(_appleJsonContext.RecentlyPlayedTracksResponse);
+        return parsed;
     }
 }
\ No newline at end of file
diff --git a/Selector.AppleMusic/AppleTimeline.cs b/Selector.AppleMusic/AppleTimeline.cs
new file mode 100644
index 0000000..fec99f5
--- /dev/null
+++ b/Selector.AppleMusic/AppleTimeline.cs
@@ -0,0 +1,94 @@
+using Selector.AppleMusic.Model;
+using Selector.AppleMusic.Watcher;
+
+namespace Selector.AppleMusic;
+
+public class AppleTimeline : Timeline<AppleMusicCurrentlyPlayingContext>
+{
+    public List<AppleMusicCurrentlyPlayingContext> Add(IEnumerable<Track> tracks)
+        => Add(tracks
+            .Select(x => new AppleMusicCurrentlyPlayingContext()
+            {
+                Track = x,
+                FirstSeen = DateTime.UtcNow,
+            }).ToList());
+
+    public List<AppleMusicCurrentlyPlayingContext> Add(List<AppleMusicCurrentlyPlayingContext> items)
+    {
+        var newItems = new List<AppleMusicCurrentlyPlayingContext>();
+
+        if (items == null || !items.Any())
+        {
+            return newItems;
+        }
+
+        if (!Recent.Any())
+        {
+            Recent.AddRange(items.Select(x =>
+                TimelineItem<AppleMusicCurrentlyPlayingContext>.From(x, DateTime.UtcNow)));
+            return newItems;
+        }
+
+        if (Recent
+            .TakeLast(items.Count)
+            .Select(x => x.Item)
+            .SequenceEqual(items, new AppleMusicCurrentlyPlayingContextComparer()))
+        {
+            return newItems;
+        }
+
+        var stop = false;
+        var found = 0;
+        var startIdx = 0;
+        while (!stop)
+        {
+            for (var i = 0; i < items.Count; i++)
+            {
+                var storedIdx = (Recent.Count - 1) - i;
+                // start from the end, minus this loops index, minus the offset
+                var pulledIdx = (items.Count - 1) - i - startIdx;
+
+                if (pulledIdx < 0)
+                {
+                    // ran to the end of new items and none matched the end, add all the new ones
+                    stop = true;
+                    break;
+                }
+
+                if (storedIdx < 0)
+                {
+                    // all the new stuff matches, we're done and there's nothing new to add
+                    stop = true;
+                    break;
+                }
+
+                if (Recent[storedIdx].Item.Track.Id == items[pulledIdx].Track.Id)
+                {
+                    // good, keep going
+                    found++;
+                    if (found >= 3)
+                    {
+                        stop = true;
+                        break;
+                    }
+                }
+                else
+                {
+                    // bad, doesn't match, break and bump stored
+                    found = 0;
+                    break;
+                }
+            }
+
+            if (!stop) startIdx += 1;
+        }
+
+        foreach (var item in items.TakeLast(startIdx))
+        {
+            newItems.Add(item);
+            Recent.Add(TimelineItem<AppleMusicCurrentlyPlayingContext>.From(item, DateTime.UtcNow));
+        }
+
+        return newItems;
+    }
+}
\ No newline at end of file
diff --git a/Selector.AppleMusic/Events.cs b/Selector.AppleMusic/Events.cs
new file mode 100644
index 0000000..649fff1
--- /dev/null
+++ b/Selector.AppleMusic/Events.cs
@@ -0,0 +1,28 @@
+using Selector.AppleMusic.Watcher;
+
+namespace Selector.AppleMusic;
+
+public class AppleListeningChangeEventArgs : EventArgs
+{
+    public AppleMusicCurrentlyPlayingContext Previous { get; set; }
+    public AppleMusicCurrentlyPlayingContext Current { get; set; }
+
+    /// <summary>
+    /// String Id for watcher, used to hold user Db Id
+    /// </summary>
+    /// <value></value>
+    public string Id { get; set; }
+    // AppleTimeline Timeline { get; set; }
+
+    public static AppleListeningChangeEventArgs From(AppleMusicCurrentlyPlayingContext previous,
+        AppleMusicCurrentlyPlayingContext current, AppleTimeline timeline, string id = null, string username = null)
+    {
+        return new AppleListeningChangeEventArgs()
+        {
+            Previous = previous,
+            Current = current,
+            // Timeline = timeline,
+            Id = id
+        };
+    }
+}
\ No newline at end of file
diff --git a/Selector.AppleMusic/Exceptions/AppleMusicException.cs b/Selector.AppleMusic/Exceptions/AppleMusicException.cs
new file mode 100644
index 0000000..ce36f73
--- /dev/null
+++ b/Selector.AppleMusic/Exceptions/AppleMusicException.cs
@@ -0,0 +1,5 @@
+namespace Selector.AppleMusic.Exceptions;
+
+public class AppleMusicException : Exception
+{
+}
\ No newline at end of file
diff --git a/Selector.AppleMusic/Exceptions/ForbiddenException.cs b/Selector.AppleMusic/Exceptions/ForbiddenException.cs
new file mode 100644
index 0000000..406c90e
--- /dev/null
+++ b/Selector.AppleMusic/Exceptions/ForbiddenException.cs
@@ -0,0 +1,5 @@
+namespace Selector.AppleMusic.Exceptions;
+
+public class ForbiddenException : AppleMusicException
+{
+}
\ No newline at end of file
diff --git a/Selector.AppleMusic/Exceptions/RateLimitException.cs b/Selector.AppleMusic/Exceptions/RateLimitException.cs
new file mode 100644
index 0000000..2dc4467
--- /dev/null
+++ b/Selector.AppleMusic/Exceptions/RateLimitException.cs
@@ -0,0 +1,5 @@
+namespace Selector.AppleMusic.Exceptions;
+
+public class RateLimitException : AppleMusicException
+{
+}
\ No newline at end of file
diff --git a/Selector.AppleMusic/Exceptions/ServiceException.cs b/Selector.AppleMusic/Exceptions/ServiceException.cs
new file mode 100644
index 0000000..56f6b6f
--- /dev/null
+++ b/Selector.AppleMusic/Exceptions/ServiceException.cs
@@ -0,0 +1,5 @@
+namespace Selector.AppleMusic.Exceptions;
+
+public class ServiceException : AppleMusicException
+{
+}
\ No newline at end of file
diff --git a/Selector.AppleMusic/Exceptions/UnauthorisedException.cs b/Selector.AppleMusic/Exceptions/UnauthorisedException.cs
new file mode 100644
index 0000000..edfb458
--- /dev/null
+++ b/Selector.AppleMusic/Exceptions/UnauthorisedException.cs
@@ -0,0 +1,5 @@
+namespace Selector.AppleMusic.Exceptions;
+
+public class UnauthorisedException : AppleMusicException
+{
+}
\ No newline at end of file
diff --git a/Selector.AppleMusic/Extensions/ServiceExtensions.cs b/Selector.AppleMusic/Extensions/ServiceExtensions.cs
index 991a2d8..740c289 100644
--- a/Selector.AppleMusic/Extensions/ServiceExtensions.cs
+++ b/Selector.AppleMusic/Extensions/ServiceExtensions.cs
@@ -1,4 +1,5 @@
 using Microsoft.Extensions.DependencyInjection;
+using Selector.AppleMusic.Watcher;
 
 namespace Selector.AppleMusic.Extensions;
 
@@ -6,7 +7,8 @@ public static class ServiceExtensions
 {
     public static IServiceCollection AddAppleMusic(this IServiceCollection services)
     {
-        services.AddSingleton<AppleMusicApiProvider>();
+        services.AddSingleton<AppleMusicApiProvider>()
+            .AddTransient<IAppleMusicWatcherFactory, AppleMusicWatcherFactory>();
 
         return services;
     }
diff --git a/Selector.AppleMusic/JsonContext.cs b/Selector.AppleMusic/JsonContext.cs
new file mode 100644
index 0000000..d6d8254
--- /dev/null
+++ b/Selector.AppleMusic/JsonContext.cs
@@ -0,0 +1,14 @@
+using System.Text.Json.Serialization;
+using Selector.AppleMusic.Model;
+
+namespace Selector.AppleMusic;
+
+[JsonSerializable(typeof(RecentlyPlayedTracksResponse))]
+[JsonSerializable(typeof(TrackAttributes))]
+[JsonSerializable(typeof(PlayParams))]
+[JsonSerializable(typeof(Track))]
+[JsonSerializable(typeof(AppleListeningChangeEventArgs))]
+[JsonSourceGenerationOptions(PropertyNameCaseInsensitive = true)]
+public partial class AppleJsonContext : JsonSerializerContext
+{
+}
\ No newline at end of file
diff --git a/Selector.AppleMusic/Model/RecentlyPlayedTracksResponse.cs b/Selector.AppleMusic/Model/RecentlyPlayedTracksResponse.cs
new file mode 100644
index 0000000..22bf352
--- /dev/null
+++ b/Selector.AppleMusic/Model/RecentlyPlayedTracksResponse.cs
@@ -0,0 +1,6 @@
+namespace Selector.AppleMusic.Model;
+
+public class RecentlyPlayedTracksResponse
+{
+    public List<Track> Data { get; set; }
+}
\ No newline at end of file
diff --git a/Selector.AppleMusic/Model/Track.cs b/Selector.AppleMusic/Model/Track.cs
new file mode 100644
index 0000000..40cdfd5
--- /dev/null
+++ b/Selector.AppleMusic/Model/Track.cs
@@ -0,0 +1,44 @@
+namespace Selector.AppleMusic.Model;
+
+public class TrackAttributes
+{
+    public string AlbumName { get; set; }
+    public List<string> GenreNames { get; set; }
+    public int TrackNumber { get; set; }
+    public int DurationInMillis { get; set; }
+    public DateTime ReleaseDate { get; set; }
+
+    public string Isrc { get; set; }
+
+    //TODO: Artwork
+    public string ComposerName { get; set; }
+    public string Url { get; set; }
+    public PlayParams PlayParams { get; set; }
+    public int DiscNumber { get; set; }
+    public bool HasLyrics { get; set; }
+    public bool IsAppleDigitalMaster { get; set; }
+
+    public string Name { get; set; }
+
+    //TODO: previews
+    public string ArtistName { get; set; }
+}
+
+public class PlayParams
+{
+    public string Id { get; set; }
+    public string Kind { get; set; }
+}
+
+public class Track
+{
+    public string Id { get; set; }
+    public string Type { get; set; }
+    public string Href { get; set; }
+    public TrackAttributes Attributes { get; set; }
+
+    public override string ToString()
+    {
+        return $"{Attributes?.Name} / {Attributes?.AlbumName} / {Attributes?.ArtistName}";
+    }
+}
\ No newline at end of file
diff --git a/Selector.AppleMusic/Selector.AppleMusic.csproj b/Selector.AppleMusic/Selector.AppleMusic.csproj
index 13733df..d072e03 100644
--- a/Selector.AppleMusic/Selector.AppleMusic.csproj
+++ b/Selector.AppleMusic/Selector.AppleMusic.csproj
@@ -11,4 +11,8 @@
       <PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.0.1" />
     </ItemGroup>
 
+    <ItemGroup>
+        <ProjectReference Include="..\Selector\Selector.csproj"/>
+    </ItemGroup>
+
 </Project>
diff --git a/Selector.AppleMusic/Watcher/Consumer/IConsumer.cs b/Selector.AppleMusic/Watcher/Consumer/IConsumer.cs
new file mode 100644
index 0000000..27dce20
--- /dev/null
+++ b/Selector.AppleMusic/Watcher/Consumer/IConsumer.cs
@@ -0,0 +1,5 @@
+namespace Selector.AppleMusic.Watcher.Consumer;
+
+public interface IApplePlayerConsumer : IConsumer<AppleListeningChangeEventArgs>
+{
+}
\ No newline at end of file
diff --git a/Selector.AppleMusic/Watcher/CurrentlyPlayingContext.cs b/Selector.AppleMusic/Watcher/CurrentlyPlayingContext.cs
new file mode 100644
index 0000000..2b5a46c
--- /dev/null
+++ b/Selector.AppleMusic/Watcher/CurrentlyPlayingContext.cs
@@ -0,0 +1,26 @@
+using Selector.AppleMusic.Model;
+
+namespace Selector.AppleMusic.Watcher;
+
+public class AppleMusicCurrentlyPlayingContext
+{
+    public DateTime FirstSeen { get; set; }
+    public Track Track { get; set; }
+}
+
+public class AppleMusicCurrentlyPlayingContextComparer : IEqualityComparer<AppleMusicCurrentlyPlayingContext>
+{
+    public bool Equals(AppleMusicCurrentlyPlayingContext? x, AppleMusicCurrentlyPlayingContext? y)
+    {
+        if (ReferenceEquals(x, y)) return true;
+        if (x is null) return false;
+        if (y is null) return false;
+        if (x.GetType() != y.GetType()) return false;
+        return x.Track.Id.Equals(y.Track.Id);
+    }
+
+    public int GetHashCode(AppleMusicCurrentlyPlayingContext obj)
+    {
+        return obj.Track.GetHashCode();
+    }
+}
\ No newline at end of file
diff --git a/Selector.AppleMusic/Watcher/IPlayerWatcher.cs b/Selector.AppleMusic/Watcher/IPlayerWatcher.cs
new file mode 100644
index 0000000..194ba9c
--- /dev/null
+++ b/Selector.AppleMusic/Watcher/IPlayerWatcher.cs
@@ -0,0 +1,20 @@
+using Selector.AppleMusic;
+using Selector.AppleMusic.Watcher;
+
+namespace Selector
+{
+    public interface IAppleMusicPlayerWatcher : IWatcher
+    {
+        public event EventHandler<AppleListeningChangeEventArgs> NetworkPoll;
+        public event EventHandler<AppleListeningChangeEventArgs> ItemChange;
+        public event EventHandler<AppleListeningChangeEventArgs> AlbumChange;
+        public event EventHandler<AppleListeningChangeEventArgs> ArtistChange;
+
+        /// <summary>
+        /// Last retrieved currently playing
+        /// </summary>
+        public AppleMusicCurrentlyPlayingContext Live { get; }
+
+        public AppleTimeline Past { get; }
+    }
+}
\ No newline at end of file
diff --git a/Selector.AppleMusic/Watcher/PlayerWatcher.cs b/Selector.AppleMusic/Watcher/PlayerWatcher.cs
new file mode 100644
index 0000000..cbbc62a
--- /dev/null
+++ b/Selector.AppleMusic/Watcher/PlayerWatcher.cs
@@ -0,0 +1,145 @@
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Logging.Abstractions;
+using Selector.AppleMusic.Exceptions;
+using Selector.AppleMusic.Model;
+
+namespace Selector.AppleMusic.Watcher;
+
+public class AppleMusicPlayerWatcher : BaseWatcher, IAppleMusicPlayerWatcher
+{
+    new protected readonly ILogger<AppleMusicPlayerWatcher> Logger;
+    private readonly AppleMusicApi _appleMusicApi;
+
+    public event EventHandler<AppleListeningChangeEventArgs> NetworkPoll;
+    public event EventHandler<AppleListeningChangeEventArgs> ItemChange;
+    public event EventHandler<AppleListeningChangeEventArgs> AlbumChange;
+    public event EventHandler<AppleListeningChangeEventArgs> ArtistChange;
+
+    public AppleMusicCurrentlyPlayingContext Live { get; protected set; }
+    protected AppleMusicCurrentlyPlayingContext Previous { get; set; }
+    public AppleTimeline Past { get; set; } = new();
+
+    public AppleMusicPlayerWatcher(AppleMusicApi appleMusicClient,
+        ILogger<AppleMusicPlayerWatcher> logger = null,
+        int pollPeriod = 3000
+    ) : base(logger)
+    {
+        _appleMusicApi = appleMusicClient;
+        Logger = logger ?? NullLogger<AppleMusicPlayerWatcher>.Instance;
+        PollPeriod = pollPeriod;
+    }
+
+    public override async Task WatchOne(CancellationToken token)
+    {
+        token.ThrowIfCancellationRequested();
+
+        try
+        {
+            Logger.LogTrace("Making Apple Music call");
+            var polledCurrent = await _appleMusicApi.GetRecentlyPlayedTracks();
+
+            // using var polledLogScope = Logger.BeginScope(new Dictionary<string, object>() { { "context", polledCurrent?.DisplayString() } });
+
+            Logger.LogTrace("Received Apple Music call");
+
+            var currentPrevious = Previous;
+            var reversedItems = polledCurrent.Data.ToList();
+            reversedItems.Reverse();
+            var addedItems = Past.Add(reversedItems);
+
+            // swap new item into live and bump existing down to previous
+            Previous = Live;
+            SetLive(polledCurrent);
+
+            OnNetworkPoll(GetEvent());
+
+            if (currentPrevious != null && addedItems.Any())
+            {
+                addedItems.Insert(0, currentPrevious);
+                foreach (var (first, second) in addedItems.Zip(addedItems.Skip(1)))
+                {
+                    Logger.LogDebug("Track changed: {prevTrack} -> {currentTrack}", first.Track, second.Track);
+                    OnItemChange(AppleListeningChangeEventArgs.From(first, second, Past, id: Id));
+                }
+            }
+        }
+        catch (RateLimitException e)
+        {
+            Logger.LogDebug("Rate Limit exception: [{message}]", e.Message);
+            // throw e;
+        }
+        catch (ForbiddenException e)
+        {
+            Logger.LogDebug("Forbidden exception: [{message}]", e.Message);
+            throw;
+        }
+        catch (ServiceException e)
+        {
+            Logger.LogDebug("Apple Music internal error: [{message}]", e.Message);
+            // throw e;
+        }
+        catch (UnauthorisedException e)
+        {
+            Logger.LogDebug("Unauthorised exception: [{message}]", e.Message);
+            // throw e;
+        }
+    }
+
+    private void SetLive(RecentlyPlayedTracksResponse recentlyPlayedTracks)
+    {
+        var lastTrack = recentlyPlayedTracks.Data?.FirstOrDefault();
+
+        if (Live != null && Live.Track != null && Live.Track.Id == lastTrack?.Id)
+        {
+            Live = new()
+            {
+                Track = Live.Track,
+                FirstSeen = Live.FirstSeen,
+            };
+        }
+        else
+        {
+            Live = new()
+            {
+                Track = recentlyPlayedTracks.Data?.FirstOrDefault(),
+                FirstSeen = DateTime.UtcNow,
+            };
+        }
+    }
+
+    public override Task Reset()
+    {
+        Previous = null;
+        Live = null;
+        Past = new();
+
+        return Task.CompletedTask;
+    }
+
+    protected AppleListeningChangeEventArgs GetEvent() =>
+        AppleListeningChangeEventArgs.From(Previous, Live, Past, id: Id);
+
+    #region Event Firers
+
+    protected virtual void OnNetworkPoll(AppleListeningChangeEventArgs args)
+    {
+        NetworkPoll?.Invoke(this, args);
+    }
+
+    protected virtual void OnItemChange(AppleListeningChangeEventArgs args)
+    {
+        ItemChange?.Invoke(this, args);
+    }
+
+    protected virtual void OnAlbumChange(AppleListeningChangeEventArgs args)
+    {
+        AlbumChange?.Invoke(this, args);
+    }
+
+    protected virtual void OnArtistChange(AppleListeningChangeEventArgs args)
+    {
+        ArtistChange?.Invoke(this, args);
+    }
+
+    #endregion
+}
\ No newline at end of file
diff --git a/Selector.AppleMusic/Watcher/WatcherFactory.cs b/Selector.AppleMusic/Watcher/WatcherFactory.cs
new file mode 100644
index 0000000..fb0b41c
--- /dev/null
+++ b/Selector.AppleMusic/Watcher/WatcherFactory.cs
@@ -0,0 +1,59 @@
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Logging.Abstractions;
+
+namespace Selector.AppleMusic.Watcher
+{
+    public interface IAppleMusicWatcherFactory
+    {
+        Task<IWatcher> Get<T>(AppleMusicApiProvider appleMusicProvider, string developerToken, string teamId,
+            string keyId, string userToken, int pollPeriod = 3000)
+            where T : class, IWatcher;
+    }
+
+    public class AppleMusicWatcherFactory : IAppleMusicWatcherFactory
+    {
+        private readonly ILoggerFactory LoggerFactory;
+        private readonly IEqual Equal;
+
+        public AppleMusicWatcherFactory(ILoggerFactory loggerFactory, IEqual equal)
+        {
+            LoggerFactory = loggerFactory;
+            Equal = equal;
+        }
+
+        public async Task<IWatcher> Get<T>(AppleMusicApiProvider appleMusicProvider, string developerToken,
+            string teamId, string keyId, string userToken, int pollPeriod = 3000)
+            where T : class, IWatcher
+        {
+            if (typeof(T).IsAssignableFrom(typeof(AppleMusicPlayerWatcher)))
+            {
+                if (!Magic.Dummy)
+                {
+                    var api = appleMusicProvider.GetApi(developerToken, teamId, keyId, userToken);
+
+                    return new AppleMusicPlayerWatcher(
+                        api,
+                        LoggerFactory?.CreateLogger<AppleMusicPlayerWatcher>() ??
+                        NullLogger<AppleMusicPlayerWatcher>.Instance,
+                        pollPeriod: pollPeriod
+                    );
+                }
+                else
+                {
+                    return new DummySpotifyPlayerWatcher(
+                        Equal,
+                        LoggerFactory?.CreateLogger<DummySpotifyPlayerWatcher>() ??
+                        NullLogger<DummySpotifyPlayerWatcher>.Instance,
+                        pollPeriod: pollPeriod
+                    )
+                    {
+                    };
+                }
+            }
+            else
+            {
+                throw new ArgumentException("Type unsupported");
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/Selector.CLI/Consumer/Factory/MappingPersisterFactory.cs b/Selector.CLI/Consumer/Factory/MappingPersisterFactory.cs
index 3e4aa8a..0a43b1a 100644
--- a/Selector.CLI/Consumer/Factory/MappingPersisterFactory.cs
+++ b/Selector.CLI/Consumer/Factory/MappingPersisterFactory.cs
@@ -1,33 +1,33 @@
 using System.Threading.Tasks;
 using Microsoft.Extensions.DependencyInjection;
 using Microsoft.Extensions.Logging;
-using Selector.Model;
 
 namespace Selector.CLI.Consumer
 {
     public interface IMappingPersisterFactory
     {
-        public Task<IPlayerConsumer> Get(IPlayerWatcher watcher = null);
+        public Task<ISpotifyPlayerConsumer> Get(ISpotifyPlayerWatcher watcher = null);
     }
-    
+
     public class MappingPersisterFactory : IMappingPersisterFactory
     {
         private readonly ILoggerFactory LoggerFactory;
         private readonly IServiceScopeFactory ScopeFactory;
 
-        public MappingPersisterFactory(ILoggerFactory loggerFactory, IServiceScopeFactory scopeFactory = null, LastFmCredentials creds = null)
+        public MappingPersisterFactory(ILoggerFactory loggerFactory, IServiceScopeFactory scopeFactory = null,
+            LastFmCredentials creds = null)
         {
             LoggerFactory = loggerFactory;
             ScopeFactory = scopeFactory;
         }
 
-        public Task<IPlayerConsumer> Get(IPlayerWatcher watcher = null)
+        public Task<ISpotifyPlayerConsumer> Get(ISpotifyPlayerWatcher watcher = null)
         {
-            return Task.FromResult<IPlayerConsumer>(new MappingPersister(
+            return Task.FromResult<ISpotifyPlayerConsumer>(new MappingPersister(
                 watcher,
                 ScopeFactory,
                 LoggerFactory.CreateLogger<MappingPersister>()
             ));
         }
     }
-}
+}
\ No newline at end of file
diff --git a/Selector.CLI/Consumer/MappingPersister.cs b/Selector.CLI/Consumer/MappingPersister.cs
index b297df2..4eb9de5 100644
--- a/Selector.CLI/Consumer/MappingPersister.cs
+++ b/Selector.CLI/Consumer/MappingPersister.cs
@@ -15,16 +15,16 @@ namespace Selector.CLI.Consumer
     /// <summary>
     /// Save name -> Spotify URI mappings as new objects come through the watcher without making extra queries of the Spotify API
     /// </summary>
-    public class MappingPersister: IPlayerConsumer
+    public class MappingPersister : ISpotifyPlayerConsumer
     {
-        protected readonly IPlayerWatcher Watcher;
+        protected readonly ISpotifyPlayerWatcher Watcher;
         protected readonly IServiceScopeFactory ScopeFactory;
         protected readonly ILogger<MappingPersister> Logger;
 
         public CancellationToken CancelToken { get; set; }
 
         public MappingPersister(
-            IPlayerWatcher watcher,
+            ISpotifyPlayerWatcher watcher,
             IServiceScopeFactory scopeFactory,
             ILogger<MappingPersister> logger = null,
             CancellationToken token = default
@@ -40,7 +40,8 @@ namespace Selector.CLI.Consumer
         {
             if (e.Current is null) return;
 
-            Task.Run(async () => {
+            Task.Run(async () =>
+            {
                 try
                 {
                     await AsyncCallback(e);
@@ -59,13 +60,14 @@ namespace Selector.CLI.Consumer
         public async Task AsyncCallback(ListeningChangeEventArgs e)
         {
             using var serviceScope = ScopeFactory.CreateScope();
-            using var scope = Logger.BeginScope(new Dictionary<string, object>() { { "spotify_username", e.SpotifyUsername }, { "id", e.Id } });
+            using var scope = Logger.BeginScope(new Dictionary<string, object>()
+                { { "spotify_username", e.SpotifyUsername }, { "id", e.Id } });
 
             if (e.Current.Item is FullTrack track)
-            { 
+            {
                 var mappingRepo = serviceScope.ServiceProvider.GetRequiredService<IScrobbleMappingRepository>();
 
-                if(!mappingRepo.GetTracks().Select(t => t.SpotifyUri).Contains(track.Uri))
+                if (!mappingRepo.GetTracks().Select(t => t.SpotifyUri).Contains(track.Uri))
                 {
                     mappingRepo.Add(new TrackLastfmSpotifyMapping()
                     {
@@ -120,7 +122,7 @@ namespace Selector.CLI.Consumer
         {
             var watcher = watch ?? Watcher ?? throw new ArgumentNullException(nameof(watch));
 
-            if (watcher is IPlayerWatcher watcherCast)
+            if (watcher is ISpotifyPlayerWatcher watcherCast)
             {
                 watcherCast.ItemChange += Callback;
             }
@@ -134,7 +136,7 @@ namespace Selector.CLI.Consumer
         {
             var watcher = watch ?? Watcher ?? throw new ArgumentNullException(nameof(watch));
 
-            if (watcher is IPlayerWatcher watcherCast)
+            if (watcher is ISpotifyPlayerWatcher watcherCast)
             {
                 watcherCast.ItemChange -= Callback;
             }
@@ -144,5 +146,4 @@ namespace Selector.CLI.Consumer
             }
         }
     }
-}
-
+}
\ No newline at end of file
diff --git a/Selector.CLI/Options.cs b/Selector.CLI/Options.cs
index e5000e3..6a4111f 100644
--- a/Selector.CLI/Options.cs
+++ b/Selector.CLI/Options.cs
@@ -5,16 +5,18 @@ using Microsoft.Extensions.DependencyInjection;
 
 namespace Selector.CLI
 {
-    static class OptionsHelper {
+    static class OptionsHelper
+    {
         public static void ConfigureOptions(RootOptions options, IConfiguration config)
         {
             config.GetSection(RootOptions.Key).Bind(options);
-            config.GetSection(FormatKeys( new[] { RootOptions.Key, WatcherOptions.Key})).Bind(options.WatcherOptions);
-            config.GetSection(FormatKeys( new[] { RootOptions.Key, DatabaseOptions.Key})).Bind(options.DatabaseOptions);
-            config.GetSection(FormatKeys( new[] { RootOptions.Key, RedisOptions.Key})).Bind(options.RedisOptions);
-            config.GetSection(FormatKeys( new[] { RootOptions.Key, JobsOptions.Key})).Bind(options.JobOptions);
-            config.GetSection(FormatKeys( new[] { RootOptions.Key, JobsOptions.Key, ScrobbleWatcherJobOptions.Key })).Bind(options.JobOptions.Scrobble);
-        }  
+            config.GetSection(FormatKeys(new[] { RootOptions.Key, WatcherOptions.Key })).Bind(options.WatcherOptions);
+            config.GetSection(FormatKeys(new[] { RootOptions.Key, DatabaseOptions.Key })).Bind(options.DatabaseOptions);
+            config.GetSection(FormatKeys(new[] { RootOptions.Key, RedisOptions.Key })).Bind(options.RedisOptions);
+            config.GetSection(FormatKeys(new[] { RootOptions.Key, JobsOptions.Key })).Bind(options.JobOptions);
+            config.GetSection(FormatKeys(new[] { RootOptions.Key, JobsOptions.Key, ScrobbleWatcherJobOptions.Key }))
+                .Bind(options.JobOptions.Scrobble);
+        }
 
         public static RootOptions ConfigureOptions(this IConfiguration config)
         {
@@ -29,12 +31,16 @@ namespace Selector.CLI
         {
             var options = config.GetSection(RootOptions.Key).Get<RootOptions>();
 
-            services.Configure<DatabaseOptions>(config.GetSection(FormatKeys(new[] { RootOptions.Key, DatabaseOptions.Key })));
-            services.Configure<RedisOptions>(config.GetSection(FormatKeys(new[] { RootOptions.Key, RedisOptions.Key })));
-            services.Configure<WatcherOptions>(config.GetSection(FormatKeys(new[] { RootOptions.Key, WatcherOptions.Key })));
+            services.Configure<DatabaseOptions>(config.GetSection(FormatKeys(new[]
+                { RootOptions.Key, DatabaseOptions.Key })));
+            services.Configure<RedisOptions>(config.GetSection(FormatKeys(new[]
+                { RootOptions.Key, RedisOptions.Key })));
+            services.Configure<WatcherOptions>(config.GetSection(FormatKeys(new[]
+                { RootOptions.Key, WatcherOptions.Key })));
 
             services.Configure<JobsOptions>(config.GetSection(FormatKeys(new[] { RootOptions.Key, JobsOptions.Key })));
-            services.Configure<ScrobbleWatcherJobOptions>(config.GetSection(FormatKeys(new[] { RootOptions.Key, JobsOptions.Key, ScrobbleWatcherJobOptions.Key })));
+            services.Configure<ScrobbleWatcherJobOptions>(config.GetSection(FormatKeys(new[]
+                { RootOptions.Key, JobsOptions.Key, ScrobbleWatcherJobOptions.Key })));
             services.Configure<AppleMusicOptions>(config.GetSection(AppleMusicOptions._Key));
 
             return options;
@@ -51,14 +57,17 @@ namespace Selector.CLI
         /// Spotify client ID
         /// </summary>
         public string ClientId { get; set; }
+
         /// <summary>
         /// Spotify app secret
         /// </summary>
         public string ClientSecret { get; set; }
+
         /// <summary>
         /// Service account refresh token for tool spotify usage
         /// </summary>
         public string RefreshToken { get; set; }
+
         public string LastfmClient { get; set; }
         public string LastfmSecret { get; set; }
         public WatcherOptions WatcherOptions { get; set; } = new();
@@ -70,7 +79,8 @@ namespace Selector.CLI
 
     public enum EqualityChecker
     {
-        Uri, String
+        Uri,
+        String
     }
 
     public class WatcherOptions
@@ -89,9 +99,10 @@ namespace Selector.CLI
         public string Name { get; set; }
         public string AccessKey { get; set; }
         public string RefreshKey { get; set; }
+        public string AppleUserToken { get; set; }
         public string LastFmUsername { get; set; }
         public int PollPeriod { get; set; } = 5000;
-        public WatcherType Type { get; set; } = WatcherType.Player;
+        public WatcherType Type { get; set; } = WatcherType.SpotifyPlayer;
         public List<Consumers> Consumers { get; set; } = default;
 #nullable enable
         public string? PlaylistUri { get; set; }
@@ -101,7 +112,12 @@ namespace Selector.CLI
 
     public enum Consumers
     {
-        AudioFeatures, AudioFeaturesCache, CacheWriter, Publisher, PlayCounter, MappingPersister
+        AudioFeatures,
+        AudioFeaturesCache,
+        CacheWriter,
+        Publisher,
+        PlayCounter,
+        MappingPersister
     }
 
     public class RedisOptions
@@ -143,4 +159,4 @@ namespace Selector.CLI
         public string KeyId { get; set; }
         public TimeSpan? Expiry { get; set; } = null;
     }
-}
+}
\ No newline at end of file
diff --git a/Selector.CLI/Selector.CLI.csproj b/Selector.CLI/Selector.CLI.csproj
index ff6bd19..618db94 100644
--- a/Selector.CLI/Selector.CLI.csproj
+++ b/Selector.CLI/Selector.CLI.csproj
@@ -59,4 +59,12 @@
     <Folder Include="Consumer\" />
     <Folder Include="Consumer\Factory\" />
   </ItemGroup>
+
+  <ItemGroup>
+    <Content Update="appsettings.Development.json">
+      <ExcludeFromSingleFile>true</ExcludeFromSingleFile>
+      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+      <CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
+    </Content>
+  </ItemGroup>
 </Project>
diff --git a/Selector.CLI/Services/DbWatcherService.cs b/Selector.CLI/Services/DbWatcherService.cs
index 250df04..29e7f9e 100644
--- a/Selector.CLI/Services/DbWatcherService.cs
+++ b/Selector.CLI/Services/DbWatcherService.cs
@@ -1,4 +1,5 @@
 using System;
+using System.Collections.Concurrent;
 using System.Collections.Generic;
 using System.Linq;
 using System.Threading;
@@ -7,13 +8,14 @@ using Microsoft.EntityFrameworkCore;
 using Microsoft.Extensions.DependencyInjection;
 using Microsoft.Extensions.Hosting;
 using Microsoft.Extensions.Logging;
-
+using Microsoft.Extensions.Options;
+using Selector.AppleMusic;
+using Selector.AppleMusic.Watcher;
 using Selector.Cache;
+using Selector.CLI.Consumer;
+using Selector.Events;
 using Selector.Model;
 using Selector.Model.Extensions;
-using Selector.Events;
-using System.Collections.Concurrent;
-using Selector.CLI.Consumer;
 
 namespace Selector.CLI
 {
@@ -22,13 +24,16 @@ namespace Selector.CLI
         private const int PollPeriod = 1000;
 
         private readonly ILogger<DbWatcherService> Logger;
+        private readonly IOptions<AppleMusicOptions> _appleMusicOptions;
         private readonly IServiceProvider ServiceProvider;
         private readonly UserEventBus UserEventBus;
 
-        private readonly IWatcherFactory WatcherFactory;
+        private readonly ISpotifyWatcherFactory _spotifyWatcherFactory;
+        private readonly IAppleMusicWatcherFactory _appleWatcherFactory;
         private readonly IWatcherCollectionFactory WatcherCollectionFactory;
         private readonly IRefreshTokenFactoryProvider SpotifyFactory;
-        
+        private readonly AppleMusicApiProvider _appleMusicProvider;
+
         private readonly IAudioFeatureInjectorFactory AudioFeatureInjectorFactory;
         private readonly IPlayCounterFactory PlayCounterFactory;
 
@@ -42,23 +47,20 @@ namespace Selector.CLI
         private ConcurrentDictionary<string, IWatcherCollection> Watchers { get; set; } = new();
 
         public DbWatcherService(
-            IWatcherFactory watcherFactory,
+            ISpotifyWatcherFactory spotifyWatcherFactory,
+            IAppleMusicWatcherFactory appleWatcherFactory,
             IWatcherCollectionFactory watcherCollectionFactory,
             IRefreshTokenFactoryProvider spotifyFactory,
-
+            AppleMusicApiProvider appleMusicProvider,
             IAudioFeatureInjectorFactory audioFeatureInjectorFactory,
             IPlayCounterFactory playCounterFactory,
-
             UserEventBus userEventBus,
-
             ILogger<DbWatcherService> logger,
+            IOptions<AppleMusicOptions> appleMusicOptions,
             IServiceProvider serviceProvider,
-
             IPublisherFactory publisherFactory = null,
             ICacheWriterFactory cacheWriterFactory = null,
-
             IMappingPersisterFactory mappingPersisterFactory = null,
-
             IUserEventFirerFactory userEventFirerFactory = null
         )
         {
@@ -66,10 +68,13 @@ namespace Selector.CLI
             ServiceProvider = serviceProvider;
             UserEventBus = userEventBus;
 
-            WatcherFactory = watcherFactory;
+            _spotifyWatcherFactory = spotifyWatcherFactory;
+            _appleWatcherFactory = appleWatcherFactory;
+            _appleMusicOptions = appleMusicOptions;
             WatcherCollectionFactory = watcherCollectionFactory;
             SpotifyFactory = spotifyFactory;
-                
+            _appleMusicProvider = appleMusicProvider;
+
             AudioFeatureInjectorFactory = audioFeatureInjectorFactory;
             PlayCounterFactory = playCounterFactory;
 
@@ -100,8 +105,8 @@ namespace Selector.CLI
             var indices = new HashSet<string>();
 
             foreach (var dbWatcher in db.Watcher
-                                        .Include(w => w.User)
-                                        .Where(w => !string.IsNullOrWhiteSpace(w.User.SpotifyRefreshToken)))
+                         .Include(w => w.User)
+                         .Where(w => !string.IsNullOrWhiteSpace(w.User.SpotifyRefreshToken)))
             {
                 var watcherCollectionIdx = dbWatcher.UserId;
                 indices.Add(watcherCollectionIdx);
@@ -131,31 +136,43 @@ namespace Selector.CLI
 
             switch (dbWatcher.Type)
             {
-                case WatcherType.Player:
-                    watcher = await WatcherFactory.Get<PlayerWatcher>(spotifyFactory, id: dbWatcher.UserId, pollPeriod: PollPeriod);
+                case WatcherType.SpotifyPlayer:
+                    watcher = await _spotifyWatcherFactory.Get<SpotifyPlayerWatcher>(spotifyFactory,
+                        id: dbWatcher.UserId, pollPeriod: PollPeriod);
 
                     consumers.Add(await AudioFeatureInjectorFactory.Get(spotifyFactory));
                     if (CacheWriterFactory is not null) consumers.Add(await CacheWriterFactory.Get());
-                    if (PublisherFactory is not null) consumers.Add(await PublisherFactory.Get());
+                    if (PublisherFactory is not null) consumers.Add(await PublisherFactory.GetSpotify());
 
-                    if (MappingPersisterFactory is not null && !Magic.Dummy) consumers.Add(await MappingPersisterFactory.Get());
+                    if (MappingPersisterFactory is not null && !Magic.Dummy)
+                        consumers.Add(await MappingPersisterFactory.Get());
 
                     if (UserEventFirerFactory is not null) consumers.Add(await UserEventFirerFactory.Get());
 
                     if (dbWatcher.User.LastFmConnected())
                     {
-                        consumers.Add(await PlayCounterFactory.Get(creds: new() { Username = dbWatcher.User.LastFmUsername }));
+                        consumers.Add(await PlayCounterFactory.Get(creds: new()
+                            { Username = dbWatcher.User.LastFmUsername }));
                     }
                     else
                     {
-                        Logger.LogDebug("[{username}] No Last.fm username, skipping play counter", dbWatcher.User.UserName);
+                        Logger.LogDebug("[{username}] No Last.fm username, skipping play counter",
+                            dbWatcher.User.UserName);
                     }
 
                     break;
 
-                case WatcherType.Playlist:
+                case WatcherType.SpotifyPlaylist:
                     throw new NotImplementedException("Playlist watchers not implemented");
-                    // break;
+                    break;
+                case WatcherType.AppleMusicPlayer:
+                    watcher = await _appleWatcherFactory.Get<AppleMusicPlayerWatcher>(_appleMusicProvider,
+                        _appleMusicOptions.Value.Key, _appleMusicOptions.Value.TeamId, _appleMusicOptions.Value.KeyId,
+                        dbWatcher.User.AppleMusicKey);
+
+                    if (PublisherFactory is not null) consumers.Add(await PublisherFactory.GetApple());
+
+                    break;
             }
 
             return watcherCollection.Add(watcher, consumers);
@@ -181,7 +198,7 @@ namespace Selector.CLI
         {
             Logger.LogInformation("Shutting down");
 
-            foreach((var key, var watcher) in Watchers)
+            foreach ((var key, var watcher) in Watchers)
             {
                 Logger.LogInformation("Stopping watcher collection [{key}]", key);
                 watcher.Stop();
@@ -195,24 +212,27 @@ namespace Selector.CLI
         private void AttachEventBus()
         {
             UserEventBus.SpotifyLinkChange += SpotifyChangeCallback;
+            UserEventBus.AppleLinkChange += AppleMusicChangeCallback;
             UserEventBus.LastfmCredChange += LastfmChangeCallback;
         }
 
         private void DetachEventBus()
         {
             UserEventBus.SpotifyLinkChange -= SpotifyChangeCallback;
+            UserEventBus.AppleLinkChange -= AppleMusicChangeCallback;
             UserEventBus.LastfmCredChange -= LastfmChangeCallback;
         }
 
         public async void SpotifyChangeCallback(object sender, SpotifyLinkChange change)
         {
-            if(Watchers.ContainsKey(change.UserId))
+            if (Watchers.ContainsKey(change.UserId))
             {
-                Logger.LogDebug("Setting new Spotify link state for [{username}], [{}]", change.UserId, change.NewLinkState);
+                Logger.LogDebug("Setting new Spotify link state for [{username}], [{}]", change.UserId,
+                    change.NewLinkState);
 
                 var watcherCollection = Watchers[change.UserId];
 
-                if(change.NewLinkState)
+                if (change.NewLinkState)
                 {
                     watcherCollection.Start();
                 }
@@ -227,8 +247,46 @@ namespace Selector.CLI
                 var db = scope.ServiceProvider.GetService<ApplicationDbContext>();
 
                 var watcherEnum = db.Watcher
-                                    .Include(w => w.User)
-                                    .Where(w => w.UserId == change.UserId);
+                    .Include(w => w.User)
+                    .Where(w => w.UserId == change.UserId);
+
+                foreach (var dbWatcher in watcherEnum)
+                {
+                    var context = await InitInstance(dbWatcher);
+                }
+
+                Watchers[change.UserId].Start();
+
+                Logger.LogDebug("Started {} watchers for [{username}]", watcherEnum.Count(), change.UserId);
+            }
+        }
+
+        public async void AppleMusicChangeCallback(object sender, AppleMusicLinkChange change)
+        {
+            if (Watchers.ContainsKey(change.UserId))
+            {
+                Logger.LogDebug("Setting new Apple Music link state for [{username}], [{}]", change.UserId,
+                    change.NewLinkState);
+
+                var watcherCollection = Watchers[change.UserId];
+
+                if (change.NewLinkState)
+                {
+                    watcherCollection.Start();
+                }
+                else
+                {
+                    watcherCollection.Stop();
+                }
+            }
+            else
+            {
+                using var scope = ServiceProvider.CreateScope();
+                var db = scope.ServiceProvider.GetService<ApplicationDbContext>();
+
+                var watcherEnum = db.Watcher
+                    .Include(w => w.User)
+                    .Where(w => w.UserId == change.UserId);
 
                 foreach (var dbWatcher in watcherEnum)
                 {
@@ -249,9 +307,9 @@ namespace Selector.CLI
 
                 var watcherCollection = Watchers[change.UserId];
 
-                foreach(var watcher in watcherCollection.Consumers)
+                foreach (var watcher in watcherCollection.Consumers)
                 {
-                    if(watcher is PlayCounter counter)
+                    if (watcher is PlayCounter counter)
                     {
                         counter.Credentials.Username = change.NewUsername;
                     }
@@ -259,9 +317,8 @@ namespace Selector.CLI
             }
             else
             {
-
                 Logger.LogDebug("No watchers running for [{username}], skipping Spotify event", change.UserId);
             }
         }
     }
-}
+}
\ No newline at end of file
diff --git a/Selector.CLI/Services/LocalWatcherService.cs b/Selector.CLI/Services/LocalWatcherService.cs
index 8e3a232..77051d7 100644
--- a/Selector.CLI/Services/LocalWatcherService.cs
+++ b/Selector.CLI/Services/LocalWatcherService.cs
@@ -4,12 +4,12 @@ using System.Linq;
 using System.Text;
 using System.Threading;
 using System.Threading.Tasks;
-
 using Microsoft.Extensions.DependencyInjection;
 using Microsoft.Extensions.Hosting;
 using Microsoft.Extensions.Logging;
 using Microsoft.Extensions.Options;
-
+using Selector.AppleMusic;
+using Selector.AppleMusic.Watcher;
 using Selector.Cache;
 using Selector.CLI.Consumer;
 
@@ -21,30 +21,38 @@ namespace Selector.CLI
 
         private readonly ILogger<LocalWatcherService> Logger;
         private readonly RootOptions Config;
-        private readonly IWatcherFactory WatcherFactory;
+        private readonly ISpotifyWatcherFactory _spotifyWatcherFactory;
+        private readonly IAppleMusicWatcherFactory _appleWatcherFactory;
         private readonly IWatcherCollectionFactory WatcherCollectionFactory;
         private readonly IRefreshTokenFactoryProvider SpotifyFactory;
+        private readonly AppleMusicApiProvider _appleMusicApiProvider;
+        private readonly IOptions<AppleMusicOptions> _appleMusicOptions;
 
         private readonly IServiceProvider ServiceProvider;
 
         private Dictionary<string, IWatcherCollection> Watchers { get; set; } = new();
 
         public LocalWatcherService(
-            IWatcherFactory watcherFactory,
+            ISpotifyWatcherFactory spotifyWatcherFactory,
+            IAppleMusicWatcherFactory appleWatcherFactory,
             IWatcherCollectionFactory watcherCollectionFactory,
             IRefreshTokenFactoryProvider spotifyFactory,
-
+            AppleMusicApiProvider appleMusicApiProvider,
+            IOptions<AppleMusicOptions> appleMusicOptions,
             IServiceProvider serviceProvider,
-
             ILogger<LocalWatcherService> logger,
             IOptions<RootOptions> config
-        ) {
+        )
+        {
             Logger = logger;
             Config = config.Value;
 
-            WatcherFactory = watcherFactory;
+            _spotifyWatcherFactory = spotifyWatcherFactory;
+            _appleWatcherFactory = appleWatcherFactory;
             WatcherCollectionFactory = watcherCollectionFactory;
             SpotifyFactory = spotifyFactory;
+            _appleMusicApiProvider = appleMusicApiProvider;
+            _appleMusicOptions = appleMusicOptions;
 
             ServiceProvider = serviceProvider;
         }
@@ -75,7 +83,8 @@ namespace Selector.CLI
                     logMsg.Append($"Creating new [{watcherOption.Type}] watcher");
                 }
 
-                if (!string.IsNullOrWhiteSpace(watcherOption.PlaylistUri)) logMsg.Append($" [{ watcherOption.PlaylistUri}]");
+                if (!string.IsNullOrWhiteSpace(watcherOption.PlaylistUri))
+                    logMsg.Append($" [{watcherOption.PlaylistUri}]");
                 Logger.LogInformation(logMsg.ToString());
 
                 var watcherCollectionIdx = watcherOption.WatcherCollection ?? ConfigInstanceKey;
@@ -90,17 +99,26 @@ namespace Selector.CLI
                 var spotifyFactory = await SpotifyFactory.GetFactory(watcherOption.RefreshKey);
 
                 IWatcher watcher = null;
-                switch(watcherOption.Type)
+                switch (watcherOption.Type)
                 {
-                    case WatcherType.Player:
-                        watcher = await WatcherFactory.Get<PlayerWatcher>(spotifyFactory, id: watcherOption.Name, pollPeriod: watcherOption.PollPeriod);
+                    case WatcherType.SpotifyPlayer:
+                        watcher = await _spotifyWatcherFactory.Get<SpotifyPlayerWatcher>(spotifyFactory,
+                            id: watcherOption.Name, pollPeriod: watcherOption.PollPeriod);
                         break;
-                    case WatcherType.Playlist:
-                        var playlistWatcher = await WatcherFactory.Get<PlaylistWatcher>(spotifyFactory, id: watcherOption.Name, pollPeriod: watcherOption.PollPeriod) as PlaylistWatcher;
+                    case WatcherType.SpotifyPlaylist:
+                        var playlistWatcher = await _spotifyWatcherFactory.Get<PlaylistWatcher>(spotifyFactory,
+                            id: watcherOption.Name, pollPeriod: watcherOption.PollPeriod) as PlaylistWatcher;
                         playlistWatcher.config = new() { PlaylistId = watcherOption.PlaylistUri };
 
                         watcher = playlistWatcher;
                         break;
+                    case WatcherType.AppleMusicPlayer:
+                        var appleMusicWatcher = await _appleWatcherFactory.Get<AppleMusicPlayerWatcher>(
+                            _appleMusicApiProvider, _appleMusicOptions.Value.Key, _appleMusicOptions.Value.TeamId,
+                            _appleMusicOptions.Value.KeyId, watcherOption.AppleUserToken);
+
+                        watcher = appleMusicWatcher;
+                        break;
                 }
 
                 List<IConsumer> consumers = new();
@@ -112,11 +130,13 @@ namespace Selector.CLI
                         switch (consumer)
                         {
                             case Consumers.AudioFeatures:
-                                consumers.Add(await ServiceProvider.GetService<AudioFeatureInjectorFactory>().Get(spotifyFactory));
+                                consumers.Add(await ServiceProvider.GetService<AudioFeatureInjectorFactory>()
+                                    .Get(spotifyFactory));
                                 break;
 
                             case Consumers.AudioFeaturesCache:
-                                consumers.Add(await ServiceProvider.GetService<CachingAudioFeatureInjectorFactory>().Get(spotifyFactory));
+                                consumers.Add(await ServiceProvider.GetService<CachingAudioFeatureInjectorFactory>()
+                                    .Get(spotifyFactory));
                                 break;
 
                             case Consumers.CacheWriter:
@@ -124,18 +144,20 @@ namespace Selector.CLI
                                 break;
 
                             case Consumers.Publisher:
-                                consumers.Add(await ServiceProvider.GetService<PublisherFactory>().Get());
+                                consumers.Add(await ServiceProvider.GetService<PublisherFactory>().GetSpotify());
                                 break;
 
                             case Consumers.PlayCounter:
                                 if (!string.IsNullOrWhiteSpace(watcherOption.LastFmUsername))
                                 {
-                                    consumers.Add(await ServiceProvider.GetService<PlayCounterFactory>().Get(creds: new() { Username = watcherOption.LastFmUsername }));
+                                    consumers.Add(await ServiceProvider.GetService<PlayCounterFactory>()
+                                        .Get(creds: new() { Username = watcherOption.LastFmUsername }));
                                 }
                                 else
                                 {
                                     Logger.LogError("No Last.fm username provided, skipping play counter");
                                 }
+
                                 break;
 
                             case Consumers.MappingPersister:
@@ -171,7 +193,7 @@ namespace Selector.CLI
         {
             Logger.LogInformation("Shutting down");
 
-            foreach((var key, var watcher) in Watchers)
+            foreach ((var key, var watcher) in Watchers)
             {
                 Logger.LogInformation("Stopping watcher collection [{key}]", key);
                 watcher.Stop();
@@ -180,4 +202,4 @@ namespace Selector.CLI
             return Task.CompletedTask;
         }
     }
-}
+}
\ No newline at end of file
diff --git a/Selector.Cache/Consumer/AppleMusic/PublisherConsumer.cs b/Selector.Cache/Consumer/AppleMusic/PublisherConsumer.cs
new file mode 100644
index 0000000..77e4b38
--- /dev/null
+++ b/Selector.Cache/Consumer/AppleMusic/PublisherConsumer.cs
@@ -0,0 +1,93 @@
+using System;
+using System.Text.Json;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Logging.Abstractions;
+using Selector.AppleMusic;
+using Selector.AppleMusic.Watcher.Consumer;
+using StackExchange.Redis;
+
+namespace Selector.Cache.Consumer.AppleMusic
+{
+    public class ApplePublisher : IApplePlayerConsumer
+    {
+        private readonly IAppleMusicPlayerWatcher Watcher;
+        private readonly ISubscriber Subscriber;
+        private readonly ILogger<ApplePublisher> Logger;
+
+        public CancellationToken CancelToken { get; set; }
+
+        public ApplePublisher(
+            IAppleMusicPlayerWatcher watcher,
+            ISubscriber subscriber,
+            ILogger<ApplePublisher> logger = null,
+            CancellationToken token = default
+        )
+        {
+            Watcher = watcher;
+            Subscriber = subscriber;
+            Logger = logger ?? NullLogger<ApplePublisher>.Instance;
+            CancelToken = token;
+        }
+
+        public void Callback(object sender, AppleListeningChangeEventArgs e)
+        {
+            if (e.Current is null) return;
+
+            Task.Run(async () =>
+            {
+                try
+                {
+                    await AsyncCallback(e);
+                }
+                catch (Exception e)
+                {
+                    Logger.LogError(e, "Error occured during callback");
+                }
+            }, CancelToken);
+        }
+
+        public async Task AsyncCallback(AppleListeningChangeEventArgs e)
+        {
+            // using var scope = Logger.GetListeningEventArgsScope(e);
+
+            var payload = JsonSerializer.Serialize(e, AppleJsonContext.Default.AppleListeningChangeEventArgs);
+
+            Logger.LogTrace("Publishing current");
+
+            // TODO: currently using spotify username for cache key, use db username
+            var receivers = await Subscriber.PublishAsync(Key.CurrentlyPlayingAppleMusic(e.Id), payload);
+
+            Logger.LogDebug("Published current, {receivers} receivers", receivers);
+        }
+
+        public void Subscribe(IWatcher watch = null)
+        {
+            var watcher = watch ?? Watcher ?? throw new ArgumentNullException("No watcher provided");
+
+            if (watcher is IAppleMusicPlayerWatcher 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 IAppleMusicPlayerWatcher watcherCast)
+            {
+                watcherCast.ItemChange -= Callback;
+            }
+            else
+            {
+                throw new ArgumentException("Provided watcher is not a PlayerWatcher");
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/Selector.Cache/Consumer/Factory/AudioInjectorCaching.cs b/Selector.Cache/Consumer/Factory/AudioInjectorCaching.cs
index e5612e5..2896c3a 100644
--- a/Selector.Cache/Consumer/Factory/AudioInjectorCaching.cs
+++ b/Selector.Cache/Consumer/Factory/AudioInjectorCaching.cs
@@ -1,28 +1,26 @@
-using System;
-using System.Collections.Generic;
-using System.Text;
 using System.Threading.Tasks;
 using Microsoft.Extensions.Logging;
-
 using SpotifyAPI.Web;
 using StackExchange.Redis;
 
 namespace Selector.Cache
-{    
-    public class CachingAudioFeatureInjectorFactory: IAudioFeatureInjectorFactory {
-
+{
+    public class CachingAudioFeatureInjectorFactory : IAudioFeatureInjectorFactory
+    {
         private readonly ILoggerFactory LoggerFactory;
         private readonly IDatabaseAsync Db;
 
         public CachingAudioFeatureInjectorFactory(
             ILoggerFactory loggerFactory,
             IDatabaseAsync db
-        ) {
+        )
+        {
             LoggerFactory = loggerFactory;
             Db = db;
         }
 
-        public async Task<IPlayerConsumer> Get(ISpotifyConfigFactory spotifyFactory, IPlayerWatcher watcher = null)
+        public async Task<ISpotifyPlayerConsumer> Get(ISpotifyConfigFactory spotifyFactory,
+            ISpotifyPlayerWatcher watcher = null)
         {
             if (!Magic.Dummy)
             {
@@ -45,4 +43,4 @@ namespace Selector.Cache
             }
         }
     }
-}
+}
\ No newline at end of file
diff --git a/Selector.Cache/Consumer/Factory/CacheWriterFactory.cs b/Selector.Cache/Consumer/Factory/CacheWriterFactory.cs
index a22d4f8..2ddfa01 100644
--- a/Selector.Cache/Consumer/Factory/CacheWriterFactory.cs
+++ b/Selector.Cache/Consumer/Factory/CacheWriterFactory.cs
@@ -1,37 +1,35 @@
-using System;
-using System.Collections.Generic;
-using System.Text;
-using System.Threading.Tasks;
+using System.Threading.Tasks;
 using Microsoft.Extensions.Logging;
-
 using StackExchange.Redis;
 
 namespace Selector.Cache
 {
-    public interface ICacheWriterFactory {
-        public Task<IPlayerConsumer> Get(IPlayerWatcher watcher = null);
+    public interface ICacheWriterFactory
+    {
+        public Task<ISpotifyPlayerConsumer> Get(ISpotifyPlayerWatcher watcher = null);
     }
 
-    public class CacheWriterFactory: ICacheWriterFactory {
-
+    public class CacheWriterFactory : ICacheWriterFactory
+    {
         private readonly ILoggerFactory LoggerFactory;
         private readonly IDatabaseAsync Cache;
 
         public CacheWriterFactory(
-            IDatabaseAsync cache, 
+            IDatabaseAsync cache,
             ILoggerFactory loggerFactory
-        ) {
+        )
+        {
             Cache = cache;
             LoggerFactory = loggerFactory;
         }
 
-        public Task<IPlayerConsumer> Get(IPlayerWatcher watcher = null)
+        public Task<ISpotifyPlayerConsumer> Get(ISpotifyPlayerWatcher watcher = null)
         {
-            return Task.FromResult<IPlayerConsumer>(new CacheWriter(
+            return Task.FromResult<ISpotifyPlayerConsumer>(new CacheWriter(
                 watcher,
                 Cache,
                 LoggerFactory.CreateLogger<CacheWriter>()
             ));
         }
     }
-}
+}
\ No newline at end of file
diff --git a/Selector.Cache/Consumer/Factory/PlayCounterCachingFactory.cs b/Selector.Cache/Consumer/Factory/PlayCounterCachingFactory.cs
index 0ca6fe8..d6c0f8a 100644
--- a/Selector.Cache/Consumer/Factory/PlayCounterCachingFactory.cs
+++ b/Selector.Cache/Consumer/Factory/PlayCounterCachingFactory.cs
@@ -1,15 +1,12 @@
 using System;
-using System.Collections.Generic;
-using System.Text;
 using System.Threading.Tasks;
-using Microsoft.Extensions.Logging;
-
-using StackExchange.Redis;
 using IF.Lastfm.Core.Api;
+using Microsoft.Extensions.Logging;
+using StackExchange.Redis;
 
 namespace Selector.Cache
 {
-    public class PlayCounterCachingFactory: IPlayCounterFactory
+    public class PlayCounterCachingFactory : IPlayCounterFactory
     {
         private readonly ILoggerFactory LoggerFactory;
         private readonly IDatabaseAsync Cache;
@@ -17,9 +14,9 @@ namespace Selector.Cache
         private readonly LastFmCredentials Creds;
 
         public PlayCounterCachingFactory(
-            ILoggerFactory loggerFactory, 
+            ILoggerFactory loggerFactory,
             IDatabaseAsync cache,
-            LastfmClient client = null, 
+            LastfmClient client = null,
             LastFmCredentials creds = null)
         {
             LoggerFactory = loggerFactory;
@@ -28,7 +25,8 @@ namespace Selector.Cache
             Creds = creds;
         }
 
-        public Task<IPlayerConsumer> Get(LastfmClient fmClient = null, LastFmCredentials creds = null, IPlayerWatcher watcher = null)
+        public Task<ISpotifyPlayerConsumer> Get(LastfmClient fmClient = null, LastFmCredentials creds = null,
+            ISpotifyPlayerWatcher watcher = null)
         {
             var client = fmClient ?? Client;
 
@@ -37,7 +35,7 @@ namespace Selector.Cache
                 throw new ArgumentNullException("No Last.fm client provided");
             }
 
-            return Task.FromResult<IPlayerConsumer>(new PlayCounterCaching(
+            return Task.FromResult<ISpotifyPlayerConsumer>(new PlayCounterCaching(
                 watcher,
                 client.Track,
                 client.Album,
@@ -49,4 +47,4 @@ namespace Selector.Cache
             ));
         }
     }
-}
+}
\ No newline at end of file
diff --git a/Selector.Cache/Consumer/Factory/PublisherFactory.cs b/Selector.Cache/Consumer/Factory/PublisherFactory.cs
index ee775bd..2f0c98d 100644
--- a/Selector.Cache/Consumer/Factory/PublisherFactory.cs
+++ b/Selector.Cache/Consumer/Factory/PublisherFactory.cs
@@ -1,37 +1,47 @@
-using System;
-using System.Collections.Generic;
-using System.Text;
-using System.Threading.Tasks;
+using System.Threading.Tasks;
 using Microsoft.Extensions.Logging;
-
+using Selector.AppleMusic.Watcher.Consumer;
+using Selector.Cache.Consumer.AppleMusic;
 using StackExchange.Redis;
 
 namespace Selector.Cache
 {
-    public interface IPublisherFactory {
-        public Task<IPlayerConsumer> Get(IPlayerWatcher watcher = null);
+    public interface IPublisherFactory
+    {
+        public Task<ISpotifyPlayerConsumer> GetSpotify(ISpotifyPlayerWatcher watcher = null);
+        public Task<IApplePlayerConsumer> GetApple(IAppleMusicPlayerWatcher watcher = null);
     }
 
-    public class PublisherFactory: IPublisherFactory {
-
+    public class PublisherFactory : IPublisherFactory
+    {
         private readonly ILoggerFactory LoggerFactory;
         private readonly ISubscriber Subscriber;
 
         public PublisherFactory(
-            ISubscriber subscriber, 
+            ISubscriber subscriber,
             ILoggerFactory loggerFactory
-        ) {
+        )
+        {
             Subscriber = subscriber;
             LoggerFactory = loggerFactory;
         }
 
-        public Task<IPlayerConsumer> Get(IPlayerWatcher watcher = null)
+        public Task<ISpotifyPlayerConsumer> GetSpotify(ISpotifyPlayerWatcher watcher = null)
         {
-            return Task.FromResult<IPlayerConsumer>(new Publisher(
+            return Task.FromResult<ISpotifyPlayerConsumer>(new SpotifyPublisher(
                 watcher,
                 Subscriber,
-                LoggerFactory.CreateLogger<Publisher>()
+                LoggerFactory.CreateLogger<SpotifyPublisher>()
+            ));
+        }
+
+        public Task<IApplePlayerConsumer> GetApple(IAppleMusicPlayerWatcher watcher = null)
+        {
+            return Task.FromResult<IApplePlayerConsumer>(new ApplePublisher(
+                watcher,
+                Subscriber,
+                LoggerFactory.CreateLogger<ApplePublisher>()
             ));
         }
     }
-}
+}
\ No newline at end of file
diff --git a/Selector.Cache/Consumer/PlayCounterCaching.cs b/Selector.Cache/Consumer/PlayCounterCaching.cs
index 738875f..84dbf7e 100644
--- a/Selector.Cache/Consumer/PlayCounterCaching.cs
+++ b/Selector.Cache/Consumer/PlayCounterCaching.cs
@@ -1,25 +1,20 @@
 using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
-using System.Text.Json;
 using System.Threading;
 using System.Threading.Tasks;
-using Microsoft.Extensions.Logging;
-
 using IF.Lastfm.Core.Api;
-using StackExchange.Redis;
+using Microsoft.Extensions.Logging;
 using SpotifyAPI.Web;
+using StackExchange.Redis;
 
 namespace Selector.Cache
 {
-    public class PlayCounterCaching: PlayCounter
+    public class PlayCounterCaching : PlayCounter
     {
         private readonly IDatabaseAsync Db;
         public TimeSpan CacheExpiry { get; set; } = TimeSpan.FromDays(1);
 
         public PlayCounterCaching(
-            IPlayerWatcher watcher,
+            ISpotifyPlayerWatcher watcher,
             ITrackApi trackClient,
             IAlbumApi albumClient,
             IArtistApi artistClient,
@@ -37,7 +32,8 @@ namespace Selector.Cache
 
         public void CacheCallback(object sender, PlayCount e)
         {
-            Task.Run(async () => {
+            Task.Run(async () =>
+            {
                 try
                 {
                     await AsyncCacheCallback(e);
@@ -56,9 +52,12 @@ namespace Selector.Cache
 
             var tasks = new Task[]
             {
-                Db.StringSetAsync(Key.TrackPlayCount(e.Username, track.Name, track.Artists[0].Name), e.Track, expiry: CacheExpiry),
-                Db.StringSetAsync(Key.AlbumPlayCount(e.Username, track.Album.Name, track.Album.Artists[0].Name), e.Album, expiry: CacheExpiry),
-                Db.StringSetAsync(Key.ArtistPlayCount(e.Username, track.Artists[0].Name), e.Artist, expiry: CacheExpiry),
+                Db.StringSetAsync(Key.TrackPlayCount(e.Username, track.Name, track.Artists[0].Name), e.Track,
+                    expiry: CacheExpiry),
+                Db.StringSetAsync(Key.AlbumPlayCount(e.Username, track.Album.Name, track.Album.Artists[0].Name),
+                    e.Album, expiry: CacheExpiry),
+                Db.StringSetAsync(Key.ArtistPlayCount(e.Username, track.Artists[0].Name), e.Artist,
+                    expiry: CacheExpiry),
                 Db.StringSetAsync(Key.UserPlayCount(e.Username), e.User, expiry: CacheExpiry),
             };
 
@@ -67,4 +66,4 @@ namespace Selector.Cache
             Logger.LogDebug("Cached play count for [{track}]", track.DisplayString());
         }
     }
-}
+}
\ No newline at end of file
diff --git a/Selector.Cache/Consumer/AudioInjectorCaching.cs b/Selector.Cache/Consumer/Spotify/AudioInjectorCaching.cs
similarity index 87%
rename from Selector.Cache/Consumer/AudioInjectorCaching.cs
rename to Selector.Cache/Consumer/Spotify/AudioInjectorCaching.cs
index 685dee7..2284c48 100644
--- a/Selector.Cache/Consumer/AudioInjectorCaching.cs
+++ b/Selector.Cache/Consumer/Spotify/AudioInjectorCaching.cs
@@ -1,10 +1,8 @@
 using System;
-using System.Collections.Generic;
 using System.Text.Json;
 using System.Threading;
 using System.Threading.Tasks;
 using Microsoft.Extensions.Logging;
-
 using SpotifyAPI.Web;
 using StackExchange.Redis;
 
@@ -16,13 +14,13 @@ namespace Selector.Cache
         public TimeSpan CacheExpiry { get; set; } = TimeSpan.FromDays(14);
 
         public CachingAudioFeatureInjector(
-            IPlayerWatcher watcher,
+            ISpotifyPlayerWatcher watcher,
             IDatabaseAsync db,
             ITracksClient trackClient,
             ILogger<CachingAudioFeatureInjector> logger = null,
             CancellationToken token = default
-        ) : base(watcher, trackClient, logger, token) {
-
+        ) : base(watcher, trackClient, logger, token)
+        {
             Db = db;
 
             NewFeature += CacheCallback;
@@ -46,12 +44,13 @@ namespace Selector.Cache
         public async Task AsyncCacheCallback(AnalysedTrack e)
         {
             var payload = JsonSerializer.Serialize(e.Features, JsonContext.Default.TrackAudioFeatures);
-            
+
             Logger.LogTrace("Caching current for [{track}]", e.Track.DisplayString());
 
             var resp = await Db.StringSetAsync(Key.AudioFeature(e.Track.Id), payload, expiry: CacheExpiry);
 
-            Logger.LogDebug("Cached audio feature for [{track}], {state}", e.Track.DisplayString(), (resp ? "value set" : "value NOT set"));
+            Logger.LogDebug("Cached audio feature for [{track}], {state}", e.Track.DisplayString(),
+                (resp ? "value set" : "value NOT set"));
         }
     }
-}
+}
\ No newline at end of file
diff --git a/Selector.Cache/Consumer/CacheWriterConsumer.cs b/Selector.Cache/Consumer/Spotify/CacheWriterConsumer.cs
similarity index 83%
rename from Selector.Cache/Consumer/CacheWriterConsumer.cs
rename to Selector.Cache/Consumer/Spotify/CacheWriterConsumer.cs
index e8c0a05..a3ec7a0 100644
--- a/Selector.Cache/Consumer/CacheWriterConsumer.cs
+++ b/Selector.Cache/Consumer/Spotify/CacheWriterConsumer.cs
@@ -1,18 +1,16 @@
 using System;
-using System.Collections.Generic;
 using System.Text.Json;
 using System.Threading;
 using System.Threading.Tasks;
 using Microsoft.Extensions.Logging;
 using Microsoft.Extensions.Logging.Abstractions;
-
 using StackExchange.Redis;
 
 namespace Selector.Cache
 {
-    public class CacheWriter : IPlayerConsumer
+    public class CacheWriter : ISpotifyPlayerConsumer
     {
-        private readonly IPlayerWatcher Watcher;
+        private readonly ISpotifyPlayerWatcher Watcher;
         private readonly IDatabaseAsync Db;
         private readonly ILogger<CacheWriter> Logger;
         public TimeSpan CacheExpiry { get; set; } = TimeSpan.FromMinutes(20);
@@ -20,11 +18,12 @@ namespace Selector.Cache
         public CancellationToken CancelToken { get; set; }
 
         public CacheWriter(
-            IPlayerWatcher watcher,
+            ISpotifyPlayerWatcher watcher,
             IDatabaseAsync db,
             ILogger<CacheWriter> logger = null,
             CancellationToken token = default
-        ){
+        )
+        {
             Watcher = watcher;
             Db = db;
             Logger = logger ?? NullLogger<CacheWriter>.Instance;
@@ -34,8 +33,9 @@ namespace Selector.Cache
         public void Callback(object sender, ListeningChangeEventArgs e)
         {
             if (e.Current is null) return;
-            
-            Task.Run(async () => {
+
+            Task.Run(async () =>
+            {
                 try
                 {
                     await AsyncCallback(e);
@@ -44,7 +44,6 @@ namespace Selector.Cache
                 {
                     Logger.LogError(e, "Error occured during callback");
                 }
-            
             }, CancelToken);
         }
 
@@ -52,24 +51,23 @@ namespace Selector.Cache
         {
             using var scope = Logger.GetListeningEventArgsScope(e);
 
-            var payload = JsonSerializer.Serialize((CurrentlyPlayingDTO) e, JsonContext.Default.CurrentlyPlayingDTO);
-            
+            var payload = JsonSerializer.Serialize((CurrentlyPlayingDTO)e, JsonContext.Default.CurrentlyPlayingDTO);
+
             Logger.LogTrace("Caching current");
 
-            var resp = await Db.StringSetAsync(Key.CurrentlyPlaying(e.Id), payload, expiry: CacheExpiry);
+            var resp = await Db.StringSetAsync(Key.CurrentlyPlayingSpotify(e.Id), payload, expiry: CacheExpiry);
 
             Logger.LogDebug("Cached current, {state}", (resp ? "value set" : "value NOT set"));
-
         }
 
         public void Subscribe(IWatcher watch = null)
         {
             var watcher = watch ?? Watcher ?? throw new ArgumentNullException("No watcher provided");
 
-            if (watcher is IPlayerWatcher watcherCast)
+            if (watcher is ISpotifyPlayerWatcher watcherCast)
             {
                 watcherCast.ItemChange += Callback;
-            } 
+            }
             else
             {
                 throw new ArgumentException("Provided watcher is not a PlayerWatcher");
@@ -80,7 +78,7 @@ namespace Selector.Cache
         {
             var watcher = watch ?? Watcher ?? throw new ArgumentNullException("No watcher provided");
 
-            if (watcher is IPlayerWatcher watcherCast)
+            if (watcher is ISpotifyPlayerWatcher watcherCast)
             {
                 watcherCast.ItemChange -= Callback;
             }
@@ -90,4 +88,4 @@ namespace Selector.Cache
             }
         }
     }
-}
+}
\ No newline at end of file
diff --git a/Selector.Cache/Consumer/PublisherConsumer.cs b/Selector.Cache/Consumer/Spotify/PublisherConsumer.cs
similarity index 76%
rename from Selector.Cache/Consumer/PublisherConsumer.cs
rename to Selector.Cache/Consumer/Spotify/PublisherConsumer.cs
index da66b46..55a640d 100644
--- a/Selector.Cache/Consumer/PublisherConsumer.cs
+++ b/Selector.Cache/Consumer/Spotify/PublisherConsumer.cs
@@ -1,32 +1,31 @@
 using System;
-using System.Collections.Generic;
 using System.Text.Json;
 using System.Threading;
 using System.Threading.Tasks;
 using Microsoft.Extensions.Logging;
 using Microsoft.Extensions.Logging.Abstractions;
-
 using StackExchange.Redis;
 
 namespace Selector.Cache
 {
-    public class Publisher : IPlayerConsumer
+    public class SpotifyPublisher : ISpotifyPlayerConsumer
     {
-        private readonly IPlayerWatcher Watcher;
+        private readonly ISpotifyPlayerWatcher Watcher;
         private readonly ISubscriber Subscriber;
-        private readonly ILogger<Publisher> Logger;
+        private readonly ILogger<SpotifyPublisher> Logger;
 
         public CancellationToken CancelToken { get; set; }
 
-        public Publisher(
-            IPlayerWatcher watcher,
+        public SpotifyPublisher(
+            ISpotifyPlayerWatcher watcher,
             ISubscriber subscriber,
-            ILogger<Publisher> logger = null,
+            ILogger<SpotifyPublisher> logger = null,
             CancellationToken token = default
-        ){
+        )
+        {
             Watcher = watcher;
             Subscriber = subscriber;
-            Logger = logger ?? NullLogger<Publisher>.Instance;
+            Logger = logger ?? NullLogger<SpotifyPublisher>.Instance;
             CancelToken = token;
         }
 
@@ -34,7 +33,8 @@ namespace Selector.Cache
         {
             if (e.Current is null) return;
 
-            Task.Run(async () => {
+            Task.Run(async () =>
+            {
                 try
                 {
                     await AsyncCallback(e);
@@ -50,12 +50,12 @@ namespace Selector.Cache
         {
             using var scope = Logger.GetListeningEventArgsScope(e);
 
-            var payload = JsonSerializer.Serialize((CurrentlyPlayingDTO) e, JsonContext.Default.CurrentlyPlayingDTO);
+            var payload = JsonSerializer.Serialize((CurrentlyPlayingDTO)e, JsonContext.Default.CurrentlyPlayingDTO);
 
             Logger.LogTrace("Publishing current");
-            
+
             // TODO: currently using spotify username for cache key, use db username
-            var receivers = await Subscriber.PublishAsync(Key.CurrentlyPlaying(e.Id), payload);
+            var receivers = await Subscriber.PublishAsync(Key.CurrentlyPlayingSpotify(e.Id), payload);
 
             Logger.LogDebug("Published current, {receivers} receivers", receivers);
         }
@@ -64,10 +64,10 @@ namespace Selector.Cache
         {
             var watcher = watch ?? Watcher ?? throw new ArgumentNullException("No watcher provided");
 
-            if (watcher is IPlayerWatcher watcherCast)
+            if (watcher is ISpotifyPlayerWatcher watcherCast)
             {
                 watcherCast.ItemChange += Callback;
-            } 
+            }
             else
             {
                 throw new ArgumentException("Provided watcher is not a PlayerWatcher");
@@ -78,7 +78,7 @@ namespace Selector.Cache
         {
             var watcher = watch ?? Watcher ?? throw new ArgumentNullException("No watcher provided");
 
-            if (watcher is IPlayerWatcher watcherCast)
+            if (watcher is ISpotifyPlayerWatcher watcherCast)
             {
                 watcherCast.ItemChange -= Callback;
             }
@@ -88,4 +88,4 @@ namespace Selector.Cache
             }
         }
     }
-}
+}
\ No newline at end of file
diff --git a/Selector.Cache/Key.cs b/Selector.Cache/Key.cs
index a9d814e..47be354 100644
--- a/Selector.Cache/Key.cs
+++ b/Selector.Cache/Key.cs
@@ -1,7 +1,4 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
+using System.Linq;
 
 namespace Selector.Cache
 {
@@ -33,25 +30,47 @@ namespace Selector.Cache
         /// </summary>
         /// <param name="user">User's database Id (Guid)</param>
         /// <returns></returns>
-        public static string CurrentlyPlaying(string user) => MajorNamespace(MinorNamespace(UserName, CurrentlyPlayingName), user);
-        public static readonly string AllCurrentlyPlaying = CurrentlyPlaying(All);
+        public static string CurrentlyPlayingSpotify(string user) =>
+            MajorNamespace(MinorNamespace(UserName, SpotifyName, CurrentlyPlayingName), user);
+
+        public static string CurrentlyPlayingAppleMusic(string user) =>
+            MajorNamespace(MinorNamespace(UserName, AppleMusicName, CurrentlyPlayingName), user);
+
+        public static readonly string AllCurrentlyPlayingSpotify = CurrentlyPlayingSpotify(All);
+        public static readonly string AllCurrentlyPlayingApple = CurrentlyPlayingAppleMusic(All);
 
         public static string Track(string trackId) => MajorNamespace(TrackName, trackId);
         public static readonly string AllTracks = Track(All);
 
-        public static string AudioFeature(string trackId) => MajorNamespace(MinorNamespace(TrackName, AudioFeatureName), trackId);
+        public static string AudioFeature(string trackId) =>
+            MajorNamespace(MinorNamespace(TrackName, AudioFeatureName), trackId);
+
         public static readonly string AllAudioFeatures = AudioFeature(All);
 
-        public static string TrackPlayCount(string username, string name, string artist) => MajorNamespace(MinorNamespace(TrackName, PlayCountName), artist, name, username);
-        public static string AlbumPlayCount(string username, string name, string artist) => MajorNamespace(MinorNamespace(AlbumName, PlayCountName), artist, name, username);
-        public static string ArtistPlayCount(string username, string name) => MajorNamespace(MinorNamespace(ArtistName, PlayCountName), name, username);
-        public static string UserPlayCount(string username) => MajorNamespace(MinorNamespace(UserName, PlayCountName), username);
+        public static string TrackPlayCount(string username, string name, string artist) =>
+            MajorNamespace(MinorNamespace(TrackName, PlayCountName), artist, name, username);
+
+        public static string AlbumPlayCount(string username, string name, string artist) =>
+            MajorNamespace(MinorNamespace(AlbumName, PlayCountName), artist, name, username);
+
+        public static string ArtistPlayCount(string username, string name) =>
+            MajorNamespace(MinorNamespace(ArtistName, PlayCountName), name, username);
+
+        public static string UserPlayCount(string username) =>
+            MajorNamespace(MinorNamespace(UserName, PlayCountName), username);
+
+        public static string UserSpotify(string username) =>
+            MajorNamespace(MinorNamespace(UserName, SpotifyName), username);
+
+        public static string UserAppleMusic(string username) =>
+            MajorNamespace(MinorNamespace(UserName, AppleMusicName), username);
 
-        public static string UserSpotify(string username) => MajorNamespace(MinorNamespace(UserName, SpotifyName), username);
-        public static string UserAppleMusic(string username) => MajorNamespace(MinorNamespace(UserName, AppleMusicName), username);
         public static readonly string AllUserSpotify = UserSpotify(All);
         public static readonly string AllUserAppleMusic = UserAppleMusic(All);
-        public static string UserLastfm(string username) => MajorNamespace(MinorNamespace(UserName, LastfmName), username);
+
+        public static string UserLastfm(string username) =>
+            MajorNamespace(MinorNamespace(UserName, LastfmName), username);
+
         public static readonly string AllUserLastfm = UserLastfm(All);
 
         public static string Watcher(int id) => MajorNamespace(WatcherName, id.ToString());
@@ -66,14 +85,17 @@ namespace Selector.Cache
         public static string[] UnNamespace(string key, params char[] args) => key.Split(args);
 
         public static string Param(string key) => UnMajorNamespace(key).Skip(1).First();
-        public static (string, string) ParamPair(string key) {
+
+        public static (string, string) ParamPair(string key)
+        {
             var split = UnMajorNamespace(key);
             return (split[1], split[2]);
         }
+
         public static (string, string, string) ParamTriplet(string key)
         {
             var split = UnMajorNamespace(key);
             return (split[1], split[2], split[3]);
         }
     }
-}
+}
\ No newline at end of file
diff --git a/Selector.Cache/Selector.Cache.csproj b/Selector.Cache/Selector.Cache.csproj
index 28bf6d6..3a03083 100644
--- a/Selector.Cache/Selector.Cache.csproj
+++ b/Selector.Cache/Selector.Cache.csproj
@@ -13,6 +13,7 @@
   </ItemGroup>
 
   <ItemGroup>
+    <ProjectReference Include="..\Selector.AppleMusic\Selector.AppleMusic.csproj"/>
     <ProjectReference Include="..\Selector\Selector.csproj" />
   </ItemGroup>
 
diff --git a/Selector.Event/CacheMappings/NowPlayingMapping.cs b/Selector.Event/CacheMappings/NowPlayingMapping.cs
index 73181b2..64271e6 100644
--- a/Selector.Event/CacheMappings/NowPlayingMapping.cs
+++ b/Selector.Event/CacheMappings/NowPlayingMapping.cs
@@ -1,9 +1,8 @@
 using System.Text.Json;
 using Microsoft.Extensions.Logging;
-
-using StackExchange.Redis;
-
+using Selector.AppleMusic;
 using Selector.Cache;
+using StackExchange.Redis;
 
 namespace Selector.Events
 {
@@ -28,20 +27,39 @@ namespace Selector.Events
             {
                 Logger.LogDebug("Forming now playing event mapping between cache and event bus");
 
-                (await Subscriber.SubscribeAsync(Key.AllCurrentlyPlaying)).OnMessage(message => {
-
+                (await Subscriber.SubscribeAsync(Key.AllCurrentlyPlayingSpotify)).OnMessage(message =>
+                {
                     try
                     {
                         var userId = Key.Param(message.Channel);
 
-                        var deserialised = JsonSerializer.Deserialize(message.Message, JsonContext.Default.CurrentlyPlayingDTO);
-                        Logger.LogDebug("Received new currently playing [{username}]", deserialised.Username);
+                        var deserialised =
+                            JsonSerializer.Deserialize(message.Message, JsonContext.Default.CurrentlyPlayingDTO);
+                        Logger.LogDebug("Received new Spotify currently playing [{username}]", deserialised.Username);
 
-                        UserEvent.OnCurrentlyPlayingChange(this, deserialised);
+                        UserEvent.OnCurrentlyPlayingChangeSpotify(this, deserialised);
                     }
                     catch (Exception e)
                     {
-                        Logger.LogError(e, "Error parsing new currently playing [{message}]", message);
+                        Logger.LogError(e, "Error parsing new Spotify currently playing [{message}]", message);
+                    }
+                });
+
+                (await Subscriber.SubscribeAsync(Key.AllCurrentlyPlayingApple)).OnMessage(message =>
+                {
+                    try
+                    {
+                        var userId = Key.Param(message.Channel);
+
+                        var deserialised = JsonSerializer.Deserialize(message.Message,
+                            AppleJsonContext.Default.AppleListeningChangeEventArgs);
+                        Logger.LogDebug("Received new Apple Music currently playing");
+
+                        UserEvent.OnCurrentlyPlayingChangeApple(this, deserialised);
+                    }
+                    catch (Exception e)
+                    {
+                        Logger.LogError(e, "Error parsing new Apple Music currently playing [{message}]", message);
                     }
                 });
             }
@@ -69,10 +87,16 @@ namespace Selector.Events
             {
                 Logger.LogDebug("Forming now playing event mapping TO cache FROM event bus");
 
-                UserEvent.CurrentlyPlaying += async (o, e) =>
+                UserEvent.CurrentlyPlayingSpotify += async (o, e) =>
                 {
                     var payload = JsonSerializer.Serialize(e, JsonContext.Default.CurrentlyPlayingDTO);
-                    await Subscriber.PublishAsync(Key.CurrentlyPlaying(e.UserId), payload);
+                    await Subscriber.PublishAsync(Key.CurrentlyPlayingSpotify(e.UserId), payload);
+                };
+
+                UserEvent.CurrentlyPlayingApple += async (o, e) =>
+                {
+                    var payload = JsonSerializer.Serialize(e, AppleJsonContext.Default.AppleListeningChangeEventArgs);
+                    await Subscriber.PublishAsync(Key.CurrentlyPlayingAppleMusic(e.Id), payload);
                 };
 
                 return Task.CompletedTask;
diff --git a/Selector.Event/Consumers/AppleUserEventFirer.cs b/Selector.Event/Consumers/AppleUserEventFirer.cs
new file mode 100644
index 0000000..4c861a0
--- /dev/null
+++ b/Selector.Event/Consumers/AppleUserEventFirer.cs
@@ -0,0 +1,84 @@
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Logging.Abstractions;
+using Selector.AppleMusic;
+using Selector.AppleMusic.Watcher.Consumer;
+
+namespace Selector.Events
+{
+    public class AppleUserEventFirer : IApplePlayerConsumer
+    {
+        protected readonly IAppleMusicPlayerWatcher Watcher;
+        protected readonly ILogger<AppleUserEventFirer> Logger;
+
+        protected readonly UserEventBus UserEvent;
+
+        public CancellationToken CancelToken { get; set; }
+
+        public AppleUserEventFirer(
+            IAppleMusicPlayerWatcher watcher,
+            UserEventBus userEvent,
+            ILogger<AppleUserEventFirer> logger = null,
+            CancellationToken token = default
+        )
+        {
+            Watcher = watcher;
+            UserEvent = userEvent;
+            Logger = logger ?? NullLogger<AppleUserEventFirer>.Instance;
+            CancelToken = token;
+        }
+
+        public void Callback(object sender, AppleListeningChangeEventArgs e)
+        {
+            if (e.Current is null) return;
+
+            Task.Run(async () =>
+            {
+                try
+                {
+                    await AsyncCallback(e);
+                }
+                catch (Exception e)
+                {
+                    Logger.LogError(e, "Error occured during callback");
+                }
+            }, CancelToken);
+        }
+
+        public Task AsyncCallback(AppleListeningChangeEventArgs e)
+        {
+            Logger.LogDebug("Firing Apple now playing event on user bus [{userId}]", e.Id);
+
+            UserEvent.OnCurrentlyPlayingChangeApple(this, e);
+
+            return Task.CompletedTask;
+        }
+
+        public void Subscribe(IWatcher watch = null)
+        {
+            var watcher = watch ?? Watcher ?? throw new ArgumentNullException("No watcher provided");
+
+            if (watcher is IAppleMusicPlayerWatcher 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 IAppleMusicPlayerWatcher watcherCast)
+            {
+                watcherCast.ItemChange -= Callback;
+            }
+            else
+            {
+                throw new ArgumentException("Provided watcher is not a PlayerWatcher");
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/Selector.Event/Consumers/UserEventFirer.cs b/Selector.Event/Consumers/SpotifyUserEventFirer.cs
similarity index 68%
rename from Selector.Event/Consumers/UserEventFirer.cs
rename to Selector.Event/Consumers/SpotifyUserEventFirer.cs
index bfcbc5a..3cbf858 100644
--- a/Selector.Event/Consumers/UserEventFirer.cs
+++ b/Selector.Event/Consumers/SpotifyUserEventFirer.cs
@@ -3,33 +3,34 @@ using Microsoft.Extensions.Logging.Abstractions;
 
 namespace Selector.Events
 {
-    public class UserEventFirer : IPlayerConsumer
+    public class SpotifyUserEventFirer : ISpotifyPlayerConsumer
     {
-        protected readonly IPlayerWatcher Watcher;
-        protected readonly ILogger<UserEventFirer> Logger;
+        protected readonly ISpotifyPlayerWatcher Watcher;
+        protected readonly ILogger<SpotifyUserEventFirer> Logger;
 
         protected readonly UserEventBus UserEvent;
 
         public CancellationToken CancelToken { get; set; }
 
-        public UserEventFirer(
-            IPlayerWatcher watcher,
+        public SpotifyUserEventFirer(
+            ISpotifyPlayerWatcher watcher,
             UserEventBus userEvent,
-            ILogger<UserEventFirer> logger = null,
+            ILogger<SpotifyUserEventFirer> logger = null,
             CancellationToken token = default
         )
         {
             Watcher = watcher;
             UserEvent = userEvent;
-            Logger = logger ?? NullLogger<UserEventFirer>.Instance;
+            Logger = logger ?? NullLogger<SpotifyUserEventFirer>.Instance;
             CancelToken = token;
         }
 
         public void Callback(object sender, ListeningChangeEventArgs e)
         {
             if (e.Current is null) return;
-            
-            Task.Run(async () => {
+
+            Task.Run(async () =>
+            {
                 try
                 {
                     await AsyncCallback(e);
@@ -43,9 +44,10 @@ namespace Selector.Events
 
         public Task AsyncCallback(ListeningChangeEventArgs e)
         {
-            Logger.LogDebug("Firing now playing event on user bus [{username}/{userId}]", e.SpotifyUsername, e.Id);
+            Logger.LogDebug("Firing Spotify now playing event on user bus [{username}/{userId}]", e.SpotifyUsername,
+                e.Id);
 
-            UserEvent.OnCurrentlyPlayingChange(this, (CurrentlyPlayingDTO) e);
+            UserEvent.OnCurrentlyPlayingChangeSpotify(this, (CurrentlyPlayingDTO)e);
 
             return Task.CompletedTask;
         }
@@ -54,7 +56,7 @@ namespace Selector.Events
         {
             var watcher = watch ?? Watcher ?? throw new ArgumentNullException("No watcher provided");
 
-            if (watcher is IPlayerWatcher watcherCast)
+            if (watcher is ISpotifyPlayerWatcher watcherCast)
             {
                 watcherCast.ItemChange += Callback;
             }
@@ -68,7 +70,7 @@ namespace Selector.Events
         {
             var watcher = watch ?? Watcher ?? throw new ArgumentNullException("No watcher provided");
 
-            if (watcher is IPlayerWatcher watcherCast)
+            if (watcher is ISpotifyPlayerWatcher watcherCast)
             {
                 watcherCast.ItemChange -= Callback;
             }
@@ -78,4 +80,4 @@ namespace Selector.Events
             }
         }
     }
-}
+}
\ No newline at end of file
diff --git a/Selector.Event/Consumers/UserEventFirerFactory.cs b/Selector.Event/Consumers/UserEventFirerFactory.cs
index 7ba68c5..d2d6b5f 100644
--- a/Selector.Event/Consumers/UserEventFirerFactory.cs
+++ b/Selector.Event/Consumers/UserEventFirerFactory.cs
@@ -4,10 +4,10 @@ namespace Selector.Events
 {
     public interface IUserEventFirerFactory
     {
-        public Task<UserEventFirer> Get(IPlayerWatcher watcher = null);
+        public Task<SpotifyUserEventFirer> Get(ISpotifyPlayerWatcher watcher = null);
     }
-    
-    public class UserEventFirerFactory: IUserEventFirerFactory
+
+    public class UserEventFirerFactory : IUserEventFirerFactory
     {
         private readonly ILoggerFactory LoggerFactory;
         private readonly UserEventBus UserEvent;
@@ -18,13 +18,13 @@ namespace Selector.Events
             UserEvent = userEvent;
         }
 
-        public Task<UserEventFirer> Get(IPlayerWatcher watcher = null)
+        public Task<SpotifyUserEventFirer> Get(ISpotifyPlayerWatcher watcher = null)
         {
-            return Task.FromResult(new UserEventFirer(
+            return Task.FromResult(new SpotifyUserEventFirer(
                 watcher,
                 UserEvent,
-                LoggerFactory.CreateLogger<UserEventFirer>()
+                LoggerFactory.CreateLogger<SpotifyUserEventFirer>()
             ));
         }
     }
-}
+}
\ No newline at end of file
diff --git a/Selector.Event/Selector.Event.csproj b/Selector.Event/Selector.Event.csproj
index bdb9907..497915a 100644
--- a/Selector.Event/Selector.Event.csproj
+++ b/Selector.Event/Selector.Event.csproj
@@ -6,6 +6,7 @@
   </PropertyGroup>
 
   <ItemGroup>
+    <ProjectReference Include="..\Selector.AppleMusic\Selector.AppleMusic.csproj"/>
     <ProjectReference Include="..\Selector\Selector.csproj" />
     <ProjectReference Include="..\Selector.Model\Selector.Model.csproj" />
     <ProjectReference Include="..\Selector.Cache\Selector.Cache.csproj" />
diff --git a/Selector.Event/UserEventBus.cs b/Selector.Event/UserEventBus.cs
index 3220457..711168f 100644
--- a/Selector.Event/UserEventBus.cs
+++ b/Selector.Event/UserEventBus.cs
@@ -1,10 +1,10 @@
 using Microsoft.Extensions.Logging;
-
+using Selector.AppleMusic;
 using Selector.Model;
 
 namespace Selector.Events
 {
-    public class UserEventBus: IEventBus
+    public class UserEventBus : IEventBus
     {
         private readonly ILogger<UserEventBus> Logger;
 
@@ -13,7 +13,8 @@ namespace Selector.Events
         public event EventHandler<AppleMusicLinkChange> AppleLinkChange;
         public event EventHandler<LastfmChange> LastfmCredChange;
 
-        public event EventHandler<CurrentlyPlayingDTO> CurrentlyPlaying;
+        public event EventHandler<CurrentlyPlayingDTO> CurrentlyPlayingSpotify;
+        public event EventHandler<AppleListeningChangeEventArgs> CurrentlyPlayingApple;
 
         public UserEventBus(ILogger<UserEventBus> logger)
         {
@@ -44,10 +45,16 @@ namespace Selector.Events
             LastfmCredChange?.Invoke(sender, args);
         }
 
-        public void OnCurrentlyPlayingChange(object sender, CurrentlyPlayingDTO args)
+        public void OnCurrentlyPlayingChangeSpotify(object sender, CurrentlyPlayingDTO args)
         {
             Logger.LogTrace("Firing currently playing event [{usernamne}/{userId}]", args?.Username, args.UserId);
-            CurrentlyPlaying?.Invoke(sender, args);
+            CurrentlyPlayingSpotify?.Invoke(sender, args);
+        }
+
+        public void OnCurrentlyPlayingChangeApple(object sender, AppleListeningChangeEventArgs args)
+        {
+            Logger.LogTrace("Firing currently playing event");
+            CurrentlyPlayingApple?.Invoke(sender, args);
         }
     }
-}
+}
\ No newline at end of file
diff --git a/Selector.MAUI/Selector.MAUI.csproj b/Selector.MAUI/Selector.MAUI.csproj
index 69684f6..fee0cc9 100644
--- a/Selector.MAUI/Selector.MAUI.csproj
+++ b/Selector.MAUI/Selector.MAUI.csproj
@@ -64,6 +64,8 @@
     </PropertyGroup>
     <PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
       <CodesignKey>iPhone Developer</CodesignKey>
+        <MtouchDebug>true</MtouchDebug>
+        <IOSDebugOverWiFi>true</IOSDebugOverWiFi>
     </PropertyGroup>
     <PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
       <CodesignKey>iPhone Developer</CodesignKey>
diff --git a/Selector.Model/ApplicationDbContext.cs b/Selector.Model/ApplicationDbContext.cs
index 6754078..94b2f21 100644
--- a/Selector.Model/ApplicationDbContext.cs
+++ b/Selector.Model/ApplicationDbContext.cs
@@ -1,19 +1,14 @@
-
-using System;
 using System.IO;
 using System.Linq;
-using System.Threading.Tasks;
+using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
 using Microsoft.EntityFrameworkCore;
 using Microsoft.EntityFrameworkCore.Design;
 using Microsoft.Extensions.Configuration;
-
-using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
 using Microsoft.Extensions.Logging;
 using Microsoft.Extensions.Logging.Abstractions;
 
 namespace Selector.Model
 {
-
     public class ApplicationDbContext : IdentityDbContext<ApplicationUser>
     {
         private readonly ILogger<ApplicationDbContext> Logger;
@@ -27,7 +22,7 @@ namespace Selector.Model
         public DbSet<SpotifyListen> SpotifyListen { get; set; }
 
         public ApplicationDbContext(
-            DbContextOptions<ApplicationDbContext> options, 
+            DbContextOptions<ApplicationDbContext> options,
             ILogger<ApplicationDbContext> logger
         ) : base(options)
         {
@@ -36,14 +31,14 @@ namespace Selector.Model
 
         protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
         {
-            
         }
 
         protected override void OnModelCreating(ModelBuilder modelBuilder)
         {
             base.OnModelCreating(modelBuilder);
 
-            modelBuilder.HasCollation("case_insensitive", locale: "en-u-ks-primary", provider: "icu", deterministic: false);
+            modelBuilder.HasCollation("case_insensitive", locale: "en-u-ks-primary", provider: "icu",
+                deterministic: false);
 
             modelBuilder.Entity<ApplicationUser>()
                 .Property(u => u.SpotifyIsLinked)
@@ -112,15 +107,16 @@ namespace Selector.Model
 
         public void CreatePlayerWatcher(string userId)
         {
-            if(Watcher.Any(w => w.UserId == userId && w.Type == WatcherType.Player))
+            if (Watcher.Any(w => w.UserId == userId && w.Type == WatcherType.SpotifyPlayer))
             {
                 Logger.LogWarning("Trying to create more than one player watcher for user [{id}]", userId);
                 return;
             }
 
-            Watcher.Add(new Watcher {
+            Watcher.Add(new Watcher
+            {
                 UserId = userId,
-                Type = WatcherType.Player
+                Type = WatcherType.SpotifyPlayer
             });
 
             SaveChanges();
@@ -129,17 +125,18 @@ namespace Selector.Model
 
     public class DesignTimeDbContextFactory : IDesignTimeDbContextFactory<ApplicationDbContext>
     {
-        private static string GetPath(string env) => $"{@Directory.GetCurrentDirectory()}/../Selector.Web/appsettings.{env}.json";
+        private static string GetPath(string env) =>
+            $"{@Directory.GetCurrentDirectory()}/../Selector.Web/appsettings.{env}.json";
 
         public ApplicationDbContext CreateDbContext(string[] args)
         {
             string configFile;
 
-            if(File.Exists(GetPath("Development")))
+            if (File.Exists(GetPath("Development")))
             {
                 configFile = GetPath("Development");
             }
-            else if(File.Exists(GetPath("Production")))
+            else if (File.Exists(GetPath("Production")))
             {
                 configFile = GetPath("Production");
             }
@@ -155,7 +152,7 @@ namespace Selector.Model
 
             var builder = new DbContextOptionsBuilder<ApplicationDbContext>();
             builder.UseNpgsql(configuration.GetConnectionString("Default"));
-            
+
             return new ApplicationDbContext(builder.Options, NullLogger<ApplicationDbContext>.Instance);
         }
     }
diff --git a/Selector.Tests/Apple/AppleTimelineTests.cs b/Selector.Tests/Apple/AppleTimelineTests.cs
new file mode 100644
index 0000000..efa61aa
--- /dev/null
+++ b/Selector.Tests/Apple/AppleTimelineTests.cs
@@ -0,0 +1,194 @@
+using System.Collections.Generic;
+using System.Linq;
+using FluentAssertions;
+using Selector.AppleMusic;
+using Selector.AppleMusic.Watcher;
+using Xunit;
+
+namespace Selector.Tests.Apple;
+
+public class AppleTimelineTests
+{
+    public static IEnumerable<object[]> MatchingData =>
+        new List<object[]>
+        {
+            new object[]
+            {
+                new List<List<AppleMusicCurrentlyPlayingContext>>
+                {
+                    new()
+                    {
+                        Helper.AppleContext("1"),
+                        Helper.AppleContext("2"),
+                        Helper.AppleContext("3"),
+                    }
+                },
+                new List<string>
+                {
+                    "1", "2", "3"
+                },
+                new List<List<string>>
+                {
+                    new()
+                    {
+                        // "1", "2", "3"
+                    }
+                }
+            },
+            new object[]
+            {
+                new List<List<AppleMusicCurrentlyPlayingContext>>
+                {
+                    new()
+                    {
+                        Helper.AppleContext("1"),
+                        Helper.AppleContext("2"),
+                        Helper.AppleContext("3"),
+                    },
+                    new()
+                    {
+                        Helper.AppleContext("3"),
+                        Helper.AppleContext("4"),
+                        Helper.AppleContext("5"),
+                    }
+                },
+                new List<string>
+                {
+                    "1", "2", "3", "4", "5"
+                },
+                new List<List<string>>
+                {
+                    new()
+                    {
+                        // "1", "2", "3"
+                    },
+                    new()
+                    {
+                        "4", "5",
+                    }
+                }
+            },
+            new object[]
+            {
+                new List<List<AppleMusicCurrentlyPlayingContext>>
+                {
+                    new()
+                    {
+                        Helper.AppleContext("1"),
+                        Helper.AppleContext("2"),
+                        Helper.AppleContext("3"),
+                    },
+                    new()
+                    {
+                        Helper.AppleContext("3"),
+                        Helper.AppleContext("4"),
+                        Helper.AppleContext("5"),
+                    },
+                    new()
+                    {
+                        Helper.AppleContext("3"),
+                        Helper.AppleContext("4"),
+                        Helper.AppleContext("5"),
+                    },
+                    new()
+                    {
+                        Helper.AppleContext("5"),
+                        Helper.AppleContext("6"),
+                        Helper.AppleContext("7"),
+                    }
+                },
+                new List<string>
+                {
+                    "1", "2", "3", "4", "5", "6", "7"
+                },
+                new List<List<string>>
+                {
+                    new()
+                    {
+                        // "1", "2", "3"
+                    },
+                    new()
+                    {
+                        "4", "5",
+                    },
+                    new()
+                    {
+                    },
+                    new()
+                    {
+                        "6", "7",
+                    }
+                }
+            },
+            new object[]
+            {
+                new List<List<AppleMusicCurrentlyPlayingContext>>
+                {
+                    new()
+                    {
+                        Helper.AppleContext("1"),
+                        Helper.AppleContext("2"),
+                        Helper.AppleContext("3"),
+                    },
+                    new()
+                    {
+                        Helper.AppleContext("3"),
+                        Helper.AppleContext("4"),
+                        Helper.AppleContext("5"),
+                    },
+                    new()
+                    {
+                        Helper.AppleContext("3"),
+                        Helper.AppleContext("4"),
+                        Helper.AppleContext("5"),
+                    },
+                    new()
+                    {
+                        Helper.AppleContext("1"),
+                        Helper.AppleContext("2"),
+                        Helper.AppleContext("3"),
+                    }
+                },
+                new List<string>
+                {
+                    "1", "2", "3", "4", "5"
+                },
+                new List<List<string>>
+                {
+                    new()
+                    {
+                        // "1", "2", "3"
+                    },
+                    new()
+                    {
+                        "4", "5",
+                    },
+                    new()
+                    {
+                    },
+                    new()
+                    {
+                    }
+                }
+            }
+        };
+
+    [Theory]
+    [MemberData(nameof(MatchingData))]
+    public void Matching(List<List<AppleMusicCurrentlyPlayingContext>> currentlyPlaying, List<string> expectedContent,
+        List<List<string>> expectedResult)
+    {
+        var timeline = new AppleTimeline();
+
+        foreach (var (batch, expectedReturn) in currentlyPlaying.Zip(expectedResult))
+        {
+            var newItems = timeline.Add(batch);
+            newItems.Select(x => x.Track.Id).Should().ContainInOrder(expectedReturn);
+        }
+
+        timeline
+            .Select(x => x.Item.Track.Id)
+            .Should()
+            .ContainInOrder(expectedContent);
+    }
+}
\ No newline at end of file
diff --git a/Selector.Tests/Consumer/AudioInjector.cs b/Selector.Tests/Consumer/AudioInjector.cs
index 1e3b0f9..8b7367a 100644
--- a/Selector.Tests/Consumer/AudioInjector.cs
+++ b/Selector.Tests/Consumer/AudioInjector.cs
@@ -1,14 +1,8 @@
 using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Threading.Tasks;
-using Xunit;
-using Moq;
-using FluentAssertions;
-using SpotifyAPI.Web;
-
-using Selector;
 using System.Threading;
+using Moq;
+using SpotifyAPI.Web;
+using Xunit;
 
 namespace Selector.Tests
 {
@@ -17,7 +11,7 @@ namespace Selector.Tests
         [Fact]
         public void Subscribe()
         {
-            var watcherMock = new Mock<IPlayerWatcher>();
+            var watcherMock = new Mock<ISpotifyPlayerWatcher>();
             var spotifyMock = new Mock<ITracksClient>();
 
             var featureInjector = new AudioFeatureInjector(watcherMock.Object, spotifyMock.Object);
@@ -30,7 +24,7 @@ namespace Selector.Tests
         [Fact]
         public void Unsubscribe()
         {
-            var watcherMock = new Mock<IPlayerWatcher>();
+            var watcherMock = new Mock<ISpotifyPlayerWatcher>();
             var spotifyMock = new Mock<ITracksClient>();
 
             var featureInjector = new AudioFeatureInjector(watcherMock.Object, spotifyMock.Object);
@@ -43,8 +37,8 @@ namespace Selector.Tests
         [Fact]
         public void SubscribeFuncArg()
         {
-            var watcherMock = new Mock<IPlayerWatcher>();
-            var watcherFuncArgMock = new Mock<IPlayerWatcher>();
+            var watcherMock = new Mock<ISpotifyPlayerWatcher>();
+            var watcherFuncArgMock = new Mock<ISpotifyPlayerWatcher>();
             var spotifyMock = new Mock<ITracksClient>();
 
             var featureInjector = new AudioFeatureInjector(watcherMock.Object, spotifyMock.Object);
@@ -58,8 +52,8 @@ namespace Selector.Tests
         [Fact]
         public void UnsubscribeFuncArg()
         {
-            var watcherMock = new Mock<IPlayerWatcher>();
-            var watcherFuncArgMock = new Mock<IPlayerWatcher>();
+            var watcherMock = new Mock<ISpotifyPlayerWatcher>();
+            var watcherFuncArgMock = new Mock<ISpotifyPlayerWatcher>();
             var spotifyMock = new Mock<ITracksClient>();
 
             var featureInjector = new AudioFeatureInjector(watcherMock.Object, spotifyMock.Object);
@@ -73,7 +67,7 @@ namespace Selector.Tests
         [Fact]
         public async void CallbackNoId()
         {
-            var watcherMock = new Mock<IPlayerWatcher>();
+            var watcherMock = new Mock<ISpotifyPlayerWatcher>();
             var spotifyMock = new Mock<ITracksClient>();
             var timelineMock = new Mock<AnalysedTrackTimeline>();
             var eventArgsMock = new Mock<ListeningChangeEventArgs>();
@@ -84,7 +78,8 @@ namespace Selector.Tests
             eventArgsMock.Object.Current = playingMock.Object;
             playingMock.Object.Item = trackMock.Object;
 
-            spotifyMock.Setup(m => m.GetAudioFeatures(It.IsAny<string>(), It.IsAny<CancellationToken>()).Result).Returns(() => featureMock.Object);
+            spotifyMock.Setup(m => m.GetAudioFeatures(It.IsAny<string>(), It.IsAny<CancellationToken>()).Result)
+                .Returns(() => featureMock.Object);
 
             var featureInjector = new AudioFeatureInjector(watcherMock.Object, spotifyMock.Object)
             {
@@ -100,7 +95,7 @@ namespace Selector.Tests
         [Fact]
         public async void CallbackWithId()
         {
-            var watcherMock = new Mock<IPlayerWatcher>();
+            var watcherMock = new Mock<ISpotifyPlayerWatcher>();
             var spotifyMock = new Mock<ITracksClient>();
             var timelineMock = new Mock<AnalysedTrackTimeline>();
             var eventArgsMock = new Mock<ListeningChangeEventArgs>();
@@ -112,7 +107,8 @@ namespace Selector.Tests
             playingMock.Object.Item = trackMock.Object;
             trackMock.Object.Id = "Fake-Id";
 
-            spotifyMock.Setup(m => m.GetAudioFeatures(It.IsAny<string>(), It.IsAny<CancellationToken>()).Result).Returns(() => featureMock.Object);
+            spotifyMock.Setup(m => m.GetAudioFeatures(It.IsAny<string>(), It.IsAny<CancellationToken>()).Result)
+                .Returns(() => featureMock.Object);
 
             var featureInjector = new AudioFeatureInjector(watcherMock.Object, spotifyMock.Object)
             {
@@ -128,4 +124,4 @@ namespace Selector.Tests
             timelineMock.VerifyNoOtherCalls();
         }
     }
-}
+}
\ No newline at end of file
diff --git a/Selector.Tests/Consumer/WebHook.cs b/Selector.Tests/Consumer/WebHook.cs
index e699afd..e736714 100644
--- a/Selector.Tests/Consumer/WebHook.cs
+++ b/Selector.Tests/Consumer/WebHook.cs
@@ -1,18 +1,12 @@
 using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
+using System.Net;
+using System.Net.Http;
 using System.Threading;
 using System.Threading.Tasks;
-using System.Net.Http;
-
-using Xunit;
+using FluentAssertions;
 using Moq;
 using Moq.Protected;
-using FluentAssertions;
-using System.Net;
-
-using SpotifyAPI.Web;
+using Xunit;
 
 namespace Selector.Tests
 {
@@ -25,10 +19,11 @@ namespace Selector.Tests
 
             var httpHandlerMock = new Mock<HttpMessageHandler>();
             httpHandlerMock.Protected()
-                .Setup<Task<HttpResponseMessage>>("SendAsync", ItExpr.IsAny<HttpRequestMessage>(), ItExpr.IsAny<CancellationToken>())
+                .Setup<Task<HttpResponseMessage>>("SendAsync", ItExpr.IsAny<HttpRequestMessage>(),
+                    ItExpr.IsAny<CancellationToken>())
                 .ReturnsAsync(msg);
 
-            var watcherMock = new Mock<IPlayerWatcher>();
+            var watcherMock = new Mock<ISpotifyPlayerWatcher>();
             watcherMock.SetupAdd(w => w.ItemChange += It.IsAny<EventHandler<ListeningChangeEventArgs>>());
             watcherMock.SetupRemove(w => w.ItemChange -= It.IsAny<EventHandler<ListeningChangeEventArgs>>());
 
@@ -49,7 +44,8 @@ namespace Selector.Tests
 
             await Task.Delay(100);
 
-            httpHandlerMock.Protected().Verify<Task<HttpResponseMessage>>("SendAsync", Times.Once(), ItExpr.IsAny<HttpRequestMessage>(), ItExpr.IsAny<CancellationToken>());
+            httpHandlerMock.Protected().Verify<Task<HttpResponseMessage>>("SendAsync", Times.Once(),
+                ItExpr.IsAny<HttpRequestMessage>(), ItExpr.IsAny<CancellationToken>());
         }
 
         [Theory]
@@ -62,10 +58,11 @@ namespace Selector.Tests
 
             var httpHandlerMock = new Mock<HttpMessageHandler>();
             httpHandlerMock.Protected()
-                .Setup<Task<HttpResponseMessage>>("SendAsync", ItExpr.IsAny<HttpRequestMessage>(), ItExpr.IsAny<CancellationToken>())
+                .Setup<Task<HttpResponseMessage>>("SendAsync", ItExpr.IsAny<HttpRequestMessage>(),
+                    ItExpr.IsAny<CancellationToken>())
                 .ReturnsAsync(msg);
 
-            var watcherMock = new Mock<IPlayerWatcher>();
+            var watcherMock = new Mock<ISpotifyPlayerWatcher>();
 
             var link = "https://link";
             var content = new StringContent("");
@@ -81,26 +78,17 @@ namespace Selector.Tests
 
             var webHook = new WebHook(watcherMock.Object, http, config);
 
-            webHook.PredicatePass += (o, e) =>
-            {
-                predicateEvent = predicate;
-            };
+            webHook.PredicatePass += (o, e) => { predicateEvent = predicate; };
 
-            webHook.SuccessfulRequest += (o, e) =>
-            {
-                successfulEvent = successful;
-            };
+            webHook.SuccessfulRequest += (o, e) => { successfulEvent = successful; };
 
-            webHook.FailedRequest += (o, e) =>
-            {
-                failedEvent = !successful;
-            };
+            webHook.FailedRequest += (o, e) => { failedEvent = !successful; };
 
-            await webHook.AsyncCallback(ListeningChangeEventArgs.From(new (), new (), new()));
+            await webHook.AsyncCallback(ListeningChangeEventArgs.From(new(), new(), new()));
 
             predicateEvent.Should().Be(predicate);
             successfulEvent.Should().Be(successful);
             failedEvent.Should().Be(!successful);
         }
     }
-}
+}
\ No newline at end of file
diff --git a/Selector.Tests/Helper.cs b/Selector.Tests/Helper.cs
index 99a353b..60ccf44 100644
--- a/Selector.Tests/Helper.cs
+++ b/Selector.Tests/Helper.cs
@@ -1,9 +1,7 @@
-using System;
-using System.Collections.Generic;
+using System.Collections.Generic;
 using System.Linq;
-using System.Text;
-using System.Threading.Tasks;
-
+using Selector.AppleMusic.Model;
+using Selector.AppleMusic.Watcher;
 using SpotifyAPI.Web;
 
 namespace Selector.Tests
@@ -12,8 +10,8 @@ namespace Selector.Tests
     {
         public static FullTrack FullTrack(string name, string album = "album name", List<string> artists = null)
         {
-            if (artists is null) artists = new List<string>() {"artist"};
-            
+            if (artists is null) artists = new List<string>() { "artist" };
+
             return new FullTrack()
             {
                 Name = name,
@@ -40,7 +38,7 @@ namespace Selector.Tests
 
         public static SimpleAlbum SimpleAlbum(string name, List<string> artists = null)
         {
-            if (artists is null) artists = new List<string>() {"artist"};
+            if (artists is null) artists = new List<string>() { "artist" };
             return new SimpleAlbum()
             {
                 Name = name,
@@ -83,7 +81,8 @@ namespace Selector.Tests
             };
         }
 
-        public static CurrentlyPlayingContext CurrentPlayback(FullTrack track, Device device = null, bool isPlaying = true, string context = "context")
+        public static CurrentlyPlayingContext CurrentPlayback(FullTrack track, Device device = null,
+            bool isPlaying = true, string context = "context")
         {
             return new CurrentlyPlayingContext()
             {
@@ -94,7 +93,8 @@ namespace Selector.Tests
             };
         }
 
-        public static CurrentlyPlaying CurrentlyPlaying(FullEpisode episode, bool isPlaying = true, string context = null)
+        public static CurrentlyPlaying CurrentlyPlaying(FullEpisode episode, bool isPlaying = true,
+            string context = null)
         {
             return new CurrentlyPlaying()
             {
@@ -104,7 +104,8 @@ namespace Selector.Tests
             };
         }
 
-        public static CurrentlyPlayingContext CurrentPlayback(FullEpisode episode, Device device = null, bool isPlaying = true, string context = null)
+        public static CurrentlyPlayingContext CurrentPlayback(FullEpisode episode, Device device = null,
+            bool isPlaying = true, string context = null)
         {
             return new CurrentlyPlayingContext()
             {
@@ -148,5 +149,16 @@ namespace Selector.Tests
                 }
             };
         }
+
+        public static AppleMusicCurrentlyPlayingContext AppleContext(string id)
+        {
+            return new()
+            {
+                Track = new Track()
+                {
+                    Id = id
+                }
+            };
+        }
     }
-}
+}
\ No newline at end of file
diff --git a/Selector.Tests/Selector.Tests.csproj b/Selector.Tests/Selector.Tests.csproj
index d9844d5..eea3ce7 100644
--- a/Selector.Tests/Selector.Tests.csproj
+++ b/Selector.Tests/Selector.Tests.csproj
@@ -23,6 +23,7 @@
   </ItemGroup>
 
   <ItemGroup>
+    <ProjectReference Include="..\Selector.AppleMusic\Selector.AppleMusic.csproj"/>
     <ProjectReference Include="..\Selector\Selector.csproj" />
   </ItemGroup>
 
diff --git a/Selector.Tests/Watcher/PlayerWatcher.cs b/Selector.Tests/Watcher/PlayerWatcher.cs
index 25df0fe..cd6d2f1 100644
--- a/Selector.Tests/Watcher/PlayerWatcher.cs
+++ b/Selector.Tests/Watcher/PlayerWatcher.cs
@@ -1,28 +1,28 @@
-using System;
 using System.Collections.Generic;
-using Xunit;
-using Moq;
-using FluentAssertions;
-using SpotifyAPI.Web;
-
 using System.Threading;
 using System.Threading.Tasks;
-using Xunit.Sdk;
+using FluentAssertions;
+using Moq;
+using SpotifyAPI.Web;
+using Xunit;
 
 namespace Selector.Tests
 {
-    public class PlayerWatcherTests
+    public class SpotifyPlayerWatcherTests
     {
         public static IEnumerable<object[]> NowPlayingData =>
-        new List<object[]>
-        {
-            new object[] { new List<CurrentlyPlayingContext>(){
-                    Helper.CurrentPlayback(Helper.FullTrack("track1", "album1", "artist1")),
-                    Helper.CurrentPlayback(Helper.FullTrack("track2", "album2", "artist2")),
-                    Helper.CurrentPlayback(Helper.FullTrack("track3", "album3", "artist3")),
+            new List<object[]>
+            {
+                new object[]
+                {
+                    new List<CurrentlyPlayingContext>()
+                    {
+                        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))]
@@ -33,9 +33,10 @@ namespace Selector.Tests
             var spotMock = new Mock<IPlayerClient>();
             var eq = new UriEqual();
 
-            spotMock.Setup(s => s.GetCurrentPlayback(It.IsAny<CancellationToken>()).Result).Returns(playingQueue.Dequeue);
+            spotMock.Setup(s => s.GetCurrentPlayback(It.IsAny<CancellationToken>()).Result)
+                .Returns(playingQueue.Dequeue);
 
-            var watcher = new PlayerWatcher(spotMock.Object, eq);
+            var watcher = new SpotifyPlayerWatcher(spotMock.Object, eq);
 
             for (var i = 0; i < playing.Count; i++)
             {
@@ -45,140 +46,209 @@ namespace Selector.Tests
         }
 
         public static IEnumerable<object[]> EventsData =>
-        new List<object[]>
-        {
-            // NO CHANGING
-            new object[] { new List<CurrentlyPlayingContext>(){
-                    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"),
+            new List<object[]>
+            {
+                // NO CHANGING
+                new object[]
+                {
+                    new List<CurrentlyPlayingContext>()
+                    {
+                        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<string>()
+                        { "ItemChange", "ContextChange", "PlayingChange", "DeviceChange", "VolumeChange" },
+                    // to not raise
+                    new List<string>() { "AlbumChange", "ArtistChange" }
                 },
-                // to raise
-                new List<string>(){ "ItemChange", "ContextChange", "PlayingChange", "DeviceChange", "VolumeChange" },
-                // to not raise
-                new List<string>(){ "AlbumChange", "ArtistChange" }
-            },
-            // TRACK CHANGE
-            new object[] { new List<CurrentlyPlayingContext>(){
-                    Helper.CurrentPlayback(Helper.FullTrack("trackchange1", "album1", "artist1")),
-                    Helper.CurrentPlayback(Helper.FullTrack("trackchange2", "album1", "artist1"))
+                // TRACK CHANGE
+                new object[]
+                {
+                    new List<CurrentlyPlayingContext>()
+                    {
+                        Helper.CurrentPlayback(Helper.FullTrack("trackchange1", "album1", "artist1")),
+                        Helper.CurrentPlayback(Helper.FullTrack("trackchange2", "album1", "artist1"))
+                    },
+                    // to raise
+                    new List<string>()
+                        { "ContextChange", "PlayingChange", "ItemChange", "DeviceChange", "VolumeChange" },
+                    // to not raise
+                    new List<string>() { "AlbumChange", "ArtistChange" }
                 },
-                // to raise
-                new List<string>(){ "ContextChange", "PlayingChange", "ItemChange", "DeviceChange", "VolumeChange" },
-                // to not raise
-                new List<string>(){ "AlbumChange", "ArtistChange" }
-            },
-            // ALBUM CHANGE
-            new object[] { new List<CurrentlyPlayingContext>(){
-                    Helper.CurrentPlayback(Helper.FullTrack("albumchange", "album1", "artist1")),
-                    Helper.CurrentPlayback(Helper.FullTrack("albumchange", "album2", "artist1"))
+                // ALBUM CHANGE
+                new object[]
+                {
+                    new List<CurrentlyPlayingContext>()
+                    {
+                        Helper.CurrentPlayback(Helper.FullTrack("albumchange", "album1", "artist1")),
+                        Helper.CurrentPlayback(Helper.FullTrack("albumchange", "album2", "artist1"))
+                    },
+                    // to raise
+                    new List<string>()
+                    {
+                        "ContextChange", "PlayingChange", "ItemChange", "AlbumChange", "DeviceChange", "VolumeChange"
+                    },
+                    // to not raise
+                    new List<string>() { "ArtistChange" }
                 },
-                // to raise
-                new List<string>(){ "ContextChange", "PlayingChange", "ItemChange", "AlbumChange", "DeviceChange", "VolumeChange" },
-                // to not raise
-                new List<string>(){ "ArtistChange" }
-            },
-            // ARTIST CHANGE
-            new object[] { new List<CurrentlyPlayingContext>(){
-                    Helper.CurrentPlayback(Helper.FullTrack("artistchange", "album1", "artist1")),
-                    Helper.CurrentPlayback(Helper.FullTrack("artistchange", "album1", "artist2"))
+                // ARTIST CHANGE
+                new object[]
+                {
+                    new List<CurrentlyPlayingContext>()
+                    {
+                        Helper.CurrentPlayback(Helper.FullTrack("artistchange", "album1", "artist1")),
+                        Helper.CurrentPlayback(Helper.FullTrack("artistchange", "album1", "artist2"))
+                    },
+                    // to raise
+                    new List<string>()
+                    {
+                        "ContextChange", "PlayingChange", "ItemChange", "ArtistChange", "DeviceChange", "VolumeChange"
+                    },
+                    // to not raise
+                    new List<string>() { "AlbumChange" }
                 },
-                // to raise
-                new List<string>(){ "ContextChange", "PlayingChange", "ItemChange", "ArtistChange", "DeviceChange", "VolumeChange" },
-                // to not raise
-                new List<string>(){ "AlbumChange" }
-            },
-            // CONTEXT CHANGE
-            new object[] { new List<CurrentlyPlayingContext>(){
-                    Helper.CurrentPlayback(Helper.FullTrack("contextchange", "album1", "artist1"), context: "context1"),
-                    Helper.CurrentPlayback(Helper.FullTrack("contextchange", "album1", "artist1"), context: "context2")
+                // CONTEXT CHANGE
+                new object[]
+                {
+                    new List<CurrentlyPlayingContext>()
+                    {
+                        Helper.CurrentPlayback(Helper.FullTrack("contextchange", "album1", "artist1"),
+                            context: "context1"),
+                        Helper.CurrentPlayback(Helper.FullTrack("contextchange", "album1", "artist1"),
+                            context: "context2")
+                    },
+                    // to raise
+                    new List<string>()
+                        { "PlayingChange", "ItemChange", "ContextChange", "DeviceChange", "VolumeChange" },
+                    // to not raise
+                    new List<string>() { "AlbumChange", "ArtistChange" }
                 },
-                // to raise
-                new List<string>(){ "PlayingChange", "ItemChange", "ContextChange", "DeviceChange", "VolumeChange" },
-                // to not raise
-                new List<string>(){ "AlbumChange", "ArtistChange" }
-            },
-            // PLAYING CHANGE
-            new object[] { new List<CurrentlyPlayingContext>(){
-                    Helper.CurrentPlayback(Helper.FullTrack("playingchange1", "album1", "artist1"), isPlaying: true, context: "context1"),
-                    Helper.CurrentPlayback(Helper.FullTrack("playingchange1", "album1", "artist1"), isPlaying: false, context: "context1")
+                // PLAYING CHANGE
+                new object[]
+                {
+                    new List<CurrentlyPlayingContext>()
+                    {
+                        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<string>()
+                        { "ContextChange", "ItemChange", "PlayingChange", "DeviceChange", "VolumeChange" },
+                    // to not raise
+                    new List<string>() { "AlbumChange", "ArtistChange" }
                 },
-                // to raise
-                new List<string>(){ "ContextChange", "ItemChange", "PlayingChange", "DeviceChange", "VolumeChange" },
-                // to not raise
-                new List<string>(){ "AlbumChange", "ArtistChange" }
-            },
-            // PLAYING CHANGE
-            new object[] { new List<CurrentlyPlayingContext>(){
-                    Helper.CurrentPlayback(Helper.FullTrack("playingchange2", "album1", "artist1"), isPlaying: false, context: "context1"),
-                    Helper.CurrentPlayback(Helper.FullTrack("playingchange2", "album1", "artist1"), isPlaying: true, context: "context1")
+                // PLAYING CHANGE
+                new object[]
+                {
+                    new List<CurrentlyPlayingContext>()
+                    {
+                        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<string>()
+                        { "ContextChange", "ItemChange", "PlayingChange", "DeviceChange", "VolumeChange" },
+                    // to not raise
+                    new List<string>() { "AlbumChange", "ArtistChange" }
                 },
-                // to raise
-                new List<string>(){ "ContextChange", "ItemChange", "PlayingChange", "DeviceChange", "VolumeChange" },
-                // to not raise
-                new List<string>(){ "AlbumChange", "ArtistChange" }
-            },
-            // CONTENT CHANGE
-            new object[] { new List<CurrentlyPlayingContext>(){
-                    Helper.CurrentPlayback(Helper.FullTrack("contentchange1", "album1", "artist1"), isPlaying: true, context: "context1"),
-                    Helper.CurrentPlayback(Helper.FullEpisode("contentchange1", "show1", "pub1"), isPlaying: true, context: "context2")
+                // CONTENT CHANGE
+                new object[]
+                {
+                    new List<CurrentlyPlayingContext>()
+                    {
+                        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<string>()
+                    {
+                        "PlayingChange", "ContentChange", "ContextChange", "ItemChange", "DeviceChange", "VolumeChange"
+                    },
+                    // to not raise
+                    new List<string>() { "AlbumChange", "ArtistChange" }
                 },
-                // to raise
-                new List<string>(){ "PlayingChange", "ContentChange", "ContextChange", "ItemChange", "DeviceChange", "VolumeChange" },
-                // to not raise
-                new List<string>(){ "AlbumChange", "ArtistChange" }
-            },
-            // CONTENT CHANGE
-            new object[] { new List<CurrentlyPlayingContext>(){
-                    Helper.CurrentPlayback(Helper.FullEpisode("contentchange1", "show1", "pub1"), isPlaying: true, context: "context2"),
-                    Helper.CurrentPlayback(Helper.FullTrack("contentchange1", "album1", "artist1"), isPlaying: true, context: "context1")
+                // CONTENT CHANGE
+                new object[]
+                {
+                    new List<CurrentlyPlayingContext>()
+                    {
+                        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<string>()
+                    {
+                        "PlayingChange", "ContentChange", "ContextChange", "ItemChange", "DeviceChange", "VolumeChange"
+                    },
+                    // to not raise
+                    new List<string>() { "AlbumChange", "ArtistChange" }
                 },
-                // to raise
-                new List<string>(){ "PlayingChange", "ContentChange", "ContextChange", "ItemChange", "DeviceChange", "VolumeChange" },
-                // to not raise
-                new List<string>(){ "AlbumChange", "ArtistChange" }
-            },
-            // DEVICE CHANGE
-            new object[] { new List<CurrentlyPlayingContext>(){
-                    Helper.CurrentPlayback(Helper.FullTrack("devicechange", "album1", "artist1"), device: Helper.Device("dev1")),
-                    Helper.CurrentPlayback(Helper.FullTrack("devicechange", "album1", "artist1"), device: Helper.Device("dev2"))
+                // DEVICE CHANGE
+                new object[]
+                {
+                    new List<CurrentlyPlayingContext>()
+                    {
+                        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<string>()
+                        { "ContextChange", "PlayingChange", "ItemChange", "VolumeChange", "DeviceChange" },
+                    // to not raise
+                    new List<string>() { "AlbumChange", "ArtistChange", "ContentChange" }
                 },
-                // to raise
-                new List<string>(){ "ContextChange", "PlayingChange", "ItemChange", "VolumeChange", "DeviceChange" },
-                // to not raise
-                new List<string>(){ "AlbumChange", "ArtistChange", "ContentChange" }
-            },
-            // VOLUME CHANGE
-            new object[] { new List<CurrentlyPlayingContext>(){
-                    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))
+                // VOLUME CHANGE
+                new object[]
+                {
+                    new List<CurrentlyPlayingContext>()
+                    {
+                        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<string>()
+                        { "ContextChange", "PlayingChange", "ItemChange", "VolumeChange", "DeviceChange" },
+                    // to not raise
+                    new List<string>() { "AlbumChange", "ArtistChange", "ContentChange" }
                 },
-                // to raise
-                new List<string>(){ "ContextChange", "PlayingChange", "ItemChange", "VolumeChange", "DeviceChange" },
-                // to not raise
-                new List<string>(){ "AlbumChange", "ArtistChange", "ContentChange" }
-            },
-            // // STARTED PLAYBACK
-            // new object[] { new List<CurrentlyPlayingContext>(){
-            //         null,
-            //         Helper.CurrentPlayback(Helper.FullTrack("track1", "album1", "artist1"), isPlaying: true, context: "context1")
-            //     },
-            //     // to raise
-            //     new List<string>(){ "PlayingChange" },
-            //     // to not raise
-            //     new List<string>(){ "AlbumChange", "ArtistChange", "ContentChange", "ContextChange", "ItemChange", "DeviceChange", "VolumeChange" }
-            // },
-            // // STARTED PLAYBACK
-            // new object[] { new List<CurrentlyPlayingContext>(){
-            //         Helper.CurrentPlayback(Helper.FullTrack("track1", "album1", "artist1"), isPlaying: true, context: "context1"),
-            //         null
-            //     },
-            //     // to raise
-            //     new List<string>(){ "PlayingChange" },
-            //     // to not raise
-            //     new List<string>(){ "AlbumChange", "ArtistChange", "ContentChange", "ContextChange", "ItemChange", "DeviceChange", "VolumeChange" }
-            // }
-        };
+                // // STARTED PLAYBACK
+                // new object[] { new List<CurrentlyPlayingContext>(){
+                //         null,
+                //         Helper.CurrentPlayback(Helper.FullTrack("track1", "album1", "artist1"), isPlaying: true, context: "context1")
+                //     },
+                //     // to raise
+                //     new List<string>(){ "PlayingChange" },
+                //     // to not raise
+                //     new List<string>(){ "AlbumChange", "ArtistChange", "ContentChange", "ContextChange", "ItemChange", "DeviceChange", "VolumeChange" }
+                // },
+                // // STARTED PLAYBACK
+                // new object[] { new List<CurrentlyPlayingContext>(){
+                //         Helper.CurrentPlayback(Helper.FullTrack("track1", "album1", "artist1"), isPlaying: true, context: "context1"),
+                //         null
+                //     },
+                //     // to raise
+                //     new List<string>(){ "PlayingChange" },
+                //     // to not raise
+                //     new List<string>(){ "AlbumChange", "ArtistChange", "ContentChange", "ContextChange", "ItemChange", "DeviceChange", "VolumeChange" }
+                // }
+            };
 
         [Theory]
         [MemberData(nameof(EventsData))]
@@ -193,7 +263,7 @@ namespace Selector.Tests
                 s => s.GetCurrentPlayback(It.IsAny<CancellationToken>()).Result
             ).Returns(playingQueue.Dequeue);
 
-            var watcher = new PlayerWatcher(spotMock.Object, eq);
+            var watcher = new SpotifyPlayerWatcher(spotMock.Object, eq);
             using var monitoredWatcher = watcher.Monitor();
 
             for (var i = 0; i < playing.Count; i++)
@@ -213,14 +283,14 @@ namespace Selector.Tests
         {
             var spotMock = new Mock<IPlayerClient>();
             var eq = new UriEqual();
-            var watch = new PlayerWatcher(spotMock.Object, eq)
+            var watch = new SpotifyPlayerWatcher(spotMock.Object, eq)
             {
                 PollPeriod = pollPeriod
             };
 
             var tokenSource = new CancellationTokenSource();
             var task = watch.Watch(tokenSource.Token);
-            
+
             await Task.Delay(execTime);
             tokenSource.Cancel();
 
@@ -238,4 +308,4 @@ namespace Selector.Tests
         //     await watch.Watch(token.Token);
         // }
     }
-}
+}
\ No newline at end of file
diff --git a/Selector.Web/Hubs/NowPlayingHub.cs b/Selector.Web/Hubs/NowPlayingHub.cs
index 1484372..52a830f 100644
--- a/Selector.Web/Hubs/NowPlayingHub.cs
+++ b/Selector.Web/Hubs/NowPlayingHub.cs
@@ -1,7 +1,6 @@
 using System;
 using System.Collections.Generic;
 using System.Data.SqlTypes;
-using System.Diagnostics;
 using System.Linq;
 using System.Text.Json;
 using System.Threading.Tasks;
@@ -12,7 +11,6 @@ using Selector.Cache;
 using Selector.Model;
 using Selector.Model.Extensions;
 using Selector.SignalR;
-using SpotifyAPI.Web;
 using StackExchange.Redis;
 
 namespace Selector.Web.Hubs
@@ -54,7 +52,7 @@ namespace Selector.Web.Hubs
 
         public async Task SendNewPlaying()
         {
-            var nowPlaying = await Cache.StringGetAsync(Key.CurrentlyPlaying(Context.UserIdentifier));
+            var nowPlaying = await Cache.StringGetAsync(Key.CurrentlyPlayingSpotify(Context.UserIdentifier));
             if (nowPlaying != RedisValue.Null)
             {
                 var deserialised = JsonSerializer.Deserialize(nowPlaying, JsonContext.Default.CurrentlyPlayingDTO);
@@ -67,16 +65,16 @@ namespace Selector.Web.Hubs
             if (string.IsNullOrWhiteSpace(trackId)) return;
 
             var user = Db.Users
-                        .AsNoTracking()
-                        .Where(u => u.Id == Context.UserIdentifier)
-                        .SingleOrDefault()
-                            ?? throw new SqlNullValueException("No user returned");
+                           .AsNoTracking()
+                           .Where(u => u.Id == Context.UserIdentifier)
+                           .SingleOrDefault()
+                       ?? throw new SqlNullValueException("No user returned");
             var watcher = Db.Watcher
-                        .AsNoTracking()
-                        .Where(w => w.UserId == Context.UserIdentifier
-                                && w.Type == WatcherType.Player)
-                        .SingleOrDefault()
-                            ?? throw new SqlNullValueException($"No player watcher found for [{user.UserName}]");
+                              .AsNoTracking()
+                              .Where(w => w.UserId == Context.UserIdentifier
+                                          && w.Type == WatcherType.SpotifyPlayer)
+                              .SingleOrDefault()
+                          ?? throw new SqlNullValueException($"No player watcher found for [{user.UserName}]");
 
             var feature = await AudioFeaturePuller.Get(user.SpotifyRefreshToken, trackId);
 
@@ -91,10 +89,10 @@ namespace Selector.Web.Hubs
             if (PlayCountPuller is not null)
             {
                 var user = Db.Users
-                        .AsNoTracking()
-                        .Where(u => u.Id == Context.UserIdentifier)
-                        .SingleOrDefault()
-                            ?? throw new SqlNullValueException("No user returned");
+                               .AsNoTracking()
+                               .Where(u => u.Id == Context.UserIdentifier)
+                               .SingleOrDefault()
+                           ?? throw new SqlNullValueException("No user returned");
 
                 if (user.LastFmConnected())
                 {
@@ -125,7 +123,8 @@ namespace Selector.Web.Hubs
 
             if (user.ScrobbleSavingEnabled())
             {
-                var artistScrobbles = ScrobbleRepository.GetAll(userId: user.Id, artistName: artist, from: GetMaximumWindow()).ToArray();
+                var artistScrobbles = ScrobbleRepository
+                    .GetAll(userId: user.Id, artistName: artist, from: GetMaximumWindow()).ToArray();
                 var artistDensity = artistScrobbles.Density(nowOptions.Value.ArtistDensityWindow);
 
                 var tasks = new List<Task>(3);
@@ -138,7 +137,9 @@ namespace Selector.Web.Hubs
                     }));
                 }
 
-                var albumDensity = artistScrobbles.Where(s => s.AlbumName.Equals(album, StringComparison.InvariantCultureIgnoreCase)).Density(nowOptions.Value.AlbumDensityWindow);
+                var albumDensity = artistScrobbles
+                    .Where(s => s.AlbumName.Equals(album, StringComparison.InvariantCultureIgnoreCase))
+                    .Density(nowOptions.Value.AlbumDensityWindow);
 
                 if (albumDensity > nowOptions.Value.AlbumDensityThreshold)
                 {
@@ -148,7 +149,9 @@ namespace Selector.Web.Hubs
                     }));
                 }
 
-                var trackDensity = artistScrobbles.Where(s => s.TrackName.Equals(track, StringComparison.InvariantCultureIgnoreCase)).Density(nowOptions.Value.TrackDensityWindow);
+                var trackDensity = artistScrobbles
+                    .Where(s => s.TrackName.Equals(track, StringComparison.InvariantCultureIgnoreCase))
+                    .Density(nowOptions.Value.TrackDensityWindow);
 
                 if (albumDensity > nowOptions.Value.TrackDensityThreshold)
                 {
@@ -165,7 +168,13 @@ namespace Selector.Web.Hubs
             }
         }
 
-        private DateTime GetMaximumWindow() => GetMaximumWindow(new TimeSpan[] { nowOptions.Value.ArtistDensityWindow, nowOptions.Value.AlbumDensityWindow, nowOptions.Value.TrackDensityWindow });
-        private DateTime GetMaximumWindow(IEnumerable<TimeSpan> windows) => windows.Select(w => DateTime.UtcNow - w).Min();
+        private DateTime GetMaximumWindow() => GetMaximumWindow(new TimeSpan[]
+        {
+            nowOptions.Value.ArtistDensityWindow, nowOptions.Value.AlbumDensityWindow,
+            nowOptions.Value.TrackDensityWindow
+        });
+
+        private DateTime GetMaximumWindow(IEnumerable<TimeSpan> windows) =>
+            windows.Select(w => DateTime.UtcNow - w).Min();
     }
 }
\ No newline at end of file
diff --git a/Selector.Web/Services/EventMappings/NowPlayingHubMapping.cs b/Selector.Web/Services/EventMappings/NowPlayingHubMapping.cs
index 835637f..cfcc707 100644
--- a/Selector.Web/Services/EventMappings/NowPlayingHubMapping.cs
+++ b/Selector.Web/Services/EventMappings/NowPlayingHubMapping.cs
@@ -1,14 +1,13 @@
 using System.Threading.Tasks;
 using Microsoft.AspNetCore.SignalR;
 using Microsoft.Extensions.Logging;
-
-using Selector.Web.Hubs;
 using Selector.Events;
 using Selector.SignalR;
+using Selector.Web.Hubs;
 
 namespace Selector.Web.Service
 {
-    public class NowPlayingHubMapping: IEventHubMapping<NowPlayingHub, INowPlayingHubClient>
+    public class NowPlayingHubMapping : IEventHubMapping<NowPlayingHub, INowPlayingHubClient>
     {
         private readonly ILogger<NowPlayingHubMapping> Logger;
         private readonly UserEventBus UserEvent;
@@ -27,14 +26,21 @@ namespace Selector.Web.Service
         {
             Logger.LogDebug("Forming now playing event mapping between event bus and SignalR hub");
 
-            UserEvent.CurrentlyPlaying += async (o, args) =>
+            UserEvent.CurrentlyPlayingSpotify += async (o, args) =>
             {
                 Logger.LogDebug("Passing now playing event to SignalR hub [{userId}]", args.UserId);
 
                 await Hub.Clients.User(args.UserId).OnNewPlaying(args);
             };
 
+            // UserEvent.CurrentlyPlayingApple += async (o, args) =>
+            // {
+            //     Logger.LogDebug("Passing now playing event to SignalR hub", args.UserId);
+            //
+            //     await Hub.Clients.User(args.UserId).OnNewPlaying(args);
+            // };
+
             return Task.CompletedTask;
         }
     }
-}
+}
\ No newline at end of file
diff --git a/Selector/Consumers/AudioFeatureInjector.cs b/Selector/Consumers/AudioFeatureInjector.cs
index c19fd46..d7ddc01 100644
--- a/Selector/Consumers/AudioFeatureInjector.cs
+++ b/Selector/Consumers/AudioFeatureInjector.cs
@@ -1,6 +1,4 @@
 using System;
-using System.Collections.Generic;
-using System.Text;
 using System.Threading;
 using System.Threading.Tasks;
 using Microsoft.Extensions.Logging;
@@ -9,9 +7,9 @@ using SpotifyAPI.Web;
 
 namespace Selector
 {
-    public class AudioFeatureInjector : IPlayerConsumer
+    public class AudioFeatureInjector : ISpotifyPlayerConsumer
     {
-        protected readonly IPlayerWatcher Watcher;
+        protected readonly ISpotifyPlayerWatcher Watcher;
         protected readonly ITracksClient TrackClient;
         protected readonly ILogger<AudioFeatureInjector> Logger;
 
@@ -22,11 +20,12 @@ namespace Selector
         public AnalysedTrackTimeline Timeline { get; set; } = new();
 
         public AudioFeatureInjector(
-            IPlayerWatcher watcher, 
-            ITracksClient trackClient, 
+            ISpotifyPlayerWatcher watcher,
+            ITracksClient trackClient,
             ILogger<AudioFeatureInjector> logger = null,
             CancellationToken token = default
-        ){
+        )
+        {
             Watcher = watcher;
             TrackClient = trackClient;
             Logger = logger ?? NullLogger<AudioFeatureInjector>.Instance;
@@ -37,7 +36,8 @@ namespace Selector
         {
             if (e.Current is null) return;
 
-            Task.Run(async () => {
+            Task.Run(async () =>
+            {
                 try
                 {
                     await AsyncCallback(e);
@@ -57,10 +57,12 @@ namespace Selector
             {
                 if (string.IsNullOrWhiteSpace(track.Id)) return;
 
-                try {
+                try
+                {
                     Logger.LogTrace("Making Spotify call");
                     var audioFeatures = await TrackClient.GetAudioFeatures(track.Id);
-                    Logger.LogDebug("Adding audio features [{track}]: [{audio_features}]", track.DisplayString(), audioFeatures.DisplayString());
+                    Logger.LogDebug("Adding audio features [{track}]: [{audio_features}]", track.DisplayString(),
+                        audioFeatures.DisplayString());
 
                     var analysedTrack = AnalysedTrack.From(track, audioFeatures);
 
@@ -103,10 +105,10 @@ namespace Selector
         {
             var watcher = watch ?? Watcher ?? throw new ArgumentNullException("No watcher provided");
 
-            if (watcher is IPlayerWatcher watcherCast)
+            if (watcher is ISpotifyPlayerWatcher watcherCast)
             {
                 watcherCast.ItemChange += Callback;
-            } 
+            }
             else
             {
                 throw new ArgumentException("Provided watcher is not a PlayerWatcher");
@@ -117,7 +119,7 @@ namespace Selector
         {
             var watcher = watch ?? Watcher ?? throw new ArgumentNullException("No watcher provided");
 
-            if (watcher is IPlayerWatcher watcherCast)
+            if (watcher is ISpotifyPlayerWatcher watcherCast)
             {
                 watcherCast.ItemChange -= Callback;
             }
@@ -129,11 +131,12 @@ namespace Selector
 
         protected virtual void OnNewFeature(AnalysedTrack args)
         {
-            NewFeature?.Invoke(this, args); 
+            NewFeature?.Invoke(this, args);
         }
     }
 
-    public class AnalysedTrack {
+    public class AnalysedTrack
+    {
         public FullTrack Track { get; set; }
         public TrackAudioFeatures Features { get; set; }
 
@@ -146,4 +149,4 @@ namespace Selector
             };
         }
     }
-}
+}
\ No newline at end of file
diff --git a/Selector/Consumers/DummyAudioFeatureInjector.cs b/Selector/Consumers/DummyAudioFeatureInjector.cs
index bbb7b74..c0ae960 100644
--- a/Selector/Consumers/DummyAudioFeatureInjector.cs
+++ b/Selector/Consumers/DummyAudioFeatureInjector.cs
@@ -1,10 +1,7 @@
 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
@@ -30,18 +27,18 @@ namespace Selector
                 Valence = 0.5f,
             }
         };
+
         private int _contextIdx = 0;
 
         private DateTime _lastNext = DateTime.UtcNow;
         private TimeSpan _contextLifespan = TimeSpan.FromSeconds(30);
 
         public DummyAudioFeatureInjector(
-            IPlayerWatcher watcher,
+            ISpotifyPlayerWatcher watcher,
             ILogger<DummyAudioFeatureInjector> logger = null,
             CancellationToken token = default
-        ): base (watcher, null, logger, token)
+        ) : base(watcher, null, logger, token)
         {
-
         }
 
         private bool ShouldCycle() => DateTime.UtcNow - _lastNext > _contextLifespan;
@@ -98,4 +95,4 @@ namespace Selector
             return Task.CompletedTask;
         }
     }
-}
+}
\ No newline at end of file
diff --git a/Selector/Consumers/Factory/AudioFeatureInjectorFactory.cs b/Selector/Consumers/Factory/AudioFeatureInjectorFactory.cs
index 8803927..0f0cac5 100644
--- a/Selector/Consumers/Factory/AudioFeatureInjectorFactory.cs
+++ b/Selector/Consumers/Factory/AudioFeatureInjectorFactory.cs
@@ -1,20 +1,17 @@
-using System;
-using System.Collections.Generic;
-using System.Text;
-using System.Threading.Tasks;
+using System.Threading.Tasks;
 using Microsoft.Extensions.Logging;
-
 using SpotifyAPI.Web;
 
 namespace Selector
 {
     public interface IAudioFeatureInjectorFactory
     {
-        public Task<IPlayerConsumer> Get(ISpotifyConfigFactory spotifyFactory, IPlayerWatcher watcher = null);
+        public Task<ISpotifyPlayerConsumer> Get(ISpotifyConfigFactory spotifyFactory,
+            ISpotifyPlayerWatcher watcher = null);
     }
-    
-    public class AudioFeatureInjectorFactory: IAudioFeatureInjectorFactory {
 
+    public class AudioFeatureInjectorFactory : IAudioFeatureInjectorFactory
+    {
         private readonly ILoggerFactory LoggerFactory;
 
         public AudioFeatureInjectorFactory(ILoggerFactory loggerFactory)
@@ -22,7 +19,8 @@ namespace Selector
             LoggerFactory = loggerFactory;
         }
 
-        public async Task<IPlayerConsumer> Get(ISpotifyConfigFactory spotifyFactory, IPlayerWatcher watcher = null)
+        public async Task<ISpotifyPlayerConsumer> Get(ISpotifyConfigFactory spotifyFactory,
+            ISpotifyPlayerWatcher watcher = null)
         {
             if (!Magic.Dummy)
             {
@@ -44,4 +42,4 @@ namespace Selector
             }
         }
     }
-}
+}
\ No newline at end of file
diff --git a/Selector/Consumers/Factory/PlayCounterFactory.cs b/Selector/Consumers/Factory/PlayCounterFactory.cs
index a22507d..9b60f10 100644
--- a/Selector/Consumers/Factory/PlayCounterFactory.cs
+++ b/Selector/Consumers/Factory/PlayCounterFactory.cs
@@ -1,41 +1,41 @@
 using System;
-using System.Collections.Generic;
-using System.Text;
 using System.Threading.Tasks;
-using Microsoft.Extensions.Logging;
-
 using IF.Lastfm.Core.Api;
+using Microsoft.Extensions.Logging;
 
 namespace Selector
 {
     public interface IPlayCounterFactory
     {
-        public Task<IPlayerConsumer> Get(LastfmClient fmClient = null, LastFmCredentials creds = null, IPlayerWatcher watcher = null);
+        public Task<ISpotifyPlayerConsumer> Get(LastfmClient fmClient = null, LastFmCredentials creds = null,
+            ISpotifyPlayerWatcher watcher = null);
     }
-    
-    public class PlayCounterFactory: IPlayCounterFactory {
 
+    public class PlayCounterFactory : IPlayCounterFactory
+    {
         private readonly ILoggerFactory LoggerFactory;
         private readonly LastfmClient Client;
         private readonly LastFmCredentials Creds;
 
-        public PlayCounterFactory(ILoggerFactory loggerFactory, LastfmClient client = null, LastFmCredentials creds = null)
+        public PlayCounterFactory(ILoggerFactory loggerFactory, LastfmClient client = null,
+            LastFmCredentials creds = null)
         {
             LoggerFactory = loggerFactory;
             Client = client;
             Creds = creds;
         }
 
-        public Task<IPlayerConsumer> Get(LastfmClient fmClient = null, LastFmCredentials creds = null, IPlayerWatcher watcher = null)
+        public Task<ISpotifyPlayerConsumer> Get(LastfmClient fmClient = null, LastFmCredentials creds = null,
+            ISpotifyPlayerWatcher watcher = null)
         {
             var client = fmClient ?? Client;
 
-            if(client is null)
+            if (client is null)
             {
                 throw new ArgumentNullException("No Last.fm client provided");
             }
 
-            return Task.FromResult<IPlayerConsumer>(new PlayCounter(
+            return Task.FromResult<ISpotifyPlayerConsumer>(new PlayCounter(
                 watcher,
                 client.Track,
                 client.Album,
@@ -46,4 +46,4 @@ namespace Selector
             ));
         }
     }
-}
+}
\ No newline at end of file
diff --git a/Selector/Consumers/Factory/WebHookFactory.cs b/Selector/Consumers/Factory/WebHookFactory.cs
index ac971cb..080d6f7 100644
--- a/Selector/Consumers/Factory/WebHookFactory.cs
+++ b/Selector/Consumers/Factory/WebHookFactory.cs
@@ -1,19 +1,15 @@
-using System;
-using System.Collections.Generic;
-using System.Text;
+using System.Net.Http;
 using System.Threading.Tasks;
 using Microsoft.Extensions.Logging;
 
-using System.Net.Http;
-
 namespace Selector
 {
     public interface IWebHookFactory
     {
-        public Task<WebHook> Get(WebHookConfig config, IPlayerWatcher watcher = null);
+        public Task<WebHook> Get(WebHookConfig config, ISpotifyPlayerWatcher watcher = null);
     }
-    
-    public class WebHookFactory: IWebHookFactory
+
+    public class WebHookFactory : IWebHookFactory
     {
         private readonly ILoggerFactory LoggerFactory;
         private readonly HttpClient Http;
@@ -24,7 +20,7 @@ namespace Selector
             Http = httpClient;
         }
 
-        public Task<WebHook> Get(WebHookConfig config, IPlayerWatcher watcher = null)
+        public Task<WebHook> Get(WebHookConfig config, ISpotifyPlayerWatcher watcher = null)
         {
             return Task.FromResult(new WebHook(
                 watcher,
@@ -34,4 +30,4 @@ namespace Selector
             ));
         }
     }
-}
+}
\ No newline at end of file
diff --git a/Selector/Consumers/IConsumer.cs b/Selector/Consumers/IConsumer.cs
index 12aa647..48180fa 100644
--- a/Selector/Consumers/IConsumer.cs
+++ b/Selector/Consumers/IConsumer.cs
@@ -1,7 +1,4 @@
-using System;
-using System.Threading.Tasks;
-
-namespace Selector
+namespace Selector
 {
     public interface IConsumer
     {
@@ -9,14 +6,16 @@ namespace Selector
         public void Unsubscribe(IWatcher watch = null);
     }
 
-    public interface IConsumer<T>: IConsumer
+    public interface IConsumer<T> : IConsumer
     {
         public void Callback(object sender, T e);
     }
 
-    public interface IPlayerConsumer: IConsumer<ListeningChangeEventArgs>
-    { }
+    public interface ISpotifyPlayerConsumer : IConsumer<ListeningChangeEventArgs>
+    {
+    }
 
     public interface IPlaylistConsumer : IConsumer<PlaylistChangeEventArgs>
-    { }
-}
+    {
+    }
+}
\ No newline at end of file
diff --git a/Selector/Consumers/PlayCounter.cs b/Selector/Consumers/PlayCounter.cs
index b872ae1..0447a3a 100644
--- a/Selector/Consumers/PlayCounter.cs
+++ b/Selector/Consumers/PlayCounter.cs
@@ -1,21 +1,17 @@
 using System;
 using System.Collections.Generic;
-using System.Text;
 using System.Threading;
 using System.Threading.Tasks;
+using IF.Lastfm.Core.Api;
 using Microsoft.Extensions.Logging;
 using Microsoft.Extensions.Logging.Abstractions;
-
 using SpotifyAPI.Web;
-using IF.Lastfm.Core.Api;
-using IF.Lastfm.Core.Objects;
-using IF.Lastfm.Core.Api.Helpers;
 
 namespace Selector
 {
-    public class PlayCounter : IPlayerConsumer
+    public class PlayCounter : ISpotifyPlayerConsumer
     {
-        protected readonly IPlayerWatcher Watcher;
+        protected readonly ISpotifyPlayerWatcher Watcher;
         protected readonly ITrackApi TrackClient;
         protected readonly IAlbumApi AlbumClient;
         protected readonly IArtistApi ArtistClient;
@@ -30,7 +26,7 @@ namespace Selector
         public AnalysedTrackTimeline Timeline { get; set; } = new();
 
         public PlayCounter(
-            IPlayerWatcher watcher,
+            ISpotifyPlayerWatcher watcher,
             ITrackApi trackClient,
             IAlbumApi albumClient,
             IArtistApi artistClient,
@@ -53,8 +49,9 @@ namespace Selector
         public void Callback(object sender, ListeningChangeEventArgs e)
         {
             if (e.Current is null) return;
-            
-            Task.Run(async () => {
+
+            Task.Run(async () =>
+            {
                 try
                 {
                     await AsyncCallback(e);
@@ -68,7 +65,8 @@ namespace Selector
 
         public async Task AsyncCallback(ListeningChangeEventArgs e)
         {
-            using var scope = Logger.BeginScope(new Dictionary<string, object>() { { "spotify_username", e.SpotifyUsername }, { "id", e.Id }, { "username", Credentials.Username } });
+            using var scope = Logger.BeginScope(new Dictionary<string, object>()
+                { { "spotify_username", e.SpotifyUsername }, { "id", e.Id }, { "username", Credentials.Username } });
 
             if (Credentials is null || string.IsNullOrWhiteSpace(Credentials.Username))
             {
@@ -78,12 +76,15 @@ namespace Selector
 
             if (e.Current.Item is FullTrack track)
             {
-                using var trackScope = Logger.BeginScope(new Dictionary<string, object>() { { "track", track.DisplayString() } });
+                using var trackScope = Logger.BeginScope(new Dictionary<string, object>()
+                    { { "track", track.DisplayString() } });
 
                 Logger.LogTrace("Making Last.fm call");
 
-                var trackInfo = TrackClient.GetInfoAsync(track.Name, track.Artists[0].Name, username: Credentials?.Username);
-                var albumInfo = AlbumClient.GetInfoAsync(track.Album.Artists[0].Name, track.Album.Name, username: Credentials?.Username);
+                var trackInfo =
+                    TrackClient.GetInfoAsync(track.Name, track.Artists[0].Name, username: Credentials?.Username);
+                var albumInfo = AlbumClient.GetInfoAsync(track.Album.Artists[0].Name, track.Album.Name,
+                    username: Credentials?.Username);
                 var artistInfo = ArtistClient.GetInfoAsync(track.Artists[0].Name);
                 var userInfo = UserClient.GetInfoAsync(Credentials.Username);
 
@@ -104,7 +105,8 @@ namespace Selector
                 }
                 else
                 {
-                    Logger.LogError(trackInfo.Exception, "Track info task faulted, [{context}]", e.Current.DisplayString());
+                    Logger.LogError(trackInfo.Exception, "Track info task faulted, [{context}]",
+                        e.Current.DisplayString());
                 }
 
                 if (albumInfo.IsCompletedSuccessfully)
@@ -120,7 +122,8 @@ namespace Selector
                 }
                 else
                 {
-                    Logger.LogError(albumInfo.Exception, "Album info task faulted, [{context}]", e.Current.DisplayString());
+                    Logger.LogError(albumInfo.Exception, "Album info task faulted, [{context}]",
+                        e.Current.DisplayString());
                 }
 
                 //TODO: Add artist count
@@ -138,10 +141,13 @@ namespace Selector
                 }
                 else
                 {
-                    Logger.LogError(userInfo.Exception, "User info task faulted, [{context}]", e.Current.DisplayString());
+                    Logger.LogError(userInfo.Exception, "User info task faulted, [{context}]",
+                        e.Current.DisplayString());
                 }
 
-                Logger.LogDebug("Adding Last.fm data [{username}], track: {track_count}, album: {album_count}, artist: {artist_count}, user: {user_count}", Credentials.Username, trackCount, albumCount, artistCount, userCount);
+                Logger.LogDebug(
+                    "Adding Last.fm data [{username}], track: {track_count}, album: {album_count}, artist: {artist_count}, user: {user_count}",
+                    Credentials.Username, trackCount, albumCount, artistCount, userCount);
 
                 PlayCount playCount = new()
                 {
@@ -175,7 +181,7 @@ namespace Selector
         {
             var watcher = watch ?? Watcher ?? throw new ArgumentNullException("No watcher provided");
 
-            if (watcher is IPlayerWatcher watcherCast)
+            if (watcher is ISpotifyPlayerWatcher watcherCast)
             {
                 watcherCast.ItemChange += Callback;
             }
@@ -189,7 +195,7 @@ namespace Selector
         {
             var watcher = watch ?? Watcher ?? throw new ArgumentNullException("No watcher provided");
 
-            if (watcher is IPlayerWatcher watcherCast)
+            if (watcher is ISpotifyPlayerWatcher watcherCast)
             {
                 watcherCast.ItemChange -= Callback;
             }
@@ -222,4 +228,4 @@ namespace Selector
     {
         public string Username { get; set; }
     }
-}
+}
\ No newline at end of file
diff --git a/Selector/Consumers/WebHook.cs b/Selector/Consumers/WebHook.cs
index d2178b9..24abac9 100644
--- a/Selector/Consumers/WebHook.cs
+++ b/Selector/Consumers/WebHook.cs
@@ -1,8 +1,7 @@
 using System;
-using System.Net.Http;
-using System.Linq;
 using System.Collections.Generic;
-using System.Text;
+using System.Linq;
+using System.Net.Http;
 using System.Threading;
 using System.Threading.Tasks;
 using Microsoft.Extensions.Logging;
@@ -19,7 +18,7 @@ namespace Selector
 
         public bool ShouldRequest(ListeningChangeEventArgs e)
         {
-            if(Predicates is not null)
+            if (Predicates is not null)
             {
                 return Predicates.Select(p => p(e)).Aggregate((a, b) => a && b);
             }
@@ -30,9 +29,9 @@ namespace Selector
         }
     }
 
-    public class WebHook : IPlayerConsumer
+    public class WebHook : ISpotifyPlayerConsumer
     {
-        protected readonly IPlayerWatcher Watcher;
+        protected readonly ISpotifyPlayerWatcher Watcher;
         protected readonly HttpClient HttpClient;
         protected readonly ILogger<WebHook> Logger;
 
@@ -47,7 +46,7 @@ namespace Selector
         public AnalysedTrackTimeline Timeline { get; set; } = new();
 
         public WebHook(
-            IPlayerWatcher watcher,
+            ISpotifyPlayerWatcher watcher,
             HttpClient httpClient,
             WebHookConfig config,
             ILogger<WebHook> logger = null,
@@ -64,8 +63,9 @@ namespace Selector
         public void Callback(object sender, ListeningChangeEventArgs e)
         {
             if (e.Current is null) return;
-            
-            Task.Run(async () => {
+
+            Task.Run(async () =>
+            {
                 try
                 {
                     await AsyncCallback(e);
@@ -79,7 +79,11 @@ namespace Selector
 
         public async Task AsyncCallback(ListeningChangeEventArgs e)
         {
-            using var scope = Logger.BeginScope(new Dictionary<string, object>() { { "spotify_username", e.SpotifyUsername }, { "id", e.Id }, { "name", Config.Name }, { "url", Config.Url } });
+            using var scope = Logger.BeginScope(new Dictionary<string, object>()
+            {
+                { "spotify_username", e.SpotifyUsername }, { "id", e.Id }, { "name", Config.Name },
+                { "url", Config.Url }
+            });
 
             if (Config.ShouldRequest(e))
             {
@@ -101,7 +105,7 @@ namespace Selector
                         OnFailedRequest(new EventArgs());
                     }
                 }
-                catch(HttpRequestException ex)
+                catch (HttpRequestException ex)
                 {
                     Logger.LogError(ex, "Exception occured during request");
                 }
@@ -120,7 +124,7 @@ namespace Selector
         {
             var watcher = watch ?? Watcher ?? throw new ArgumentNullException("No watcher provided");
 
-            if (watcher is IPlayerWatcher watcherCast)
+            if (watcher is ISpotifyPlayerWatcher watcherCast)
             {
                 watcherCast.ItemChange += Callback;
             }
@@ -134,7 +138,7 @@ namespace Selector
         {
             var watcher = watch ?? Watcher ?? throw new ArgumentNullException("No watcher provided");
 
-            if (watcher is IPlayerWatcher watcherCast)
+            if (watcher is ISpotifyPlayerWatcher watcherCast)
             {
                 watcherCast.ItemChange -= Callback;
             }
@@ -159,4 +163,4 @@ namespace Selector
             FailedRequest?.Invoke(this, args);
         }
     }
-}
+}
\ No newline at end of file
diff --git a/Selector/Enums.cs b/Selector/Enums.cs
index 3ef9f13..f368e15 100644
--- a/Selector/Enums.cs
+++ b/Selector/Enums.cs
@@ -2,6 +2,8 @@ namespace Selector
 {
     public enum WatcherType
     {
-        Player, Playlist
+        SpotifyPlayer,
+        SpotifyPlaylist,
+        AppleMusicPlayer
     }
 }
\ No newline at end of file
diff --git a/Selector/Extensions/ServiceExtensions.cs b/Selector/Extensions/ServiceExtensions.cs
index f530398..3fd3564 100644
--- a/Selector/Extensions/ServiceExtensions.cs
+++ b/Selector/Extensions/ServiceExtensions.cs
@@ -1,10 +1,6 @@
-using System;
-using System.Collections.Generic;
-using System.Text;
+using IF.Lastfm.Core.Api;
 using Microsoft.Extensions.DependencyInjection;
 
-using IF.Lastfm.Core.Api;
-
 namespace Selector.Extensions
 {
     public static class ServiceExtensions
@@ -52,10 +48,10 @@ namespace Selector.Extensions
 
         public static IServiceCollection AddWatcher(this IServiceCollection services)
         {
-            services.AddSingleton<IWatcherFactory, WatcherFactory>();
+            services.AddSingleton<ISpotifyWatcherFactory, SpotifyWatcherFactory>();
             services.AddSingleton<IWatcherCollectionFactory, WatcherCollectionFactory>();
 
             return services;
         }
     }
-}
+}
\ No newline at end of file
diff --git a/Selector/Watcher/BaseSpotifyWatcher.cs b/Selector/Watcher/BaseSpotifyWatcher.cs
new file mode 100644
index 0000000..76f195f
--- /dev/null
+++ b/Selector/Watcher/BaseSpotifyWatcher.cs
@@ -0,0 +1,19 @@
+using System.Collections.Generic;
+using System.Linq;
+using Microsoft.Extensions.Logging;
+
+namespace Selector;
+
+public abstract class BaseSpotifyWatcher(ILogger<BaseWatcher> logger = null) : BaseWatcher(logger)
+{
+    public string SpotifyUsername { get; set; }
+
+    protected override Dictionary<string, object> LogScopeContext =>
+        new[]
+            {
+                base.LogScopeContext,
+                new Dictionary<string, object>() { { "spotify_username", SpotifyUsername } }
+            }
+            .SelectMany(x => x)
+            .ToDictionary();
+}
\ No newline at end of file
diff --git a/Selector/Watcher/BaseWatcher.cs b/Selector/Watcher/BaseWatcher.cs
index 772f942..f15cd2f 100644
--- a/Selector/Watcher/BaseWatcher.cs
+++ b/Selector/Watcher/BaseWatcher.cs
@@ -3,17 +3,15 @@ using System.Collections.Generic;
 using System.Diagnostics;
 using System.Threading;
 using System.Threading.Tasks;
-
 using Microsoft.Extensions.Logging;
 using Microsoft.Extensions.Logging.Abstractions;
 
 namespace Selector
 {
-    public abstract class BaseWatcher: IWatcher
+    public abstract class BaseWatcher : IWatcher
     {
         protected readonly ILogger<BaseWatcher> Logger;
         public string Id { get; set; }
-        public string SpotifyUsername { get; set; }
         private Stopwatch ExecutionTimer { get; set; }
 
         public BaseWatcher(ILogger<BaseWatcher> logger = null)
@@ -25,13 +23,16 @@ namespace Selector
         public abstract Task WatchOne(CancellationToken token);
         public abstract Task Reset();
 
+        protected virtual Dictionary<string, object> LogScopeContext => new()
+            { { "id", Id } };
+
         public async Task Watch(CancellationToken cancelToken)
         {
-            using var logScope = Logger.BeginScope(new Dictionary<string, object>() { { "spotify_username", SpotifyUsername }, { "id", Id } });
+            using var logScope = Logger.BeginScope(LogScopeContext);
 
             Logger.LogDebug("Starting watcher");
-            while (true) {
-
+            while (true)
+            {
                 ExecutionTimer.Start();
 
                 cancelToken.ThrowIfCancellationRequested();
@@ -49,16 +50,18 @@ namespace Selector
                 var waitTime = decimal.ToInt32(Math.Max(0, PollPeriod - ExecutionTimer.ElapsedMilliseconds));
                 ExecutionTimer.Reset();
 
-                Logger.LogTrace("Finished watch one, delaying \"{poll_period}\"ms ({wait_time}ms)...", PollPeriod, waitTime);
+                Logger.LogTrace("Finished watch one, delaying \"{poll_period}\"ms ({wait_time}ms)...", PollPeriod,
+                    waitTime);
                 await Task.Delay(waitTime, cancelToken);
             }
         }
 
         private int _pollPeriod;
+
         public int PollPeriod
         {
             get => _pollPeriod;
             set => _pollPeriod = Math.Max(0, value);
         }
     }
-}
+}
\ No newline at end of file
diff --git a/Selector/Watcher/DummyPlayerWatcher.cs b/Selector/Watcher/DummySpotifyPlayerWatcher.cs
similarity index 90%
rename from Selector/Watcher/DummyPlayerWatcher.cs
rename to Selector/Watcher/DummySpotifyPlayerWatcher.cs
index 384edc0..3520b62 100644
--- a/Selector/Watcher/DummyPlayerWatcher.cs
+++ b/Selector/Watcher/DummySpotifyPlayerWatcher.cs
@@ -1,14 +1,14 @@
 using System;
-using Microsoft.Extensions.Logging;
-using SpotifyAPI.Web;
 using System.Collections.Generic;
 using System.Threading;
 using System.Threading.Tasks;
+using Microsoft.Extensions.Logging;
+using SpotifyAPI.Web;
 
 namespace Selector
 {
-	public class DummyPlayerWatcher: PlayerWatcher
-	{
+    public class DummySpotifyPlayerWatcher : SpotifyPlayerWatcher
+    {
         private CurrentlyPlayingContext[] _contexts = new[]
         {
             new CurrentlyPlayingContext
@@ -26,7 +26,6 @@ namespace Selector
                 ShuffleState = false,
                 Context = new Context
                 {
-
                 },
                 IsPlaying = true,
                 Item = new FullTrack
@@ -71,7 +70,6 @@ namespace Selector
                 ShuffleState = false,
                 Context = new Context
                 {
-
                 },
                 IsPlaying = true,
                 Item = new FullTrack
@@ -108,11 +106,11 @@ namespace Selector
         private DateTime _lastNext = DateTime.UtcNow;
         private TimeSpan _contextLifespan = TimeSpan.FromSeconds(30);
 
-        public DummyPlayerWatcher(IEqual equalityChecker,
-                ILogger<DummyPlayerWatcher> logger = null,
-                int pollPeriod = 3000) : base(null, equalityChecker, logger, pollPeriod)
-		{
-		}
+        public DummySpotifyPlayerWatcher(IEqual equalityChecker,
+            ILogger<DummySpotifyPlayerWatcher> logger = null,
+            int pollPeriod = 3000) : base(null, equalityChecker, logger, pollPeriod)
+        {
+        }
 
         private bool ShouldCycle() => DateTime.UtcNow - _lastNext > _contextLifespan;
 
@@ -122,7 +120,7 @@ namespace Selector
 
             _contextIdx++;
 
-            if(_contextIdx >= _contexts.Length)
+            if (_contextIdx >= _contexts.Length)
             {
                 _contextIdx = 0;
             }
@@ -141,7 +139,8 @@ namespace Selector
 
             var polledCurrent = GetContext();
 
-            using var polledLogScope = Logger.BeginScope(new Dictionary<string, object>() { { "context", polledCurrent?.DisplayString() } });
+            using var polledLogScope = Logger.BeginScope(new Dictionary<string, object>()
+                { { "context", polledCurrent?.DisplayString() } });
 
             if (polledCurrent != null) StoreCurrentPlaying(polledCurrent);
 
@@ -159,5 +158,4 @@ namespace Selector
             return Task.CompletedTask;
         }
     }
-}
-
+}
\ No newline at end of file
diff --git a/Selector/Watcher/Interfaces/IPlayerWatcher.cs b/Selector/Watcher/Interfaces/ISpotifyPlayerWatcher.cs
similarity index 94%
rename from Selector/Watcher/Interfaces/IPlayerWatcher.cs
rename to Selector/Watcher/Interfaces/ISpotifyPlayerWatcher.cs
index 3c2cde1..8614b5b 100644
--- a/Selector/Watcher/Interfaces/IPlayerWatcher.cs
+++ b/Selector/Watcher/Interfaces/ISpotifyPlayerWatcher.cs
@@ -3,12 +3,13 @@ using SpotifyAPI.Web;
 
 namespace Selector
 {
-    public interface IPlayerWatcher: IWatcher
+    public interface ISpotifyPlayerWatcher : IWatcher
     {
         /// <summary>
         /// Track or episode changes
         /// </summary>
         public event EventHandler<ListeningChangeEventArgs> NetworkPoll;
+
         public event EventHandler<ListeningChangeEventArgs> ItemChange;
         public event EventHandler<ListeningChangeEventArgs> AlbumChange;
         public event EventHandler<ListeningChangeEventArgs> ArtistChange;
@@ -23,6 +24,7 @@ namespace Selector
         /// Last retrieved currently playing
         /// </summary>
         public CurrentlyPlayingContext Live { get; }
+
         public PlayerTimeline Past { get; }
     }
-}
+}
\ No newline at end of file
diff --git a/Selector/Watcher/Interfaces/IWatcherFactory.cs b/Selector/Watcher/Interfaces/ISpotifyWatcherFactory.cs
similarity index 55%
rename from Selector/Watcher/Interfaces/IWatcherFactory.cs
rename to Selector/Watcher/Interfaces/ISpotifyWatcherFactory.cs
index 8bbe1cd..e17bf38 100644
--- a/Selector/Watcher/Interfaces/IWatcherFactory.cs
+++ b/Selector/Watcher/Interfaces/ISpotifyWatcherFactory.cs
@@ -1,13 +1,10 @@
-using System;
-using System.Collections.Generic;
-using System.Text;
-using System.Threading.Tasks;
+using System.Threading.Tasks;
 
 namespace Selector
 {
-    public interface IWatcherFactory
+    public interface ISpotifyWatcherFactory
     {
         public Task<IWatcher> Get<T>(ISpotifyConfigFactory spotifyFactory, string id, int pollPeriod)
             where T : class, IWatcher;
     }
-}
+}
\ No newline at end of file
diff --git a/Selector/Watcher/PlaylistWatcher.cs b/Selector/Watcher/PlaylistWatcher.cs
index 95d631d..f23505a 100644
--- a/Selector/Watcher/PlaylistWatcher.cs
+++ b/Selector/Watcher/PlaylistWatcher.cs
@@ -1,13 +1,12 @@
 using System;
 using System.Collections.Generic;
+using System.Linq;
 using System.Threading;
 using System.Threading.Tasks;
-using SpotifyAPI.Web;
-
 using Microsoft.Extensions.Logging;
 using Microsoft.Extensions.Logging.Abstractions;
-using System.Linq;
 using Selector.Equality;
+using SpotifyAPI.Web;
 
 namespace Selector
 {
@@ -17,7 +16,7 @@ namespace Selector
         public bool PullTracks { get; set; } = true;
     }
 
-    public class PlaylistWatcher: BaseWatcher, IPlaylistWatcher
+    public class PlaylistWatcher : BaseSpotifyWatcher, IPlaylistWatcher
     {
         new private readonly ILogger<PlaylistWatcher> Logger;
         private readonly ISpotifyClient spotifyClient;
@@ -44,11 +43,11 @@ namespace Selector
         private IEqualityComparer<PlaylistTrack<IPlayableItem>> EqualityComparer = new PlayableItemEqualityComparer();
 
         public PlaylistWatcher(PlaylistWatcherConfig config,
-                ISpotifyClient spotifyClient,
-                ILogger<PlaylistWatcher> logger = null,
-                int pollPeriod = 3000
-        ) : base(logger) {
-
+            ISpotifyClient spotifyClient,
+            ILogger<PlaylistWatcher> logger = null,
+            int pollPeriod = 3000
+        ) : base(logger)
+        {
             this.spotifyClient = spotifyClient;
             this.config = config;
             Logger = logger ?? NullLogger<PlaylistWatcher>.Instance;
@@ -64,16 +63,18 @@ namespace Selector
             return Task.CompletedTask;
         }
 
-        public override async Task WatchOne(CancellationToken token = default) 
+        public override async Task WatchOne(CancellationToken token = default)
         {
             token.ThrowIfCancellationRequested();
 
-            using var logScope = Logger.BeginScope(new Dictionary<string, object> { { "playlist_id", config.PlaylistId }, { "pull_tracks", config.PullTracks } });
-            
-            try{
+            using var logScope = Logger.BeginScope(new Dictionary<string, object>
+                { { "playlist_id", config.PlaylistId }, { "pull_tracks", config.PullTracks } });
+
+            try
+            {
                 string id;
 
-                if(config.PlaylistId.Contains(':'))
+                if (config.PlaylistId.Contains(':'))
                 {
                     id = config.PlaylistId.Split(':').Last();
                 }
@@ -97,18 +98,18 @@ namespace Selector
                 await CheckSnapshot();
                 CheckStringValues();
             }
-            catch(APIUnauthorizedException e)
+            catch (APIUnauthorizedException e)
             {
                 Logger.LogDebug("Unauthorised error: [{message}] (should be refreshed and retried?)", e.Message);
                 //throw e;
             }
-            catch(APITooManyRequestsException e)
+            catch (APITooManyRequestsException e)
             {
                 Logger.LogDebug("Too many requests error: [{message}]", e.Message);
                 await Task.Delay(e.RetryAfter, token);
                 // throw e;
             }
-            catch(APIException e)
+            catch (APIException e)
             {
                 Logger.LogDebug("API error: [{message}]", e.Message);
                 // throw e;
@@ -117,7 +118,7 @@ namespace Selector
 
         private async Task CheckSnapshot()
         {
-            switch(Previous, Live)
+            switch (Previous, Live)
             {
                 case (null, not null): // gone null
                     await PageLiveTracks();
@@ -128,13 +129,14 @@ namespace Selector
 
                     if (Live.SnapshotId != Previous.SnapshotId)
                     {
-                        Logger.LogDebug("Snapshot Id changed: {previous} -> {current}", Previous.SnapshotId, Live.SnapshotId);
+                        Logger.LogDebug("Snapshot Id changed: {previous} -> {current}", Previous.SnapshotId,
+                            Live.SnapshotId);
                         await PageLiveTracks();
                         OnSnapshotChange(GetEvent());
                     }
 
                     break;
-            }   
+            }
         }
 
         private async Task PageLiveTracks()
@@ -171,7 +173,9 @@ namespace Selector
             }
         }
 
-        private PlaylistChangeEventArgs GetEvent() => PlaylistChangeEventArgs.From(Previous, Live, Past, tracks: CurrentTracks, addedTracks: LastAddedTracks, removedTracks: LastRemovedTracks, id: Id, username: SpotifyUsername);
+        private PlaylistChangeEventArgs GetEvent() => PlaylistChangeEventArgs.From(Previous, Live, Past,
+            tracks: CurrentTracks, addedTracks: LastAddedTracks, removedTracks: LastRemovedTracks, id: Id,
+            username: SpotifyUsername);
 
         private void CheckStringValues()
         {
@@ -183,11 +187,12 @@ namespace Selector
                     break;
                 case (not null, not null): // continuing non-null
 
-                    if((Previous, Live) is ({ Name: not null }, { Name: not null }))
+                    if ((Previous, Live) is ({ Name: not null }, { Name: not null }))
                     {
                         if (!Live.Name.Equals(Previous.Name))
                         {
-                            Logger.LogDebug("Name changed: {previous} -> {current}", Previous.SnapshotId, Live.SnapshotId);
+                            Logger.LogDebug("Name changed: {previous} -> {current}", Previous.SnapshotId,
+                                Live.SnapshotId);
                             OnNameChanged(GetEvent());
                         }
                     }
@@ -196,7 +201,8 @@ namespace Selector
                     {
                         if (!Live.Description.Equals(Previous.Description))
                         {
-                            Logger.LogDebug("Description changed: {previous} -> {current}", Previous.SnapshotId, Live.SnapshotId);
+                            Logger.LogDebug("Description changed: {previous} -> {current}", Previous.SnapshotId,
+                                Live.SnapshotId);
                             OnDescriptionChanged(GetEvent());
                         }
                     }
@@ -209,12 +215,13 @@ namespace Selector
         /// Store currently playing in last plays. Determine whether new list or appending required
         /// </summary>
         /// <param name="current">New currently playing to store</param>
-        private void StoreCurrentPlaying(FullPlaylist current) 
+        private void StoreCurrentPlaying(FullPlaylist current)
         {
             Past?.Add(current);
         }
 
         #region Event Firers
+
         protected virtual void OnNetworkPoll(PlaylistChangeEventArgs args)
         {
             Logger.LogTrace("Firing network poll event");
@@ -226,7 +233,7 @@ namespace Selector
         {
             Logger.LogTrace("Firing snapshot change event");
 
-            SnapshotChange?.Invoke(this, args); 
+            SnapshotChange?.Invoke(this, args);
         }
 
         protected virtual void OnTracksAdded(PlaylistChangeEventArgs args)
@@ -259,4 +266,4 @@ namespace Selector
 
         #endregion
     }
-}
+}
\ No newline at end of file
diff --git a/Selector/Watcher/PlayerWatcher.cs b/Selector/Watcher/SpotifyPlayerWatcher.cs
similarity index 79%
rename from Selector/Watcher/PlayerWatcher.cs
rename to Selector/Watcher/SpotifyPlayerWatcher.cs
index 83cf8d3..7bb910e 100644
--- a/Selector/Watcher/PlayerWatcher.cs
+++ b/Selector/Watcher/SpotifyPlayerWatcher.cs
@@ -2,16 +2,15 @@
 using System.Collections.Generic;
 using System.Threading;
 using System.Threading.Tasks;
-using SpotifyAPI.Web;
-
 using Microsoft.Extensions.Logging;
 using Microsoft.Extensions.Logging.Abstractions;
+using SpotifyAPI.Web;
 
 namespace Selector
 {
-    public class PlayerWatcher: BaseWatcher, IPlayerWatcher
+    public class SpotifyPlayerWatcher : BaseSpotifyWatcher, ISpotifyPlayerWatcher
     {
-        new protected readonly ILogger<PlayerWatcher> Logger;
+        new protected readonly ILogger<SpotifyPlayerWatcher> Logger;
         private readonly IPlayerClient spotifyClient;
         private readonly IEqual eq;
 
@@ -30,15 +29,15 @@ namespace Selector
         protected CurrentlyPlayingContext Previous { get; set; }
         public PlayerTimeline Past { get; set; } = new();
 
-        public PlayerWatcher(IPlayerClient spotifyClient, 
-                IEqual equalityChecker,
-                ILogger<PlayerWatcher> logger = null,
-                int pollPeriod = 3000
-        ) : base(logger) {
-
+        public SpotifyPlayerWatcher(IPlayerClient spotifyClient,
+            IEqual equalityChecker,
+            ILogger<SpotifyPlayerWatcher> logger = null,
+            int pollPeriod = 3000
+        ) : base(logger)
+        {
             this.spotifyClient = spotifyClient;
             eq = equalityChecker;
-            Logger = logger ?? NullLogger<PlayerWatcher>.Instance;
+            Logger = logger ?? NullLogger<SpotifyPlayerWatcher>.Instance;
             PollPeriod = pollPeriod;
         }
 
@@ -51,15 +50,17 @@ namespace Selector
             return Task.CompletedTask;
         }
 
-        public override async Task WatchOne(CancellationToken token = default) 
+        public override async Task WatchOne(CancellationToken token = default)
         {
             token.ThrowIfCancellationRequested();
-            
-            try{
+
+            try
+            {
                 Logger.LogTrace("Making Spotify call");
                 var polledCurrent = await spotifyClient.GetCurrentPlayback();
 
-                using var polledLogScope = Logger.BeginScope(new Dictionary<string, object>() { { "context", polledCurrent?.DisplayString() } });
+                using var polledLogScope = Logger.BeginScope(new Dictionary<string, object>()
+                    { { "context", polledCurrent?.DisplayString() } });
 
                 Logger.LogTrace("Received Spotify call");
 
@@ -75,20 +76,19 @@ namespace Selector
                 CheckContext();
                 CheckItem();
                 CheckDevice();
-
             }
-            catch(APIUnauthorizedException e)
+            catch (APIUnauthorizedException e)
             {
                 Logger.LogDebug("Unauthorised error: [{message}] (should be refreshed and retried?)", e.Message);
                 //throw e;
             }
-            catch(APITooManyRequestsException e)
+            catch (APITooManyRequestsException e)
             {
                 Logger.LogDebug("Too many requests error: [{message}]", e.Message);
                 await Task.Delay(e.RetryAfter, token);
                 // throw e;
             }
-            catch(APIException e)
+            catch (APIException e)
             {
                 Logger.LogDebug("API error: [{message}]", e.Message);
                 // throw e;
@@ -97,7 +97,6 @@ namespace Selector
 
         protected void CheckItem()
         {
-
             switch (Previous, Live)
             {
                 case (null or { Item: null }, { Item: FullTrack track }):
@@ -127,38 +126,46 @@ namespace Selector
                 case ({ Item: FullTrack previousTrack }, { Item: FullTrack currentTrack }):
                     if (!eq.IsEqual(previousTrack, currentTrack))
                     {
-                        Logger.LogDebug("Track changed: {prevTrack} -> {currentTrack}", previousTrack.DisplayString(), currentTrack.DisplayString());
+                        Logger.LogDebug("Track changed: {prevTrack} -> {currentTrack}", previousTrack.DisplayString(),
+                            currentTrack.DisplayString());
                         OnItemChange(GetEvent());
                     }
 
                     if (!eq.IsEqual(previousTrack.Album, currentTrack.Album))
                     {
-                        Logger.LogDebug("Album changed: {previous} -> {current}", previousTrack.Album.DisplayString(), currentTrack.Album.DisplayString());
+                        Logger.LogDebug("Album changed: {previous} -> {current}", previousTrack.Album.DisplayString(),
+                            currentTrack.Album.DisplayString());
                         OnAlbumChange(GetEvent());
                     }
 
                     if (!eq.IsEqual(previousTrack.Artists[0], currentTrack.Artists[0]))
                     {
-                        Logger.LogDebug("Artist changed: {previous} -> {current}", previousTrack.Artists.DisplayString(), currentTrack.Artists.DisplayString());
+                        Logger.LogDebug("Artist changed: {previous} -> {current}",
+                            previousTrack.Artists.DisplayString(), currentTrack.Artists.DisplayString());
                         OnArtistChange(GetEvent());
                     }
+
                     break;
                 case ({ Item: FullTrack previousTrack }, { Item: FullEpisode currentEp }):
-                    Logger.LogDebug("Media type changed: {previous}, {current}", previousTrack.DisplayString(), currentEp.DisplayString());
+                    Logger.LogDebug("Media type changed: {previous}, {current}", previousTrack.DisplayString(),
+                        currentEp.DisplayString());
                     OnContentChange(GetEvent());
                     OnItemChange(GetEvent());
                     break;
                 case ({ Item: FullEpisode previousEpisode }, { Item: FullTrack currentTrack }):
-                    Logger.LogDebug("Media type changed: {previous}, {current}", previousEpisode.DisplayString(), currentTrack.DisplayString());
+                    Logger.LogDebug("Media type changed: {previous}, {current}", previousEpisode.DisplayString(),
+                        currentTrack.DisplayString());
                     OnContentChange(GetEvent());
                     OnItemChange(GetEvent());
                     break;
                 case ({ Item: FullEpisode previousEp }, { Item: FullEpisode currentEp }):
                     if (!eq.IsEqual(previousEp, currentEp))
                     {
-                        Logger.LogDebug("Podcast changed: {previous_ep} -> {current_ep}", previousEp.DisplayString(), currentEp.DisplayString());
+                        Logger.LogDebug("Podcast changed: {previous_ep} -> {current_ep}", previousEp.DisplayString(),
+                            currentEp.DisplayString());
                         OnItemChange(GetEvent());
                     }
+
                     break;
             }
         }
@@ -173,7 +180,8 @@ namespace Selector
             }
             else if (!eq.IsEqual(Previous?.Context, Live?.Context))
             {
-                Logger.LogDebug("Context changed: {previous_context} -> {live_context}", Previous?.Context?.DisplayString() ?? "none", Live?.Context?.DisplayString() ?? "none");
+                Logger.LogDebug("Context changed: {previous_context} -> {live_context}",
+                    Previous?.Context?.DisplayString() ?? "none", Live?.Context?.DisplayString() ?? "none");
                 OnContextChange(GetEvent());
             }
         }
@@ -196,7 +204,8 @@ namespace Selector
             // IS PLAYING
             if (Previous?.IsPlaying != Live?.IsPlaying)
             {
-                Logger.LogDebug("Playing state changed: {previous_playing} -> {live_playing}", Previous?.IsPlaying, Live?.IsPlaying);
+                Logger.LogDebug("Playing state changed: {previous_playing} -> {live_playing}", Previous?.IsPlaying,
+                    Live?.IsPlaying);
                 OnPlayingChange(GetEvent());
             }
         }
@@ -206,30 +215,34 @@ namespace Selector
             // DEVICE
             if (!eq.IsEqual(Previous?.Device, Live?.Device))
             {
-                Logger.LogDebug("Device changed: {previous_device} -> {live_device}", Previous?.Device?.DisplayString() ?? "none", Live?.Device?.DisplayString() ?? "none");
+                Logger.LogDebug("Device changed: {previous_device} -> {live_device}",
+                    Previous?.Device?.DisplayString() ?? "none", Live?.Device?.DisplayString() ?? "none");
                 OnDeviceChange(GetEvent());
             }
 
             // VOLUME
             if (Previous?.Device?.VolumePercent != Live?.Device?.VolumePercent)
             {
-                Logger.LogDebug("Volume changed: {previous_volume}% -> {live_volume}%", Previous?.Device?.VolumePercent, Live?.Device?.VolumePercent);
+                Logger.LogDebug("Volume changed: {previous_volume}% -> {live_volume}%", Previous?.Device?.VolumePercent,
+                    Live?.Device?.VolumePercent);
                 OnVolumeChange(GetEvent());
             }
         }
 
-        protected ListeningChangeEventArgs GetEvent() => ListeningChangeEventArgs.From(Previous, Live, Past, id: Id, username: SpotifyUsername);
+        protected ListeningChangeEventArgs GetEvent() =>
+            ListeningChangeEventArgs.From(Previous, Live, Past, id: Id, username: SpotifyUsername);
 
         /// <summary>
         /// Store currently playing in last plays. Determine whether new list or appending required
         /// </summary>
         /// <param name="current">New currently playing to store</param>
-        protected void StoreCurrentPlaying(CurrentlyPlayingContext current) 
+        protected void StoreCurrentPlaying(CurrentlyPlayingContext current)
         {
             Past?.Add(current);
         }
 
         #region Event Firers
+
         protected virtual void OnNetworkPoll(ListeningChangeEventArgs args)
         {
             NetworkPoll?.Invoke(this, args);
@@ -237,27 +250,27 @@ namespace Selector
 
         protected virtual void OnItemChange(ListeningChangeEventArgs args)
         {
-            ItemChange?.Invoke(this, args); 
+            ItemChange?.Invoke(this, args);
         }
 
         protected virtual void OnAlbumChange(ListeningChangeEventArgs args)
         {
-            AlbumChange?.Invoke(this, args); 
+            AlbumChange?.Invoke(this, args);
         }
 
         protected virtual void OnArtistChange(ListeningChangeEventArgs args)
         {
-            ArtistChange?.Invoke(this, args); 
+            ArtistChange?.Invoke(this, args);
         }
 
         protected virtual void OnContextChange(ListeningChangeEventArgs args)
         {
-            ContextChange?.Invoke(this, args); 
+            ContextChange?.Invoke(this, args);
         }
 
         protected virtual void OnContentChange(ListeningChangeEventArgs args)
         {
-            ContentChange?.Invoke(this, args); 
+            ContentChange?.Invoke(this, args);
         }
 
         protected virtual void OnVolumeChange(ListeningChangeEventArgs args)
@@ -267,14 +280,14 @@ namespace Selector
 
         protected virtual void OnDeviceChange(ListeningChangeEventArgs args)
         {
-            DeviceChange?.Invoke(this, args); 
+            DeviceChange?.Invoke(this, args);
         }
 
         protected virtual void OnPlayingChange(ListeningChangeEventArgs args)
         {
-            PlayingChange?.Invoke(this, args); 
+            PlayingChange?.Invoke(this, args);
         }
 
         #endregion
     }
-}
+}
\ No newline at end of file
diff --git a/Selector/Watcher/WatcherFactory.cs b/Selector/Watcher/SpotifyWatcherFactory.cs
similarity index 74%
rename from Selector/Watcher/WatcherFactory.cs
rename to Selector/Watcher/SpotifyWatcherFactory.cs
index 26fc096..dc95539 100644
--- a/Selector/Watcher/WatcherFactory.cs
+++ b/Selector/Watcher/SpotifyWatcherFactory.cs
@@ -1,6 +1,4 @@
 using System;
-using System.Collections.Generic;
-using System.Text;
 using System.Threading.Tasks;
 using Microsoft.Extensions.Logging;
 using Microsoft.Extensions.Logging.Abstractions;
@@ -8,23 +6,24 @@ using SpotifyAPI.Web;
 
 namespace Selector
 {
-    public class WatcherFactory : IWatcherFactory {
-
+    public class SpotifyWatcherFactory : ISpotifyWatcherFactory
+    {
         private readonly ILoggerFactory LoggerFactory;
         private readonly IEqual Equal;
 
-        public WatcherFactory(ILoggerFactory loggerFactory, IEqual equal)
+        public SpotifyWatcherFactory(ILoggerFactory loggerFactory, IEqual equal)
         {
             LoggerFactory = loggerFactory;
             Equal = equal;
         }
 
-        public async Task<IWatcher> Get<T>(ISpotifyConfigFactory spotifyFactory, string id = null, int pollPeriod = 3000)
+        public async Task<IWatcher> Get<T>(ISpotifyConfigFactory spotifyFactory, string id = null,
+            int pollPeriod = 3000)
             where T : class, IWatcher
         {
-            if(typeof(T).IsAssignableFrom(typeof(PlayerWatcher)))
+            if (typeof(T).IsAssignableFrom(typeof(SpotifyPlayerWatcher)))
             {
-                if(!Magic.Dummy)
+                if (!Magic.Dummy)
                 {
                     var config = await spotifyFactory.GetConfig();
                     var client = new SpotifyClient(config);
@@ -32,10 +31,11 @@ namespace Selector
                     // TODO: catch spotify exceptions
                     var user = await client.UserProfile.Current();
 
-                    return new PlayerWatcher(
+                    return new SpotifyPlayerWatcher(
                         client.Player,
                         Equal,
-                        LoggerFactory?.CreateLogger<PlayerWatcher>() ?? NullLogger<PlayerWatcher>.Instance,
+                        LoggerFactory?.CreateLogger<SpotifyPlayerWatcher>() ??
+                        NullLogger<SpotifyPlayerWatcher>.Instance,
                         pollPeriod: pollPeriod
                     )
                     {
@@ -45,9 +45,10 @@ namespace Selector
                 }
                 else
                 {
-                    return new DummyPlayerWatcher(
+                    return new DummySpotifyPlayerWatcher(
                         Equal,
-                        LoggerFactory?.CreateLogger<DummyPlayerWatcher>() ?? NullLogger<DummyPlayerWatcher>.Instance,
+                        LoggerFactory?.CreateLogger<DummySpotifyPlayerWatcher>() ??
+                        NullLogger<DummySpotifyPlayerWatcher>.Instance,
                         pollPeriod: pollPeriod
                     )
                     {
@@ -81,4 +82,4 @@ namespace Selector
             }
         }
     }
-}
+}
\ No newline at end of file