apple watcher and timeline working

This commit is contained in:
Andy Pack 2025-03-30 21:50:28 +01:00
parent 6814ebd72c
commit 17b1f464dd
Signed by: sarsoo
GPG Key ID: A55BA3536A5E0ED7
71 changed files with 1877 additions and 719 deletions
Dockerfile.CLIDockerfile.Web
Selector.AppleMusic
Selector.CLI
Selector.Cache
Selector.Event
Selector.MAUI
Selector.Model
Selector.Tests
Selector.Web
Hubs
Services/EventMappings
Selector

@ -2,6 +2,7 @@ FROM mcr.microsoft.com/dotnet/sdk:9.0 AS base
COPY *.sln . COPY *.sln .
COPY Selector/*.csproj ./Selector/ COPY Selector/*.csproj ./Selector/
COPY Selector.AppleMusic/*.csproj ./Selector.AppleMusic/
COPY Selector.Cache/*.csproj ./Selector.Cache/ COPY Selector.Cache/*.csproj ./Selector.Cache/
COPY Selector.Data/*.csproj ./Selector.Data/ COPY Selector.Data/*.csproj ./Selector.Data/
COPY Selector.Event/*.csproj ./Selector.Event/ COPY Selector.Event/*.csproj ./Selector.Event/

@ -10,6 +10,7 @@ FROM mcr.microsoft.com/dotnet/sdk:9.0 AS base
COPY *.sln . COPY *.sln .
COPY Selector/*.csproj ./Selector/ COPY Selector/*.csproj ./Selector/
COPY Selector.AppleMusic/*.csproj ./Selector.AppleMusic/
COPY Selector.Cache/*.csproj ./Selector.Cache/ COPY Selector.Cache/*.csproj ./Selector.Cache/
COPY Selector.Data/*.csproj ./Selector.Data/ COPY Selector.Data/*.csproj ./Selector.Data/
COPY Selector.Event/*.csproj ./Selector.Event/ COPY Selector.Event/*.csproj ./Selector.Event/

@ -1,8 +1,14 @@
namespace Selector.AppleMusic; using System.Net;
using System.Net.Http.Json;
using Selector.AppleMusic.Exceptions;
using Selector.AppleMusic.Model;
namespace Selector.AppleMusic;
public class AppleMusicApi(HttpClient client, string developerToken, string userToken) public class AppleMusicApi(HttpClient client, string developerToken, string userToken)
{ {
private static readonly string _apiBaseUrl = "https://api.music.apple.com/v1"; private static readonly string _apiBaseUrl = "https://api.music.apple.com/v1";
private readonly AppleJsonContext _appleJsonContext = AppleJsonContext.Default;
private async Task<HttpResponseMessage> MakeRequest(HttpMethod httpMethod, string requestUri) private async Task<HttpResponseMessage> MakeRequest(HttpMethod httpMethod, string requestUri)
{ {
@ -14,8 +20,32 @@ public class AppleMusicApi(HttpClient client, string developerToken, string user
return response; return response;
} }
public async Task GetRecentlyPlayedTracks() private void CheckResponse(HttpResponseMessage response)
{ {
var response = await MakeRequest(HttpMethod.Get, "/me/recent/played/tracks"); if (!response.IsSuccessStatusCode)
{
if (response.StatusCode == HttpStatusCode.Unauthorized)
{
throw new UnauthorisedException();
}
else if (response.StatusCode == HttpStatusCode.Forbidden)
{
throw new ForbiddenException();
}
else if (response.StatusCode == HttpStatusCode.TooManyRequests)
{
throw new RateLimitException();
}
}
}
public async Task<RecentlyPlayedTracksResponse> GetRecentlyPlayedTracks()
{
var response = await MakeRequest(HttpMethod.Get, "/me/recent/played/tracks?types=songs");
CheckResponse(response);
var parsed = await response.Content.ReadFromJsonAsync(_appleJsonContext.RecentlyPlayedTracksResponse);
return parsed;
} }
} }

@ -0,0 +1,94 @@
using Selector.AppleMusic.Model;
using Selector.AppleMusic.Watcher;
namespace Selector.AppleMusic;
public class AppleTimeline : Timeline<AppleMusicCurrentlyPlayingContext>
{
public List<AppleMusicCurrentlyPlayingContext> Add(IEnumerable<Track> tracks)
=> Add(tracks
.Select(x => new AppleMusicCurrentlyPlayingContext()
{
Track = x,
FirstSeen = DateTime.UtcNow,
}).ToList());
public List<AppleMusicCurrentlyPlayingContext> Add(List<AppleMusicCurrentlyPlayingContext> items)
{
var newItems = new List<AppleMusicCurrentlyPlayingContext>();
if (items == null || !items.Any())
{
return newItems;
}
if (!Recent.Any())
{
Recent.AddRange(items.Select(x =>
TimelineItem<AppleMusicCurrentlyPlayingContext>.From(x, DateTime.UtcNow)));
return newItems;
}
if (Recent
.TakeLast(items.Count)
.Select(x => x.Item)
.SequenceEqual(items, new AppleMusicCurrentlyPlayingContextComparer()))
{
return newItems;
}
var stop = false;
var found = 0;
var startIdx = 0;
while (!stop)
{
for (var i = 0; i < items.Count; i++)
{
var storedIdx = (Recent.Count - 1) - i;
// start from the end, minus this loops index, minus the offset
var pulledIdx = (items.Count - 1) - i - startIdx;
if (pulledIdx < 0)
{
// ran to the end of new items and none matched the end, add all the new ones
stop = true;
break;
}
if (storedIdx < 0)
{
// all the new stuff matches, we're done and there's nothing new to add
stop = true;
break;
}
if (Recent[storedIdx].Item.Track.Id == items[pulledIdx].Track.Id)
{
// good, keep going
found++;
if (found >= 3)
{
stop = true;
break;
}
}
else
{
// bad, doesn't match, break and bump stored
found = 0;
break;
}
}
if (!stop) startIdx += 1;
}
foreach (var item in items.TakeLast(startIdx))
{
newItems.Add(item);
Recent.Add(TimelineItem<AppleMusicCurrentlyPlayingContext>.From(item, DateTime.UtcNow));
}
return newItems;
}
}

@ -0,0 +1,28 @@
using Selector.AppleMusic.Watcher;
namespace Selector.AppleMusic;
public class AppleListeningChangeEventArgs : EventArgs
{
public AppleMusicCurrentlyPlayingContext Previous { get; set; }
public AppleMusicCurrentlyPlayingContext Current { get; set; }
/// <summary>
/// String Id for watcher, used to hold user Db Id
/// </summary>
/// <value></value>
public string Id { get; set; }
// AppleTimeline Timeline { get; set; }
public static AppleListeningChangeEventArgs From(AppleMusicCurrentlyPlayingContext previous,
AppleMusicCurrentlyPlayingContext current, AppleTimeline timeline, string id = null, string username = null)
{
return new AppleListeningChangeEventArgs()
{
Previous = previous,
Current = current,
// Timeline = timeline,
Id = id
};
}
}

@ -0,0 +1,5 @@
namespace Selector.AppleMusic.Exceptions;
public class AppleMusicException : Exception
{
}

@ -0,0 +1,5 @@
namespace Selector.AppleMusic.Exceptions;
public class ForbiddenException : AppleMusicException
{
}

@ -0,0 +1,5 @@
namespace Selector.AppleMusic.Exceptions;
public class RateLimitException : AppleMusicException
{
}

@ -0,0 +1,5 @@
namespace Selector.AppleMusic.Exceptions;
public class ServiceException : AppleMusicException
{
}

@ -0,0 +1,5 @@
namespace Selector.AppleMusic.Exceptions;
public class UnauthorisedException : AppleMusicException
{
}

@ -1,4 +1,5 @@
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Selector.AppleMusic.Watcher;
namespace Selector.AppleMusic.Extensions; namespace Selector.AppleMusic.Extensions;
@ -6,7 +7,8 @@ public static class ServiceExtensions
{ {
public static IServiceCollection AddAppleMusic(this IServiceCollection services) public static IServiceCollection AddAppleMusic(this IServiceCollection services)
{ {
services.AddSingleton<AppleMusicApiProvider>(); services.AddSingleton<AppleMusicApiProvider>()
.AddTransient<IAppleMusicWatcherFactory, AppleMusicWatcherFactory>();
return services; return services;
} }

@ -0,0 +1,14 @@
using System.Text.Json.Serialization;
using Selector.AppleMusic.Model;
namespace Selector.AppleMusic;
[JsonSerializable(typeof(RecentlyPlayedTracksResponse))]
[JsonSerializable(typeof(TrackAttributes))]
[JsonSerializable(typeof(PlayParams))]
[JsonSerializable(typeof(Track))]
[JsonSerializable(typeof(AppleListeningChangeEventArgs))]
[JsonSourceGenerationOptions(PropertyNameCaseInsensitive = true)]
public partial class AppleJsonContext : JsonSerializerContext
{
}

@ -0,0 +1,6 @@
namespace Selector.AppleMusic.Model;
public class RecentlyPlayedTracksResponse
{
public List<Track> Data { get; set; }
}

@ -0,0 +1,44 @@
namespace Selector.AppleMusic.Model;
public class TrackAttributes
{
public string AlbumName { get; set; }
public List<string> GenreNames { get; set; }
public int TrackNumber { get; set; }
public int DurationInMillis { get; set; }
public DateTime ReleaseDate { get; set; }
public string Isrc { get; set; }
//TODO: Artwork
public string ComposerName { get; set; }
public string Url { get; set; }
public PlayParams PlayParams { get; set; }
public int DiscNumber { get; set; }
public bool HasLyrics { get; set; }
public bool IsAppleDigitalMaster { get; set; }
public string Name { get; set; }
//TODO: previews
public string ArtistName { get; set; }
}
public class PlayParams
{
public string Id { get; set; }
public string Kind { get; set; }
}
public class Track
{
public string Id { get; set; }
public string Type { get; set; }
public string Href { get; set; }
public TrackAttributes Attributes { get; set; }
public override string ToString()
{
return $"{Attributes?.Name} / {Attributes?.AlbumName} / {Attributes?.ArtistName}";
}
}

@ -11,4 +11,8 @@
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.0.1" /> <PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.0.1" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Selector\Selector.csproj"/>
</ItemGroup>
</Project> </Project>

@ -0,0 +1,5 @@
namespace Selector.AppleMusic.Watcher.Consumer;
public interface IApplePlayerConsumer : IConsumer<AppleListeningChangeEventArgs>
{
}

@ -0,0 +1,26 @@
using Selector.AppleMusic.Model;
namespace Selector.AppleMusic.Watcher;
public class AppleMusicCurrentlyPlayingContext
{
public DateTime FirstSeen { get; set; }
public Track Track { get; set; }
}
public class AppleMusicCurrentlyPlayingContextComparer : IEqualityComparer<AppleMusicCurrentlyPlayingContext>
{
public bool Equals(AppleMusicCurrentlyPlayingContext? x, AppleMusicCurrentlyPlayingContext? y)
{
if (ReferenceEquals(x, y)) return true;
if (x is null) return false;
if (y is null) return false;
if (x.GetType() != y.GetType()) return false;
return x.Track.Id.Equals(y.Track.Id);
}
public int GetHashCode(AppleMusicCurrentlyPlayingContext obj)
{
return obj.Track.GetHashCode();
}
}

@ -0,0 +1,20 @@
using Selector.AppleMusic;
using Selector.AppleMusic.Watcher;
namespace Selector
{
public interface IAppleMusicPlayerWatcher : IWatcher
{
public event EventHandler<AppleListeningChangeEventArgs> NetworkPoll;
public event EventHandler<AppleListeningChangeEventArgs> ItemChange;
public event EventHandler<AppleListeningChangeEventArgs> AlbumChange;
public event EventHandler<AppleListeningChangeEventArgs> ArtistChange;
/// <summary>
/// Last retrieved currently playing
/// </summary>
public AppleMusicCurrentlyPlayingContext Live { get; }
public AppleTimeline Past { get; }
}
}

@ -0,0 +1,145 @@
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Selector.AppleMusic.Exceptions;
using Selector.AppleMusic.Model;
namespace Selector.AppleMusic.Watcher;
public class AppleMusicPlayerWatcher : BaseWatcher, IAppleMusicPlayerWatcher
{
new protected readonly ILogger<AppleMusicPlayerWatcher> Logger;
private readonly AppleMusicApi _appleMusicApi;
public event EventHandler<AppleListeningChangeEventArgs> NetworkPoll;
public event EventHandler<AppleListeningChangeEventArgs> ItemChange;
public event EventHandler<AppleListeningChangeEventArgs> AlbumChange;
public event EventHandler<AppleListeningChangeEventArgs> ArtistChange;
public AppleMusicCurrentlyPlayingContext Live { get; protected set; }
protected AppleMusicCurrentlyPlayingContext Previous { get; set; }
public AppleTimeline Past { get; set; } = new();
public AppleMusicPlayerWatcher(AppleMusicApi appleMusicClient,
ILogger<AppleMusicPlayerWatcher> logger = null,
int pollPeriod = 3000
) : base(logger)
{
_appleMusicApi = appleMusicClient;
Logger = logger ?? NullLogger<AppleMusicPlayerWatcher>.Instance;
PollPeriod = pollPeriod;
}
public override async Task WatchOne(CancellationToken token)
{
token.ThrowIfCancellationRequested();
try
{
Logger.LogTrace("Making Apple Music call");
var polledCurrent = await _appleMusicApi.GetRecentlyPlayedTracks();
// using var polledLogScope = Logger.BeginScope(new Dictionary<string, object>() { { "context", polledCurrent?.DisplayString() } });
Logger.LogTrace("Received Apple Music call");
var currentPrevious = Previous;
var reversedItems = polledCurrent.Data.ToList();
reversedItems.Reverse();
var addedItems = Past.Add(reversedItems);
// swap new item into live and bump existing down to previous
Previous = Live;
SetLive(polledCurrent);
OnNetworkPoll(GetEvent());
if (currentPrevious != null && addedItems.Any())
{
addedItems.Insert(0, currentPrevious);
foreach (var (first, second) in addedItems.Zip(addedItems.Skip(1)))
{
Logger.LogDebug("Track changed: {prevTrack} -> {currentTrack}", first.Track, second.Track);
OnItemChange(AppleListeningChangeEventArgs.From(first, second, Past, id: Id));
}
}
}
catch (RateLimitException e)
{
Logger.LogDebug("Rate Limit exception: [{message}]", e.Message);
// throw e;
}
catch (ForbiddenException e)
{
Logger.LogDebug("Forbidden exception: [{message}]", e.Message);
throw;
}
catch (ServiceException e)
{
Logger.LogDebug("Apple Music internal error: [{message}]", e.Message);
// throw e;
}
catch (UnauthorisedException e)
{
Logger.LogDebug("Unauthorised exception: [{message}]", e.Message);
// throw e;
}
}
private void SetLive(RecentlyPlayedTracksResponse recentlyPlayedTracks)
{
var lastTrack = recentlyPlayedTracks.Data?.FirstOrDefault();
if (Live != null && Live.Track != null && Live.Track.Id == lastTrack?.Id)
{
Live = new()
{
Track = Live.Track,
FirstSeen = Live.FirstSeen,
};
}
else
{
Live = new()
{
Track = recentlyPlayedTracks.Data?.FirstOrDefault(),
FirstSeen = DateTime.UtcNow,
};
}
}
public override Task Reset()
{
Previous = null;
Live = null;
Past = new();
return Task.CompletedTask;
}
protected AppleListeningChangeEventArgs GetEvent() =>
AppleListeningChangeEventArgs.From(Previous, Live, Past, id: Id);
#region Event Firers
protected virtual void OnNetworkPoll(AppleListeningChangeEventArgs args)
{
NetworkPoll?.Invoke(this, args);
}
protected virtual void OnItemChange(AppleListeningChangeEventArgs args)
{
ItemChange?.Invoke(this, args);
}
protected virtual void OnAlbumChange(AppleListeningChangeEventArgs args)
{
AlbumChange?.Invoke(this, args);
}
protected virtual void OnArtistChange(AppleListeningChangeEventArgs args)
{
ArtistChange?.Invoke(this, args);
}
#endregion
}

@ -0,0 +1,59 @@
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
namespace Selector.AppleMusic.Watcher
{
public interface IAppleMusicWatcherFactory
{
Task<IWatcher> Get<T>(AppleMusicApiProvider appleMusicProvider, string developerToken, string teamId,
string keyId, string userToken, int pollPeriod = 3000)
where T : class, IWatcher;
}
public class AppleMusicWatcherFactory : IAppleMusicWatcherFactory
{
private readonly ILoggerFactory LoggerFactory;
private readonly IEqual Equal;
public AppleMusicWatcherFactory(ILoggerFactory loggerFactory, IEqual equal)
{
LoggerFactory = loggerFactory;
Equal = equal;
}
public async Task<IWatcher> Get<T>(AppleMusicApiProvider appleMusicProvider, string developerToken,
string teamId, string keyId, string userToken, int pollPeriod = 3000)
where T : class, IWatcher
{
if (typeof(T).IsAssignableFrom(typeof(AppleMusicPlayerWatcher)))
{
if (!Magic.Dummy)
{
var api = appleMusicProvider.GetApi(developerToken, teamId, keyId, userToken);
return new AppleMusicPlayerWatcher(
api,
LoggerFactory?.CreateLogger<AppleMusicPlayerWatcher>() ??
NullLogger<AppleMusicPlayerWatcher>.Instance,
pollPeriod: pollPeriod
);
}
else
{
return new DummySpotifyPlayerWatcher(
Equal,
LoggerFactory?.CreateLogger<DummySpotifyPlayerWatcher>() ??
NullLogger<DummySpotifyPlayerWatcher>.Instance,
pollPeriod: pollPeriod
)
{
};
}
}
else
{
throw new ArgumentException("Type unsupported");
}
}
}
}

@ -1,33 +1,33 @@
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Selector.Model;
namespace Selector.CLI.Consumer namespace Selector.CLI.Consumer
{ {
public interface IMappingPersisterFactory public interface IMappingPersisterFactory
{ {
public Task<IPlayerConsumer> Get(IPlayerWatcher watcher = null); public Task<ISpotifyPlayerConsumer> Get(ISpotifyPlayerWatcher watcher = null);
} }
public class MappingPersisterFactory : IMappingPersisterFactory public class MappingPersisterFactory : IMappingPersisterFactory
{ {
private readonly ILoggerFactory LoggerFactory; private readonly ILoggerFactory LoggerFactory;
private readonly IServiceScopeFactory ScopeFactory; private readonly IServiceScopeFactory ScopeFactory;
public MappingPersisterFactory(ILoggerFactory loggerFactory, IServiceScopeFactory scopeFactory = null, LastFmCredentials creds = null) public MappingPersisterFactory(ILoggerFactory loggerFactory, IServiceScopeFactory scopeFactory = null,
LastFmCredentials creds = null)
{ {
LoggerFactory = loggerFactory; LoggerFactory = loggerFactory;
ScopeFactory = scopeFactory; ScopeFactory = scopeFactory;
} }
public Task<IPlayerConsumer> Get(IPlayerWatcher watcher = null) public Task<ISpotifyPlayerConsumer> Get(ISpotifyPlayerWatcher watcher = null)
{ {
return Task.FromResult<IPlayerConsumer>(new MappingPersister( return Task.FromResult<ISpotifyPlayerConsumer>(new MappingPersister(
watcher, watcher,
ScopeFactory, ScopeFactory,
LoggerFactory.CreateLogger<MappingPersister>() LoggerFactory.CreateLogger<MappingPersister>()
)); ));
} }
} }
} }

@ -15,16 +15,16 @@ namespace Selector.CLI.Consumer
/// <summary> /// <summary>
/// Save name -> Spotify URI mappings as new objects come through the watcher without making extra queries of the Spotify API /// Save name -> Spotify URI mappings as new objects come through the watcher without making extra queries of the Spotify API
/// </summary> /// </summary>
public class MappingPersister: IPlayerConsumer public class MappingPersister : ISpotifyPlayerConsumer
{ {
protected readonly IPlayerWatcher Watcher; protected readonly ISpotifyPlayerWatcher Watcher;
protected readonly IServiceScopeFactory ScopeFactory; protected readonly IServiceScopeFactory ScopeFactory;
protected readonly ILogger<MappingPersister> Logger; protected readonly ILogger<MappingPersister> Logger;
public CancellationToken CancelToken { get; set; } public CancellationToken CancelToken { get; set; }
public MappingPersister( public MappingPersister(
IPlayerWatcher watcher, ISpotifyPlayerWatcher watcher,
IServiceScopeFactory scopeFactory, IServiceScopeFactory scopeFactory,
ILogger<MappingPersister> logger = null, ILogger<MappingPersister> logger = null,
CancellationToken token = default CancellationToken token = default
@ -40,7 +40,8 @@ namespace Selector.CLI.Consumer
{ {
if (e.Current is null) return; if (e.Current is null) return;
Task.Run(async () => { Task.Run(async () =>
{
try try
{ {
await AsyncCallback(e); await AsyncCallback(e);
@ -59,13 +60,14 @@ namespace Selector.CLI.Consumer
public async Task AsyncCallback(ListeningChangeEventArgs e) public async Task AsyncCallback(ListeningChangeEventArgs e)
{ {
using var serviceScope = ScopeFactory.CreateScope(); using var serviceScope = ScopeFactory.CreateScope();
using var scope = Logger.BeginScope(new Dictionary<string, object>() { { "spotify_username", e.SpotifyUsername }, { "id", e.Id } }); using var scope = Logger.BeginScope(new Dictionary<string, object>()
{ { "spotify_username", e.SpotifyUsername }, { "id", e.Id } });
if (e.Current.Item is FullTrack track) if (e.Current.Item is FullTrack track)
{ {
var mappingRepo = serviceScope.ServiceProvider.GetRequiredService<IScrobbleMappingRepository>(); var mappingRepo = serviceScope.ServiceProvider.GetRequiredService<IScrobbleMappingRepository>();
if(!mappingRepo.GetTracks().Select(t => t.SpotifyUri).Contains(track.Uri)) if (!mappingRepo.GetTracks().Select(t => t.SpotifyUri).Contains(track.Uri))
{ {
mappingRepo.Add(new TrackLastfmSpotifyMapping() mappingRepo.Add(new TrackLastfmSpotifyMapping()
{ {
@ -120,7 +122,7 @@ namespace Selector.CLI.Consumer
{ {
var watcher = watch ?? Watcher ?? throw new ArgumentNullException(nameof(watch)); var watcher = watch ?? Watcher ?? throw new ArgumentNullException(nameof(watch));
if (watcher is IPlayerWatcher watcherCast) if (watcher is ISpotifyPlayerWatcher watcherCast)
{ {
watcherCast.ItemChange += Callback; watcherCast.ItemChange += Callback;
} }
@ -134,7 +136,7 @@ namespace Selector.CLI.Consumer
{ {
var watcher = watch ?? Watcher ?? throw new ArgumentNullException(nameof(watch)); var watcher = watch ?? Watcher ?? throw new ArgumentNullException(nameof(watch));
if (watcher is IPlayerWatcher watcherCast) if (watcher is ISpotifyPlayerWatcher watcherCast)
{ {
watcherCast.ItemChange -= Callback; watcherCast.ItemChange -= Callback;
} }
@ -144,5 +146,4 @@ namespace Selector.CLI.Consumer
} }
} }
} }
} }

@ -5,16 +5,18 @@ using Microsoft.Extensions.DependencyInjection;
namespace Selector.CLI namespace Selector.CLI
{ {
static class OptionsHelper { static class OptionsHelper
{
public static void ConfigureOptions(RootOptions options, IConfiguration config) public static void ConfigureOptions(RootOptions options, IConfiguration config)
{ {
config.GetSection(RootOptions.Key).Bind(options); config.GetSection(RootOptions.Key).Bind(options);
config.GetSection(FormatKeys( new[] { RootOptions.Key, WatcherOptions.Key})).Bind(options.WatcherOptions); config.GetSection(FormatKeys(new[] { RootOptions.Key, WatcherOptions.Key })).Bind(options.WatcherOptions);
config.GetSection(FormatKeys( new[] { RootOptions.Key, DatabaseOptions.Key})).Bind(options.DatabaseOptions); config.GetSection(FormatKeys(new[] { RootOptions.Key, DatabaseOptions.Key })).Bind(options.DatabaseOptions);
config.GetSection(FormatKeys( new[] { RootOptions.Key, RedisOptions.Key})).Bind(options.RedisOptions); config.GetSection(FormatKeys(new[] { RootOptions.Key, RedisOptions.Key })).Bind(options.RedisOptions);
config.GetSection(FormatKeys( new[] { RootOptions.Key, JobsOptions.Key})).Bind(options.JobOptions); config.GetSection(FormatKeys(new[] { RootOptions.Key, JobsOptions.Key })).Bind(options.JobOptions);
config.GetSection(FormatKeys( new[] { RootOptions.Key, JobsOptions.Key, ScrobbleWatcherJobOptions.Key })).Bind(options.JobOptions.Scrobble); config.GetSection(FormatKeys(new[] { RootOptions.Key, JobsOptions.Key, ScrobbleWatcherJobOptions.Key }))
} .Bind(options.JobOptions.Scrobble);
}
public static RootOptions ConfigureOptions(this IConfiguration config) public static RootOptions ConfigureOptions(this IConfiguration config)
{ {
@ -29,12 +31,16 @@ namespace Selector.CLI
{ {
var options = config.GetSection(RootOptions.Key).Get<RootOptions>(); var options = config.GetSection(RootOptions.Key).Get<RootOptions>();
services.Configure<DatabaseOptions>(config.GetSection(FormatKeys(new[] { RootOptions.Key, DatabaseOptions.Key }))); services.Configure<DatabaseOptions>(config.GetSection(FormatKeys(new[]
services.Configure<RedisOptions>(config.GetSection(FormatKeys(new[] { RootOptions.Key, RedisOptions.Key }))); { RootOptions.Key, DatabaseOptions.Key })));
services.Configure<WatcherOptions>(config.GetSection(FormatKeys(new[] { RootOptions.Key, WatcherOptions.Key }))); services.Configure<RedisOptions>(config.GetSection(FormatKeys(new[]
{ RootOptions.Key, RedisOptions.Key })));
services.Configure<WatcherOptions>(config.GetSection(FormatKeys(new[]
{ RootOptions.Key, WatcherOptions.Key })));
services.Configure<JobsOptions>(config.GetSection(FormatKeys(new[] { RootOptions.Key, JobsOptions.Key }))); services.Configure<JobsOptions>(config.GetSection(FormatKeys(new[] { RootOptions.Key, JobsOptions.Key })));
services.Configure<ScrobbleWatcherJobOptions>(config.GetSection(FormatKeys(new[] { RootOptions.Key, JobsOptions.Key, ScrobbleWatcherJobOptions.Key }))); services.Configure<ScrobbleWatcherJobOptions>(config.GetSection(FormatKeys(new[]
{ RootOptions.Key, JobsOptions.Key, ScrobbleWatcherJobOptions.Key })));
services.Configure<AppleMusicOptions>(config.GetSection(AppleMusicOptions._Key)); services.Configure<AppleMusicOptions>(config.GetSection(AppleMusicOptions._Key));
return options; return options;
@ -51,14 +57,17 @@ namespace Selector.CLI
/// Spotify client ID /// Spotify client ID
/// </summary> /// </summary>
public string ClientId { get; set; } public string ClientId { get; set; }
/// <summary> /// <summary>
/// Spotify app secret /// Spotify app secret
/// </summary> /// </summary>
public string ClientSecret { get; set; } public string ClientSecret { get; set; }
/// <summary> /// <summary>
/// Service account refresh token for tool spotify usage /// Service account refresh token for tool spotify usage
/// </summary> /// </summary>
public string RefreshToken { get; set; } public string RefreshToken { get; set; }
public string LastfmClient { get; set; } public string LastfmClient { get; set; }
public string LastfmSecret { get; set; } public string LastfmSecret { get; set; }
public WatcherOptions WatcherOptions { get; set; } = new(); public WatcherOptions WatcherOptions { get; set; } = new();
@ -70,7 +79,8 @@ namespace Selector.CLI
public enum EqualityChecker public enum EqualityChecker
{ {
Uri, String Uri,
String
} }
public class WatcherOptions public class WatcherOptions
@ -89,9 +99,10 @@ namespace Selector.CLI
public string Name { get; set; } public string Name { get; set; }
public string AccessKey { get; set; } public string AccessKey { get; set; }
public string RefreshKey { get; set; } public string RefreshKey { get; set; }
public string AppleUserToken { get; set; }
public string LastFmUsername { get; set; } public string LastFmUsername { get; set; }
public int PollPeriod { get; set; } = 5000; public int PollPeriod { get; set; } = 5000;
public WatcherType Type { get; set; } = WatcherType.Player; public WatcherType Type { get; set; } = WatcherType.SpotifyPlayer;
public List<Consumers> Consumers { get; set; } = default; public List<Consumers> Consumers { get; set; } = default;
#nullable enable #nullable enable
public string? PlaylistUri { get; set; } public string? PlaylistUri { get; set; }
@ -101,7 +112,12 @@ namespace Selector.CLI
public enum Consumers public enum Consumers
{ {
AudioFeatures, AudioFeaturesCache, CacheWriter, Publisher, PlayCounter, MappingPersister AudioFeatures,
AudioFeaturesCache,
CacheWriter,
Publisher,
PlayCounter,
MappingPersister
} }
public class RedisOptions public class RedisOptions
@ -143,4 +159,4 @@ namespace Selector.CLI
public string KeyId { get; set; } public string KeyId { get; set; }
public TimeSpan? Expiry { get; set; } = null; public TimeSpan? Expiry { get; set; } = null;
} }
} }

@ -59,4 +59,12 @@
<Folder Include="Consumer\" /> <Folder Include="Consumer\" />
<Folder Include="Consumer\Factory\" /> <Folder Include="Consumer\Factory\" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<Content Update="appsettings.Development.json">
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
</Content>
</ItemGroup>
</Project> </Project>

@ -1,4 +1,5 @@
using System; using System;
using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading; using System.Threading;
@ -7,13 +8,14 @@ using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Selector.AppleMusic;
using Selector.AppleMusic.Watcher;
using Selector.Cache; using Selector.Cache;
using Selector.CLI.Consumer;
using Selector.Events;
using Selector.Model; using Selector.Model;
using Selector.Model.Extensions; using Selector.Model.Extensions;
using Selector.Events;
using System.Collections.Concurrent;
using Selector.CLI.Consumer;
namespace Selector.CLI namespace Selector.CLI
{ {
@ -22,13 +24,16 @@ namespace Selector.CLI
private const int PollPeriod = 1000; private const int PollPeriod = 1000;
private readonly ILogger<DbWatcherService> Logger; private readonly ILogger<DbWatcherService> Logger;
private readonly IOptions<AppleMusicOptions> _appleMusicOptions;
private readonly IServiceProvider ServiceProvider; private readonly IServiceProvider ServiceProvider;
private readonly UserEventBus UserEventBus; private readonly UserEventBus UserEventBus;
private readonly IWatcherFactory WatcherFactory; private readonly ISpotifyWatcherFactory _spotifyWatcherFactory;
private readonly IAppleMusicWatcherFactory _appleWatcherFactory;
private readonly IWatcherCollectionFactory WatcherCollectionFactory; private readonly IWatcherCollectionFactory WatcherCollectionFactory;
private readonly IRefreshTokenFactoryProvider SpotifyFactory; private readonly IRefreshTokenFactoryProvider SpotifyFactory;
private readonly AppleMusicApiProvider _appleMusicProvider;
private readonly IAudioFeatureInjectorFactory AudioFeatureInjectorFactory; private readonly IAudioFeatureInjectorFactory AudioFeatureInjectorFactory;
private readonly IPlayCounterFactory PlayCounterFactory; private readonly IPlayCounterFactory PlayCounterFactory;
@ -42,23 +47,20 @@ namespace Selector.CLI
private ConcurrentDictionary<string, IWatcherCollection> Watchers { get; set; } = new(); private ConcurrentDictionary<string, IWatcherCollection> Watchers { get; set; } = new();
public DbWatcherService( public DbWatcherService(
IWatcherFactory watcherFactory, ISpotifyWatcherFactory spotifyWatcherFactory,
IAppleMusicWatcherFactory appleWatcherFactory,
IWatcherCollectionFactory watcherCollectionFactory, IWatcherCollectionFactory watcherCollectionFactory,
IRefreshTokenFactoryProvider spotifyFactory, IRefreshTokenFactoryProvider spotifyFactory,
AppleMusicApiProvider appleMusicProvider,
IAudioFeatureInjectorFactory audioFeatureInjectorFactory, IAudioFeatureInjectorFactory audioFeatureInjectorFactory,
IPlayCounterFactory playCounterFactory, IPlayCounterFactory playCounterFactory,
UserEventBus userEventBus, UserEventBus userEventBus,
ILogger<DbWatcherService> logger, ILogger<DbWatcherService> logger,
IOptions<AppleMusicOptions> appleMusicOptions,
IServiceProvider serviceProvider, IServiceProvider serviceProvider,
IPublisherFactory publisherFactory = null, IPublisherFactory publisherFactory = null,
ICacheWriterFactory cacheWriterFactory = null, ICacheWriterFactory cacheWriterFactory = null,
IMappingPersisterFactory mappingPersisterFactory = null, IMappingPersisterFactory mappingPersisterFactory = null,
IUserEventFirerFactory userEventFirerFactory = null IUserEventFirerFactory userEventFirerFactory = null
) )
{ {
@ -66,10 +68,13 @@ namespace Selector.CLI
ServiceProvider = serviceProvider; ServiceProvider = serviceProvider;
UserEventBus = userEventBus; UserEventBus = userEventBus;
WatcherFactory = watcherFactory; _spotifyWatcherFactory = spotifyWatcherFactory;
_appleWatcherFactory = appleWatcherFactory;
_appleMusicOptions = appleMusicOptions;
WatcherCollectionFactory = watcherCollectionFactory; WatcherCollectionFactory = watcherCollectionFactory;
SpotifyFactory = spotifyFactory; SpotifyFactory = spotifyFactory;
_appleMusicProvider = appleMusicProvider;
AudioFeatureInjectorFactory = audioFeatureInjectorFactory; AudioFeatureInjectorFactory = audioFeatureInjectorFactory;
PlayCounterFactory = playCounterFactory; PlayCounterFactory = playCounterFactory;
@ -100,8 +105,8 @@ namespace Selector.CLI
var indices = new HashSet<string>(); var indices = new HashSet<string>();
foreach (var dbWatcher in db.Watcher foreach (var dbWatcher in db.Watcher
.Include(w => w.User) .Include(w => w.User)
.Where(w => !string.IsNullOrWhiteSpace(w.User.SpotifyRefreshToken))) .Where(w => !string.IsNullOrWhiteSpace(w.User.SpotifyRefreshToken)))
{ {
var watcherCollectionIdx = dbWatcher.UserId; var watcherCollectionIdx = dbWatcher.UserId;
indices.Add(watcherCollectionIdx); indices.Add(watcherCollectionIdx);
@ -131,31 +136,43 @@ namespace Selector.CLI
switch (dbWatcher.Type) switch (dbWatcher.Type)
{ {
case WatcherType.Player: case WatcherType.SpotifyPlayer:
watcher = await WatcherFactory.Get<PlayerWatcher>(spotifyFactory, id: dbWatcher.UserId, pollPeriod: PollPeriod); watcher = await _spotifyWatcherFactory.Get<SpotifyPlayerWatcher>(spotifyFactory,
id: dbWatcher.UserId, pollPeriod: PollPeriod);
consumers.Add(await AudioFeatureInjectorFactory.Get(spotifyFactory)); 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.Get());
if (PublisherFactory is not null) consumers.Add(await PublisherFactory.Get()); if (PublisherFactory is not null) consumers.Add(await PublisherFactory.GetSpotify());
if (MappingPersisterFactory is not null && !Magic.Dummy) consumers.Add(await MappingPersisterFactory.Get()); if (MappingPersisterFactory is not null && !Magic.Dummy)
consumers.Add(await MappingPersisterFactory.Get());
if (UserEventFirerFactory is not null) consumers.Add(await UserEventFirerFactory.Get()); if (UserEventFirerFactory is not null) consumers.Add(await UserEventFirerFactory.Get());
if (dbWatcher.User.LastFmConnected()) if (dbWatcher.User.LastFmConnected())
{ {
consumers.Add(await PlayCounterFactory.Get(creds: new() { Username = dbWatcher.User.LastFmUsername })); consumers.Add(await PlayCounterFactory.Get(creds: new()
{ Username = dbWatcher.User.LastFmUsername }));
} }
else else
{ {
Logger.LogDebug("[{username}] No Last.fm username, skipping play counter", dbWatcher.User.UserName); Logger.LogDebug("[{username}] No Last.fm username, skipping play counter",
dbWatcher.User.UserName);
} }
break; break;
case WatcherType.Playlist: case WatcherType.SpotifyPlaylist:
throw new NotImplementedException("Playlist watchers not implemented"); throw new NotImplementedException("Playlist watchers not implemented");
// break; break;
case WatcherType.AppleMusicPlayer:
watcher = await _appleWatcherFactory.Get<AppleMusicPlayerWatcher>(_appleMusicProvider,
_appleMusicOptions.Value.Key, _appleMusicOptions.Value.TeamId, _appleMusicOptions.Value.KeyId,
dbWatcher.User.AppleMusicKey);
if (PublisherFactory is not null) consumers.Add(await PublisherFactory.GetApple());
break;
} }
return watcherCollection.Add(watcher, consumers); return watcherCollection.Add(watcher, consumers);
@ -181,7 +198,7 @@ namespace Selector.CLI
{ {
Logger.LogInformation("Shutting down"); Logger.LogInformation("Shutting down");
foreach((var key, var watcher) in Watchers) foreach ((var key, var watcher) in Watchers)
{ {
Logger.LogInformation("Stopping watcher collection [{key}]", key); Logger.LogInformation("Stopping watcher collection [{key}]", key);
watcher.Stop(); watcher.Stop();
@ -195,24 +212,27 @@ namespace Selector.CLI
private void AttachEventBus() private void AttachEventBus()
{ {
UserEventBus.SpotifyLinkChange += SpotifyChangeCallback; UserEventBus.SpotifyLinkChange += SpotifyChangeCallback;
UserEventBus.AppleLinkChange += AppleMusicChangeCallback;
UserEventBus.LastfmCredChange += LastfmChangeCallback; UserEventBus.LastfmCredChange += LastfmChangeCallback;
} }
private void DetachEventBus() private void DetachEventBus()
{ {
UserEventBus.SpotifyLinkChange -= SpotifyChangeCallback; UserEventBus.SpotifyLinkChange -= SpotifyChangeCallback;
UserEventBus.AppleLinkChange -= AppleMusicChangeCallback;
UserEventBus.LastfmCredChange -= LastfmChangeCallback; UserEventBus.LastfmCredChange -= LastfmChangeCallback;
} }
public async void SpotifyChangeCallback(object sender, SpotifyLinkChange change) public async void SpotifyChangeCallback(object sender, SpotifyLinkChange change)
{ {
if(Watchers.ContainsKey(change.UserId)) if (Watchers.ContainsKey(change.UserId))
{ {
Logger.LogDebug("Setting new Spotify link state for [{username}], [{}]", change.UserId, change.NewLinkState); Logger.LogDebug("Setting new Spotify link state for [{username}], [{}]", change.UserId,
change.NewLinkState);
var watcherCollection = Watchers[change.UserId]; var watcherCollection = Watchers[change.UserId];
if(change.NewLinkState) if (change.NewLinkState)
{ {
watcherCollection.Start(); watcherCollection.Start();
} }
@ -227,8 +247,46 @@ namespace Selector.CLI
var db = scope.ServiceProvider.GetService<ApplicationDbContext>(); var db = scope.ServiceProvider.GetService<ApplicationDbContext>();
var watcherEnum = db.Watcher var watcherEnum = db.Watcher
.Include(w => w.User) .Include(w => w.User)
.Where(w => w.UserId == change.UserId); .Where(w => w.UserId == change.UserId);
foreach (var dbWatcher in watcherEnum)
{
var context = await InitInstance(dbWatcher);
}
Watchers[change.UserId].Start();
Logger.LogDebug("Started {} watchers for [{username}]", watcherEnum.Count(), change.UserId);
}
}
public async void AppleMusicChangeCallback(object sender, AppleMusicLinkChange change)
{
if (Watchers.ContainsKey(change.UserId))
{
Logger.LogDebug("Setting new Apple Music link state for [{username}], [{}]", change.UserId,
change.NewLinkState);
var watcherCollection = Watchers[change.UserId];
if (change.NewLinkState)
{
watcherCollection.Start();
}
else
{
watcherCollection.Stop();
}
}
else
{
using var scope = ServiceProvider.CreateScope();
var db = scope.ServiceProvider.GetService<ApplicationDbContext>();
var watcherEnum = db.Watcher
.Include(w => w.User)
.Where(w => w.UserId == change.UserId);
foreach (var dbWatcher in watcherEnum) foreach (var dbWatcher in watcherEnum)
{ {
@ -249,9 +307,9 @@ namespace Selector.CLI
var watcherCollection = Watchers[change.UserId]; var watcherCollection = Watchers[change.UserId];
foreach(var watcher in watcherCollection.Consumers) foreach (var watcher in watcherCollection.Consumers)
{ {
if(watcher is PlayCounter counter) if (watcher is PlayCounter counter)
{ {
counter.Credentials.Username = change.NewUsername; counter.Credentials.Username = change.NewUsername;
} }
@ -259,9 +317,8 @@ namespace Selector.CLI
} }
else else
{ {
Logger.LogDebug("No watchers running for [{username}], skipping Spotify event", change.UserId); Logger.LogDebug("No watchers running for [{username}], skipping Spotify event", change.UserId);
} }
} }
} }
} }

@ -4,12 +4,12 @@ using System.Linq;
using System.Text; using System.Text;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using Selector.AppleMusic;
using Selector.AppleMusic.Watcher;
using Selector.Cache; using Selector.Cache;
using Selector.CLI.Consumer; using Selector.CLI.Consumer;
@ -21,30 +21,38 @@ namespace Selector.CLI
private readonly ILogger<LocalWatcherService> Logger; private readonly ILogger<LocalWatcherService> Logger;
private readonly RootOptions Config; private readonly RootOptions Config;
private readonly IWatcherFactory WatcherFactory; private readonly ISpotifyWatcherFactory _spotifyWatcherFactory;
private readonly IAppleMusicWatcherFactory _appleWatcherFactory;
private readonly IWatcherCollectionFactory WatcherCollectionFactory; private readonly IWatcherCollectionFactory WatcherCollectionFactory;
private readonly IRefreshTokenFactoryProvider SpotifyFactory; private readonly IRefreshTokenFactoryProvider SpotifyFactory;
private readonly AppleMusicApiProvider _appleMusicApiProvider;
private readonly IOptions<AppleMusicOptions> _appleMusicOptions;
private readonly IServiceProvider ServiceProvider; private readonly IServiceProvider ServiceProvider;
private Dictionary<string, IWatcherCollection> Watchers { get; set; } = new(); private Dictionary<string, IWatcherCollection> Watchers { get; set; } = new();
public LocalWatcherService( public LocalWatcherService(
IWatcherFactory watcherFactory, ISpotifyWatcherFactory spotifyWatcherFactory,
IAppleMusicWatcherFactory appleWatcherFactory,
IWatcherCollectionFactory watcherCollectionFactory, IWatcherCollectionFactory watcherCollectionFactory,
IRefreshTokenFactoryProvider spotifyFactory, IRefreshTokenFactoryProvider spotifyFactory,
AppleMusicApiProvider appleMusicApiProvider,
IOptions<AppleMusicOptions> appleMusicOptions,
IServiceProvider serviceProvider, IServiceProvider serviceProvider,
ILogger<LocalWatcherService> logger, ILogger<LocalWatcherService> logger,
IOptions<RootOptions> config IOptions<RootOptions> config
) { )
{
Logger = logger; Logger = logger;
Config = config.Value; Config = config.Value;
WatcherFactory = watcherFactory; _spotifyWatcherFactory = spotifyWatcherFactory;
_appleWatcherFactory = appleWatcherFactory;
WatcherCollectionFactory = watcherCollectionFactory; WatcherCollectionFactory = watcherCollectionFactory;
SpotifyFactory = spotifyFactory; SpotifyFactory = spotifyFactory;
_appleMusicApiProvider = appleMusicApiProvider;
_appleMusicOptions = appleMusicOptions;
ServiceProvider = serviceProvider; ServiceProvider = serviceProvider;
} }
@ -75,7 +83,8 @@ namespace Selector.CLI
logMsg.Append($"Creating new [{watcherOption.Type}] watcher"); logMsg.Append($"Creating new [{watcherOption.Type}] watcher");
} }
if (!string.IsNullOrWhiteSpace(watcherOption.PlaylistUri)) logMsg.Append($" [{ watcherOption.PlaylistUri}]"); if (!string.IsNullOrWhiteSpace(watcherOption.PlaylistUri))
logMsg.Append($" [{watcherOption.PlaylistUri}]");
Logger.LogInformation(logMsg.ToString()); Logger.LogInformation(logMsg.ToString());
var watcherCollectionIdx = watcherOption.WatcherCollection ?? ConfigInstanceKey; var watcherCollectionIdx = watcherOption.WatcherCollection ?? ConfigInstanceKey;
@ -90,17 +99,26 @@ namespace Selector.CLI
var spotifyFactory = await SpotifyFactory.GetFactory(watcherOption.RefreshKey); var spotifyFactory = await SpotifyFactory.GetFactory(watcherOption.RefreshKey);
IWatcher watcher = null; IWatcher watcher = null;
switch(watcherOption.Type) switch (watcherOption.Type)
{ {
case WatcherType.Player: case WatcherType.SpotifyPlayer:
watcher = await WatcherFactory.Get<PlayerWatcher>(spotifyFactory, id: watcherOption.Name, pollPeriod: watcherOption.PollPeriod); watcher = await _spotifyWatcherFactory.Get<SpotifyPlayerWatcher>(spotifyFactory,
id: watcherOption.Name, pollPeriod: watcherOption.PollPeriod);
break; break;
case WatcherType.Playlist: case WatcherType.SpotifyPlaylist:
var playlistWatcher = await WatcherFactory.Get<PlaylistWatcher>(spotifyFactory, id: watcherOption.Name, pollPeriod: watcherOption.PollPeriod) as PlaylistWatcher; var playlistWatcher = await _spotifyWatcherFactory.Get<PlaylistWatcher>(spotifyFactory,
id: watcherOption.Name, pollPeriod: watcherOption.PollPeriod) as PlaylistWatcher;
playlistWatcher.config = new() { PlaylistId = watcherOption.PlaylistUri }; playlistWatcher.config = new() { PlaylistId = watcherOption.PlaylistUri };
watcher = playlistWatcher; watcher = playlistWatcher;
break; break;
case WatcherType.AppleMusicPlayer:
var appleMusicWatcher = await _appleWatcherFactory.Get<AppleMusicPlayerWatcher>(
_appleMusicApiProvider, _appleMusicOptions.Value.Key, _appleMusicOptions.Value.TeamId,
_appleMusicOptions.Value.KeyId, watcherOption.AppleUserToken);
watcher = appleMusicWatcher;
break;
} }
List<IConsumer> consumers = new(); List<IConsumer> consumers = new();
@ -112,11 +130,13 @@ namespace Selector.CLI
switch (consumer) switch (consumer)
{ {
case Consumers.AudioFeatures: case Consumers.AudioFeatures:
consumers.Add(await ServiceProvider.GetService<AudioFeatureInjectorFactory>().Get(spotifyFactory)); consumers.Add(await ServiceProvider.GetService<AudioFeatureInjectorFactory>()
.Get(spotifyFactory));
break; break;
case Consumers.AudioFeaturesCache: case Consumers.AudioFeaturesCache:
consumers.Add(await ServiceProvider.GetService<CachingAudioFeatureInjectorFactory>().Get(spotifyFactory)); consumers.Add(await ServiceProvider.GetService<CachingAudioFeatureInjectorFactory>()
.Get(spotifyFactory));
break; break;
case Consumers.CacheWriter: case Consumers.CacheWriter:
@ -124,18 +144,20 @@ namespace Selector.CLI
break; break;
case Consumers.Publisher: case Consumers.Publisher:
consumers.Add(await ServiceProvider.GetService<PublisherFactory>().Get()); consumers.Add(await ServiceProvider.GetService<PublisherFactory>().GetSpotify());
break; break;
case Consumers.PlayCounter: case Consumers.PlayCounter:
if (!string.IsNullOrWhiteSpace(watcherOption.LastFmUsername)) if (!string.IsNullOrWhiteSpace(watcherOption.LastFmUsername))
{ {
consumers.Add(await ServiceProvider.GetService<PlayCounterFactory>().Get(creds: new() { Username = watcherOption.LastFmUsername })); consumers.Add(await ServiceProvider.GetService<PlayCounterFactory>()
.Get(creds: new() { Username = watcherOption.LastFmUsername }));
} }
else else
{ {
Logger.LogError("No Last.fm username provided, skipping play counter"); Logger.LogError("No Last.fm username provided, skipping play counter");
} }
break; break;
case Consumers.MappingPersister: case Consumers.MappingPersister:
@ -171,7 +193,7 @@ namespace Selector.CLI
{ {
Logger.LogInformation("Shutting down"); Logger.LogInformation("Shutting down");
foreach((var key, var watcher) in Watchers) foreach ((var key, var watcher) in Watchers)
{ {
Logger.LogInformation("Stopping watcher collection [{key}]", key); Logger.LogInformation("Stopping watcher collection [{key}]", key);
watcher.Stop(); watcher.Stop();
@ -180,4 +202,4 @@ namespace Selector.CLI
return Task.CompletedTask; return Task.CompletedTask;
} }
} }
} }

@ -0,0 +1,93 @@
using System;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Selector.AppleMusic;
using Selector.AppleMusic.Watcher.Consumer;
using StackExchange.Redis;
namespace Selector.Cache.Consumer.AppleMusic
{
public class ApplePublisher : IApplePlayerConsumer
{
private readonly IAppleMusicPlayerWatcher Watcher;
private readonly ISubscriber Subscriber;
private readonly ILogger<ApplePublisher> Logger;
public CancellationToken CancelToken { get; set; }
public ApplePublisher(
IAppleMusicPlayerWatcher watcher,
ISubscriber subscriber,
ILogger<ApplePublisher> logger = null,
CancellationToken token = default
)
{
Watcher = watcher;
Subscriber = subscriber;
Logger = logger ?? NullLogger<ApplePublisher>.Instance;
CancelToken = token;
}
public void Callback(object sender, AppleListeningChangeEventArgs e)
{
if (e.Current is null) return;
Task.Run(async () =>
{
try
{
await AsyncCallback(e);
}
catch (Exception e)
{
Logger.LogError(e, "Error occured during callback");
}
}, CancelToken);
}
public async Task AsyncCallback(AppleListeningChangeEventArgs e)
{
// using var scope = Logger.GetListeningEventArgsScope(e);
var payload = JsonSerializer.Serialize(e, AppleJsonContext.Default.AppleListeningChangeEventArgs);
Logger.LogTrace("Publishing current");
// TODO: currently using spotify username for cache key, use db username
var receivers = await Subscriber.PublishAsync(Key.CurrentlyPlayingAppleMusic(e.Id), payload);
Logger.LogDebug("Published current, {receivers} receivers", receivers);
}
public void Subscribe(IWatcher watch = null)
{
var watcher = watch ?? Watcher ?? throw new ArgumentNullException("No watcher provided");
if (watcher is IAppleMusicPlayerWatcher watcherCast)
{
watcherCast.ItemChange += Callback;
}
else
{
throw new ArgumentException("Provided watcher is not a PlayerWatcher");
}
}
public void Unsubscribe(IWatcher watch = null)
{
var watcher = watch ?? Watcher ?? throw new ArgumentNullException("No watcher provided");
if (watcher is IAppleMusicPlayerWatcher watcherCast)
{
watcherCast.ItemChange -= Callback;
}
else
{
throw new ArgumentException("Provided watcher is not a PlayerWatcher");
}
}
}
}

@ -1,28 +1,26 @@
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using SpotifyAPI.Web; using SpotifyAPI.Web;
using StackExchange.Redis; using StackExchange.Redis;
namespace Selector.Cache namespace Selector.Cache
{ {
public class CachingAudioFeatureInjectorFactory: IAudioFeatureInjectorFactory { public class CachingAudioFeatureInjectorFactory : IAudioFeatureInjectorFactory
{
private readonly ILoggerFactory LoggerFactory; private readonly ILoggerFactory LoggerFactory;
private readonly IDatabaseAsync Db; private readonly IDatabaseAsync Db;
public CachingAudioFeatureInjectorFactory( public CachingAudioFeatureInjectorFactory(
ILoggerFactory loggerFactory, ILoggerFactory loggerFactory,
IDatabaseAsync db IDatabaseAsync db
) { )
{
LoggerFactory = loggerFactory; LoggerFactory = loggerFactory;
Db = db; Db = db;
} }
public async Task<IPlayerConsumer> Get(ISpotifyConfigFactory spotifyFactory, IPlayerWatcher watcher = null) public async Task<ISpotifyPlayerConsumer> Get(ISpotifyConfigFactory spotifyFactory,
ISpotifyPlayerWatcher watcher = null)
{ {
if (!Magic.Dummy) if (!Magic.Dummy)
{ {
@ -45,4 +43,4 @@ namespace Selector.Cache
} }
} }
} }
} }

@ -1,37 +1,35 @@
using System; using System.Threading.Tasks;
using System.Collections.Generic;
using System.Text;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using StackExchange.Redis; using StackExchange.Redis;
namespace Selector.Cache namespace Selector.Cache
{ {
public interface ICacheWriterFactory { public interface ICacheWriterFactory
public Task<IPlayerConsumer> Get(IPlayerWatcher watcher = null); {
public Task<ISpotifyPlayerConsumer> Get(ISpotifyPlayerWatcher watcher = null);
} }
public class CacheWriterFactory: ICacheWriterFactory { public class CacheWriterFactory : ICacheWriterFactory
{
private readonly ILoggerFactory LoggerFactory; private readonly ILoggerFactory LoggerFactory;
private readonly IDatabaseAsync Cache; private readonly IDatabaseAsync Cache;
public CacheWriterFactory( public CacheWriterFactory(
IDatabaseAsync cache, IDatabaseAsync cache,
ILoggerFactory loggerFactory ILoggerFactory loggerFactory
) { )
{
Cache = cache; Cache = cache;
LoggerFactory = loggerFactory; LoggerFactory = loggerFactory;
} }
public Task<IPlayerConsumer> Get(IPlayerWatcher watcher = null) public Task<ISpotifyPlayerConsumer> Get(ISpotifyPlayerWatcher watcher = null)
{ {
return Task.FromResult<IPlayerConsumer>(new CacheWriter( return Task.FromResult<ISpotifyPlayerConsumer>(new CacheWriter(
watcher, watcher,
Cache, Cache,
LoggerFactory.CreateLogger<CacheWriter>() LoggerFactory.CreateLogger<CacheWriter>()
)); ));
} }
} }
} }

@ -1,15 +1,12 @@
using System; using System;
using System.Collections.Generic;
using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using StackExchange.Redis;
using IF.Lastfm.Core.Api; using IF.Lastfm.Core.Api;
using Microsoft.Extensions.Logging;
using StackExchange.Redis;
namespace Selector.Cache namespace Selector.Cache
{ {
public class PlayCounterCachingFactory: IPlayCounterFactory public class PlayCounterCachingFactory : IPlayCounterFactory
{ {
private readonly ILoggerFactory LoggerFactory; private readonly ILoggerFactory LoggerFactory;
private readonly IDatabaseAsync Cache; private readonly IDatabaseAsync Cache;
@ -17,9 +14,9 @@ namespace Selector.Cache
private readonly LastFmCredentials Creds; private readonly LastFmCredentials Creds;
public PlayCounterCachingFactory( public PlayCounterCachingFactory(
ILoggerFactory loggerFactory, ILoggerFactory loggerFactory,
IDatabaseAsync cache, IDatabaseAsync cache,
LastfmClient client = null, LastfmClient client = null,
LastFmCredentials creds = null) LastFmCredentials creds = null)
{ {
LoggerFactory = loggerFactory; LoggerFactory = loggerFactory;
@ -28,7 +25,8 @@ namespace Selector.Cache
Creds = creds; Creds = creds;
} }
public Task<IPlayerConsumer> Get(LastfmClient fmClient = null, LastFmCredentials creds = null, IPlayerWatcher watcher = null) public Task<ISpotifyPlayerConsumer> Get(LastfmClient fmClient = null, LastFmCredentials creds = null,
ISpotifyPlayerWatcher watcher = null)
{ {
var client = fmClient ?? Client; var client = fmClient ?? Client;
@ -37,7 +35,7 @@ namespace Selector.Cache
throw new ArgumentNullException("No Last.fm client provided"); throw new ArgumentNullException("No Last.fm client provided");
} }
return Task.FromResult<IPlayerConsumer>(new PlayCounterCaching( return Task.FromResult<ISpotifyPlayerConsumer>(new PlayCounterCaching(
watcher, watcher,
client.Track, client.Track,
client.Album, client.Album,
@ -49,4 +47,4 @@ namespace Selector.Cache
)); ));
} }
} }
} }

@ -1,37 +1,47 @@
using System; using System.Threading.Tasks;
using System.Collections.Generic;
using System.Text;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Selector.AppleMusic.Watcher.Consumer;
using Selector.Cache.Consumer.AppleMusic;
using StackExchange.Redis; using StackExchange.Redis;
namespace Selector.Cache namespace Selector.Cache
{ {
public interface IPublisherFactory { public interface IPublisherFactory
public Task<IPlayerConsumer> Get(IPlayerWatcher watcher = null); {
public Task<ISpotifyPlayerConsumer> GetSpotify(ISpotifyPlayerWatcher watcher = null);
public Task<IApplePlayerConsumer> GetApple(IAppleMusicPlayerWatcher watcher = null);
} }
public class PublisherFactory: IPublisherFactory { public class PublisherFactory : IPublisherFactory
{
private readonly ILoggerFactory LoggerFactory; private readonly ILoggerFactory LoggerFactory;
private readonly ISubscriber Subscriber; private readonly ISubscriber Subscriber;
public PublisherFactory( public PublisherFactory(
ISubscriber subscriber, ISubscriber subscriber,
ILoggerFactory loggerFactory ILoggerFactory loggerFactory
) { )
{
Subscriber = subscriber; Subscriber = subscriber;
LoggerFactory = loggerFactory; LoggerFactory = loggerFactory;
} }
public Task<IPlayerConsumer> Get(IPlayerWatcher watcher = null) public Task<ISpotifyPlayerConsumer> GetSpotify(ISpotifyPlayerWatcher watcher = null)
{ {
return Task.FromResult<IPlayerConsumer>(new Publisher( return Task.FromResult<ISpotifyPlayerConsumer>(new SpotifyPublisher(
watcher, watcher,
Subscriber, Subscriber,
LoggerFactory.CreateLogger<Publisher>() LoggerFactory.CreateLogger<SpotifyPublisher>()
));
}
public Task<IApplePlayerConsumer> GetApple(IAppleMusicPlayerWatcher watcher = null)
{
return Task.FromResult<IApplePlayerConsumer>(new ApplePublisher(
watcher,
Subscriber,
LoggerFactory.CreateLogger<ApplePublisher>()
)); ));
} }
} }
} }

@ -1,25 +1,20 @@
using System; using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.Json;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using IF.Lastfm.Core.Api; using IF.Lastfm.Core.Api;
using StackExchange.Redis; using Microsoft.Extensions.Logging;
using SpotifyAPI.Web; using SpotifyAPI.Web;
using StackExchange.Redis;
namespace Selector.Cache namespace Selector.Cache
{ {
public class PlayCounterCaching: PlayCounter public class PlayCounterCaching : PlayCounter
{ {
private readonly IDatabaseAsync Db; private readonly IDatabaseAsync Db;
public TimeSpan CacheExpiry { get; set; } = TimeSpan.FromDays(1); public TimeSpan CacheExpiry { get; set; } = TimeSpan.FromDays(1);
public PlayCounterCaching( public PlayCounterCaching(
IPlayerWatcher watcher, ISpotifyPlayerWatcher watcher,
ITrackApi trackClient, ITrackApi trackClient,
IAlbumApi albumClient, IAlbumApi albumClient,
IArtistApi artistClient, IArtistApi artistClient,
@ -37,7 +32,8 @@ namespace Selector.Cache
public void CacheCallback(object sender, PlayCount e) public void CacheCallback(object sender, PlayCount e)
{ {
Task.Run(async () => { Task.Run(async () =>
{
try try
{ {
await AsyncCacheCallback(e); await AsyncCacheCallback(e);
@ -56,9 +52,12 @@ namespace Selector.Cache
var tasks = new Task[] var tasks = new Task[]
{ {
Db.StringSetAsync(Key.TrackPlayCount(e.Username, track.Name, track.Artists[0].Name), e.Track, expiry: CacheExpiry), Db.StringSetAsync(Key.TrackPlayCount(e.Username, track.Name, track.Artists[0].Name), e.Track,
Db.StringSetAsync(Key.AlbumPlayCount(e.Username, track.Album.Name, track.Album.Artists[0].Name), e.Album, expiry: CacheExpiry), expiry: CacheExpiry),
Db.StringSetAsync(Key.ArtistPlayCount(e.Username, track.Artists[0].Name), e.Artist, expiry: CacheExpiry), Db.StringSetAsync(Key.AlbumPlayCount(e.Username, track.Album.Name, track.Album.Artists[0].Name),
e.Album, expiry: CacheExpiry),
Db.StringSetAsync(Key.ArtistPlayCount(e.Username, track.Artists[0].Name), e.Artist,
expiry: CacheExpiry),
Db.StringSetAsync(Key.UserPlayCount(e.Username), e.User, expiry: CacheExpiry), Db.StringSetAsync(Key.UserPlayCount(e.Username), e.User, expiry: CacheExpiry),
}; };
@ -67,4 +66,4 @@ namespace Selector.Cache
Logger.LogDebug("Cached play count for [{track}]", track.DisplayString()); Logger.LogDebug("Cached play count for [{track}]", track.DisplayString());
} }
} }
} }

@ -1,10 +1,8 @@
using System; using System;
using System.Collections.Generic;
using System.Text.Json; using System.Text.Json;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using SpotifyAPI.Web; using SpotifyAPI.Web;
using StackExchange.Redis; using StackExchange.Redis;
@ -16,13 +14,13 @@ namespace Selector.Cache
public TimeSpan CacheExpiry { get; set; } = TimeSpan.FromDays(14); public TimeSpan CacheExpiry { get; set; } = TimeSpan.FromDays(14);
public CachingAudioFeatureInjector( public CachingAudioFeatureInjector(
IPlayerWatcher watcher, ISpotifyPlayerWatcher watcher,
IDatabaseAsync db, IDatabaseAsync db,
ITracksClient trackClient, ITracksClient trackClient,
ILogger<CachingAudioFeatureInjector> logger = null, ILogger<CachingAudioFeatureInjector> logger = null,
CancellationToken token = default CancellationToken token = default
) : base(watcher, trackClient, logger, token) { ) : base(watcher, trackClient, logger, token)
{
Db = db; Db = db;
NewFeature += CacheCallback; NewFeature += CacheCallback;
@ -46,12 +44,13 @@ namespace Selector.Cache
public async Task AsyncCacheCallback(AnalysedTrack e) public async Task AsyncCacheCallback(AnalysedTrack e)
{ {
var payload = JsonSerializer.Serialize(e.Features, JsonContext.Default.TrackAudioFeatures); var payload = JsonSerializer.Serialize(e.Features, JsonContext.Default.TrackAudioFeatures);
Logger.LogTrace("Caching current for [{track}]", e.Track.DisplayString()); Logger.LogTrace("Caching current for [{track}]", e.Track.DisplayString());
var resp = await Db.StringSetAsync(Key.AudioFeature(e.Track.Id), payload, expiry: CacheExpiry); var resp = await Db.StringSetAsync(Key.AudioFeature(e.Track.Id), payload, expiry: CacheExpiry);
Logger.LogDebug("Cached audio feature for [{track}], {state}", e.Track.DisplayString(), (resp ? "value set" : "value NOT set")); Logger.LogDebug("Cached audio feature for [{track}], {state}", e.Track.DisplayString(),
(resp ? "value set" : "value NOT set"));
} }
} }
} }

@ -1,18 +1,16 @@
using System; using System;
using System.Collections.Generic;
using System.Text.Json; using System.Text.Json;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Logging.Abstractions;
using StackExchange.Redis; using StackExchange.Redis;
namespace Selector.Cache namespace Selector.Cache
{ {
public class CacheWriter : IPlayerConsumer public class CacheWriter : ISpotifyPlayerConsumer
{ {
private readonly IPlayerWatcher Watcher; private readonly ISpotifyPlayerWatcher Watcher;
private readonly IDatabaseAsync Db; private readonly IDatabaseAsync Db;
private readonly ILogger<CacheWriter> Logger; private readonly ILogger<CacheWriter> Logger;
public TimeSpan CacheExpiry { get; set; } = TimeSpan.FromMinutes(20); public TimeSpan CacheExpiry { get; set; } = TimeSpan.FromMinutes(20);
@ -20,11 +18,12 @@ namespace Selector.Cache
public CancellationToken CancelToken { get; set; } public CancellationToken CancelToken { get; set; }
public CacheWriter( public CacheWriter(
IPlayerWatcher watcher, ISpotifyPlayerWatcher watcher,
IDatabaseAsync db, IDatabaseAsync db,
ILogger<CacheWriter> logger = null, ILogger<CacheWriter> logger = null,
CancellationToken token = default CancellationToken token = default
){ )
{
Watcher = watcher; Watcher = watcher;
Db = db; Db = db;
Logger = logger ?? NullLogger<CacheWriter>.Instance; Logger = logger ?? NullLogger<CacheWriter>.Instance;
@ -34,8 +33,9 @@ namespace Selector.Cache
public void Callback(object sender, ListeningChangeEventArgs e) public void Callback(object sender, ListeningChangeEventArgs e)
{ {
if (e.Current is null) return; if (e.Current is null) return;
Task.Run(async () => { Task.Run(async () =>
{
try try
{ {
await AsyncCallback(e); await AsyncCallback(e);
@ -44,7 +44,6 @@ namespace Selector.Cache
{ {
Logger.LogError(e, "Error occured during callback"); Logger.LogError(e, "Error occured during callback");
} }
}, CancelToken); }, CancelToken);
} }
@ -52,24 +51,23 @@ namespace Selector.Cache
{ {
using var scope = Logger.GetListeningEventArgsScope(e); using var scope = Logger.GetListeningEventArgsScope(e);
var payload = JsonSerializer.Serialize((CurrentlyPlayingDTO) e, JsonContext.Default.CurrentlyPlayingDTO); var payload = JsonSerializer.Serialize((CurrentlyPlayingDTO)e, JsonContext.Default.CurrentlyPlayingDTO);
Logger.LogTrace("Caching current"); Logger.LogTrace("Caching current");
var resp = await Db.StringSetAsync(Key.CurrentlyPlaying(e.Id), payload, expiry: CacheExpiry); var resp = await Db.StringSetAsync(Key.CurrentlyPlayingSpotify(e.Id), payload, expiry: CacheExpiry);
Logger.LogDebug("Cached current, {state}", (resp ? "value set" : "value NOT set")); Logger.LogDebug("Cached current, {state}", (resp ? "value set" : "value NOT set"));
} }
public void Subscribe(IWatcher watch = null) public void Subscribe(IWatcher watch = null)
{ {
var watcher = watch ?? Watcher ?? throw new ArgumentNullException("No watcher provided"); var watcher = watch ?? Watcher ?? throw new ArgumentNullException("No watcher provided");
if (watcher is IPlayerWatcher watcherCast) if (watcher is ISpotifyPlayerWatcher watcherCast)
{ {
watcherCast.ItemChange += Callback; watcherCast.ItemChange += Callback;
} }
else else
{ {
throw new ArgumentException("Provided watcher is not a PlayerWatcher"); throw new ArgumentException("Provided watcher is not a PlayerWatcher");
@ -80,7 +78,7 @@ namespace Selector.Cache
{ {
var watcher = watch ?? Watcher ?? throw new ArgumentNullException("No watcher provided"); var watcher = watch ?? Watcher ?? throw new ArgumentNullException("No watcher provided");
if (watcher is IPlayerWatcher watcherCast) if (watcher is ISpotifyPlayerWatcher watcherCast)
{ {
watcherCast.ItemChange -= Callback; watcherCast.ItemChange -= Callback;
} }
@ -90,4 +88,4 @@ namespace Selector.Cache
} }
} }
} }
} }

@ -1,32 +1,31 @@
using System; using System;
using System.Collections.Generic;
using System.Text.Json; using System.Text.Json;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Logging.Abstractions;
using StackExchange.Redis; using StackExchange.Redis;
namespace Selector.Cache namespace Selector.Cache
{ {
public class Publisher : IPlayerConsumer public class SpotifyPublisher : ISpotifyPlayerConsumer
{ {
private readonly IPlayerWatcher Watcher; private readonly ISpotifyPlayerWatcher Watcher;
private readonly ISubscriber Subscriber; private readonly ISubscriber Subscriber;
private readonly ILogger<Publisher> Logger; private readonly ILogger<SpotifyPublisher> Logger;
public CancellationToken CancelToken { get; set; } public CancellationToken CancelToken { get; set; }
public Publisher( public SpotifyPublisher(
IPlayerWatcher watcher, ISpotifyPlayerWatcher watcher,
ISubscriber subscriber, ISubscriber subscriber,
ILogger<Publisher> logger = null, ILogger<SpotifyPublisher> logger = null,
CancellationToken token = default CancellationToken token = default
){ )
{
Watcher = watcher; Watcher = watcher;
Subscriber = subscriber; Subscriber = subscriber;
Logger = logger ?? NullLogger<Publisher>.Instance; Logger = logger ?? NullLogger<SpotifyPublisher>.Instance;
CancelToken = token; CancelToken = token;
} }
@ -34,7 +33,8 @@ namespace Selector.Cache
{ {
if (e.Current is null) return; if (e.Current is null) return;
Task.Run(async () => { Task.Run(async () =>
{
try try
{ {
await AsyncCallback(e); await AsyncCallback(e);
@ -50,12 +50,12 @@ namespace Selector.Cache
{ {
using var scope = Logger.GetListeningEventArgsScope(e); using var scope = Logger.GetListeningEventArgsScope(e);
var payload = JsonSerializer.Serialize((CurrentlyPlayingDTO) e, JsonContext.Default.CurrentlyPlayingDTO); var payload = JsonSerializer.Serialize((CurrentlyPlayingDTO)e, JsonContext.Default.CurrentlyPlayingDTO);
Logger.LogTrace("Publishing current"); Logger.LogTrace("Publishing current");
// TODO: currently using spotify username for cache key, use db username // TODO: currently using spotify username for cache key, use db username
var receivers = await Subscriber.PublishAsync(Key.CurrentlyPlaying(e.Id), payload); var receivers = await Subscriber.PublishAsync(Key.CurrentlyPlayingSpotify(e.Id), payload);
Logger.LogDebug("Published current, {receivers} receivers", receivers); Logger.LogDebug("Published current, {receivers} receivers", receivers);
} }
@ -64,10 +64,10 @@ namespace Selector.Cache
{ {
var watcher = watch ?? Watcher ?? throw new ArgumentNullException("No watcher provided"); var watcher = watch ?? Watcher ?? throw new ArgumentNullException("No watcher provided");
if (watcher is IPlayerWatcher watcherCast) if (watcher is ISpotifyPlayerWatcher watcherCast)
{ {
watcherCast.ItemChange += Callback; watcherCast.ItemChange += Callback;
} }
else else
{ {
throw new ArgumentException("Provided watcher is not a PlayerWatcher"); throw new ArgumentException("Provided watcher is not a PlayerWatcher");
@ -78,7 +78,7 @@ namespace Selector.Cache
{ {
var watcher = watch ?? Watcher ?? throw new ArgumentNullException("No watcher provided"); var watcher = watch ?? Watcher ?? throw new ArgumentNullException("No watcher provided");
if (watcher is IPlayerWatcher watcherCast) if (watcher is ISpotifyPlayerWatcher watcherCast)
{ {
watcherCast.ItemChange -= Callback; watcherCast.ItemChange -= Callback;
} }
@ -88,4 +88,4 @@ namespace Selector.Cache
} }
} }
} }
} }

@ -1,7 +1,4 @@
using System; using System.Linq;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace Selector.Cache namespace Selector.Cache
{ {
@ -33,25 +30,47 @@ namespace Selector.Cache
/// </summary> /// </summary>
/// <param name="user">User's database Id (Guid)</param> /// <param name="user">User's database Id (Guid)</param>
/// <returns></returns> /// <returns></returns>
public static string CurrentlyPlaying(string user) => MajorNamespace(MinorNamespace(UserName, CurrentlyPlayingName), user); public static string CurrentlyPlayingSpotify(string user) =>
public static readonly string AllCurrentlyPlaying = CurrentlyPlaying(All); MajorNamespace(MinorNamespace(UserName, SpotifyName, CurrentlyPlayingName), user);
public static string CurrentlyPlayingAppleMusic(string user) =>
MajorNamespace(MinorNamespace(UserName, AppleMusicName, CurrentlyPlayingName), user);
public static readonly string AllCurrentlyPlayingSpotify = CurrentlyPlayingSpotify(All);
public static readonly string AllCurrentlyPlayingApple = CurrentlyPlayingAppleMusic(All);
public static string Track(string trackId) => MajorNamespace(TrackName, trackId); public static string Track(string trackId) => MajorNamespace(TrackName, trackId);
public static readonly string AllTracks = Track(All); public static readonly string AllTracks = Track(All);
public static string AudioFeature(string trackId) => MajorNamespace(MinorNamespace(TrackName, AudioFeatureName), trackId); public static string AudioFeature(string trackId) =>
MajorNamespace(MinorNamespace(TrackName, AudioFeatureName), trackId);
public static readonly string AllAudioFeatures = AudioFeature(All); public static readonly string AllAudioFeatures = AudioFeature(All);
public static string TrackPlayCount(string username, string name, string artist) => MajorNamespace(MinorNamespace(TrackName, PlayCountName), artist, name, username); public static string TrackPlayCount(string username, string name, string artist) =>
public static string AlbumPlayCount(string username, string name, string artist) => MajorNamespace(MinorNamespace(AlbumName, PlayCountName), artist, name, username); MajorNamespace(MinorNamespace(TrackName, PlayCountName), artist, name, username);
public static string ArtistPlayCount(string username, string name) => MajorNamespace(MinorNamespace(ArtistName, PlayCountName), name, username);
public static string UserPlayCount(string username) => MajorNamespace(MinorNamespace(UserName, PlayCountName), username); public static string AlbumPlayCount(string username, string name, string artist) =>
MajorNamespace(MinorNamespace(AlbumName, PlayCountName), artist, name, username);
public static string ArtistPlayCount(string username, string name) =>
MajorNamespace(MinorNamespace(ArtistName, PlayCountName), name, username);
public static string UserPlayCount(string username) =>
MajorNamespace(MinorNamespace(UserName, PlayCountName), username);
public static string UserSpotify(string username) =>
MajorNamespace(MinorNamespace(UserName, SpotifyName), username);
public static string UserAppleMusic(string username) =>
MajorNamespace(MinorNamespace(UserName, AppleMusicName), username);
public static string UserSpotify(string username) => MajorNamespace(MinorNamespace(UserName, SpotifyName), username);
public static string UserAppleMusic(string username) => MajorNamespace(MinorNamespace(UserName, AppleMusicName), username);
public static readonly string AllUserSpotify = UserSpotify(All); public static readonly string AllUserSpotify = UserSpotify(All);
public static readonly string AllUserAppleMusic = UserAppleMusic(All); public static readonly string AllUserAppleMusic = UserAppleMusic(All);
public static string UserLastfm(string username) => MajorNamespace(MinorNamespace(UserName, LastfmName), username);
public static string UserLastfm(string username) =>
MajorNamespace(MinorNamespace(UserName, LastfmName), username);
public static readonly string AllUserLastfm = UserLastfm(All); public static readonly string AllUserLastfm = UserLastfm(All);
public static string Watcher(int id) => MajorNamespace(WatcherName, id.ToString()); public static string Watcher(int id) => MajorNamespace(WatcherName, id.ToString());
@ -66,14 +85,17 @@ namespace Selector.Cache
public static string[] UnNamespace(string key, params char[] args) => key.Split(args); public static string[] UnNamespace(string key, params char[] args) => key.Split(args);
public static string Param(string key) => UnMajorNamespace(key).Skip(1).First(); public static string Param(string key) => UnMajorNamespace(key).Skip(1).First();
public static (string, string) ParamPair(string key) {
public static (string, string) ParamPair(string key)
{
var split = UnMajorNamespace(key); var split = UnMajorNamespace(key);
return (split[1], split[2]); return (split[1], split[2]);
} }
public static (string, string, string) ParamTriplet(string key) public static (string, string, string) ParamTriplet(string key)
{ {
var split = UnMajorNamespace(key); var split = UnMajorNamespace(key);
return (split[1], split[2], split[3]); return (split[1], split[2], split[3]);
} }
} }
} }

@ -13,6 +13,7 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\Selector.AppleMusic\Selector.AppleMusic.csproj"/>
<ProjectReference Include="..\Selector\Selector.csproj" /> <ProjectReference Include="..\Selector\Selector.csproj" />
</ItemGroup> </ItemGroup>

@ -1,9 +1,8 @@
using System.Text.Json; using System.Text.Json;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Selector.AppleMusic;
using StackExchange.Redis;
using Selector.Cache; using Selector.Cache;
using StackExchange.Redis;
namespace Selector.Events namespace Selector.Events
{ {
@ -28,20 +27,39 @@ namespace Selector.Events
{ {
Logger.LogDebug("Forming now playing event mapping between cache and event bus"); Logger.LogDebug("Forming now playing event mapping between cache and event bus");
(await Subscriber.SubscribeAsync(Key.AllCurrentlyPlaying)).OnMessage(message => { (await Subscriber.SubscribeAsync(Key.AllCurrentlyPlayingSpotify)).OnMessage(message =>
{
try try
{ {
var userId = Key.Param(message.Channel); var userId = Key.Param(message.Channel);
var deserialised = JsonSerializer.Deserialize(message.Message, JsonContext.Default.CurrentlyPlayingDTO); var deserialised =
Logger.LogDebug("Received new currently playing [{username}]", deserialised.Username); JsonSerializer.Deserialize(message.Message, JsonContext.Default.CurrentlyPlayingDTO);
Logger.LogDebug("Received new Spotify currently playing [{username}]", deserialised.Username);
UserEvent.OnCurrentlyPlayingChange(this, deserialised); UserEvent.OnCurrentlyPlayingChangeSpotify(this, deserialised);
} }
catch (Exception e) catch (Exception e)
{ {
Logger.LogError(e, "Error parsing new currently playing [{message}]", message); Logger.LogError(e, "Error parsing new Spotify currently playing [{message}]", message);
}
});
(await Subscriber.SubscribeAsync(Key.AllCurrentlyPlayingApple)).OnMessage(message =>
{
try
{
var userId = Key.Param(message.Channel);
var deserialised = JsonSerializer.Deserialize(message.Message,
AppleJsonContext.Default.AppleListeningChangeEventArgs);
Logger.LogDebug("Received new Apple Music currently playing");
UserEvent.OnCurrentlyPlayingChangeApple(this, deserialised);
}
catch (Exception e)
{
Logger.LogError(e, "Error parsing new Apple Music currently playing [{message}]", message);
} }
}); });
} }
@ -69,10 +87,16 @@ namespace Selector.Events
{ {
Logger.LogDebug("Forming now playing event mapping TO cache FROM event bus"); Logger.LogDebug("Forming now playing event mapping TO cache FROM event bus");
UserEvent.CurrentlyPlaying += async (o, e) => UserEvent.CurrentlyPlayingSpotify += async (o, e) =>
{ {
var payload = JsonSerializer.Serialize(e, JsonContext.Default.CurrentlyPlayingDTO); var payload = JsonSerializer.Serialize(e, JsonContext.Default.CurrentlyPlayingDTO);
await Subscriber.PublishAsync(Key.CurrentlyPlaying(e.UserId), payload); await Subscriber.PublishAsync(Key.CurrentlyPlayingSpotify(e.UserId), payload);
};
UserEvent.CurrentlyPlayingApple += async (o, e) =>
{
var payload = JsonSerializer.Serialize(e, AppleJsonContext.Default.AppleListeningChangeEventArgs);
await Subscriber.PublishAsync(Key.CurrentlyPlayingAppleMusic(e.Id), payload);
}; };
return Task.CompletedTask; return Task.CompletedTask;

@ -0,0 +1,84 @@
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Selector.AppleMusic;
using Selector.AppleMusic.Watcher.Consumer;
namespace Selector.Events
{
public class AppleUserEventFirer : IApplePlayerConsumer
{
protected readonly IAppleMusicPlayerWatcher Watcher;
protected readonly ILogger<AppleUserEventFirer> Logger;
protected readonly UserEventBus UserEvent;
public CancellationToken CancelToken { get; set; }
public AppleUserEventFirer(
IAppleMusicPlayerWatcher watcher,
UserEventBus userEvent,
ILogger<AppleUserEventFirer> logger = null,
CancellationToken token = default
)
{
Watcher = watcher;
UserEvent = userEvent;
Logger = logger ?? NullLogger<AppleUserEventFirer>.Instance;
CancelToken = token;
}
public void Callback(object sender, AppleListeningChangeEventArgs e)
{
if (e.Current is null) return;
Task.Run(async () =>
{
try
{
await AsyncCallback(e);
}
catch (Exception e)
{
Logger.LogError(e, "Error occured during callback");
}
}, CancelToken);
}
public Task AsyncCallback(AppleListeningChangeEventArgs e)
{
Logger.LogDebug("Firing Apple now playing event on user bus [{userId}]", e.Id);
UserEvent.OnCurrentlyPlayingChangeApple(this, e);
return Task.CompletedTask;
}
public void Subscribe(IWatcher watch = null)
{
var watcher = watch ?? Watcher ?? throw new ArgumentNullException("No watcher provided");
if (watcher is IAppleMusicPlayerWatcher watcherCast)
{
watcherCast.ItemChange += Callback;
}
else
{
throw new ArgumentException("Provided watcher is not a PlayerWatcher");
}
}
public void Unsubscribe(IWatcher watch = null)
{
var watcher = watch ?? Watcher ?? throw new ArgumentNullException("No watcher provided");
if (watcher is IAppleMusicPlayerWatcher watcherCast)
{
watcherCast.ItemChange -= Callback;
}
else
{
throw new ArgumentException("Provided watcher is not a PlayerWatcher");
}
}
}
}

@ -3,33 +3,34 @@ using Microsoft.Extensions.Logging.Abstractions;
namespace Selector.Events namespace Selector.Events
{ {
public class UserEventFirer : IPlayerConsumer public class SpotifyUserEventFirer : ISpotifyPlayerConsumer
{ {
protected readonly IPlayerWatcher Watcher; protected readonly ISpotifyPlayerWatcher Watcher;
protected readonly ILogger<UserEventFirer> Logger; protected readonly ILogger<SpotifyUserEventFirer> Logger;
protected readonly UserEventBus UserEvent; protected readonly UserEventBus UserEvent;
public CancellationToken CancelToken { get; set; } public CancellationToken CancelToken { get; set; }
public UserEventFirer( public SpotifyUserEventFirer(
IPlayerWatcher watcher, ISpotifyPlayerWatcher watcher,
UserEventBus userEvent, UserEventBus userEvent,
ILogger<UserEventFirer> logger = null, ILogger<SpotifyUserEventFirer> logger = null,
CancellationToken token = default CancellationToken token = default
) )
{ {
Watcher = watcher; Watcher = watcher;
UserEvent = userEvent; UserEvent = userEvent;
Logger = logger ?? NullLogger<UserEventFirer>.Instance; Logger = logger ?? NullLogger<SpotifyUserEventFirer>.Instance;
CancelToken = token; CancelToken = token;
} }
public void Callback(object sender, ListeningChangeEventArgs e) public void Callback(object sender, ListeningChangeEventArgs e)
{ {
if (e.Current is null) return; if (e.Current is null) return;
Task.Run(async () => { Task.Run(async () =>
{
try try
{ {
await AsyncCallback(e); await AsyncCallback(e);
@ -43,9 +44,10 @@ namespace Selector.Events
public Task AsyncCallback(ListeningChangeEventArgs e) public Task AsyncCallback(ListeningChangeEventArgs e)
{ {
Logger.LogDebug("Firing now playing event on user bus [{username}/{userId}]", e.SpotifyUsername, e.Id); Logger.LogDebug("Firing Spotify now playing event on user bus [{username}/{userId}]", e.SpotifyUsername,
e.Id);
UserEvent.OnCurrentlyPlayingChange(this, (CurrentlyPlayingDTO) e); UserEvent.OnCurrentlyPlayingChangeSpotify(this, (CurrentlyPlayingDTO)e);
return Task.CompletedTask; return Task.CompletedTask;
} }
@ -54,7 +56,7 @@ namespace Selector.Events
{ {
var watcher = watch ?? Watcher ?? throw new ArgumentNullException("No watcher provided"); var watcher = watch ?? Watcher ?? throw new ArgumentNullException("No watcher provided");
if (watcher is IPlayerWatcher watcherCast) if (watcher is ISpotifyPlayerWatcher watcherCast)
{ {
watcherCast.ItemChange += Callback; watcherCast.ItemChange += Callback;
} }
@ -68,7 +70,7 @@ namespace Selector.Events
{ {
var watcher = watch ?? Watcher ?? throw new ArgumentNullException("No watcher provided"); var watcher = watch ?? Watcher ?? throw new ArgumentNullException("No watcher provided");
if (watcher is IPlayerWatcher watcherCast) if (watcher is ISpotifyPlayerWatcher watcherCast)
{ {
watcherCast.ItemChange -= Callback; watcherCast.ItemChange -= Callback;
} }
@ -78,4 +80,4 @@ namespace Selector.Events
} }
} }
} }
} }

@ -4,10 +4,10 @@ namespace Selector.Events
{ {
public interface IUserEventFirerFactory public interface IUserEventFirerFactory
{ {
public Task<UserEventFirer> Get(IPlayerWatcher watcher = null); public Task<SpotifyUserEventFirer> Get(ISpotifyPlayerWatcher watcher = null);
} }
public class UserEventFirerFactory: IUserEventFirerFactory public class UserEventFirerFactory : IUserEventFirerFactory
{ {
private readonly ILoggerFactory LoggerFactory; private readonly ILoggerFactory LoggerFactory;
private readonly UserEventBus UserEvent; private readonly UserEventBus UserEvent;
@ -18,13 +18,13 @@ namespace Selector.Events
UserEvent = userEvent; UserEvent = userEvent;
} }
public Task<UserEventFirer> Get(IPlayerWatcher watcher = null) public Task<SpotifyUserEventFirer> Get(ISpotifyPlayerWatcher watcher = null)
{ {
return Task.FromResult(new UserEventFirer( return Task.FromResult(new SpotifyUserEventFirer(
watcher, watcher,
UserEvent, UserEvent,
LoggerFactory.CreateLogger<UserEventFirer>() LoggerFactory.CreateLogger<SpotifyUserEventFirer>()
)); ));
} }
} }
} }

@ -6,6 +6,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\Selector.AppleMusic\Selector.AppleMusic.csproj"/>
<ProjectReference Include="..\Selector\Selector.csproj" /> <ProjectReference Include="..\Selector\Selector.csproj" />
<ProjectReference Include="..\Selector.Model\Selector.Model.csproj" /> <ProjectReference Include="..\Selector.Model\Selector.Model.csproj" />
<ProjectReference Include="..\Selector.Cache\Selector.Cache.csproj" /> <ProjectReference Include="..\Selector.Cache\Selector.Cache.csproj" />

@ -1,10 +1,10 @@
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Selector.AppleMusic;
using Selector.Model; using Selector.Model;
namespace Selector.Events namespace Selector.Events
{ {
public class UserEventBus: IEventBus public class UserEventBus : IEventBus
{ {
private readonly ILogger<UserEventBus> Logger; private readonly ILogger<UserEventBus> Logger;
@ -13,7 +13,8 @@ namespace Selector.Events
public event EventHandler<AppleMusicLinkChange> AppleLinkChange; public event EventHandler<AppleMusicLinkChange> AppleLinkChange;
public event EventHandler<LastfmChange> LastfmCredChange; public event EventHandler<LastfmChange> LastfmCredChange;
public event EventHandler<CurrentlyPlayingDTO> CurrentlyPlaying; public event EventHandler<CurrentlyPlayingDTO> CurrentlyPlayingSpotify;
public event EventHandler<AppleListeningChangeEventArgs> CurrentlyPlayingApple;
public UserEventBus(ILogger<UserEventBus> logger) public UserEventBus(ILogger<UserEventBus> logger)
{ {
@ -44,10 +45,16 @@ namespace Selector.Events
LastfmCredChange?.Invoke(sender, args); LastfmCredChange?.Invoke(sender, args);
} }
public void OnCurrentlyPlayingChange(object sender, CurrentlyPlayingDTO args) public void OnCurrentlyPlayingChangeSpotify(object sender, CurrentlyPlayingDTO args)
{ {
Logger.LogTrace("Firing currently playing event [{usernamne}/{userId}]", args?.Username, args.UserId); Logger.LogTrace("Firing currently playing event [{usernamne}/{userId}]", args?.Username, args.UserId);
CurrentlyPlaying?.Invoke(sender, args); CurrentlyPlayingSpotify?.Invoke(sender, args);
}
public void OnCurrentlyPlayingChangeApple(object sender, AppleListeningChangeEventArgs args)
{
Logger.LogTrace("Firing currently playing event");
CurrentlyPlayingApple?.Invoke(sender, args);
} }
} }
} }

@ -64,6 +64,8 @@
</PropertyGroup> </PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' "> <PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
<CodesignKey>iPhone Developer</CodesignKey> <CodesignKey>iPhone Developer</CodesignKey>
<MtouchDebug>true</MtouchDebug>
<IOSDebugOverWiFi>true</IOSDebugOverWiFi>
</PropertyGroup> </PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Release' "> <PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
<CodesignKey>iPhone Developer</CodesignKey> <CodesignKey>iPhone Developer</CodesignKey>

@ -1,19 +1,14 @@
using System;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Design; using Microsoft.EntityFrameworkCore.Design;
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Logging.Abstractions;
namespace Selector.Model namespace Selector.Model
{ {
public class ApplicationDbContext : IdentityDbContext<ApplicationUser> public class ApplicationDbContext : IdentityDbContext<ApplicationUser>
{ {
private readonly ILogger<ApplicationDbContext> Logger; private readonly ILogger<ApplicationDbContext> Logger;
@ -27,7 +22,7 @@ namespace Selector.Model
public DbSet<SpotifyListen> SpotifyListen { get; set; } public DbSet<SpotifyListen> SpotifyListen { get; set; }
public ApplicationDbContext( public ApplicationDbContext(
DbContextOptions<ApplicationDbContext> options, DbContextOptions<ApplicationDbContext> options,
ILogger<ApplicationDbContext> logger ILogger<ApplicationDbContext> logger
) : base(options) ) : base(options)
{ {
@ -36,14 +31,14 @@ namespace Selector.Model
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{ {
} }
protected override void OnModelCreating(ModelBuilder modelBuilder) protected override void OnModelCreating(ModelBuilder modelBuilder)
{ {
base.OnModelCreating(modelBuilder); base.OnModelCreating(modelBuilder);
modelBuilder.HasCollation("case_insensitive", locale: "en-u-ks-primary", provider: "icu", deterministic: false); modelBuilder.HasCollation("case_insensitive", locale: "en-u-ks-primary", provider: "icu",
deterministic: false);
modelBuilder.Entity<ApplicationUser>() modelBuilder.Entity<ApplicationUser>()
.Property(u => u.SpotifyIsLinked) .Property(u => u.SpotifyIsLinked)
@ -112,15 +107,16 @@ namespace Selector.Model
public void CreatePlayerWatcher(string userId) public void CreatePlayerWatcher(string userId)
{ {
if(Watcher.Any(w => w.UserId == userId && w.Type == WatcherType.Player)) if (Watcher.Any(w => w.UserId == userId && w.Type == WatcherType.SpotifyPlayer))
{ {
Logger.LogWarning("Trying to create more than one player watcher for user [{id}]", userId); Logger.LogWarning("Trying to create more than one player watcher for user [{id}]", userId);
return; return;
} }
Watcher.Add(new Watcher { Watcher.Add(new Watcher
{
UserId = userId, UserId = userId,
Type = WatcherType.Player Type = WatcherType.SpotifyPlayer
}); });
SaveChanges(); SaveChanges();
@ -129,17 +125,18 @@ namespace Selector.Model
public class DesignTimeDbContextFactory : IDesignTimeDbContextFactory<ApplicationDbContext> public class DesignTimeDbContextFactory : IDesignTimeDbContextFactory<ApplicationDbContext>
{ {
private static string GetPath(string env) => $"{@Directory.GetCurrentDirectory()}/../Selector.Web/appsettings.{env}.json"; private static string GetPath(string env) =>
$"{@Directory.GetCurrentDirectory()}/../Selector.Web/appsettings.{env}.json";
public ApplicationDbContext CreateDbContext(string[] args) public ApplicationDbContext CreateDbContext(string[] args)
{ {
string configFile; string configFile;
if(File.Exists(GetPath("Development"))) if (File.Exists(GetPath("Development")))
{ {
configFile = GetPath("Development"); configFile = GetPath("Development");
} }
else if(File.Exists(GetPath("Production"))) else if (File.Exists(GetPath("Production")))
{ {
configFile = GetPath("Production"); configFile = GetPath("Production");
} }
@ -155,7 +152,7 @@ namespace Selector.Model
var builder = new DbContextOptionsBuilder<ApplicationDbContext>(); var builder = new DbContextOptionsBuilder<ApplicationDbContext>();
builder.UseNpgsql(configuration.GetConnectionString("Default")); builder.UseNpgsql(configuration.GetConnectionString("Default"));
return new ApplicationDbContext(builder.Options, NullLogger<ApplicationDbContext>.Instance); return new ApplicationDbContext(builder.Options, NullLogger<ApplicationDbContext>.Instance);
} }
} }

@ -0,0 +1,194 @@
using System.Collections.Generic;
using System.Linq;
using FluentAssertions;
using Selector.AppleMusic;
using Selector.AppleMusic.Watcher;
using Xunit;
namespace Selector.Tests.Apple;
public class AppleTimelineTests
{
public static IEnumerable<object[]> MatchingData =>
new List<object[]>
{
new object[]
{
new List<List<AppleMusicCurrentlyPlayingContext>>
{
new()
{
Helper.AppleContext("1"),
Helper.AppleContext("2"),
Helper.AppleContext("3"),
}
},
new List<string>
{
"1", "2", "3"
},
new List<List<string>>
{
new()
{
// "1", "2", "3"
}
}
},
new object[]
{
new List<List<AppleMusicCurrentlyPlayingContext>>
{
new()
{
Helper.AppleContext("1"),
Helper.AppleContext("2"),
Helper.AppleContext("3"),
},
new()
{
Helper.AppleContext("3"),
Helper.AppleContext("4"),
Helper.AppleContext("5"),
}
},
new List<string>
{
"1", "2", "3", "4", "5"
},
new List<List<string>>
{
new()
{
// "1", "2", "3"
},
new()
{
"4", "5",
}
}
},
new object[]
{
new List<List<AppleMusicCurrentlyPlayingContext>>
{
new()
{
Helper.AppleContext("1"),
Helper.AppleContext("2"),
Helper.AppleContext("3"),
},
new()
{
Helper.AppleContext("3"),
Helper.AppleContext("4"),
Helper.AppleContext("5"),
},
new()
{
Helper.AppleContext("3"),
Helper.AppleContext("4"),
Helper.AppleContext("5"),
},
new()
{
Helper.AppleContext("5"),
Helper.AppleContext("6"),
Helper.AppleContext("7"),
}
},
new List<string>
{
"1", "2", "3", "4", "5", "6", "7"
},
new List<List<string>>
{
new()
{
// "1", "2", "3"
},
new()
{
"4", "5",
},
new()
{
},
new()
{
"6", "7",
}
}
},
new object[]
{
new List<List<AppleMusicCurrentlyPlayingContext>>
{
new()
{
Helper.AppleContext("1"),
Helper.AppleContext("2"),
Helper.AppleContext("3"),
},
new()
{
Helper.AppleContext("3"),
Helper.AppleContext("4"),
Helper.AppleContext("5"),
},
new()
{
Helper.AppleContext("3"),
Helper.AppleContext("4"),
Helper.AppleContext("5"),
},
new()
{
Helper.AppleContext("1"),
Helper.AppleContext("2"),
Helper.AppleContext("3"),
}
},
new List<string>
{
"1", "2", "3", "4", "5"
},
new List<List<string>>
{
new()
{
// "1", "2", "3"
},
new()
{
"4", "5",
},
new()
{
},
new()
{
}
}
}
};
[Theory]
[MemberData(nameof(MatchingData))]
public void Matching(List<List<AppleMusicCurrentlyPlayingContext>> currentlyPlaying, List<string> expectedContent,
List<List<string>> expectedResult)
{
var timeline = new AppleTimeline();
foreach (var (batch, expectedReturn) in currentlyPlaying.Zip(expectedResult))
{
var newItems = timeline.Add(batch);
newItems.Select(x => x.Track.Id).Should().ContainInOrder(expectedReturn);
}
timeline
.Select(x => x.Item.Track.Id)
.Should()
.ContainInOrder(expectedContent);
}
}

@ -1,14 +1,8 @@
using System; using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Xunit;
using Moq;
using FluentAssertions;
using SpotifyAPI.Web;
using Selector;
using System.Threading; using System.Threading;
using Moq;
using SpotifyAPI.Web;
using Xunit;
namespace Selector.Tests namespace Selector.Tests
{ {
@ -17,7 +11,7 @@ namespace Selector.Tests
[Fact] [Fact]
public void Subscribe() public void Subscribe()
{ {
var watcherMock = new Mock<IPlayerWatcher>(); var watcherMock = new Mock<ISpotifyPlayerWatcher>();
var spotifyMock = new Mock<ITracksClient>(); var spotifyMock = new Mock<ITracksClient>();
var featureInjector = new AudioFeatureInjector(watcherMock.Object, spotifyMock.Object); var featureInjector = new AudioFeatureInjector(watcherMock.Object, spotifyMock.Object);
@ -30,7 +24,7 @@ namespace Selector.Tests
[Fact] [Fact]
public void Unsubscribe() public void Unsubscribe()
{ {
var watcherMock = new Mock<IPlayerWatcher>(); var watcherMock = new Mock<ISpotifyPlayerWatcher>();
var spotifyMock = new Mock<ITracksClient>(); var spotifyMock = new Mock<ITracksClient>();
var featureInjector = new AudioFeatureInjector(watcherMock.Object, spotifyMock.Object); var featureInjector = new AudioFeatureInjector(watcherMock.Object, spotifyMock.Object);
@ -43,8 +37,8 @@ namespace Selector.Tests
[Fact] [Fact]
public void SubscribeFuncArg() public void SubscribeFuncArg()
{ {
var watcherMock = new Mock<IPlayerWatcher>(); var watcherMock = new Mock<ISpotifyPlayerWatcher>();
var watcherFuncArgMock = new Mock<IPlayerWatcher>(); var watcherFuncArgMock = new Mock<ISpotifyPlayerWatcher>();
var spotifyMock = new Mock<ITracksClient>(); var spotifyMock = new Mock<ITracksClient>();
var featureInjector = new AudioFeatureInjector(watcherMock.Object, spotifyMock.Object); var featureInjector = new AudioFeatureInjector(watcherMock.Object, spotifyMock.Object);
@ -58,8 +52,8 @@ namespace Selector.Tests
[Fact] [Fact]
public void UnsubscribeFuncArg() public void UnsubscribeFuncArg()
{ {
var watcherMock = new Mock<IPlayerWatcher>(); var watcherMock = new Mock<ISpotifyPlayerWatcher>();
var watcherFuncArgMock = new Mock<IPlayerWatcher>(); var watcherFuncArgMock = new Mock<ISpotifyPlayerWatcher>();
var spotifyMock = new Mock<ITracksClient>(); var spotifyMock = new Mock<ITracksClient>();
var featureInjector = new AudioFeatureInjector(watcherMock.Object, spotifyMock.Object); var featureInjector = new AudioFeatureInjector(watcherMock.Object, spotifyMock.Object);
@ -73,7 +67,7 @@ namespace Selector.Tests
[Fact] [Fact]
public async void CallbackNoId() public async void CallbackNoId()
{ {
var watcherMock = new Mock<IPlayerWatcher>(); var watcherMock = new Mock<ISpotifyPlayerWatcher>();
var spotifyMock = new Mock<ITracksClient>(); var spotifyMock = new Mock<ITracksClient>();
var timelineMock = new Mock<AnalysedTrackTimeline>(); var timelineMock = new Mock<AnalysedTrackTimeline>();
var eventArgsMock = new Mock<ListeningChangeEventArgs>(); var eventArgsMock = new Mock<ListeningChangeEventArgs>();
@ -84,7 +78,8 @@ namespace Selector.Tests
eventArgsMock.Object.Current = playingMock.Object; eventArgsMock.Object.Current = playingMock.Object;
playingMock.Object.Item = trackMock.Object; playingMock.Object.Item = trackMock.Object;
spotifyMock.Setup(m => m.GetAudioFeatures(It.IsAny<string>(), It.IsAny<CancellationToken>()).Result).Returns(() => featureMock.Object); spotifyMock.Setup(m => m.GetAudioFeatures(It.IsAny<string>(), It.IsAny<CancellationToken>()).Result)
.Returns(() => featureMock.Object);
var featureInjector = new AudioFeatureInjector(watcherMock.Object, spotifyMock.Object) var featureInjector = new AudioFeatureInjector(watcherMock.Object, spotifyMock.Object)
{ {
@ -100,7 +95,7 @@ namespace Selector.Tests
[Fact] [Fact]
public async void CallbackWithId() public async void CallbackWithId()
{ {
var watcherMock = new Mock<IPlayerWatcher>(); var watcherMock = new Mock<ISpotifyPlayerWatcher>();
var spotifyMock = new Mock<ITracksClient>(); var spotifyMock = new Mock<ITracksClient>();
var timelineMock = new Mock<AnalysedTrackTimeline>(); var timelineMock = new Mock<AnalysedTrackTimeline>();
var eventArgsMock = new Mock<ListeningChangeEventArgs>(); var eventArgsMock = new Mock<ListeningChangeEventArgs>();
@ -112,7 +107,8 @@ namespace Selector.Tests
playingMock.Object.Item = trackMock.Object; playingMock.Object.Item = trackMock.Object;
trackMock.Object.Id = "Fake-Id"; trackMock.Object.Id = "Fake-Id";
spotifyMock.Setup(m => m.GetAudioFeatures(It.IsAny<string>(), It.IsAny<CancellationToken>()).Result).Returns(() => featureMock.Object); spotifyMock.Setup(m => m.GetAudioFeatures(It.IsAny<string>(), It.IsAny<CancellationToken>()).Result)
.Returns(() => featureMock.Object);
var featureInjector = new AudioFeatureInjector(watcherMock.Object, spotifyMock.Object) var featureInjector = new AudioFeatureInjector(watcherMock.Object, spotifyMock.Object)
{ {
@ -128,4 +124,4 @@ namespace Selector.Tests
timelineMock.VerifyNoOtherCalls(); timelineMock.VerifyNoOtherCalls();
} }
} }
} }

@ -1,18 +1,12 @@
using System; using System;
using System.Collections.Generic; using System.Net;
using System.Linq; using System.Net.Http;
using System.Text;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Net.Http; using FluentAssertions;
using Xunit;
using Moq; using Moq;
using Moq.Protected; using Moq.Protected;
using FluentAssertions; using Xunit;
using System.Net;
using SpotifyAPI.Web;
namespace Selector.Tests namespace Selector.Tests
{ {
@ -25,10 +19,11 @@ namespace Selector.Tests
var httpHandlerMock = new Mock<HttpMessageHandler>(); var httpHandlerMock = new Mock<HttpMessageHandler>();
httpHandlerMock.Protected() httpHandlerMock.Protected()
.Setup<Task<HttpResponseMessage>>("SendAsync", ItExpr.IsAny<HttpRequestMessage>(), ItExpr.IsAny<CancellationToken>()) .Setup<Task<HttpResponseMessage>>("SendAsync", ItExpr.IsAny<HttpRequestMessage>(),
ItExpr.IsAny<CancellationToken>())
.ReturnsAsync(msg); .ReturnsAsync(msg);
var watcherMock = new Mock<IPlayerWatcher>(); var watcherMock = new Mock<ISpotifyPlayerWatcher>();
watcherMock.SetupAdd(w => w.ItemChange += It.IsAny<EventHandler<ListeningChangeEventArgs>>()); watcherMock.SetupAdd(w => w.ItemChange += It.IsAny<EventHandler<ListeningChangeEventArgs>>());
watcherMock.SetupRemove(w => w.ItemChange -= It.IsAny<EventHandler<ListeningChangeEventArgs>>()); watcherMock.SetupRemove(w => w.ItemChange -= It.IsAny<EventHandler<ListeningChangeEventArgs>>());
@ -49,7 +44,8 @@ namespace Selector.Tests
await Task.Delay(100); await Task.Delay(100);
httpHandlerMock.Protected().Verify<Task<HttpResponseMessage>>("SendAsync", Times.Once(), ItExpr.IsAny<HttpRequestMessage>(), ItExpr.IsAny<CancellationToken>()); httpHandlerMock.Protected().Verify<Task<HttpResponseMessage>>("SendAsync", Times.Once(),
ItExpr.IsAny<HttpRequestMessage>(), ItExpr.IsAny<CancellationToken>());
} }
[Theory] [Theory]
@ -62,10 +58,11 @@ namespace Selector.Tests
var httpHandlerMock = new Mock<HttpMessageHandler>(); var httpHandlerMock = new Mock<HttpMessageHandler>();
httpHandlerMock.Protected() httpHandlerMock.Protected()
.Setup<Task<HttpResponseMessage>>("SendAsync", ItExpr.IsAny<HttpRequestMessage>(), ItExpr.IsAny<CancellationToken>()) .Setup<Task<HttpResponseMessage>>("SendAsync", ItExpr.IsAny<HttpRequestMessage>(),
ItExpr.IsAny<CancellationToken>())
.ReturnsAsync(msg); .ReturnsAsync(msg);
var watcherMock = new Mock<IPlayerWatcher>(); var watcherMock = new Mock<ISpotifyPlayerWatcher>();
var link = "https://link"; var link = "https://link";
var content = new StringContent(""); var content = new StringContent("");
@ -81,26 +78,17 @@ namespace Selector.Tests
var webHook = new WebHook(watcherMock.Object, http, config); var webHook = new WebHook(watcherMock.Object, http, config);
webHook.PredicatePass += (o, e) => webHook.PredicatePass += (o, e) => { predicateEvent = predicate; };
{
predicateEvent = predicate;
};
webHook.SuccessfulRequest += (o, e) => webHook.SuccessfulRequest += (o, e) => { successfulEvent = successful; };
{
successfulEvent = successful;
};
webHook.FailedRequest += (o, e) => webHook.FailedRequest += (o, e) => { failedEvent = !successful; };
{
failedEvent = !successful;
};
await webHook.AsyncCallback(ListeningChangeEventArgs.From(new (), new (), new())); await webHook.AsyncCallback(ListeningChangeEventArgs.From(new(), new(), new()));
predicateEvent.Should().Be(predicate); predicateEvent.Should().Be(predicate);
successfulEvent.Should().Be(successful); successfulEvent.Should().Be(successful);
failedEvent.Should().Be(!successful); failedEvent.Should().Be(!successful);
} }
} }
} }

@ -1,9 +1,7 @@
using System; using System.Collections.Generic;
using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Text; using Selector.AppleMusic.Model;
using System.Threading.Tasks; using Selector.AppleMusic.Watcher;
using SpotifyAPI.Web; using SpotifyAPI.Web;
namespace Selector.Tests namespace Selector.Tests
@ -12,8 +10,8 @@ namespace Selector.Tests
{ {
public static FullTrack FullTrack(string name, string album = "album name", List<string> artists = null) public static FullTrack FullTrack(string name, string album = "album name", List<string> artists = null)
{ {
if (artists is null) artists = new List<string>() {"artist"}; if (artists is null) artists = new List<string>() { "artist" };
return new FullTrack() return new FullTrack()
{ {
Name = name, Name = name,
@ -40,7 +38,7 @@ namespace Selector.Tests
public static SimpleAlbum SimpleAlbum(string name, List<string> artists = null) public static SimpleAlbum SimpleAlbum(string name, List<string> artists = null)
{ {
if (artists is null) artists = new List<string>() {"artist"}; if (artists is null) artists = new List<string>() { "artist" };
return new SimpleAlbum() return new SimpleAlbum()
{ {
Name = name, Name = name,
@ -83,7 +81,8 @@ namespace Selector.Tests
}; };
} }
public static CurrentlyPlayingContext CurrentPlayback(FullTrack track, Device device = null, bool isPlaying = true, string context = "context") public static CurrentlyPlayingContext CurrentPlayback(FullTrack track, Device device = null,
bool isPlaying = true, string context = "context")
{ {
return new CurrentlyPlayingContext() return new CurrentlyPlayingContext()
{ {
@ -94,7 +93,8 @@ namespace Selector.Tests
}; };
} }
public static CurrentlyPlaying CurrentlyPlaying(FullEpisode episode, bool isPlaying = true, string context = null) public static CurrentlyPlaying CurrentlyPlaying(FullEpisode episode, bool isPlaying = true,
string context = null)
{ {
return new CurrentlyPlaying() return new CurrentlyPlaying()
{ {
@ -104,7 +104,8 @@ namespace Selector.Tests
}; };
} }
public static CurrentlyPlayingContext CurrentPlayback(FullEpisode episode, Device device = null, bool isPlaying = true, string context = null) public static CurrentlyPlayingContext CurrentPlayback(FullEpisode episode, Device device = null,
bool isPlaying = true, string context = null)
{ {
return new CurrentlyPlayingContext() return new CurrentlyPlayingContext()
{ {
@ -148,5 +149,16 @@ namespace Selector.Tests
} }
}; };
} }
public static AppleMusicCurrentlyPlayingContext AppleContext(string id)
{
return new()
{
Track = new Track()
{
Id = id
}
};
}
} }
} }

@ -23,6 +23,7 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\Selector.AppleMusic\Selector.AppleMusic.csproj"/>
<ProjectReference Include="..\Selector\Selector.csproj" /> <ProjectReference Include="..\Selector\Selector.csproj" />
</ItemGroup> </ItemGroup>

@ -1,28 +1,28 @@
using System;
using System.Collections.Generic; using System.Collections.Generic;
using Xunit;
using Moq;
using FluentAssertions;
using SpotifyAPI.Web;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Xunit.Sdk; using FluentAssertions;
using Moq;
using SpotifyAPI.Web;
using Xunit;
namespace Selector.Tests namespace Selector.Tests
{ {
public class PlayerWatcherTests public class SpotifyPlayerWatcherTests
{ {
public static IEnumerable<object[]> NowPlayingData => public static IEnumerable<object[]> NowPlayingData =>
new List<object[]> new List<object[]>
{ {
new object[] { new List<CurrentlyPlayingContext>(){ new object[]
Helper.CurrentPlayback(Helper.FullTrack("track1", "album1", "artist1")), {
Helper.CurrentPlayback(Helper.FullTrack("track2", "album2", "artist2")), new List<CurrentlyPlayingContext>()
Helper.CurrentPlayback(Helper.FullTrack("track3", "album3", "artist3")), {
Helper.CurrentPlayback(Helper.FullTrack("track1", "album1", "artist1")),
Helper.CurrentPlayback(Helper.FullTrack("track2", "album2", "artist2")),
Helper.CurrentPlayback(Helper.FullTrack("track3", "album3", "artist3")),
}
} }
} };
};
[Theory] [Theory]
[MemberData(nameof(NowPlayingData))] [MemberData(nameof(NowPlayingData))]
@ -33,9 +33,10 @@ namespace Selector.Tests
var spotMock = new Mock<IPlayerClient>(); var spotMock = new Mock<IPlayerClient>();
var eq = new UriEqual(); var eq = new UriEqual();
spotMock.Setup(s => s.GetCurrentPlayback(It.IsAny<CancellationToken>()).Result).Returns(playingQueue.Dequeue); spotMock.Setup(s => s.GetCurrentPlayback(It.IsAny<CancellationToken>()).Result)
.Returns(playingQueue.Dequeue);
var watcher = new PlayerWatcher(spotMock.Object, eq); var watcher = new SpotifyPlayerWatcher(spotMock.Object, eq);
for (var i = 0; i < playing.Count; i++) for (var i = 0; i < playing.Count; i++)
{ {
@ -45,140 +46,209 @@ namespace Selector.Tests
} }
public static IEnumerable<object[]> EventsData => public static IEnumerable<object[]> EventsData =>
new List<object[]> new List<object[]>
{ {
// NO CHANGING // NO CHANGING
new object[] { new List<CurrentlyPlayingContext>(){ new object[]
Helper.CurrentPlayback(Helper.FullTrack("nochange", "album1", "artist1"), isPlaying: true, context: "context1"), {
Helper.CurrentPlayback(Helper.FullTrack("nochange", "album1", "artist1"), isPlaying: true, context: "context1"), new List<CurrentlyPlayingContext>()
Helper.CurrentPlayback(Helper.FullTrack("nochange", "album1", "artist1"), isPlaying: true, context: "context1"), {
Helper.CurrentPlayback(Helper.FullTrack("nochange", "album1", "artist1"), isPlaying: true,
context: "context1"),
Helper.CurrentPlayback(Helper.FullTrack("nochange", "album1", "artist1"), isPlaying: true,
context: "context1"),
Helper.CurrentPlayback(Helper.FullTrack("nochange", "album1", "artist1"), isPlaying: true,
context: "context1"),
},
// to raise
new List<string>()
{ "ItemChange", "ContextChange", "PlayingChange", "DeviceChange", "VolumeChange" },
// to not raise
new List<string>() { "AlbumChange", "ArtistChange" }
}, },
// to raise // TRACK CHANGE
new List<string>(){ "ItemChange", "ContextChange", "PlayingChange", "DeviceChange", "VolumeChange" }, new object[]
// to not raise {
new List<string>(){ "AlbumChange", "ArtistChange" } new List<CurrentlyPlayingContext>()
}, {
// TRACK CHANGE Helper.CurrentPlayback(Helper.FullTrack("trackchange1", "album1", "artist1")),
new object[] { new List<CurrentlyPlayingContext>(){ Helper.CurrentPlayback(Helper.FullTrack("trackchange2", "album1", "artist1"))
Helper.CurrentPlayback(Helper.FullTrack("trackchange1", "album1", "artist1")), },
Helper.CurrentPlayback(Helper.FullTrack("trackchange2", "album1", "artist1")) // to raise
new List<string>()
{ "ContextChange", "PlayingChange", "ItemChange", "DeviceChange", "VolumeChange" },
// to not raise
new List<string>() { "AlbumChange", "ArtistChange" }
}, },
// to raise // ALBUM CHANGE
new List<string>(){ "ContextChange", "PlayingChange", "ItemChange", "DeviceChange", "VolumeChange" }, new object[]
// to not raise {
new List<string>(){ "AlbumChange", "ArtistChange" } new List<CurrentlyPlayingContext>()
}, {
// ALBUM CHANGE Helper.CurrentPlayback(Helper.FullTrack("albumchange", "album1", "artist1")),
new object[] { new List<CurrentlyPlayingContext>(){ Helper.CurrentPlayback(Helper.FullTrack("albumchange", "album2", "artist1"))
Helper.CurrentPlayback(Helper.FullTrack("albumchange", "album1", "artist1")), },
Helper.CurrentPlayback(Helper.FullTrack("albumchange", "album2", "artist1")) // to raise
new List<string>()
{
"ContextChange", "PlayingChange", "ItemChange", "AlbumChange", "DeviceChange", "VolumeChange"
},
// to not raise
new List<string>() { "ArtistChange" }
}, },
// to raise // ARTIST CHANGE
new List<string>(){ "ContextChange", "PlayingChange", "ItemChange", "AlbumChange", "DeviceChange", "VolumeChange" }, new object[]
// to not raise {
new List<string>(){ "ArtistChange" } new List<CurrentlyPlayingContext>()
}, {
// ARTIST CHANGE Helper.CurrentPlayback(Helper.FullTrack("artistchange", "album1", "artist1")),
new object[] { new List<CurrentlyPlayingContext>(){ Helper.CurrentPlayback(Helper.FullTrack("artistchange", "album1", "artist2"))
Helper.CurrentPlayback(Helper.FullTrack("artistchange", "album1", "artist1")), },
Helper.CurrentPlayback(Helper.FullTrack("artistchange", "album1", "artist2")) // to raise
new List<string>()
{
"ContextChange", "PlayingChange", "ItemChange", "ArtistChange", "DeviceChange", "VolumeChange"
},
// to not raise
new List<string>() { "AlbumChange" }
}, },
// to raise // CONTEXT CHANGE
new List<string>(){ "ContextChange", "PlayingChange", "ItemChange", "ArtistChange", "DeviceChange", "VolumeChange" }, new object[]
// to not raise {
new List<string>(){ "AlbumChange" } new List<CurrentlyPlayingContext>()
}, {
// CONTEXT CHANGE Helper.CurrentPlayback(Helper.FullTrack("contextchange", "album1", "artist1"),
new object[] { new List<CurrentlyPlayingContext>(){ context: "context1"),
Helper.CurrentPlayback(Helper.FullTrack("contextchange", "album1", "artist1"), context: "context1"), Helper.CurrentPlayback(Helper.FullTrack("contextchange", "album1", "artist1"),
Helper.CurrentPlayback(Helper.FullTrack("contextchange", "album1", "artist1"), context: "context2") context: "context2")
},
// to raise
new List<string>()
{ "PlayingChange", "ItemChange", "ContextChange", "DeviceChange", "VolumeChange" },
// to not raise
new List<string>() { "AlbumChange", "ArtistChange" }
}, },
// to raise // PLAYING CHANGE
new List<string>(){ "PlayingChange", "ItemChange", "ContextChange", "DeviceChange", "VolumeChange" }, new object[]
// to not raise {
new List<string>(){ "AlbumChange", "ArtistChange" } new List<CurrentlyPlayingContext>()
}, {
// PLAYING CHANGE Helper.CurrentPlayback(Helper.FullTrack("playingchange1", "album1", "artist1"), isPlaying: true,
new object[] { new List<CurrentlyPlayingContext>(){ context: "context1"),
Helper.CurrentPlayback(Helper.FullTrack("playingchange1", "album1", "artist1"), isPlaying: true, context: "context1"), Helper.CurrentPlayback(Helper.FullTrack("playingchange1", "album1", "artist1"),
Helper.CurrentPlayback(Helper.FullTrack("playingchange1", "album1", "artist1"), isPlaying: false, context: "context1") isPlaying: false, context: "context1")
},
// to raise
new List<string>()
{ "ContextChange", "ItemChange", "PlayingChange", "DeviceChange", "VolumeChange" },
// to not raise
new List<string>() { "AlbumChange", "ArtistChange" }
}, },
// to raise // PLAYING CHANGE
new List<string>(){ "ContextChange", "ItemChange", "PlayingChange", "DeviceChange", "VolumeChange" }, new object[]
// to not raise {
new List<string>(){ "AlbumChange", "ArtistChange" } new List<CurrentlyPlayingContext>()
}, {
// PLAYING CHANGE Helper.CurrentPlayback(Helper.FullTrack("playingchange2", "album1", "artist1"),
new object[] { new List<CurrentlyPlayingContext>(){ isPlaying: false, context: "context1"),
Helper.CurrentPlayback(Helper.FullTrack("playingchange2", "album1", "artist1"), isPlaying: false, context: "context1"), Helper.CurrentPlayback(Helper.FullTrack("playingchange2", "album1", "artist1"), isPlaying: true,
Helper.CurrentPlayback(Helper.FullTrack("playingchange2", "album1", "artist1"), isPlaying: true, context: "context1") context: "context1")
},
// to raise
new List<string>()
{ "ContextChange", "ItemChange", "PlayingChange", "DeviceChange", "VolumeChange" },
// to not raise
new List<string>() { "AlbumChange", "ArtistChange" }
}, },
// to raise // CONTENT CHANGE
new List<string>(){ "ContextChange", "ItemChange", "PlayingChange", "DeviceChange", "VolumeChange" }, new object[]
// to not raise {
new List<string>(){ "AlbumChange", "ArtistChange" } new List<CurrentlyPlayingContext>()
}, {
// CONTENT CHANGE Helper.CurrentPlayback(Helper.FullTrack("contentchange1", "album1", "artist1"), isPlaying: true,
new object[] { new List<CurrentlyPlayingContext>(){ context: "context1"),
Helper.CurrentPlayback(Helper.FullTrack("contentchange1", "album1", "artist1"), isPlaying: true, context: "context1"), Helper.CurrentPlayback(Helper.FullEpisode("contentchange1", "show1", "pub1"), isPlaying: true,
Helper.CurrentPlayback(Helper.FullEpisode("contentchange1", "show1", "pub1"), isPlaying: true, context: "context2") context: "context2")
},
// to raise
new List<string>()
{
"PlayingChange", "ContentChange", "ContextChange", "ItemChange", "DeviceChange", "VolumeChange"
},
// to not raise
new List<string>() { "AlbumChange", "ArtistChange" }
}, },
// to raise // CONTENT CHANGE
new List<string>(){ "PlayingChange", "ContentChange", "ContextChange", "ItemChange", "DeviceChange", "VolumeChange" }, new object[]
// to not raise {
new List<string>(){ "AlbumChange", "ArtistChange" } new List<CurrentlyPlayingContext>()
}, {
// CONTENT CHANGE Helper.CurrentPlayback(Helper.FullEpisode("contentchange1", "show1", "pub1"), isPlaying: true,
new object[] { new List<CurrentlyPlayingContext>(){ context: "context2"),
Helper.CurrentPlayback(Helper.FullEpisode("contentchange1", "show1", "pub1"), isPlaying: true, context: "context2"), Helper.CurrentPlayback(Helper.FullTrack("contentchange1", "album1", "artist1"), isPlaying: true,
Helper.CurrentPlayback(Helper.FullTrack("contentchange1", "album1", "artist1"), isPlaying: true, context: "context1") context: "context1")
},
// to raise
new List<string>()
{
"PlayingChange", "ContentChange", "ContextChange", "ItemChange", "DeviceChange", "VolumeChange"
},
// to not raise
new List<string>() { "AlbumChange", "ArtistChange" }
}, },
// to raise // DEVICE CHANGE
new List<string>(){ "PlayingChange", "ContentChange", "ContextChange", "ItemChange", "DeviceChange", "VolumeChange" }, new object[]
// to not raise {
new List<string>(){ "AlbumChange", "ArtistChange" } new List<CurrentlyPlayingContext>()
}, {
// DEVICE CHANGE Helper.CurrentPlayback(Helper.FullTrack("devicechange", "album1", "artist1"),
new object[] { new List<CurrentlyPlayingContext>(){ device: Helper.Device("dev1")),
Helper.CurrentPlayback(Helper.FullTrack("devicechange", "album1", "artist1"), device: Helper.Device("dev1")), Helper.CurrentPlayback(Helper.FullTrack("devicechange", "album1", "artist1"),
Helper.CurrentPlayback(Helper.FullTrack("devicechange", "album1", "artist1"), device: Helper.Device("dev2")) device: Helper.Device("dev2"))
},
// to raise
new List<string>()
{ "ContextChange", "PlayingChange", "ItemChange", "VolumeChange", "DeviceChange" },
// to not raise
new List<string>() { "AlbumChange", "ArtistChange", "ContentChange" }
}, },
// to raise // VOLUME CHANGE
new List<string>(){ "ContextChange", "PlayingChange", "ItemChange", "VolumeChange", "DeviceChange" }, new object[]
// to not raise {
new List<string>(){ "AlbumChange", "ArtistChange", "ContentChange" } new List<CurrentlyPlayingContext>()
}, {
// VOLUME CHANGE Helper.CurrentPlayback(Helper.FullTrack("volumechange", "album1", "artist1"),
new object[] { new List<CurrentlyPlayingContext>(){ device: Helper.Device("dev1", volume: 50)),
Helper.CurrentPlayback(Helper.FullTrack("volumechange", "album1", "artist1"), device: Helper.Device("dev1", volume: 50)), Helper.CurrentPlayback(Helper.FullTrack("volumechange", "album1", "artist1"),
Helper.CurrentPlayback(Helper.FullTrack("volumechange", "album1", "artist1"), device: Helper.Device("dev1", volume: 60)) device: Helper.Device("dev1", volume: 60))
},
// to raise
new List<string>()
{ "ContextChange", "PlayingChange", "ItemChange", "VolumeChange", "DeviceChange" },
// to not raise
new List<string>() { "AlbumChange", "ArtistChange", "ContentChange" }
}, },
// to raise // // STARTED PLAYBACK
new List<string>(){ "ContextChange", "PlayingChange", "ItemChange", "VolumeChange", "DeviceChange" }, // new object[] { new List<CurrentlyPlayingContext>(){
// to not raise // null,
new List<string>(){ "AlbumChange", "ArtistChange", "ContentChange" } // Helper.CurrentPlayback(Helper.FullTrack("track1", "album1", "artist1"), isPlaying: true, context: "context1")
}, // },
// // STARTED PLAYBACK // // to raise
// new object[] { new List<CurrentlyPlayingContext>(){ // new List<string>(){ "PlayingChange" },
// null, // // to not raise
// Helper.CurrentPlayback(Helper.FullTrack("track1", "album1", "artist1"), isPlaying: true, context: "context1") // new List<string>(){ "AlbumChange", "ArtistChange", "ContentChange", "ContextChange", "ItemChange", "DeviceChange", "VolumeChange" }
// }, // },
// // to raise // // STARTED PLAYBACK
// new List<string>(){ "PlayingChange" }, // new object[] { new List<CurrentlyPlayingContext>(){
// // to not raise // Helper.CurrentPlayback(Helper.FullTrack("track1", "album1", "artist1"), isPlaying: true, context: "context1"),
// new List<string>(){ "AlbumChange", "ArtistChange", "ContentChange", "ContextChange", "ItemChange", "DeviceChange", "VolumeChange" } // null
// }, // },
// // STARTED PLAYBACK // // to raise
// new object[] { new List<CurrentlyPlayingContext>(){ // new List<string>(){ "PlayingChange" },
// Helper.CurrentPlayback(Helper.FullTrack("track1", "album1", "artist1"), isPlaying: true, context: "context1"), // // to not raise
// null // new List<string>(){ "AlbumChange", "ArtistChange", "ContentChange", "ContextChange", "ItemChange", "DeviceChange", "VolumeChange" }
// }, // }
// // to raise };
// new List<string>(){ "PlayingChange" },
// // to not raise
// new List<string>(){ "AlbumChange", "ArtistChange", "ContentChange", "ContextChange", "ItemChange", "DeviceChange", "VolumeChange" }
// }
};
[Theory] [Theory]
[MemberData(nameof(EventsData))] [MemberData(nameof(EventsData))]
@ -193,7 +263,7 @@ namespace Selector.Tests
s => s.GetCurrentPlayback(It.IsAny<CancellationToken>()).Result s => s.GetCurrentPlayback(It.IsAny<CancellationToken>()).Result
).Returns(playingQueue.Dequeue); ).Returns(playingQueue.Dequeue);
var watcher = new PlayerWatcher(spotMock.Object, eq); var watcher = new SpotifyPlayerWatcher(spotMock.Object, eq);
using var monitoredWatcher = watcher.Monitor(); using var monitoredWatcher = watcher.Monitor();
for (var i = 0; i < playing.Count; i++) for (var i = 0; i < playing.Count; i++)
@ -213,14 +283,14 @@ namespace Selector.Tests
{ {
var spotMock = new Mock<IPlayerClient>(); var spotMock = new Mock<IPlayerClient>();
var eq = new UriEqual(); var eq = new UriEqual();
var watch = new PlayerWatcher(spotMock.Object, eq) var watch = new SpotifyPlayerWatcher(spotMock.Object, eq)
{ {
PollPeriod = pollPeriod PollPeriod = pollPeriod
}; };
var tokenSource = new CancellationTokenSource(); var tokenSource = new CancellationTokenSource();
var task = watch.Watch(tokenSource.Token); var task = watch.Watch(tokenSource.Token);
await Task.Delay(execTime); await Task.Delay(execTime);
tokenSource.Cancel(); tokenSource.Cancel();
@ -238,4 +308,4 @@ namespace Selector.Tests
// await watch.Watch(token.Token); // await watch.Watch(token.Token);
// } // }
} }
} }

@ -1,7 +1,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Data.SqlTypes; using System.Data.SqlTypes;
using System.Diagnostics;
using System.Linq; using System.Linq;
using System.Text.Json; using System.Text.Json;
using System.Threading.Tasks; using System.Threading.Tasks;
@ -12,7 +11,6 @@ using Selector.Cache;
using Selector.Model; using Selector.Model;
using Selector.Model.Extensions; using Selector.Model.Extensions;
using Selector.SignalR; using Selector.SignalR;
using SpotifyAPI.Web;
using StackExchange.Redis; using StackExchange.Redis;
namespace Selector.Web.Hubs namespace Selector.Web.Hubs
@ -54,7 +52,7 @@ namespace Selector.Web.Hubs
public async Task SendNewPlaying() public async Task SendNewPlaying()
{ {
var nowPlaying = await Cache.StringGetAsync(Key.CurrentlyPlaying(Context.UserIdentifier)); var nowPlaying = await Cache.StringGetAsync(Key.CurrentlyPlayingSpotify(Context.UserIdentifier));
if (nowPlaying != RedisValue.Null) if (nowPlaying != RedisValue.Null)
{ {
var deserialised = JsonSerializer.Deserialize(nowPlaying, JsonContext.Default.CurrentlyPlayingDTO); var deserialised = JsonSerializer.Deserialize(nowPlaying, JsonContext.Default.CurrentlyPlayingDTO);
@ -67,16 +65,16 @@ namespace Selector.Web.Hubs
if (string.IsNullOrWhiteSpace(trackId)) return; if (string.IsNullOrWhiteSpace(trackId)) return;
var user = Db.Users var user = Db.Users
.AsNoTracking() .AsNoTracking()
.Where(u => u.Id == Context.UserIdentifier) .Where(u => u.Id == Context.UserIdentifier)
.SingleOrDefault() .SingleOrDefault()
?? throw new SqlNullValueException("No user returned"); ?? throw new SqlNullValueException("No user returned");
var watcher = Db.Watcher var watcher = Db.Watcher
.AsNoTracking() .AsNoTracking()
.Where(w => w.UserId == Context.UserIdentifier .Where(w => w.UserId == Context.UserIdentifier
&& w.Type == WatcherType.Player) && w.Type == WatcherType.SpotifyPlayer)
.SingleOrDefault() .SingleOrDefault()
?? throw new SqlNullValueException($"No player watcher found for [{user.UserName}]"); ?? throw new SqlNullValueException($"No player watcher found for [{user.UserName}]");
var feature = await AudioFeaturePuller.Get(user.SpotifyRefreshToken, trackId); var feature = await AudioFeaturePuller.Get(user.SpotifyRefreshToken, trackId);
@ -91,10 +89,10 @@ namespace Selector.Web.Hubs
if (PlayCountPuller is not null) if (PlayCountPuller is not null)
{ {
var user = Db.Users var user = Db.Users
.AsNoTracking() .AsNoTracking()
.Where(u => u.Id == Context.UserIdentifier) .Where(u => u.Id == Context.UserIdentifier)
.SingleOrDefault() .SingleOrDefault()
?? throw new SqlNullValueException("No user returned"); ?? throw new SqlNullValueException("No user returned");
if (user.LastFmConnected()) if (user.LastFmConnected())
{ {
@ -125,7 +123,8 @@ namespace Selector.Web.Hubs
if (user.ScrobbleSavingEnabled()) if (user.ScrobbleSavingEnabled())
{ {
var artistScrobbles = ScrobbleRepository.GetAll(userId: user.Id, artistName: artist, from: GetMaximumWindow()).ToArray(); var artistScrobbles = ScrobbleRepository
.GetAll(userId: user.Id, artistName: artist, from: GetMaximumWindow()).ToArray();
var artistDensity = artistScrobbles.Density(nowOptions.Value.ArtistDensityWindow); var artistDensity = artistScrobbles.Density(nowOptions.Value.ArtistDensityWindow);
var tasks = new List<Task>(3); var tasks = new List<Task>(3);
@ -138,7 +137,9 @@ namespace Selector.Web.Hubs
})); }));
} }
var albumDensity = artistScrobbles.Where(s => s.AlbumName.Equals(album, StringComparison.InvariantCultureIgnoreCase)).Density(nowOptions.Value.AlbumDensityWindow); var albumDensity = artistScrobbles
.Where(s => s.AlbumName.Equals(album, StringComparison.InvariantCultureIgnoreCase))
.Density(nowOptions.Value.AlbumDensityWindow);
if (albumDensity > nowOptions.Value.AlbumDensityThreshold) if (albumDensity > nowOptions.Value.AlbumDensityThreshold)
{ {
@ -148,7 +149,9 @@ namespace Selector.Web.Hubs
})); }));
} }
var trackDensity = artistScrobbles.Where(s => s.TrackName.Equals(track, StringComparison.InvariantCultureIgnoreCase)).Density(nowOptions.Value.TrackDensityWindow); var trackDensity = artistScrobbles
.Where(s => s.TrackName.Equals(track, StringComparison.InvariantCultureIgnoreCase))
.Density(nowOptions.Value.TrackDensityWindow);
if (albumDensity > nowOptions.Value.TrackDensityThreshold) if (albumDensity > nowOptions.Value.TrackDensityThreshold)
{ {
@ -165,7 +168,13 @@ namespace Selector.Web.Hubs
} }
} }
private DateTime GetMaximumWindow() => GetMaximumWindow(new TimeSpan[] { nowOptions.Value.ArtistDensityWindow, nowOptions.Value.AlbumDensityWindow, nowOptions.Value.TrackDensityWindow }); private DateTime GetMaximumWindow() => GetMaximumWindow(new TimeSpan[]
private DateTime GetMaximumWindow(IEnumerable<TimeSpan> windows) => windows.Select(w => DateTime.UtcNow - w).Min(); {
nowOptions.Value.ArtistDensityWindow, nowOptions.Value.AlbumDensityWindow,
nowOptions.Value.TrackDensityWindow
});
private DateTime GetMaximumWindow(IEnumerable<TimeSpan> windows) =>
windows.Select(w => DateTime.UtcNow - w).Min();
} }
} }

@ -1,14 +1,13 @@
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.SignalR; using Microsoft.AspNetCore.SignalR;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Selector.Web.Hubs;
using Selector.Events; using Selector.Events;
using Selector.SignalR; using Selector.SignalR;
using Selector.Web.Hubs;
namespace Selector.Web.Service namespace Selector.Web.Service
{ {
public class NowPlayingHubMapping: IEventHubMapping<NowPlayingHub, INowPlayingHubClient> public class NowPlayingHubMapping : IEventHubMapping<NowPlayingHub, INowPlayingHubClient>
{ {
private readonly ILogger<NowPlayingHubMapping> Logger; private readonly ILogger<NowPlayingHubMapping> Logger;
private readonly UserEventBus UserEvent; private readonly UserEventBus UserEvent;
@ -27,14 +26,21 @@ namespace Selector.Web.Service
{ {
Logger.LogDebug("Forming now playing event mapping between event bus and SignalR hub"); Logger.LogDebug("Forming now playing event mapping between event bus and SignalR hub");
UserEvent.CurrentlyPlaying += async (o, args) => UserEvent.CurrentlyPlayingSpotify += async (o, args) =>
{ {
Logger.LogDebug("Passing now playing event to SignalR hub [{userId}]", args.UserId); Logger.LogDebug("Passing now playing event to SignalR hub [{userId}]", args.UserId);
await Hub.Clients.User(args.UserId).OnNewPlaying(args); await Hub.Clients.User(args.UserId).OnNewPlaying(args);
}; };
// UserEvent.CurrentlyPlayingApple += async (o, args) =>
// {
// Logger.LogDebug("Passing now playing event to SignalR hub", args.UserId);
//
// await Hub.Clients.User(args.UserId).OnNewPlaying(args);
// };
return Task.CompletedTask; return Task.CompletedTask;
} }
} }
} }

@ -1,6 +1,4 @@
using System; using System;
using System.Collections.Generic;
using System.Text;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
@ -9,9 +7,9 @@ using SpotifyAPI.Web;
namespace Selector namespace Selector
{ {
public class AudioFeatureInjector : IPlayerConsumer public class AudioFeatureInjector : ISpotifyPlayerConsumer
{ {
protected readonly IPlayerWatcher Watcher; protected readonly ISpotifyPlayerWatcher Watcher;
protected readonly ITracksClient TrackClient; protected readonly ITracksClient TrackClient;
protected readonly ILogger<AudioFeatureInjector> Logger; protected readonly ILogger<AudioFeatureInjector> Logger;
@ -22,11 +20,12 @@ namespace Selector
public AnalysedTrackTimeline Timeline { get; set; } = new(); public AnalysedTrackTimeline Timeline { get; set; } = new();
public AudioFeatureInjector( public AudioFeatureInjector(
IPlayerWatcher watcher, ISpotifyPlayerWatcher watcher,
ITracksClient trackClient, ITracksClient trackClient,
ILogger<AudioFeatureInjector> logger = null, ILogger<AudioFeatureInjector> logger = null,
CancellationToken token = default CancellationToken token = default
){ )
{
Watcher = watcher; Watcher = watcher;
TrackClient = trackClient; TrackClient = trackClient;
Logger = logger ?? NullLogger<AudioFeatureInjector>.Instance; Logger = logger ?? NullLogger<AudioFeatureInjector>.Instance;
@ -37,7 +36,8 @@ namespace Selector
{ {
if (e.Current is null) return; if (e.Current is null) return;
Task.Run(async () => { Task.Run(async () =>
{
try try
{ {
await AsyncCallback(e); await AsyncCallback(e);
@ -57,10 +57,12 @@ namespace Selector
{ {
if (string.IsNullOrWhiteSpace(track.Id)) return; if (string.IsNullOrWhiteSpace(track.Id)) return;
try { try
{
Logger.LogTrace("Making Spotify call"); Logger.LogTrace("Making Spotify call");
var audioFeatures = await TrackClient.GetAudioFeatures(track.Id); var audioFeatures = await TrackClient.GetAudioFeatures(track.Id);
Logger.LogDebug("Adding audio features [{track}]: [{audio_features}]", track.DisplayString(), audioFeatures.DisplayString()); Logger.LogDebug("Adding audio features [{track}]: [{audio_features}]", track.DisplayString(),
audioFeatures.DisplayString());
var analysedTrack = AnalysedTrack.From(track, audioFeatures); var analysedTrack = AnalysedTrack.From(track, audioFeatures);
@ -103,10 +105,10 @@ namespace Selector
{ {
var watcher = watch ?? Watcher ?? throw new ArgumentNullException("No watcher provided"); var watcher = watch ?? Watcher ?? throw new ArgumentNullException("No watcher provided");
if (watcher is IPlayerWatcher watcherCast) if (watcher is ISpotifyPlayerWatcher watcherCast)
{ {
watcherCast.ItemChange += Callback; watcherCast.ItemChange += Callback;
} }
else else
{ {
throw new ArgumentException("Provided watcher is not a PlayerWatcher"); throw new ArgumentException("Provided watcher is not a PlayerWatcher");
@ -117,7 +119,7 @@ namespace Selector
{ {
var watcher = watch ?? Watcher ?? throw new ArgumentNullException("No watcher provided"); var watcher = watch ?? Watcher ?? throw new ArgumentNullException("No watcher provided");
if (watcher is IPlayerWatcher watcherCast) if (watcher is ISpotifyPlayerWatcher watcherCast)
{ {
watcherCast.ItemChange -= Callback; watcherCast.ItemChange -= Callback;
} }
@ -129,11 +131,12 @@ namespace Selector
protected virtual void OnNewFeature(AnalysedTrack args) protected virtual void OnNewFeature(AnalysedTrack args)
{ {
NewFeature?.Invoke(this, args); NewFeature?.Invoke(this, args);
} }
} }
public class AnalysedTrack { public class AnalysedTrack
{
public FullTrack Track { get; set; } public FullTrack Track { get; set; }
public TrackAudioFeatures Features { get; set; } public TrackAudioFeatures Features { get; set; }
@ -146,4 +149,4 @@ namespace Selector
}; };
} }
} }
} }

@ -1,10 +1,7 @@
using System; using System;
using System.Collections.Generic;
using System.Text;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using SpotifyAPI.Web; using SpotifyAPI.Web;
namespace Selector namespace Selector
@ -30,18 +27,18 @@ namespace Selector
Valence = 0.5f, Valence = 0.5f,
} }
}; };
private int _contextIdx = 0; private int _contextIdx = 0;
private DateTime _lastNext = DateTime.UtcNow; private DateTime _lastNext = DateTime.UtcNow;
private TimeSpan _contextLifespan = TimeSpan.FromSeconds(30); private TimeSpan _contextLifespan = TimeSpan.FromSeconds(30);
public DummyAudioFeatureInjector( public DummyAudioFeatureInjector(
IPlayerWatcher watcher, ISpotifyPlayerWatcher watcher,
ILogger<DummyAudioFeatureInjector> logger = null, ILogger<DummyAudioFeatureInjector> logger = null,
CancellationToken token = default CancellationToken token = default
): base (watcher, null, logger, token) ) : base(watcher, null, logger, token)
{ {
} }
private bool ShouldCycle() => DateTime.UtcNow - _lastNext > _contextLifespan; private bool ShouldCycle() => DateTime.UtcNow - _lastNext > _contextLifespan;
@ -98,4 +95,4 @@ namespace Selector
return Task.CompletedTask; return Task.CompletedTask;
} }
} }
} }

@ -1,20 +1,17 @@
using System; using System.Threading.Tasks;
using System.Collections.Generic;
using System.Text;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using SpotifyAPI.Web; using SpotifyAPI.Web;
namespace Selector namespace Selector
{ {
public interface IAudioFeatureInjectorFactory public interface IAudioFeatureInjectorFactory
{ {
public Task<IPlayerConsumer> Get(ISpotifyConfigFactory spotifyFactory, IPlayerWatcher watcher = null); public Task<ISpotifyPlayerConsumer> Get(ISpotifyConfigFactory spotifyFactory,
ISpotifyPlayerWatcher watcher = null);
} }
public class AudioFeatureInjectorFactory: IAudioFeatureInjectorFactory {
public class AudioFeatureInjectorFactory : IAudioFeatureInjectorFactory
{
private readonly ILoggerFactory LoggerFactory; private readonly ILoggerFactory LoggerFactory;
public AudioFeatureInjectorFactory(ILoggerFactory loggerFactory) public AudioFeatureInjectorFactory(ILoggerFactory loggerFactory)
@ -22,7 +19,8 @@ namespace Selector
LoggerFactory = loggerFactory; LoggerFactory = loggerFactory;
} }
public async Task<IPlayerConsumer> Get(ISpotifyConfigFactory spotifyFactory, IPlayerWatcher watcher = null) public async Task<ISpotifyPlayerConsumer> Get(ISpotifyConfigFactory spotifyFactory,
ISpotifyPlayerWatcher watcher = null)
{ {
if (!Magic.Dummy) if (!Magic.Dummy)
{ {
@ -44,4 +42,4 @@ namespace Selector
} }
} }
} }
} }

@ -1,41 +1,41 @@
using System; using System;
using System.Collections.Generic;
using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using IF.Lastfm.Core.Api; using IF.Lastfm.Core.Api;
using Microsoft.Extensions.Logging;
namespace Selector namespace Selector
{ {
public interface IPlayCounterFactory public interface IPlayCounterFactory
{ {
public Task<IPlayerConsumer> Get(LastfmClient fmClient = null, LastFmCredentials creds = null, IPlayerWatcher watcher = null); public Task<ISpotifyPlayerConsumer> Get(LastfmClient fmClient = null, LastFmCredentials creds = null,
ISpotifyPlayerWatcher watcher = null);
} }
public class PlayCounterFactory: IPlayCounterFactory {
public class PlayCounterFactory : IPlayCounterFactory
{
private readonly ILoggerFactory LoggerFactory; private readonly ILoggerFactory LoggerFactory;
private readonly LastfmClient Client; private readonly LastfmClient Client;
private readonly LastFmCredentials Creds; private readonly LastFmCredentials Creds;
public PlayCounterFactory(ILoggerFactory loggerFactory, LastfmClient client = null, LastFmCredentials creds = null) public PlayCounterFactory(ILoggerFactory loggerFactory, LastfmClient client = null,
LastFmCredentials creds = null)
{ {
LoggerFactory = loggerFactory; LoggerFactory = loggerFactory;
Client = client; Client = client;
Creds = creds; Creds = creds;
} }
public Task<IPlayerConsumer> Get(LastfmClient fmClient = null, LastFmCredentials creds = null, IPlayerWatcher watcher = null) public Task<ISpotifyPlayerConsumer> Get(LastfmClient fmClient = null, LastFmCredentials creds = null,
ISpotifyPlayerWatcher watcher = null)
{ {
var client = fmClient ?? Client; var client = fmClient ?? Client;
if(client is null) if (client is null)
{ {
throw new ArgumentNullException("No Last.fm client provided"); throw new ArgumentNullException("No Last.fm client provided");
} }
return Task.FromResult<IPlayerConsumer>(new PlayCounter( return Task.FromResult<ISpotifyPlayerConsumer>(new PlayCounter(
watcher, watcher,
client.Track, client.Track,
client.Album, client.Album,
@ -46,4 +46,4 @@ namespace Selector
)); ));
} }
} }
} }

@ -1,19 +1,15 @@
using System; using System.Net.Http;
using System.Collections.Generic;
using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using System.Net.Http;
namespace Selector namespace Selector
{ {
public interface IWebHookFactory public interface IWebHookFactory
{ {
public Task<WebHook> Get(WebHookConfig config, IPlayerWatcher watcher = null); public Task<WebHook> Get(WebHookConfig config, ISpotifyPlayerWatcher watcher = null);
} }
public class WebHookFactory: IWebHookFactory public class WebHookFactory : IWebHookFactory
{ {
private readonly ILoggerFactory LoggerFactory; private readonly ILoggerFactory LoggerFactory;
private readonly HttpClient Http; private readonly HttpClient Http;
@ -24,7 +20,7 @@ namespace Selector
Http = httpClient; Http = httpClient;
} }
public Task<WebHook> Get(WebHookConfig config, IPlayerWatcher watcher = null) public Task<WebHook> Get(WebHookConfig config, ISpotifyPlayerWatcher watcher = null)
{ {
return Task.FromResult(new WebHook( return Task.FromResult(new WebHook(
watcher, watcher,
@ -34,4 +30,4 @@ namespace Selector
)); ));
} }
} }
} }

@ -1,7 +1,4 @@
using System; namespace Selector
using System.Threading.Tasks;
namespace Selector
{ {
public interface IConsumer public interface IConsumer
{ {
@ -9,14 +6,16 @@ namespace Selector
public void Unsubscribe(IWatcher watch = null); public void Unsubscribe(IWatcher watch = null);
} }
public interface IConsumer<T>: IConsumer public interface IConsumer<T> : IConsumer
{ {
public void Callback(object sender, T e); public void Callback(object sender, T e);
} }
public interface IPlayerConsumer: IConsumer<ListeningChangeEventArgs> public interface ISpotifyPlayerConsumer : IConsumer<ListeningChangeEventArgs>
{ } {
}
public interface IPlaylistConsumer : IConsumer<PlaylistChangeEventArgs> public interface IPlaylistConsumer : IConsumer<PlaylistChangeEventArgs>
{ } {
} }
}

@ -1,21 +1,17 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Text;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using IF.Lastfm.Core.Api;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Logging.Abstractions;
using SpotifyAPI.Web; using SpotifyAPI.Web;
using IF.Lastfm.Core.Api;
using IF.Lastfm.Core.Objects;
using IF.Lastfm.Core.Api.Helpers;
namespace Selector namespace Selector
{ {
public class PlayCounter : IPlayerConsumer public class PlayCounter : ISpotifyPlayerConsumer
{ {
protected readonly IPlayerWatcher Watcher; protected readonly ISpotifyPlayerWatcher Watcher;
protected readonly ITrackApi TrackClient; protected readonly ITrackApi TrackClient;
protected readonly IAlbumApi AlbumClient; protected readonly IAlbumApi AlbumClient;
protected readonly IArtistApi ArtistClient; protected readonly IArtistApi ArtistClient;
@ -30,7 +26,7 @@ namespace Selector
public AnalysedTrackTimeline Timeline { get; set; } = new(); public AnalysedTrackTimeline Timeline { get; set; } = new();
public PlayCounter( public PlayCounter(
IPlayerWatcher watcher, ISpotifyPlayerWatcher watcher,
ITrackApi trackClient, ITrackApi trackClient,
IAlbumApi albumClient, IAlbumApi albumClient,
IArtistApi artistClient, IArtistApi artistClient,
@ -53,8 +49,9 @@ namespace Selector
public void Callback(object sender, ListeningChangeEventArgs e) public void Callback(object sender, ListeningChangeEventArgs e)
{ {
if (e.Current is null) return; if (e.Current is null) return;
Task.Run(async () => { Task.Run(async () =>
{
try try
{ {
await AsyncCallback(e); await AsyncCallback(e);
@ -68,7 +65,8 @@ namespace Selector
public async Task AsyncCallback(ListeningChangeEventArgs e) public async Task AsyncCallback(ListeningChangeEventArgs e)
{ {
using var scope = Logger.BeginScope(new Dictionary<string, object>() { { "spotify_username", e.SpotifyUsername }, { "id", e.Id }, { "username", Credentials.Username } }); using var scope = Logger.BeginScope(new Dictionary<string, object>()
{ { "spotify_username", e.SpotifyUsername }, { "id", e.Id }, { "username", Credentials.Username } });
if (Credentials is null || string.IsNullOrWhiteSpace(Credentials.Username)) if (Credentials is null || string.IsNullOrWhiteSpace(Credentials.Username))
{ {
@ -78,12 +76,15 @@ namespace Selector
if (e.Current.Item is FullTrack track) if (e.Current.Item is FullTrack track)
{ {
using var trackScope = Logger.BeginScope(new Dictionary<string, object>() { { "track", track.DisplayString() } }); using var trackScope = Logger.BeginScope(new Dictionary<string, object>()
{ { "track", track.DisplayString() } });
Logger.LogTrace("Making Last.fm call"); Logger.LogTrace("Making Last.fm call");
var trackInfo = TrackClient.GetInfoAsync(track.Name, track.Artists[0].Name, username: Credentials?.Username); var trackInfo =
var albumInfo = AlbumClient.GetInfoAsync(track.Album.Artists[0].Name, track.Album.Name, username: Credentials?.Username); TrackClient.GetInfoAsync(track.Name, track.Artists[0].Name, username: Credentials?.Username);
var albumInfo = AlbumClient.GetInfoAsync(track.Album.Artists[0].Name, track.Album.Name,
username: Credentials?.Username);
var artistInfo = ArtistClient.GetInfoAsync(track.Artists[0].Name); var artistInfo = ArtistClient.GetInfoAsync(track.Artists[0].Name);
var userInfo = UserClient.GetInfoAsync(Credentials.Username); var userInfo = UserClient.GetInfoAsync(Credentials.Username);
@ -104,7 +105,8 @@ namespace Selector
} }
else else
{ {
Logger.LogError(trackInfo.Exception, "Track info task faulted, [{context}]", e.Current.DisplayString()); Logger.LogError(trackInfo.Exception, "Track info task faulted, [{context}]",
e.Current.DisplayString());
} }
if (albumInfo.IsCompletedSuccessfully) if (albumInfo.IsCompletedSuccessfully)
@ -120,7 +122,8 @@ namespace Selector
} }
else else
{ {
Logger.LogError(albumInfo.Exception, "Album info task faulted, [{context}]", e.Current.DisplayString()); Logger.LogError(albumInfo.Exception, "Album info task faulted, [{context}]",
e.Current.DisplayString());
} }
//TODO: Add artist count //TODO: Add artist count
@ -138,10 +141,13 @@ namespace Selector
} }
else else
{ {
Logger.LogError(userInfo.Exception, "User info task faulted, [{context}]", e.Current.DisplayString()); Logger.LogError(userInfo.Exception, "User info task faulted, [{context}]",
e.Current.DisplayString());
} }
Logger.LogDebug("Adding Last.fm data [{username}], track: {track_count}, album: {album_count}, artist: {artist_count}, user: {user_count}", Credentials.Username, trackCount, albumCount, artistCount, userCount); Logger.LogDebug(
"Adding Last.fm data [{username}], track: {track_count}, album: {album_count}, artist: {artist_count}, user: {user_count}",
Credentials.Username, trackCount, albumCount, artistCount, userCount);
PlayCount playCount = new() PlayCount playCount = new()
{ {
@ -175,7 +181,7 @@ namespace Selector
{ {
var watcher = watch ?? Watcher ?? throw new ArgumentNullException("No watcher provided"); var watcher = watch ?? Watcher ?? throw new ArgumentNullException("No watcher provided");
if (watcher is IPlayerWatcher watcherCast) if (watcher is ISpotifyPlayerWatcher watcherCast)
{ {
watcherCast.ItemChange += Callback; watcherCast.ItemChange += Callback;
} }
@ -189,7 +195,7 @@ namespace Selector
{ {
var watcher = watch ?? Watcher ?? throw new ArgumentNullException("No watcher provided"); var watcher = watch ?? Watcher ?? throw new ArgumentNullException("No watcher provided");
if (watcher is IPlayerWatcher watcherCast) if (watcher is ISpotifyPlayerWatcher watcherCast)
{ {
watcherCast.ItemChange -= Callback; watcherCast.ItemChange -= Callback;
} }
@ -222,4 +228,4 @@ namespace Selector
{ {
public string Username { get; set; } public string Username { get; set; }
} }
} }

@ -1,8 +1,7 @@
using System; using System;
using System.Net.Http;
using System.Linq;
using System.Collections.Generic; using System.Collections.Generic;
using System.Text; using System.Linq;
using System.Net.Http;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
@ -19,7 +18,7 @@ namespace Selector
public bool ShouldRequest(ListeningChangeEventArgs e) public bool ShouldRequest(ListeningChangeEventArgs e)
{ {
if(Predicates is not null) if (Predicates is not null)
{ {
return Predicates.Select(p => p(e)).Aggregate((a, b) => a && b); return Predicates.Select(p => p(e)).Aggregate((a, b) => a && b);
} }
@ -30,9 +29,9 @@ namespace Selector
} }
} }
public class WebHook : IPlayerConsumer public class WebHook : ISpotifyPlayerConsumer
{ {
protected readonly IPlayerWatcher Watcher; protected readonly ISpotifyPlayerWatcher Watcher;
protected readonly HttpClient HttpClient; protected readonly HttpClient HttpClient;
protected readonly ILogger<WebHook> Logger; protected readonly ILogger<WebHook> Logger;
@ -47,7 +46,7 @@ namespace Selector
public AnalysedTrackTimeline Timeline { get; set; } = new(); public AnalysedTrackTimeline Timeline { get; set; } = new();
public WebHook( public WebHook(
IPlayerWatcher watcher, ISpotifyPlayerWatcher watcher,
HttpClient httpClient, HttpClient httpClient,
WebHookConfig config, WebHookConfig config,
ILogger<WebHook> logger = null, ILogger<WebHook> logger = null,
@ -64,8 +63,9 @@ namespace Selector
public void Callback(object sender, ListeningChangeEventArgs e) public void Callback(object sender, ListeningChangeEventArgs e)
{ {
if (e.Current is null) return; if (e.Current is null) return;
Task.Run(async () => { Task.Run(async () =>
{
try try
{ {
await AsyncCallback(e); await AsyncCallback(e);
@ -79,7 +79,11 @@ namespace Selector
public async Task AsyncCallback(ListeningChangeEventArgs e) public async Task AsyncCallback(ListeningChangeEventArgs e)
{ {
using var scope = Logger.BeginScope(new Dictionary<string, object>() { { "spotify_username", e.SpotifyUsername }, { "id", e.Id }, { "name", Config.Name }, { "url", Config.Url } }); using var scope = Logger.BeginScope(new Dictionary<string, object>()
{
{ "spotify_username", e.SpotifyUsername }, { "id", e.Id }, { "name", Config.Name },
{ "url", Config.Url }
});
if (Config.ShouldRequest(e)) if (Config.ShouldRequest(e))
{ {
@ -101,7 +105,7 @@ namespace Selector
OnFailedRequest(new EventArgs()); OnFailedRequest(new EventArgs());
} }
} }
catch(HttpRequestException ex) catch (HttpRequestException ex)
{ {
Logger.LogError(ex, "Exception occured during request"); Logger.LogError(ex, "Exception occured during request");
} }
@ -120,7 +124,7 @@ namespace Selector
{ {
var watcher = watch ?? Watcher ?? throw new ArgumentNullException("No watcher provided"); var watcher = watch ?? Watcher ?? throw new ArgumentNullException("No watcher provided");
if (watcher is IPlayerWatcher watcherCast) if (watcher is ISpotifyPlayerWatcher watcherCast)
{ {
watcherCast.ItemChange += Callback; watcherCast.ItemChange += Callback;
} }
@ -134,7 +138,7 @@ namespace Selector
{ {
var watcher = watch ?? Watcher ?? throw new ArgumentNullException("No watcher provided"); var watcher = watch ?? Watcher ?? throw new ArgumentNullException("No watcher provided");
if (watcher is IPlayerWatcher watcherCast) if (watcher is ISpotifyPlayerWatcher watcherCast)
{ {
watcherCast.ItemChange -= Callback; watcherCast.ItemChange -= Callback;
} }
@ -159,4 +163,4 @@ namespace Selector
FailedRequest?.Invoke(this, args); FailedRequest?.Invoke(this, args);
} }
} }
} }

@ -2,6 +2,8 @@ namespace Selector
{ {
public enum WatcherType public enum WatcherType
{ {
Player, Playlist SpotifyPlayer,
SpotifyPlaylist,
AppleMusicPlayer
} }
} }

@ -1,10 +1,6 @@
using System; using IF.Lastfm.Core.Api;
using System.Collections.Generic;
using System.Text;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using IF.Lastfm.Core.Api;
namespace Selector.Extensions namespace Selector.Extensions
{ {
public static class ServiceExtensions public static class ServiceExtensions
@ -52,10 +48,10 @@ namespace Selector.Extensions
public static IServiceCollection AddWatcher(this IServiceCollection services) public static IServiceCollection AddWatcher(this IServiceCollection services)
{ {
services.AddSingleton<IWatcherFactory, WatcherFactory>(); services.AddSingleton<ISpotifyWatcherFactory, SpotifyWatcherFactory>();
services.AddSingleton<IWatcherCollectionFactory, WatcherCollectionFactory>(); services.AddSingleton<IWatcherCollectionFactory, WatcherCollectionFactory>();
return services; return services;
} }
} }
} }

@ -0,0 +1,19 @@
using System.Collections.Generic;
using System.Linq;
using Microsoft.Extensions.Logging;
namespace Selector;
public abstract class BaseSpotifyWatcher(ILogger<BaseWatcher> logger = null) : BaseWatcher(logger)
{
public string SpotifyUsername { get; set; }
protected override Dictionary<string, object> LogScopeContext =>
new[]
{
base.LogScopeContext,
new Dictionary<string, object>() { { "spotify_username", SpotifyUsername } }
}
.SelectMany(x => x)
.ToDictionary();
}

@ -3,17 +3,15 @@ using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Logging.Abstractions;
namespace Selector namespace Selector
{ {
public abstract class BaseWatcher: IWatcher public abstract class BaseWatcher : IWatcher
{ {
protected readonly ILogger<BaseWatcher> Logger; protected readonly ILogger<BaseWatcher> Logger;
public string Id { get; set; } public string Id { get; set; }
public string SpotifyUsername { get; set; }
private Stopwatch ExecutionTimer { get; set; } private Stopwatch ExecutionTimer { get; set; }
public BaseWatcher(ILogger<BaseWatcher> logger = null) public BaseWatcher(ILogger<BaseWatcher> logger = null)
@ -25,13 +23,16 @@ namespace Selector
public abstract Task WatchOne(CancellationToken token); public abstract Task WatchOne(CancellationToken token);
public abstract Task Reset(); public abstract Task Reset();
protected virtual Dictionary<string, object> LogScopeContext => new()
{ { "id", Id } };
public async Task Watch(CancellationToken cancelToken) public async Task Watch(CancellationToken cancelToken)
{ {
using var logScope = Logger.BeginScope(new Dictionary<string, object>() { { "spotify_username", SpotifyUsername }, { "id", Id } }); using var logScope = Logger.BeginScope(LogScopeContext);
Logger.LogDebug("Starting watcher"); Logger.LogDebug("Starting watcher");
while (true) { while (true)
{
ExecutionTimer.Start(); ExecutionTimer.Start();
cancelToken.ThrowIfCancellationRequested(); cancelToken.ThrowIfCancellationRequested();
@ -49,16 +50,18 @@ namespace Selector
var waitTime = decimal.ToInt32(Math.Max(0, PollPeriod - ExecutionTimer.ElapsedMilliseconds)); var waitTime = decimal.ToInt32(Math.Max(0, PollPeriod - ExecutionTimer.ElapsedMilliseconds));
ExecutionTimer.Reset(); ExecutionTimer.Reset();
Logger.LogTrace("Finished watch one, delaying \"{poll_period}\"ms ({wait_time}ms)...", PollPeriod, waitTime); Logger.LogTrace("Finished watch one, delaying \"{poll_period}\"ms ({wait_time}ms)...", PollPeriod,
waitTime);
await Task.Delay(waitTime, cancelToken); await Task.Delay(waitTime, cancelToken);
} }
} }
private int _pollPeriod; private int _pollPeriod;
public int PollPeriod public int PollPeriod
{ {
get => _pollPeriod; get => _pollPeriod;
set => _pollPeriod = Math.Max(0, value); set => _pollPeriod = Math.Max(0, value);
} }
} }
} }

@ -1,14 +1,14 @@
using System; using System;
using Microsoft.Extensions.Logging;
using SpotifyAPI.Web;
using System.Collections.Generic; using System.Collections.Generic;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using SpotifyAPI.Web;
namespace Selector namespace Selector
{ {
public class DummyPlayerWatcher: PlayerWatcher public class DummySpotifyPlayerWatcher : SpotifyPlayerWatcher
{ {
private CurrentlyPlayingContext[] _contexts = new[] private CurrentlyPlayingContext[] _contexts = new[]
{ {
new CurrentlyPlayingContext new CurrentlyPlayingContext
@ -26,7 +26,6 @@ namespace Selector
ShuffleState = false, ShuffleState = false,
Context = new Context Context = new Context
{ {
}, },
IsPlaying = true, IsPlaying = true,
Item = new FullTrack Item = new FullTrack
@ -71,7 +70,6 @@ namespace Selector
ShuffleState = false, ShuffleState = false,
Context = new Context Context = new Context
{ {
}, },
IsPlaying = true, IsPlaying = true,
Item = new FullTrack Item = new FullTrack
@ -108,11 +106,11 @@ namespace Selector
private DateTime _lastNext = DateTime.UtcNow; private DateTime _lastNext = DateTime.UtcNow;
private TimeSpan _contextLifespan = TimeSpan.FromSeconds(30); private TimeSpan _contextLifespan = TimeSpan.FromSeconds(30);
public DummyPlayerWatcher(IEqual equalityChecker, public DummySpotifyPlayerWatcher(IEqual equalityChecker,
ILogger<DummyPlayerWatcher> logger = null, ILogger<DummySpotifyPlayerWatcher> logger = null,
int pollPeriod = 3000) : base(null, equalityChecker, logger, pollPeriod) int pollPeriod = 3000) : base(null, equalityChecker, logger, pollPeriod)
{ {
} }
private bool ShouldCycle() => DateTime.UtcNow - _lastNext > _contextLifespan; private bool ShouldCycle() => DateTime.UtcNow - _lastNext > _contextLifespan;
@ -122,7 +120,7 @@ namespace Selector
_contextIdx++; _contextIdx++;
if(_contextIdx >= _contexts.Length) if (_contextIdx >= _contexts.Length)
{ {
_contextIdx = 0; _contextIdx = 0;
} }
@ -141,7 +139,8 @@ namespace Selector
var polledCurrent = GetContext(); var polledCurrent = GetContext();
using var polledLogScope = Logger.BeginScope(new Dictionary<string, object>() { { "context", polledCurrent?.DisplayString() } }); using var polledLogScope = Logger.BeginScope(new Dictionary<string, object>()
{ { "context", polledCurrent?.DisplayString() } });
if (polledCurrent != null) StoreCurrentPlaying(polledCurrent); if (polledCurrent != null) StoreCurrentPlaying(polledCurrent);
@ -159,5 +158,4 @@ namespace Selector
return Task.CompletedTask; return Task.CompletedTask;
} }
} }
} }

@ -3,12 +3,13 @@ using SpotifyAPI.Web;
namespace Selector namespace Selector
{ {
public interface IPlayerWatcher: IWatcher public interface ISpotifyPlayerWatcher : IWatcher
{ {
/// <summary> /// <summary>
/// Track or episode changes /// Track or episode changes
/// </summary> /// </summary>
public event EventHandler<ListeningChangeEventArgs> NetworkPoll; public event EventHandler<ListeningChangeEventArgs> NetworkPoll;
public event EventHandler<ListeningChangeEventArgs> ItemChange; public event EventHandler<ListeningChangeEventArgs> ItemChange;
public event EventHandler<ListeningChangeEventArgs> AlbumChange; public event EventHandler<ListeningChangeEventArgs> AlbumChange;
public event EventHandler<ListeningChangeEventArgs> ArtistChange; public event EventHandler<ListeningChangeEventArgs> ArtistChange;
@ -23,6 +24,7 @@ namespace Selector
/// Last retrieved currently playing /// Last retrieved currently playing
/// </summary> /// </summary>
public CurrentlyPlayingContext Live { get; } public CurrentlyPlayingContext Live { get; }
public PlayerTimeline Past { get; } public PlayerTimeline Past { get; }
} }
} }

@ -1,13 +1,10 @@
using System; using System.Threading.Tasks;
using System.Collections.Generic;
using System.Text;
using System.Threading.Tasks;
namespace Selector namespace Selector
{ {
public interface IWatcherFactory public interface ISpotifyWatcherFactory
{ {
public Task<IWatcher> Get<T>(ISpotifyConfigFactory spotifyFactory, string id, int pollPeriod) public Task<IWatcher> Get<T>(ISpotifyConfigFactory spotifyFactory, string id, int pollPeriod)
where T : class, IWatcher; where T : class, IWatcher;
} }
} }

@ -1,13 +1,12 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using SpotifyAPI.Web;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Logging.Abstractions;
using System.Linq;
using Selector.Equality; using Selector.Equality;
using SpotifyAPI.Web;
namespace Selector namespace Selector
{ {
@ -17,7 +16,7 @@ namespace Selector
public bool PullTracks { get; set; } = true; public bool PullTracks { get; set; } = true;
} }
public class PlaylistWatcher: BaseWatcher, IPlaylistWatcher public class PlaylistWatcher : BaseSpotifyWatcher, IPlaylistWatcher
{ {
new private readonly ILogger<PlaylistWatcher> Logger; new private readonly ILogger<PlaylistWatcher> Logger;
private readonly ISpotifyClient spotifyClient; private readonly ISpotifyClient spotifyClient;
@ -44,11 +43,11 @@ namespace Selector
private IEqualityComparer<PlaylistTrack<IPlayableItem>> EqualityComparer = new PlayableItemEqualityComparer(); private IEqualityComparer<PlaylistTrack<IPlayableItem>> EqualityComparer = new PlayableItemEqualityComparer();
public PlaylistWatcher(PlaylistWatcherConfig config, public PlaylistWatcher(PlaylistWatcherConfig config,
ISpotifyClient spotifyClient, ISpotifyClient spotifyClient,
ILogger<PlaylistWatcher> logger = null, ILogger<PlaylistWatcher> logger = null,
int pollPeriod = 3000 int pollPeriod = 3000
) : base(logger) { ) : base(logger)
{
this.spotifyClient = spotifyClient; this.spotifyClient = spotifyClient;
this.config = config; this.config = config;
Logger = logger ?? NullLogger<PlaylistWatcher>.Instance; Logger = logger ?? NullLogger<PlaylistWatcher>.Instance;
@ -64,16 +63,18 @@ namespace Selector
return Task.CompletedTask; return Task.CompletedTask;
} }
public override async Task WatchOne(CancellationToken token = default) public override async Task WatchOne(CancellationToken token = default)
{ {
token.ThrowIfCancellationRequested(); token.ThrowIfCancellationRequested();
using var logScope = Logger.BeginScope(new Dictionary<string, object> { { "playlist_id", config.PlaylistId }, { "pull_tracks", config.PullTracks } }); using var logScope = Logger.BeginScope(new Dictionary<string, object>
{ { "playlist_id", config.PlaylistId }, { "pull_tracks", config.PullTracks } });
try{
try
{
string id; string id;
if(config.PlaylistId.Contains(':')) if (config.PlaylistId.Contains(':'))
{ {
id = config.PlaylistId.Split(':').Last(); id = config.PlaylistId.Split(':').Last();
} }
@ -97,18 +98,18 @@ namespace Selector
await CheckSnapshot(); await CheckSnapshot();
CheckStringValues(); CheckStringValues();
} }
catch(APIUnauthorizedException e) catch (APIUnauthorizedException e)
{ {
Logger.LogDebug("Unauthorised error: [{message}] (should be refreshed and retried?)", e.Message); Logger.LogDebug("Unauthorised error: [{message}] (should be refreshed and retried?)", e.Message);
//throw e; //throw e;
} }
catch(APITooManyRequestsException e) catch (APITooManyRequestsException e)
{ {
Logger.LogDebug("Too many requests error: [{message}]", e.Message); Logger.LogDebug("Too many requests error: [{message}]", e.Message);
await Task.Delay(e.RetryAfter, token); await Task.Delay(e.RetryAfter, token);
// throw e; // throw e;
} }
catch(APIException e) catch (APIException e)
{ {
Logger.LogDebug("API error: [{message}]", e.Message); Logger.LogDebug("API error: [{message}]", e.Message);
// throw e; // throw e;
@ -117,7 +118,7 @@ namespace Selector
private async Task CheckSnapshot() private async Task CheckSnapshot()
{ {
switch(Previous, Live) switch (Previous, Live)
{ {
case (null, not null): // gone null case (null, not null): // gone null
await PageLiveTracks(); await PageLiveTracks();
@ -128,13 +129,14 @@ namespace Selector
if (Live.SnapshotId != Previous.SnapshotId) if (Live.SnapshotId != Previous.SnapshotId)
{ {
Logger.LogDebug("Snapshot Id changed: {previous} -> {current}", Previous.SnapshotId, Live.SnapshotId); Logger.LogDebug("Snapshot Id changed: {previous} -> {current}", Previous.SnapshotId,
Live.SnapshotId);
await PageLiveTracks(); await PageLiveTracks();
OnSnapshotChange(GetEvent()); OnSnapshotChange(GetEvent());
} }
break; break;
} }
} }
private async Task PageLiveTracks() private async Task PageLiveTracks()
@ -171,7 +173,9 @@ namespace Selector
} }
} }
private PlaylistChangeEventArgs GetEvent() => PlaylistChangeEventArgs.From(Previous, Live, Past, tracks: CurrentTracks, addedTracks: LastAddedTracks, removedTracks: LastRemovedTracks, id: Id, username: SpotifyUsername); private PlaylistChangeEventArgs GetEvent() => PlaylistChangeEventArgs.From(Previous, Live, Past,
tracks: CurrentTracks, addedTracks: LastAddedTracks, removedTracks: LastRemovedTracks, id: Id,
username: SpotifyUsername);
private void CheckStringValues() private void CheckStringValues()
{ {
@ -183,11 +187,12 @@ namespace Selector
break; break;
case (not null, not null): // continuing non-null case (not null, not null): // continuing non-null
if((Previous, Live) is ({ Name: not null }, { Name: not null })) if ((Previous, Live) is ({ Name: not null }, { Name: not null }))
{ {
if (!Live.Name.Equals(Previous.Name)) if (!Live.Name.Equals(Previous.Name))
{ {
Logger.LogDebug("Name changed: {previous} -> {current}", Previous.SnapshotId, Live.SnapshotId); Logger.LogDebug("Name changed: {previous} -> {current}", Previous.SnapshotId,
Live.SnapshotId);
OnNameChanged(GetEvent()); OnNameChanged(GetEvent());
} }
} }
@ -196,7 +201,8 @@ namespace Selector
{ {
if (!Live.Description.Equals(Previous.Description)) if (!Live.Description.Equals(Previous.Description))
{ {
Logger.LogDebug("Description changed: {previous} -> {current}", Previous.SnapshotId, Live.SnapshotId); Logger.LogDebug("Description changed: {previous} -> {current}", Previous.SnapshotId,
Live.SnapshotId);
OnDescriptionChanged(GetEvent()); OnDescriptionChanged(GetEvent());
} }
} }
@ -209,12 +215,13 @@ namespace Selector
/// Store currently playing in last plays. Determine whether new list or appending required /// Store currently playing in last plays. Determine whether new list or appending required
/// </summary> /// </summary>
/// <param name="current">New currently playing to store</param> /// <param name="current">New currently playing to store</param>
private void StoreCurrentPlaying(FullPlaylist current) private void StoreCurrentPlaying(FullPlaylist current)
{ {
Past?.Add(current); Past?.Add(current);
} }
#region Event Firers #region Event Firers
protected virtual void OnNetworkPoll(PlaylistChangeEventArgs args) protected virtual void OnNetworkPoll(PlaylistChangeEventArgs args)
{ {
Logger.LogTrace("Firing network poll event"); Logger.LogTrace("Firing network poll event");
@ -226,7 +233,7 @@ namespace Selector
{ {
Logger.LogTrace("Firing snapshot change event"); Logger.LogTrace("Firing snapshot change event");
SnapshotChange?.Invoke(this, args); SnapshotChange?.Invoke(this, args);
} }
protected virtual void OnTracksAdded(PlaylistChangeEventArgs args) protected virtual void OnTracksAdded(PlaylistChangeEventArgs args)
@ -259,4 +266,4 @@ namespace Selector
#endregion #endregion
} }
} }

@ -2,16 +2,15 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using SpotifyAPI.Web;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Logging.Abstractions;
using SpotifyAPI.Web;
namespace Selector namespace Selector
{ {
public class PlayerWatcher: BaseWatcher, IPlayerWatcher public class SpotifyPlayerWatcher : BaseSpotifyWatcher, ISpotifyPlayerWatcher
{ {
new protected readonly ILogger<PlayerWatcher> Logger; new protected readonly ILogger<SpotifyPlayerWatcher> Logger;
private readonly IPlayerClient spotifyClient; private readonly IPlayerClient spotifyClient;
private readonly IEqual eq; private readonly IEqual eq;
@ -30,15 +29,15 @@ namespace Selector
protected CurrentlyPlayingContext Previous { get; set; } protected CurrentlyPlayingContext Previous { get; set; }
public PlayerTimeline Past { get; set; } = new(); public PlayerTimeline Past { get; set; } = new();
public PlayerWatcher(IPlayerClient spotifyClient, public SpotifyPlayerWatcher(IPlayerClient spotifyClient,
IEqual equalityChecker, IEqual equalityChecker,
ILogger<PlayerWatcher> logger = null, ILogger<SpotifyPlayerWatcher> logger = null,
int pollPeriod = 3000 int pollPeriod = 3000
) : base(logger) { ) : base(logger)
{
this.spotifyClient = spotifyClient; this.spotifyClient = spotifyClient;
eq = equalityChecker; eq = equalityChecker;
Logger = logger ?? NullLogger<PlayerWatcher>.Instance; Logger = logger ?? NullLogger<SpotifyPlayerWatcher>.Instance;
PollPeriod = pollPeriod; PollPeriod = pollPeriod;
} }
@ -51,15 +50,17 @@ namespace Selector
return Task.CompletedTask; return Task.CompletedTask;
} }
public override async Task WatchOne(CancellationToken token = default) public override async Task WatchOne(CancellationToken token = default)
{ {
token.ThrowIfCancellationRequested(); token.ThrowIfCancellationRequested();
try{ try
{
Logger.LogTrace("Making Spotify call"); Logger.LogTrace("Making Spotify call");
var polledCurrent = await spotifyClient.GetCurrentPlayback(); var polledCurrent = await spotifyClient.GetCurrentPlayback();
using var polledLogScope = Logger.BeginScope(new Dictionary<string, object>() { { "context", polledCurrent?.DisplayString() } }); using var polledLogScope = Logger.BeginScope(new Dictionary<string, object>()
{ { "context", polledCurrent?.DisplayString() } });
Logger.LogTrace("Received Spotify call"); Logger.LogTrace("Received Spotify call");
@ -75,20 +76,19 @@ namespace Selector
CheckContext(); CheckContext();
CheckItem(); CheckItem();
CheckDevice(); CheckDevice();
} }
catch(APIUnauthorizedException e) catch (APIUnauthorizedException e)
{ {
Logger.LogDebug("Unauthorised error: [{message}] (should be refreshed and retried?)", e.Message); Logger.LogDebug("Unauthorised error: [{message}] (should be refreshed and retried?)", e.Message);
//throw e; //throw e;
} }
catch(APITooManyRequestsException e) catch (APITooManyRequestsException e)
{ {
Logger.LogDebug("Too many requests error: [{message}]", e.Message); Logger.LogDebug("Too many requests error: [{message}]", e.Message);
await Task.Delay(e.RetryAfter, token); await Task.Delay(e.RetryAfter, token);
// throw e; // throw e;
} }
catch(APIException e) catch (APIException e)
{ {
Logger.LogDebug("API error: [{message}]", e.Message); Logger.LogDebug("API error: [{message}]", e.Message);
// throw e; // throw e;
@ -97,7 +97,6 @@ namespace Selector
protected void CheckItem() protected void CheckItem()
{ {
switch (Previous, Live) switch (Previous, Live)
{ {
case (null or { Item: null }, { Item: FullTrack track }): case (null or { Item: null }, { Item: FullTrack track }):
@ -127,38 +126,46 @@ namespace Selector
case ({ Item: FullTrack previousTrack }, { Item: FullTrack currentTrack }): case ({ Item: FullTrack previousTrack }, { Item: FullTrack currentTrack }):
if (!eq.IsEqual(previousTrack, currentTrack)) if (!eq.IsEqual(previousTrack, currentTrack))
{ {
Logger.LogDebug("Track changed: {prevTrack} -> {currentTrack}", previousTrack.DisplayString(), currentTrack.DisplayString()); Logger.LogDebug("Track changed: {prevTrack} -> {currentTrack}", previousTrack.DisplayString(),
currentTrack.DisplayString());
OnItemChange(GetEvent()); OnItemChange(GetEvent());
} }
if (!eq.IsEqual(previousTrack.Album, currentTrack.Album)) if (!eq.IsEqual(previousTrack.Album, currentTrack.Album))
{ {
Logger.LogDebug("Album changed: {previous} -> {current}", previousTrack.Album.DisplayString(), currentTrack.Album.DisplayString()); Logger.LogDebug("Album changed: {previous} -> {current}", previousTrack.Album.DisplayString(),
currentTrack.Album.DisplayString());
OnAlbumChange(GetEvent()); OnAlbumChange(GetEvent());
} }
if (!eq.IsEqual(previousTrack.Artists[0], currentTrack.Artists[0])) if (!eq.IsEqual(previousTrack.Artists[0], currentTrack.Artists[0]))
{ {
Logger.LogDebug("Artist changed: {previous} -> {current}", previousTrack.Artists.DisplayString(), currentTrack.Artists.DisplayString()); Logger.LogDebug("Artist changed: {previous} -> {current}",
previousTrack.Artists.DisplayString(), currentTrack.Artists.DisplayString());
OnArtistChange(GetEvent()); OnArtistChange(GetEvent());
} }
break; break;
case ({ Item: FullTrack previousTrack }, { Item: FullEpisode currentEp }): case ({ Item: FullTrack previousTrack }, { Item: FullEpisode currentEp }):
Logger.LogDebug("Media type changed: {previous}, {current}", previousTrack.DisplayString(), currentEp.DisplayString()); Logger.LogDebug("Media type changed: {previous}, {current}", previousTrack.DisplayString(),
currentEp.DisplayString());
OnContentChange(GetEvent()); OnContentChange(GetEvent());
OnItemChange(GetEvent()); OnItemChange(GetEvent());
break; break;
case ({ Item: FullEpisode previousEpisode }, { Item: FullTrack currentTrack }): case ({ Item: FullEpisode previousEpisode }, { Item: FullTrack currentTrack }):
Logger.LogDebug("Media type changed: {previous}, {current}", previousEpisode.DisplayString(), currentTrack.DisplayString()); Logger.LogDebug("Media type changed: {previous}, {current}", previousEpisode.DisplayString(),
currentTrack.DisplayString());
OnContentChange(GetEvent()); OnContentChange(GetEvent());
OnItemChange(GetEvent()); OnItemChange(GetEvent());
break; break;
case ({ Item: FullEpisode previousEp }, { Item: FullEpisode currentEp }): case ({ Item: FullEpisode previousEp }, { Item: FullEpisode currentEp }):
if (!eq.IsEqual(previousEp, currentEp)) if (!eq.IsEqual(previousEp, currentEp))
{ {
Logger.LogDebug("Podcast changed: {previous_ep} -> {current_ep}", previousEp.DisplayString(), currentEp.DisplayString()); Logger.LogDebug("Podcast changed: {previous_ep} -> {current_ep}", previousEp.DisplayString(),
currentEp.DisplayString());
OnItemChange(GetEvent()); OnItemChange(GetEvent());
} }
break; break;
} }
} }
@ -173,7 +180,8 @@ namespace Selector
} }
else if (!eq.IsEqual(Previous?.Context, Live?.Context)) else if (!eq.IsEqual(Previous?.Context, Live?.Context))
{ {
Logger.LogDebug("Context changed: {previous_context} -> {live_context}", Previous?.Context?.DisplayString() ?? "none", Live?.Context?.DisplayString() ?? "none"); Logger.LogDebug("Context changed: {previous_context} -> {live_context}",
Previous?.Context?.DisplayString() ?? "none", Live?.Context?.DisplayString() ?? "none");
OnContextChange(GetEvent()); OnContextChange(GetEvent());
} }
} }
@ -196,7 +204,8 @@ namespace Selector
// IS PLAYING // IS PLAYING
if (Previous?.IsPlaying != Live?.IsPlaying) if (Previous?.IsPlaying != Live?.IsPlaying)
{ {
Logger.LogDebug("Playing state changed: {previous_playing} -> {live_playing}", Previous?.IsPlaying, Live?.IsPlaying); Logger.LogDebug("Playing state changed: {previous_playing} -> {live_playing}", Previous?.IsPlaying,
Live?.IsPlaying);
OnPlayingChange(GetEvent()); OnPlayingChange(GetEvent());
} }
} }
@ -206,30 +215,34 @@ namespace Selector
// DEVICE // DEVICE
if (!eq.IsEqual(Previous?.Device, Live?.Device)) if (!eq.IsEqual(Previous?.Device, Live?.Device))
{ {
Logger.LogDebug("Device changed: {previous_device} -> {live_device}", Previous?.Device?.DisplayString() ?? "none", Live?.Device?.DisplayString() ?? "none"); Logger.LogDebug("Device changed: {previous_device} -> {live_device}",
Previous?.Device?.DisplayString() ?? "none", Live?.Device?.DisplayString() ?? "none");
OnDeviceChange(GetEvent()); OnDeviceChange(GetEvent());
} }
// VOLUME // VOLUME
if (Previous?.Device?.VolumePercent != Live?.Device?.VolumePercent) if (Previous?.Device?.VolumePercent != Live?.Device?.VolumePercent)
{ {
Logger.LogDebug("Volume changed: {previous_volume}% -> {live_volume}%", Previous?.Device?.VolumePercent, Live?.Device?.VolumePercent); Logger.LogDebug("Volume changed: {previous_volume}% -> {live_volume}%", Previous?.Device?.VolumePercent,
Live?.Device?.VolumePercent);
OnVolumeChange(GetEvent()); OnVolumeChange(GetEvent());
} }
} }
protected ListeningChangeEventArgs GetEvent() => ListeningChangeEventArgs.From(Previous, Live, Past, id: Id, username: SpotifyUsername); protected ListeningChangeEventArgs GetEvent() =>
ListeningChangeEventArgs.From(Previous, Live, Past, id: Id, username: SpotifyUsername);
/// <summary> /// <summary>
/// Store currently playing in last plays. Determine whether new list or appending required /// Store currently playing in last plays. Determine whether new list or appending required
/// </summary> /// </summary>
/// <param name="current">New currently playing to store</param> /// <param name="current">New currently playing to store</param>
protected void StoreCurrentPlaying(CurrentlyPlayingContext current) protected void StoreCurrentPlaying(CurrentlyPlayingContext current)
{ {
Past?.Add(current); Past?.Add(current);
} }
#region Event Firers #region Event Firers
protected virtual void OnNetworkPoll(ListeningChangeEventArgs args) protected virtual void OnNetworkPoll(ListeningChangeEventArgs args)
{ {
NetworkPoll?.Invoke(this, args); NetworkPoll?.Invoke(this, args);
@ -237,27 +250,27 @@ namespace Selector
protected virtual void OnItemChange(ListeningChangeEventArgs args) protected virtual void OnItemChange(ListeningChangeEventArgs args)
{ {
ItemChange?.Invoke(this, args); ItemChange?.Invoke(this, args);
} }
protected virtual void OnAlbumChange(ListeningChangeEventArgs args) protected virtual void OnAlbumChange(ListeningChangeEventArgs args)
{ {
AlbumChange?.Invoke(this, args); AlbumChange?.Invoke(this, args);
} }
protected virtual void OnArtistChange(ListeningChangeEventArgs args) protected virtual void OnArtistChange(ListeningChangeEventArgs args)
{ {
ArtistChange?.Invoke(this, args); ArtistChange?.Invoke(this, args);
} }
protected virtual void OnContextChange(ListeningChangeEventArgs args) protected virtual void OnContextChange(ListeningChangeEventArgs args)
{ {
ContextChange?.Invoke(this, args); ContextChange?.Invoke(this, args);
} }
protected virtual void OnContentChange(ListeningChangeEventArgs args) protected virtual void OnContentChange(ListeningChangeEventArgs args)
{ {
ContentChange?.Invoke(this, args); ContentChange?.Invoke(this, args);
} }
protected virtual void OnVolumeChange(ListeningChangeEventArgs args) protected virtual void OnVolumeChange(ListeningChangeEventArgs args)
@ -267,14 +280,14 @@ namespace Selector
protected virtual void OnDeviceChange(ListeningChangeEventArgs args) protected virtual void OnDeviceChange(ListeningChangeEventArgs args)
{ {
DeviceChange?.Invoke(this, args); DeviceChange?.Invoke(this, args);
} }
protected virtual void OnPlayingChange(ListeningChangeEventArgs args) protected virtual void OnPlayingChange(ListeningChangeEventArgs args)
{ {
PlayingChange?.Invoke(this, args); PlayingChange?.Invoke(this, args);
} }
#endregion #endregion
} }
} }

@ -1,6 +1,4 @@
using System; using System;
using System.Collections.Generic;
using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Logging.Abstractions;
@ -8,23 +6,24 @@ using SpotifyAPI.Web;
namespace Selector namespace Selector
{ {
public class WatcherFactory : IWatcherFactory { public class SpotifyWatcherFactory : ISpotifyWatcherFactory
{
private readonly ILoggerFactory LoggerFactory; private readonly ILoggerFactory LoggerFactory;
private readonly IEqual Equal; private readonly IEqual Equal;
public WatcherFactory(ILoggerFactory loggerFactory, IEqual equal) public SpotifyWatcherFactory(ILoggerFactory loggerFactory, IEqual equal)
{ {
LoggerFactory = loggerFactory; LoggerFactory = loggerFactory;
Equal = equal; Equal = equal;
} }
public async Task<IWatcher> Get<T>(ISpotifyConfigFactory spotifyFactory, string id = null, int pollPeriod = 3000) public async Task<IWatcher> Get<T>(ISpotifyConfigFactory spotifyFactory, string id = null,
int pollPeriod = 3000)
where T : class, IWatcher where T : class, IWatcher
{ {
if(typeof(T).IsAssignableFrom(typeof(PlayerWatcher))) if (typeof(T).IsAssignableFrom(typeof(SpotifyPlayerWatcher)))
{ {
if(!Magic.Dummy) if (!Magic.Dummy)
{ {
var config = await spotifyFactory.GetConfig(); var config = await spotifyFactory.GetConfig();
var client = new SpotifyClient(config); var client = new SpotifyClient(config);
@ -32,10 +31,11 @@ namespace Selector
// TODO: catch spotify exceptions // TODO: catch spotify exceptions
var user = await client.UserProfile.Current(); var user = await client.UserProfile.Current();
return new PlayerWatcher( return new SpotifyPlayerWatcher(
client.Player, client.Player,
Equal, Equal,
LoggerFactory?.CreateLogger<PlayerWatcher>() ?? NullLogger<PlayerWatcher>.Instance, LoggerFactory?.CreateLogger<SpotifyPlayerWatcher>() ??
NullLogger<SpotifyPlayerWatcher>.Instance,
pollPeriod: pollPeriod pollPeriod: pollPeriod
) )
{ {
@ -45,9 +45,10 @@ namespace Selector
} }
else else
{ {
return new DummyPlayerWatcher( return new DummySpotifyPlayerWatcher(
Equal, Equal,
LoggerFactory?.CreateLogger<DummyPlayerWatcher>() ?? NullLogger<DummyPlayerWatcher>.Instance, LoggerFactory?.CreateLogger<DummySpotifyPlayerWatcher>() ??
NullLogger<DummySpotifyPlayerWatcher>.Instance,
pollPeriod: pollPeriod pollPeriod: pollPeriod
) )
{ {
@ -81,4 +82,4 @@ namespace Selector
} }
} }
} }
} }