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