From df986e86eeea0d63029696c07cbb6b1fd679b010 Mon Sep 17 00:00:00 2001 From: Andy Pack <andy@sarsoo.xyz> Date: Mon, 31 Mar 2025 21:33:01 +0100 Subject: [PATCH] splitting spotify and last.fm into separate projects, adding apple listen database type --- Selector.AppleMusic/AppleMusicApi.cs | 2 +- .../{Watcher => }/Consumer/IConsumer.cs | 2 +- Selector.AppleMusic/Events.cs | 7 +- Selector.AppleMusic/Watcher/WatcherFactory.cs | 18 +- Selector.CLI/Command/HostCommand.cs | 47 +- .../Factory/MappingPersisterFactory.cs | 2 + Selector.CLI/Consumer/MappingPersister.cs | 7 +- .../Extensions/CommandContextExtensions.cs | 31 +- Selector.CLI/Extensions/ServiceExtensions.cs | 30 +- Selector.CLI/ScrobbleMapper.cs | 43 +- Selector.CLI/Services/DbWatcherService.cs | 27 +- Selector.CLI/Services/LocalWatcherService.cs | 27 +- .../AppleMusic/CacheWriterConsumer.cs | 93 +++ .../Consumer/AppleMusic/PublisherConsumer.cs | 5 +- .../Consumer/Factory/AudioInjectorCaching.cs | 4 + .../Consumer/Factory/CacheWriterFactory.cs | 21 +- .../Factory/PlayCounterCachingFactory.cs | 3 + .../Consumer/Factory/PublisherFactory.cs | 4 +- .../Consumer/Spotify/AudioInjectorCaching.cs | 5 +- .../Consumer/Spotify/CacheWriterConsumer.cs | 20 +- .../{ => Spotify}/PlayCounterCaching.cs | 5 +- .../Consumer/Spotify/PublisherConsumer.cs | 13 +- .../Extensions/ServiceExtensions.cs | 12 +- Selector.Cache/Selector.Cache.csproj | 1 + Selector.Cache/Services/AudioFeaturePuller.cs | 11 +- Selector.Cache/Services/DurationPuller.cs | 32 +- Selector.Cache/Services/PlayCountPuller.cs | 24 +- Selector.Event/CacheJsonContext.cs | 5 +- .../CacheMappings/AppleMusicMapping.cs | 16 +- Selector.Event/CacheMappings/LastfmMapping.cs | 16 +- .../CacheMappings/NowPlayingMapping.cs | 69 ++- .../CacheMappings/SpotifyMapping.cs | 16 +- .../Consumers/AppleUserEventFirer.cs | 2 +- .../Consumers/SpotifyUserEventFirer.cs | 6 +- .../Consumers/UserEventFirerFactory.cs | 1 + Selector.Event/UserEventBus.cs | 1 + .../Extensions/ServiceExtensions.cs | 26 + .../Mapping/ScrobbleAlbumMapping.cs | 10 +- .../Mapping/ScrobbleArtistMapping.cs | 12 +- .../Mapping/ScrobbleMapping.cs | 22 +- .../Mapping/ScrobbleTrackMapping.cs | 16 +- .../Scrobble => Selector.LastFm}/Scrobble.cs | 20 +- .../ScrobbleMatcher.cs | 13 +- .../ScrobbleRequest.cs | 36 +- Selector.LastFm/Selector.LastFm.csproj | 20 + Selector.MAUI/App.xaml.cs | 14 +- Selector.MAUI/Selector.MAUI.csproj | 2 + Selector.MAUI/Shared/PlayCountCard.razor | 4 +- Selector.Model/ApplicationDbContext.cs | 12 + .../Extensions/ServiceExtensions.cs | 11 +- Selector.Model/Listen/AppleMusicListen.cs | 26 + ...0250331203003_add_apple_listen.Designer.cs | 548 ++++++++++++++++++ .../20250331203003_add_apple_listen.cs | 52 ++ .../ApplicationDbContextModelSnapshot.cs | 48 ++ Selector.Model/Scrobble/DBPlayCountPuller.cs | 6 +- Selector.Model/Selector.Model.csproj | 4 +- Selector.SignalR/INow.cs | 7 +- Selector.SignalR/NowHubCache.cs | 50 +- Selector.SignalR/NowHubClient.cs | 25 +- Selector.SignalR/Selector.SignalR.csproj | 1 + .../ConfigFactory/ISpotifyConfigFactory.cs | 7 +- .../ConfigFactory/RefreshTokenFactory.cs | 15 +- .../Consumer}/AudioFeatureInjector.cs | 13 +- .../Consumer}/DummyAudioFeatureInjector.cs | 10 +- .../Factory/AudioFeatureInjectorFactory.cs | 6 +- .../Consumer}/Factory/PlayCounterFactory.cs | 6 +- .../Consumer}/Factory/WebHookFactory.cs | 6 +- Selector.Spotify/Consumer/IConsumer.cs | 9 + .../Consumer}/PlayCounter.cs | 16 +- .../Consumer}/WebHook.cs | 17 +- Selector.Spotify/Credentials.cs | 17 + .../CurrentlyPlayingDTO.cs | 19 +- .../Equality/PlayableItemEqualityComparer.cs | 12 +- .../Equality/StringComparers.cs | 58 +- .../Equality/StringEqual.cs | 6 +- .../Equality/UriEqual.cs | 20 +- .../Interfaces => Selector.Spotify}/Events.cs | 30 +- .../Extensions/LoggingExtensions.cs | 12 + .../Extensions/ServiceExtensions.cs | 30 + .../Extensions/SpotifyExtensions.cs | 45 +- .../CachingRefreshTokenFactoryProvider.cs | 16 +- .../IRefreshTokenFactoryProvider.cs | 6 +- .../RefreshTokenFactoryProvider.cs | 16 +- {Selector => Selector.Spotify}/JsonContext.cs | 8 +- Selector.Spotify/Selector.Spotify.csproj | 20 + .../Timeline/AnalysedTrackTimeline.cs | 13 +- .../Timeline}/ITrackTimeline.cs | 10 +- .../Timeline/PlayerTimeline.cs | 24 +- .../Watcher/BaseSpotifyWatcher.cs | 4 +- .../Watcher/DummySpotifyPlayerWatcher.cs | 9 +- .../Watcher/Interfaces/IPlaylistWatcher.cs | 7 +- .../Interfaces/ISpotifyPlayerWatcher.cs | 30 + .../Interfaces/ISpotifyWatcherFactory.cs | 4 +- .../Watcher/PlaylistWatcher.cs | 12 +- .../Watcher/SpotifyPlayerWatcher.cs | 50 +- .../Watcher/SpotifyWatcherFactory.cs | 7 +- Selector.Tests/Consumer/AudioInjector.cs | 22 +- .../Consumer/AudioInjectorFactory.cs | 17 +- Selector.Tests/Consumer/WebHook.cs | 10 +- Selector.Tests/Equality/Equal.cs | 243 ++++---- Selector.Tests/Equality/UriEqual.cs | 213 +++---- Selector.Tests/Selector.Tests.csproj | 2 + Selector.Tests/Timeline/PlayerTimeline.cs | 141 ++--- Selector.Tests/Watcher/PlayerWatcher.cs | 8 +- Selector.Tests/Watcher/PlaylistWatcher.cs | 99 ++-- .../Pages/Account/Manage/_Layout.cshtml | 3 +- Selector.Web/Hubs/NowPlayingHub.cs | 5 +- Selector.Web/Startup.cs | 52 +- Selector.sln | 12 + Selector/Consumers/IConsumer.cs | 8 - Selector/Equality/Equal.cs | 8 +- Selector/Events.cs | 12 + Selector/Extensions/LoggingExtensions.cs | 12 - Selector/Extensions/ServiceExtensions.cs | 45 +- Selector/Listen/PlayDensity.cs | 31 + Selector/Scrobble/PlayDensity.cs | 31 - Selector/Selector.csproj | 2 - Selector/Spotify/Credentials.cs | 14 - .../Watcher/Collection/WatcherCollection.cs | 26 +- .../Interfaces/ISpotifyPlayerWatcher.cs | 30 - 120 files changed, 2112 insertions(+), 1137 deletions(-) rename Selector.AppleMusic/{Watcher => }/Consumer/IConsumer.cs (63%) create mode 100644 Selector.Cache/Consumer/AppleMusic/CacheWriterConsumer.cs rename Selector.Cache/Consumer/{ => Spotify}/PlayCounterCaching.cs (93%) create mode 100644 Selector.LastFm/Extensions/ServiceExtensions.cs rename {Selector/Scrobble => Selector.LastFm}/Mapping/ScrobbleAlbumMapping.cs (85%) rename {Selector/Scrobble => Selector.LastFm}/Mapping/ScrobbleArtistMapping.cs (80%) rename {Selector/Scrobble => Selector.LastFm}/Mapping/ScrobbleMapping.cs (90%) rename {Selector/Scrobble => Selector.LastFm}/Mapping/ScrobbleTrackMapping.cs (77%) rename {Selector/Scrobble => Selector.LastFm}/Scrobble.cs (62%) rename {Selector/Scrobble => Selector.LastFm}/ScrobbleMatcher.cs (92%) rename {Selector/Scrobble => Selector.LastFm}/ScrobbleRequest.cs (78%) create mode 100644 Selector.LastFm/Selector.LastFm.csproj create mode 100644 Selector.Model/Listen/AppleMusicListen.cs create mode 100644 Selector.Model/Migrations/20250331203003_add_apple_listen.Designer.cs create mode 100644 Selector.Model/Migrations/20250331203003_add_apple_listen.cs rename {Selector/Spotify => Selector.Spotify}/ConfigFactory/ISpotifyConfigFactory.cs (59%) rename {Selector/Spotify => Selector.Spotify}/ConfigFactory/RefreshTokenFactory.cs (88%) rename {Selector/Consumers => Selector.Spotify/Consumer}/AudioFeatureInjector.cs (94%) rename {Selector/Consumers => Selector.Spotify/Consumer}/DummyAudioFeatureInjector.cs (93%) rename {Selector/Consumers => Selector.Spotify/Consumer}/Factory/AudioFeatureInjectorFactory.cs (91%) rename {Selector/Consumers => Selector.Spotify/Consumer}/Factory/PlayCounterFactory.cs (93%) rename {Selector/Consumers => Selector.Spotify/Consumer}/Factory/WebHookFactory.cs (87%) create mode 100644 Selector.Spotify/Consumer/IConsumer.cs rename {Selector/Consumers => Selector.Spotify/Consumer}/PlayCounter.cs (95%) rename {Selector/Consumers => Selector.Spotify/Consumer}/WebHook.cs (91%) create mode 100644 Selector.Spotify/Credentials.cs rename {Selector/Spotify => Selector.Spotify}/CurrentlyPlayingDTO.cs (87%) rename {Selector => Selector.Spotify}/Equality/PlayableItemEqualityComparer.cs (61%) rename {Selector => Selector.Spotify}/Equality/StringComparers.cs (51%) rename {Selector => Selector.Spotify}/Equality/StringEqual.cs (92%) rename {Selector => Selector.Spotify}/Equality/UriEqual.cs (58%) rename {Selector/Watcher/Interfaces => Selector.Spotify}/Events.cs (68%) create mode 100644 Selector.Spotify/Extensions/LoggingExtensions.cs create mode 100644 Selector.Spotify/Extensions/ServiceExtensions.cs rename {Selector => Selector.Spotify}/Extensions/SpotifyExtensions.cs (68%) rename {Selector/Spotify => Selector.Spotify}/FactoryProvider/CachingRefreshTokenFactoryProvider.cs (82%) rename {Selector/Spotify => Selector.Spotify}/FactoryProvider/IRefreshTokenFactoryProvider.cs (62%) rename {Selector/Spotify => Selector.Spotify}/FactoryProvider/RefreshTokenFactoryProvider.cs (56%) rename {Selector => Selector.Spotify}/JsonContext.cs (53%) create mode 100644 Selector.Spotify/Selector.Spotify.csproj rename {Selector => Selector.Spotify}/Timeline/AnalysedTrackTimeline.cs (87%) rename {Selector/Timeline/Interfaces => Selector.Spotify/Timeline}/ITrackTimeline.cs (79%) rename {Selector => Selector.Spotify}/Timeline/PlayerTimeline.cs (84%) rename {Selector => Selector.Spotify}/Watcher/BaseSpotifyWatcher.cs (87%) rename {Selector => Selector.Spotify}/Watcher/DummySpotifyPlayerWatcher.cs (96%) rename {Selector => Selector.Spotify}/Watcher/Interfaces/IPlaylistWatcher.cs (87%) create mode 100644 Selector.Spotify/Watcher/Interfaces/ISpotifyPlayerWatcher.cs rename {Selector => Selector.Spotify}/Watcher/Interfaces/ISpotifyWatcherFactory.cs (74%) rename {Selector => Selector.Spotify}/Watcher/PlaylistWatcher.cs (97%) rename {Selector => Selector.Spotify}/Watcher/SpotifyPlayerWatcher.cs (84%) rename {Selector => Selector.Spotify}/Watcher/SpotifyWatcherFactory.cs (96%) create mode 100644 Selector/Events.cs delete mode 100644 Selector/Extensions/LoggingExtensions.cs create mode 100644 Selector/Listen/PlayDensity.cs delete mode 100644 Selector/Scrobble/PlayDensity.cs delete mode 100644 Selector/Spotify/Credentials.cs delete mode 100644 Selector/Watcher/Interfaces/ISpotifyPlayerWatcher.cs diff --git a/Selector.AppleMusic/AppleMusicApi.cs b/Selector.AppleMusic/AppleMusicApi.cs index a61f26c..cd69df2 100644 --- a/Selector.AppleMusic/AppleMusicApi.cs +++ b/Selector.AppleMusic/AppleMusicApi.cs @@ -41,7 +41,7 @@ public class AppleMusicApi(HttpClient client, string developerToken, string user public async Task<RecentlyPlayedTracksResponse> GetRecentlyPlayedTracks() { - var response = await MakeRequest(HttpMethod.Get, "/me/recent/played/tracks?types=songs"); + var response = await MakeRequest(HttpMethod.Get, "/me/recent/played/tracks"); CheckResponse(response); diff --git a/Selector.AppleMusic/Watcher/Consumer/IConsumer.cs b/Selector.AppleMusic/Consumer/IConsumer.cs similarity index 63% rename from Selector.AppleMusic/Watcher/Consumer/IConsumer.cs rename to Selector.AppleMusic/Consumer/IConsumer.cs index 27dce20..9f7ce76 100644 --- a/Selector.AppleMusic/Watcher/Consumer/IConsumer.cs +++ b/Selector.AppleMusic/Consumer/IConsumer.cs @@ -1,4 +1,4 @@ -namespace Selector.AppleMusic.Watcher.Consumer; +namespace Selector.AppleMusic.Consumer; public interface IApplePlayerConsumer : IConsumer<AppleListeningChangeEventArgs> { diff --git a/Selector.AppleMusic/Events.cs b/Selector.AppleMusic/Events.cs index 649fff1..3d5121c 100644 --- a/Selector.AppleMusic/Events.cs +++ b/Selector.AppleMusic/Events.cs @@ -2,16 +2,11 @@ using Selector.AppleMusic.Watcher; namespace Selector.AppleMusic; -public class AppleListeningChangeEventArgs : EventArgs +public class AppleListeningChangeEventArgs : ListeningChangeEventArgs { 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, diff --git a/Selector.AppleMusic/Watcher/WatcherFactory.cs b/Selector.AppleMusic/Watcher/WatcherFactory.cs index fb0b41c..22beb71 100644 --- a/Selector.AppleMusic/Watcher/WatcherFactory.cs +++ b/Selector.AppleMusic/Watcher/WatcherFactory.cs @@ -6,7 +6,7 @@ namespace Selector.AppleMusic.Watcher public interface IAppleMusicWatcherFactory { Task<IWatcher> Get<T>(AppleMusicApiProvider appleMusicProvider, string developerToken, string teamId, - string keyId, string userToken, int pollPeriod = 3000) + string keyId, string userToken, string id = null, int pollPeriod = 3000) where T : class, IWatcher; } @@ -22,7 +22,7 @@ namespace Selector.AppleMusic.Watcher } public async Task<IWatcher> Get<T>(AppleMusicApiProvider appleMusicProvider, string developerToken, - string teamId, string keyId, string userToken, int pollPeriod = 3000) + string teamId, string keyId, string userToken, string id = null, int pollPeriod = 3000) where T : class, IWatcher { if (typeof(T).IsAssignableFrom(typeof(AppleMusicPlayerWatcher))) @@ -36,18 +36,14 @@ namespace Selector.AppleMusic.Watcher LoggerFactory?.CreateLogger<AppleMusicPlayerWatcher>() ?? NullLogger<AppleMusicPlayerWatcher>.Instance, pollPeriod: pollPeriod - ); + ) + { + Id = id + }; } else { - return new DummySpotifyPlayerWatcher( - Equal, - LoggerFactory?.CreateLogger<DummySpotifyPlayerWatcher>() ?? - NullLogger<DummySpotifyPlayerWatcher>.Instance, - pollPeriod: pollPeriod - ) - { - }; + throw new NotImplementedException(); } } else diff --git a/Selector.CLI/Command/HostCommand.cs b/Selector.CLI/Command/HostCommand.cs index 0fb6934..54a6778 100644 --- a/Selector.CLI/Command/HostCommand.cs +++ b/Selector.CLI/Command/HostCommand.cs @@ -1,19 +1,21 @@ -using Microsoft.Extensions.DependencyInjection; +using System; +using System.CommandLine; +using System.CommandLine.Invocation; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using NLog.Extensions.Logging; +using Selector.AppleMusic.Extensions; using Selector.Cache.Extensions; using Selector.CLI.Extensions; using Selector.Events; using Selector.Extensions; -using System; -using System.CommandLine; -using System.CommandLine.Invocation; -using Selector.AppleMusic.Extensions; +using Selector.Model.Extensions; +using Selector.Spotify; namespace Selector.CLI { - public class HostRootCommand: RootCommand + public class HostRootCommand : RootCommand { public HostRootCommand() { @@ -32,7 +34,7 @@ namespace Selector.CLI { try { - var host = CreateHostBuilder(Environment.GetCommandLineArgs(),ConfigureDefault, ConfigureDefaultNlog) + var host = CreateHostBuilder(Environment.GetCommandLineArgs(), ConfigureDefault, ConfigureDefaultNlog) .Build(); var logger = host.Services.GetRequiredService<ILogger<HostCommand>>(); @@ -41,7 +43,7 @@ namespace Selector.CLI host.Run(); } - catch(Exception ex) + catch (Exception ex) { Console.WriteLine(ex); return 1; @@ -54,7 +56,7 @@ namespace Selector.CLI { AppDomain.CurrentDomain.UnhandledException += (_, e) => { - if(e.ExceptionObject is Exception ex) + if (e.ExceptionObject is Exception ex) { logger.LogError(ex, "Unhandled exception thrown"); @@ -97,7 +99,7 @@ namespace Selector.CLI Console.WriteLine("> Adding Services..."); // SERVICES services.AddHttpClient() - .ConfigureDb(config); + .ConfigureDb(config); services.AddConsumerFactories(); services.AddCLIConsumerFactories(); @@ -108,13 +110,14 @@ namespace Selector.CLI } services.AddWatcher() - .AddEvents() - .AddSpotify() - .AddAppleMusic(); + .AddSpotifyWatcher() + .AddEvents() + .AddSpotify() + .AddAppleMusic(); services.ConfigureLastFm(config) - .ConfigureEqual(config) - .ConfigureJobs(config); + .ConfigureEqual(config) + .ConfigureJobs(config); if (config.RedisOptions.Enabled) { @@ -123,8 +126,8 @@ namespace Selector.CLI Console.WriteLine("> Adding cache event maps..."); services.AddTransient<IEventMapping, FromPubSub.SpotifyLink>() - .AddTransient<IEventMapping, FromPubSub.AppleMusicLink>() - .AddTransient<IEventMapping, FromPubSub.Lastfm>(); + .AddTransient<IEventMapping, FromPubSub.AppleMusicLink>() + .AddTransient<IEventMapping, FromPubSub.Lastfm>(); Console.WriteLine("> Adding caching Spotify consumers..."); services.AddCachingSpotify(); @@ -150,14 +153,16 @@ namespace Selector.CLI public static void ConfigureDefaultNlog(HostBuilderContext context, ILoggingBuilder builder) { builder.ClearProviders() - .SetMinimumLevel(LogLevel.Trace) - .AddNLog(context.Configuration); + .SetMinimumLevel(LogLevel.Trace) + .AddNLog(context.Configuration); } - static IHostBuilder CreateHostBuilder(string[] args, Action<HostBuilderContext, IServiceCollection> buildServices, Action<HostBuilderContext, ILoggingBuilder> buildLogs) + static IHostBuilder CreateHostBuilder(string[] args, + Action<HostBuilderContext, IServiceCollection> buildServices, + Action<HostBuilderContext, ILoggingBuilder> buildLogs) => Host.CreateDefaultBuilder(args) .UseSystemd() .ConfigureServices((context, services) => buildServices(context, services)) .ConfigureLogging((context, builder) => buildLogs(context, builder)); } -} +} \ No newline at end of file diff --git a/Selector.CLI/Consumer/Factory/MappingPersisterFactory.cs b/Selector.CLI/Consumer/Factory/MappingPersisterFactory.cs index 0a43b1a..113a4de 100644 --- a/Selector.CLI/Consumer/Factory/MappingPersisterFactory.cs +++ b/Selector.CLI/Consumer/Factory/MappingPersisterFactory.cs @@ -1,6 +1,8 @@ using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; +using Selector.Spotify; +using Selector.Spotify.Consumer; namespace Selector.CLI.Consumer { diff --git a/Selector.CLI/Consumer/MappingPersister.cs b/Selector.CLI/Consumer/MappingPersister.cs index 4eb9de5..378c0fd 100644 --- a/Selector.CLI/Consumer/MappingPersister.cs +++ b/Selector.CLI/Consumer/MappingPersister.cs @@ -7,7 +7,10 @@ using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; +using Selector.Extensions; using Selector.Model; +using Selector.Spotify; +using Selector.Spotify.Consumer; using SpotifyAPI.Web; namespace Selector.CLI.Consumer @@ -36,7 +39,7 @@ namespace Selector.CLI.Consumer CancelToken = token; } - public void Callback(object sender, ListeningChangeEventArgs e) + public void Callback(object sender, SpotifyListeningChangeEventArgs e) { if (e.Current is null) return; @@ -57,7 +60,7 @@ namespace Selector.CLI.Consumer }, CancelToken); } - public async Task AsyncCallback(ListeningChangeEventArgs e) + public async Task AsyncCallback(SpotifyListeningChangeEventArgs e) { using var serviceScope = ScopeFactory.CreateScope(); using var scope = Logger.BeginScope(new Dictionary<string, object>() diff --git a/Selector.CLI/Extensions/CommandContextExtensions.cs b/Selector.CLI/Extensions/CommandContextExtensions.cs index 249eeb0..2d37d2e 100644 --- a/Selector.CLI/Extensions/CommandContextExtensions.cs +++ b/Selector.CLI/Extensions/CommandContextExtensions.cs @@ -1,12 +1,13 @@ -using IF.Lastfm.Core.Api; +using System.Linq; +using IF.Lastfm.Core.Api; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using Selector.Model; +using Selector.Spotify.ConfigFactory; using SpotifyAPI.Web; using StackExchange.Redis; -using System.Linq; namespace Selector.CLI.Extensions { @@ -16,8 +17,8 @@ namespace Selector.CLI.Extensions { var configBuild = new ConfigurationBuilder(); configBuild.AddJsonFile("appsettings.json", optional: true) - .AddJsonFile("appsettings.Development.json", optional: true) - .AddJsonFile("appsettings.Production.json", optional: true); + .AddJsonFile("appsettings.Development.json", optional: true) + .AddJsonFile("appsettings.Production.json", optional: true); context.Config = configBuild.Build().ConfigureOptions(); return context; @@ -28,10 +29,7 @@ namespace Selector.CLI.Extensions context.Logger = LoggerFactory.Create(builder => { //builder.AddConsole(a => a.); - builder.AddSimpleConsole(options => - { - options.SingleLine = true; - }); + builder.AddSimpleConsole(options => { options.SingleLine = true; }); builder.SetMinimumLevel(LogLevel.Trace); }); @@ -46,7 +44,9 @@ namespace Selector.CLI.Extensions } context.DatabaseConfig = new DbContextOptionsBuilder<ApplicationDbContext>(); - context.DatabaseConfig.UseNpgsql(string.IsNullOrWhiteSpace(connectionString) ? context.Config.DatabaseOptions.ConnectionString : connectionString); + context.DatabaseConfig.UseNpgsql(string.IsNullOrWhiteSpace(connectionString) + ? context.Config.DatabaseOptions.ConnectionString + : connectionString); return context; } @@ -58,7 +58,8 @@ namespace Selector.CLI.Extensions context.WithConfig(); } - context.LastFmClient = new LastfmClient(new LastAuth(context.Config.LastfmClient, context.Config.LastfmSecret)); + context.LastFmClient = + new LastfmClient(new LastAuth(context.Config.LastfmClient, context.Config.LastfmSecret)); return context; } @@ -72,21 +73,23 @@ namespace Selector.CLI.Extensions var refreshToken = context.Config.RefreshToken; - if(string.IsNullOrWhiteSpace(refreshToken)) + if (string.IsNullOrWhiteSpace(refreshToken)) { if (context.DatabaseConfig is null) { context.WithDb(); } - using var db = new ApplicationDbContext(context.DatabaseConfig.Options, NullLogger<ApplicationDbContext>.Instance); + using var db = new ApplicationDbContext(context.DatabaseConfig.Options, + NullLogger<ApplicationDbContext>.Instance); var user = db.Users.FirstOrDefault(u => u.UserName == "sarsoo"); refreshToken = user?.SpotifyRefreshToken; } - var configFactory = new RefreshTokenFactory(context.Config.ClientId, context.Config.ClientSecret, refreshToken); + var configFactory = + new RefreshTokenFactory(context.Config.ClientId, context.Config.ClientSecret, refreshToken); context.Spotify = new SpotifyClient(configFactory.GetConfig().Result); @@ -109,4 +112,4 @@ namespace Selector.CLI.Extensions return context; } } -} +} \ No newline at end of file diff --git a/Selector.CLI/Extensions/ServiceExtensions.cs b/Selector.CLI/Extensions/ServiceExtensions.cs index a2bc1cd..ab4e593 100644 --- a/Selector.CLI/Extensions/ServiceExtensions.cs +++ b/Selector.CLI/Extensions/ServiceExtensions.cs @@ -1,4 +1,5 @@ -using Microsoft.EntityFrameworkCore; +using System; +using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using Quartz; using Selector.Cache.Extensions; @@ -7,7 +8,7 @@ using Selector.CLI.Jobs; using Selector.Extensions; using Selector.Model; using Selector.Model.Services; -using System; +using Selector.Spotify.Equality; namespace Selector.CLI.Extensions { @@ -41,16 +42,13 @@ namespace Selector.CLI.Extensions { Console.WriteLine("> Adding Jobs..."); - services.AddQuartz(options => { - + services.AddQuartz(options => + { options.UseMicrosoftDependencyInjectionJobFactory(); options.UseSimpleTypeLoader(); options.UseInMemoryStore(); - options.UseDefaultThreadPool(tp => - { - tp.MaxConcurrency = 5; - }); + options.UseDefaultThreadPool(tp => { tp.MaxConcurrency = 5; }); if (config.JobOptions.Scrobble.Enabled) { @@ -68,7 +66,8 @@ namespace Selector.CLI.Extensions .WithIdentity("scrobble-watcher-agile-trigger") .ForJob(scrobbleKey) .StartNow() - .WithSimpleSchedule(x => x.WithInterval(config.JobOptions.Scrobble.InterJobDelay).RepeatForever()) + .WithSimpleSchedule(x => + x.WithInterval(config.JobOptions.Scrobble.InterJobDelay).RepeatForever()) .WithDescription("Periodic trigger for scrobble watcher") ); @@ -86,17 +85,14 @@ namespace Selector.CLI.Extensions .WithCronSchedule(config.JobOptions.Scrobble.FullScrobbleCron) .WithDescription("Periodic trigger for scrobble watcher") ); - } + } else { Console.WriteLine("> Skipping Scrobble Jobs..."); } }); - services.AddQuartzHostedService(options => { - - options.WaitForJobsToComplete = true; - }); + services.AddQuartzHostedService(options => { options.WaitForJobsToComplete = true; }); services.AddTransient<ScrobbleWatcherJob>(); services.AddTransient<IJob, ScrobbleWatcherJob>(); @@ -115,7 +111,7 @@ namespace Selector.CLI.Extensions ); services.AddTransient<IScrobbleRepository, ScrobbleRepository>() - .AddTransient<ISpotifyListenRepository, SpotifyListenRepository>(); + .AddTransient<ISpotifyListenRepository, SpotifyListenRepository>(); services.AddTransient<IListenRepository, MetaListenRepository>(); //services.AddTransient<IListenRepository, SpotifyListenRepository>(); @@ -152,5 +148,5 @@ namespace Selector.CLI.Extensions return services; } - } -} + } +} \ No newline at end of file diff --git a/Selector.CLI/ScrobbleMapper.cs b/Selector.CLI/ScrobbleMapper.cs index 581c8c8..08672bd 100644 --- a/Selector.CLI/ScrobbleMapper.cs +++ b/Selector.CLI/ScrobbleMapper.cs @@ -1,13 +1,13 @@ -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Logging.Abstractions; -using Selector.Model; -using Selector.Operations; -using SpotifyAPI.Web; -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Selector.Mapping; +using Selector.Model; +using Selector.Operations; +using SpotifyAPI.Web; namespace Selector { @@ -30,7 +30,9 @@ namespace Selector private readonly IScrobbleRepository scrobbleRepo; private readonly IScrobbleMappingRepository mappingRepo; - public ScrobbleMapper(ISearchClient _searchClient, ScrobbleMapperConfig _config, IScrobbleRepository _scrobbleRepository, IScrobbleMappingRepository _scrobbleMappingRepository, ILogger<ScrobbleMapper> _logger, ILoggerFactory _loggerFactory = null) + public ScrobbleMapper(ISearchClient _searchClient, ScrobbleMapperConfig _config, + IScrobbleRepository _scrobbleRepository, IScrobbleMappingRepository _scrobbleMappingRepository, + ILogger<ScrobbleMapper> _logger, ILoggerFactory _loggerFactory = null) { searchClient = _searchClient; config = _config; @@ -68,9 +70,9 @@ namespace Selector .ExceptBy(currentTracks.Select(a => (a.LastfmArtistName, a.LastfmTrackName)), a => a); var requests = tracksToPull.Select(a => new ScrobbleTrackMapping( - searchClient, - loggerFactory.CreateLogger<ScrobbleTrackMapping>(), - a.TrackName, a.ArtistName) + searchClient, + loggerFactory.CreateLogger<ScrobbleTrackMapping>(), + a.TrackName, a.ArtistName) ).ToArray(); logger.LogInformation("Found {} tracks to map, starting", requests.Length); @@ -95,11 +97,12 @@ namespace Selector if (existingTrackUris.Contains(track.Uri)) { var artistName = track.Artists.FirstOrDefault()?.Name; - var duplicates = currentTracks.Where(a => a.LastfmArtistName.Equals(artistName, StringComparison.OrdinalIgnoreCase) - && a.LastfmTrackName.Equals(track.Name, StringComparison.OrdinalIgnoreCase)); - logger.LogWarning("Found duplicate Spotify uri ({}), [{}, {}] {}", - track.Uri, - track.Name, + var duplicates = currentTracks.Where(a => + a.LastfmArtistName.Equals(artistName, StringComparison.OrdinalIgnoreCase) + && a.LastfmTrackName.Equals(track.Name, StringComparison.OrdinalIgnoreCase)); + logger.LogWarning("Found duplicate Spotify uri ({}), [{}, {}] {}", + track.Uri, + track.Name, artistName, string.Join(", ", duplicates.Select(d => $"{d.LastfmTrackName} {d.LastfmArtistName}")) ); @@ -114,7 +117,7 @@ namespace Selector }); } - if(!existingAlbumUris.Contains(track.Album.Uri)) + if (!existingAlbumUris.Contains(track.Album.Uri)) { mappingRepo.Add(new AlbumLastfmSpotifyMapping() { @@ -124,7 +127,7 @@ namespace Selector }); } - foreach(var artist in track.Artists.UnionBy(track.Album.Artists, a => a.Name)) + foreach (var artist in track.Artists.UnionBy(track.Album.Artists, a => a.Name)) { if (!existingArtistUris.Contains(artist.Uri)) { @@ -138,7 +141,7 @@ namespace Selector } } - private BatchingOperation<T> GetOperation<T>(IEnumerable<T> requests) where T: IOperation - => new (config.InterRequestDelay, config.Timeout, config.SimultaneousConnections, requests); + private BatchingOperation<T> GetOperation<T>(IEnumerable<T> requests) where T : IOperation + => new(config.InterRequestDelay, config.Timeout, config.SimultaneousConnections, requests); } -} +} \ No newline at end of file diff --git a/Selector.CLI/Services/DbWatcherService.cs b/Selector.CLI/Services/DbWatcherService.cs index 29e7f9e..ca084c1 100644 --- a/Selector.CLI/Services/DbWatcherService.cs +++ b/Selector.CLI/Services/DbWatcherService.cs @@ -16,6 +16,11 @@ using Selector.CLI.Consumer; using Selector.Events; using Selector.Model; using Selector.Model.Extensions; +using Selector.Spotify; +using Selector.Spotify.Consumer; +using Selector.Spotify.Consumer.Factory; +using Selector.Spotify.FactoryProvider; +using Selector.Spotify.Watcher; namespace Selector.CLI { @@ -106,8 +111,17 @@ namespace Selector.CLI foreach (var dbWatcher in db.Watcher .Include(w => w.User) - .Where(w => !string.IsNullOrWhiteSpace(w.User.SpotifyRefreshToken))) + .Where(w => + ((w.Type == WatcherType.SpotifyPlayer || w.Type == WatcherType.SpotifyPlaylist) && + !string.IsNullOrWhiteSpace(w.User.SpotifyRefreshToken)) || + (w.Type == WatcherType.AppleMusicPlayer && w.User.AppleMusicLinked) + )) { + using var logScope = Logger.BeginScope(new Dictionary<string, string> + { + { "username", dbWatcher.User.UserName } + }); + var watcherCollectionIdx = dbWatcher.UserId; indices.Add(watcherCollectionIdx); @@ -128,20 +142,20 @@ namespace Selector.CLI var watcherCollection = Watchers[watcherCollectionIdx]; - Logger.LogDebug("Getting Spotify factory"); - var spotifyFactory = await SpotifyFactory.GetFactory(dbWatcher.User.SpotifyRefreshToken); - IWatcher watcher = null; List<IConsumer> consumers = new(); switch (dbWatcher.Type) { case WatcherType.SpotifyPlayer: + Logger.LogDebug("Getting Spotify factory"); + var spotifyFactory = await SpotifyFactory.GetFactory(dbWatcher.User.SpotifyRefreshToken); + 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 (CacheWriterFactory is not null) consumers.Add(await CacheWriterFactory.GetSpotify()); if (PublisherFactory is not null) consumers.Add(await PublisherFactory.GetSpotify()); if (MappingPersisterFactory is not null && !Magic.Dummy) @@ -168,8 +182,9 @@ namespace Selector.CLI case WatcherType.AppleMusicPlayer: watcher = await _appleWatcherFactory.Get<AppleMusicPlayerWatcher>(_appleMusicProvider, _appleMusicOptions.Value.Key, _appleMusicOptions.Value.TeamId, _appleMusicOptions.Value.KeyId, - dbWatcher.User.AppleMusicKey); + dbWatcher.User.AppleMusicKey, id: dbWatcher.UserId); + if (CacheWriterFactory is not null) consumers.Add(await CacheWriterFactory.GetApple()); if (PublisherFactory is not null) consumers.Add(await PublisherFactory.GetApple()); break; diff --git a/Selector.CLI/Services/LocalWatcherService.cs b/Selector.CLI/Services/LocalWatcherService.cs index 77051d7..6a597a3 100644 --- a/Selector.CLI/Services/LocalWatcherService.cs +++ b/Selector.CLI/Services/LocalWatcherService.cs @@ -12,6 +12,10 @@ using Selector.AppleMusic; using Selector.AppleMusic.Watcher; using Selector.Cache; using Selector.CLI.Consumer; +using Selector.Spotify; +using Selector.Spotify.Consumer.Factory; +using Selector.Spotify.FactoryProvider; +using Selector.Spotify.Watcher; namespace Selector.CLI { @@ -115,7 +119,8 @@ namespace Selector.CLI case WatcherType.AppleMusicPlayer: var appleMusicWatcher = await _appleWatcherFactory.Get<AppleMusicPlayerWatcher>( _appleMusicApiProvider, _appleMusicOptions.Value.Key, _appleMusicOptions.Value.TeamId, - _appleMusicOptions.Value.KeyId, watcherOption.AppleUserToken); + _appleMusicOptions.Value.KeyId, watcherOption.AppleUserToken, + id: watcherOption.Name); watcher = appleMusicWatcher; break; @@ -140,11 +145,27 @@ namespace Selector.CLI break; case Consumers.CacheWriter: - consumers.Add(await ServiceProvider.GetService<CacheWriterFactory>().Get()); + if (watcher is ISpotifyPlayerWatcher or IPlaylistWatcher) + { + consumers.Add(await ServiceProvider.GetService<CacheWriterFactory>().GetSpotify()); + } + else + { + consumers.Add(await ServiceProvider.GetService<CacheWriterFactory>().GetApple()); + } + break; case Consumers.Publisher: - consumers.Add(await ServiceProvider.GetService<PublisherFactory>().GetSpotify()); + if (watcher is ISpotifyPlayerWatcher or IPlaylistWatcher) + { + consumers.Add(await ServiceProvider.GetService<PublisherFactory>().GetSpotify()); + } + else + { + consumers.Add(await ServiceProvider.GetService<PublisherFactory>().GetApple()); + } + break; case Consumers.PlayCounter: diff --git a/Selector.Cache/Consumer/AppleMusic/CacheWriterConsumer.cs b/Selector.Cache/Consumer/AppleMusic/CacheWriterConsumer.cs new file mode 100644 index 0000000..d54275f --- /dev/null +++ b/Selector.Cache/Consumer/AppleMusic/CacheWriterConsumer.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.Consumer; +using StackExchange.Redis; + +namespace Selector.Cache +{ + public class AppleCacheWriter : IApplePlayerConsumer + { + private readonly IAppleMusicPlayerWatcher Watcher; + private readonly IDatabaseAsync Db; + private readonly ILogger<AppleCacheWriter> Logger; + public TimeSpan CacheExpiry { get; set; } = TimeSpan.FromMinutes(20); + + public CancellationToken CancelToken { get; set; } + + public AppleCacheWriter( + IAppleMusicPlayerWatcher watcher, + IDatabaseAsync db, + ILogger<AppleCacheWriter> logger = null, + CancellationToken token = default + ) + { + Watcher = watcher; + Db = db; + Logger = logger ?? NullLogger<AppleCacheWriter>.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("Caching current"); + + var resp = await Db.StringSetAsync(Key.CurrentlyPlayingAppleMusic(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 IAppleMusicPlayerWatcher watcherCastApple) + { + watcherCastApple.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 watcherCastApple) + { + watcherCastApple.ItemChange -= Callback; + } + else + { + throw new ArgumentException("Provided watcher is not a PlayerWatcher"); + } + } + } +} \ No newline at end of file diff --git a/Selector.Cache/Consumer/AppleMusic/PublisherConsumer.cs b/Selector.Cache/Consumer/AppleMusic/PublisherConsumer.cs index 77e4b38..3b63d85 100644 --- a/Selector.Cache/Consumer/AppleMusic/PublisherConsumer.cs +++ b/Selector.Cache/Consumer/AppleMusic/PublisherConsumer.cs @@ -5,7 +5,7 @@ using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using Selector.AppleMusic; -using Selector.AppleMusic.Watcher.Consumer; +using Selector.AppleMusic.Consumer; using StackExchange.Redis; namespace Selector.Cache.Consumer.AppleMusic @@ -57,7 +57,8 @@ namespace Selector.Cache.Consumer.AppleMusic 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); + var receivers = + await Subscriber.PublishAsync(RedisChannel.Literal(Key.CurrentlyPlayingAppleMusic(e.Id)), payload); Logger.LogDebug("Published current, {receivers} receivers", receivers); } diff --git a/Selector.Cache/Consumer/Factory/AudioInjectorCaching.cs b/Selector.Cache/Consumer/Factory/AudioInjectorCaching.cs index 2896c3a..176e7c9 100644 --- a/Selector.Cache/Consumer/Factory/AudioInjectorCaching.cs +++ b/Selector.Cache/Consumer/Factory/AudioInjectorCaching.cs @@ -1,5 +1,9 @@ using System.Threading.Tasks; using Microsoft.Extensions.Logging; +using Selector.Spotify; +using Selector.Spotify.ConfigFactory; +using Selector.Spotify.Consumer; +using Selector.Spotify.Consumer.Factory; using SpotifyAPI.Web; using StackExchange.Redis; diff --git a/Selector.Cache/Consumer/Factory/CacheWriterFactory.cs b/Selector.Cache/Consumer/Factory/CacheWriterFactory.cs index 2ddfa01..6f73cc7 100644 --- a/Selector.Cache/Consumer/Factory/CacheWriterFactory.cs +++ b/Selector.Cache/Consumer/Factory/CacheWriterFactory.cs @@ -1,12 +1,16 @@ using System.Threading.Tasks; using Microsoft.Extensions.Logging; +using Selector.AppleMusic.Consumer; +using Selector.Spotify; +using Selector.Spotify.Consumer; using StackExchange.Redis; namespace Selector.Cache { public interface ICacheWriterFactory { - public Task<ISpotifyPlayerConsumer> Get(ISpotifyPlayerWatcher watcher = null); + public Task<ISpotifyPlayerConsumer> GetSpotify(ISpotifyPlayerWatcher watcher = null); + public Task<IApplePlayerConsumer> GetApple(IAppleMusicPlayerWatcher watcher = null); } public class CacheWriterFactory : ICacheWriterFactory @@ -23,12 +27,21 @@ namespace Selector.Cache LoggerFactory = loggerFactory; } - public Task<ISpotifyPlayerConsumer> Get(ISpotifyPlayerWatcher watcher = null) + public Task<ISpotifyPlayerConsumer> GetSpotify(ISpotifyPlayerWatcher watcher = null) { - return Task.FromResult<ISpotifyPlayerConsumer>(new CacheWriter( + return Task.FromResult<ISpotifyPlayerConsumer>(new SpotifyCacheWriter( watcher, Cache, - LoggerFactory.CreateLogger<CacheWriter>() + LoggerFactory.CreateLogger<SpotifyCacheWriter>() + )); + } + + public Task<IApplePlayerConsumer> GetApple(IAppleMusicPlayerWatcher watcher = null) + { + return Task.FromResult<IApplePlayerConsumer>(new AppleCacheWriter( + watcher, + Cache, + LoggerFactory.CreateLogger<AppleCacheWriter>() )); } } diff --git a/Selector.Cache/Consumer/Factory/PlayCounterCachingFactory.cs b/Selector.Cache/Consumer/Factory/PlayCounterCachingFactory.cs index d6c0f8a..21eae71 100644 --- a/Selector.Cache/Consumer/Factory/PlayCounterCachingFactory.cs +++ b/Selector.Cache/Consumer/Factory/PlayCounterCachingFactory.cs @@ -2,6 +2,9 @@ using System.Threading.Tasks; using IF.Lastfm.Core.Api; using Microsoft.Extensions.Logging; +using Selector.Spotify; +using Selector.Spotify.Consumer; +using Selector.Spotify.Consumer.Factory; using StackExchange.Redis; namespace Selector.Cache diff --git a/Selector.Cache/Consumer/Factory/PublisherFactory.cs b/Selector.Cache/Consumer/Factory/PublisherFactory.cs index 2f0c98d..75970e8 100644 --- a/Selector.Cache/Consumer/Factory/PublisherFactory.cs +++ b/Selector.Cache/Consumer/Factory/PublisherFactory.cs @@ -1,7 +1,9 @@ using System.Threading.Tasks; using Microsoft.Extensions.Logging; -using Selector.AppleMusic.Watcher.Consumer; +using Selector.AppleMusic.Consumer; using Selector.Cache.Consumer.AppleMusic; +using Selector.Spotify; +using Selector.Spotify.Consumer; using StackExchange.Redis; namespace Selector.Cache diff --git a/Selector.Cache/Consumer/Spotify/AudioInjectorCaching.cs b/Selector.Cache/Consumer/Spotify/AudioInjectorCaching.cs index 2284c48..32c1de3 100644 --- a/Selector.Cache/Consumer/Spotify/AudioInjectorCaching.cs +++ b/Selector.Cache/Consumer/Spotify/AudioInjectorCaching.cs @@ -3,6 +3,9 @@ using System.Text.Json; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; +using Selector.Extensions; +using Selector.Spotify; +using Selector.Spotify.Consumer; using SpotifyAPI.Web; using StackExchange.Redis; @@ -43,7 +46,7 @@ namespace Selector.Cache public async Task AsyncCacheCallback(AnalysedTrack e) { - var payload = JsonSerializer.Serialize(e.Features, JsonContext.Default.TrackAudioFeatures); + var payload = JsonSerializer.Serialize(e.Features, SpotifyJsonContext.Default.TrackAudioFeatures); Logger.LogTrace("Caching current for [{track}]", e.Track.DisplayString()); diff --git a/Selector.Cache/Consumer/Spotify/CacheWriterConsumer.cs b/Selector.Cache/Consumer/Spotify/CacheWriterConsumer.cs index a3ec7a0..6fedcda 100644 --- a/Selector.Cache/Consumer/Spotify/CacheWriterConsumer.cs +++ b/Selector.Cache/Consumer/Spotify/CacheWriterConsumer.cs @@ -4,33 +4,36 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; +using Selector.Extensions; +using Selector.Spotify; +using Selector.Spotify.Consumer; using StackExchange.Redis; namespace Selector.Cache { - public class CacheWriter : ISpotifyPlayerConsumer + public class SpotifyCacheWriter : ISpotifyPlayerConsumer { private readonly ISpotifyPlayerWatcher Watcher; private readonly IDatabaseAsync Db; - private readonly ILogger<CacheWriter> Logger; + private readonly ILogger<SpotifyCacheWriter> Logger; public TimeSpan CacheExpiry { get; set; } = TimeSpan.FromMinutes(20); public CancellationToken CancelToken { get; set; } - public CacheWriter( + public SpotifyCacheWriter( ISpotifyPlayerWatcher watcher, IDatabaseAsync db, - ILogger<CacheWriter> logger = null, + ILogger<SpotifyCacheWriter> logger = null, CancellationToken token = default ) { Watcher = watcher; Db = db; - Logger = logger ?? NullLogger<CacheWriter>.Instance; + Logger = logger ?? NullLogger<SpotifyCacheWriter>.Instance; CancelToken = token; } - public void Callback(object sender, ListeningChangeEventArgs e) + public void Callback(object sender, SpotifyListeningChangeEventArgs e) { if (e.Current is null) return; @@ -47,11 +50,12 @@ namespace Selector.Cache }, CancelToken); } - public async Task AsyncCallback(ListeningChangeEventArgs e) + public async Task AsyncCallback(SpotifyListeningChangeEventArgs e) { using var scope = Logger.GetListeningEventArgsScope(e); - var payload = JsonSerializer.Serialize((CurrentlyPlayingDTO)e, JsonContext.Default.CurrentlyPlayingDTO); + var payload = + JsonSerializer.Serialize((CurrentlyPlayingDTO)e, SpotifyJsonContext.Default.CurrentlyPlayingDTO); Logger.LogTrace("Caching current"); diff --git a/Selector.Cache/Consumer/PlayCounterCaching.cs b/Selector.Cache/Consumer/Spotify/PlayCounterCaching.cs similarity index 93% rename from Selector.Cache/Consumer/PlayCounterCaching.cs rename to Selector.Cache/Consumer/Spotify/PlayCounterCaching.cs index 84dbf7e..6145c09 100644 --- a/Selector.Cache/Consumer/PlayCounterCaching.cs +++ b/Selector.Cache/Consumer/Spotify/PlayCounterCaching.cs @@ -3,6 +3,9 @@ using System.Threading; using System.Threading.Tasks; using IF.Lastfm.Core.Api; using Microsoft.Extensions.Logging; +using Selector.Extensions; +using Selector.Spotify; +using Selector.Spotify.Consumer; using SpotifyAPI.Web; using StackExchange.Redis; @@ -47,7 +50,7 @@ namespace Selector.Cache public async Task AsyncCacheCallback(PlayCount e) { - var track = e.ListeningEvent.Current.Item as FullTrack; + var track = e.SpotifyListeningEvent.Current.Item as FullTrack; Logger.LogTrace("Caching play count for [{track}]", track.DisplayString()); var tasks = new Task[] diff --git a/Selector.Cache/Consumer/Spotify/PublisherConsumer.cs b/Selector.Cache/Consumer/Spotify/PublisherConsumer.cs index 55a640d..90f5ef5 100644 --- a/Selector.Cache/Consumer/Spotify/PublisherConsumer.cs +++ b/Selector.Cache/Consumer/Spotify/PublisherConsumer.cs @@ -4,6 +4,9 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; +using Selector.Extensions; +using Selector.Spotify; +using Selector.Spotify.Consumer; using StackExchange.Redis; namespace Selector.Cache @@ -29,7 +32,7 @@ namespace Selector.Cache CancelToken = token; } - public void Callback(object sender, ListeningChangeEventArgs e) + public void Callback(object sender, SpotifyListeningChangeEventArgs e) { if (e.Current is null) return; @@ -46,16 +49,18 @@ namespace Selector.Cache }, CancelToken); } - public async Task AsyncCallback(ListeningChangeEventArgs e) + public async Task AsyncCallback(SpotifyListeningChangeEventArgs e) { using var scope = Logger.GetListeningEventArgsScope(e); - var payload = JsonSerializer.Serialize((CurrentlyPlayingDTO)e, JsonContext.Default.CurrentlyPlayingDTO); + var payload = + JsonSerializer.Serialize((CurrentlyPlayingDTO)e, SpotifyJsonContext.Default.CurrentlyPlayingDTO); Logger.LogTrace("Publishing current"); // TODO: currently using spotify username for cache key, use db username - var receivers = await Subscriber.PublishAsync(Key.CurrentlyPlayingSpotify(e.Id), payload); + var receivers = + await Subscriber.PublishAsync(RedisChannel.Literal(Key.CurrentlyPlayingSpotify(e.Id)), payload); Logger.LogDebug("Published current, {receivers} receivers", receivers); } diff --git a/Selector.Cache/Extensions/ServiceExtensions.cs b/Selector.Cache/Extensions/ServiceExtensions.cs index 8005d61..45cb0bd 100644 --- a/Selector.Cache/Extensions/ServiceExtensions.cs +++ b/Selector.Cache/Extensions/ServiceExtensions.cs @@ -1,8 +1,6 @@ using System; -using System.Collections.Generic; -using System.Text; using Microsoft.Extensions.DependencyInjection; - +using Selector.Spotify.Consumer.Factory; using StackExchange.Redis; namespace Selector.Cache.Extensions @@ -21,8 +19,10 @@ namespace Selector.Cache.Extensions var connMulti = ConnectionMultiplexer.Connect(connectionStr); services.AddSingleton(connMulti); - services.AddTransient<IDatabaseAsync>(services => services.GetService<ConnectionMultiplexer>().GetDatabase()); - services.AddTransient<ISubscriber>(services => services.GetService<ConnectionMultiplexer>().GetSubscriber()); + services.AddTransient<IDatabaseAsync>( + services => services.GetService<ConnectionMultiplexer>().GetDatabase()); + services.AddTransient<ISubscriber>(services => + services.GetService<ConnectionMultiplexer>().GetSubscriber()); return services; } @@ -56,4 +56,4 @@ namespace Selector.Cache.Extensions return services; } } -} +} \ No newline at end of file diff --git a/Selector.Cache/Selector.Cache.csproj b/Selector.Cache/Selector.Cache.csproj index 3a03083..186249e 100644 --- a/Selector.Cache/Selector.Cache.csproj +++ b/Selector.Cache/Selector.Cache.csproj @@ -14,6 +14,7 @@ <ItemGroup> <ProjectReference Include="..\Selector.AppleMusic\Selector.AppleMusic.csproj"/> + <ProjectReference Include="..\Selector.Spotify\Selector.Spotify.csproj"/> <ProjectReference Include="..\Selector\Selector.csproj" /> </ItemGroup> diff --git a/Selector.Cache/Services/AudioFeaturePuller.cs b/Selector.Cache/Services/AudioFeaturePuller.cs index 7d63f38..7ea75e1 100644 --- a/Selector.Cache/Services/AudioFeaturePuller.cs +++ b/Selector.Cache/Services/AudioFeaturePuller.cs @@ -1,6 +1,8 @@ using System; using System.Text.Json; using System.Threading.Tasks; +using Selector.Spotify; +using Selector.Spotify.FactoryProvider; using SpotifyAPI.Web; using StackExchange.Redis; @@ -22,12 +24,12 @@ namespace Selector.Cache public async Task<TrackAudioFeatures> Get(string refreshToken, string trackId) { - if(string.IsNullOrWhiteSpace(trackId)) throw new ArgumentNullException("No track Id provided"); + if (string.IsNullOrWhiteSpace(trackId)) throw new ArgumentNullException("No track Id provided"); var track = await Cache?.StringGetAsync(Key.AudioFeature(trackId)); if (Cache is null || track == RedisValue.Null) { - if(!string.IsNullOrWhiteSpace(refreshToken) && !Magic.Dummy) + if (!string.IsNullOrWhiteSpace(refreshToken) && !Magic.Dummy) { var factory = await SpotifyFactory.GetFactory(refreshToken); var spotifyClient = new SpotifyClient(await factory.GetConfig()); @@ -35,17 +37,16 @@ namespace Selector.Cache // TODO: Error checking return await spotifyClient.Tracks.GetAudioFeatures(trackId); } - else + else { return null; } } else { - var deserialised = JsonSerializer.Deserialize(track, JsonContext.Default.TrackAudioFeatures); + var deserialised = JsonSerializer.Deserialize(track, SpotifyJsonContext.Default.TrackAudioFeatures); return deserialised; } } - } } \ No newline at end of file diff --git a/Selector.Cache/Services/DurationPuller.cs b/Selector.Cache/Services/DurationPuller.cs index f670e7b..fd890ce 100644 --- a/Selector.Cache/Services/DurationPuller.cs +++ b/Selector.Cache/Services/DurationPuller.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Microsoft.Extensions.Logging; -using Newtonsoft.Json.Linq; using SpotifyAPI.Web; using StackExchange.Redis; @@ -21,7 +20,6 @@ namespace Selector.Cache public DurationPuller( ILogger<DurationPuller> logger, - ITracksClient spotifyClient, IDatabaseAsync cache = null ) @@ -41,7 +39,8 @@ namespace Selector.Cache var cachedVal = await Cache?.HashGetAsync(Key.Track(trackId), Key.Duration); if (Cache is null || cachedVal == RedisValue.Null || cachedVal.IsNullOrEmpty) { - try { + try + { Logger.LogDebug("Missed cache, pulling"); var info = await SpotifyClient.Get(trackId); @@ -55,13 +54,14 @@ namespace Selector.Cache catch (APIUnauthorizedException e) { Logger.LogError("Unauthorised error: [{message}] (should be refreshed and retried?)", e.Message); - throw e; + throw; } catch (APITooManyRequestsException e) { - if(_retries <= 3) + if (_retries <= 3) { - Logger.LogWarning("Too many requests error, retrying ({}): [{message}]", e.RetryAfter, e.Message); + Logger.LogWarning("Too many requests error, retrying ({}): [{message}]", e.RetryAfter, + e.Message); _retries++; await Task.Delay(e.RetryAfter); return await Get(uri); @@ -69,7 +69,7 @@ namespace Selector.Cache else { Logger.LogError("Too many requests error, done retrying: [{message}]", e.Message); - throw e; + throw; } } catch (APIException e) @@ -84,13 +84,13 @@ namespace Selector.Cache else { Logger.LogError("API error, done retrying: [{message}]", e.Message); - throw e; + throw; } } } else { - return (int?) cachedVal; + return (int?)cachedVal; } } @@ -111,17 +111,17 @@ namespace Selector.Cache } else { - ret[input] = (int) cachedVal; + ret[input] = (int)cachedVal; } } var retries = new List<string>(); - foreach(var chunk in toPullFromSpotify.Chunk(50)) + foreach (var chunk in toPullFromSpotify.Chunk(50)) { await PullChunk(chunk, ret); await Task.Delay(TimeSpan.FromMilliseconds(500)); - } + } return ret; } @@ -144,7 +144,7 @@ namespace Selector.Cache catch (APIUnauthorizedException e) { Logger.LogError("Unauthorised error: [{message}] (should be refreshed and retried?)", e.Message); - throw e; + throw; } catch (APITooManyRequestsException e) { @@ -159,7 +159,7 @@ namespace Selector.Cache else { Logger.LogError("Too many requests error, done retrying: [{message}]", e.Message); - throw e; + throw; } } catch (APIException e) @@ -175,9 +175,9 @@ namespace Selector.Cache else { Logger.LogError("API error, done retrying: [{message}]", e.Message); - throw e; + throw; } } } } -} +} \ No newline at end of file diff --git a/Selector.Cache/Services/PlayCountPuller.cs b/Selector.Cache/Services/PlayCountPuller.cs index 7ab6873..83c1c79 100644 --- a/Selector.Cache/Services/PlayCountPuller.cs +++ b/Selector.Cache/Services/PlayCountPuller.cs @@ -1,13 +1,11 @@ using System; -using System.Collections.Generic; using System.Linq; -using System.Text; using System.Threading.Tasks; -using Microsoft.Extensions.Logging; - using IF.Lastfm.Core.Api; using IF.Lastfm.Core.Api.Helpers; using IF.Lastfm.Core.Objects; +using Microsoft.Extensions.Logging; +using Selector.Spotify.Consumer; using StackExchange.Redis; namespace Selector.Cache @@ -24,7 +22,6 @@ namespace Selector.Cache public PlayCountPuller( ILogger<PlayCountPuller> logger, - ITrackApi trackClient, IAlbumApi albumClient, IArtistApi artistClient, @@ -47,7 +44,7 @@ namespace Selector.Cache var trackCache = Cache?.StringGetAsync(Key.TrackPlayCount(username, track, artist)); var albumCache = Cache?.StringGetAsync(Key.AlbumPlayCount(username, album, albumArtist)); - var artistCache = Cache?.StringGetAsync(Key.ArtistPlayCount(username, artist)); + var artistCache = Cache?.StringGetAsync(Key.ArtistPlayCount(username, artist)); var userCache = Cache?.StringGetAsync(Key.UserPlayCount(username)); var cacheTasks = new Task[] { trackCache, albumCache, artistCache, userCache }; @@ -66,7 +63,7 @@ namespace Selector.Cache if (trackCache is not null && trackCache.IsCompletedSuccessfully && trackCache.Result != RedisValue.Null) { - playCount.Track = (int) trackCache.Result; + playCount.Track = (int)trackCache.Result; } else { @@ -75,7 +72,7 @@ namespace Selector.Cache if (albumCache is not null && albumCache.IsCompletedSuccessfully && albumCache.Result != RedisValue.Null) { - playCount.Album = (int) albumCache.Result; + playCount.Album = (int)albumCache.Result; } else { @@ -84,7 +81,7 @@ namespace Selector.Cache if (artistCache is not null && artistCache.IsCompletedSuccessfully && artistCache.Result != RedisValue.Null) { - playCount.Artist = (int) artistCache.Result; + playCount.Artist = (int)artistCache.Result; } else { @@ -93,14 +90,14 @@ namespace Selector.Cache if (userCache is not null && userCache.IsCompletedSuccessfully && userCache.Result != RedisValue.Null) { - playCount.User = (int) userCache.Result; + playCount.User = (int)userCache.Result; } else { userHttp = UserClient.GetInfoAsync(username); } - await Task.WhenAll(new Task[] {trackHttp, albumHttp, artistHttp, userHttp}.Where(t => t is not null)); + await Task.WhenAll(new Task[] { trackHttp, albumHttp, artistHttp, userHttp }.Where(t => t is not null)); if (trackHttp is not null && trackHttp.IsCompletedSuccessfully) { @@ -136,11 +133,12 @@ namespace Selector.Cache } else { - Logger.LogDebug("User info error [{username}] [{userHttp.Result.Status}]", username, userHttp.Result.Status); + Logger.LogDebug("User info error [{username}] [{userHttp.Result.Status}]", username, + userHttp.Result.Status); } } return playCount; } } -} +} \ No newline at end of file diff --git a/Selector.Event/CacheJsonContext.cs b/Selector.Event/CacheJsonContext.cs index a254b71..aee6637 100644 --- a/Selector.Event/CacheJsonContext.cs +++ b/Selector.Event/CacheJsonContext.cs @@ -1,4 +1,5 @@ using System.Text.Json.Serialization; +using Selector.Spotify; namespace Selector.Events { @@ -6,7 +7,7 @@ namespace Selector.Events [JsonSerializable(typeof(SpotifyLinkChange))] [JsonSerializable(typeof(AppleMusicLinkChange))] [JsonSerializable(typeof((string, CurrentlyPlayingDTO)))] - public partial class CacheJsonContext: JsonSerializerContext + public partial class CacheJsonContext : JsonSerializerContext { } -} +} \ No newline at end of file diff --git a/Selector.Event/CacheMappings/AppleMusicMapping.cs b/Selector.Event/CacheMappings/AppleMusicMapping.cs index 337ed68..40ac743 100644 --- a/Selector.Event/CacheMappings/AppleMusicMapping.cs +++ b/Selector.Event/CacheMappings/AppleMusicMapping.cs @@ -1,9 +1,7 @@ using System.Text.Json; using Microsoft.Extensions.Logging; - -using StackExchange.Redis; - using Selector.Cache; +using StackExchange.Redis; namespace Selector.Events { @@ -35,18 +33,20 @@ namespace Selector.Events { Logger.LogDebug("Forming Apple Music link event mapping FROM cache TO event bus"); - (await Subscriber.SubscribeAsync(Key.AllUserAppleMusic)).OnMessage(message => { - + (await Subscriber.SubscribeAsync(RedisChannel.Pattern(Key.AllUserAppleMusic))).OnMessage(message => + { try { var userId = Key.Param(message.Channel); - var deserialised = JsonSerializer.Deserialize(message.Message, CacheJsonContext.Default.AppleMusicLinkChange); + var deserialised = JsonSerializer.Deserialize(message.Message, + CacheJsonContext.Default.AppleMusicLinkChange); Logger.LogDebug("Received new Apple Music link event for [{userId}]", deserialised.UserId); if (!userId.Equals(deserialised.UserId)) { - Logger.LogWarning("Serialised user ID [{}] does not match cache channel [{}]", userId, deserialised.UserId); + Logger.LogWarning("Serialised user ID [{}] does not match cache channel [{}]", userId, + deserialised.UserId); } UserEvent.OnAppleMusicLinkChange(this, deserialised); @@ -88,7 +88,7 @@ namespace Selector.Events UserEvent.AppleLinkChange += async (o, e) => { var payload = JsonSerializer.Serialize(e, CacheJsonContext.Default.AppleMusicLinkChange); - await Subscriber.PublishAsync(Key.UserAppleMusic(e.UserId), payload); + await Subscriber.PublishAsync(RedisChannel.Literal(Key.UserAppleMusic(e.UserId)), payload); }; return Task.CompletedTask; diff --git a/Selector.Event/CacheMappings/LastfmMapping.cs b/Selector.Event/CacheMappings/LastfmMapping.cs index ff91b68..5ec8dcb 100644 --- a/Selector.Event/CacheMappings/LastfmMapping.cs +++ b/Selector.Event/CacheMappings/LastfmMapping.cs @@ -1,9 +1,7 @@ using System.Text.Json; using Microsoft.Extensions.Logging; - -using StackExchange.Redis; - using Selector.Cache; +using StackExchange.Redis; namespace Selector.Events { @@ -35,18 +33,20 @@ namespace Selector.Events { Logger.LogDebug("Forming Last.fm username event mapping FROM cache TO event bus"); - (await Subscriber.SubscribeAsync(Key.AllUserLastfm)).OnMessage(message => { - + (await Subscriber.SubscribeAsync(RedisChannel.Pattern(Key.AllUserLastfm))).OnMessage(message => + { try { var userId = Key.Param(message.Channel); - var deserialised = JsonSerializer.Deserialize(message.Message, CacheJsonContext.Default.LastfmChange); + var deserialised = + JsonSerializer.Deserialize(message.Message, CacheJsonContext.Default.LastfmChange); Logger.LogDebug("Received new Last.fm username event for [{userId}]", deserialised.UserId); if (!userId.Equals(deserialised.UserId)) { - Logger.LogWarning("Serialised user ID [{}] does not match cache channel [{}]", userId, deserialised.UserId); + Logger.LogWarning("Serialised user ID [{}] does not match cache channel [{}]", userId, + deserialised.UserId); } UserEvent.OnLastfmCredChange(this, deserialised); @@ -84,7 +84,7 @@ namespace Selector.Events UserEvent.LastfmCredChange += async (o, e) => { var payload = JsonSerializer.Serialize(e, CacheJsonContext.Default.LastfmChange); - await Subscriber.PublishAsync(Key.UserLastfm(e.UserId), payload); + await Subscriber.PublishAsync(RedisChannel.Literal(Key.UserLastfm(e.UserId)), payload); }; return Task.CompletedTask; diff --git a/Selector.Event/CacheMappings/NowPlayingMapping.cs b/Selector.Event/CacheMappings/NowPlayingMapping.cs index 64271e6..0c9bb2a 100644 --- a/Selector.Event/CacheMappings/NowPlayingMapping.cs +++ b/Selector.Event/CacheMappings/NowPlayingMapping.cs @@ -2,6 +2,7 @@ using System.Text.Json; using Microsoft.Extensions.Logging; using Selector.AppleMusic; using Selector.Cache; +using Selector.Spotify; using StackExchange.Redis; namespace Selector.Events @@ -27,41 +28,45 @@ namespace Selector.Events { Logger.LogDebug("Forming now playing event mapping between cache and event bus"); - (await Subscriber.SubscribeAsync(Key.AllCurrentlyPlayingSpotify)).OnMessage(message => - { - try + (await Subscriber.SubscribeAsync(RedisChannel.Pattern(Key.AllCurrentlyPlayingSpotify))).OnMessage( + message => { - var userId = Key.Param(message.Channel); + try + { + var userId = Key.Param(message.Channel); - var deserialised = - JsonSerializer.Deserialize(message.Message, JsonContext.Default.CurrentlyPlayingDTO); - Logger.LogDebug("Received new Spotify currently playing [{username}]", deserialised.Username); + var deserialised = + JsonSerializer.Deserialize(message.Message, + SpotifyJsonContext.Default.CurrentlyPlayingDTO); + Logger.LogDebug("Received new Spotify currently playing [{username}]", + deserialised.Username); - UserEvent.OnCurrentlyPlayingChangeSpotify(this, deserialised); - } - catch (Exception e) + UserEvent.OnCurrentlyPlayingChangeSpotify(this, deserialised); + } + catch (Exception e) + { + Logger.LogError(e, "Error parsing new Spotify currently playing [{message}]", message); + } + }); + + (await Subscriber.SubscribeAsync(RedisChannel.Pattern(Key.AllCurrentlyPlayingApple))).OnMessage( + message => { - Logger.LogError(e, "Error parsing new Spotify currently playing [{message}]", message); - } - }); + try + { + var userId = Key.Param(message.Channel); - (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"); - 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); - } - }); + UserEvent.OnCurrentlyPlayingChangeApple(this, deserialised); + } + catch (Exception e) + { + Logger.LogError(e, "Error parsing new Apple Music currently playing [{message}]", message); + } + }); } } } @@ -89,14 +94,14 @@ namespace Selector.Events UserEvent.CurrentlyPlayingSpotify += async (o, e) => { - var payload = JsonSerializer.Serialize(e, JsonContext.Default.CurrentlyPlayingDTO); - await Subscriber.PublishAsync(Key.CurrentlyPlayingSpotify(e.UserId), payload); + var payload = JsonSerializer.Serialize(e, SpotifyJsonContext.Default.CurrentlyPlayingDTO); + await Subscriber.PublishAsync(RedisChannel.Literal(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); + await Subscriber.PublishAsync(RedisChannel.Literal(Key.CurrentlyPlayingAppleMusic(e.Id)), payload); }; return Task.CompletedTask; diff --git a/Selector.Event/CacheMappings/SpotifyMapping.cs b/Selector.Event/CacheMappings/SpotifyMapping.cs index 88f067e..0b08c33 100644 --- a/Selector.Event/CacheMappings/SpotifyMapping.cs +++ b/Selector.Event/CacheMappings/SpotifyMapping.cs @@ -1,9 +1,7 @@ using System.Text.Json; using Microsoft.Extensions.Logging; - -using StackExchange.Redis; - using Selector.Cache; +using StackExchange.Redis; namespace Selector.Events { @@ -35,18 +33,20 @@ namespace Selector.Events { Logger.LogDebug("Forming Spotify link event mapping FROM cache TO event bus"); - (await Subscriber.SubscribeAsync(Key.AllUserSpotify)).OnMessage(message => { - + (await Subscriber.SubscribeAsync(RedisChannel.Pattern(Key.AllUserSpotify))).OnMessage(message => + { try { var userId = Key.Param(message.Channel); - var deserialised = JsonSerializer.Deserialize(message.Message, CacheJsonContext.Default.SpotifyLinkChange); + var deserialised = JsonSerializer.Deserialize(message.Message, + CacheJsonContext.Default.SpotifyLinkChange); Logger.LogDebug("Received new Spotify link event for [{userId}]", deserialised.UserId); if (!userId.Equals(deserialised.UserId)) { - Logger.LogWarning("Serialised user ID [{}] does not match cache channel [{}]", userId, deserialised.UserId); + Logger.LogWarning("Serialised user ID [{}] does not match cache channel [{}]", userId, + deserialised.UserId); } UserEvent.OnSpotifyLinkChange(this, deserialised); @@ -88,7 +88,7 @@ namespace Selector.Events UserEvent.SpotifyLinkChange += async (o, e) => { var payload = JsonSerializer.Serialize(e, CacheJsonContext.Default.SpotifyLinkChange); - await Subscriber.PublishAsync(Key.UserSpotify(e.UserId), payload); + await Subscriber.PublishAsync(RedisChannel.Literal(Key.UserSpotify(e.UserId)), payload); }; return Task.CompletedTask; diff --git a/Selector.Event/Consumers/AppleUserEventFirer.cs b/Selector.Event/Consumers/AppleUserEventFirer.cs index 4c861a0..470a33f 100644 --- a/Selector.Event/Consumers/AppleUserEventFirer.cs +++ b/Selector.Event/Consumers/AppleUserEventFirer.cs @@ -1,7 +1,7 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using Selector.AppleMusic; -using Selector.AppleMusic.Watcher.Consumer; +using Selector.AppleMusic.Consumer; namespace Selector.Events { diff --git a/Selector.Event/Consumers/SpotifyUserEventFirer.cs b/Selector.Event/Consumers/SpotifyUserEventFirer.cs index 3cbf858..06e3755 100644 --- a/Selector.Event/Consumers/SpotifyUserEventFirer.cs +++ b/Selector.Event/Consumers/SpotifyUserEventFirer.cs @@ -1,5 +1,7 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; +using Selector.Spotify; +using Selector.Spotify.Consumer; namespace Selector.Events { @@ -25,7 +27,7 @@ namespace Selector.Events CancelToken = token; } - public void Callback(object sender, ListeningChangeEventArgs e) + public void Callback(object sender, SpotifyListeningChangeEventArgs e) { if (e.Current is null) return; @@ -42,7 +44,7 @@ namespace Selector.Events }, CancelToken); } - public Task AsyncCallback(ListeningChangeEventArgs e) + public Task AsyncCallback(SpotifyListeningChangeEventArgs e) { Logger.LogDebug("Firing Spotify now playing event on user bus [{username}/{userId}]", e.SpotifyUsername, e.Id); diff --git a/Selector.Event/Consumers/UserEventFirerFactory.cs b/Selector.Event/Consumers/UserEventFirerFactory.cs index d2d6b5f..6c8fc91 100644 --- a/Selector.Event/Consumers/UserEventFirerFactory.cs +++ b/Selector.Event/Consumers/UserEventFirerFactory.cs @@ -1,4 +1,5 @@ using Microsoft.Extensions.Logging; +using Selector.Spotify; namespace Selector.Events { diff --git a/Selector.Event/UserEventBus.cs b/Selector.Event/UserEventBus.cs index 711168f..af47554 100644 --- a/Selector.Event/UserEventBus.cs +++ b/Selector.Event/UserEventBus.cs @@ -1,6 +1,7 @@ using Microsoft.Extensions.Logging; using Selector.AppleMusic; using Selector.Model; +using Selector.Spotify; namespace Selector.Events { diff --git a/Selector.LastFm/Extensions/ServiceExtensions.cs b/Selector.LastFm/Extensions/ServiceExtensions.cs new file mode 100644 index 0000000..0358b4b --- /dev/null +++ b/Selector.LastFm/Extensions/ServiceExtensions.cs @@ -0,0 +1,26 @@ +using IF.Lastfm.Core.Api; +using Microsoft.Extensions.DependencyInjection; + +namespace Selector.Extensions; + +public static class ServiceExtensions +{ + public static IServiceCollection AddLastFm(this IServiceCollection services, string client, string secret) + { + var lastAuth = new LastAuth(client, secret); + services.AddSingleton(lastAuth); + services.AddTransient(sp => new LastfmClient(sp.GetService<LastAuth>())); + + services.AddTransient<ITrackApi>(sp => sp.GetService<LastfmClient>().Track); + services.AddTransient<IAlbumApi>(sp => sp.GetService<LastfmClient>().Album); + services.AddTransient<IArtistApi>(sp => sp.GetService<LastfmClient>().Artist); + + services.AddTransient<IUserApi>(sp => sp.GetService<LastfmClient>().User); + + services.AddTransient<IChartApi>(sp => sp.GetService<LastfmClient>().Chart); + services.AddTransient<ILibraryApi>(sp => sp.GetService<LastfmClient>().Library); + services.AddTransient<ITagApi>(sp => sp.GetService<LastfmClient>().Tag); + + return services; + } +} \ No newline at end of file diff --git a/Selector/Scrobble/Mapping/ScrobbleAlbumMapping.cs b/Selector.LastFm/Mapping/ScrobbleAlbumMapping.cs similarity index 85% rename from Selector/Scrobble/Mapping/ScrobbleAlbumMapping.cs rename to Selector.LastFm/Mapping/ScrobbleAlbumMapping.cs index 232cf94..a1578a2 100644 --- a/Selector/Scrobble/Mapping/ScrobbleAlbumMapping.cs +++ b/Selector.LastFm/Mapping/ScrobbleAlbumMapping.cs @@ -1,10 +1,7 @@ using Microsoft.Extensions.Logging; using SpotifyAPI.Web; -using System; -using System.Linq; -using System.Threading.Tasks; -namespace Selector +namespace Selector.Mapping { /// <inheritdoc/> public class ScrobbleAlbumMapping : ScrobbleMapping @@ -12,7 +9,8 @@ namespace Selector public string AlbumName { get; set; } public string ArtistName { get; set; } - public ScrobbleAlbumMapping(ISearchClient _searchClient, ILogger<ScrobbleAlbumMapping> _logger, string albumName, string artistName) : base(_searchClient, _logger) + public ScrobbleAlbumMapping(ISearchClient _searchClient, ILogger<ScrobbleAlbumMapping> _logger, + string albumName, string artistName) : base(_searchClient, _logger) { AlbumName = albumName; ArtistName = artistName; @@ -38,4 +36,4 @@ namespace Selector } } } -} +} \ No newline at end of file diff --git a/Selector/Scrobble/Mapping/ScrobbleArtistMapping.cs b/Selector.LastFm/Mapping/ScrobbleArtistMapping.cs similarity index 80% rename from Selector/Scrobble/Mapping/ScrobbleArtistMapping.cs rename to Selector.LastFm/Mapping/ScrobbleArtistMapping.cs index 4b8c7d5..54f62b6 100644 --- a/Selector/Scrobble/Mapping/ScrobbleArtistMapping.cs +++ b/Selector.LastFm/Mapping/ScrobbleArtistMapping.cs @@ -1,19 +1,15 @@ using Microsoft.Extensions.Logging; using SpotifyAPI.Web; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -namespace Selector +namespace Selector.Mapping { /// <inheritdoc/> public class ScrobbleArtistMapping : ScrobbleMapping { public string ArtistName { get; set; } - public ScrobbleArtistMapping(ISearchClient _searchClient, ILogger<ScrobbleArtistMapping> _logger, string artistName) : base(_searchClient, _logger) + public ScrobbleArtistMapping(ISearchClient _searchClient, ILogger<ScrobbleArtistMapping> _logger, + string artistName) : base(_searchClient, _logger) { ArtistName = artistName; } @@ -37,4 +33,4 @@ namespace Selector } } } -} +} \ No newline at end of file diff --git a/Selector/Scrobble/Mapping/ScrobbleMapping.cs b/Selector.LastFm/Mapping/ScrobbleMapping.cs similarity index 90% rename from Selector/Scrobble/Mapping/ScrobbleMapping.cs rename to Selector.LastFm/Mapping/ScrobbleMapping.cs index c6c4981..9f452cf 100644 --- a/Selector/Scrobble/Mapping/ScrobbleMapping.cs +++ b/Selector.LastFm/Mapping/ScrobbleMapping.cs @@ -1,14 +1,15 @@ -using Microsoft.Extensions.Logging; +using System.Diagnostics; +using Microsoft.Extensions.Logging; using Selector.Operations; using SpotifyAPI.Web; -using System; -using System.Diagnostics; -using System.Threading.Tasks; -namespace Selector +namespace Selector.Mapping { - public enum LastfmObject{ - Track, Album, Artist + public enum LastfmObject + { + Track, + Album, + Artist } /// <summary> @@ -45,7 +46,7 @@ namespace Selector logger.LogInformation("Mapping Last.fm {} ({}) to Spotify", Query, QueryType); var netTime = Stopwatch.StartNew(); - currentTask = searchClient.Item(new (QueryType, Query)); + currentTask = searchClient.Item(new(QueryType, Query)); currentTask.ContinueWith(async t => { try @@ -76,7 +77,8 @@ namespace Selector } catch (Exception e) { - logger.LogError(e, "Error while mapping Last.fm {} ({}) to Spotify on attempt {}", Query, QueryType, Attempts); + logger.LogError(e, "Error while mapping Last.fm {} ({}) to Spotify on attempt {}", Query, QueryType, + Attempts); Succeeded = false; } }); @@ -93,4 +95,4 @@ namespace Selector Success?.Invoke(this, new EventArgs()); } } -} +} \ No newline at end of file diff --git a/Selector/Scrobble/Mapping/ScrobbleTrackMapping.cs b/Selector.LastFm/Mapping/ScrobbleTrackMapping.cs similarity index 77% rename from Selector/Scrobble/Mapping/ScrobbleTrackMapping.cs rename to Selector.LastFm/Mapping/ScrobbleTrackMapping.cs index ce2f0be..6ad5d4e 100644 --- a/Selector/Scrobble/Mapping/ScrobbleTrackMapping.cs +++ b/Selector.LastFm/Mapping/ScrobbleTrackMapping.cs @@ -1,12 +1,7 @@ using Microsoft.Extensions.Logging; using SpotifyAPI.Web; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -namespace Selector +namespace Selector.Mapping { /// <inheritdoc/> public class ScrobbleTrackMapping : ScrobbleMapping @@ -14,7 +9,8 @@ namespace Selector public string TrackName { get; set; } public string ArtistName { get; set; } - public ScrobbleTrackMapping(ISearchClient _searchClient, ILogger<ScrobbleTrackMapping> _logger, string trackName, string artistName) : base(_searchClient, _logger) + public ScrobbleTrackMapping(ISearchClient _searchClient, ILogger<ScrobbleTrackMapping> _logger, + string trackName, string artistName) : base(_searchClient, _logger) { TrackName = trackName; ArtistName = artistName; @@ -32,12 +28,12 @@ namespace Selector { var topResult = response.Result.Tracks.Items.FirstOrDefault(); - if(topResult is not null - && topResult.Name.Equals(TrackName, StringComparison.InvariantCultureIgnoreCase) + if (topResult is not null + && topResult.Name.Equals(TrackName, StringComparison.InvariantCultureIgnoreCase) && topResult.Artists.First().Name.Equals(ArtistName, StringComparison.InvariantCultureIgnoreCase)) { result = topResult; } } } -} +} \ No newline at end of file diff --git a/Selector/Scrobble/Scrobble.cs b/Selector.LastFm/Scrobble.cs similarity index 62% rename from Selector/Scrobble/Scrobble.cs rename to Selector.LastFm/Scrobble.cs index fa4542b..1ef1c58 100644 --- a/Selector/Scrobble/Scrobble.cs +++ b/Selector.LastFm/Scrobble.cs @@ -1,19 +1,21 @@ -using System; +using IF.Lastfm.Core.Objects; namespace Selector { - public class Scrobble: IListen + public class Scrobble : IListen { public DateTime Timestamp { get; set; } - public string TrackName { get; set; } - public string AlbumName { get; set; } + public string? TrackName { get; set; } + public string? AlbumName { get; set; } + /// <summary> /// Not populated by default from the service, where not the same as <see cref="ArtistName"/> these have been manually entered /// </summary> - public string AlbumArtistName { get; set; } - public string ArtistName { get; set; } - - public static explicit operator Scrobble(IF.Lastfm.Core.Objects.LastTrack track) => new() + public string? AlbumArtistName { get; set; } + + public string? ArtistName { get; set; } + + public static explicit operator Scrobble(LastTrack track) => new() { Timestamp = track.TimePlayed?.UtcDateTime ?? DateTime.MinValue, @@ -24,4 +26,4 @@ namespace Selector public override string ToString() => $"({Timestamp}) {TrackName}, {AlbumName}, {ArtistName}"; } -} +} \ No newline at end of file diff --git a/Selector/Scrobble/ScrobbleMatcher.cs b/Selector.LastFm/ScrobbleMatcher.cs similarity index 92% rename from Selector/Scrobble/ScrobbleMatcher.cs rename to Selector.LastFm/ScrobbleMatcher.cs index 05afc4a..aa858b4 100644 --- a/Selector/Scrobble/ScrobbleMatcher.cs +++ b/Selector.LastFm/ScrobbleMatcher.cs @@ -1,7 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Linq; +using System.Diagnostics.CodeAnalysis; namespace Selector { @@ -13,7 +10,8 @@ namespace Selector public static bool MatchTime(IListen nativeScrobble, IListen serviceScrobble) => serviceScrobble.Timestamp.Equals(nativeScrobble.Timestamp); - public static (IEnumerable<IListen>, IEnumerable<IListen>) IdentifyDiffs(IEnumerable<IListen> existing, IEnumerable<IListen> toApply, bool matchContents = true) + public static (IEnumerable<IListen>, IEnumerable<IListen>) IdentifyDiffs(IEnumerable<IListen> existing, + IEnumerable<IListen> toApply, bool matchContents = true) { existing = existing.OrderBy(s => s.Timestamp); toApply = toApply.OrderBy(s => s.Timestamp); @@ -96,7 +94,8 @@ namespace Selector } } - public static (IEnumerable<IListen>, IEnumerable<IListen>) IdentifyDiffsContains(IEnumerable<IListen> existing, IEnumerable<IListen> toApply) + public static (IEnumerable<IListen>, IEnumerable<IListen>) IdentifyDiffsContains(IEnumerable<IListen> existing, + IEnumerable<IListen> toApply) { var toAdd = toApply.Where(s => !existing.Contains(s, new ListenComp())); var toRemove = existing.Where(s => !toApply.Contains(s, new ListenComp())); @@ -111,4 +110,4 @@ namespace Selector public int GetHashCode([DisallowNull] IListen obj) => obj.Timestamp.GetHashCode(); } } -} +} \ No newline at end of file diff --git a/Selector/Scrobble/ScrobbleRequest.cs b/Selector.LastFm/ScrobbleRequest.cs similarity index 78% rename from Selector/Scrobble/ScrobbleRequest.cs rename to Selector.LastFm/ScrobbleRequest.cs index 0884df1..90aec08 100644 --- a/Selector/Scrobble/ScrobbleRequest.cs +++ b/Selector.LastFm/ScrobbleRequest.cs @@ -1,13 +1,9 @@ -using IF.Lastfm.Core.Api; +using System.Diagnostics; +using IF.Lastfm.Core.Api; using IF.Lastfm.Core.Api.Helpers; using IF.Lastfm.Core.Objects; using Microsoft.Extensions.Logging; using Selector.Operations; -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Linq; -using System.Threading.Tasks; namespace Selector { @@ -34,7 +30,8 @@ namespace Selector private TaskCompletionSource AggregateTaskSource { get; set; } = new(); public Task Task => AggregateTaskSource.Task; - public ScrobbleRequest(IUserApi _userClient, ILogger<ScrobbleRequest> _logger, string _username, int _pageNumber, int _pageSize, DateTime? _from, DateTime? _to, int maxRetries = 5) + public ScrobbleRequest(IUserApi _userClient, ILogger<ScrobbleRequest> _logger, string _username, + int _pageNumber, int _pageSize, DateTime? _from, DateTime? _to, int maxRetries = 5) { userClient = _userClient; logger = _logger; @@ -50,15 +47,24 @@ namespace Selector public Task Execute() { - using var scope = logger.BeginScope(new Dictionary<string, object>() { { "username", username }, { "page_number", pageNumber }, { "page_size", pageSize }, { "from", from }, { "to", to } }); + using var scope = logger.BeginScope(new Dictionary<string, object>() + { + { "username", username }, { "page_number", pageNumber }, { "page_size", pageSize }, { "from", from }, + { "to", to } + }); logger.LogInformation("Starting request"); var netTime = Stopwatch.StartNew(); - currentTask = userClient.GetRecentScrobbles(username, pagenumber: pageNumber, count: pageSize, from: from, to: to); + currentTask = + userClient.GetRecentScrobbles(username, pagenumber: pageNumber, count: pageSize, from: from, to: to); currentTask.ContinueWith(async t => { - using var scope = logger.BeginScope(new Dictionary<string, object>() { { "username", username }, { "page_number", pageNumber }, { "page_size", pageSize }, { "from", from }, { "to", to } }); + using var scope = logger.BeginScope(new Dictionary<string, object>() + { + { "username", username }, { "page_number", pageNumber }, { "page_size", pageSize }, + { "from", from }, { "to", to } + }); try { @@ -81,12 +87,14 @@ namespace Selector { if (Attempts < MaxAttempts) { - logger.LogDebug("Request failed: {}, retrying ({} of {})", result.Status, Attempts + 1, MaxAttempts); + logger.LogDebug("Request failed: {}, retrying ({} of {})", result.Status, Attempts + 1, + MaxAttempts); await Execute(); } else { - logger.LogDebug("Request failed: {}, max retries exceeded {}, not retrying", result.Status, MaxAttempts); + logger.LogDebug("Request failed: {}, max retries exceeded {}, not retrying", + result.Status, MaxAttempts); AggregateTaskSource.SetCanceled(); } } @@ -97,7 +105,7 @@ namespace Selector AggregateTaskSource.SetException(t.Exception); } } - catch(Exception e) + catch (Exception e) { logger.LogError(e, "Error while making scrobble request on attempt {}", Attempts); Succeeded = false; @@ -113,4 +121,4 @@ namespace Selector Success?.Invoke(this, new EventArgs()); } } -} +} \ No newline at end of file diff --git a/Selector.LastFm/Selector.LastFm.csproj b/Selector.LastFm/Selector.LastFm.csproj new file mode 100644 index 0000000..9b2f3de --- /dev/null +++ b/Selector.LastFm/Selector.LastFm.csproj @@ -0,0 +1,20 @@ +<Project Sdk="Microsoft.NET.Sdk"> + + <PropertyGroup> + <TargetFramework>net9.0</TargetFramework> + <ImplicitUsings>enable</ImplicitUsings> + <Nullable>enable</Nullable> + </PropertyGroup> + + <ItemGroup> + <PackageReference Include="Microsoft.Extensions.Logging" Version="9.0.0"/> + <PackageReference Include="SpotifyAPI.Web" Version="7.2.1"/> + <PackageReference Include="Inflatable.Lastfm" Version="1.2.0"/> + <PackageReference Include="System.Linq.Async" Version="6.0.1"/> + </ItemGroup> + + <ItemGroup> + <ProjectReference Include="..\Selector\Selector.csproj"/> + </ItemGroup> + +</Project> diff --git a/Selector.MAUI/App.xaml.cs b/Selector.MAUI/App.xaml.cs index ff7334e..6265009 100644 --- a/Selector.MAUI/App.xaml.cs +++ b/Selector.MAUI/App.xaml.cs @@ -10,17 +10,16 @@ public partial class App : Application private readonly ILogger<App> logger; public App(NowHubClient nowClient, ILogger<App> logger) - { - InitializeComponent(); + { + InitializeComponent(); - MainPage = new MainPage(); this.nowClient = nowClient; this.logger = logger; } protected override Window CreateWindow(IActivationState activationState) { - Window window = base.CreateWindow(activationState); + Window window = new Window(new MainPage()); window.Resumed += async (s, e) => { @@ -36,16 +35,13 @@ public partial class App : Application await nowClient.OnConnected(); logger.LogInformation("Hubs reconnected"); - } - catch(Exception ex) + catch (Exception ex) { logger.LogError(ex, "Error while reconnecting hubs"); } - }; return window; } -} - +} \ No newline at end of file diff --git a/Selector.MAUI/Selector.MAUI.csproj b/Selector.MAUI/Selector.MAUI.csproj index fee0cc9..19d36fe 100644 --- a/Selector.MAUI/Selector.MAUI.csproj +++ b/Selector.MAUI/Selector.MAUI.csproj @@ -85,6 +85,8 @@ </ItemGroup> <ItemGroup> + <PackageReference Include="Microsoft.Maui.Controls" Version="9.0.14"/> + <PackageReference Include="Microsoft.AspNetCore.Components.WebView.Maui" Version="9.0.14"/> <PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="9.0.0" /> <PackageReference Include="Microsoft.Extensions.Http" Version="9.0.0" /> <PackageReference Include="System.Net.Http.Json" Version="9.0.0" /> diff --git a/Selector.MAUI/Shared/PlayCountCard.razor b/Selector.MAUI/Shared/PlayCountCard.razor index 162c867..6d8fffc 100644 --- a/Selector.MAUI/Shared/PlayCountCard.razor +++ b/Selector.MAUI/Shared/PlayCountCard.razor @@ -1,5 +1,5 @@ -@using SpotifyAPI.Web; - +@using Selector.Spotify.Consumer +@using SpotifyAPI.Web @if (Count is not null) { <div class="card info-card"> diff --git a/Selector.Model/ApplicationDbContext.cs b/Selector.Model/ApplicationDbContext.cs index 94b2f21..ae7b162 100644 --- a/Selector.Model/ApplicationDbContext.cs +++ b/Selector.Model/ApplicationDbContext.cs @@ -20,6 +20,7 @@ namespace Selector.Model public DbSet<ArtistLastfmSpotifyMapping> ArtistMapping { get; set; } public DbSet<SpotifyListen> SpotifyListen { get; set; } + public DbSet<AppleMusicListen> AppleMusicListen { get; set; } public ApplicationDbContext( DbContextOptions<ApplicationDbContext> options, @@ -102,6 +103,17 @@ namespace Selector.Model //modelBuilder.Entity<SpotifyListen>() // .HasIndex(x => new { x.UserId, x.ArtistName, x.TrackName }); + modelBuilder.Entity<AppleMusicListen>().HasKey(s => s.Id); + modelBuilder.Entity<AppleMusicListen>() + .Property(s => s.TrackName) + .UseCollation("case_insensitive"); + modelBuilder.Entity<AppleMusicListen>() + .Property(s => s.AlbumName) + .UseCollation("case_insensitive"); + modelBuilder.Entity<AppleMusicListen>() + .Property(s => s.ArtistName) + .UseCollation("case_insensitive"); + SeedData.Seed(modelBuilder); } diff --git a/Selector.Model/Extensions/ServiceExtensions.cs b/Selector.Model/Extensions/ServiceExtensions.cs index e564136..1ab8212 100644 --- a/Selector.Model/Extensions/ServiceExtensions.cs +++ b/Selector.Model/Extensions/ServiceExtensions.cs @@ -2,11 +2,20 @@ using Microsoft.Extensions.DependencyInjection; using Selector.Cache; using Selector.Model.Authorisation; +using Selector.Spotify; +using Selector.Spotify.Watcher; namespace Selector.Model.Extensions { public static class ServiceExtensions { + public static IServiceCollection AddSpotifyWatcher(this IServiceCollection services) + { + services.AddSingleton<ISpotifyWatcherFactory, SpotifyWatcherFactory>(); + + return services; + } + public static void AddAuthorisationHandlers(this IServiceCollection services) { services.AddScoped<IAuthorizationHandler, WatcherIsOwnerAuthHandler>(); @@ -23,4 +32,4 @@ namespace Selector.Model.Extensions return services; } } -} +} \ No newline at end of file diff --git a/Selector.Model/Listen/AppleMusicListen.cs b/Selector.Model/Listen/AppleMusicListen.cs new file mode 100644 index 0000000..41f9b7c --- /dev/null +++ b/Selector.Model/Listen/AppleMusicListen.cs @@ -0,0 +1,26 @@ +using Selector.AppleMusic.Watcher; + +namespace Selector.Model; + +public class AppleMusicListen : Listen, IUserListen +{ + public int Id { get; set; } + + public string TrackId { get; set; } + public string Isrc { get; set; } + + public string UserId { get; set; } + public ApplicationUser User { get; set; } + + public static explicit operator AppleMusicListen(AppleMusicCurrentlyPlayingContext track) => new() + { + Timestamp = track.FirstSeen, + + TrackId = track.Track.Id, + Isrc = track.Track.Attributes.Isrc, + + TrackName = track.Track.Attributes.Name, + AlbumName = track.Track.Attributes.AlbumName, + ArtistName = track.Track.Attributes.ArtistName, + }; +} \ No newline at end of file diff --git a/Selector.Model/Migrations/20250331203003_add_apple_listen.Designer.cs b/Selector.Model/Migrations/20250331203003_add_apple_listen.Designer.cs new file mode 100644 index 0000000..c100829 --- /dev/null +++ b/Selector.Model/Migrations/20250331203003_add_apple_listen.Designer.cs @@ -0,0 +1,548 @@ +// <auto-generated /> +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using Selector.Model; + +#nullable disable + +namespace Selector.Model.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20250331203003_add_apple_listen")] + partial class add_apple_listen + { + /// <inheritdoc /> + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("Npgsql:CollationDefinition:case_insensitive", "en-u-ks-primary,en-u-ks-primary,icu,False") + .HasAnnotation("ProductVersion", "9.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => + { + b.Property<string>("Id") + .HasColumnType("text"); + + b.Property<string>("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("text"); + + b.Property<string>("Name") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property<string>("NormalizedName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex"); + + b.ToTable("AspNetRoles", (string)null); + + b.HasData( + new + { + Id = "00c64c0a-3387-4933-9575-83443fa9092b", + Name = "Admin", + NormalizedName = "ADMIN" + }); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id")); + + b.Property<string>("ClaimType") + .HasColumnType("text"); + + b.Property<string>("ClaimValue") + .HasColumnType("text"); + + b.Property<string>("RoleId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id")); + + b.Property<string>("ClaimType") + .HasColumnType("text"); + + b.Property<string>("ClaimValue") + .HasColumnType("text"); + + b.Property<string>("UserId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b => + { + b.Property<string>("LoginProvider") + .HasColumnType("text"); + + b.Property<string>("ProviderKey") + .HasColumnType("text"); + + b.Property<string>("ProviderDisplayName") + .HasColumnType("text"); + + b.Property<string>("UserId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b => + { + b.Property<string>("UserId") + .HasColumnType("text"); + + b.Property<string>("RoleId") + .HasColumnType("text"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b => + { + b.Property<string>("UserId") + .HasColumnType("text"); + + b.Property<string>("LoginProvider") + .HasColumnType("text"); + + b.Property<string>("Name") + .HasColumnType("text"); + + b.Property<string>("Value") + .HasColumnType("text"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens", (string)null); + }); + + modelBuilder.Entity("Selector.Model.AlbumLastfmSpotifyMapping", b => + { + b.Property<string>("SpotifyUri") + .HasColumnType("text"); + + b.Property<string>("LastfmAlbumName") + .HasColumnType("text") + .UseCollation("case_insensitive"); + + b.Property<string>("LastfmArtistName") + .HasColumnType("text") + .UseCollation("case_insensitive"); + + b.HasKey("SpotifyUri"); + + b.ToTable("AlbumMapping"); + }); + + modelBuilder.Entity("Selector.Model.AppleMusicListen", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id")); + + b.Property<string>("AlbumName") + .HasColumnType("text") + .UseCollation("case_insensitive"); + + b.Property<string>("ArtistName") + .HasColumnType("text") + .UseCollation("case_insensitive"); + + b.Property<string>("Isrc") + .HasColumnType("text"); + + b.Property<DateTime>("Timestamp") + .HasColumnType("timestamp with time zone"); + + b.Property<string>("TrackId") + .HasColumnType("text"); + + b.Property<string>("TrackName") + .HasColumnType("text") + .UseCollation("case_insensitive"); + + b.Property<string>("UserId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AppleMusicListen"); + }); + + modelBuilder.Entity("Selector.Model.ApplicationUser", b => + { + b.Property<string>("Id") + .HasColumnType("text"); + + b.Property<int>("AccessFailedCount") + .HasColumnType("integer"); + + b.Property<string>("AppleMusicKey") + .HasColumnType("text"); + + b.Property<DateTime>("AppleMusicLastRefresh") + .HasColumnType("timestamp with time zone"); + + b.Property<bool>("AppleMusicLinked") + .HasColumnType("boolean"); + + b.Property<string>("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("text"); + + b.Property<string>("Email") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property<bool>("EmailConfirmed") + .HasColumnType("boolean"); + + b.Property<string>("LastFmUsername") + .HasColumnType("text") + .UseCollation("case_insensitive"); + + b.Property<bool>("LockoutEnabled") + .HasColumnType("boolean"); + + b.Property<DateTimeOffset?>("LockoutEnd") + .HasColumnType("timestamp with time zone"); + + b.Property<string>("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property<string>("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property<string>("PasswordHash") + .HasColumnType("text"); + + b.Property<string>("PhoneNumber") + .HasColumnType("text"); + + b.Property<bool>("PhoneNumberConfirmed") + .HasColumnType("boolean"); + + b.Property<bool>("SaveScrobbles") + .HasColumnType("boolean"); + + b.Property<string>("SecurityStamp") + .HasColumnType("text"); + + b.Property<string>("SpotifyAccessToken") + .HasColumnType("text"); + + b.Property<bool>("SpotifyIsLinked") + .HasColumnType("boolean"); + + b.Property<DateTime>("SpotifyLastRefresh") + .HasColumnType("timestamp with time zone"); + + b.Property<string>("SpotifyRefreshToken") + .HasColumnType("text"); + + b.Property<int>("SpotifyTokenExpiry") + .HasColumnType("integer"); + + b.Property<bool>("TwoFactorEnabled") + .HasColumnType("boolean"); + + b.Property<string>("UserName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex"); + + b.ToTable("AspNetUsers", (string)null); + }); + + modelBuilder.Entity("Selector.Model.ArtistLastfmSpotifyMapping", b => + { + b.Property<string>("SpotifyUri") + .HasColumnType("text"); + + b.Property<string>("LastfmArtistName") + .HasColumnType("text") + .UseCollation("case_insensitive"); + + b.HasKey("SpotifyUri"); + + b.ToTable("ArtistMapping"); + }); + + modelBuilder.Entity("Selector.Model.SpotifyListen", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id")); + + b.Property<string>("AlbumName") + .HasColumnType("text") + .UseCollation("case_insensitive"); + + b.Property<string>("ArtistName") + .HasColumnType("text") + .UseCollation("case_insensitive"); + + b.Property<int?>("PlayedDuration") + .HasColumnType("integer"); + + b.Property<DateTime>("Timestamp") + .HasColumnType("timestamp with time zone"); + + b.Property<string>("TrackName") + .HasColumnType("text") + .UseCollation("case_insensitive"); + + b.Property<string>("TrackUri") + .HasColumnType("text"); + + b.Property<string>("UserId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("SpotifyListen"); + }); + + modelBuilder.Entity("Selector.Model.TrackLastfmSpotifyMapping", b => + { + b.Property<string>("SpotifyUri") + .HasColumnType("text"); + + b.Property<string>("LastfmArtistName") + .HasColumnType("text") + .UseCollation("case_insensitive"); + + b.Property<string>("LastfmTrackName") + .HasColumnType("text") + .UseCollation("case_insensitive"); + + b.HasKey("SpotifyUri"); + + b.ToTable("TrackMapping"); + }); + + modelBuilder.Entity("Selector.Model.UserScrobble", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id")); + + b.Property<string>("AlbumArtistName") + .HasColumnType("text"); + + b.Property<string>("AlbumName") + .HasColumnType("text") + .UseCollation("case_insensitive"); + + b.Property<string>("ArtistName") + .HasColumnType("text") + .UseCollation("case_insensitive"); + + b.Property<DateTime>("Timestamp") + .HasColumnType("timestamp with time zone"); + + b.Property<string>("TrackName") + .HasColumnType("text") + .UseCollation("case_insensitive"); + + b.Property<string>("UserId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("Scrobble"); + }); + + modelBuilder.Entity("Selector.Model.Watcher", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id")); + + b.Property<int>("Type") + .HasColumnType("integer"); + + b.Property<string>("UserId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("Watcher"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b => + { + b.HasOne("Selector.Model.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b => + { + b.HasOne("Selector.Model.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Selector.Model.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b => + { + b.HasOne("Selector.Model.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Selector.Model.AppleMusicListen", b => + { + b.HasOne("Selector.Model.ApplicationUser", "User") + .WithMany() + .HasForeignKey("UserId"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Selector.Model.SpotifyListen", b => + { + b.HasOne("Selector.Model.ApplicationUser", "User") + .WithMany() + .HasForeignKey("UserId"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Selector.Model.UserScrobble", b => + { + b.HasOne("Selector.Model.ApplicationUser", "User") + .WithMany("Scrobbles") + .HasForeignKey("UserId"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Selector.Model.Watcher", b => + { + b.HasOne("Selector.Model.ApplicationUser", "User") + .WithMany("Watchers") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Selector.Model.ApplicationUser", b => + { + b.Navigation("Scrobbles"); + + b.Navigation("Watchers"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Selector.Model/Migrations/20250331203003_add_apple_listen.cs b/Selector.Model/Migrations/20250331203003_add_apple_listen.cs new file mode 100644 index 0000000..f90b892 --- /dev/null +++ b/Selector.Model/Migrations/20250331203003_add_apple_listen.cs @@ -0,0 +1,52 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Selector.Model.Migrations +{ + /// <inheritdoc /> + public partial class add_apple_listen : Migration + { + /// <inheritdoc /> + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "AppleMusicListen", + columns: table => new + { + Id = table.Column<int>(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + TrackId = table.Column<string>(type: "text", nullable: true), + Isrc = table.Column<string>(type: "text", nullable: true), + UserId = table.Column<string>(type: "text", nullable: true), + TrackName = table.Column<string>(type: "text", nullable: true, collation: "case_insensitive"), + AlbumName = table.Column<string>(type: "text", nullable: true, collation: "case_insensitive"), + ArtistName = table.Column<string>(type: "text", nullable: true, collation: "case_insensitive"), + Timestamp = table.Column<DateTime>(type: "timestamp with time zone", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_AppleMusicListen", x => x.Id); + table.ForeignKey( + name: "FK_AppleMusicListen_AspNetUsers_UserId", + column: x => x.UserId, + principalTable: "AspNetUsers", + principalColumn: "Id"); + }); + + migrationBuilder.CreateIndex( + name: "IX_AppleMusicListen_UserId", + table: "AppleMusicListen", + column: "UserId"); + } + + /// <inheritdoc /> + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "AppleMusicListen"); + } + } +} diff --git a/Selector.Model/Migrations/ApplicationDbContextModelSnapshot.cs b/Selector.Model/Migrations/ApplicationDbContextModelSnapshot.cs index 464de93..7dbaa78 100644 --- a/Selector.Model/Migrations/ApplicationDbContextModelSnapshot.cs +++ b/Selector.Model/Migrations/ApplicationDbContextModelSnapshot.cs @@ -181,6 +181,45 @@ namespace Selector.Model.Migrations b.ToTable("AlbumMapping"); }); + modelBuilder.Entity("Selector.Model.AppleMusicListen", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id")); + + b.Property<string>("AlbumName") + .HasColumnType("text") + .UseCollation("case_insensitive"); + + b.Property<string>("ArtistName") + .HasColumnType("text") + .UseCollation("case_insensitive"); + + b.Property<string>("Isrc") + .HasColumnType("text"); + + b.Property<DateTime>("Timestamp") + .HasColumnType("timestamp with time zone"); + + b.Property<string>("TrackId") + .HasColumnType("text"); + + b.Property<string>("TrackName") + .HasColumnType("text") + .UseCollation("case_insensitive"); + + b.Property<string>("UserId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AppleMusicListen"); + }); + modelBuilder.Entity("Selector.Model.ApplicationUser", b => { b.Property<string>("Id") @@ -456,6 +495,15 @@ namespace Selector.Model.Migrations .IsRequired(); }); + modelBuilder.Entity("Selector.Model.AppleMusicListen", b => + { + b.HasOne("Selector.Model.ApplicationUser", "User") + .WithMany() + .HasForeignKey("UserId"); + + b.Navigation("User"); + }); + modelBuilder.Entity("Selector.Model.SpotifyListen", b => { b.HasOne("Selector.Model.ApplicationUser", "User") diff --git a/Selector.Model/Scrobble/DBPlayCountPuller.cs b/Selector.Model/Scrobble/DBPlayCountPuller.cs index 07a0ed3..24a3994 100644 --- a/Selector.Model/Scrobble/DBPlayCountPuller.cs +++ b/Selector.Model/Scrobble/DBPlayCountPuller.cs @@ -4,6 +4,7 @@ using System.Linq; using System.Threading.Tasks; using Microsoft.Extensions.Options; using Selector.Model; +using Selector.Spotify.Consumer; namespace Selector.Cache { @@ -27,7 +28,8 @@ namespace Selector.Cache var userScrobbleCount = ScrobbleRepository.Count(username: username); - var artistScrobbles = ScrobbleRepository.GetAll(username: username, artistName: artist, tracking: false, orderTime: true).ToArray(); + var artistScrobbles = ScrobbleRepository + .GetAll(username: username, artistName: artist, tracking: false, orderTime: true).ToArray(); var albumScrobbles = artistScrobbles.Where( s => s.AlbumName.Equals(album, StringComparison.CurrentCultureIgnoreCase)).ToArray(); var trackScrobbles = artistScrobbles.Where( @@ -67,4 +69,4 @@ namespace Selector.Cache return Task.FromResult(playCount); } } -} +} \ No newline at end of file diff --git a/Selector.Model/Selector.Model.csproj b/Selector.Model/Selector.Model.csproj index 4a99c9e..66c6498 100644 --- a/Selector.Model/Selector.Model.csproj +++ b/Selector.Model/Selector.Model.csproj @@ -7,6 +7,9 @@ </PropertyGroup> <ItemGroup> + <ProjectReference Include="..\Selector.AppleMusic\Selector.AppleMusic.csproj"/> + <ProjectReference Include="..\Selector.LastFm\Selector.LastFm.csproj"/> + <ProjectReference Include="..\Selector.Spotify\Selector.Spotify.csproj"/> <ProjectReference Include="..\Selector\Selector.csproj" /> </ItemGroup> @@ -30,7 +33,6 @@ <ItemGroup> <Folder Include="Migrations\" /> - <Folder Include="Listen\" /> </ItemGroup> <ItemGroup> diff --git a/Selector.SignalR/INow.cs b/Selector.SignalR/INow.cs index 6c44343..b5b4aaf 100644 --- a/Selector.SignalR/INow.cs +++ b/Selector.SignalR/INow.cs @@ -1,6 +1,6 @@ -using System; +using Selector.Spotify; +using Selector.Spotify.Consumer; using SpotifyAPI.Web; -using System.Threading.Tasks; namespace Selector.SignalR; @@ -20,5 +20,4 @@ public interface INowPlayingHub Task SendFacts(string track, string artist, string album, string albumArtist); Task SendNewPlaying(); Task SendPlayCount(string track, string artist, string album, string albumArtist); -} - +} \ No newline at end of file diff --git a/Selector.SignalR/NowHubCache.cs b/Selector.SignalR/NowHubCache.cs index 37d9533..de6b92c 100644 --- a/Selector.SignalR/NowHubCache.cs +++ b/Selector.SignalR/NowHubCache.cs @@ -1,41 +1,42 @@ -using System; -using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging; +using Selector.Spotify; +using Selector.Spotify.Consumer; using SpotifyAPI.Web; namespace Selector.SignalR; public class NowHubCache { - private readonly NowHubClient _connection; + private readonly NowHubClient _connection; private readonly ILogger<NowHubCache> logger; public TrackAudioFeatures LastFeature { get; private set; } - public List<Card> LastCards { get; private set; } = new(); - private readonly object updateLock = new(); + public List<Card> LastCards { get; private set; } = new(); + private readonly object updateLock = new(); - private readonly object bindingLock = new(); - private bool isBound = false; + private readonly object bindingLock = new(); + private bool isBound = false; - public PlayCount LastPlayCount { get; private set; } - public CurrentlyPlayingDTO LastPlaying { get; private set; } + public PlayCount LastPlayCount { get; private set; } + public CurrentlyPlayingDTO LastPlaying { get; private set; } - public event EventHandler NewAudioFeature; - public event EventHandler NewCard; - public event EventHandler NewPlayCount; - public event EventHandler NewNowPlaying; + public event EventHandler NewAudioFeature; + public event EventHandler NewCard; + public event EventHandler NewPlayCount; + public event EventHandler NewNowPlaying; public NowHubCache(NowHubClient connection, ILogger<NowHubCache> logger) - { - _connection = connection; + { + _connection = connection; this.logger = logger; } - public void BindClient() - { - lock(bindingLock) - { - if(!isBound) - { + public void BindClient() + { + lock (bindingLock) + { + if (!isBound) + { _connection.OnNewAudioFeature(af => { lock (updateLock) @@ -108,7 +109,6 @@ public class NowHubCache isBound = true; } - } - } -} - + } + } +} \ No newline at end of file diff --git a/Selector.SignalR/NowHubClient.cs b/Selector.SignalR/NowHubClient.cs index 31abdad..b98796e 100644 --- a/Selector.SignalR/NowHubClient.cs +++ b/Selector.SignalR/NowHubClient.cs @@ -1,11 +1,11 @@ -using System; -using System.Threading.Tasks; -using Microsoft.AspNetCore.SignalR.Client; +using Microsoft.AspNetCore.SignalR.Client; +using Selector.Spotify; +using Selector.Spotify.Consumer; using SpotifyAPI.Web; namespace Selector.SignalR; -public class NowHubClient: BaseSignalRClient, INowPlayingHub, IDisposable +public class NowHubClient : BaseSignalRClient, INowPlayingHub, IDisposable { private List<IDisposable> NewPlayingCallbacks = new(); private List<IDisposable> NewAudioFeatureCallbacks = new(); @@ -13,9 +13,9 @@ public class NowHubClient: BaseSignalRClient, INowPlayingHub, IDisposable private List<IDisposable> NewCardCallbacks = new(); private bool disposedValue; - public NowHubClient(string token = null): base("nowhub", token) - { - } + public NowHubClient(string token = null) : base("nowhub", token) + { + } public void OnNewPlaying(Action<CurrentlyPlayingDTO> action) { @@ -93,10 +93,10 @@ public class NowHubClient: BaseSignalRClient, INowPlayingHub, IDisposable { if (disposing) { - foreach(var callback in NewPlayingCallbacks - .Concat(NewAudioFeatureCallbacks) - .Concat(NewPlayCountCallbacks) - .Concat(NewCardCallbacks)) + foreach (var callback in NewPlayingCallbacks + .Concat(NewAudioFeatureCallbacks) + .Concat(NewPlayCountCallbacks) + .Concat(NewCardCallbacks)) { callback.Dispose(); } @@ -114,5 +114,4 @@ public class NowHubClient: BaseSignalRClient, INowPlayingHub, IDisposable Dispose(disposing: true); GC.SuppressFinalize(this); } -} - +} \ No newline at end of file diff --git a/Selector.SignalR/Selector.SignalR.csproj b/Selector.SignalR/Selector.SignalR.csproj index 9234cf8..97dcaa6 100644 --- a/Selector.SignalR/Selector.SignalR.csproj +++ b/Selector.SignalR/Selector.SignalR.csproj @@ -8,6 +8,7 @@ </PropertyGroup> <ItemGroup> + <ProjectReference Include="..\Selector.Spotify\Selector.Spotify.csproj"/> <ProjectReference Include="..\Selector\Selector.csproj" /> <!-- <ProjectReference Include="..\Selector.Model\Selector.Model.csproj" /> --> </ItemGroup> diff --git a/Selector/Spotify/ConfigFactory/ISpotifyConfigFactory.cs b/Selector.Spotify/ConfigFactory/ISpotifyConfigFactory.cs similarity index 59% rename from Selector/Spotify/ConfigFactory/ISpotifyConfigFactory.cs rename to Selector.Spotify/ConfigFactory/ISpotifyConfigFactory.cs index 6bb792d..2448242 100644 --- a/Selector/Spotify/ConfigFactory/ISpotifyConfigFactory.cs +++ b/Selector.Spotify/ConfigFactory/ISpotifyConfigFactory.cs @@ -1,10 +1,9 @@ -using System.Threading.Tasks; -using SpotifyAPI.Web; +using SpotifyAPI.Web; -namespace Selector +namespace Selector.Spotify.ConfigFactory { public interface ISpotifyConfigFactory { public Task<SpotifyClientConfig> GetConfig(); } -} +} \ No newline at end of file diff --git a/Selector/Spotify/ConfigFactory/RefreshTokenFactory.cs b/Selector.Spotify/ConfigFactory/RefreshTokenFactory.cs similarity index 88% rename from Selector/Spotify/ConfigFactory/RefreshTokenFactory.cs rename to Selector.Spotify/ConfigFactory/RefreshTokenFactory.cs index 3321ed3..b354e73 100644 --- a/Selector/Spotify/ConfigFactory/RefreshTokenFactory.cs +++ b/Selector.Spotify/ConfigFactory/RefreshTokenFactory.cs @@ -1,9 +1,6 @@ -using System.Threading.Tasks; +using SpotifyAPI.Web; -using SpotifyAPI.Web; - - -namespace Selector +namespace Selector.Spotify.ConfigFactory { /// <summary> /// Get config from a refresh token @@ -14,7 +11,8 @@ namespace Selector private string ClientSecret { get; set; } private string RefreshToken { get; set; } - public RefreshTokenFactory(string clientId, string clientSecret, string refreshToken) { + public RefreshTokenFactory(string clientId, string clientSecret, string refreshToken) + { ClientId = clientId; ClientSecret = clientSecret; RefreshToken = refreshToken; @@ -27,7 +25,8 @@ namespace Selector var config = SpotifyClientConfig .CreateDefault() - .WithAuthenticator(new AuthorizationCodeAuthenticator(ClientId, ClientSecret, new(){ + .WithAuthenticator(new AuthorizationCodeAuthenticator(ClientId, ClientSecret, new() + { AccessToken = refreshed.AccessToken, TokenType = refreshed.TokenType, ExpiresIn = refreshed.ExpiresIn, @@ -39,4 +38,4 @@ namespace Selector return config; } } -} +} \ No newline at end of file diff --git a/Selector/Consumers/AudioFeatureInjector.cs b/Selector.Spotify/Consumer/AudioFeatureInjector.cs similarity index 94% rename from Selector/Consumers/AudioFeatureInjector.cs rename to Selector.Spotify/Consumer/AudioFeatureInjector.cs index d7ddc01..9201168 100644 --- a/Selector/Consumers/AudioFeatureInjector.cs +++ b/Selector.Spotify/Consumer/AudioFeatureInjector.cs @@ -1,11 +1,10 @@ -using System; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; +using Selector.Extensions; +using Selector.Spotify.Timeline; using SpotifyAPI.Web; -namespace Selector +namespace Selector.Spotify.Consumer { public class AudioFeatureInjector : ISpotifyPlayerConsumer { @@ -32,7 +31,7 @@ namespace Selector CancelToken = token; } - public void Callback(object sender, ListeningChangeEventArgs e) + public void Callback(object sender, SpotifyListeningChangeEventArgs e) { if (e.Current is null) return; @@ -49,7 +48,7 @@ namespace Selector }, CancelToken); } - public virtual async Task AsyncCallback(ListeningChangeEventArgs e) + public virtual async Task AsyncCallback(SpotifyListeningChangeEventArgs e) { using var scope = Logger.GetListeningEventArgsScope(e); diff --git a/Selector/Consumers/DummyAudioFeatureInjector.cs b/Selector.Spotify/Consumer/DummyAudioFeatureInjector.cs similarity index 93% rename from Selector/Consumers/DummyAudioFeatureInjector.cs rename to Selector.Spotify/Consumer/DummyAudioFeatureInjector.cs index c0ae960..e38ffa8 100644 --- a/Selector/Consumers/DummyAudioFeatureInjector.cs +++ b/Selector.Spotify/Consumer/DummyAudioFeatureInjector.cs @@ -1,10 +1,8 @@ -using System; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging; +using Selector.Extensions; using SpotifyAPI.Web; -namespace Selector +namespace Selector.Spotify.Consumer { public class DummyAudioFeatureInjector : AudioFeatureInjector { @@ -62,7 +60,7 @@ namespace Selector return _features[_contextIdx]; } - public override Task AsyncCallback(ListeningChangeEventArgs e) + public override Task AsyncCallback(SpotifyListeningChangeEventArgs e) { using var scope = Logger.GetListeningEventArgsScope(e); diff --git a/Selector/Consumers/Factory/AudioFeatureInjectorFactory.cs b/Selector.Spotify/Consumer/Factory/AudioFeatureInjectorFactory.cs similarity index 91% rename from Selector/Consumers/Factory/AudioFeatureInjectorFactory.cs rename to Selector.Spotify/Consumer/Factory/AudioFeatureInjectorFactory.cs index 0f0cac5..3ea3701 100644 --- a/Selector/Consumers/Factory/AudioFeatureInjectorFactory.cs +++ b/Selector.Spotify/Consumer/Factory/AudioFeatureInjectorFactory.cs @@ -1,8 +1,8 @@ -using System.Threading.Tasks; -using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging; +using Selector.Spotify.ConfigFactory; using SpotifyAPI.Web; -namespace Selector +namespace Selector.Spotify.Consumer.Factory { public interface IAudioFeatureInjectorFactory { diff --git a/Selector/Consumers/Factory/PlayCounterFactory.cs b/Selector.Spotify/Consumer/Factory/PlayCounterFactory.cs similarity index 93% rename from Selector/Consumers/Factory/PlayCounterFactory.cs rename to Selector.Spotify/Consumer/Factory/PlayCounterFactory.cs index 9b60f10..6b25847 100644 --- a/Selector/Consumers/Factory/PlayCounterFactory.cs +++ b/Selector.Spotify/Consumer/Factory/PlayCounterFactory.cs @@ -1,9 +1,7 @@ -using System; -using System.Threading.Tasks; -using IF.Lastfm.Core.Api; +using IF.Lastfm.Core.Api; using Microsoft.Extensions.Logging; -namespace Selector +namespace Selector.Spotify.Consumer.Factory { public interface IPlayCounterFactory { diff --git a/Selector/Consumers/Factory/WebHookFactory.cs b/Selector.Spotify/Consumer/Factory/WebHookFactory.cs similarity index 87% rename from Selector/Consumers/Factory/WebHookFactory.cs rename to Selector.Spotify/Consumer/Factory/WebHookFactory.cs index 080d6f7..85b3671 100644 --- a/Selector/Consumers/Factory/WebHookFactory.cs +++ b/Selector.Spotify/Consumer/Factory/WebHookFactory.cs @@ -1,8 +1,6 @@ -using System.Net.Http; -using System.Threading.Tasks; -using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging; -namespace Selector +namespace Selector.Spotify.Consumer.Factory { public interface IWebHookFactory { diff --git a/Selector.Spotify/Consumer/IConsumer.cs b/Selector.Spotify/Consumer/IConsumer.cs new file mode 100644 index 0000000..8c58eb6 --- /dev/null +++ b/Selector.Spotify/Consumer/IConsumer.cs @@ -0,0 +1,9 @@ +namespace Selector.Spotify.Consumer; + +public interface ISpotifyPlayerConsumer : IConsumer<SpotifyListeningChangeEventArgs> +{ +} + +public interface IPlaylistConsumer : IConsumer<PlaylistChangeEventArgs> +{ +} \ No newline at end of file diff --git a/Selector/Consumers/PlayCounter.cs b/Selector.Spotify/Consumer/PlayCounter.cs similarity index 95% rename from Selector/Consumers/PlayCounter.cs rename to Selector.Spotify/Consumer/PlayCounter.cs index 0447a3a..2211e4a 100644 --- a/Selector/Consumers/PlayCounter.cs +++ b/Selector.Spotify/Consumer/PlayCounter.cs @@ -1,13 +1,11 @@ -using System; -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; using IF.Lastfm.Core.Api; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; +using Selector.Extensions; +using Selector.Spotify.Timeline; using SpotifyAPI.Web; -namespace Selector +namespace Selector.Spotify.Consumer { public class PlayCounter : ISpotifyPlayerConsumer { @@ -46,7 +44,7 @@ namespace Selector CancelToken = token; } - public void Callback(object sender, ListeningChangeEventArgs e) + public void Callback(object sender, SpotifyListeningChangeEventArgs e) { if (e.Current is null) return; @@ -63,7 +61,7 @@ namespace Selector }, CancelToken); } - public async Task AsyncCallback(ListeningChangeEventArgs e) + public async Task AsyncCallback(SpotifyListeningChangeEventArgs e) { using var scope = Logger.BeginScope(new Dictionary<string, object>() { { "spotify_username", e.SpotifyUsername }, { "id", e.Id }, { "username", Credentials.Username } }); @@ -155,7 +153,7 @@ namespace Selector Album = albumCount, Artist = artistCount, User = userCount, - ListeningEvent = e + SpotifyListeningEvent = e }; if (!string.IsNullOrWhiteSpace(Credentials.Username)) @@ -221,7 +219,7 @@ namespace Selector public IEnumerable<CountSample> TrackCountData { get; set; } public IEnumerable<CountSample> AlbumCountData { get; set; } public IEnumerable<CountSample> ArtistCountData { get; set; } - public ListeningChangeEventArgs ListeningEvent { get; set; } + public SpotifyListeningChangeEventArgs SpotifyListeningEvent { get; set; } } public class LastFmCredentials diff --git a/Selector/Consumers/WebHook.cs b/Selector.Spotify/Consumer/WebHook.cs similarity index 91% rename from Selector/Consumers/WebHook.cs rename to Selector.Spotify/Consumer/WebHook.cs index 24abac9..4680997 100644 --- a/Selector/Consumers/WebHook.cs +++ b/Selector.Spotify/Consumer/WebHook.cs @@ -1,22 +1,17 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Net.Http; -using System.Threading; -using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; +using Selector.Spotify.Timeline; -namespace Selector +namespace Selector.Spotify.Consumer { public class WebHookConfig { public string Name { get; set; } - public IEnumerable<Predicate<ListeningChangeEventArgs>> Predicates { get; set; } + public IEnumerable<Predicate<SpotifyListeningChangeEventArgs>> Predicates { get; set; } public string Url { get; set; } public HttpContent Content { get; set; } - public bool ShouldRequest(ListeningChangeEventArgs e) + public bool ShouldRequest(SpotifyListeningChangeEventArgs e) { if (Predicates is not null) { @@ -60,7 +55,7 @@ namespace Selector CancelToken = token; } - public void Callback(object sender, ListeningChangeEventArgs e) + public void Callback(object sender, SpotifyListeningChangeEventArgs e) { if (e.Current is null) return; @@ -77,7 +72,7 @@ namespace Selector }, CancelToken); } - public async Task AsyncCallback(ListeningChangeEventArgs e) + public async Task AsyncCallback(SpotifyListeningChangeEventArgs e) { using var scope = Logger.BeginScope(new Dictionary<string, object>() { diff --git a/Selector.Spotify/Credentials.cs b/Selector.Spotify/Credentials.cs new file mode 100644 index 0000000..f52d95f --- /dev/null +++ b/Selector.Spotify/Credentials.cs @@ -0,0 +1,17 @@ +namespace Selector.Spotify; + +public class SpotifyAppCredentials +{ + public string ClientId { get; set; } + public string ClientSecret { get; set; } + + public SpotifyAppCredentials() + { + } + + public SpotifyAppCredentials(string clientId, string clientSecret) + { + ClientId = clientId; + ClientSecret = clientSecret; + } +} \ No newline at end of file diff --git a/Selector/Spotify/CurrentlyPlayingDTO.cs b/Selector.Spotify/CurrentlyPlayingDTO.cs similarity index 87% rename from Selector/Spotify/CurrentlyPlayingDTO.cs rename to Selector.Spotify/CurrentlyPlayingDTO.cs index d8b25c8..0af0b99 100644 --- a/Selector/Spotify/CurrentlyPlayingDTO.cs +++ b/Selector.Spotify/CurrentlyPlayingDTO.cs @@ -1,10 +1,10 @@ -using System; - +using Selector.Extensions; using SpotifyAPI.Web; -namespace Selector { - - public class CurrentlyPlayingDTO { +namespace Selector.Spotify +{ + public class CurrentlyPlayingDTO + { public CurrentlyPlayingContextDTO Context { get; set; } public string Username { get; set; } public string UserId { get; set; } @@ -12,9 +12,9 @@ namespace Selector { public FullTrack Track { get; set; } public FullEpisode Episode { get; set; } - public static explicit operator CurrentlyPlayingDTO(ListeningChangeEventArgs e) + public static explicit operator CurrentlyPlayingDTO(SpotifyListeningChangeEventArgs e) { - if(e.Current.Item is FullTrack track) + if (e.Current.Item is FullTrack track) { return new() { @@ -52,13 +52,14 @@ namespace Selector { public long Timestamp { get; set; } public int ProgressMs { get; set; } public bool IsPlaying { get; set; } - + public string CurrentlyPlayingType { get; set; } public Actions Actions { get; set; } public static implicit operator CurrentlyPlayingContextDTO(CurrentlyPlayingContext context) { - return new CurrentlyPlayingContextDTO { + return new CurrentlyPlayingContextDTO + { Device = context.Device, RepeatState = context.RepeatState, ShuffleState = context.ShuffleState, diff --git a/Selector/Equality/PlayableItemEqualityComparer.cs b/Selector.Spotify/Equality/PlayableItemEqualityComparer.cs similarity index 61% rename from Selector/Equality/PlayableItemEqualityComparer.cs rename to Selector.Spotify/Equality/PlayableItemEqualityComparer.cs index d4d36ac..2220eaf 100644 --- a/Selector/Equality/PlayableItemEqualityComparer.cs +++ b/Selector.Spotify/Equality/PlayableItemEqualityComparer.cs @@ -1,11 +1,10 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; +using System.Diagnostics.CodeAnalysis; +using Selector.Extensions; using SpotifyAPI.Web; -namespace Selector.Equality +namespace Selector.Spotify.Equality { - public class PlayableItemEqualityComparer: IEqualityComparer<PlaylistTrack<IPlayableItem>> + public class PlayableItemEqualityComparer : IEqualityComparer<PlaylistTrack<IPlayableItem>> { public bool Equals(PlaylistTrack<IPlayableItem> x, PlaylistTrack<IPlayableItem> y) { @@ -17,5 +16,4 @@ namespace Selector.Equality return obj.GetUri().GetHashCode(); } } -} - +} \ No newline at end of file diff --git a/Selector/Equality/StringComparers.cs b/Selector.Spotify/Equality/StringComparers.cs similarity index 51% rename from Selector/Equality/StringComparers.cs rename to Selector.Spotify/Equality/StringComparers.cs index 8430346..119b969 100644 --- a/Selector/Equality/StringComparers.cs +++ b/Selector.Spotify/Equality/StringComparers.cs @@ -1,12 +1,8 @@ -using System; -using System.Collections.Generic; -using System.Linq; using SpotifyAPI.Web; -namespace Selector +namespace Selector.Spotify.Equality { - - public abstract class NoHashCode<T>: EqualityComparer<T> + public abstract class NoHashCode<T> : EqualityComparer<T> { public override int GetHashCode(T obj) { @@ -16,79 +12,89 @@ namespace Selector public class FullTrackStringComparer : NoHashCode<FullTrack> { - public override bool Equals(FullTrack track1, FullTrack track2) => FullTrackStringComparer.IsEqual(track1, track2); + public override bool Equals(FullTrack track1, FullTrack track2) => IsEqual(track1, track2); public static bool IsEqual(FullTrack track1, FullTrack track2) => track1.Name == track2.Name - && Enumerable.SequenceEqual(track1.Artists.Select(a => a.Name), track2.Artists.Select(a => a.Name)) - && SimpleAlbumStringComparer.IsEqual(track1.Album, track2.Album); + && Enumerable.SequenceEqual( + track1.Artists.Select(a => a.Name), + track2.Artists.Select(a => a.Name)) + && SimpleAlbumStringComparer.IsEqual( + track1.Album, track2.Album); } public class FullEpisodeStringComparer : NoHashCode<FullEpisode> { - public override bool Equals(FullEpisode ep1, FullEpisode ep2) => FullEpisodeStringComparer.IsEqual(ep1, ep2); + public override bool Equals(FullEpisode ep1, FullEpisode ep2) => IsEqual(ep1, ep2); - public static bool IsEqual(FullEpisode ep1, FullEpisode ep2) => ep1.Name == ep2.Name - && SimpleShowStringComparer.IsEqual(ep1.Show, ep2.Show); + public static bool IsEqual(FullEpisode ep1, FullEpisode ep2) => ep1.Name == ep2.Name + && SimpleShowStringComparer.IsEqual(ep1.Show, + ep2.Show); } public class FullAlbumStringComparer : NoHashCode<FullAlbum> { - public override bool Equals(FullAlbum album1, FullAlbum album2) => FullAlbumStringComparer.IsEqual(album1, album2); + public override bool Equals(FullAlbum album1, FullAlbum album2) => IsEqual(album1, album2); public static bool IsEqual(FullAlbum album1, FullAlbum album2) => album1.Name == album2.Name - && Enumerable.SequenceEqual(album1.Artists.Select(a => a.Name), album2.Artists.Select(a => a.Name)); + && Enumerable.SequenceEqual( + album1.Artists.Select(a => a.Name), + album2.Artists.Select(a => a.Name)); } public class FullShowStringComparer : NoHashCode<FullShow> { - public override bool Equals(FullShow show1, FullShow show2) => FullShowStringComparer.IsEqual(show1, show2); + public override bool Equals(FullShow show1, FullShow show2) => IsEqual(show1, show2); public static bool IsEqual(FullShow show1, FullShow show2) => show1.Name == show2.Name - && show1.Publisher == show2.Publisher; + && show1.Publisher == show2.Publisher; } public class FullArtistStringComparer : NoHashCode<FullArtist> { - public override bool Equals(FullArtist artist1, FullArtist artist2) => FullArtistStringComparer.IsEqual(artist1, artist2); + public override bool Equals(FullArtist artist1, FullArtist artist2) => IsEqual(artist1, artist2); public static bool IsEqual(FullArtist artist1, FullArtist artist2) => artist1.Name == artist2.Name; } public class SimpleTrackStringComparer : NoHashCode<SimpleTrack> { - public override bool Equals(SimpleTrack track1, SimpleTrack track2) => SimpleTrackStringComparer.IsEqual(track1, track2); + public override bool Equals(SimpleTrack track1, SimpleTrack track2) => IsEqual(track1, track2); public static bool IsEqual(SimpleTrack track1, SimpleTrack track2) => track1.Name == track2.Name - && Enumerable.SequenceEqual(track1.Artists.Select(a => a.Name), track2.Artists.Select(a => a.Name)); + && Enumerable.SequenceEqual( + track1.Artists.Select(a => a.Name), + track2.Artists.Select(a => a.Name)); } public class SimpleEpisodeStringComparer : NoHashCode<SimpleEpisode> { - public override bool Equals(SimpleEpisode ep1, SimpleEpisode ep2) => SimpleEpisodeStringComparer.IsEqual(ep1, ep2); + public override bool Equals(SimpleEpisode ep1, SimpleEpisode ep2) => IsEqual(ep1, ep2); public static bool IsEqual(SimpleEpisode ep1, SimpleEpisode ep2) => ep1.Name == ep2.Name; } public class SimpleAlbumStringComparer : NoHashCode<SimpleAlbum> { - public override bool Equals(SimpleAlbum album1, SimpleAlbum album2) => SimpleAlbumStringComparer.IsEqual(album1, album2); + public override bool Equals(SimpleAlbum album1, SimpleAlbum album2) => IsEqual(album1, album2); public static bool IsEqual(SimpleAlbum album1, SimpleAlbum album2) => album1.Name == album2.Name - && Enumerable.SequenceEqual(album1.Artists.Select(a => a.Name), album2.Artists.Select(a => a.Name)); + && Enumerable.SequenceEqual( + album1.Artists.Select(a => a.Name), + album2.Artists.Select(a => a.Name)); } public class SimpleShowStringComparer : NoHashCode<SimpleShow> { - public override bool Equals(SimpleShow show1, SimpleShow show2) => SimpleShowStringComparer.IsEqual(show1, show2); + public override bool Equals(SimpleShow show1, SimpleShow show2) => IsEqual(show1, show2); public static bool IsEqual(SimpleShow show1, SimpleShow show2) => show1.Name == show2.Name - && show1.Publisher == show2.Publisher; + && show1.Publisher == show2.Publisher; } public class SimpleArtistStringComparer : NoHashCode<SimpleArtist> { - public override bool Equals(SimpleArtist artist1, SimpleArtist artist2) => SimpleArtistStringComparer.IsEqual(artist1, artist2); + public override bool Equals(SimpleArtist artist1, SimpleArtist artist2) => IsEqual(artist1, artist2); public static bool IsEqual(SimpleArtist artist1, SimpleArtist artist2) => artist1.Name == artist2.Name; } -} +} \ No newline at end of file diff --git a/Selector/Equality/StringEqual.cs b/Selector.Spotify/Equality/StringEqual.cs similarity index 92% rename from Selector/Equality/StringEqual.cs rename to Selector.Spotify/Equality/StringEqual.cs index a3bde2d..a68fbc8 100644 --- a/Selector/Equality/StringEqual.cs +++ b/Selector.Spotify/Equality/StringEqual.cs @@ -1,8 +1,8 @@ using SpotifyAPI.Web; -namespace Selector +namespace Selector.Spotify.Equality { - public class StringEqual: Equal + public class StringEqual : Equal { public StringEqual() { @@ -22,4 +22,4 @@ namespace Selector }; } } -} +} \ No newline at end of file diff --git a/Selector/Equality/UriEqual.cs b/Selector.Spotify/Equality/UriEqual.cs similarity index 58% rename from Selector/Equality/UriEqual.cs rename to Selector.Spotify/Equality/UriEqual.cs index 0c5cf2b..b17eb57 100644 --- a/Selector/Equality/UriEqual.cs +++ b/Selector.Spotify/Equality/UriEqual.cs @@ -1,6 +1,4 @@ -using System; - -namespace Selector +namespace Selector.Spotify.Equality { public class UriEqual : IEqual { @@ -10,18 +8,18 @@ namespace Selector if (item is null ^ other is null) return false; var uri = typeof(T).GetProperty("Uri"); - - if(uri is not null) - return (string) uri.GetValue(item) == (string) uri.GetValue(other); - else + + if (uri is not null) + return (string)uri.GetValue(item) == (string)uri.GetValue(other); + else { var id = typeof(T).GetProperty("Id"); - if(id is not null) - return (string) id.GetValue(item) == (string) id.GetValue(other); - else + if (id is not null) + return (string)id.GetValue(item) == (string)id.GetValue(other); + else throw new ArgumentException($"{typeof(T)} does not contain a uri or id"); } } } -} +} \ No newline at end of file diff --git a/Selector/Watcher/Interfaces/Events.cs b/Selector.Spotify/Events.cs similarity index 68% rename from Selector/Watcher/Interfaces/Events.cs rename to Selector.Spotify/Events.cs index 0ce51f9..04ea357 100644 --- a/Selector/Watcher/Interfaces/Events.cs +++ b/Selector.Spotify/Events.cs @@ -1,26 +1,24 @@ -using System; -using System.Collections.Generic; +using Selector.Spotify.Timeline; using SpotifyAPI.Web; -namespace Selector +namespace Selector.Spotify { - public class ListeningChangeEventArgs: EventArgs { + public class SpotifyListeningChangeEventArgs : ListeningChangeEventArgs + { public CurrentlyPlayingContext Previous { get; set; } public CurrentlyPlayingContext Current { get; set; } + /// <summary> /// Spotify Username /// </summary> public string SpotifyUsername { get; set; } - /// <summary> - /// String Id for watcher, used to hold user Db Id - /// </summary> - /// <value></value> - public string Id { get; set; } + PlayerTimeline Timeline { get; set; } - public static ListeningChangeEventArgs From(CurrentlyPlayingContext previous, CurrentlyPlayingContext current, PlayerTimeline timeline, string id = null, string username = null) + public static SpotifyListeningChangeEventArgs From(CurrentlyPlayingContext previous, + CurrentlyPlayingContext current, PlayerTimeline timeline, string id = null, string username = null) { - return new ListeningChangeEventArgs() + return new SpotifyListeningChangeEventArgs() { Previous = previous, Current = current, @@ -35,21 +33,27 @@ namespace Selector { public FullPlaylist Previous { get; set; } public FullPlaylist Current { get; set; } + /// <summary> /// Spotify Username /// </summary> public string SpotifyUsername { get; set; } + /// <summary> /// String Id for watcher, used to hold user Db Id /// </summary> /// <value></value> public string Id { get; set; } + Timeline<FullPlaylist> Timeline { get; set; } ICollection<PlaylistTrack<IPlayableItem>> CurrentTracks { get; set; } ICollection<PlaylistTrack<IPlayableItem>> AddedTracks { get; set; } ICollection<PlaylistTrack<IPlayableItem>> RemovedTracks { get; set; } - public static PlaylistChangeEventArgs From(FullPlaylist previous, FullPlaylist current, Timeline<FullPlaylist> timeline, ICollection<PlaylistTrack<IPlayableItem>> tracks = null, ICollection<PlaylistTrack<IPlayableItem>> addedTracks = null, ICollection<PlaylistTrack<IPlayableItem>> removedTracks = null, string id = null, string username = null) + public static PlaylistChangeEventArgs From(FullPlaylist previous, FullPlaylist current, + Timeline<FullPlaylist> timeline, ICollection<PlaylistTrack<IPlayableItem>> tracks = null, + ICollection<PlaylistTrack<IPlayableItem>> addedTracks = null, + ICollection<PlaylistTrack<IPlayableItem>> removedTracks = null, string id = null, string username = null) { return new PlaylistChangeEventArgs() { @@ -64,4 +68,4 @@ namespace Selector }; } } -} +} \ No newline at end of file diff --git a/Selector.Spotify/Extensions/LoggingExtensions.cs b/Selector.Spotify/Extensions/LoggingExtensions.cs new file mode 100644 index 0000000..457a800 --- /dev/null +++ b/Selector.Spotify/Extensions/LoggingExtensions.cs @@ -0,0 +1,12 @@ +using Microsoft.Extensions.Logging; +using Selector.Spotify; + +namespace Selector.Extensions +{ + public static class LoggingExtensions + { + public static IDisposable GetListeningEventArgsScope(this ILogger logger, SpotifyListeningChangeEventArgs e) => + logger.BeginScope(new Dictionary<string, object>() + { { "spotify_username", e.SpotifyUsername }, { "id", e.Id } }); + } +} \ No newline at end of file diff --git a/Selector.Spotify/Extensions/ServiceExtensions.cs b/Selector.Spotify/Extensions/ServiceExtensions.cs new file mode 100644 index 0000000..e4c7a37 --- /dev/null +++ b/Selector.Spotify/Extensions/ServiceExtensions.cs @@ -0,0 +1,30 @@ +using Microsoft.Extensions.DependencyInjection; +using Selector.Spotify.Consumer.Factory; +using Selector.Spotify.FactoryProvider; + +namespace Selector.Extensions; + +public static class ServiceExtensions +{ + public static IServiceCollection AddConsumerFactories(this IServiceCollection services) + { + services.AddTransient<IAudioFeatureInjectorFactory, AudioFeatureInjectorFactory>(); + services.AddTransient<AudioFeatureInjectorFactory>(); + + services.AddTransient<IPlayCounterFactory, PlayCounterFactory>(); + services.AddTransient<PlayCounterFactory>(); + + services.AddTransient<IWebHookFactory, WebHookFactory>(); + services.AddTransient<WebHookFactory>(); + + return services; + } + + public static IServiceCollection AddSpotify(this IServiceCollection services) + { + services.AddSingleton<IRefreshTokenFactoryProvider, RefreshTokenFactoryProvider>(); + services.AddSingleton<IRefreshTokenFactoryProvider, CachingRefreshTokenFactoryProvider>(); + + return services; + } +} \ No newline at end of file diff --git a/Selector/Extensions/SpotifyExtensions.cs b/Selector.Spotify/Extensions/SpotifyExtensions.cs similarity index 68% rename from Selector/Extensions/SpotifyExtensions.cs rename to Selector.Spotify/Extensions/SpotifyExtensions.cs index e332c81..8dcc64d 100644 --- a/Selector/Extensions/SpotifyExtensions.cs +++ b/Selector.Spotify/Extensions/SpotifyExtensions.cs @@ -1,33 +1,34 @@ -using System; -using System.Collections.Generic; -using System.Text; -using System.Linq; +using SpotifyAPI.Web; -using SpotifyAPI.Web; - -namespace Selector +namespace Selector.Extensions { public static class SpotifyExtensions { public static string DisplayString(this FullPlaylist playlist) => $"{playlist.Name}"; - public static string DisplayString(this FullTrack track) => $"{track.Name} / {track.Album?.Name} / {track.Artists?.DisplayString()}"; - public static string DisplayString(this SimpleAlbum album) => $"{album.Name} / {album.Artists?.DisplayString()}"; + public static string DisplayString(this FullTrack track) => + $"{track.Name} / {track.Album?.Name} / {track.Artists?.DisplayString()}"; + + public static string DisplayString(this SimpleAlbum album) => + $"{album.Name} / {album.Artists?.DisplayString()}"; + public static string DisplayString(this SimpleArtist artist) => artist.Name; public static string DisplayString(this FullEpisode ep) => $"{ep.Name} / {ep.Show?.DisplayString()}"; public static string DisplayString(this SimpleShow show) => $"{show.Name} / {show.Publisher}"; - public static string DisplayString(this CurrentlyPlayingContext currentPlaying) { - + public static string DisplayString(this CurrentlyPlayingContext currentPlaying) + { if (currentPlaying.Item is FullTrack track) { - return $"{currentPlaying.IsPlaying}, {track.DisplayString()}, {currentPlaying.Device?.DisplayString() ?? "no device"}, {currentPlaying?.Context?.DisplayString() ?? "no context"}"; + return + $"{currentPlaying.IsPlaying}, {track.DisplayString()}, {currentPlaying.Device?.DisplayString() ?? "no device"}, {currentPlaying?.Context?.DisplayString() ?? "no context"}"; } else if (currentPlaying.Item is FullEpisode episode) { - return $"{currentPlaying.IsPlaying}, {episode.DisplayString()}, {currentPlaying.Device?.DisplayString() ?? "no device"}"; + return + $"{currentPlaying.IsPlaying}, {episode.DisplayString()}, {currentPlaying.Device?.DisplayString() ?? "no device"}"; } else if (currentPlaying.Item is null) { @@ -40,15 +41,23 @@ namespace Selector } public static string DisplayString(this Context context) => $"{context.Type}, {context.Uri}"; - public static string DisplayString(this Device device) => $"{device.Name} ({device.Id}) {device.VolumePercent}%"; - public static string DisplayString(this TrackAudioFeatures feature) => $"Acou. {feature.Acousticness}, Dance {feature.Danceability}, Energy {feature.Energy}, Instru. {feature.Instrumentalness}, Key {feature.Key}, Live {feature.Liveness}, Loud {feature.Loudness} dB, Mode {feature.Mode}, Speech {feature.Speechiness}, Tempo {feature.Tempo} BPM, Time Sig. {feature.TimeSignature}, Valence {feature.Valence}"; - public static string DisplayString(this IEnumerable<SimpleArtist> artists) => string.Join(", ", artists.Select(a => a.DisplayString())); + public static string DisplayString(this Device device) => + $"{device.Name} ({device.Id}) {device.VolumePercent}%"; + + public static string DisplayString(this TrackAudioFeatures feature) => + $"Acou. {feature.Acousticness}, Dance {feature.Danceability}, Energy {feature.Energy}, Instru. {feature.Instrumentalness}, Key {feature.Key}, Live {feature.Liveness}, Loud {feature.Loudness} dB, Mode {feature.Mode}, Speech {feature.Speechiness}, Tempo {feature.Tempo} BPM, Time Sig. {feature.TimeSignature}, Valence {feature.Valence}"; + + public static string DisplayString(this IEnumerable<SimpleArtist> artists) => + string.Join(", ", artists.Select(a => a.DisplayString())); public static bool IsInstrumental(this TrackAudioFeatures feature) => feature.Instrumentalness > 0.5; public static bool IsLive(this TrackAudioFeatures feature) => feature.Liveness > 0.8f; public static bool IsSpokenWord(this TrackAudioFeatures feature) => feature.Speechiness > 0.66f; - public static bool IsSpeechAndMusic(this TrackAudioFeatures feature) => feature.Speechiness is >= 0.33f and <= 0.66f; + + public static bool IsSpeechAndMusic(this TrackAudioFeatures feature) => + feature.Speechiness is >= 0.33f and <= 0.66f; + public static bool IsNotSpeech(this TrackAudioFeatures feature) => feature.Speechiness < 0.33f; public static string GetUri(this IPlayableItem y) @@ -83,4 +92,4 @@ namespace Selector } } } -} +} \ No newline at end of file diff --git a/Selector/Spotify/FactoryProvider/CachingRefreshTokenFactoryProvider.cs b/Selector.Spotify/FactoryProvider/CachingRefreshTokenFactoryProvider.cs similarity index 82% rename from Selector/Spotify/FactoryProvider/CachingRefreshTokenFactoryProvider.cs rename to Selector.Spotify/FactoryProvider/CachingRefreshTokenFactoryProvider.cs index 4c8875d..fcf1ba9 100644 --- a/Selector/Spotify/FactoryProvider/CachingRefreshTokenFactoryProvider.cs +++ b/Selector.Spotify/FactoryProvider/CachingRefreshTokenFactoryProvider.cs @@ -1,18 +1,16 @@ -using System; -using System.Collections.Generic; -using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; +using Selector.Spotify.ConfigFactory; using SpotifyAPI.Web; - -namespace Selector +namespace Selector.Spotify.FactoryProvider { public class CachingRefreshTokenFactoryProvider : RefreshTokenFactoryProvider { protected readonly ILogger<CachingRefreshTokenFactoryProvider> Logger; - public CachingRefreshTokenFactoryProvider(IOptions<SpotifyAppCredentials> credentials, ILogger<CachingRefreshTokenFactoryProvider> logger) : base(credentials) + public CachingRefreshTokenFactoryProvider(IOptions<SpotifyAppCredentials> credentials, + ILogger<CachingRefreshTokenFactoryProvider> logger) : base(credentials) { Logger = logger; } @@ -29,11 +27,11 @@ namespace Selector var client = new SpotifyClient(newConfig); var userDetails = await client.UserProfile.Current(); - if(Configs.ContainsKey(userDetails.Id)) + if (Configs.ContainsKey(userDetails.Id)) { return Configs[userDetails.Id]; } - else + else { Logger.LogDebug("New user token factory added [{spotify_name}]", userDetails.DisplayName); Configs[userDetails.Id] = configProvider; @@ -41,4 +39,4 @@ namespace Selector } } } -} +} \ No newline at end of file diff --git a/Selector/Spotify/FactoryProvider/IRefreshTokenFactoryProvider.cs b/Selector.Spotify/FactoryProvider/IRefreshTokenFactoryProvider.cs similarity index 62% rename from Selector/Spotify/FactoryProvider/IRefreshTokenFactoryProvider.cs rename to Selector.Spotify/FactoryProvider/IRefreshTokenFactoryProvider.cs index c4b9b5d..991a9a6 100644 --- a/Selector/Spotify/FactoryProvider/IRefreshTokenFactoryProvider.cs +++ b/Selector.Spotify/FactoryProvider/IRefreshTokenFactoryProvider.cs @@ -1,9 +1,9 @@ -using System.Threading.Tasks; +using Selector.Spotify.ConfigFactory; -namespace Selector +namespace Selector.Spotify.FactoryProvider { public interface IRefreshTokenFactoryProvider { public Task<RefreshTokenFactory> GetFactory(string refreshToken); } -} +} \ No newline at end of file diff --git a/Selector/Spotify/FactoryProvider/RefreshTokenFactoryProvider.cs b/Selector.Spotify/FactoryProvider/RefreshTokenFactoryProvider.cs similarity index 56% rename from Selector/Spotify/FactoryProvider/RefreshTokenFactoryProvider.cs rename to Selector.Spotify/FactoryProvider/RefreshTokenFactoryProvider.cs index b8d5c06..23825fb 100644 --- a/Selector/Spotify/FactoryProvider/RefreshTokenFactoryProvider.cs +++ b/Selector.Spotify/FactoryProvider/RefreshTokenFactoryProvider.cs @@ -1,12 +1,7 @@ -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -using SpotifyAPI.Web; +using Selector.Spotify.ConfigFactory; - -namespace Selector +namespace Selector.Spotify.FactoryProvider { /// <summary> /// Get config from a refresh token @@ -22,9 +17,10 @@ namespace Selector public virtual Task<RefreshTokenFactory> GetFactory(string refreshToken) { - if(string.IsNullOrEmpty(refreshToken)) throw new ArgumentException("Null or empty refresh key provided"); + if (string.IsNullOrEmpty(refreshToken)) throw new ArgumentException("Null or empty refresh key provided"); - return Task.FromResult(new RefreshTokenFactory(Credentials.ClientId, Credentials.ClientSecret, refreshToken)); + return Task.FromResult( + new RefreshTokenFactory(Credentials.ClientId, Credentials.ClientSecret, refreshToken)); } } -} +} \ No newline at end of file diff --git a/Selector/JsonContext.cs b/Selector.Spotify/JsonContext.cs similarity index 53% rename from Selector/JsonContext.cs rename to Selector.Spotify/JsonContext.cs index 5002800..3f774b7 100644 --- a/Selector/JsonContext.cs +++ b/Selector.Spotify/JsonContext.cs @@ -1,12 +1,12 @@ using System.Text.Json.Serialization; using SpotifyAPI.Web; -namespace Selector +namespace Selector.Spotify { [JsonSerializable(typeof(CurrentlyPlayingDTO))] [JsonSerializable(typeof(TrackAudioFeatures))] - [JsonSerializable(typeof(ListeningChangeEventArgs))] - public partial class JsonContext: JsonSerializerContext + [JsonSerializable(typeof(SpotifyListeningChangeEventArgs))] + public partial class SpotifyJsonContext : JsonSerializerContext { } -} +} \ No newline at end of file diff --git a/Selector.Spotify/Selector.Spotify.csproj b/Selector.Spotify/Selector.Spotify.csproj new file mode 100644 index 0000000..9b2f3de --- /dev/null +++ b/Selector.Spotify/Selector.Spotify.csproj @@ -0,0 +1,20 @@ +<Project Sdk="Microsoft.NET.Sdk"> + + <PropertyGroup> + <TargetFramework>net9.0</TargetFramework> + <ImplicitUsings>enable</ImplicitUsings> + <Nullable>enable</Nullable> + </PropertyGroup> + + <ItemGroup> + <PackageReference Include="Microsoft.Extensions.Logging" Version="9.0.0"/> + <PackageReference Include="SpotifyAPI.Web" Version="7.2.1"/> + <PackageReference Include="Inflatable.Lastfm" Version="1.2.0"/> + <PackageReference Include="System.Linq.Async" Version="6.0.1"/> + </ItemGroup> + + <ItemGroup> + <ProjectReference Include="..\Selector\Selector.csproj"/> + </ItemGroup> + +</Project> diff --git a/Selector/Timeline/AnalysedTrackTimeline.cs b/Selector.Spotify/Timeline/AnalysedTrackTimeline.cs similarity index 87% rename from Selector/Timeline/AnalysedTrackTimeline.cs rename to Selector.Spotify/Timeline/AnalysedTrackTimeline.cs index 89ffc77..3dd90a5 100644 --- a/Selector/Timeline/AnalysedTrackTimeline.cs +++ b/Selector.Spotify/Timeline/AnalysedTrackTimeline.cs @@ -1,17 +1,14 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using System.Linq; +using Selector.Spotify.Consumer; using SpotifyAPI.Web; -namespace Selector +namespace Selector.Spotify.Timeline { - public class AnalysedTrackTimeline + public class AnalysedTrackTimeline : Timeline<AnalysedTrack>, ITrackTimeline<AnalysedTrack> { public IEqual EqualityChecker { get; set; } - public AnalysedTrack Get(FullTrack track) + public AnalysedTrack Get(FullTrack track) => GetAll(track) .LastOrDefault(); @@ -47,4 +44,4 @@ namespace Selector => Recent .Where(i => EqualityChecker.IsEqual(i.Item.Track.Artists[0], artist)); } -} +} \ No newline at end of file diff --git a/Selector/Timeline/Interfaces/ITrackTimeline.cs b/Selector.Spotify/Timeline/ITrackTimeline.cs similarity index 79% rename from Selector/Timeline/Interfaces/ITrackTimeline.cs rename to Selector.Spotify/Timeline/ITrackTimeline.cs index 54eeb9e..fc1b30b 100644 --- a/Selector/Timeline/Interfaces/ITrackTimeline.cs +++ b/Selector.Spotify/Timeline/ITrackTimeline.cs @@ -1,10 +1,8 @@ -using System; -using System.Collections.Generic; -using SpotifyAPI.Web; +using SpotifyAPI.Web; -namespace Selector +namespace Selector.Spotify.Timeline { - public interface ITrackTimeline<T>: ITimeline<T> + public interface ITrackTimeline<T> : ITimeline<T> { public T Get(FullTrack track); public IEnumerable<T> GetAll(FullTrack track); @@ -18,4 +16,4 @@ namespace Selector public IEnumerable<T> GetAll(SimpleArtist artist); public IEnumerable<TimelineItem<T>> GetAllTimelineItems(SimpleArtist artist); } -} +} \ No newline at end of file diff --git a/Selector/Timeline/PlayerTimeline.cs b/Selector.Spotify/Timeline/PlayerTimeline.cs similarity index 84% rename from Selector/Timeline/PlayerTimeline.cs rename to Selector.Spotify/Timeline/PlayerTimeline.cs index 8610b47..a362bff 100644 --- a/Selector/Timeline/PlayerTimeline.cs +++ b/Selector.Spotify/Timeline/PlayerTimeline.cs @@ -1,19 +1,15 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using System.Linq; -using SpotifyAPI.Web; +using SpotifyAPI.Web; -namespace Selector +namespace Selector.Spotify.Timeline { - public class PlayerTimeline + public class PlayerTimeline : Timeline<CurrentlyPlayingContext>, ITrackTimeline<CurrentlyPlayingContext> { public IEqual EqualityChecker { get; set; } public override void Add(CurrentlyPlayingContext item) => Add(item, DateHelper.FromUnixMilli(item.Timestamp)); - public CurrentlyPlayingContext Get(FullTrack track) + public CurrentlyPlayingContext Get(FullTrack track) => GetAll(track) .LastOrDefault(); @@ -24,7 +20,7 @@ namespace Selector public IEnumerable<TimelineItem<CurrentlyPlayingContext>> GetAllTimelineItems(FullTrack track) => Recent .Where(i => i.Item.Item is FullTrack iterTrack - && EqualityChecker.IsEqual(iterTrack, track)); + && EqualityChecker.IsEqual(iterTrack, track)); public CurrentlyPlayingContext Get(FullEpisode ep) => GetAll(ep) @@ -37,7 +33,7 @@ namespace Selector public IEnumerable<TimelineItem<CurrentlyPlayingContext>> GetAllTimelineItems(FullEpisode ep) => Recent .Where(i => i.Item.Item is FullEpisode iterEp - && EqualityChecker.IsEqual(iterEp, ep)); + && EqualityChecker.IsEqual(iterEp, ep)); public CurrentlyPlayingContext Get(SimpleAlbum album) => GetAll(album) @@ -50,7 +46,7 @@ namespace Selector public IEnumerable<TimelineItem<CurrentlyPlayingContext>> GetAllTimelineItems(SimpleAlbum album) => Recent .Where(i => i.Item.Item is FullTrack iterTrack - && EqualityChecker.IsEqual(iterTrack.Album, album)); + && EqualityChecker.IsEqual(iterTrack.Album, album)); public CurrentlyPlayingContext Get(SimpleArtist artist) => GetAll(artist) @@ -63,7 +59,7 @@ namespace Selector public IEnumerable<TimelineItem<CurrentlyPlayingContext>> GetAllTimelineItems(SimpleArtist artist) => Recent .Where(i => i.Item.Item is FullTrack iterTrack - && EqualityChecker.IsEqual(iterTrack.Artists[0], artist)); + && EqualityChecker.IsEqual(iterTrack.Artists[0], artist)); public CurrentlyPlayingContext Get(Device device) => GetAll(device) @@ -73,7 +69,7 @@ namespace Selector => GetAllTimelineItems(device) .Select(t => t.Item); - public IEnumerable<TimelineItem<CurrentlyPlayingContext>> GetAllTimelineItems(Device device) + public IEnumerable<TimelineItem<CurrentlyPlayingContext>> GetAllTimelineItems(Device device) => Recent .Where(i => EqualityChecker.IsEqual(i.Item.Device, device)); @@ -89,4 +85,4 @@ namespace Selector => Recent .Where(i => EqualityChecker.IsEqual(i.Item.Context, context)); } -} +} \ No newline at end of file diff --git a/Selector/Watcher/BaseSpotifyWatcher.cs b/Selector.Spotify/Watcher/BaseSpotifyWatcher.cs similarity index 87% rename from Selector/Watcher/BaseSpotifyWatcher.cs rename to Selector.Spotify/Watcher/BaseSpotifyWatcher.cs index 76f195f..352c828 100644 --- a/Selector/Watcher/BaseSpotifyWatcher.cs +++ b/Selector.Spotify/Watcher/BaseSpotifyWatcher.cs @@ -1,8 +1,6 @@ -using System.Collections.Generic; -using System.Linq; using Microsoft.Extensions.Logging; -namespace Selector; +namespace Selector.Spotify.Watcher; public abstract class BaseSpotifyWatcher(ILogger<BaseWatcher> logger = null) : BaseWatcher(logger) { diff --git a/Selector/Watcher/DummySpotifyPlayerWatcher.cs b/Selector.Spotify/Watcher/DummySpotifyPlayerWatcher.cs similarity index 96% rename from Selector/Watcher/DummySpotifyPlayerWatcher.cs rename to Selector.Spotify/Watcher/DummySpotifyPlayerWatcher.cs index 3520b62..55af0a7 100644 --- a/Selector/Watcher/DummySpotifyPlayerWatcher.cs +++ b/Selector.Spotify/Watcher/DummySpotifyPlayerWatcher.cs @@ -1,11 +1,8 @@ -using System; -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging; +using Selector.Extensions; using SpotifyAPI.Web; -namespace Selector +namespace Selector.Spotify.Watcher { public class DummySpotifyPlayerWatcher : SpotifyPlayerWatcher { diff --git a/Selector/Watcher/Interfaces/IPlaylistWatcher.cs b/Selector.Spotify/Watcher/Interfaces/IPlaylistWatcher.cs similarity index 87% rename from Selector/Watcher/Interfaces/IPlaylistWatcher.cs rename to Selector.Spotify/Watcher/Interfaces/IPlaylistWatcher.cs index 8c1d6d6..7a8a621 100644 --- a/Selector/Watcher/Interfaces/IPlaylistWatcher.cs +++ b/Selector.Spotify/Watcher/Interfaces/IPlaylistWatcher.cs @@ -1,9 +1,8 @@ -using System; using SpotifyAPI.Web; -namespace Selector +namespace Selector.Spotify { - public interface IPlaylistWatcher: IWatcher + public interface IPlaylistWatcher : IWatcher { public event EventHandler<PlaylistChangeEventArgs> NetworkPoll; public event EventHandler<PlaylistChangeEventArgs> SnapshotChange; @@ -17,4 +16,4 @@ namespace Selector public FullPlaylist Live { get; } public Timeline<FullPlaylist> Past { get; } } -} +} \ No newline at end of file diff --git a/Selector.Spotify/Watcher/Interfaces/ISpotifyPlayerWatcher.cs b/Selector.Spotify/Watcher/Interfaces/ISpotifyPlayerWatcher.cs new file mode 100644 index 0000000..e571c8d --- /dev/null +++ b/Selector.Spotify/Watcher/Interfaces/ISpotifyPlayerWatcher.cs @@ -0,0 +1,30 @@ +using Selector.Spotify.Timeline; +using SpotifyAPI.Web; + +namespace Selector.Spotify +{ + public interface ISpotifyPlayerWatcher : IWatcher + { + /// <summary> + /// Track or episode changes + /// </summary> + public event EventHandler<SpotifyListeningChangeEventArgs> NetworkPoll; + + public event EventHandler<SpotifyListeningChangeEventArgs> ItemChange; + public event EventHandler<SpotifyListeningChangeEventArgs> AlbumChange; + public event EventHandler<SpotifyListeningChangeEventArgs> ArtistChange; + public event EventHandler<SpotifyListeningChangeEventArgs> ContextChange; + public event EventHandler<SpotifyListeningChangeEventArgs> ContentChange; + + public event EventHandler<SpotifyListeningChangeEventArgs> VolumeChange; + public event EventHandler<SpotifyListeningChangeEventArgs> DeviceChange; + public event EventHandler<SpotifyListeningChangeEventArgs> PlayingChange; + + /// <summary> + /// 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/ISpotifyWatcherFactory.cs b/Selector.Spotify/Watcher/Interfaces/ISpotifyWatcherFactory.cs similarity index 74% rename from Selector/Watcher/Interfaces/ISpotifyWatcherFactory.cs rename to Selector.Spotify/Watcher/Interfaces/ISpotifyWatcherFactory.cs index e17bf38..0f391cf 100644 --- a/Selector/Watcher/Interfaces/ISpotifyWatcherFactory.cs +++ b/Selector.Spotify/Watcher/Interfaces/ISpotifyWatcherFactory.cs @@ -1,6 +1,6 @@ -using System.Threading.Tasks; +using Selector.Spotify.ConfigFactory; -namespace Selector +namespace Selector.Spotify { public interface ISpotifyWatcherFactory { diff --git a/Selector/Watcher/PlaylistWatcher.cs b/Selector.Spotify/Watcher/PlaylistWatcher.cs similarity index 97% rename from Selector/Watcher/PlaylistWatcher.cs rename to Selector.Spotify/Watcher/PlaylistWatcher.cs index f23505a..fb206e9 100644 --- a/Selector/Watcher/PlaylistWatcher.cs +++ b/Selector.Spotify/Watcher/PlaylistWatcher.cs @@ -1,14 +1,10 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; -using Selector.Equality; +using Selector.Extensions; +using Selector.Spotify.Equality; using SpotifyAPI.Web; -namespace Selector +namespace Selector.Spotify.Watcher { public class PlaylistWatcherConfig { diff --git a/Selector/Watcher/SpotifyPlayerWatcher.cs b/Selector.Spotify/Watcher/SpotifyPlayerWatcher.cs similarity index 84% rename from Selector/Watcher/SpotifyPlayerWatcher.cs rename to Selector.Spotify/Watcher/SpotifyPlayerWatcher.cs index 7bb910e..0096628 100644 --- a/Selector/Watcher/SpotifyPlayerWatcher.cs +++ b/Selector.Spotify/Watcher/SpotifyPlayerWatcher.cs @@ -1,12 +1,10 @@ -using System; -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; +using Selector.Extensions; +using Selector.Spotify.Timeline; using SpotifyAPI.Web; -namespace Selector +namespace Selector.Spotify.Watcher { public class SpotifyPlayerWatcher : BaseSpotifyWatcher, ISpotifyPlayerWatcher { @@ -14,16 +12,16 @@ namespace Selector private readonly IPlayerClient spotifyClient; private readonly IEqual eq; - public event EventHandler<ListeningChangeEventArgs> NetworkPoll; - public event EventHandler<ListeningChangeEventArgs> ItemChange; - public event EventHandler<ListeningChangeEventArgs> AlbumChange; - public event EventHandler<ListeningChangeEventArgs> ArtistChange; - public event EventHandler<ListeningChangeEventArgs> ContextChange; - public event EventHandler<ListeningChangeEventArgs> ContentChange; + public event EventHandler<SpotifyListeningChangeEventArgs> NetworkPoll; + public event EventHandler<SpotifyListeningChangeEventArgs> ItemChange; + public event EventHandler<SpotifyListeningChangeEventArgs> AlbumChange; + public event EventHandler<SpotifyListeningChangeEventArgs> ArtistChange; + public event EventHandler<SpotifyListeningChangeEventArgs> ContextChange; + public event EventHandler<SpotifyListeningChangeEventArgs> ContentChange; - public event EventHandler<ListeningChangeEventArgs> VolumeChange; - public event EventHandler<ListeningChangeEventArgs> DeviceChange; - public event EventHandler<ListeningChangeEventArgs> PlayingChange; + public event EventHandler<SpotifyListeningChangeEventArgs> VolumeChange; + public event EventHandler<SpotifyListeningChangeEventArgs> DeviceChange; + public event EventHandler<SpotifyListeningChangeEventArgs> PlayingChange; public CurrentlyPlayingContext Live { get; protected set; } protected CurrentlyPlayingContext Previous { get; set; } @@ -229,8 +227,8 @@ namespace Selector } } - protected ListeningChangeEventArgs GetEvent() => - ListeningChangeEventArgs.From(Previous, Live, Past, id: Id, username: SpotifyUsername); + protected SpotifyListeningChangeEventArgs GetEvent() => + SpotifyListeningChangeEventArgs.From(Previous, Live, Past, id: Id, username: SpotifyUsername); /// <summary> /// Store currently playing in last plays. Determine whether new list or appending required @@ -243,47 +241,47 @@ namespace Selector #region Event Firers - protected virtual void OnNetworkPoll(ListeningChangeEventArgs args) + protected virtual void OnNetworkPoll(SpotifyListeningChangeEventArgs args) { NetworkPoll?.Invoke(this, args); } - protected virtual void OnItemChange(ListeningChangeEventArgs args) + protected virtual void OnItemChange(SpotifyListeningChangeEventArgs args) { ItemChange?.Invoke(this, args); } - protected virtual void OnAlbumChange(ListeningChangeEventArgs args) + protected virtual void OnAlbumChange(SpotifyListeningChangeEventArgs args) { AlbumChange?.Invoke(this, args); } - protected virtual void OnArtistChange(ListeningChangeEventArgs args) + protected virtual void OnArtistChange(SpotifyListeningChangeEventArgs args) { ArtistChange?.Invoke(this, args); } - protected virtual void OnContextChange(ListeningChangeEventArgs args) + protected virtual void OnContextChange(SpotifyListeningChangeEventArgs args) { ContextChange?.Invoke(this, args); } - protected virtual void OnContentChange(ListeningChangeEventArgs args) + protected virtual void OnContentChange(SpotifyListeningChangeEventArgs args) { ContentChange?.Invoke(this, args); } - protected virtual void OnVolumeChange(ListeningChangeEventArgs args) + protected virtual void OnVolumeChange(SpotifyListeningChangeEventArgs args) { VolumeChange?.Invoke(this, args); } - protected virtual void OnDeviceChange(ListeningChangeEventArgs args) + protected virtual void OnDeviceChange(SpotifyListeningChangeEventArgs args) { DeviceChange?.Invoke(this, args); } - protected virtual void OnPlayingChange(ListeningChangeEventArgs args) + protected virtual void OnPlayingChange(SpotifyListeningChangeEventArgs args) { PlayingChange?.Invoke(this, args); } diff --git a/Selector/Watcher/SpotifyWatcherFactory.cs b/Selector.Spotify/Watcher/SpotifyWatcherFactory.cs similarity index 96% rename from Selector/Watcher/SpotifyWatcherFactory.cs rename to Selector.Spotify/Watcher/SpotifyWatcherFactory.cs index dc95539..9f41e57 100644 --- a/Selector/Watcher/SpotifyWatcherFactory.cs +++ b/Selector.Spotify/Watcher/SpotifyWatcherFactory.cs @@ -1,10 +1,9 @@ -using System; -using System.Threading.Tasks; -using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; +using Selector.Spotify.ConfigFactory; using SpotifyAPI.Web; -namespace Selector +namespace Selector.Spotify.Watcher { public class SpotifyWatcherFactory : ISpotifyWatcherFactory { diff --git a/Selector.Tests/Consumer/AudioInjector.cs b/Selector.Tests/Consumer/AudioInjector.cs index 8b7367a..768e4fc 100644 --- a/Selector.Tests/Consumer/AudioInjector.cs +++ b/Selector.Tests/Consumer/AudioInjector.cs @@ -1,6 +1,10 @@ using System; using System.Threading; +using System.Threading.Tasks; using Moq; +using Selector.Spotify; +using Selector.Spotify.Consumer; +using Selector.Spotify.Timeline; using SpotifyAPI.Web; using Xunit; @@ -18,7 +22,7 @@ namespace Selector.Tests featureInjector.Subscribe(); - watcherMock.VerifyAdd(m => m.ItemChange += It.IsAny<EventHandler<ListeningChangeEventArgs>>()); + watcherMock.VerifyAdd(m => m.ItemChange += It.IsAny<EventHandler<SpotifyListeningChangeEventArgs>>()); } [Fact] @@ -31,7 +35,7 @@ namespace Selector.Tests featureInjector.Unsubscribe(); - watcherMock.VerifyRemove(m => m.ItemChange -= It.IsAny<EventHandler<ListeningChangeEventArgs>>()); + watcherMock.VerifyRemove(m => m.ItemChange -= It.IsAny<EventHandler<SpotifyListeningChangeEventArgs>>()); } [Fact] @@ -45,7 +49,8 @@ namespace Selector.Tests featureInjector.Subscribe(watcherFuncArgMock.Object); - watcherFuncArgMock.VerifyAdd(m => m.ItemChange += It.IsAny<EventHandler<ListeningChangeEventArgs>>()); + watcherFuncArgMock.VerifyAdd(m => + m.ItemChange += It.IsAny<EventHandler<SpotifyListeningChangeEventArgs>>()); watcherMock.VerifyNoOtherCalls(); } @@ -60,17 +65,18 @@ namespace Selector.Tests featureInjector.Unsubscribe(watcherFuncArgMock.Object); - watcherFuncArgMock.VerifyRemove(m => m.ItemChange -= It.IsAny<EventHandler<ListeningChangeEventArgs>>()); + watcherFuncArgMock.VerifyRemove(m => + m.ItemChange -= It.IsAny<EventHandler<SpotifyListeningChangeEventArgs>>()); watcherMock.VerifyNoOtherCalls(); } [Fact] - public async void CallbackNoId() + public async Task CallbackNoId() { var watcherMock = new Mock<ISpotifyPlayerWatcher>(); var spotifyMock = new Mock<ITracksClient>(); var timelineMock = new Mock<AnalysedTrackTimeline>(); - var eventArgsMock = new Mock<ListeningChangeEventArgs>(); + var eventArgsMock = new Mock<SpotifyListeningChangeEventArgs>(); var playingMock = new Mock<CurrentlyPlayingContext>(); var trackMock = new Mock<FullTrack>(); var featureMock = new Mock<TrackAudioFeatures>(); @@ -93,12 +99,12 @@ namespace Selector.Tests } [Fact] - public async void CallbackWithId() + public async Task CallbackWithId() { var watcherMock = new Mock<ISpotifyPlayerWatcher>(); var spotifyMock = new Mock<ITracksClient>(); var timelineMock = new Mock<AnalysedTrackTimeline>(); - var eventArgsMock = new Mock<ListeningChangeEventArgs>(); + var eventArgsMock = new Mock<SpotifyListeningChangeEventArgs>(); var playingMock = new Mock<CurrentlyPlayingContext>(); var trackMock = new Mock<FullTrack>(); var featureMock = new Mock<TrackAudioFeatures>(); diff --git a/Selector.Tests/Consumer/AudioInjectorFactory.cs b/Selector.Tests/Consumer/AudioInjectorFactory.cs index 60085cc..3a6b75e 100644 --- a/Selector.Tests/Consumer/AudioInjectorFactory.cs +++ b/Selector.Tests/Consumer/AudioInjectorFactory.cs @@ -1,14 +1,9 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Microsoft.Extensions.Logging; -using Xunit; -using Moq; using FluentAssertions; -using SpotifyAPI.Web; - -using Selector; +using Microsoft.Extensions.Logging; +using Moq; +using Selector.Spotify.ConfigFactory; +using Selector.Spotify.Consumer.Factory; +using Xunit; namespace Selector.Tests { @@ -28,4 +23,4 @@ namespace Selector.Tests consumer.Should().NotBeNull(); } } -} +} \ No newline at end of file diff --git a/Selector.Tests/Consumer/WebHook.cs b/Selector.Tests/Consumer/WebHook.cs index e736714..14b9030 100644 --- a/Selector.Tests/Consumer/WebHook.cs +++ b/Selector.Tests/Consumer/WebHook.cs @@ -6,6 +6,8 @@ using System.Threading.Tasks; using FluentAssertions; using Moq; using Moq.Protected; +using Selector.Spotify; +using Selector.Spotify.Consumer; using Xunit; namespace Selector.Tests @@ -24,8 +26,8 @@ namespace Selector.Tests .ReturnsAsync(msg); var watcherMock = new Mock<ISpotifyPlayerWatcher>(); - watcherMock.SetupAdd(w => w.ItemChange += It.IsAny<EventHandler<ListeningChangeEventArgs>>()); - watcherMock.SetupRemove(w => w.ItemChange -= It.IsAny<EventHandler<ListeningChangeEventArgs>>()); + watcherMock.SetupAdd(w => w.ItemChange += It.IsAny<EventHandler<SpotifyListeningChangeEventArgs>>()); + watcherMock.SetupRemove(w => w.ItemChange -= It.IsAny<EventHandler<SpotifyListeningChangeEventArgs>>()); var link = "https://link"; var content = new StringContent(""); @@ -40,7 +42,7 @@ namespace Selector.Tests var webHook = new WebHook(watcherMock.Object, http, config); webHook.Subscribe(); - watcherMock.Raise(w => w.ItemChange += null, this, new ListeningChangeEventArgs()); + watcherMock.Raise(w => w.ItemChange += null, this, new SpotifyListeningChangeEventArgs()); await Task.Delay(100); @@ -84,7 +86,7 @@ namespace Selector.Tests webHook.FailedRequest += (o, e) => { failedEvent = !successful; }; - await webHook.AsyncCallback(ListeningChangeEventArgs.From(new(), new(), new())); + await webHook.AsyncCallback(SpotifyListeningChangeEventArgs.From(new(), new(), new())); predicateEvent.Should().Be(predicate); successfulEvent.Should().Be(successful); diff --git a/Selector.Tests/Equality/Equal.cs b/Selector.Tests/Equality/Equal.cs index 38f8835..c7ee3ce 100644 --- a/Selector.Tests/Equality/Equal.cs +++ b/Selector.Tests/Equality/Equal.cs @@ -1,62 +1,66 @@ -using System; using System.Collections.Generic; -using Xunit; -using Moq; using FluentAssertions; +using Selector.Spotify.Equality; using SpotifyAPI.Web; - -using Selector; +using Xunit; namespace Selector.Tests { public class EqualTests { - public static IEnumerable<object[]> TrackData => - new List<object[]> - { - // SAME - new object[] { - Helper.FullTrack("1", "1", "1"), - Helper.FullTrack("1", "1", "1"), - true - }, - // WRONG ALBUM - new object[] { - Helper.FullTrack("1", "1", "1"), - Helper.FullTrack("1", "2", "1"), - false - }, - // WRONG TRACK - new object[] { - Helper.FullTrack("1", "1", "1"), - Helper.FullTrack("2", "1", "1"), - false - }, - // WRONG ARTIST - new object[] { - Helper.FullTrack("1", "1", "1"), - Helper.FullTrack("1", "1", "2"), - false - }, - // WRONG TRACK/ARTIST - new object[] { - Helper.FullTrack("1", "1", "1"), - Helper.FullTrack("2", "1", "2"), - false - }, - // RIGHT MULTIPLE ARTISTS - new object[] { - Helper.FullTrack("1", "1", new List<string>() { "1", "2" }), - Helper.FullTrack("1", "1", new List<string>() { "1", "2" }), - true - }, - // WRONG ARTISTS - new object[] { - Helper.FullTrack("1", "1", new List<string>() { "1", "2" }), - Helper.FullTrack("1", "1", new List<string>() { "1" }), - false - } - }; + public static IEnumerable<object[]> TrackData => + new List<object[]> + { + // SAME + new object[] + { + Helper.FullTrack("1", "1", "1"), + Helper.FullTrack("1", "1", "1"), + true + }, + // WRONG ALBUM + new object[] + { + Helper.FullTrack("1", "1", "1"), + Helper.FullTrack("1", "2", "1"), + false + }, + // WRONG TRACK + new object[] + { + Helper.FullTrack("1", "1", "1"), + Helper.FullTrack("2", "1", "1"), + false + }, + // WRONG ARTIST + new object[] + { + Helper.FullTrack("1", "1", "1"), + Helper.FullTrack("1", "1", "2"), + false + }, + // WRONG TRACK/ARTIST + new object[] + { + Helper.FullTrack("1", "1", "1"), + Helper.FullTrack("2", "1", "2"), + false + }, + // RIGHT MULTIPLE ARTISTS + new object[] + { + Helper.FullTrack("1", "1", new List<string>() { "1", "2" }), + Helper.FullTrack("1", "1", new List<string>() { "1", "2" }), + true + }, + // WRONG ARTISTS + new object[] + { + Helper.FullTrack("1", "1", new List<string>() { "1", "2" }), + Helper.FullTrack("1", "1", new List<string>() { "1" }), + false + } + }; [Theory] [MemberData(nameof(TrackData))] @@ -67,39 +71,44 @@ namespace Selector.Tests } public static IEnumerable<object[]> AlbumData => - new List<object[]> - { - // SAME - new object[] { - Helper.SimpleAlbum("1", "1"), - Helper.SimpleAlbum("1", "1"), - true - }, - // DIFFERENT NAME - new object[] { - Helper.SimpleAlbum("1", "1"), - Helper.SimpleAlbum("2", "1"), - false - }, - // DIFFERENT ARTIST - new object[] { - Helper.SimpleAlbum("1", "1"), - Helper.SimpleAlbum("1", "2"), - false - }, - // SAME ARTISTS - new object[] { - Helper.SimpleAlbum("1", new List<string>() { "1", "2" }), - Helper.SimpleAlbum("1", new List<string>() { "1", "2" }), - true - }, - // DIFFERENT ARTISTS - new object[] { - Helper.SimpleAlbum("1", new List<string>() { "1", "2" }), - Helper.SimpleAlbum("1", new List<string>() { "1" }), - false - }, - }; + new List<object[]> + { + // SAME + new object[] + { + Helper.SimpleAlbum("1", "1"), + Helper.SimpleAlbum("1", "1"), + true + }, + // DIFFERENT NAME + new object[] + { + Helper.SimpleAlbum("1", "1"), + Helper.SimpleAlbum("2", "1"), + false + }, + // DIFFERENT ARTIST + new object[] + { + Helper.SimpleAlbum("1", "1"), + Helper.SimpleAlbum("1", "2"), + false + }, + // SAME ARTISTS + new object[] + { + Helper.SimpleAlbum("1", new List<string>() { "1", "2" }), + Helper.SimpleAlbum("1", new List<string>() { "1", "2" }), + true + }, + // DIFFERENT ARTISTS + new object[] + { + Helper.SimpleAlbum("1", new List<string>() { "1", "2" }), + Helper.SimpleAlbum("1", new List<string>() { "1" }), + false + }, + }; [Theory] [MemberData(nameof(AlbumData))] @@ -110,21 +119,23 @@ namespace Selector.Tests } public static IEnumerable<object[]> ArtistData => - new List<object[]> - { - // SAME - new object[] { - Helper.SimpleArtist("1"), - Helper.SimpleArtist("1"), - true - }, - // DIFFERENT - new object[] { - Helper.SimpleArtist("1"), - Helper.SimpleArtist("2"), - false - } - }; + new List<object[]> + { + // SAME + new object[] + { + Helper.SimpleArtist("1"), + Helper.SimpleArtist("1"), + true + }, + // DIFFERENT + new object[] + { + Helper.SimpleArtist("1"), + Helper.SimpleArtist("2"), + false + } + }; [Theory] [MemberData(nameof(ArtistData))] @@ -135,21 +146,23 @@ namespace Selector.Tests } public static IEnumerable<object[]> EpisodeData => - new List<object[]> - { - // SAME - new object[] { - Helper.FullEpisode("1"), - Helper.FullEpisode("1"), - true - }, - // DIFFERENT - new object[] { - Helper.FullEpisode("1"), - Helper.FullEpisode("2"), - false - } - }; + new List<object[]> + { + // SAME + new object[] + { + Helper.FullEpisode("1"), + Helper.FullEpisode("1"), + true + }, + // DIFFERENT + new object[] + { + Helper.FullEpisode("1"), + Helper.FullEpisode("2"), + false + } + }; [Theory] [MemberData(nameof(EpisodeData))] @@ -159,4 +172,4 @@ namespace Selector.Tests eq.IsEqual(episode1, episode2).Should().Be(shouldEqual); } } -} +} \ No newline at end of file diff --git a/Selector.Tests/Equality/UriEqual.cs b/Selector.Tests/Equality/UriEqual.cs index b852999..9a92dbe 100644 --- a/Selector.Tests/Equality/UriEqual.cs +++ b/Selector.Tests/Equality/UriEqual.cs @@ -1,32 +1,31 @@ -using System; using System.Collections.Generic; -using Xunit; -using Moq; using FluentAssertions; +using Selector.Spotify.Equality; using SpotifyAPI.Web; - -using Selector; +using Xunit; namespace Selector.Tests { public class UriEqualTests { - public static IEnumerable<object[]> TrackData => - new List<object[]> - { - // SAME - new object[] { - Helper.FullTrack("1"), - Helper.FullTrack("1"), - true - }, - // WRONG - new object[] { - Helper.FullTrack("1"), - Helper.FullTrack("2"), - false - } - }; + public static IEnumerable<object[]> TrackData => + new List<object[]> + { + // SAME + new object[] + { + Helper.FullTrack("1"), + Helper.FullTrack("1"), + true + }, + // WRONG + new object[] + { + Helper.FullTrack("1"), + Helper.FullTrack("2"), + false + } + }; [Theory] [MemberData(nameof(TrackData))] @@ -38,22 +37,24 @@ namespace Selector.Tests .Be(shouldEqual); } - public static IEnumerable<object[]> AlbumData => - new List<object[]> - { - // SAME - new object[] { - Helper.SimpleAlbum("1"), - Helper.SimpleAlbum("1"), - true - }, - // WRONG - new object[] { - Helper.SimpleAlbum("1"), - Helper.SimpleAlbum("2"), - false - } - }; + public static IEnumerable<object[]> AlbumData => + new List<object[]> + { + // SAME + new object[] + { + Helper.SimpleAlbum("1"), + Helper.SimpleAlbum("1"), + true + }, + // WRONG + new object[] + { + Helper.SimpleAlbum("1"), + Helper.SimpleAlbum("2"), + false + } + }; [Theory] [MemberData(nameof(AlbumData))] @@ -65,22 +66,24 @@ namespace Selector.Tests .Be(shouldEqual); } - public static IEnumerable<object[]> ArtistData => - new List<object[]> - { - // SAME - new object[] { - Helper.SimpleArtist("1"), - Helper.SimpleArtist("1"), - true - }, - // WRONG - new object[] { - Helper.SimpleArtist("1"), - Helper.SimpleArtist("2"), - false - } - }; + public static IEnumerable<object[]> ArtistData => + new List<object[]> + { + // SAME + new object[] + { + Helper.SimpleArtist("1"), + Helper.SimpleArtist("1"), + true + }, + // WRONG + new object[] + { + Helper.SimpleArtist("1"), + Helper.SimpleArtist("2"), + false + } + }; [Theory] [MemberData(nameof(ArtistData))] @@ -92,22 +95,24 @@ namespace Selector.Tests .Be(shouldEqual); } - public static IEnumerable<object[]> EpisodeData => - new List<object[]> - { - // SAME - new object[] { - Helper.FullEpisode("1"), - Helper.FullEpisode("1"), - true - }, - // WRONG - new object[] { - Helper.FullEpisode("1"), - Helper.FullEpisode("2"), - false - } - }; + public static IEnumerable<object[]> EpisodeData => + new List<object[]> + { + // SAME + new object[] + { + Helper.FullEpisode("1"), + Helper.FullEpisode("1"), + true + }, + // WRONG + new object[] + { + Helper.FullEpisode("1"), + Helper.FullEpisode("2"), + false + } + }; [Theory] [MemberData(nameof(EpisodeData))] @@ -119,22 +124,24 @@ namespace Selector.Tests .Be(shouldEqual); } - public static IEnumerable<object[]> ContextData => - new List<object[]> - { - // SAME - new object[] { - Helper.Context("1"), - Helper.Context("1"), - true - }, - // WRONG - new object[] { - Helper.Context("1"), - Helper.Context("2"), - false - } - }; + public static IEnumerable<object[]> ContextData => + new List<object[]> + { + // SAME + new object[] + { + Helper.Context("1"), + Helper.Context("1"), + true + }, + // WRONG + new object[] + { + Helper.Context("1"), + Helper.Context("2"), + false + } + }; [Theory] [MemberData(nameof(ContextData))] @@ -146,22 +153,24 @@ namespace Selector.Tests .Be(shouldEqual); } - public static IEnumerable<object[]> DeviceData => - new List<object[]> - { - // SAME - new object[] { - Helper.Device("1"), - Helper.Device("1"), - true - }, - // WRONG - new object[] { - Helper.Device("1"), - Helper.Device("2"), - false - } - }; + public static IEnumerable<object[]> DeviceData => + new List<object[]> + { + // SAME + new object[] + { + Helper.Device("1"), + Helper.Device("1"), + true + }, + // WRONG + new object[] + { + Helper.Device("1"), + Helper.Device("2"), + false + } + }; [Theory] [MemberData(nameof(DeviceData))] @@ -173,4 +182,4 @@ namespace Selector.Tests .Be(shouldEqual); } } -} +} \ No newline at end of file diff --git a/Selector.Tests/Selector.Tests.csproj b/Selector.Tests/Selector.Tests.csproj index eea3ce7..c7d478f 100644 --- a/Selector.Tests/Selector.Tests.csproj +++ b/Selector.Tests/Selector.Tests.csproj @@ -24,6 +24,8 @@ <ItemGroup> <ProjectReference Include="..\Selector.AppleMusic\Selector.AppleMusic.csproj"/> + <ProjectReference Include="..\Selector.LastFm\Selector.LastFm.csproj"/> + <ProjectReference Include="..\Selector.Spotify\Selector.Spotify.csproj"/> <ProjectReference Include="..\Selector\Selector.csproj" /> </ItemGroup> diff --git a/Selector.Tests/Timeline/PlayerTimeline.cs b/Selector.Tests/Timeline/PlayerTimeline.cs index 9b327e5..98b6bec 100644 --- a/Selector.Tests/Timeline/PlayerTimeline.cs +++ b/Selector.Tests/Timeline/PlayerTimeline.cs @@ -1,35 +1,35 @@ using System; using System.Collections.Generic; using System.Linq; -using Xunit; -using Moq; using FluentAssertions; +using Selector.Spotify.Timeline; using SpotifyAPI.Web; - -using Selector; +using Xunit; namespace Selector.Tests { public class PlayerTimelineTests { - public static IEnumerable<object[]> CountData => - new List<object[]> - { - new object[] { - new CurrentlyPlayingContext[] + public static IEnumerable<object[]> CountData => + new List<object[]> + { + new object[] { - Helper.CurrentPlayback(Helper.FullTrack("uri1")) - } - }, - new object[] { - new CurrentlyPlayingContext[] + new CurrentlyPlayingContext[] + { + Helper.CurrentPlayback(Helper.FullTrack("uri1")) + } + }, + new object[] { - Helper.CurrentPlayback(Helper.FullTrack("uri1")), - Helper.CurrentPlayback(Helper.FullTrack("uri2")), - Helper.CurrentPlayback(Helper.FullTrack("uri3")), - } - }, - }; + new CurrentlyPlayingContext[] + { + Helper.CurrentPlayback(Helper.FullTrack("uri1")), + Helper.CurrentPlayback(Helper.FullTrack("uri2")), + Helper.CurrentPlayback(Helper.FullTrack("uri3")), + } + }, + }; [Theory] [MemberData(nameof(CountData))] @@ -37,7 +37,7 @@ namespace Selector.Tests { var timeline = new PlayerTimeline(); - foreach(var i in currentlyPlaying) + foreach (var i in currentlyPlaying) { timeline.Add(i); } @@ -45,52 +45,60 @@ namespace Selector.Tests timeline.Count.Should().Be(currentlyPlaying.Length); } - public static IEnumerable<object[]> MaxSizeData => - new List<object[]> - { - new object[] { - new CurrentlyPlayingContext[] + public static IEnumerable<object[]> MaxSizeData => + new List<object[]> + { + new object[] { - Helper.CurrentPlayback(Helper.FullTrack("uri1")) - }, 5, 1 - }, - new object[] { - new CurrentlyPlayingContext[] + new CurrentlyPlayingContext[] + { + Helper.CurrentPlayback(Helper.FullTrack("uri1")) + }, + 5, 1 + }, + new object[] { - Helper.CurrentPlayback(Helper.FullTrack("uri1")), - Helper.CurrentPlayback(Helper.FullTrack("uri2")), - Helper.CurrentPlayback(Helper.FullTrack("uri3")) - }, 1, 1 - }, - new object[] { - new CurrentlyPlayingContext[] + new CurrentlyPlayingContext[] + { + Helper.CurrentPlayback(Helper.FullTrack("uri1")), + Helper.CurrentPlayback(Helper.FullTrack("uri2")), + Helper.CurrentPlayback(Helper.FullTrack("uri3")) + }, + 1, 1 + }, + new object[] { - Helper.CurrentPlayback(Helper.FullTrack("uri1")), - Helper.CurrentPlayback(Helper.FullTrack("uri2")), - Helper.CurrentPlayback(Helper.FullTrack("uri3")), - Helper.CurrentPlayback(Helper.FullTrack("uri4")), - Helper.CurrentPlayback(Helper.FullTrack("uri5")), - Helper.CurrentPlayback(Helper.FullTrack("uri6")), - Helper.CurrentPlayback(Helper.FullTrack("uri7")), - Helper.CurrentPlayback(Helper.FullTrack("uri8")), - }, 5, 5 - }, - new object[] { - new CurrentlyPlayingContext[] + new CurrentlyPlayingContext[] + { + Helper.CurrentPlayback(Helper.FullTrack("uri1")), + Helper.CurrentPlayback(Helper.FullTrack("uri2")), + Helper.CurrentPlayback(Helper.FullTrack("uri3")), + Helper.CurrentPlayback(Helper.FullTrack("uri4")), + Helper.CurrentPlayback(Helper.FullTrack("uri5")), + Helper.CurrentPlayback(Helper.FullTrack("uri6")), + Helper.CurrentPlayback(Helper.FullTrack("uri7")), + Helper.CurrentPlayback(Helper.FullTrack("uri8")), + }, + 5, 5 + }, + new object[] { - Helper.CurrentPlayback(Helper.FullTrack("uri1")), - Helper.CurrentPlayback(Helper.FullTrack("uri2")), - Helper.CurrentPlayback(Helper.FullTrack("uri3")), - Helper.CurrentPlayback(Helper.FullTrack("uri4")), - Helper.CurrentPlayback(Helper.FullTrack("uri5")), - Helper.CurrentPlayback(Helper.FullTrack("uri6")), - Helper.CurrentPlayback(Helper.FullTrack("uri7")), - Helper.CurrentPlayback(Helper.FullTrack("uri8")), - Helper.CurrentPlayback(Helper.FullTrack("uri9")), - Helper.CurrentPlayback(Helper.FullTrack("uri10")) - }, null, 10 - } - }; + new CurrentlyPlayingContext[] + { + Helper.CurrentPlayback(Helper.FullTrack("uri1")), + Helper.CurrentPlayback(Helper.FullTrack("uri2")), + Helper.CurrentPlayback(Helper.FullTrack("uri3")), + Helper.CurrentPlayback(Helper.FullTrack("uri4")), + Helper.CurrentPlayback(Helper.FullTrack("uri5")), + Helper.CurrentPlayback(Helper.FullTrack("uri6")), + Helper.CurrentPlayback(Helper.FullTrack("uri7")), + Helper.CurrentPlayback(Helper.FullTrack("uri8")), + Helper.CurrentPlayback(Helper.FullTrack("uri9")), + Helper.CurrentPlayback(Helper.FullTrack("uri10")) + }, + null, 10 + } + }; [Theory] [MemberData(nameof(MaxSizeData))] @@ -133,7 +141,8 @@ namespace Selector.Tests [Fact] public void Sort() { - var timeline = new PlayerTimeline(){ + var timeline = new PlayerTimeline() + { SortOnBackDate = false }; @@ -164,7 +173,7 @@ namespace Selector.Tests var earlier = Helper.CurrentPlayback(Helper.FullTrack("uri1")); var earlierDate = DateTime.Now; - var middle= Helper.CurrentPlayback(Helper.FullTrack("uri3")); + var middle = Helper.CurrentPlayback(Helper.FullTrack("uri3")); var middleDate = DateTime.Now.AddDays(1); var later = Helper.CurrentPlayback(Helper.FullTrack("uri2")); @@ -181,4 +190,4 @@ namespace Selector.Tests timeline.Select(i => i.Item).Should().Equal(earlier, middle, later); } } -} +} \ No newline at end of file diff --git a/Selector.Tests/Watcher/PlayerWatcher.cs b/Selector.Tests/Watcher/PlayerWatcher.cs index cd6d2f1..b62a2bf 100644 --- a/Selector.Tests/Watcher/PlayerWatcher.cs +++ b/Selector.Tests/Watcher/PlayerWatcher.cs @@ -3,6 +3,8 @@ using System.Threading; using System.Threading.Tasks; using FluentAssertions; using Moq; +using Selector.Spotify.Equality; +using Selector.Spotify.Watcher; using SpotifyAPI.Web; using Xunit; @@ -26,7 +28,7 @@ namespace Selector.Tests [Theory] [MemberData(nameof(NowPlayingData))] - public async void NowPlaying(List<CurrentlyPlayingContext> playing) + public async Task NowPlaying(List<CurrentlyPlayingContext> playing) { var playingQueue = new Queue<CurrentlyPlayingContext>(playing); @@ -252,7 +254,7 @@ namespace Selector.Tests [Theory] [MemberData(nameof(EventsData))] - public async void Events(List<CurrentlyPlayingContext> playing, List<string> toRaise, List<string> toNotRaise) + public async Task Events(List<CurrentlyPlayingContext> playing, List<string> toRaise, List<string> toNotRaise) { var playingQueue = new Queue<CurrentlyPlayingContext>(playing); @@ -279,7 +281,7 @@ namespace Selector.Tests [InlineData(1000, 3500, 4)] [InlineData(500, 3800, 8)] [InlineData(100, 250, 3)] - public async void Watch(int pollPeriod, int execTime, int numberOfCalls) + public async Task Watch(int pollPeriod, int execTime, int numberOfCalls) { var spotMock = new Mock<IPlayerClient>(); var eq = new UriEqual(); diff --git a/Selector.Tests/Watcher/PlaylistWatcher.cs b/Selector.Tests/Watcher/PlaylistWatcher.cs index b17bd85..d69e9aa 100644 --- a/Selector.Tests/Watcher/PlaylistWatcher.cs +++ b/Selector.Tests/Watcher/PlaylistWatcher.cs @@ -1,39 +1,41 @@ -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 Selector.Spotify.Watcher; +using SpotifyAPI.Web; +using Xunit; namespace Selector.Tests { public class PlaylistWatcherTests { public static IEnumerable<object[]> CurrentPlaylistData => - new List<object[]> - { - new object[] { new List<FullPlaylist>(){ - Helper.FullPlaylist("playlist1"), - Helper.FullPlaylist("playlist1"), - Helper.FullPlaylist("playlist1"), - Helper.FullPlaylist("playlist1") + new List<object[]> + { + new object[] + { + new List<FullPlaylist>() + { + Helper.FullPlaylist("playlist1"), + Helper.FullPlaylist("playlist1"), + Helper.FullPlaylist("playlist1"), + Helper.FullPlaylist("playlist1") + } } - } - }; + }; [Theory] [MemberData(nameof(CurrentPlaylistData))] - public async void CurrentPlaylist(List<FullPlaylist> playing) + public async Task CurrentPlaylist(List<FullPlaylist> playing) { var playlistDequeue = new Queue<FullPlaylist>(playing); var spotMock = new Mock<ISpotifyClient>(); - spotMock.Setup(s => s.Playlists.Get(It.IsAny<string>(), It.IsAny<CancellationToken>()).Result).Returns(playlistDequeue.Dequeue); + spotMock.Setup(s => s.Playlists.Get(It.IsAny<string>(), It.IsAny<CancellationToken>()).Result) + .Returns(playlistDequeue.Dequeue); var config = new PlaylistWatcherConfig() { PlaylistId = "spotify:playlist:test" }; var watcher = new PlaylistWatcher(config, spotMock.Object); @@ -46,41 +48,48 @@ namespace Selector.Tests } public static IEnumerable<object[]> EventsData => - new List<object[]> - { - // NO CHANGING - new object[] { new List<FullPlaylist>(){ - Helper.FullPlaylist("Playlist", snapshotId: "snapshot1"), - Helper.FullPlaylist("Playlist", snapshotId: "snapshot1"), - Helper.FullPlaylist("Playlist", snapshotId: "snapshot1"), + new List<object[]> + { + // NO CHANGING + new object[] + { + new List<FullPlaylist>() + { + Helper.FullPlaylist("Playlist", snapshotId: "snapshot1"), + Helper.FullPlaylist("Playlist", snapshotId: "snapshot1"), + Helper.FullPlaylist("Playlist", snapshotId: "snapshot1"), + }, + // to raise + new List<string>() { }, + // to not raise + new List<string>() { "SnapshotChange" } }, - // to raise - new List<string>(){ }, - // to not raise - new List<string>(){ "SnapshotChange" } - }, - // CHANGING SNAPSHOT - new object[] { new List<FullPlaylist>(){ - Helper.FullPlaylist("Playlist", snapshotId: "snapshot1"), - Helper.FullPlaylist("Playlist", snapshotId: "snapshot2"), - Helper.FullPlaylist("Playlist", snapshotId: "snapshot2"), - }, - // to raise - new List<string>(){ "SnapshotChange" }, - // to not raise - new List<string>(){ } - } - }; + // CHANGING SNAPSHOT + new object[] + { + new List<FullPlaylist>() + { + Helper.FullPlaylist("Playlist", snapshotId: "snapshot1"), + Helper.FullPlaylist("Playlist", snapshotId: "snapshot2"), + Helper.FullPlaylist("Playlist", snapshotId: "snapshot2"), + }, + // to raise + new List<string>() { "SnapshotChange" }, + // to not raise + new List<string>() { } + } + }; [Theory] [MemberData(nameof(EventsData))] - public async void Events(List<FullPlaylist> playing, List<string> toRaise, List<string> toNotRaise) + public async Task Events(List<FullPlaylist> playing, List<string> toRaise, List<string> toNotRaise) { var playlistDequeue = new Queue<FullPlaylist>(playing); var spotMock = new Mock<ISpotifyClient>(); - spotMock.Setup(s => s.Playlists.Get(It.IsAny<string>(), It.IsAny<CancellationToken>()).Result).Returns(playlistDequeue.Dequeue); + spotMock.Setup(s => s.Playlists.Get(It.IsAny<string>(), It.IsAny<CancellationToken>()).Result) + .Returns(playlistDequeue.Dequeue); var config = new PlaylistWatcherConfig() { PlaylistId = "spotify:playlist:test" }; var watcher = new PlaylistWatcher(config, spotMock.Object); @@ -96,4 +105,4 @@ namespace Selector.Tests toNotRaise.ForEach(r => monitoredWatcher.Should().NotRaise(r)); } } -} +} \ No newline at end of file diff --git a/Selector.Web/Areas/Identity/Pages/Account/Manage/_Layout.cshtml b/Selector.Web/Areas/Identity/Pages/Account/Manage/_Layout.cshtml index c477298..983b985 100644 --- a/Selector.Web/Areas/Identity/Pages/Account/Manage/_Layout.cshtml +++ b/Selector.Web/Areas/Identity/Pages/Account/Manage/_Layout.cshtml @@ -5,7 +5,8 @@ } else { - Layout = "/Areas/Identity/Pages/_Layout.cshtml"; + throw new NotImplementedException(); + // Layout = "/Areas/Identity/Pages/_Layout.cshtml"; } } diff --git a/Selector.Web/Hubs/NowPlayingHub.cs b/Selector.Web/Hubs/NowPlayingHub.cs index 52a830f..563e635 100644 --- a/Selector.Web/Hubs/NowPlayingHub.cs +++ b/Selector.Web/Hubs/NowPlayingHub.cs @@ -11,6 +11,8 @@ using Selector.Cache; using Selector.Model; using Selector.Model.Extensions; using Selector.SignalR; +using Selector.Spotify; +using Selector.Spotify.Consumer; using StackExchange.Redis; namespace Selector.Web.Hubs @@ -55,7 +57,8 @@ namespace Selector.Web.Hubs var nowPlaying = await Cache.StringGetAsync(Key.CurrentlyPlayingSpotify(Context.UserIdentifier)); if (nowPlaying != RedisValue.Null) { - var deserialised = JsonSerializer.Deserialize(nowPlaying, JsonContext.Default.CurrentlyPlayingDTO); + var deserialised = + JsonSerializer.Deserialize(nowPlaying, SpotifyJsonContext.Default.CurrentlyPlayingDTO); await Clients.Caller.OnNewPlaying(deserialised); } } diff --git a/Selector.Web/Startup.cs b/Selector.Web/Startup.cs index 7044b20..8ebfadd 100644 --- a/Selector.Web/Startup.cs +++ b/Selector.Web/Startup.cs @@ -15,6 +15,7 @@ using Selector.Events; using Selector.Extensions; using Selector.Model; using Selector.Model.Extensions; +using Selector.Spotify; using Selector.Web.Auth; using Selector.Web.Extensions; using Selector.Web.Hubs; @@ -33,10 +34,7 @@ namespace Selector.Web // This method gets called by the runtime. Use this method to add services to the container. public void ConfigureServices(IServiceCollection services) { - services.Configure<RootOptions>(options => - { - OptionsHelper.ConfigureOptions(options, Configuration); - }); + services.Configure<RootOptions>(options => { OptionsHelper.ConfigureOptions(options, Configuration); }); services.Configure<RedisOptions>(options => { Configuration.GetSection(string.Join(':', RootOptions.Key, RedisOptions.Key)).Bind(options); @@ -45,10 +43,7 @@ namespace Selector.Web { Configuration.GetSection(string.Join(':', RootOptions.Key, NowPlayingOptions.Key)).Bind(options); }); - services.Configure<JwtOptions>(options => - { - Configuration.GetSection(JwtOptions._Key).Bind(options); - }); + services.Configure<JwtOptions>(options => { Configuration.GetSection(JwtOptions._Key).Bind(options); }); services.Configure<AppleMusicOptions>(options => { Configuration.GetSection(AppleMusicOptions._Key).Bind(options); @@ -60,23 +55,23 @@ namespace Selector.Web { options.ClientId = config.ClientId; options.ClientSecret = config.ClientSecret; - }); + }); services.AddRazorPages(o => - { - o.Conventions.AllowAnonymousToPage("/"); - o.Conventions.AuthorizePage("/Now", AuthConstants.CookieAuthentication); - o.Conventions.AuthorizePage("/Past", AuthConstants.CookieAuthentication); - o.Conventions.AllowAnonymousToPage("/Privacy"); - o.Conventions.AllowAnonymousToPage("/Error"); - o.Conventions.AllowAnonymousToAreaPage("Identity", "/Login"); - o.Conventions.AllowAnonymousToAreaPage("Identity", "/Logout"); - o.Conventions.AllowAnonymousToAreaPage("Identity", "/Register"); - o.Conventions.AllowAnonymousToAreaPage("Identity", "/AccessDenied"); - o.Conventions.AllowAnonymousToAreaPage("Identity", "/Lockout"); - o.Conventions.AuthorizeAreaPage("Identity", "/Manage", AuthConstants.CookieAuthentication); - }) - .AddRazorRuntimeCompilation(); + { + o.Conventions.AllowAnonymousToPage("/"); + o.Conventions.AuthorizePage("/Now", AuthConstants.CookieAuthentication); + o.Conventions.AuthorizePage("/Past", AuthConstants.CookieAuthentication); + o.Conventions.AllowAnonymousToPage("/Privacy"); + o.Conventions.AllowAnonymousToPage("/Error"); + o.Conventions.AllowAnonymousToAreaPage("Identity", "/Login"); + o.Conventions.AllowAnonymousToAreaPage("Identity", "/Logout"); + o.Conventions.AllowAnonymousToAreaPage("Identity", "/Register"); + o.Conventions.AllowAnonymousToAreaPage("Identity", "/AccessDenied"); + o.Conventions.AllowAnonymousToAreaPage("Identity", "/Lockout"); + o.Conventions.AuthorizeAreaPage("Identity", "/Manage", AuthConstants.CookieAuthentication); + }) + .AddRazorRuntimeCompilation(); services.AddControllers(); services.AddSignalR(o => o.EnableDetailedErrors = true); services.AddHttpClient(); @@ -95,12 +90,12 @@ namespace Selector.Web public void ConfigureDB(IServiceCollection services, RootOptions config) { - services.AddDbContext<ApplicationDbContext>(options => + services.AddDbContext<ApplicationDbContext>(options => options.UseNpgsql(Configuration.GetConnectionString("Default")) ); services.AddDBPlayCountPuller(); services.AddTransient<IScrobbleRepository, ScrobbleRepository>() - .AddTransient<ISpotifyListenRepository, SpotifyListenRepository>(); + .AddTransient<ISpotifyListenRepository, SpotifyListenRepository>(); services.AddTransient<IListenRepository, MetaListenRepository>(); //services.AddTransient<IListenRepository, SpotifyListenRepository>(); @@ -130,7 +125,7 @@ namespace Selector.Web // User settings. options.User.AllowedUserNameCharacters = - "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._@+"; + "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._@+"; options.User.RequireUniqueEmail = false; options.SignIn.RequireConfirmedEmail = false; }); @@ -173,7 +168,8 @@ namespace Selector.Web { options.FallbackPolicy = new AuthorizationPolicyBuilder() .RequireAuthenticatedUser() - .AddAuthenticationSchemes(IdentityConstants.ApplicationScheme, JwtBearerDefaults.AuthenticationScheme) + .AddAuthenticationSchemes(IdentityConstants.ApplicationScheme, + JwtBearerDefaults.AuthenticationScheme) .Build(); options.AddPolicy(AuthConstants.CookieAuthentication, new AuthorizationPolicyBuilder() @@ -259,4 +255,4 @@ namespace Selector.Web } } } -} +} \ No newline at end of file diff --git a/Selector.sln b/Selector.sln index 1f7647c..e32cb68 100644 --- a/Selector.sln +++ b/Selector.sln @@ -25,6 +25,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Selector.SignalR", "Selecto EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Selector.AppleMusic", "Selector.AppleMusic\Selector.AppleMusic.csproj", "{5049A2F6-9604-49B1-B826-1CAC1B009D5D}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Selector.Spotify", "Selector.Spotify\Selector.Spotify.csproj", "{21E8CF70-8590-4CD3-BAA5-FDD2CD174682}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Selector.LastFm", "Selector.LastFm\Selector.LastFm.csproj", "{9EBEC247-C0C3-4201-928F-94558659E191}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -75,6 +79,14 @@ Global {5049A2F6-9604-49B1-B826-1CAC1B009D5D}.Debug|Any CPU.Build.0 = Debug|Any CPU {5049A2F6-9604-49B1-B826-1CAC1B009D5D}.Release|Any CPU.ActiveCfg = Release|Any CPU {5049A2F6-9604-49B1-B826-1CAC1B009D5D}.Release|Any CPU.Build.0 = Release|Any CPU + {21E8CF70-8590-4CD3-BAA5-FDD2CD174682}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {21E8CF70-8590-4CD3-BAA5-FDD2CD174682}.Debug|Any CPU.Build.0 = Debug|Any CPU + {21E8CF70-8590-4CD3-BAA5-FDD2CD174682}.Release|Any CPU.ActiveCfg = Release|Any CPU + {21E8CF70-8590-4CD3-BAA5-FDD2CD174682}.Release|Any CPU.Build.0 = Release|Any CPU + {9EBEC247-C0C3-4201-928F-94558659E191}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9EBEC247-C0C3-4201-928F-94558659E191}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9EBEC247-C0C3-4201-928F-94558659E191}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9EBEC247-C0C3-4201-928F-94558659E191}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/Selector/Consumers/IConsumer.cs b/Selector/Consumers/IConsumer.cs index 48180fa..29c9613 100644 --- a/Selector/Consumers/IConsumer.cs +++ b/Selector/Consumers/IConsumer.cs @@ -10,12 +10,4 @@ { public void Callback(object sender, T e); } - - public interface ISpotifyPlayerConsumer : IConsumer<ListeningChangeEventArgs> - { - } - - public interface IPlaylistConsumer : IConsumer<PlaylistChangeEventArgs> - { - } } \ No newline at end of file diff --git a/Selector/Equality/Equal.cs b/Selector/Equality/Equal.cs index 0678d94..4f173dc 100644 --- a/Selector/Equality/Equal.cs +++ b/Selector/Equality/Equal.cs @@ -1,7 +1,5 @@ using System; using System.Collections.Generic; -using System.Text; -using SpotifyAPI.Web; namespace Selector { @@ -11,12 +9,12 @@ namespace Selector public bool IsEqual<T>(T item, T other) { - if (item is null && other is null) return true; + if (item is null && other is null) return true; if (item is null ^ other is null) return false; if (comps.ContainsKey(typeof(T))) { - var comp = (IEqualityComparer<T>) comps[typeof(T)]; + var comp = (IEqualityComparer<T>)comps[typeof(T)]; return comp.Equals(item, other); } else @@ -25,4 +23,4 @@ namespace Selector } } } -} +} \ No newline at end of file diff --git a/Selector/Events.cs b/Selector/Events.cs new file mode 100644 index 0000000..c98d1f9 --- /dev/null +++ b/Selector/Events.cs @@ -0,0 +1,12 @@ +using System; + +namespace Selector; + +public class ListeningChangeEventArgs : EventArgs +{ + /// <summary> + /// String Id for watcher, used to hold user Db Id + /// </summary> + /// <value></value> + public string Id { get; set; } +} \ No newline at end of file diff --git a/Selector/Extensions/LoggingExtensions.cs b/Selector/Extensions/LoggingExtensions.cs deleted file mode 100644 index dc03ee9..0000000 --- a/Selector/Extensions/LoggingExtensions.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System; -using System.Collections.Generic; -using Microsoft.Extensions.Logging; - -namespace Selector -{ - public static class LoggingExtensions - { - public static IDisposable GetListeningEventArgsScope(this ILogger logger, ListeningChangeEventArgs e) => logger.BeginScope(new Dictionary<string, object>() { { "spotify_username", e.SpotifyUsername }, { "id", e.Id } }); - } -} - diff --git a/Selector/Extensions/ServiceExtensions.cs b/Selector/Extensions/ServiceExtensions.cs index 3fd3564..bf8a242 100644 --- a/Selector/Extensions/ServiceExtensions.cs +++ b/Selector/Extensions/ServiceExtensions.cs @@ -1,54 +1,11 @@ -using IF.Lastfm.Core.Api; -using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection; namespace Selector.Extensions { public static class ServiceExtensions { - public static IServiceCollection AddConsumerFactories(this IServiceCollection services) - { - services.AddTransient<IAudioFeatureInjectorFactory, AudioFeatureInjectorFactory>(); - services.AddTransient<AudioFeatureInjectorFactory>(); - - services.AddTransient<IPlayCounterFactory, PlayCounterFactory>(); - services.AddTransient<PlayCounterFactory>(); - - services.AddTransient<IWebHookFactory, WebHookFactory>(); - services.AddTransient<WebHookFactory>(); - - return services; - } - - public static IServiceCollection AddSpotify(this IServiceCollection services) - { - services.AddSingleton<IRefreshTokenFactoryProvider, RefreshTokenFactoryProvider>(); - services.AddSingleton<IRefreshTokenFactoryProvider, CachingRefreshTokenFactoryProvider>(); - - return services; - } - - public static IServiceCollection AddLastFm(this IServiceCollection services, string client, string secret) - { - var lastAuth = new LastAuth(client, secret); - services.AddSingleton(lastAuth); - services.AddTransient(sp => new LastfmClient(sp.GetService<LastAuth>())); - - services.AddTransient<ITrackApi>(sp => sp.GetService<LastfmClient>().Track); - services.AddTransient<IAlbumApi>(sp => sp.GetService<LastfmClient>().Album); - services.AddTransient<IArtistApi>(sp => sp.GetService<LastfmClient>().Artist); - - services.AddTransient<IUserApi>(sp => sp.GetService<LastfmClient>().User); - - services.AddTransient<IChartApi>(sp => sp.GetService<LastfmClient>().Chart); - services.AddTransient<ILibraryApi>(sp => sp.GetService<LastfmClient>().Library); - services.AddTransient<ITagApi>(sp => sp.GetService<LastfmClient>().Tag); - - return services; - } - public static IServiceCollection AddWatcher(this IServiceCollection services) { - services.AddSingleton<ISpotifyWatcherFactory, SpotifyWatcherFactory>(); services.AddSingleton<IWatcherCollectionFactory, WatcherCollectionFactory>(); return services; diff --git a/Selector/Listen/PlayDensity.cs b/Selector/Listen/PlayDensity.cs new file mode 100644 index 0000000..60515c2 --- /dev/null +++ b/Selector/Listen/PlayDensity.cs @@ -0,0 +1,31 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Selector +{ + public static class PlayDensity + { + public static decimal Density(this IEnumerable<IListen> scrobbles, TimeSpan window) => + scrobbles.Density(DateTime.UtcNow - window, DateTime.UtcNow); + + public static decimal Density(this IEnumerable<IListen> scrobbles, DateTime from, DateTime to) + { + var filteredScrobbles = scrobbles.Where(s => s.Timestamp > from && s.Timestamp < to); + + var dayDelta = (decimal)(to - from).Days; + + return filteredScrobbles.Count() / dayDelta; + } + + public static decimal Density(this IEnumerable<IListen> scrobbles) + { + var minDate = scrobbles.Select(s => s.Timestamp).Min(); + var maxDate = scrobbles.Select(s => s.Timestamp).Max(); + + var dayDelta = (decimal)(maxDate - minDate).Days; + + return scrobbles.Count() / dayDelta; + } + } +} \ No newline at end of file diff --git a/Selector/Scrobble/PlayDensity.cs b/Selector/Scrobble/PlayDensity.cs deleted file mode 100644 index 445af28..0000000 --- a/Selector/Scrobble/PlayDensity.cs +++ /dev/null @@ -1,31 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; - -namespace Selector -{ - public static class PlayDensity - { - public static decimal Density(this IEnumerable<IListen> scrobbles, TimeSpan window) => scrobbles.Density(DateTime.UtcNow - window, DateTime.UtcNow); - - public static decimal Density(this IEnumerable<IListen> scrobbles, DateTime from, DateTime to) - { - var filteredScrobbles = scrobbles.Where(s => s.Timestamp > from && s.Timestamp < to); - - var dayDelta = (decimal) (to - from).Days; - - return filteredScrobbles.Count() / dayDelta; - } - - public static decimal Density(this IEnumerable<IListen> scrobbles) - { - var minDate = scrobbles.Select(s => s.Timestamp).Min(); - var maxDate = scrobbles.Select(s => s.Timestamp).Max(); - - var dayDelta = (decimal) (maxDate - minDate).Days; - - return scrobbles.Count() / dayDelta; - } - } -} - diff --git a/Selector/Selector.csproj b/Selector/Selector.csproj index b3f51d9..28311e0 100644 --- a/Selector/Selector.csproj +++ b/Selector/Selector.csproj @@ -9,8 +9,6 @@ <ItemGroup> <PackageReference Include="Microsoft.Extensions.Logging" Version="9.0.0" /> - <PackageReference Include="SpotifyAPI.Web" Version="7.2.1" /> - <PackageReference Include="Inflatable.Lastfm" Version="1.2.0" /> <PackageReference Include="System.Linq.Async" Version="6.0.1" /> </ItemGroup> diff --git a/Selector/Spotify/Credentials.cs b/Selector/Spotify/Credentials.cs deleted file mode 100644 index 1be4ed6..0000000 --- a/Selector/Spotify/Credentials.cs +++ /dev/null @@ -1,14 +0,0 @@ -namespace Selector { - public class SpotifyAppCredentials { - public string ClientId { get; set; } - public string ClientSecret { get; set; } - - public SpotifyAppCredentials() { } - - public SpotifyAppCredentials(string clientId, string clientSecret) - { - ClientId = clientId; - ClientSecret = clientSecret; - } - } -} \ No newline at end of file diff --git a/Selector/Watcher/Collection/WatcherCollection.cs b/Selector/Watcher/Collection/WatcherCollection.cs index bbf2317..c6931f8 100644 --- a/Selector/Watcher/Collection/WatcherCollection.cs +++ b/Selector/Watcher/Collection/WatcherCollection.cs @@ -1,16 +1,15 @@ -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Logging.Abstractions; using System; using System.Collections; using System.Collections.Generic; using System.Linq; -using System.Text; using System.Threading; using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; namespace Selector { - public class WatcherCollection: IWatcherCollection, IDisposable, IEnumerable<WatcherContext> + public class WatcherCollection : IWatcherCollection, IDisposable, IEnumerable<WatcherContext> { private readonly ILogger<WatcherCollection> Logger; public bool IsRunning { get; private set; } = false; @@ -28,12 +27,12 @@ namespace Selector .SelectMany(w => w.Consumers) .Where(t => t is not null); - public IEnumerable<Task> Tasks + public IEnumerable<Task> Tasks => Watchers .Select(w => w.Task) .Where(t => t is not null); - public IEnumerable<CancellationTokenSource> TokenSources + public IEnumerable<CancellationTokenSource> TokenSources => Watchers .Select(w => w.TokenSource) .Where(t => t is not null); @@ -60,13 +59,14 @@ namespace Selector if (IsRunning) return; Logger.LogDebug("Starting {} watcher(s)", Count); - foreach(var watcher in Watchers) + foreach (var watcher in Watchers) { watcher.Start(); } + IsRunning = true; } - + public void Stop() { if (!IsRunning) return; @@ -78,6 +78,7 @@ namespace Selector { watcher.Stop(); } + Task.WaitAll(Tasks.ToArray()); IsRunning = false; } @@ -87,20 +88,21 @@ namespace Selector } catch (AggregateException ex) { - if(ex.InnerException is TaskCanceledException || ex.InnerExceptions.Any(e => e is TaskCanceledException)) + if (ex.InnerException is TaskCanceledException || + ex.InnerExceptions.Any(e => e is TaskCanceledException)) { Logger.LogTrace("Caught task cancelled exception"); } else { - throw ex; + throw; } } } public void Dispose() { - foreach(var watcher in Watchers) + foreach (var watcher in Watchers) { watcher.Dispose(); } @@ -109,4 +111,4 @@ namespace Selector public IEnumerator<WatcherContext> GetEnumerator() => Watchers.GetEnumerator(); IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); } -} +} \ No newline at end of file diff --git a/Selector/Watcher/Interfaces/ISpotifyPlayerWatcher.cs b/Selector/Watcher/Interfaces/ISpotifyPlayerWatcher.cs deleted file mode 100644 index 8614b5b..0000000 --- a/Selector/Watcher/Interfaces/ISpotifyPlayerWatcher.cs +++ /dev/null @@ -1,30 +0,0 @@ -using System; -using SpotifyAPI.Web; - -namespace Selector -{ - 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; - public event EventHandler<ListeningChangeEventArgs> ContextChange; - public event EventHandler<ListeningChangeEventArgs> ContentChange; - - public event EventHandler<ListeningChangeEventArgs> VolumeChange; - public event EventHandler<ListeningChangeEventArgs> DeviceChange; - public event EventHandler<ListeningChangeEventArgs> PlayingChange; - - /// <summary> - /// Last retrieved currently playing - /// </summary> - public CurrentlyPlayingContext Live { get; } - - public PlayerTimeline Past { get; } - } -} \ No newline at end of file