splitting spotify and last.fm into separate projects, adding apple listen database type
This commit is contained in:
parent
17b1f464dd
commit
df986e86ee
Selector.AppleMusic
Selector.CLI
Selector.Cache
Selector.Event
Selector.LastFm
Selector.MAUI
Selector.Model
ApplicationDbContext.cs
Extensions
Listen
Migrations
20250331203003_add_apple_listen.Designer.cs20250331203003_add_apple_listen.csApplicationDbContextModelSnapshot.cs
Scrobble
Selector.Model.csprojSelector.SignalR
Selector.Spotify
ConfigFactory
Consumer
Credentials.csCurrentlyPlayingDTO.csEquality
Events.csExtensions
FactoryProvider
JsonContext.csSelector.Spotify.csprojTimeline
Watcher
Selector.Tests
@ -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);
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
namespace Selector.AppleMusic.Watcher.Consumer;
|
||||
namespace Selector.AppleMusic.Consumer;
|
||||
|
||||
public interface IApplePlayerConsumer : IConsumer<AppleListeningChangeEventArgs>
|
||||
{
|
@ -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,
|
||||
|
@ -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
|
||||
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
{
|
||||
|
@ -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>()
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
|
@ -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:
|
||||
|
93
Selector.Cache/Consumer/AppleMusic/CacheWriterConsumer.cs
Normal file
93
Selector.Cache/Consumer/AppleMusic/CacheWriterConsumer.cs
Normal file
@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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>()
|
||||
));
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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());
|
||||
|
||||
|
@ -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");
|
||||
|
||||
|
@ -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[]
|
@ -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);
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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>
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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
|
||||
{
|
||||
|
@ -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);
|
||||
|
@ -1,4 +1,5 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Selector.Spotify;
|
||||
|
||||
namespace Selector.Events
|
||||
{
|
||||
|
@ -1,6 +1,7 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Selector.AppleMusic;
|
||||
using Selector.Model;
|
||||
using Selector.Spotify;
|
||||
|
||||
namespace Selector.Events
|
||||
{
|
||||
|
26
Selector.LastFm/Extensions/ServiceExtensions.cs
Normal file
26
Selector.LastFm/Extensions/ServiceExtensions.cs
Normal file
@ -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;
|
||||
}
|
||||
}
|
10
Selector/Scrobble/Mapping/ScrobbleAlbumMapping.cs → Selector.LastFm/Mapping/ScrobbleAlbumMapping.cs
10
Selector/Scrobble/Mapping/ScrobbleAlbumMapping.cs → 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
16
Selector/Scrobble/Mapping/ScrobbleTrackMapping.cs → Selector.LastFm/Mapping/ScrobbleTrackMapping.cs
16
Selector/Scrobble/Mapping/ScrobbleTrackMapping.cs → 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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}";
|
||||
}
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
20
Selector.LastFm/Selector.LastFm.csproj
Normal file
20
Selector.LastFm/Selector.LastFm.csproj
Normal file
@ -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>
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -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" />
|
||||
|
@ -1,5 +1,5 @@
|
||||
@using SpotifyAPI.Web;
|
||||
|
||||
@using Selector.Spotify.Consumer
|
||||
@using SpotifyAPI.Web
|
||||
@if (Count is not null) {
|
||||
|
||||
<div class="card info-card">
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
26
Selector.Model/Listen/AppleMusicListen.cs
Normal file
26
Selector.Model/Listen/AppleMusicListen.cs
Normal file
@ -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,
|
||||
};
|
||||
}
|
548
Selector.Model/Migrations/20250331203003_add_apple_listen.Designer.cs
generated
Normal file
548
Selector.Model/Migrations/20250331203003_add_apple_listen.Designer.cs
generated
Normal file
@ -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
|
||||
}
|
||||
}
|
||||
}
|
52
Selector.Model/Migrations/20250331203003_add_apple_listen.cs
Normal file
52
Selector.Model/Migrations/20250331203003_add_apple_listen.cs
Normal file
@ -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");
|
||||
}
|
||||
}
|
||||
}
|
@ -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")
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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>
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -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>
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
|
@ -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);
|
||||
|
@ -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
|
||||
{
|
@ -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
|
||||
{
|
6
Selector/Consumers/Factory/WebHookFactory.cs → Selector.Spotify/Consumer/Factory/WebHookFactory.cs
6
Selector/Consumers/Factory/WebHookFactory.cs → 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
|
||||
{
|
9
Selector.Spotify/Consumer/IConsumer.cs
Normal file
9
Selector.Spotify/Consumer/IConsumer.cs
Normal file
@ -0,0 +1,9 @@
|
||||
namespace Selector.Spotify.Consumer;
|
||||
|
||||
public interface ISpotifyPlayerConsumer : IConsumer<SpotifyListeningChangeEventArgs>
|
||||
{
|
||||
}
|
||||
|
||||
public interface IPlaylistConsumer : IConsumer<PlaylistChangeEventArgs>
|
||||
{
|
||||
}
|
@ -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
|
@ -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>()
|
||||
{
|
17
Selector.Spotify/Credentials.cs
Normal file
17
Selector.Spotify/Credentials.cs
Normal file
@ -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;
|
||||
}
|
||||
}
|
@ -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,
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
12
Selector.Spotify/Extensions/LoggingExtensions.cs
Normal file
12
Selector.Spotify/Extensions/LoggingExtensions.cs
Normal file
@ -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 } });
|
||||
}
|
||||
}
|
30
Selector.Spotify/Extensions/ServiceExtensions.cs
Normal file
30
Selector.Spotify/Extensions/ServiceExtensions.cs
Normal file
@ -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;
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
20
Selector.Spotify/Selector.Spotify.csproj
Normal file
20
Selector.Spotify/Selector.Spotify.csproj
Normal file
@ -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>
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
@ -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)
|
||||
{
|
@ -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
|
||||
{
|
@ -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; }
|
||||
}
|
||||
}
|
||||
}
|
30
Selector.Spotify/Watcher/Interfaces/ISpotifyPlayerWatcher.cs
Normal file
30
Selector.Spotify/Watcher/Interfaces/ISpotifyPlayerWatcher.cs
Normal file
@ -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; }
|
||||
}
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
using System.Threading.Tasks;
|
||||
using Selector.Spotify.ConfigFactory;
|
||||
|
||||
namespace Selector
|
||||
namespace Selector.Spotify
|
||||
{
|
||||
public interface ISpotifyWatcherFactory
|
||||
{
|
@ -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
|
||||
{
|
@ -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);
|
||||
}
|
@ -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
|
||||
{
|
@ -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>();
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user