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