adding apple music scrobbling, warning fixing
This commit is contained in:
parent
7c6f82fd7b
commit
0d7b23d9ea
Dockerfile.CLIDockerfile.Web
Selector.AppleMusic
AppleMusicApi.csAppleTimeline.cs
Consumer
CurrentlyPlayingDTO.csEvents.csExtensions
Model
Selector.AppleMusic.csprojWatcher
Selector.CLI
Selector.Cache
Selector.Data
Selector.Event
Selector.LastFm
Selector.MAUI
Selector.Model
Selector.SignalR
Selector.Spotify
Consumer
Equality
Events.csExtensions
JsonContext.csSelector.Spotify.csprojTimeline
Watcher
Selector.Tests
Selector.Web
Selector
@ -3,6 +3,8 @@ FROM mcr.microsoft.com/dotnet/sdk:9.0 AS base
|
||||
COPY *.sln .
|
||||
COPY Selector/*.csproj ./Selector/
|
||||
COPY Selector.AppleMusic/*.csproj ./Selector.AppleMusic/
|
||||
COPY Selector.Spotify/*.csproj ./Selector.Spotify/
|
||||
COPY Selector.LastFm/*.csproj ./Selector.LastFm/
|
||||
COPY Selector.Cache/*.csproj ./Selector.Cache/
|
||||
COPY Selector.Data/*.csproj ./Selector.Data/
|
||||
COPY Selector.Event/*.csproj ./Selector.Event/
|
||||
|
@ -11,6 +11,8 @@ FROM mcr.microsoft.com/dotnet/sdk:9.0 AS base
|
||||
COPY *.sln .
|
||||
COPY Selector/*.csproj ./Selector/
|
||||
COPY Selector.AppleMusic/*.csproj ./Selector.AppleMusic/
|
||||
COPY Selector.Spotify/*.csproj ./Selector.Spotify/
|
||||
COPY Selector.LastFm/*.csproj ./Selector.LastFm/
|
||||
COPY Selector.Cache/*.csproj ./Selector.Cache/
|
||||
COPY Selector.Data/*.csproj ./Selector.Data/
|
||||
COPY Selector.Event/*.csproj ./Selector.Event/
|
||||
|
@ -43,7 +43,7 @@ public class AppleMusicApi(HttpClient client, string developerToken, string user
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<RecentlyPlayedTracksResponse> GetRecentlyPlayedTracks()
|
||||
public async Task<RecentlyPlayedTracksResponse?> GetRecentlyPlayedTracks()
|
||||
{
|
||||
var response = await MakeRequest(HttpMethod.Get, "/me/recent/played/tracks");
|
||||
|
||||
|
@ -1,3 +1,4 @@
|
||||
using System.Collections.Concurrent;
|
||||
using Selector.AppleMusic.Model;
|
||||
using Selector.AppleMusic.Watcher;
|
||||
|
||||
@ -26,6 +27,7 @@ public class AppleTimeline : Timeline<AppleMusicCurrentlyPlayingContext>
|
||||
{
|
||||
Recent.AddRange(items.Select(x =>
|
||||
TimelineItem<AppleMusicCurrentlyPlayingContext>.From(x, DateTime.UtcNow)));
|
||||
Recent.ForEach(x => x.Item.Scrobbled = true);
|
||||
return newItems;
|
||||
}
|
||||
|
||||
@ -37,34 +39,44 @@ public class AppleTimeline : Timeline<AppleMusicCurrentlyPlayingContext>
|
||||
return newItems;
|
||||
}
|
||||
|
||||
var (found, startIdx) = Loop(items, 0);
|
||||
|
||||
TimelineItem<AppleMusicCurrentlyPlayingContext>? popped = null;
|
||||
if (found == 0)
|
||||
var dict = new ConcurrentDictionary<int, (int, int)>();
|
||||
Parallel.ForEach(Enumerable.Range(0, 3), idx =>
|
||||
{
|
||||
var (foundOffseted, startIdxOffseted) = Loop(items, 1);
|
||||
var (found, startIdx) = Loop(items, idx);
|
||||
dict.TryAdd(idx, (found, startIdx));
|
||||
});
|
||||
|
||||
if (foundOffseted > found)
|
||||
int maxFound = dict[0].Item1;
|
||||
int storedIdx = 0;
|
||||
int startIdx = dict[0].Item2;
|
||||
foreach (var item in dict)
|
||||
{
|
||||
if (item.Value.Item1 > maxFound)
|
||||
{
|
||||
popped = Recent[^1];
|
||||
Recent.RemoveAt(Recent.Count - 1);
|
||||
|
||||
startIdx = startIdxOffseted;
|
||||
storedIdx = item.Key;
|
||||
maxFound = item.Value.Item1;
|
||||
startIdx = item.Value.Item2;
|
||||
}
|
||||
}
|
||||
|
||||
var popped = new List<AppleMusicCurrentlyPlayingContext>();
|
||||
popped.AddRange(Recent.TakeLast(storedIdx).Select(x => x.Item));
|
||||
|
||||
foreach (var item in items.TakeLast(startIdx))
|
||||
{
|
||||
newItems.Add(item);
|
||||
Recent.Add(TimelineItem<AppleMusicCurrentlyPlayingContext>.From(item, DateTime.UtcNow));
|
||||
}
|
||||
|
||||
if (popped is not null)
|
||||
if (popped.Any())
|
||||
{
|
||||
var idx = newItems.FindIndex(x => x.Track.Id == popped.Item.Track.Id);
|
||||
if (idx >= 0)
|
||||
foreach (var item in popped)
|
||||
{
|
||||
newItems.RemoveAt(idx);
|
||||
var idx = newItems.FindIndex(x => x.Track.Id == item.Track.Id);
|
||||
if (idx >= 0)
|
||||
{
|
||||
newItems.RemoveAt(idx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -116,7 +128,7 @@ public class AppleTimeline : Timeline<AppleMusicCurrentlyPlayingContext>
|
||||
{
|
||||
// good, keep going
|
||||
found++;
|
||||
if (found >= 3)
|
||||
if (found >= 4)
|
||||
{
|
||||
stop = true;
|
||||
break;
|
||||
|
119
Selector.AppleMusic/Consumer/AppleMusicScrobbler.cs
Normal file
119
Selector.AppleMusic/Consumer/AppleMusicScrobbler.cs
Normal file
@ -0,0 +1,119 @@
|
||||
using IF.Lastfm.Core.Api;
|
||||
using IF.Lastfm.Core.Objects;
|
||||
using IF.Lastfm.Core.Scrobblers;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Selector.AppleMusic.Consumer;
|
||||
|
||||
public class AppleMusicScrobbler :
|
||||
BaseSequentialPlayerConsumer<IAppleMusicPlayerWatcher, AppleListeningChangeEventArgs>, IApplePlayerConsumer
|
||||
{
|
||||
public CancellationToken CancelToken { get; set; }
|
||||
private readonly SemaphoreSlim _lock = new SemaphoreSlim(1, 1);
|
||||
private readonly IScrobbler _scrobbler;
|
||||
private readonly LastfmClient _lastClient;
|
||||
|
||||
public AppleMusicScrobbler(IAppleMusicPlayerWatcher? watcher,
|
||||
LastfmClient LastClient,
|
||||
ILogger<AppleMusicScrobbler> logger,
|
||||
CancellationToken token = default) : base(watcher, logger)
|
||||
{
|
||||
CancelToken = token;
|
||||
|
||||
_lastClient = LastClient;
|
||||
_scrobbler = _lastClient.Scrobbler;
|
||||
}
|
||||
|
||||
public async Task Auth(string lastfmUsername,
|
||||
string lastfmPassword)
|
||||
{
|
||||
var response = await _lastClient.Auth.GetSessionTokenAsync(lastfmUsername, lastfmPassword);
|
||||
|
||||
if (response.Success)
|
||||
{
|
||||
Logger.LogInformation("[{username}] Successfully authenticated to Last.fm for Apple Music scrobbling",
|
||||
lastfmUsername);
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.LogError("[{username}] Failed to authenticate to Last.fm for Apple Music scrobbling ({})",
|
||||
lastfmUsername, response.Status);
|
||||
}
|
||||
}
|
||||
|
||||
protected override async Task ProcessEvent(AppleListeningChangeEventArgs e)
|
||||
{
|
||||
await _lock.WaitAsync(CancelToken);
|
||||
|
||||
try
|
||||
{
|
||||
var lastScrobbled = e.Timeline.LastOrDefault(t => t.Item.Scrobbled);
|
||||
var toScrobble = e.Timeline.Where(e => !e.Item.Scrobbled).ToList();
|
||||
|
||||
Logger.LogInformation("Sending any cached Apple Music scrobbles");
|
||||
var response = await _scrobbler.SendCachedScrobblesAsync();
|
||||
if (response.Success)
|
||||
{
|
||||
Logger.LogInformation("Sent [{}] cached Apple Music scrobbles", response.AcceptedCount);
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.LogError(response.Exception, "Failed to send cached Apple Music scrobbles");
|
||||
}
|
||||
|
||||
if (!toScrobble.Any()) return;
|
||||
|
||||
var scrobbleTimes = new List<DateTimeOffset>();
|
||||
if (lastScrobbled != null)
|
||||
{
|
||||
var times = (toScrobble.Last().Time - lastScrobbled.Time).Seconds;
|
||||
var intervals = times / (toScrobble.Count + 1);
|
||||
|
||||
foreach (var interval in Enumerable.Range(0, toScrobble.Count))
|
||||
{
|
||||
scrobbleTimes.Add(toScrobble.Last().Time - TimeSpan.FromSeconds(interval * int.Max(31, intervals)));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (var interval in Enumerable.Range(0, toScrobble.Count))
|
||||
{
|
||||
scrobbleTimes.Add(toScrobble.Last().Time - TimeSpan.FromSeconds(interval * 31));
|
||||
}
|
||||
}
|
||||
|
||||
scrobbleTimes.Reverse();
|
||||
|
||||
Logger.LogInformation("Sending scrobbles for [{}]",
|
||||
string.Join(", ", toScrobble.Select(x => x.Item.Track)));
|
||||
|
||||
var scrobbleResponse = await _scrobbler.ScrobbleAsync(
|
||||
toScrobble.Zip(scrobbleTimes)
|
||||
.Select((s) =>
|
||||
new Scrobble(
|
||||
s.First.Item.Track.Attributes.ArtistName,
|
||||
s.First.Item.Track.Attributes.AlbumName,
|
||||
s.First.Item.Track.Attributes.Name,
|
||||
s.Second)));
|
||||
|
||||
foreach (var track in toScrobble)
|
||||
{
|
||||
track.Item.Scrobbled = true;
|
||||
}
|
||||
|
||||
if (scrobbleResponse.Success)
|
||||
{
|
||||
Logger.LogInformation("Sent [{}] Apple Music scrobbles", response.AcceptedCount);
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.LogError(response.Exception, "Failed to send Apple Music scrobbles, ignored [{}]",
|
||||
string.Join(", ", scrobbleResponse.Ignored));
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
_lock.Release();
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,31 @@
|
||||
using IF.Lastfm.Core.Api;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Selector.AppleMusic.Consumer.Factory
|
||||
{
|
||||
public interface IAppleMusicScrobblerFactory
|
||||
{
|
||||
public Task<AppleMusicScrobbler> Get(IAppleMusicPlayerWatcher? watcher = null);
|
||||
}
|
||||
|
||||
public class AppleMusicScrobblerFactory : IAppleMusicScrobblerFactory
|
||||
{
|
||||
private readonly ILoggerFactory _loggerFactory;
|
||||
private readonly LastfmClient _lastClient;
|
||||
|
||||
public AppleMusicScrobblerFactory(ILoggerFactory loggerFactory, LastfmClient lastClient)
|
||||
{
|
||||
_loggerFactory = loggerFactory;
|
||||
_lastClient = lastClient;
|
||||
}
|
||||
|
||||
public Task<AppleMusicScrobbler> Get(IAppleMusicPlayerWatcher? watcher = null)
|
||||
{
|
||||
return Task.FromResult(new AppleMusicScrobbler(
|
||||
watcher,
|
||||
_lastClient,
|
||||
_loggerFactory.CreateLogger<AppleMusicScrobbler>()
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
@ -4,16 +4,16 @@ namespace Selector.AppleMusic
|
||||
{
|
||||
public class AppleCurrentlyPlayingDTO
|
||||
{
|
||||
public Track Track { get; set; }
|
||||
public required Track? Track { get; set; }
|
||||
|
||||
// public string Username { get; set; }
|
||||
public string UserId { get; set; }
|
||||
public required string UserId { get; set; }
|
||||
|
||||
public static explicit operator AppleCurrentlyPlayingDTO(AppleListeningChangeEventArgs e)
|
||||
{
|
||||
return new()
|
||||
{
|
||||
Track = e.Current.Track,
|
||||
Track = e.Current?.Track,
|
||||
UserId = e.Id
|
||||
};
|
||||
}
|
||||
|
@ -4,13 +4,13 @@ namespace Selector.AppleMusic;
|
||||
|
||||
public class AppleListeningChangeEventArgs : ListeningChangeEventArgs
|
||||
{
|
||||
public AppleMusicCurrentlyPlayingContext Previous { get; set; }
|
||||
public AppleMusicCurrentlyPlayingContext Current { get; set; }
|
||||
public required AppleMusicCurrentlyPlayingContext? Previous { get; set; }
|
||||
public required AppleMusicCurrentlyPlayingContext? Current { get; set; }
|
||||
|
||||
AppleTimeline Timeline { get; set; }
|
||||
public required AppleTimeline Timeline { get; set; }
|
||||
|
||||
public static AppleListeningChangeEventArgs From(AppleMusicCurrentlyPlayingContext previous,
|
||||
AppleMusicCurrentlyPlayingContext current, AppleTimeline timeline, string id = null, string username = null)
|
||||
public static AppleListeningChangeEventArgs From(AppleMusicCurrentlyPlayingContext? previous,
|
||||
AppleMusicCurrentlyPlayingContext? current, AppleTimeline timeline, string id, string? username = null)
|
||||
{
|
||||
return new AppleListeningChangeEventArgs()
|
||||
{
|
||||
|
@ -1,4 +1,5 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Selector.AppleMusic.Consumer.Factory;
|
||||
using Selector.AppleMusic.Watcher;
|
||||
|
||||
namespace Selector.AppleMusic.Extensions;
|
||||
@ -8,7 +9,8 @@ public static class ServiceExtensions
|
||||
public static IServiceCollection AddAppleMusic(this IServiceCollection services)
|
||||
{
|
||||
services.AddSingleton<AppleMusicApiProvider>()
|
||||
.AddTransient<IAppleMusicWatcherFactory, AppleMusicWatcherFactory>();
|
||||
.AddTransient<IAppleMusicWatcherFactory, AppleMusicWatcherFactory>()
|
||||
.AddTransient<IAppleMusicScrobblerFactory, AppleMusicScrobblerFactory>();
|
||||
|
||||
return services;
|
||||
}
|
||||
|
@ -2,5 +2,5 @@ namespace Selector.AppleMusic.Model;
|
||||
|
||||
public class RecentlyPlayedTracksResponse
|
||||
{
|
||||
public List<Track> Data { get; set; }
|
||||
public List<Track>? Data { get; set; }
|
||||
}
|
@ -2,46 +2,46 @@ namespace Selector.AppleMusic.Model;
|
||||
|
||||
public class TrackAttributes
|
||||
{
|
||||
public string AlbumName { get; set; }
|
||||
public List<string> GenreNames { get; set; }
|
||||
public required string AlbumName { get; set; }
|
||||
public required 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; }
|
||||
public required string Isrc { get; set; }
|
||||
|
||||
//TODO: Artwork
|
||||
public Artwork Artwork { get; set; }
|
||||
public string ComposerName { get; set; }
|
||||
public string Url { get; set; }
|
||||
public PlayParams PlayParams { get; set; }
|
||||
public required Artwork Artwork { get; set; }
|
||||
public required string ComposerName { get; set; }
|
||||
public required string Url { get; set; }
|
||||
public required PlayParams PlayParams { get; set; }
|
||||
public int DiscNumber { get; set; }
|
||||
public bool HasLyrics { get; set; }
|
||||
public bool IsAppleDigitalMaster { get; set; }
|
||||
|
||||
public string Name { get; set; }
|
||||
public required string Name { get; set; }
|
||||
|
||||
//TODO: previews
|
||||
public string ArtistName { get; set; }
|
||||
public required string ArtistName { get; set; }
|
||||
}
|
||||
|
||||
public class Artwork
|
||||
{
|
||||
public string Url { get; set; }
|
||||
public required string Url { get; set; }
|
||||
}
|
||||
|
||||
public class PlayParams
|
||||
{
|
||||
public string Id { get; set; }
|
||||
public string Kind { get; set; }
|
||||
public required string Id { get; set; }
|
||||
public required 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 required string Id { get; set; }
|
||||
public required string Type { get; set; }
|
||||
public required string Href { get; set; }
|
||||
public required TrackAttributes Attributes { get; set; }
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
|
@ -4,9 +4,11 @@
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<IsAotCompatible>true</IsAotCompatible>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Inflatable.Lastfm" Version="1.2.0"/>
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.3"/>
|
||||
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.7.0"/>
|
||||
</ItemGroup>
|
||||
|
@ -5,7 +5,8 @@ namespace Selector.AppleMusic.Watcher;
|
||||
public class AppleMusicCurrentlyPlayingContext
|
||||
{
|
||||
public DateTime FirstSeen { get; set; }
|
||||
public Track Track { get; set; }
|
||||
public required Track Track { get; set; }
|
||||
public bool Scrobbled { get; set; }
|
||||
}
|
||||
|
||||
public class AppleMusicCurrentlyPlayingContextComparer : IEqualityComparer<AppleMusicCurrentlyPlayingContext>
|
||||
|
@ -12,7 +12,7 @@ namespace Selector
|
||||
/// <summary>
|
||||
/// Last retrieved currently playing
|
||||
/// </summary>
|
||||
public AppleMusicCurrentlyPlayingContext Live { get; }
|
||||
public AppleMusicCurrentlyPlayingContext? Live { get; }
|
||||
|
||||
public AppleTimeline Past { get; }
|
||||
}
|
||||
|
@ -10,10 +10,10 @@ public class AppleMusicPlayerWatcher : BaseWatcher, IAppleMusicPlayerWatcher
|
||||
private new 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 event EventHandler<AppleListeningChangeEventArgs>? NetworkPoll;
|
||||
public event EventHandler<AppleListeningChangeEventArgs>? ItemChange;
|
||||
public event EventHandler<AppleListeningChangeEventArgs>? AlbumChange;
|
||||
public event EventHandler<AppleListeningChangeEventArgs>? ArtistChange;
|
||||
|
||||
public AppleMusicCurrentlyPlayingContext? Live { get; private set; }
|
||||
private AppleMusicCurrentlyPlayingContext? Previous { get; set; }
|
||||
@ -70,7 +70,7 @@ public class AppleMusicPlayerWatcher : BaseWatcher, IAppleMusicPlayerWatcher
|
||||
addedItems.Insert(0, currentPrevious);
|
||||
foreach (var (first, second) in addedItems.Zip(addedItems.Skip(1)))
|
||||
{
|
||||
Logger.LogDebug("Track changed: {prevTrack} -> {currentTrack}", first.Track, second.Track);
|
||||
Logger.LogInformation("Track changed: {prevTrack} -> {currentTrack}", first.Track, second.Track);
|
||||
OnItemChange(AppleListeningChangeEventArgs.From(first, second, Past, id: Id));
|
||||
}
|
||||
}
|
||||
@ -85,7 +85,7 @@ public class AppleMusicPlayerWatcher : BaseWatcher, IAppleMusicPlayerWatcher
|
||||
Logger.LogError(e, "Forbidden exception");
|
||||
// throw;
|
||||
}
|
||||
catch (ServiceException e)
|
||||
catch (ServiceException)
|
||||
{
|
||||
Logger.LogInformation("Apple Music internal error");
|
||||
// throw;
|
||||
@ -112,15 +112,20 @@ public class AppleMusicPlayerWatcher : BaseWatcher, IAppleMusicPlayerWatcher
|
||||
{
|
||||
Track = Live.Track,
|
||||
FirstSeen = Live.FirstSeen,
|
||||
Scrobbled = Live.Scrobbled,
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
Live = new()
|
||||
if (recentlyPlayedTracks.Data is not null && recentlyPlayedTracks.Data.Any())
|
||||
{
|
||||
Track = recentlyPlayedTracks.Data?.FirstOrDefault(),
|
||||
FirstSeen = DateTime.UtcNow,
|
||||
};
|
||||
Live = new()
|
||||
{
|
||||
Track = recentlyPlayedTracks.Data.First(),
|
||||
FirstSeen = DateTime.UtcNow,
|
||||
Scrobbled = false,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -6,23 +6,14 @@ namespace Selector.AppleMusic.Watcher
|
||||
public interface IAppleMusicWatcherFactory
|
||||
{
|
||||
Task<IWatcher> Get<T>(AppleMusicApiProvider appleMusicProvider, string developerToken, string teamId,
|
||||
string keyId, string userToken, string id = null, int pollPeriod = 3000)
|
||||
string keyId, string userToken, string id, int pollPeriod = 3000)
|
||||
where T : class, IWatcher;
|
||||
}
|
||||
|
||||
public class AppleMusicWatcherFactory : IAppleMusicWatcherFactory
|
||||
public class AppleMusicWatcherFactory(ILoggerFactory loggerFactory) : 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, string id = null, int pollPeriod = 3000)
|
||||
public Task<IWatcher> Get<T>(AppleMusicApiProvider appleMusicProvider, string developerToken,
|
||||
string teamId, string keyId, string userToken, string id, int pollPeriod = 3000)
|
||||
where T : class, IWatcher
|
||||
{
|
||||
if (typeof(T).IsAssignableFrom(typeof(AppleMusicPlayerWatcher)))
|
||||
@ -31,15 +22,15 @@ namespace Selector.AppleMusic.Watcher
|
||||
{
|
||||
var api = appleMusicProvider.GetApi(developerToken, teamId, keyId, userToken);
|
||||
|
||||
return new AppleMusicPlayerWatcher(
|
||||
return Task.FromResult<IWatcher>(new AppleMusicPlayerWatcher(
|
||||
api,
|
||||
LoggerFactory?.CreateLogger<AppleMusicPlayerWatcher>() ??
|
||||
loggerFactory?.CreateLogger<AppleMusicPlayerWatcher>() ??
|
||||
NullLogger<AppleMusicPlayerWatcher>.Instance,
|
||||
pollPeriod: pollPeriod
|
||||
)
|
||||
{
|
||||
Id = id
|
||||
};
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@ -110,7 +110,8 @@ namespace Selector.CLI.Extensions
|
||||
);
|
||||
|
||||
services.AddTransient<IScrobbleRepository, ScrobbleRepository>()
|
||||
.AddTransient<ISpotifyListenRepository, SpotifyListenRepository>();
|
||||
.AddTransient<ISpotifyListenRepository, SpotifyListenRepository>()
|
||||
.AddTransient<IAppleListenRepository, AppleListenRepository>();
|
||||
|
||||
services.AddTransient<IListenRepository, MetaListenRepository>();
|
||||
//services.AddTransient<IListenRepository, SpotifyListenRepository>();
|
||||
|
@ -5,6 +5,8 @@
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<StartupObject>Selector.CLI.Program</StartupObject>
|
||||
<LangVersion>latest</LangVersion>
|
||||
<IsAotCompatible>true</IsAotCompatible>
|
||||
<EnableConfigurationBindingGenerator>true</EnableConfigurationBindingGenerator>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
@ -10,6 +10,7 @@ using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Selector.AppleMusic;
|
||||
using Selector.AppleMusic.Consumer.Factory;
|
||||
using Selector.AppleMusic.Watcher;
|
||||
using Selector.Cache;
|
||||
using Selector.CLI.Consumer;
|
||||
@ -24,87 +25,45 @@ using Selector.Spotify.Watcher;
|
||||
|
||||
namespace Selector.CLI
|
||||
{
|
||||
class DbWatcherService : IHostedService
|
||||
class DbWatcherService(
|
||||
ISpotifyWatcherFactory spotifyWatcherFactory,
|
||||
IAppleMusicWatcherFactory appleWatcherFactory,
|
||||
IWatcherCollectionFactory watcherCollectionFactory,
|
||||
IRefreshTokenFactoryProvider factory,
|
||||
AppleMusicApiProvider appleMusicProvider,
|
||||
// IAudioFeatureInjectorFactory audioFeatureInjectorFactory,
|
||||
IAppleMusicScrobblerFactory scrobblerFactory,
|
||||
IPlayCounterFactory playCounterFactory,
|
||||
UserEventBus userEventBus,
|
||||
ILogger<DbWatcherService> logger,
|
||||
IOptions<AppleMusicOptions> appleMusicOptions,
|
||||
IServiceProvider serviceProvider,
|
||||
IPublisherFactory publisherFactory = null,
|
||||
ICacheWriterFactory cacheWriterFactory = null,
|
||||
IMappingPersisterFactory mappingPersisterFactory = null,
|
||||
IUserEventFirerFactory userEventFirerFactory = null)
|
||||
: IHostedService
|
||||
{
|
||||
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 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;
|
||||
|
||||
private readonly IUserEventFirerFactory UserEventFirerFactory;
|
||||
|
||||
private readonly IPublisherFactory PublisherFactory;
|
||||
private readonly ICacheWriterFactory CacheWriterFactory;
|
||||
|
||||
private readonly IMappingPersisterFactory MappingPersisterFactory;
|
||||
// private readonly IAudioFeatureInjectorFactory AudioFeatureInjectorFactory = audioFeatureInjectorFactory;
|
||||
|
||||
private ConcurrentDictionary<string, IWatcherCollection> Watchers { get; set; } = new();
|
||||
|
||||
public DbWatcherService(
|
||||
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
|
||||
)
|
||||
{
|
||||
Logger = logger;
|
||||
ServiceProvider = serviceProvider;
|
||||
UserEventBus = userEventBus;
|
||||
|
||||
_spotifyWatcherFactory = spotifyWatcherFactory;
|
||||
_appleWatcherFactory = appleWatcherFactory;
|
||||
_appleMusicOptions = appleMusicOptions;
|
||||
WatcherCollectionFactory = watcherCollectionFactory;
|
||||
SpotifyFactory = spotifyFactory;
|
||||
_appleMusicProvider = appleMusicProvider;
|
||||
|
||||
AudioFeatureInjectorFactory = audioFeatureInjectorFactory;
|
||||
PlayCounterFactory = playCounterFactory;
|
||||
|
||||
UserEventFirerFactory = userEventFirerFactory;
|
||||
|
||||
PublisherFactory = publisherFactory;
|
||||
CacheWriterFactory = cacheWriterFactory;
|
||||
|
||||
MappingPersisterFactory = mappingPersisterFactory;
|
||||
}
|
||||
|
||||
public async Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
Logger.LogInformation("Starting database watcher service...");
|
||||
logger.LogInformation("Starting database watcher service...");
|
||||
|
||||
var watcherIndices = await InitInstances();
|
||||
AttachEventBus();
|
||||
|
||||
Logger.LogInformation("Starting {count} affected watcher collection(s)...", watcherIndices.Count());
|
||||
logger.LogInformation("Starting {count} affected watcher collection(s)...", watcherIndices.Count());
|
||||
StartWatcherCollections(watcherIndices);
|
||||
}
|
||||
|
||||
private async Task<IEnumerable<string>> InitInstances()
|
||||
{
|
||||
using var scope = ServiceProvider.CreateScope();
|
||||
using var scope = serviceProvider.CreateScope();
|
||||
var db = scope.ServiceProvider.GetService<ApplicationDbContext>();
|
||||
|
||||
var indices = new HashSet<string>();
|
||||
@ -112,12 +71,12 @@ namespace Selector.CLI
|
||||
foreach (var dbWatcher in db.Watcher
|
||||
.Include(w => w.User)
|
||||
.Where(w =>
|
||||
((w.Type == WatcherType.SpotifyPlayer || w.Type == WatcherType.SpotifyPlaylist) &&
|
||||
!string.IsNullOrWhiteSpace(w.User.SpotifyRefreshToken)) ||
|
||||
// ((w.Type == WatcherType.SpotifyPlayer || w.Type == WatcherType.SpotifyPlaylist) &&
|
||||
// !string.IsNullOrWhiteSpace(w.User.SpotifyRefreshToken)) ||
|
||||
(w.Type == WatcherType.AppleMusicPlayer && w.User.AppleMusicLinked)
|
||||
))
|
||||
{
|
||||
using var logScope = Logger.BeginScope(new Dictionary<string, string>
|
||||
using var logScope = logger.BeginScope(new Dictionary<string, string>
|
||||
{
|
||||
{ "username", dbWatcher.User.UserName }
|
||||
});
|
||||
@ -133,12 +92,12 @@ namespace Selector.CLI
|
||||
|
||||
private async Task<IWatcherContext> InitInstance(Watcher dbWatcher)
|
||||
{
|
||||
Logger.LogInformation("Creating new [{type}] watcher", dbWatcher.Type);
|
||||
logger.LogInformation("Creating new [{type}] watcher", dbWatcher.Type);
|
||||
|
||||
var watcherCollectionIdx = dbWatcher.UserId;
|
||||
|
||||
if (!Watchers.ContainsKey(watcherCollectionIdx))
|
||||
Watchers[watcherCollectionIdx] = WatcherCollectionFactory.Get();
|
||||
Watchers[watcherCollectionIdx] = watcherCollectionFactory.Get();
|
||||
|
||||
var watcherCollection = Watchers[watcherCollectionIdx];
|
||||
|
||||
@ -148,30 +107,30 @@ namespace Selector.CLI
|
||||
switch (dbWatcher.Type)
|
||||
{
|
||||
case WatcherType.SpotifyPlayer:
|
||||
Logger.LogDebug("Getting Spotify factory");
|
||||
var spotifyFactory = await SpotifyFactory.GetFactory(dbWatcher.User.SpotifyRefreshToken);
|
||||
logger.LogDebug("Getting Spotify factory");
|
||||
var spotifyFactory = await factory.GetFactory(dbWatcher.User.SpotifyRefreshToken);
|
||||
|
||||
watcher = await _spotifyWatcherFactory.Get<SpotifyPlayerWatcher>(spotifyFactory,
|
||||
watcher = await spotifyWatcherFactory.Get<SpotifyPlayerWatcher>(spotifyFactory,
|
||||
id: dbWatcher.UserId, pollPeriod: PollPeriod);
|
||||
|
||||
// deprecated, thanks Spotify!
|
||||
// consumers.Add(await AudioFeatureInjectorFactory.Get(spotifyFactory));
|
||||
if (CacheWriterFactory is not null) consumers.Add(await CacheWriterFactory.GetSpotify());
|
||||
if (PublisherFactory is not null) consumers.Add(await PublisherFactory.GetSpotify());
|
||||
if (cacheWriterFactory is not null) consumers.Add(await cacheWriterFactory.GetSpotify());
|
||||
if (publisherFactory is not null) consumers.Add(await publisherFactory.GetSpotify());
|
||||
|
||||
if (MappingPersisterFactory is not null && !Magic.Dummy)
|
||||
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.GetSpotify());
|
||||
if (userEventFirerFactory is not null) consumers.Add(await userEventFirerFactory.GetSpotify());
|
||||
|
||||
if (dbWatcher.User.LastFmConnected())
|
||||
{
|
||||
consumers.Add(await PlayCounterFactory.Get(creds: new()
|
||||
consumers.Add(await playCounterFactory.Get(creds: new()
|
||||
{ Username = dbWatcher.User.LastFmUsername }));
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.LogDebug("[{username}] No Last.fm username, skipping play counter",
|
||||
logger.LogDebug("[{username}] No Last.fm username, skipping play counter",
|
||||
dbWatcher.User.UserName);
|
||||
}
|
||||
|
||||
@ -179,16 +138,28 @@ namespace Selector.CLI
|
||||
|
||||
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,
|
||||
watcher = await appleWatcherFactory.Get<AppleMusicPlayerWatcher>(appleMusicProvider,
|
||||
appleMusicOptions.Value.Key, appleMusicOptions.Value.TeamId, appleMusicOptions.Value.KeyId,
|
||||
dbWatcher.User.AppleMusicKey, id: dbWatcher.UserId);
|
||||
|
||||
if (CacheWriterFactory is not null) consumers.Add(await CacheWriterFactory.GetApple());
|
||||
if (PublisherFactory is not null) consumers.Add(await PublisherFactory.GetApple());
|
||||
if (cacheWriterFactory is not null) consumers.Add(await cacheWriterFactory.GetApple());
|
||||
if (publisherFactory is not null) consumers.Add(await publisherFactory.GetApple());
|
||||
|
||||
if (UserEventFirerFactory is not null) consumers.Add(await UserEventFirerFactory.GetApple());
|
||||
if (userEventFirerFactory is not null) consumers.Add(await userEventFirerFactory.GetApple());
|
||||
|
||||
// if (dbWatcher.User.LastFmConnected() && !string.IsNullOrWhiteSpace(dbWatcher.User.LastFmPassword))
|
||||
// {
|
||||
// var scrobbler = await scrobblerFactory.Get();
|
||||
// await scrobbler.Auth(dbWatcher.User.LastFmUsername, dbWatcher.User.LastFmPassword);
|
||||
// consumers.Add(scrobbler);
|
||||
// }
|
||||
// else
|
||||
// {
|
||||
// logger.LogDebug("[{username}] No Last.fm username/password, skipping scrobbler",
|
||||
// dbWatcher.User.UserName);
|
||||
// }
|
||||
|
||||
break;
|
||||
}
|
||||
@ -202,23 +173,23 @@ namespace Selector.CLI
|
||||
{
|
||||
try
|
||||
{
|
||||
Logger.LogInformation("Starting watcher collection [{index}]", index);
|
||||
logger.LogInformation("Starting watcher collection [{index}]", index);
|
||||
Watchers[index].Start();
|
||||
}
|
||||
catch (KeyNotFoundException)
|
||||
{
|
||||
Logger.LogError("Unable to retrieve watcher collection [{index}] when starting", index);
|
||||
logger.LogError("Unable to retrieve watcher collection [{index}] when starting", index);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
Logger.LogInformation("Shutting down");
|
||||
logger.LogInformation("Shutting down");
|
||||
|
||||
foreach ((var key, var watcher) in Watchers)
|
||||
{
|
||||
Logger.LogInformation("Stopping watcher collection [{key}]", key);
|
||||
logger.LogInformation("Stopping watcher collection [{key}]", key);
|
||||
watcher.Stop();
|
||||
}
|
||||
|
||||
@ -229,23 +200,23 @@ namespace Selector.CLI
|
||||
|
||||
private void AttachEventBus()
|
||||
{
|
||||
UserEventBus.SpotifyLinkChange += SpotifyChangeCallback;
|
||||
UserEventBus.AppleLinkChange += AppleMusicChangeCallback;
|
||||
UserEventBus.LastfmCredChange += LastfmChangeCallback;
|
||||
userEventBus.SpotifyLinkChange += SpotifyChangeCallback;
|
||||
userEventBus.AppleLinkChange += AppleMusicChangeCallback;
|
||||
userEventBus.LastfmCredChange += LastfmChangeCallback;
|
||||
}
|
||||
|
||||
private void DetachEventBus()
|
||||
{
|
||||
UserEventBus.SpotifyLinkChange -= SpotifyChangeCallback;
|
||||
UserEventBus.AppleLinkChange -= AppleMusicChangeCallback;
|
||||
UserEventBus.LastfmCredChange -= LastfmChangeCallback;
|
||||
userEventBus.SpotifyLinkChange -= SpotifyChangeCallback;
|
||||
userEventBus.AppleLinkChange -= AppleMusicChangeCallback;
|
||||
userEventBus.LastfmCredChange -= LastfmChangeCallback;
|
||||
}
|
||||
|
||||
public async void SpotifyChangeCallback(object sender, SpotifyLinkChange change)
|
||||
{
|
||||
if (Watchers.ContainsKey(change.UserId))
|
||||
{
|
||||
Logger.LogDebug("Setting new Spotify link state for [{username}], [{}]", change.UserId,
|
||||
logger.LogDebug("Setting new Spotify link state for [{username}], [{}]", change.UserId,
|
||||
change.NewLinkState);
|
||||
|
||||
var watcherCollection = Watchers[change.UserId];
|
||||
@ -261,7 +232,7 @@ namespace Selector.CLI
|
||||
}
|
||||
else
|
||||
{
|
||||
using var scope = ServiceProvider.CreateScope();
|
||||
using var scope = serviceProvider.CreateScope();
|
||||
var db = scope.ServiceProvider.GetService<ApplicationDbContext>();
|
||||
|
||||
var watcherEnum = db.Watcher
|
||||
@ -275,7 +246,7 @@ namespace Selector.CLI
|
||||
|
||||
Watchers[change.UserId].Start();
|
||||
|
||||
Logger.LogDebug("Started {} watchers for [{username}]", watcherEnum.Count(), change.UserId);
|
||||
logger.LogDebug("Started {} watchers for [{username}]", watcherEnum.Count(), change.UserId);
|
||||
}
|
||||
}
|
||||
|
||||
@ -283,7 +254,7 @@ namespace Selector.CLI
|
||||
{
|
||||
if (Watchers.ContainsKey(change.UserId))
|
||||
{
|
||||
Logger.LogDebug("Setting new Apple Music link state for [{username}], [{}]", change.UserId,
|
||||
logger.LogDebug("Setting new Apple Music link state for [{username}], [{}]", change.UserId,
|
||||
change.NewLinkState);
|
||||
|
||||
var watcherCollection = Watchers[change.UserId];
|
||||
@ -299,7 +270,7 @@ namespace Selector.CLI
|
||||
}
|
||||
else
|
||||
{
|
||||
using var scope = ServiceProvider.CreateScope();
|
||||
using var scope = serviceProvider.CreateScope();
|
||||
var db = scope.ServiceProvider.GetService<ApplicationDbContext>();
|
||||
|
||||
var watcherEnum = db.Watcher
|
||||
@ -313,7 +284,7 @@ namespace Selector.CLI
|
||||
|
||||
Watchers[change.UserId].Start();
|
||||
|
||||
Logger.LogDebug("Started {} watchers for [{username}]", watcherEnum.Count(), change.UserId);
|
||||
logger.LogDebug("Started {} watchers for [{username}]", watcherEnum.Count(), change.UserId);
|
||||
}
|
||||
}
|
||||
|
||||
@ -321,7 +292,7 @@ namespace Selector.CLI
|
||||
{
|
||||
if (Watchers.ContainsKey(change.UserId))
|
||||
{
|
||||
Logger.LogDebug("Setting new username for [{}], [{}]", change.UserId, change.NewUsername);
|
||||
logger.LogDebug("Setting new username for [{}], [{}]", change.UserId, change.NewUsername);
|
||||
|
||||
var watcherCollection = Watchers[change.UserId];
|
||||
|
||||
@ -335,7 +306,7 @@ namespace Selector.CLI
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.LogDebug("No watchers running for [{username}], skipping Spotify event", change.UserId);
|
||||
logger.LogDebug("No watchers running for [{username}], skipping Spotify event", change.UserId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -134,15 +134,15 @@ namespace Selector.CLI
|
||||
{
|
||||
switch (consumer)
|
||||
{
|
||||
case Consumers.AudioFeatures:
|
||||
consumers.Add(await ServiceProvider.GetService<AudioFeatureInjectorFactory>()
|
||||
.Get(spotifyFactory));
|
||||
break;
|
||||
// case Consumers.AudioFeatures:
|
||||
// consumers.Add(await ServiceProvider.GetService<AudioFeatureInjectorFactory>()
|
||||
// .Get(spotifyFactory));
|
||||
// break;
|
||||
|
||||
case Consumers.AudioFeaturesCache:
|
||||
consumers.Add(await ServiceProvider.GetService<CachingAudioFeatureInjectorFactory>()
|
||||
.Get(spotifyFactory));
|
||||
break;
|
||||
// case Consumers.AudioFeaturesCache:
|
||||
// consumers.Add(await ServiceProvider.GetService<CachingAudioFeatureInjectorFactory>()
|
||||
// .Get(spotifyFactory));
|
||||
// break;
|
||||
|
||||
case Consumers.CacheWriter:
|
||||
if (watcher is ISpotifyPlayerWatcher or IPlaylistWatcher)
|
||||
|
@ -1,3 +1,4 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Selector.Spotify;
|
||||
@ -9,18 +10,19 @@ using StackExchange.Redis;
|
||||
|
||||
namespace Selector.Cache
|
||||
{
|
||||
[Obsolete]
|
||||
public class CachingAudioFeatureInjectorFactory : IAudioFeatureInjectorFactory
|
||||
{
|
||||
private readonly ILoggerFactory LoggerFactory;
|
||||
private readonly IDatabaseAsync Db;
|
||||
private readonly ILoggerFactory _loggerFactory;
|
||||
private readonly IDatabaseAsync _db;
|
||||
|
||||
public CachingAudioFeatureInjectorFactory(
|
||||
ILoggerFactory loggerFactory,
|
||||
IDatabaseAsync db
|
||||
)
|
||||
{
|
||||
LoggerFactory = loggerFactory;
|
||||
Db = db;
|
||||
_loggerFactory = loggerFactory;
|
||||
_db = db;
|
||||
}
|
||||
|
||||
public async Task<ISpotifyPlayerConsumer> Get(ISpotifyConfigFactory spotifyFactory,
|
||||
@ -33,16 +35,16 @@ namespace Selector.Cache
|
||||
|
||||
return new CachingAudioFeatureInjector(
|
||||
watcher,
|
||||
Db,
|
||||
_db,
|
||||
client.Tracks,
|
||||
LoggerFactory.CreateLogger<CachingAudioFeatureInjector>()
|
||||
_loggerFactory.CreateLogger<CachingAudioFeatureInjector>()
|
||||
);
|
||||
}
|
||||
else
|
||||
{
|
||||
return new DummyAudioFeatureInjector(
|
||||
watcher,
|
||||
LoggerFactory.CreateLogger<DummyAudioFeatureInjector>()
|
||||
_loggerFactory.CreateLogger<DummyAudioFeatureInjector>()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -11,6 +11,7 @@ using StackExchange.Redis;
|
||||
|
||||
namespace Selector.Cache
|
||||
{
|
||||
[Obsolete]
|
||||
public class CachingAudioFeatureInjector : AudioFeatureInjector
|
||||
{
|
||||
private readonly IDatabaseAsync Db;
|
||||
|
@ -20,17 +20,17 @@ namespace Selector.Cache.Extensions
|
||||
var connMulti = ConnectionMultiplexer.Connect(connectionStr);
|
||||
services.AddSingleton(connMulti);
|
||||
services.AddTransient<IDatabaseAsync>(
|
||||
services => services.GetService<ConnectionMultiplexer>().GetDatabase());
|
||||
services.AddTransient<ISubscriber>(services =>
|
||||
services.GetService<ConnectionMultiplexer>().GetSubscriber());
|
||||
s => s.GetService<ConnectionMultiplexer>().GetDatabase());
|
||||
services.AddTransient<ISubscriber>(s =>
|
||||
s.GetService<ConnectionMultiplexer>().GetSubscriber());
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
public static IServiceCollection AddCachingConsumerFactories(this IServiceCollection services)
|
||||
{
|
||||
services.AddTransient<IAudioFeatureInjectorFactory, CachingAudioFeatureInjectorFactory>();
|
||||
services.AddTransient<CachingAudioFeatureInjectorFactory>();
|
||||
// services.AddTransient<IAudioFeatureInjectorFactory, CachingAudioFeatureInjectorFactory>();
|
||||
// services.AddTransient<CachingAudioFeatureInjectorFactory>();
|
||||
services.AddTransient<IPlayCounterFactory, PlayCounterCachingFactory>();
|
||||
services.AddTransient<PlayCounterCachingFactory>();
|
||||
|
||||
|
@ -4,6 +4,7 @@
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<EnableDefaultCompileItems>true</EnableDefaultCompileItems>
|
||||
<LangVersion>latest</LangVersion>
|
||||
<IsAotCompatible>true</IsAotCompatible>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
@ -3,6 +3,7 @@
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<IsAotCompatible>true</IsAotCompatible>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
@ -3,6 +3,7 @@
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<IsAotCompatible>true</IsAotCompatible>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
@ -12,15 +12,15 @@ public static class ServiceExtensions
|
||||
services.AddTransient<LastAuth>(sp => new LastAuth(client, secret));
|
||||
services.AddTransient(sp => new LastfmClient(sp.GetService<LastAuth>()));
|
||||
|
||||
services.AddTransient<ITrackApi>(sp => sp.GetService<LastfmClient>().Track);
|
||||
services.AddTransient<IAlbumApi>(sp => sp.GetService<LastfmClient>().Album);
|
||||
services.AddTransient<IArtistApi>(sp => sp.GetService<LastfmClient>().Artist);
|
||||
services.AddTransient<ITrackApi>(sp => sp.GetRequiredService<LastfmClient>().Track);
|
||||
services.AddTransient<IAlbumApi>(sp => sp.GetRequiredService<LastfmClient>().Album);
|
||||
services.AddTransient<IArtistApi>(sp => sp.GetRequiredService<LastfmClient>().Artist);
|
||||
|
||||
services.AddTransient<IUserApi>(sp => sp.GetService<LastfmClient>().User);
|
||||
services.AddTransient<IUserApi>(sp => sp.GetRequiredService<LastfmClient>().User);
|
||||
|
||||
services.AddTransient<IChartApi>(sp => sp.GetService<LastfmClient>().Chart);
|
||||
services.AddTransient<ILibraryApi>(sp => sp.GetService<LastfmClient>().Library);
|
||||
services.AddTransient<ITagApi>(sp => sp.GetService<LastfmClient>().Tag);
|
||||
services.AddTransient<IChartApi>(sp => sp.GetRequiredService<LastfmClient>().Chart);
|
||||
services.AddTransient<ILibraryApi>(sp => sp.GetRequiredService<LastfmClient>().Library);
|
||||
services.AddTransient<ITagApi>(sp => sp.GetRequiredService<LastfmClient>().Tag);
|
||||
|
||||
services.AddTransient<IScrobbler, MemoryScrobbler>();
|
||||
|
||||
|
@ -16,9 +16,9 @@ namespace Selector.Mapping
|
||||
ArtistName = artistName;
|
||||
}
|
||||
|
||||
private SimpleAlbum result;
|
||||
public SimpleAlbum Album => result;
|
||||
public override object Result => result;
|
||||
private SimpleAlbum? _result;
|
||||
public SimpleAlbum? Album => _result;
|
||||
public override object? Result => _result;
|
||||
|
||||
public override string Query => $"{AlbumName} {ArtistName}";
|
||||
|
||||
@ -26,13 +26,13 @@ namespace Selector.Mapping
|
||||
|
||||
public override void HandleResponse(Task<SearchResponse> response)
|
||||
{
|
||||
var topResult = response.Result.Albums.Items.FirstOrDefault();
|
||||
var topResult = response.Result.Albums.Items?.FirstOrDefault();
|
||||
|
||||
if (topResult is not null
|
||||
&& topResult.Name.Equals(AlbumName, StringComparison.InvariantCultureIgnoreCase)
|
||||
&& topResult.Artists.First().Name.Equals(ArtistName, StringComparison.InvariantCultureIgnoreCase))
|
||||
{
|
||||
result = topResult;
|
||||
_result = topResult;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -8,15 +8,15 @@ namespace Selector.Mapping
|
||||
{
|
||||
public string ArtistName { get; set; }
|
||||
|
||||
public ScrobbleArtistMapping(ISearchClient _searchClient, ILogger<ScrobbleArtistMapping> _logger,
|
||||
string artistName) : base(_searchClient, _logger)
|
||||
public ScrobbleArtistMapping(ISearchClient searchClient, ILogger<ScrobbleArtistMapping> logger,
|
||||
string artistName) : base(searchClient, logger)
|
||||
{
|
||||
ArtistName = artistName;
|
||||
}
|
||||
|
||||
private FullArtist result;
|
||||
public FullArtist Artist => result;
|
||||
public override object Result => result;
|
||||
private FullArtist? _result;
|
||||
public FullArtist? Artist => _result;
|
||||
public override object? Result => _result;
|
||||
|
||||
public override string Query => ArtistName;
|
||||
|
||||
@ -24,12 +24,12 @@ namespace Selector.Mapping
|
||||
|
||||
public override void HandleResponse(Task<SearchResponse> response)
|
||||
{
|
||||
var topResult = response.Result.Artists.Items.FirstOrDefault();
|
||||
var topResult = response.Result.Artists.Items?.FirstOrDefault();
|
||||
|
||||
if (topResult is not null
|
||||
&& topResult.Name.Equals(ArtistName, StringComparison.InvariantCultureIgnoreCase))
|
||||
{
|
||||
result = topResult;
|
||||
_result = topResult;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -17,42 +17,42 @@ namespace Selector.Mapping
|
||||
/// </summary>
|
||||
public abstract class ScrobbleMapping : IOperation
|
||||
{
|
||||
private readonly ILogger<ScrobbleMapping> logger;
|
||||
private readonly ISearchClient searchClient;
|
||||
private readonly ILogger<ScrobbleMapping> _logger;
|
||||
private readonly ISearchClient _searchClient;
|
||||
|
||||
public event EventHandler Success;
|
||||
public event EventHandler? Success;
|
||||
|
||||
public int MaxAttempts { get; private set; } = 5;
|
||||
public int Attempts { get; private set; }
|
||||
|
||||
private Task<SearchResponse> currentTask { get; set; }
|
||||
private Task<SearchResponse>? CurrentTask { get; set; }
|
||||
public bool Succeeded { get; private set; } = false;
|
||||
|
||||
public abstract object Result { get; }
|
||||
public abstract object? Result { get; }
|
||||
public abstract string Query { get; }
|
||||
public abstract SearchRequest.Types QueryType { get; }
|
||||
|
||||
private TaskCompletionSource AggregateTaskSource { get; set; } = new();
|
||||
public Task Task => AggregateTaskSource.Task;
|
||||
|
||||
public ScrobbleMapping(ISearchClient _searchClient, ILogger<ScrobbleMapping> _logger)
|
||||
public ScrobbleMapping(ISearchClient searchClient, ILogger<ScrobbleMapping> logger)
|
||||
{
|
||||
logger = _logger;
|
||||
searchClient = _searchClient;
|
||||
_logger = logger;
|
||||
_searchClient = searchClient;
|
||||
}
|
||||
|
||||
public Task Execute()
|
||||
{
|
||||
logger.LogInformation("Mapping Last.fm {} ({}) to Spotify", Query, QueryType);
|
||||
_logger.LogInformation("Mapping Last.fm {} ({}) to Spotify", Query, QueryType);
|
||||
|
||||
var netTime = Stopwatch.StartNew();
|
||||
currentTask = searchClient.Item(new(QueryType, Query));
|
||||
currentTask.ContinueWith(async t =>
|
||||
CurrentTask = _searchClient.Item(new(QueryType, Query));
|
||||
CurrentTask.ContinueWith(async t =>
|
||||
{
|
||||
try
|
||||
{
|
||||
netTime.Stop();
|
||||
logger.LogTrace("Network request took {:n} ms", netTime.ElapsedMilliseconds);
|
||||
_logger.LogTrace("Network request took {:n} ms", netTime.ElapsedMilliseconds);
|
||||
|
||||
if (t.IsCompletedSuccessfully)
|
||||
{
|
||||
@ -62,22 +62,23 @@ namespace Selector.Mapping
|
||||
}
|
||||
else
|
||||
{
|
||||
if (t.Exception.InnerException is APITooManyRequestsException ex)
|
||||
if (t.Exception?.InnerException is APITooManyRequestsException ex)
|
||||
{
|
||||
logger.LogError("Spotify search request too many requests, waiting for {}", ex.RetryAfter);
|
||||
_logger.LogError("Spotify search request too many requests, waiting for {}", ex.RetryAfter);
|
||||
await Task.Delay(ex.RetryAfter.Add(TimeSpan.FromSeconds(1)));
|
||||
await Execute();
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.LogError("Spotify search request task faulted, {}", t.Exception);
|
||||
AggregateTaskSource.SetException(t.Exception);
|
||||
_logger.LogError("Spotify search request task faulted, {}", t.Exception);
|
||||
if (t.Exception != null) AggregateTaskSource.SetException(t.Exception);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
logger.LogError(e, "Error while mapping Last.fm {} ({}) to Spotify on attempt {}", Query, QueryType,
|
||||
_logger.LogError(e, "Error while mapping Last.fm {} ({}) to Spotify on attempt {}", Query,
|
||||
QueryType,
|
||||
Attempts);
|
||||
Succeeded = false;
|
||||
}
|
||||
@ -92,7 +93,7 @@ namespace Selector.Mapping
|
||||
protected virtual void OnSuccess()
|
||||
{
|
||||
// Raise the event in a thread-safe manner using the ?. operator.
|
||||
Success?.Invoke(this, new EventArgs());
|
||||
Success?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
}
|
||||
}
|
@ -9,16 +9,16 @@ namespace Selector.Mapping
|
||||
public string TrackName { get; set; }
|
||||
public string ArtistName { get; set; }
|
||||
|
||||
public ScrobbleTrackMapping(ISearchClient _searchClient, ILogger<ScrobbleTrackMapping> _logger,
|
||||
string trackName, string artistName) : base(_searchClient, _logger)
|
||||
public ScrobbleTrackMapping(ISearchClient searchClient, ILogger<ScrobbleTrackMapping> logger,
|
||||
string trackName, string artistName) : base(searchClient, logger)
|
||||
{
|
||||
TrackName = trackName;
|
||||
ArtistName = artistName;
|
||||
}
|
||||
|
||||
private FullTrack result;
|
||||
public FullTrack Track => result;
|
||||
public override object Result => result;
|
||||
private FullTrack? _result;
|
||||
public FullTrack? Track => _result;
|
||||
public override object? Result => _result;
|
||||
|
||||
public override string Query => $"{TrackName} {ArtistName}";
|
||||
|
||||
@ -26,13 +26,13 @@ namespace Selector.Mapping
|
||||
|
||||
public override void HandleResponse(Task<SearchResponse> response)
|
||||
{
|
||||
var topResult = response.Result.Tracks.Items.FirstOrDefault();
|
||||
var topResult = response.Result.Tracks.Items?.FirstOrDefault();
|
||||
|
||||
if (topResult is not null
|
||||
&& topResult.Name.Equals(TrackName, StringComparison.InvariantCultureIgnoreCase)
|
||||
&& topResult.Artists.First().Name.Equals(ArtistName, StringComparison.InvariantCultureIgnoreCase))
|
||||
{
|
||||
result = topResult;
|
||||
_result = topResult;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -5,15 +5,15 @@ namespace Selector
|
||||
public class Scrobble : IListen
|
||||
{
|
||||
public DateTime Timestamp { get; set; }
|
||||
public string? TrackName { get; set; }
|
||||
public string? AlbumName { get; set; }
|
||||
public required string TrackName { get; set; }
|
||||
public required string AlbumName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Not populated by default from the service, where not the same as <see cref="ArtistName"/> these have been manually entered
|
||||
/// </summary>
|
||||
public string? AlbumArtistName { get; set; }
|
||||
|
||||
public string? ArtistName { get; set; }
|
||||
public required string ArtistName { get; set; }
|
||||
|
||||
public static explicit operator Scrobble(LastTrack track) => new()
|
||||
{
|
||||
|
@ -105,7 +105,7 @@ namespace Selector
|
||||
|
||||
public class ListenComp : IEqualityComparer<IListen>
|
||||
{
|
||||
public bool Equals(IListen x, IListen y) => x.Timestamp == y.Timestamp;
|
||||
public bool Equals(IListen? x, IListen? y) => x?.Timestamp == y?.Timestamp;
|
||||
|
||||
public int GetHashCode([DisallowNull] IListen obj) => obj.Timestamp.GetHashCode();
|
||||
}
|
||||
|
@ -9,67 +9,68 @@ namespace Selector
|
||||
{
|
||||
public class ScrobbleRequest : IOperation
|
||||
{
|
||||
private readonly ILogger<ScrobbleRequest> logger;
|
||||
private readonly IUserApi userClient;
|
||||
private readonly ILogger<ScrobbleRequest> _logger;
|
||||
private readonly IUserApi _userClient;
|
||||
|
||||
public event EventHandler Success;
|
||||
public event EventHandler? Success;
|
||||
|
||||
public int MaxAttempts { get; private set; }
|
||||
public int Attempts { get; private set; }
|
||||
public IEnumerable<LastTrack> Scrobbles { get; private set; }
|
||||
public IEnumerable<LastTrack> Scrobbles { get; private set; } = [];
|
||||
public int TotalPages { get; private set; }
|
||||
private Task<PageResponse<LastTrack>> currentTask { get; set; }
|
||||
private Task<PageResponse<LastTrack>>? CurrentTask { get; set; }
|
||||
public bool Succeeded { get; private set; } = false;
|
||||
|
||||
private string username { get; set; }
|
||||
private int pageNumber { get; set; }
|
||||
int pageSize { get; set; }
|
||||
DateTime? from { get; set; }
|
||||
DateTime? to { get; set; }
|
||||
private string Username { get; set; }
|
||||
private int PageNumber { get; set; }
|
||||
private int PageSize { get; set; }
|
||||
private DateTime? From { get; set; }
|
||||
private DateTime? To { get; set; }
|
||||
|
||||
private TaskCompletionSource AggregateTaskSource { get; set; } = new();
|
||||
public Task Task => AggregateTaskSource.Task;
|
||||
|
||||
public ScrobbleRequest(IUserApi _userClient, ILogger<ScrobbleRequest> _logger, string _username,
|
||||
int _pageNumber, int _pageSize, DateTime? _from, DateTime? _to, int maxRetries = 5)
|
||||
public ScrobbleRequest(IUserApi userClient, ILogger<ScrobbleRequest> logger, string username,
|
||||
int pageNumber, int pageSize, DateTime? from, DateTime? to, int maxRetries = 5)
|
||||
{
|
||||
userClient = _userClient;
|
||||
logger = _logger;
|
||||
_userClient = userClient;
|
||||
_logger = logger;
|
||||
|
||||
username = _username;
|
||||
pageNumber = _pageNumber;
|
||||
pageSize = _pageSize;
|
||||
from = _from;
|
||||
to = _to;
|
||||
Username = username;
|
||||
PageNumber = pageNumber;
|
||||
PageSize = pageSize;
|
||||
From = from;
|
||||
To = to;
|
||||
|
||||
MaxAttempts = maxRetries;
|
||||
}
|
||||
|
||||
public Task Execute()
|
||||
{
|
||||
using var scope = logger.BeginScope(new Dictionary<string, object>()
|
||||
using var scope = _logger.BeginScope(new Dictionary<string, object>()
|
||||
{
|
||||
{ "username", username }, { "page_number", pageNumber }, { "page_size", pageSize }, { "from", from },
|
||||
{ "to", to }
|
||||
{ "username", Username }, { "page_number", PageNumber }, { "page_size", PageSize },
|
||||
{ "from", From ?? DateTime.MinValue },
|
||||
{ "to", To ?? DateTime.MinValue }
|
||||
});
|
||||
|
||||
logger.LogInformation("Starting request");
|
||||
_logger.LogInformation("Starting request");
|
||||
|
||||
var netTime = Stopwatch.StartNew();
|
||||
currentTask =
|
||||
userClient.GetRecentScrobbles(username, pagenumber: pageNumber, count: pageSize, from: from, to: to);
|
||||
currentTask.ContinueWith(async t =>
|
||||
CurrentTask =
|
||||
_userClient.GetRecentScrobbles(Username, pagenumber: PageNumber, count: PageSize, from: From, to: To);
|
||||
CurrentTask.ContinueWith(async t =>
|
||||
{
|
||||
using var scope = logger.BeginScope(new Dictionary<string, object>()
|
||||
using var scope = _logger.BeginScope(new Dictionary<string, object>()
|
||||
{
|
||||
{ "username", username }, { "page_number", pageNumber }, { "page_size", pageSize },
|
||||
{ "from", from }, { "to", to }
|
||||
{ "username", Username }, { "page_number", PageNumber }, { "page_size", PageSize },
|
||||
{ "from", From ?? DateTime.MinValue }, { "to", To ?? DateTime.MinValue }
|
||||
});
|
||||
|
||||
try
|
||||
{
|
||||
netTime.Stop();
|
||||
logger.LogTrace("Network request took {:n} ms", netTime.ElapsedMilliseconds);
|
||||
_logger.LogTrace("Network request took {:n} ms", netTime.ElapsedMilliseconds);
|
||||
|
||||
if (t.IsCompletedSuccessfully)
|
||||
{
|
||||
@ -87,13 +88,13 @@ namespace Selector
|
||||
{
|
||||
if (Attempts < MaxAttempts)
|
||||
{
|
||||
logger.LogDebug("Request failed: {}, retrying ({} of {})", result.Status, Attempts + 1,
|
||||
_logger.LogDebug("Request failed: {}, retrying ({} of {})", result.Status, Attempts + 1,
|
||||
MaxAttempts);
|
||||
await Execute();
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.LogDebug("Request failed: {}, max retries exceeded {}, not retrying",
|
||||
_logger.LogDebug("Request failed: {}, max retries exceeded {}, not retrying",
|
||||
result.Status, MaxAttempts);
|
||||
AggregateTaskSource.SetCanceled();
|
||||
}
|
||||
@ -101,13 +102,13 @@ namespace Selector
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.LogError("Scrobble request task faulted, {}", t.Exception);
|
||||
AggregateTaskSource.SetException(t.Exception);
|
||||
_logger.LogError("Scrobble request task faulted, {}", t.Exception);
|
||||
if (t.Exception != null) AggregateTaskSource.SetException(t.Exception);
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
logger.LogError(e, "Error while making scrobble request on attempt {}", Attempts);
|
||||
_logger.LogError(e, "Error while making scrobble request on attempt {}", Attempts);
|
||||
Succeeded = false;
|
||||
}
|
||||
});
|
||||
@ -118,7 +119,7 @@ namespace Selector
|
||||
|
||||
protected virtual void OnSuccess()
|
||||
{
|
||||
Success?.Invoke(this, new EventArgs());
|
||||
Success?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
}
|
||||
}
|
@ -4,6 +4,7 @@
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<IsAotCompatible>true</IsAotCompatible>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
9
Selector.MAUI/MauiJsonContext.cs
Normal file
9
Selector.MAUI/MauiJsonContext.cs
Normal file
@ -0,0 +1,9 @@
|
||||
using System.Text.Json.Serialization;
|
||||
using Selector.MAUI.Services;
|
||||
|
||||
namespace Selector.MAUI;
|
||||
|
||||
[JsonSerializable(typeof(SelectorNetClient.TokenNetworkResponse))]
|
||||
public partial class MauiJsonContext : JsonSerializerContext
|
||||
{
|
||||
}
|
@ -4,13 +4,15 @@
|
||||
<TargetFrameworks Condition="$([MSBuild]::IsOSPlatform('OSX'))">$(TargetFrameworks);net9.0-ios;net9.0-maccatalyst</TargetFrameworks>
|
||||
<TargetFrameworks Condition="$([MSBuild]::IsOSPlatform('windows'))">$(TargetFrameworks);net9.0-windows10.0.19041.0</TargetFrameworks>
|
||||
<!-- Uncomment to also build the tizen app. You will need to install tizen by following this: https://github.com/Samsung/Tizen.NET -->
|
||||
<!-- <TargetFrameworks>$(TargetFrameworks);net8.0-tizen</TargetFrameworks> -->
|
||||
<!-- <TargetFrameworks>$(TargetFrameworks);net9.0-tizen</TargetFrameworks> -->
|
||||
<OutputType>Exe</OutputType>
|
||||
<RootNamespace>Selector.MAUI</RootNamespace>
|
||||
<UseMaui>true</UseMaui>
|
||||
<SingleProject>true</SingleProject>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<EnableDefaultCssItems>false</EnableDefaultCssItems>
|
||||
<!-- <PublishAot>true</PublishAot> -->
|
||||
<IsAotCompatible>true</IsAotCompatible>
|
||||
|
||||
<!-- Display name -->
|
||||
<ApplicationTitle>Selector</ApplicationTitle>
|
||||
|
@ -1,5 +1,4 @@
|
||||
using System;
|
||||
using System.Net;
|
||||
using System.Net;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Net.Http.Json;
|
||||
using Selector.SignalR;
|
||||
@ -45,11 +44,12 @@ public class SelectorNetClient : ISelectorNetClient
|
||||
ArgumentNullException.ThrowIfNullOrEmpty(username);
|
||||
ArgumentNullException.ThrowIfNullOrEmpty(password);
|
||||
|
||||
var result = await _client.PostAsync(_baseUrl + "/api/auth/token", new FormUrlEncodedContent(new Dictionary<string, string>
|
||||
{
|
||||
{ "Username", username },
|
||||
{ "Password", password }
|
||||
}));
|
||||
var result = await _client.PostAsync(_baseUrl + "/api/auth/token", new FormUrlEncodedContent(
|
||||
new Dictionary<string, string>
|
||||
{
|
||||
{ "Username", username },
|
||||
{ "Password", password }
|
||||
}));
|
||||
|
||||
return FormTokenResponse(result);
|
||||
}
|
||||
@ -86,7 +86,7 @@ public class SelectorNetClient : ISelectorNetClient
|
||||
break;
|
||||
case HttpStatusCode.OK:
|
||||
ret.Status = TokenResponseStatus.OK;
|
||||
ret.Token = result.Content.ReadFromJsonAsync<TokenNetworkResponse>().Result.Token;
|
||||
ret.Token = result.Content.ReadFromJsonAsync(MauiJsonContext.Default.TokenNetworkResponse).Result.Token;
|
||||
_nowClient.Token = ret.Token;
|
||||
_pastClient.Token = ret.Token;
|
||||
break;
|
||||
@ -110,7 +110,11 @@ public class SelectorNetClient : ISelectorNetClient
|
||||
|
||||
public enum TokenResponseStatus
|
||||
{
|
||||
Malformed, UserSearchFailed, BadCreds, ExpiredCreds, OK
|
||||
Malformed,
|
||||
UserSearchFailed,
|
||||
BadCreds,
|
||||
ExpiredCreds,
|
||||
OK
|
||||
}
|
||||
|
||||
private class TokenModel
|
||||
@ -118,4 +122,4 @@ public class SelectorNetClient : ISelectorNetClient
|
||||
public string Username { get; set; }
|
||||
public string Password { get; set; }
|
||||
}
|
||||
}
|
||||
}
|
@ -1,31 +1,24 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Selector.Model
|
||||
{
|
||||
public class ApplicationUser : IdentityUser
|
||||
{
|
||||
[PersonalData]
|
||||
public bool SpotifyIsLinked { get; set; }
|
||||
[PersonalData]
|
||||
public DateTime SpotifyLastRefresh { get; set; }
|
||||
[PersonalData] public bool SpotifyIsLinked { get; set; }
|
||||
[PersonalData] public DateTime SpotifyLastRefresh { get; set; }
|
||||
public int SpotifyTokenExpiry { get; set; }
|
||||
public string SpotifyAccessToken { get; set; }
|
||||
public string SpotifyRefreshToken { get; set; }
|
||||
|
||||
[PersonalData]
|
||||
public bool AppleMusicLinked { get; set; }
|
||||
[PersonalData] public bool AppleMusicLinked { get; set; }
|
||||
public string AppleMusicKey { get; set; }
|
||||
[PersonalData]
|
||||
public DateTime AppleMusicLastRefresh { get; set; }
|
||||
[PersonalData] public DateTime AppleMusicLastRefresh { get; set; }
|
||||
|
||||
[PersonalData]
|
||||
public string LastFmUsername { get; set; }
|
||||
[PersonalData]
|
||||
public bool SaveScrobbles { get; set; }
|
||||
[PersonalData] public string LastFmUsername { get; set; }
|
||||
[PersonalData] public string LastFmPassword { get; set; }
|
||||
[PersonalData] public bool SaveScrobbles { get; set; }
|
||||
|
||||
public List<Watcher> Watchers { get; set; }
|
||||
public List<UserScrobble> Scrobbles { get; set; }
|
||||
@ -51,7 +44,8 @@ namespace Selector.Model
|
||||
|
||||
public string LastFmUsername { get; set; }
|
||||
|
||||
public static explicit operator ApplicationUserDTO(ApplicationUser user) => new() {
|
||||
public static explicit operator ApplicationUserDTO(ApplicationUser user) => new()
|
||||
{
|
||||
Id = user.Id,
|
||||
UserName = user.UserName,
|
||||
Email = user.Email,
|
||||
|
141
Selector.Model/Listen/AppleListenRepository.cs
Normal file
141
Selector.Model/Listen/AppleListenRepository.cs
Normal file
@ -0,0 +1,141 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Selector.Model
|
||||
{
|
||||
public class AppleListenRepository : IAppleListenRepository
|
||||
{
|
||||
private readonly ApplicationDbContext db;
|
||||
|
||||
public AppleListenRepository(ApplicationDbContext context)
|
||||
{
|
||||
db = context;
|
||||
}
|
||||
|
||||
public void Add(AppleMusicListen item)
|
||||
{
|
||||
db.AppleMusicListen.Add(item);
|
||||
}
|
||||
|
||||
public void AddRange(IEnumerable<AppleMusicListen> item)
|
||||
{
|
||||
db.AppleMusicListen.AddRange(item);
|
||||
}
|
||||
|
||||
public AppleMusicListen Find(DateTime key, string include = null)
|
||||
{
|
||||
var listens = db.AppleMusicListen.Where(s => s.Timestamp == key);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(include))
|
||||
{
|
||||
listens = listens.Include(include);
|
||||
}
|
||||
|
||||
return listens.FirstOrDefault();
|
||||
}
|
||||
|
||||
public IQueryable<IListen> GetAll(string include = null, string userId = null, string username = null,
|
||||
string trackName = null, string albumName = null, string artistName = null, DateTime? from = null,
|
||||
DateTime? to = null, bool tracking = true, bool orderTime = false)
|
||||
{
|
||||
var listens = db.AppleMusicListen.AsQueryable();
|
||||
|
||||
if (!tracking)
|
||||
{
|
||||
listens = listens.AsNoTracking();
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(include))
|
||||
{
|
||||
listens = listens.Include(include);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(userId))
|
||||
{
|
||||
listens = listens.Where(s => s.UserId == userId);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(username))
|
||||
{
|
||||
var normalUsername = username.ToUpperInvariant();
|
||||
var user = db.Users.AsNoTracking().Where(u => u.NormalizedUserName == normalUsername).FirstOrDefault();
|
||||
if (user is not null)
|
||||
{
|
||||
listens = listens.Where(s => s.UserId == user.Id);
|
||||
}
|
||||
else
|
||||
{
|
||||
listens = Enumerable.Empty<AppleMusicListen>().AsQueryable();
|
||||
}
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(trackName))
|
||||
{
|
||||
listens = listens.Where(s => s.TrackName == trackName);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(albumName))
|
||||
{
|
||||
listens = listens.Where(s => s.AlbumName == albumName);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(artistName))
|
||||
{
|
||||
listens = listens.Where(s => s.ArtistName == artistName);
|
||||
}
|
||||
|
||||
if (from is not null)
|
||||
{
|
||||
listens = listens.Where(u => u.Timestamp >= from.Value);
|
||||
}
|
||||
|
||||
if (to is not null)
|
||||
{
|
||||
listens = listens.Where(u => u.Timestamp < to.Value);
|
||||
}
|
||||
|
||||
if (orderTime)
|
||||
{
|
||||
listens = listens.OrderBy(x => x.Timestamp);
|
||||
}
|
||||
|
||||
return listens;
|
||||
}
|
||||
|
||||
// public IEnumerable<IListen> GetAll(string include = null, string userId = null, string username = null, string trackName = null, string albumName = null, string artistName = null, DateTime? from = null, DateTime? to = null, bool tracking = true, bool orderTime = false)
|
||||
// => GetAllQueryable(include: include, userId: userId, username: username, trackName: trackName, albumName: albumName, artistName: artistName, from: from, to: to, tracking: tracking, orderTime: orderTime).AsEnumerable();
|
||||
|
||||
public void Remove(DateTime key)
|
||||
{
|
||||
Remove(Find(key));
|
||||
}
|
||||
|
||||
public void Remove(AppleMusicListen scrobble)
|
||||
{
|
||||
db.AppleMusicListen.Remove(scrobble);
|
||||
}
|
||||
|
||||
public void RemoveRange(IEnumerable<AppleMusicListen> scrobbles)
|
||||
{
|
||||
db.AppleMusicListen.RemoveRange(scrobbles);
|
||||
}
|
||||
|
||||
public void Update(AppleMusicListen item)
|
||||
{
|
||||
db.AppleMusicListen.Update(item);
|
||||
}
|
||||
|
||||
public Task<int> Save()
|
||||
{
|
||||
return db.SaveChangesAsync();
|
||||
}
|
||||
|
||||
public int Count(string userId = null, string username = null, string trackName = null, string albumName = null,
|
||||
string artistName = null, DateTime? from = null, DateTime? to = null)
|
||||
=> GetAll(userId: userId, username: username, trackName: trackName, albumName: albumName,
|
||||
artistName: artistName, from: from, to: to, tracking: false).Count();
|
||||
}
|
||||
}
|
@ -8,6 +8,7 @@ public class AppleMusicListen : Listen, IUserListen
|
||||
|
||||
public string TrackId { get; set; }
|
||||
public string Isrc { get; set; }
|
||||
public bool IsScrobbled { get; set; }
|
||||
|
||||
public string UserId { get; set; }
|
||||
public ApplicationUser User { get; set; }
|
||||
@ -18,6 +19,7 @@ public class AppleMusicListen : Listen, IUserListen
|
||||
|
||||
TrackId = track.Track.Id,
|
||||
Isrc = track.Track.Attributes.Isrc,
|
||||
IsScrobbled = track.Scrobbled,
|
||||
|
||||
TrackName = track.Track.Attributes.Name,
|
||||
AlbumName = track.Track.Attributes.AlbumName,
|
||||
|
23
Selector.Model/Listen/IAppleListenRepository.cs
Normal file
23
Selector.Model/Listen/IAppleListenRepository.cs
Normal file
@ -0,0 +1,23 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Selector.Model
|
||||
{
|
||||
public interface IAppleListenRepository : IListenRepository
|
||||
{
|
||||
void Add(AppleMusicListen item);
|
||||
|
||||
void AddRange(IEnumerable<AppleMusicListen> item);
|
||||
|
||||
//IEnumerable<AppleMusicListen> GetAll(string include = null, string userId = null, string username = null, string trackName = null, string albumName = null, string artistName = null, DateTime? from = null, DateTime? to = null);
|
||||
AppleMusicListen Find(DateTime key, string include = null);
|
||||
void Remove(DateTime key);
|
||||
public void Remove(AppleMusicListen scrobble);
|
||||
public void RemoveRange(IEnumerable<AppleMusicListen> scrobbles);
|
||||
void Update(AppleMusicListen item);
|
||||
|
||||
Task<int> Save();
|
||||
//int Count(string userId = null, string username = null, string trackName = null, string albumName = null, string artistName = null, DateTime? from = null, DateTime? to = null);
|
||||
}
|
||||
}
|
@ -4,6 +4,7 @@
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<EnableDefaultCompileItems>true</EnableDefaultCompileItems>
|
||||
<LangVersion>latest</LangVersion>
|
||||
<IsAotCompatible>true</IsAotCompatible>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
@ -1,33 +1,27 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Selector.Model.Services
|
||||
{
|
||||
public class MigratorService : IHostedService
|
||||
public class MigratorService(
|
||||
IServiceScopeFactory scopeProvider,
|
||||
IOptions<DatabaseOptions> options,
|
||||
ILogger<MigratorService> logger)
|
||||
: IHostedService
|
||||
{
|
||||
private readonly IServiceScopeFactory scopeProvider;
|
||||
private readonly DatabaseOptions options;
|
||||
private readonly ILogger<MigratorService> logger;
|
||||
|
||||
public MigratorService(IServiceScopeFactory _scopeProvider, IOptions<DatabaseOptions> _options, ILogger<MigratorService> _logger)
|
||||
{
|
||||
scopeProvider = _scopeProvider;
|
||||
options = _options.Value;
|
||||
logger = _logger;
|
||||
}
|
||||
private readonly DatabaseOptions _options = options.Value;
|
||||
|
||||
public Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
if(options.Migrate)
|
||||
if (_options.Migrate)
|
||||
{
|
||||
using var scope = scopeProvider.CreateScope();
|
||||
|
||||
|
||||
logger.LogInformation("Applying migrations");
|
||||
scope.ServiceProvider.GetRequiredService<ApplicationDbContext>().Database.Migrate();
|
||||
}
|
||||
@ -40,4 +34,4 @@ namespace Selector.Model.Services
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,16 +1,15 @@
|
||||
using System;
|
||||
using Microsoft.AspNetCore.SignalR.Client;
|
||||
using Microsoft.AspNetCore.SignalR.Client;
|
||||
|
||||
namespace Selector.SignalR;
|
||||
|
||||
public abstract class BaseSignalRClient: IAsyncDisposable
|
||||
public abstract class BaseSignalRClient : IAsyncDisposable
|
||||
{
|
||||
private readonly string _baseUrl;
|
||||
protected HubConnection hubConnection;
|
||||
public string Token { get; set; }
|
||||
public string? Token { get; set; }
|
||||
|
||||
public BaseSignalRClient(string path, string token)
|
||||
{
|
||||
public BaseSignalRClient(string path, string? token)
|
||||
{
|
||||
//var baseOverride = Environment.GetEnvironmentVariable("SELECTOR_BASE_URL");
|
||||
|
||||
//if (!string.IsNullOrWhiteSpace(baseOverride))
|
||||
@ -26,13 +25,8 @@ public abstract class BaseSignalRClient: IAsyncDisposable
|
||||
_baseUrl = "https://selector.sarsoo.xyz";
|
||||
|
||||
hubConnection = new HubConnectionBuilder()
|
||||
.WithUrl(_baseUrl + "/" + path, options =>
|
||||
{
|
||||
options.AccessTokenProvider = () =>
|
||||
{
|
||||
return Task.FromResult(Token);
|
||||
};
|
||||
})
|
||||
.WithUrl(_baseUrl + "/" + path,
|
||||
options => { options.AccessTokenProvider = () => { return Task.FromResult(Token); }; })
|
||||
.WithAutomaticReconnect()
|
||||
.Build();
|
||||
}
|
||||
@ -48,5 +42,4 @@ public abstract class BaseSignalRClient: IAsyncDisposable
|
||||
{
|
||||
await hubConnection.StartAsync();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -1,10 +1,6 @@
|
||||
using System;
|
||||
using Selector.SignalR;
|
||||
|
||||
namespace Selector.SignalR;
|
||||
namespace Selector.SignalR;
|
||||
|
||||
public class Card
|
||||
{
|
||||
public string Content { get; set; }
|
||||
}
|
||||
|
||||
public required string Content { get; set; }
|
||||
}
|
@ -1,15 +1,11 @@
|
||||
using System;
|
||||
using Selector.SignalR;
|
||||
|
||||
namespace Selector.SignalR;
|
||||
namespace Selector.SignalR;
|
||||
|
||||
public class PastParams
|
||||
{
|
||||
public string Track { get; set; }
|
||||
public string Album { get; set; }
|
||||
public string Artist { get; set; }
|
||||
|
||||
public string From { get; set; }
|
||||
public string To { get; set; }
|
||||
}
|
||||
public required string Track { get; set; }
|
||||
public required string Album { get; set; }
|
||||
public required string Artist { get; set; }
|
||||
|
||||
public required string From { get; set; }
|
||||
public required string To { get; set; }
|
||||
}
|
@ -9,64 +9,64 @@ namespace Selector.SignalR;
|
||||
public class NowHubCache
|
||||
{
|
||||
private readonly NowHubClient _connection;
|
||||
private readonly ILogger<NowHubCache> logger;
|
||||
private readonly ILogger<NowHubCache> _logger;
|
||||
|
||||
public TrackAudioFeatures LastFeature { get; private set; }
|
||||
public TrackAudioFeatures? LastFeature { get; private set; }
|
||||
public List<Card> LastCards { get; private set; } = new();
|
||||
private readonly object updateLock = new();
|
||||
private readonly object _updateLock = new();
|
||||
|
||||
private readonly object bindingLock = new();
|
||||
private bool isBound = false;
|
||||
private readonly object _bindingLock = new();
|
||||
private bool _isBound = false;
|
||||
|
||||
public PlayCount LastPlayCount { get; private set; }
|
||||
public SpotifyCurrentlyPlayingDTO LastPlayingSpotify { get; private set; }
|
||||
public AppleCurrentlyPlayingDTO LastPlayingApple { get; private set; }
|
||||
public PlayCount? LastPlayCount { get; private set; }
|
||||
public SpotifyCurrentlyPlayingDTO? LastPlayingSpotify { get; private set; }
|
||||
public AppleCurrentlyPlayingDTO? LastPlayingApple { get; private set; }
|
||||
|
||||
public event EventHandler NewAudioFeature;
|
||||
public event EventHandler NewCard;
|
||||
public event EventHandler NewPlayCount;
|
||||
public event EventHandler NewNowPlayingSpotify;
|
||||
public event EventHandler NewNowPlayingApple;
|
||||
public event EventHandler? NewAudioFeature;
|
||||
public event EventHandler? NewCard;
|
||||
public event EventHandler? NewPlayCount;
|
||||
public event EventHandler? NewNowPlayingSpotify;
|
||||
public event EventHandler? NewNowPlayingApple;
|
||||
|
||||
public NowHubCache(NowHubClient connection, ILogger<NowHubCache> logger)
|
||||
{
|
||||
_connection = connection;
|
||||
this.logger = logger;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public void BindClient()
|
||||
{
|
||||
lock (bindingLock)
|
||||
lock (_bindingLock)
|
||||
{
|
||||
if (!isBound)
|
||||
if (!_isBound)
|
||||
{
|
||||
_connection.OnNewAudioFeature(af =>
|
||||
{
|
||||
lock (updateLock)
|
||||
lock (_updateLock)
|
||||
{
|
||||
logger.LogInformation("New audio features received: {0}", af);
|
||||
_logger.LogInformation("New audio features received: {0}", af);
|
||||
LastFeature = af;
|
||||
NewAudioFeature?.Invoke(this, null);
|
||||
NewAudioFeature?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
});
|
||||
|
||||
_connection.OnNewCard(c =>
|
||||
{
|
||||
lock (updateLock)
|
||||
lock (_updateLock)
|
||||
{
|
||||
logger.LogInformation("New card received: {0}", c);
|
||||
_logger.LogInformation("New card received: {0}", c);
|
||||
LastCards.Add(c);
|
||||
NewCard?.Invoke(this, null);
|
||||
NewCard?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
});
|
||||
|
||||
_connection.OnNewPlayCount(pc =>
|
||||
{
|
||||
lock (updateLock)
|
||||
lock (_updateLock)
|
||||
{
|
||||
logger.LogInformation("New play count received: {0}", pc);
|
||||
_logger.LogInformation("New play count received: {0}", pc);
|
||||
LastPlayCount = pc;
|
||||
NewPlayCount?.Invoke(this, null);
|
||||
NewPlayCount?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
});
|
||||
|
||||
@ -74,12 +74,12 @@ public class NowHubCache
|
||||
{
|
||||
try
|
||||
{
|
||||
lock (updateLock)
|
||||
lock (_updateLock)
|
||||
{
|
||||
logger.LogInformation("New Spotify now playing recieved: {0}", np);
|
||||
_logger.LogInformation("New Spotify now playing recieved: {0}", np);
|
||||
LastPlayingSpotify = np;
|
||||
LastCards.Clear();
|
||||
NewNowPlayingSpotify?.Invoke(this, null);
|
||||
NewNowPlayingSpotify?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
|
||||
if (LastPlayingSpotify?.Track is not null)
|
||||
@ -106,20 +106,20 @@ public class NowHubCache
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
logger.LogError(e, "Error while handling new Spotify now playing");
|
||||
_logger.LogError(e, "Error while handling new Spotify now playing");
|
||||
}
|
||||
});
|
||||
|
||||
_connection.OnNewPlayingApple(async np =>
|
||||
_connection.OnNewPlayingApple(np =>
|
||||
{
|
||||
try
|
||||
{
|
||||
lock (updateLock)
|
||||
lock (_updateLock)
|
||||
{
|
||||
logger.LogInformation("New Apple now playing recieved: {0}", np);
|
||||
_logger.LogInformation("New Apple now playing recieved: {0}", np);
|
||||
LastPlayingApple = np;
|
||||
LastCards.Clear();
|
||||
NewNowPlayingApple?.Invoke(this, null);
|
||||
NewNowPlayingApple?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
|
||||
if (LastPlayingApple?.Track is not null)
|
||||
@ -146,11 +146,13 @@ public class NowHubCache
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
logger.LogError(e, "Error while handling new Apple now playing");
|
||||
_logger.LogError(e, "Error while handling new Apple now playing");
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
});
|
||||
|
||||
isBound = true;
|
||||
_isBound = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -8,65 +8,65 @@ namespace Selector.SignalR;
|
||||
|
||||
public class NowHubClient : BaseSignalRClient, INowPlayingHub, IDisposable
|
||||
{
|
||||
private List<IDisposable> NewPlayingSpotifyCallbacks = new();
|
||||
private List<IDisposable> NewPlayingAppleCallbacks = new();
|
||||
private List<IDisposable> NewAudioFeatureCallbacks = new();
|
||||
private List<IDisposable> NewPlayCountCallbacks = new();
|
||||
private List<IDisposable> NewCardCallbacks = new();
|
||||
private bool disposedValue;
|
||||
private readonly List<IDisposable> _newPlayingSpotifyCallbacks = new();
|
||||
private readonly List<IDisposable> _newPlayingAppleCallbacks = new();
|
||||
private readonly List<IDisposable> _newAudioFeatureCallbacks = new();
|
||||
private readonly List<IDisposable> _newPlayCountCallbacks = new();
|
||||
private readonly List<IDisposable> _newCardCallbacks = new();
|
||||
private bool _disposedValue;
|
||||
|
||||
public NowHubClient(string token = null) : base("nowhub", token)
|
||||
public NowHubClient(string? token = null) : base("nowhub", token)
|
||||
{
|
||||
}
|
||||
|
||||
public void OnNewPlayingSpotify(Action<SpotifyCurrentlyPlayingDTO> action)
|
||||
{
|
||||
NewPlayingSpotifyCallbacks.Add(hubConnection.On(nameof(OnNewPlayingSpotify), action));
|
||||
_newPlayingSpotifyCallbacks.Add(hubConnection.On(nameof(OnNewPlayingSpotify), action));
|
||||
}
|
||||
|
||||
public void OnNewPlayingApple(Action<AppleCurrentlyPlayingDTO> action)
|
||||
{
|
||||
NewPlayingAppleCallbacks.Add(hubConnection.On(nameof(OnNewPlayingApple), action));
|
||||
_newPlayingAppleCallbacks.Add(hubConnection.On(nameof(OnNewPlayingApple), action));
|
||||
}
|
||||
|
||||
public void OnNewAudioFeature(Action<TrackAudioFeatures> action)
|
||||
{
|
||||
NewAudioFeatureCallbacks.Add(hubConnection.On(nameof(OnNewAudioFeature), action));
|
||||
_newAudioFeatureCallbacks.Add(hubConnection.On(nameof(OnNewAudioFeature), action));
|
||||
}
|
||||
|
||||
public void OnNewPlayCount(Action<PlayCount> action)
|
||||
{
|
||||
NewPlayCountCallbacks.Add(hubConnection.On(nameof(OnNewPlayCount), action));
|
||||
_newPlayCountCallbacks.Add(hubConnection.On(nameof(OnNewPlayCount), action));
|
||||
}
|
||||
|
||||
public void OnNewCard(Action<Card> action)
|
||||
{
|
||||
NewCardCallbacks.Add(hubConnection.On(nameof(OnNewCard), action));
|
||||
_newCardCallbacks.Add(hubConnection.On(nameof(OnNewCard), action));
|
||||
}
|
||||
|
||||
public void OnNewPlayingSpotify(Func<SpotifyCurrentlyPlayingDTO, Task> action)
|
||||
{
|
||||
NewPlayingSpotifyCallbacks.Add(hubConnection.On(nameof(OnNewPlayingSpotify), action));
|
||||
_newPlayingSpotifyCallbacks.Add(hubConnection.On(nameof(OnNewPlayingSpotify), action));
|
||||
}
|
||||
|
||||
public void OnNewPlayingApple(Func<AppleCurrentlyPlayingDTO, Task> action)
|
||||
{
|
||||
NewPlayingAppleCallbacks.Add(hubConnection.On(nameof(OnNewPlayingApple), action));
|
||||
_newPlayingAppleCallbacks.Add(hubConnection.On(nameof(OnNewPlayingApple), action));
|
||||
}
|
||||
|
||||
public void OnNewAudioFeature(Func<TrackAudioFeatures, Task> action)
|
||||
{
|
||||
NewAudioFeatureCallbacks.Add(hubConnection.On(nameof(OnNewAudioFeature), action));
|
||||
_newAudioFeatureCallbacks.Add(hubConnection.On(nameof(OnNewAudioFeature), action));
|
||||
}
|
||||
|
||||
public void OnNewPlayCount(Func<PlayCount, Task> action)
|
||||
{
|
||||
NewPlayCountCallbacks.Add(hubConnection.On(nameof(OnNewPlayCount), action));
|
||||
_newPlayCountCallbacks.Add(hubConnection.On(nameof(OnNewPlayCount), action));
|
||||
}
|
||||
|
||||
public void OnNewCard(Func<Card, Task> action)
|
||||
{
|
||||
NewCardCallbacks.Add(hubConnection.On(nameof(OnNewCard), action));
|
||||
_newCardCallbacks.Add(hubConnection.On(nameof(OnNewCard), action));
|
||||
}
|
||||
|
||||
public Task OnConnected()
|
||||
@ -102,14 +102,14 @@ public class NowHubClient : BaseSignalRClient, INowPlayingHub, IDisposable
|
||||
|
||||
protected virtual void Dispose(bool disposing)
|
||||
{
|
||||
if (!disposedValue)
|
||||
if (!_disposedValue)
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
foreach (var callback in NewPlayingSpotifyCallbacks
|
||||
.Concat(NewAudioFeatureCallbacks)
|
||||
.Concat(NewPlayCountCallbacks)
|
||||
.Concat(NewCardCallbacks))
|
||||
foreach (var callback in _newPlayingSpotifyCallbacks
|
||||
.Concat(_newAudioFeatureCallbacks)
|
||||
.Concat(_newPlayCountCallbacks)
|
||||
.Concat(_newCardCallbacks))
|
||||
{
|
||||
callback.Dispose();
|
||||
}
|
||||
@ -117,7 +117,7 @@ public class NowHubClient : BaseSignalRClient, INowPlayingHub, IDisposable
|
||||
base.DisposeAsync();
|
||||
}
|
||||
|
||||
disposedValue = true;
|
||||
_disposedValue = true;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -2,18 +2,18 @@
|
||||
|
||||
namespace Selector.SignalR;
|
||||
|
||||
public class PastHubClient: BaseSignalRClient, IPastHub, IDisposable
|
||||
public class PastHubClient : BaseSignalRClient, IPastHub, IDisposable
|
||||
{
|
||||
private List<IDisposable> SearchResultCallbacks = new();
|
||||
private bool disposedValue;
|
||||
private List<IDisposable> _searchResultCallbacks = new();
|
||||
private bool _disposedValue;
|
||||
|
||||
public PastHubClient(string token = null): base("nowhub", token)
|
||||
{
|
||||
}
|
||||
public PastHubClient(string? token = null) : base("nowhub", token)
|
||||
{
|
||||
}
|
||||
|
||||
public void OnRankResult(Action<IRankResult> action)
|
||||
{
|
||||
SearchResultCallbacks.Add(hubConnection.On(nameof(OnRankResult), action));
|
||||
_searchResultCallbacks.Add(hubConnection.On(nameof(OnRankResult), action));
|
||||
}
|
||||
|
||||
public Task OnConnected()
|
||||
@ -28,11 +28,11 @@ public class PastHubClient: BaseSignalRClient, IPastHub, IDisposable
|
||||
|
||||
protected virtual void Dispose(bool disposing)
|
||||
{
|
||||
if (!disposedValue)
|
||||
if (!_disposedValue)
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
foreach(var callback in SearchResultCallbacks)
|
||||
foreach (var callback in _searchResultCallbacks)
|
||||
{
|
||||
callback.Dispose();
|
||||
}
|
||||
@ -40,7 +40,7 @@ public class PastHubClient: BaseSignalRClient, IPastHub, IDisposable
|
||||
base.DisposeAsync();
|
||||
}
|
||||
|
||||
disposedValue = true;
|
||||
_disposedValue = true;
|
||||
}
|
||||
}
|
||||
|
||||
@ -50,5 +50,4 @@ public class PastHubClient: BaseSignalRClient, IPastHub, IDisposable
|
||||
Dispose(disposing: true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -2,20 +2,16 @@
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>net9.0</TargetFrameworks>
|
||||
<TargetFrameworks Condition="$([MSBuild]::IsOSPlatform('OSX'))">$(TargetFrameworks);net9.0-ios;net9.0-maccatalyst</TargetFrameworks>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<IsAotCompatible>true</IsAotCompatible>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Selector.AppleMusic\Selector.AppleMusic.csproj"/>
|
||||
<ProjectReference Include="..\Selector.Spotify\Selector.Spotify.csproj"/>
|
||||
<ProjectReference Include="..\Selector\Selector.csproj" />
|
||||
<!-- <ProjectReference Include="..\Selector.Model\Selector.Model.csproj" /> -->
|
||||
</ItemGroup>
|
||||
<!-- <ItemGroup>
|
||||
<None Remove="Microsoft.AspNetCore.SignalR.Client" />
|
||||
</ItemGroup> -->
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="9.0.3"/>
|
||||
</ItemGroup>
|
||||
|
@ -18,9 +18,9 @@ namespace Selector.Spotify.Consumer
|
||||
public AnalysedTrackTimeline Timeline { get; set; } = new();
|
||||
|
||||
public AudioFeatureInjector(
|
||||
ISpotifyPlayerWatcher watcher,
|
||||
ISpotifyPlayerWatcher? watcher,
|
||||
ITracksClient trackClient,
|
||||
ILogger<AudioFeatureInjector> logger = null,
|
||||
ILogger<AudioFeatureInjector> logger,
|
||||
CancellationToken token = default
|
||||
) : base(watcher, logger)
|
||||
{
|
||||
@ -58,12 +58,12 @@ namespace Selector.Spotify.Consumer
|
||||
catch (APITooManyRequestsException ex)
|
||||
{
|
||||
Logger.LogDebug("Too many requests error: [{message}]", ex.Message);
|
||||
throw ex;
|
||||
throw;
|
||||
}
|
||||
catch (APIException ex)
|
||||
{
|
||||
Logger.LogDebug("API error: [{message}]", ex.Message);
|
||||
throw ex;
|
||||
throw;
|
||||
}
|
||||
}
|
||||
else if (e.Current.Item is FullEpisode episode)
|
||||
|
@ -4,6 +4,7 @@ using SpotifyAPI.Web;
|
||||
|
||||
namespace Selector.Spotify.Consumer
|
||||
{
|
||||
[Obsolete]
|
||||
public class DummyAudioFeatureInjector : AudioFeatureInjector
|
||||
{
|
||||
private TrackAudioFeatures[] _features = new[]
|
||||
@ -33,7 +34,7 @@ namespace Selector.Spotify.Consumer
|
||||
|
||||
public DummyAudioFeatureInjector(
|
||||
ISpotifyPlayerWatcher watcher,
|
||||
ILogger<DummyAudioFeatureInjector> logger = null,
|
||||
ILogger<DummyAudioFeatureInjector> logger,
|
||||
CancellationToken token = default
|
||||
) : base(watcher, null, logger, token)
|
||||
{
|
||||
|
@ -8,7 +8,7 @@ namespace Selector.Spotify.Consumer.Factory
|
||||
public interface IAudioFeatureInjectorFactory
|
||||
{
|
||||
public Task<ISpotifyPlayerConsumer> Get(ISpotifyConfigFactory spotifyFactory,
|
||||
ISpotifyPlayerWatcher watcher = null);
|
||||
ISpotifyPlayerWatcher? watcher = null);
|
||||
}
|
||||
|
||||
[Obsolete]
|
||||
@ -22,7 +22,7 @@ namespace Selector.Spotify.Consumer.Factory
|
||||
}
|
||||
|
||||
public async Task<ISpotifyPlayerConsumer> Get(ISpotifyConfigFactory spotifyFactory,
|
||||
ISpotifyPlayerWatcher watcher = null)
|
||||
ISpotifyPlayerWatcher? watcher = null)
|
||||
{
|
||||
if (!Magic.Dummy)
|
||||
{
|
||||
|
@ -5,32 +5,32 @@ namespace Selector.Spotify.Consumer.Factory
|
||||
{
|
||||
public interface IPlayCounterFactory
|
||||
{
|
||||
public Task<ISpotifyPlayerConsumer> Get(LastfmClient fmClient = null, LastFmCredentials creds = null,
|
||||
ISpotifyPlayerWatcher watcher = null);
|
||||
public Task<ISpotifyPlayerConsumer> Get(LastfmClient? fmClient = null, LastFmCredentials? creds = null,
|
||||
ISpotifyPlayerWatcher? watcher = null);
|
||||
}
|
||||
|
||||
public class PlayCounterFactory : IPlayCounterFactory
|
||||
{
|
||||
private readonly ILoggerFactory LoggerFactory;
|
||||
private readonly LastfmClient Client;
|
||||
private readonly LastFmCredentials Creds;
|
||||
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;
|
||||
_loggerFactory = loggerFactory;
|
||||
_client = client;
|
||||
_creds = creds;
|
||||
}
|
||||
|
||||
public Task<ISpotifyPlayerConsumer> Get(LastfmClient fmClient = null, LastFmCredentials creds = null,
|
||||
ISpotifyPlayerWatcher watcher = null)
|
||||
public Task<ISpotifyPlayerConsumer> Get(LastfmClient? fmClient = null, LastFmCredentials? creds = null,
|
||||
ISpotifyPlayerWatcher? watcher = null)
|
||||
{
|
||||
var client = fmClient ?? Client;
|
||||
var client = fmClient ?? _client;
|
||||
|
||||
if (client is null)
|
||||
{
|
||||
throw new ArgumentNullException("No Last.fm client provided");
|
||||
throw new ArgumentNullException(nameof(client));
|
||||
}
|
||||
|
||||
return Task.FromResult<ISpotifyPlayerConsumer>(new PlayCounter(
|
||||
@ -39,8 +39,8 @@ namespace Selector.Spotify.Consumer.Factory
|
||||
client.Album,
|
||||
client.Artist,
|
||||
client.User,
|
||||
credentials: creds ?? Creds,
|
||||
LoggerFactory.CreateLogger<PlayCounter>()
|
||||
credentials: creds ?? _creds,
|
||||
_loggerFactory.CreateLogger<PlayCounter>()
|
||||
));
|
||||
}
|
||||
}
|
||||
|
@ -4,27 +4,27 @@ namespace Selector.Spotify.Consumer.Factory
|
||||
{
|
||||
public interface IWebHookFactory
|
||||
{
|
||||
public Task<WebHook> Get(WebHookConfig config, ISpotifyPlayerWatcher watcher = null);
|
||||
public Task<WebHook> Get(WebHookConfig config, ISpotifyPlayerWatcher? watcher = null);
|
||||
}
|
||||
|
||||
public class WebHookFactory : IWebHookFactory
|
||||
{
|
||||
private readonly ILoggerFactory LoggerFactory;
|
||||
private readonly HttpClient Http;
|
||||
private readonly ILoggerFactory _loggerFactory;
|
||||
private readonly HttpClient _http;
|
||||
|
||||
public WebHookFactory(ILoggerFactory loggerFactory, HttpClient httpClient)
|
||||
{
|
||||
LoggerFactory = loggerFactory;
|
||||
Http = httpClient;
|
||||
_loggerFactory = loggerFactory;
|
||||
_http = httpClient;
|
||||
}
|
||||
|
||||
public Task<WebHook> Get(WebHookConfig config, ISpotifyPlayerWatcher watcher = null)
|
||||
public Task<WebHook> Get(WebHookConfig config, ISpotifyPlayerWatcher? watcher = null)
|
||||
{
|
||||
return Task.FromResult(new WebHook(
|
||||
watcher,
|
||||
Http,
|
||||
_http,
|
||||
config,
|
||||
LoggerFactory.CreateLogger<WebHook>()
|
||||
_loggerFactory.CreateLogger<WebHook>()
|
||||
));
|
||||
}
|
||||
}
|
||||
|
@ -13,8 +13,8 @@ namespace Selector.Spotify.Consumer
|
||||
IAlbumApi albumClient,
|
||||
IArtistApi artistClient,
|
||||
IUserApi userClient,
|
||||
LastFmCredentials credentials = null,
|
||||
ILogger<PlayCounter> logger = null,
|
||||
LastFmCredentials? credentials = null,
|
||||
ILogger<PlayCounter>? logger = null,
|
||||
CancellationToken token = default)
|
||||
: BaseParallelPlayerConsumer<ISpotifyPlayerWatcher, SpotifyListeningChangeEventArgs>(watcher, logger),
|
||||
ISpotifyPlayerConsumer
|
||||
@ -24,10 +24,10 @@ namespace Selector.Spotify.Consumer
|
||||
protected readonly IAlbumApi AlbumClient = albumClient;
|
||||
protected readonly IArtistApi ArtistClient = artistClient;
|
||||
protected readonly IUserApi UserClient = userClient;
|
||||
public readonly LastFmCredentials Credentials = credentials;
|
||||
protected readonly ILogger<PlayCounter> Logger = logger ?? NullLogger<PlayCounter>.Instance;
|
||||
public readonly LastFmCredentials? Credentials = credentials;
|
||||
protected new readonly ILogger<PlayCounter> Logger = logger ?? NullLogger<PlayCounter>.Instance;
|
||||
|
||||
protected event EventHandler<PlayCount> NewPlayCount;
|
||||
protected event EventHandler<PlayCount>? NewPlayCount;
|
||||
|
||||
public CancellationToken CancelToken { get; set; } = token;
|
||||
|
||||
|
@ -5,14 +5,14 @@ namespace Selector.Spotify.Consumer
|
||||
{
|
||||
public class WebHookConfig
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public IEnumerable<Predicate<SpotifyListeningChangeEventArgs>> Predicates { get; set; }
|
||||
public string Url { get; set; }
|
||||
public HttpContent Content { get; set; }
|
||||
public required string Name { get; set; }
|
||||
public IEnumerable<Predicate<SpotifyListeningChangeEventArgs>> Predicates { get; set; } = [];
|
||||
public required string Url { get; set; }
|
||||
public HttpContent? Content { get; set; }
|
||||
|
||||
public bool ShouldRequest(SpotifyListeningChangeEventArgs e)
|
||||
{
|
||||
if (Predicates is not null)
|
||||
if (Predicates.Any())
|
||||
{
|
||||
return Predicates.Select(p => p(e)).Aggregate((a, b) => a && b);
|
||||
}
|
||||
@ -24,10 +24,10 @@ namespace Selector.Spotify.Consumer
|
||||
}
|
||||
|
||||
public class WebHook(
|
||||
ISpotifyPlayerWatcher watcher,
|
||||
ISpotifyPlayerWatcher? watcher,
|
||||
HttpClient httpClient,
|
||||
WebHookConfig config,
|
||||
ILogger<WebHook> logger = null,
|
||||
ILogger<WebHook> logger,
|
||||
CancellationToken token = default)
|
||||
: BaseParallelPlayerConsumer<ISpotifyPlayerWatcher, SpotifyListeningChangeEventArgs>(watcher, logger),
|
||||
ISpotifyPlayerConsumer
|
||||
@ -36,9 +36,9 @@ namespace Selector.Spotify.Consumer
|
||||
|
||||
protected readonly WebHookConfig Config = config;
|
||||
|
||||
public event EventHandler PredicatePass;
|
||||
public event EventHandler SuccessfulRequest;
|
||||
public event EventHandler FailedRequest;
|
||||
public event EventHandler? PredicatePass;
|
||||
public event EventHandler? SuccessfulRequest;
|
||||
public event EventHandler? FailedRequest;
|
||||
|
||||
public CancellationToken CancelToken { get; set; } = token;
|
||||
|
||||
@ -59,19 +59,19 @@ namespace Selector.Spotify.Consumer
|
||||
try
|
||||
{
|
||||
Logger.LogDebug("Predicate passed, making request");
|
||||
OnPredicatePass(new EventArgs());
|
||||
OnPredicatePass(EventArgs.Empty);
|
||||
|
||||
var response = await HttpClient.PostAsync(Config.Url, Config.Content, CancelToken);
|
||||
|
||||
if (response.IsSuccessStatusCode)
|
||||
{
|
||||
Logger.LogDebug("Request success");
|
||||
OnSuccessfulRequest(new EventArgs());
|
||||
OnSuccessfulRequest(EventArgs.Empty);
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.LogDebug("Request failed [{error}] [{content}]", response.StatusCode, response.Content);
|
||||
OnFailedRequest(new EventArgs());
|
||||
OnFailedRequest(EventArgs.Empty);
|
||||
}
|
||||
}
|
||||
catch (HttpRequestException ex)
|
||||
|
@ -6,9 +6,17 @@ namespace Selector.Spotify.Equality
|
||||
{
|
||||
public class PlayableItemEqualityComparer : IEqualityComparer<PlaylistTrack<IPlayableItem>>
|
||||
{
|
||||
public bool Equals(PlaylistTrack<IPlayableItem> x, PlaylistTrack<IPlayableItem> y)
|
||||
public bool Equals(PlaylistTrack<IPlayableItem>? x, PlaylistTrack<IPlayableItem>? y)
|
||||
{
|
||||
return x.GetUri().Equals(y.GetUri());
|
||||
switch (x, y)
|
||||
{
|
||||
case (null, null):
|
||||
case (null, not null):
|
||||
case (not null, null):
|
||||
return false;
|
||||
case var (left, right):
|
||||
return left.GetUri().Equals(right.GetUri());
|
||||
}
|
||||
}
|
||||
|
||||
public int GetHashCode([DisallowNull] PlaylistTrack<IPlayableItem> obj)
|
||||
|
@ -12,89 +12,89 @@ namespace Selector.Spotify.Equality
|
||||
|
||||
public class FullTrackStringComparer : NoHashCode<FullTrack>
|
||||
{
|
||||
public override bool Equals(FullTrack track1, FullTrack track2) => IsEqual(track1, track2);
|
||||
public override bool Equals(FullTrack? track1, FullTrack? track2) => IsEqual(track1, track2);
|
||||
|
||||
public static bool IsEqual(FullTrack track1, FullTrack track2) => track1.Name == track2.Name
|
||||
&& Enumerable.SequenceEqual(
|
||||
track1.Artists.Select(a => a.Name),
|
||||
track2.Artists.Select(a => a.Name))
|
||||
&& SimpleAlbumStringComparer.IsEqual(
|
||||
track1.Album, track2.Album);
|
||||
public static bool IsEqual(FullTrack? track1, FullTrack? track2) => track1.Name == track2.Name
|
||||
&& Enumerable.SequenceEqual(
|
||||
track1.Artists.Select(a => a.Name),
|
||||
track2.Artists.Select(a => a.Name))
|
||||
&& SimpleAlbumStringComparer.IsEqual(
|
||||
track1.Album, track2.Album);
|
||||
}
|
||||
|
||||
public class FullEpisodeStringComparer : NoHashCode<FullEpisode>
|
||||
{
|
||||
public override bool Equals(FullEpisode ep1, FullEpisode ep2) => IsEqual(ep1, ep2);
|
||||
public override bool Equals(FullEpisode? ep1, FullEpisode? ep2) => IsEqual(ep1, ep2);
|
||||
|
||||
public static bool IsEqual(FullEpisode ep1, FullEpisode ep2) => ep1.Name == ep2.Name
|
||||
&& SimpleShowStringComparer.IsEqual(ep1.Show,
|
||||
ep2.Show);
|
||||
public static bool IsEqual(FullEpisode? ep1, FullEpisode? ep2) => ep1.Name == ep2.Name
|
||||
&& SimpleShowStringComparer.IsEqual(ep1.Show,
|
||||
ep2.Show);
|
||||
}
|
||||
|
||||
public class FullAlbumStringComparer : NoHashCode<FullAlbum>
|
||||
{
|
||||
public override bool Equals(FullAlbum album1, FullAlbum album2) => IsEqual(album1, album2);
|
||||
public override bool Equals(FullAlbum? album1, FullAlbum? album2) => IsEqual(album1, album2);
|
||||
|
||||
public static bool IsEqual(FullAlbum album1, FullAlbum album2) => album1.Name == album2.Name
|
||||
&& Enumerable.SequenceEqual(
|
||||
album1.Artists.Select(a => a.Name),
|
||||
album2.Artists.Select(a => a.Name));
|
||||
public static bool IsEqual(FullAlbum? album1, FullAlbum? album2) => album1.Name == album2.Name
|
||||
&& Enumerable.SequenceEqual(
|
||||
album1.Artists.Select(a => a.Name),
|
||||
album2.Artists.Select(a => a.Name));
|
||||
}
|
||||
|
||||
public class FullShowStringComparer : NoHashCode<FullShow>
|
||||
{
|
||||
public override bool Equals(FullShow show1, FullShow show2) => IsEqual(show1, show2);
|
||||
public override bool Equals(FullShow? show1, FullShow? show2) => IsEqual(show1, show2);
|
||||
|
||||
public static bool IsEqual(FullShow show1, FullShow show2) => show1.Name == show2.Name
|
||||
&& show1.Publisher == show2.Publisher;
|
||||
public static bool IsEqual(FullShow? show1, FullShow? show2) => show1.Name == show2.Name
|
||||
&& show1.Publisher == show2.Publisher;
|
||||
}
|
||||
|
||||
public class FullArtistStringComparer : NoHashCode<FullArtist>
|
||||
{
|
||||
public override bool Equals(FullArtist artist1, FullArtist artist2) => IsEqual(artist1, artist2);
|
||||
public override bool Equals(FullArtist? artist1, FullArtist? artist2) => IsEqual(artist1, artist2);
|
||||
|
||||
public static bool IsEqual(FullArtist artist1, FullArtist artist2) => artist1.Name == artist2.Name;
|
||||
public static bool IsEqual(FullArtist? artist1, FullArtist? artist2) => artist1.Name == artist2.Name;
|
||||
}
|
||||
|
||||
public class SimpleTrackStringComparer : NoHashCode<SimpleTrack>
|
||||
{
|
||||
public override bool Equals(SimpleTrack track1, SimpleTrack track2) => IsEqual(track1, track2);
|
||||
public override bool Equals(SimpleTrack? track1, SimpleTrack? track2) => IsEqual(track1, track2);
|
||||
|
||||
public static bool IsEqual(SimpleTrack track1, SimpleTrack track2) => track1.Name == track2.Name
|
||||
&& Enumerable.SequenceEqual(
|
||||
track1.Artists.Select(a => a.Name),
|
||||
track2.Artists.Select(a => a.Name));
|
||||
public static bool IsEqual(SimpleTrack? track1, SimpleTrack? track2) => track1.Name == track2.Name
|
||||
&& Enumerable.SequenceEqual(
|
||||
track1.Artists.Select(a => a.Name),
|
||||
track2.Artists.Select(a => a.Name));
|
||||
}
|
||||
|
||||
public class SimpleEpisodeStringComparer : NoHashCode<SimpleEpisode>
|
||||
{
|
||||
public override bool Equals(SimpleEpisode ep1, SimpleEpisode ep2) => IsEqual(ep1, ep2);
|
||||
public override bool Equals(SimpleEpisode? ep1, SimpleEpisode? ep2) => IsEqual(ep1, ep2);
|
||||
|
||||
public static bool IsEqual(SimpleEpisode ep1, SimpleEpisode ep2) => ep1.Name == ep2.Name;
|
||||
public static bool IsEqual(SimpleEpisode? ep1, SimpleEpisode? ep2) => ep1.Name == ep2.Name;
|
||||
}
|
||||
|
||||
public class SimpleAlbumStringComparer : NoHashCode<SimpleAlbum>
|
||||
{
|
||||
public override bool Equals(SimpleAlbum album1, SimpleAlbum album2) => IsEqual(album1, album2);
|
||||
public override bool Equals(SimpleAlbum? album1, SimpleAlbum? album2) => IsEqual(album1, album2);
|
||||
|
||||
public static bool IsEqual(SimpleAlbum album1, SimpleAlbum album2) => album1.Name == album2.Name
|
||||
&& Enumerable.SequenceEqual(
|
||||
album1.Artists.Select(a => a.Name),
|
||||
album2.Artists.Select(a => a.Name));
|
||||
public static bool IsEqual(SimpleAlbum? album1, SimpleAlbum? album2) => album1?.Name == album2?.Name
|
||||
&& Enumerable.SequenceEqual(
|
||||
album1?.Artists.Select(a => a.Name),
|
||||
album2?.Artists.Select(a => a.Name));
|
||||
}
|
||||
|
||||
public class SimpleShowStringComparer : NoHashCode<SimpleShow>
|
||||
{
|
||||
public override bool Equals(SimpleShow show1, SimpleShow show2) => IsEqual(show1, show2);
|
||||
public override bool Equals(SimpleShow? show1, SimpleShow? show2) => IsEqual(show1, show2);
|
||||
|
||||
public static bool IsEqual(SimpleShow show1, SimpleShow show2) => show1.Name == show2.Name
|
||||
&& show1.Publisher == show2.Publisher;
|
||||
public static bool IsEqual(SimpleShow? show1, SimpleShow? show2) => show1?.Name == show2?.Name
|
||||
&& show1?.Publisher == show2?.Publisher;
|
||||
}
|
||||
|
||||
public class SimpleArtistStringComparer : NoHashCode<SimpleArtist>
|
||||
{
|
||||
public override bool Equals(SimpleArtist artist1, SimpleArtist artist2) => IsEqual(artist1, artist2);
|
||||
public override bool Equals(SimpleArtist? artist1, SimpleArtist? artist2) => IsEqual(artist1, artist2);
|
||||
|
||||
public static bool IsEqual(SimpleArtist artist1, SimpleArtist artist2) => artist1.Name == artist2.Name;
|
||||
public static bool IsEqual(SimpleArtist? artist1, SimpleArtist? artist2) => artist1?.Name == artist2?.Name;
|
||||
}
|
||||
}
|
@ -5,18 +5,18 @@ namespace Selector.Spotify
|
||||
{
|
||||
public class SpotifyListeningChangeEventArgs : ListeningChangeEventArgs
|
||||
{
|
||||
public CurrentlyPlayingContext Previous { get; set; }
|
||||
public CurrentlyPlayingContext Current { get; set; }
|
||||
public CurrentlyPlayingContext? Previous { get; set; }
|
||||
public required CurrentlyPlayingContext Current { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Spotify Username
|
||||
/// </summary>
|
||||
public string SpotifyUsername { get; set; }
|
||||
public required string SpotifyUsername { get; set; }
|
||||
|
||||
PlayerTimeline Timeline { get; set; }
|
||||
public required PlayerTimeline Timeline { get; set; }
|
||||
|
||||
public static SpotifyListeningChangeEventArgs From(CurrentlyPlayingContext previous,
|
||||
CurrentlyPlayingContext current, PlayerTimeline timeline, string id = null, string username = null)
|
||||
public static SpotifyListeningChangeEventArgs From(CurrentlyPlayingContext? previous,
|
||||
CurrentlyPlayingContext current, PlayerTimeline timeline, string id, string username)
|
||||
{
|
||||
return new SpotifyListeningChangeEventArgs()
|
||||
{
|
||||
@ -31,29 +31,30 @@ namespace Selector.Spotify
|
||||
|
||||
public class PlaylistChangeEventArgs : EventArgs
|
||||
{
|
||||
public FullPlaylist Previous { get; set; }
|
||||
public FullPlaylist Current { get; set; }
|
||||
public FullPlaylist? Previous { get; set; }
|
||||
public required FullPlaylist Current { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Spotify Username
|
||||
/// </summary>
|
||||
public string SpotifyUsername { get; set; }
|
||||
public required string SpotifyUsername { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// String Id for watcher, used to hold user Db Id
|
||||
/// </summary>
|
||||
/// <value></value>
|
||||
public string Id { get; set; }
|
||||
public required string Id { get; set; }
|
||||
|
||||
Timeline<FullPlaylist> Timeline { get; set; }
|
||||
ICollection<PlaylistTrack<IPlayableItem>> CurrentTracks { get; set; }
|
||||
ICollection<PlaylistTrack<IPlayableItem>> AddedTracks { get; set; }
|
||||
ICollection<PlaylistTrack<IPlayableItem>> RemovedTracks { get; set; }
|
||||
public required Timeline<FullPlaylist> Timeline { get; set; }
|
||||
private ICollection<PlaylistTrack<IPlayableItem>>? CurrentTracks { get; set; }
|
||||
private ICollection<PlaylistTrack<IPlayableItem>>? AddedTracks { get; set; }
|
||||
private ICollection<PlaylistTrack<IPlayableItem>>? RemovedTracks { get; set; }
|
||||
|
||||
public static PlaylistChangeEventArgs From(FullPlaylist previous, FullPlaylist current,
|
||||
Timeline<FullPlaylist> timeline, ICollection<PlaylistTrack<IPlayableItem>> tracks = null,
|
||||
ICollection<PlaylistTrack<IPlayableItem>> addedTracks = null,
|
||||
ICollection<PlaylistTrack<IPlayableItem>> removedTracks = null, string id = null, string username = null)
|
||||
Timeline<FullPlaylist> timeline, string id, string username,
|
||||
ICollection<PlaylistTrack<IPlayableItem>>? tracks = null,
|
||||
ICollection<PlaylistTrack<IPlayableItem>>? addedTracks = null,
|
||||
ICollection<PlaylistTrack<IPlayableItem>>? removedTracks = null)
|
||||
{
|
||||
return new PlaylistChangeEventArgs()
|
||||
{
|
||||
|
@ -8,8 +8,8 @@ public static class ServiceExtensions
|
||||
{
|
||||
public static IServiceCollection AddConsumerFactories(this IServiceCollection services)
|
||||
{
|
||||
services.AddTransient<IAudioFeatureInjectorFactory, AudioFeatureInjectorFactory>();
|
||||
services.AddTransient<AudioFeatureInjectorFactory>();
|
||||
// services.AddTransient<IAudioFeatureInjectorFactory, AudioFeatureInjectorFactory>();
|
||||
// services.AddTransient<AudioFeatureInjectorFactory>();
|
||||
|
||||
services.AddTransient<IPlayCounterFactory, PlayCounterFactory>();
|
||||
services.AddTransient<PlayCounterFactory>();
|
||||
|
@ -5,7 +5,7 @@ namespace Selector.Spotify
|
||||
{
|
||||
[JsonSerializable(typeof(SpotifyCurrentlyPlayingDTO))]
|
||||
[JsonSerializable(typeof(TrackAudioFeatures))]
|
||||
[JsonSerializable(typeof(SpotifyListeningChangeEventArgs))]
|
||||
// [JsonSerializable(typeof(SpotifyListeningChangeEventArgs))]
|
||||
public partial class SpotifyJsonContext : JsonSerializerContext
|
||||
{
|
||||
}
|
||||
|
@ -4,6 +4,7 @@
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<IsAotCompatible>true</IsAotCompatible>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
@ -4,15 +4,15 @@ namespace Selector.Spotify.Timeline
|
||||
{
|
||||
public interface ITrackTimeline<T> : ITimeline<T>
|
||||
{
|
||||
public T Get(FullTrack track);
|
||||
public T? Get(FullTrack track);
|
||||
public IEnumerable<T> GetAll(FullTrack track);
|
||||
public IEnumerable<TimelineItem<T>> GetAllTimelineItems(FullTrack track);
|
||||
|
||||
public T Get(SimpleAlbum album);
|
||||
public T? Get(SimpleAlbum album);
|
||||
public IEnumerable<T> GetAll(SimpleAlbum album);
|
||||
public IEnumerable<TimelineItem<T>> GetAllTimelineItems(SimpleAlbum album);
|
||||
|
||||
public T Get(SimpleArtist artist);
|
||||
public T? Get(SimpleArtist artist);
|
||||
public IEnumerable<T> GetAll(SimpleArtist artist);
|
||||
public IEnumerable<TimelineItem<T>> GetAllTimelineItems(SimpleArtist artist);
|
||||
}
|
||||
|
@ -5,11 +5,11 @@ namespace Selector.Spotify.Timeline
|
||||
public class PlayerTimeline
|
||||
: Timeline<CurrentlyPlayingContext>, ITrackTimeline<CurrentlyPlayingContext>
|
||||
{
|
||||
public IEqual EqualityChecker { get; set; }
|
||||
public required IEqual EqualityChecker { get; set; }
|
||||
|
||||
public override void Add(CurrentlyPlayingContext item) => Add(item, DateHelper.FromUnixMilli(item.Timestamp));
|
||||
|
||||
public CurrentlyPlayingContext Get(FullTrack track)
|
||||
public CurrentlyPlayingContext? Get(FullTrack track)
|
||||
=> GetAll(track)
|
||||
.LastOrDefault();
|
||||
|
||||
@ -22,7 +22,7 @@ namespace Selector.Spotify.Timeline
|
||||
.Where(i => i.Item.Item is FullTrack iterTrack
|
||||
&& EqualityChecker.IsEqual(iterTrack, track));
|
||||
|
||||
public CurrentlyPlayingContext Get(FullEpisode ep)
|
||||
public CurrentlyPlayingContext? Get(FullEpisode ep)
|
||||
=> GetAll(ep)
|
||||
.LastOrDefault();
|
||||
|
||||
@ -35,7 +35,7 @@ namespace Selector.Spotify.Timeline
|
||||
.Where(i => i.Item.Item is FullEpisode iterEp
|
||||
&& EqualityChecker.IsEqual(iterEp, ep));
|
||||
|
||||
public CurrentlyPlayingContext Get(SimpleAlbum album)
|
||||
public CurrentlyPlayingContext? Get(SimpleAlbum album)
|
||||
=> GetAll(album)
|
||||
.LastOrDefault();
|
||||
|
||||
@ -48,7 +48,7 @@ namespace Selector.Spotify.Timeline
|
||||
.Where(i => i.Item.Item is FullTrack iterTrack
|
||||
&& EqualityChecker.IsEqual(iterTrack.Album, album));
|
||||
|
||||
public CurrentlyPlayingContext Get(SimpleArtist artist)
|
||||
public CurrentlyPlayingContext? Get(SimpleArtist artist)
|
||||
=> GetAll(artist)
|
||||
.LastOrDefault();
|
||||
|
||||
@ -61,7 +61,7 @@ namespace Selector.Spotify.Timeline
|
||||
.Where(i => i.Item.Item is FullTrack iterTrack
|
||||
&& EqualityChecker.IsEqual(iterTrack.Artists[0], artist));
|
||||
|
||||
public CurrentlyPlayingContext Get(Device device)
|
||||
public CurrentlyPlayingContext? Get(Device device)
|
||||
=> GetAll(device)
|
||||
.LastOrDefault();
|
||||
|
||||
@ -73,7 +73,7 @@ namespace Selector.Spotify.Timeline
|
||||
=> Recent
|
||||
.Where(i => EqualityChecker.IsEqual(i.Item.Device, device));
|
||||
|
||||
public CurrentlyPlayingContext Get(Context context)
|
||||
public CurrentlyPlayingContext? Get(Context context)
|
||||
=> GetAll(context)
|
||||
.LastOrDefault();
|
||||
|
||||
|
@ -2,9 +2,9 @@ using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Selector.Spotify.Watcher;
|
||||
|
||||
public abstract class BaseSpotifyWatcher(ILogger<BaseWatcher> logger = null) : BaseWatcher(logger)
|
||||
public abstract class BaseSpotifyWatcher(ILogger<BaseWatcher>? logger) : BaseWatcher(logger)
|
||||
{
|
||||
public string SpotifyUsername { get; set; }
|
||||
public required string SpotifyUsername { get; set; }
|
||||
|
||||
protected override Dictionary<string, object> LogScopeContext =>
|
||||
new[]
|
||||
|
@ -9,41 +9,42 @@ namespace Selector.Spotify.Watcher
|
||||
public class SpotifyPlayerWatcher : BaseSpotifyWatcher, ISpotifyPlayerWatcher
|
||||
{
|
||||
new protected readonly ILogger<SpotifyPlayerWatcher> Logger;
|
||||
private readonly IPlayerClient spotifyClient;
|
||||
private readonly IEqual eq;
|
||||
private readonly IPlayerClient _spotifyClient;
|
||||
private readonly IEqual _eq;
|
||||
|
||||
public event EventHandler<SpotifyListeningChangeEventArgs> NetworkPoll;
|
||||
public event EventHandler<SpotifyListeningChangeEventArgs> ItemChange;
|
||||
public event EventHandler<SpotifyListeningChangeEventArgs> AlbumChange;
|
||||
public event EventHandler<SpotifyListeningChangeEventArgs> ArtistChange;
|
||||
public event EventHandler<SpotifyListeningChangeEventArgs> ContextChange;
|
||||
public event EventHandler<SpotifyListeningChangeEventArgs> ContentChange;
|
||||
public event EventHandler<SpotifyListeningChangeEventArgs>? NetworkPoll;
|
||||
public event EventHandler<SpotifyListeningChangeEventArgs>? ItemChange;
|
||||
public event EventHandler<SpotifyListeningChangeEventArgs>? AlbumChange;
|
||||
public event EventHandler<SpotifyListeningChangeEventArgs>? ArtistChange;
|
||||
public event EventHandler<SpotifyListeningChangeEventArgs>? ContextChange;
|
||||
public event EventHandler<SpotifyListeningChangeEventArgs>? ContentChange;
|
||||
|
||||
public event EventHandler<SpotifyListeningChangeEventArgs> VolumeChange;
|
||||
public event EventHandler<SpotifyListeningChangeEventArgs> DeviceChange;
|
||||
public event EventHandler<SpotifyListeningChangeEventArgs> PlayingChange;
|
||||
public event EventHandler<SpotifyListeningChangeEventArgs>? VolumeChange;
|
||||
public event EventHandler<SpotifyListeningChangeEventArgs>? DeviceChange;
|
||||
public event EventHandler<SpotifyListeningChangeEventArgs>? PlayingChange;
|
||||
|
||||
public CurrentlyPlayingContext Live { get; protected set; }
|
||||
protected CurrentlyPlayingContext Previous { get; set; }
|
||||
public PlayerTimeline Past { get; set; } = new();
|
||||
public CurrentlyPlayingContext? Live { get; protected set; }
|
||||
protected CurrentlyPlayingContext? Previous { get; set; }
|
||||
public PlayerTimeline Past { get; set; }
|
||||
|
||||
public SpotifyPlayerWatcher(IPlayerClient spotifyClient,
|
||||
IEqual equalityChecker,
|
||||
ILogger<SpotifyPlayerWatcher> logger = null,
|
||||
ILogger<SpotifyPlayerWatcher>? logger = null,
|
||||
int pollPeriod = 3000
|
||||
) : base(logger)
|
||||
{
|
||||
this.spotifyClient = spotifyClient;
|
||||
eq = equalityChecker;
|
||||
_spotifyClient = spotifyClient;
|
||||
_eq = equalityChecker;
|
||||
Logger = logger ?? NullLogger<SpotifyPlayerWatcher>.Instance;
|
||||
PollPeriod = pollPeriod;
|
||||
Past = new() { EqualityChecker = equalityChecker };
|
||||
}
|
||||
|
||||
public override Task Reset()
|
||||
{
|
||||
Previous = null;
|
||||
Live = null;
|
||||
Past = new();
|
||||
Past = new() { EqualityChecker = _eq };
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
@ -55,7 +56,7 @@ namespace Selector.Spotify.Watcher
|
||||
try
|
||||
{
|
||||
Logger.LogTrace("Making Spotify call");
|
||||
var polledCurrent = await spotifyClient.GetCurrentPlayback();
|
||||
var polledCurrent = await _spotifyClient.GetCurrentPlayback();
|
||||
|
||||
using var polledLogScope = Logger.BeginScope(new Dictionary<string, object>()
|
||||
{ { "context", polledCurrent?.DisplayString() } });
|
||||
@ -122,7 +123,7 @@ namespace Selector.Spotify.Watcher
|
||||
switch (Previous, Live)
|
||||
{
|
||||
case ({ Item: FullTrack previousTrack }, { Item: FullTrack currentTrack }):
|
||||
if (!eq.IsEqual(previousTrack, currentTrack))
|
||||
if (!_eq.IsEqual(previousTrack, currentTrack))
|
||||
{
|
||||
Logger.LogInformation("Track changed: {prevTrack} -> {currentTrack}",
|
||||
previousTrack.DisplayString(),
|
||||
@ -130,14 +131,14 @@ namespace Selector.Spotify.Watcher
|
||||
OnItemChange(GetEvent());
|
||||
}
|
||||
|
||||
if (!eq.IsEqual(previousTrack.Album, currentTrack.Album))
|
||||
if (!_eq.IsEqual(previousTrack.Album, currentTrack.Album))
|
||||
{
|
||||
Logger.LogDebug("Album changed: {previous} -> {current}", previousTrack.Album.DisplayString(),
|
||||
currentTrack.Album.DisplayString());
|
||||
OnAlbumChange(GetEvent());
|
||||
}
|
||||
|
||||
if (!eq.IsEqual(previousTrack.Artists[0], currentTrack.Artists[0]))
|
||||
if (!_eq.IsEqual(previousTrack.Artists[0], currentTrack.Artists[0]))
|
||||
{
|
||||
Logger.LogDebug("Artist changed: {previous} -> {current}",
|
||||
previousTrack.Artists.DisplayString(), currentTrack.Artists.DisplayString());
|
||||
@ -158,7 +159,7 @@ namespace Selector.Spotify.Watcher
|
||||
OnItemChange(GetEvent());
|
||||
break;
|
||||
case ({ Item: FullEpisode previousEp }, { Item: FullEpisode currentEp }):
|
||||
if (!eq.IsEqual(previousEp, currentEp))
|
||||
if (!_eq.IsEqual(previousEp, currentEp))
|
||||
{
|
||||
Logger.LogDebug("Podcast changed: {previous_ep} -> {current_ep}", previousEp.DisplayString(),
|
||||
currentEp.DisplayString());
|
||||
@ -177,7 +178,7 @@ namespace Selector.Spotify.Watcher
|
||||
Logger.LogDebug("Context started: {context}", Live?.Context.DisplayString());
|
||||
OnContextChange(GetEvent());
|
||||
}
|
||||
else if (!eq.IsEqual(Previous?.Context, Live?.Context))
|
||||
else if (!_eq.IsEqual(Previous?.Context, Live?.Context))
|
||||
{
|
||||
Logger.LogDebug("Context changed: {previous_context} -> {live_context}",
|
||||
Previous?.Context?.DisplayString() ?? "none", Live?.Context?.DisplayString() ?? "none");
|
||||
@ -212,7 +213,7 @@ namespace Selector.Spotify.Watcher
|
||||
protected void CheckDevice()
|
||||
{
|
||||
// DEVICE
|
||||
if (!eq.IsEqual(Previous?.Device, Live?.Device))
|
||||
if (!_eq.IsEqual(Previous?.Device, Live?.Device))
|
||||
{
|
||||
Logger.LogDebug("Device changed: {previous_device} -> {live_device}",
|
||||
Previous?.Device?.DisplayString() ?? "none", Live?.Device?.DisplayString() ?? "none");
|
||||
|
@ -12,6 +12,7 @@ using Xunit;
|
||||
|
||||
namespace Selector.Tests
|
||||
{
|
||||
[Obsolete]
|
||||
public class AudioInjectorTests
|
||||
{
|
||||
[Fact]
|
||||
@ -20,7 +21,8 @@ namespace Selector.Tests
|
||||
var watcherMock = new Mock<ISpotifyPlayerWatcher>();
|
||||
var spotifyMock = new Mock<ITracksClient>();
|
||||
|
||||
var featureInjector = new AudioFeatureInjector(watcherMock.Object, spotifyMock.Object);
|
||||
var featureInjector = new AudioFeatureInjector(watcherMock.Object, spotifyMock.Object,
|
||||
NullLogger<AudioFeatureInjector>.Instance);
|
||||
|
||||
featureInjector.Subscribe();
|
||||
|
||||
@ -33,7 +35,8 @@ namespace Selector.Tests
|
||||
var watcherMock = new Mock<ISpotifyPlayerWatcher>();
|
||||
var spotifyMock = new Mock<ITracksClient>();
|
||||
|
||||
var featureInjector = new AudioFeatureInjector(watcherMock.Object, spotifyMock.Object);
|
||||
var featureInjector = new AudioFeatureInjector(watcherMock.Object, spotifyMock.Object,
|
||||
NullLogger<AudioFeatureInjector>.Instance);
|
||||
|
||||
featureInjector.Unsubscribe();
|
||||
|
||||
@ -47,7 +50,8 @@ namespace Selector.Tests
|
||||
var watcherFuncArgMock = new Mock<ISpotifyPlayerWatcher>();
|
||||
var spotifyMock = new Mock<ITracksClient>();
|
||||
|
||||
var featureInjector = new AudioFeatureInjector(watcherMock.Object, spotifyMock.Object);
|
||||
var featureInjector = new AudioFeatureInjector(watcherMock.Object, spotifyMock.Object,
|
||||
NullLogger<AudioFeatureInjector>.Instance);
|
||||
|
||||
featureInjector.Subscribe(watcherFuncArgMock.Object);
|
||||
|
||||
@ -63,7 +67,8 @@ namespace Selector.Tests
|
||||
var watcherFuncArgMock = new Mock<ISpotifyPlayerWatcher>();
|
||||
var spotifyMock = new Mock<ITracksClient>();
|
||||
|
||||
var featureInjector = new AudioFeatureInjector(watcherMock.Object, spotifyMock.Object);
|
||||
var featureInjector = new AudioFeatureInjector(watcherMock.Object, spotifyMock.Object,
|
||||
NullLogger<AudioFeatureInjector>.Instance);
|
||||
|
||||
featureInjector.Unsubscribe(watcherFuncArgMock.Object);
|
||||
|
||||
|
@ -1,3 +1,4 @@
|
||||
using System;
|
||||
using FluentAssertions;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Moq;
|
||||
@ -7,6 +8,7 @@ using Xunit;
|
||||
|
||||
namespace Selector.Tests
|
||||
{
|
||||
[Obsolete]
|
||||
public class AudioInjectorFactoryTests
|
||||
{
|
||||
[Fact]
|
||||
|
@ -37,14 +37,17 @@ namespace Selector.Tests
|
||||
{
|
||||
Url = link,
|
||||
Content = content,
|
||||
Name = "test"
|
||||
};
|
||||
|
||||
var http = new HttpClient(httpHandlerMock.Object);
|
||||
|
||||
var webHook = new WebHook(watcherMock.Object, http, config);
|
||||
var webHook = new WebHook(watcherMock.Object, http, config, logger: NullLogger<WebHook>.Instance);
|
||||
|
||||
webHook.Subscribe();
|
||||
watcherMock.Raise(w => w.ItemChange += null, this, new SpotifyListeningChangeEventArgs());
|
||||
watcherMock.Raise(w => w.ItemChange += null, this,
|
||||
new SpotifyListeningChangeEventArgs()
|
||||
{ Id = "test", SpotifyUsername = "test", Current = null, Timeline = null });
|
||||
|
||||
await Task.Delay(100);
|
||||
|
||||
@ -74,6 +77,7 @@ namespace Selector.Tests
|
||||
{
|
||||
Url = link,
|
||||
Content = content,
|
||||
Name = "test"
|
||||
};
|
||||
|
||||
var http = new HttpClient(httpHandlerMock.Object);
|
||||
@ -89,7 +93,12 @@ namespace Selector.Tests
|
||||
webHook.FailedRequest += (o, e) => { failedEvent = !successful; };
|
||||
|
||||
await (Task)typeof(WebHook).GetMethod("ProcessEvent", BindingFlags.NonPublic | BindingFlags.Instance)
|
||||
.Invoke(webHook, new object[] { SpotifyListeningChangeEventArgs.From(new(), new(), new()) });
|
||||
.Invoke(webHook,
|
||||
new object[]
|
||||
{
|
||||
SpotifyListeningChangeEventArgs.From(new(), new(), new() { EqualityChecker = null }, null,
|
||||
"test")
|
||||
});
|
||||
|
||||
predicateEvent.Should().Be(predicate);
|
||||
successfulEvent.Should().Be(successful);
|
||||
|
@ -154,9 +154,12 @@ namespace Selector.Tests
|
||||
{
|
||||
return new()
|
||||
{
|
||||
Track = new Track()
|
||||
Track = new Track
|
||||
{
|
||||
Id = id
|
||||
Id = id,
|
||||
Type = "track",
|
||||
Href = null,
|
||||
Attributes = null,
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -35,7 +35,7 @@ namespace Selector.Tests
|
||||
[MemberData(nameof(CountData))]
|
||||
public void Count(CurrentlyPlayingContext[] currentlyPlaying)
|
||||
{
|
||||
var timeline = new PlayerTimeline();
|
||||
var timeline = new PlayerTimeline() { EqualityChecker = null };
|
||||
|
||||
foreach (var i in currentlyPlaying)
|
||||
{
|
||||
@ -106,7 +106,8 @@ namespace Selector.Tests
|
||||
{
|
||||
var timeline = new PlayerTimeline
|
||||
{
|
||||
MaxSize = maxSize
|
||||
MaxSize = maxSize,
|
||||
EqualityChecker = null
|
||||
};
|
||||
|
||||
foreach (var i in currentlyPlaying)
|
||||
@ -120,7 +121,7 @@ namespace Selector.Tests
|
||||
[Fact]
|
||||
public void Clear()
|
||||
{
|
||||
var timeline = new PlayerTimeline();
|
||||
var timeline = new PlayerTimeline() { EqualityChecker = null };
|
||||
var tracks = new CurrentlyPlayingContext[]
|
||||
{
|
||||
Helper.CurrentPlayback(Helper.FullTrack("uri1")),
|
||||
@ -143,7 +144,8 @@ namespace Selector.Tests
|
||||
{
|
||||
var timeline = new PlayerTimeline()
|
||||
{
|
||||
SortOnBackDate = false
|
||||
SortOnBackDate = false,
|
||||
EqualityChecker = null
|
||||
};
|
||||
|
||||
var earlier = Helper.CurrentPlayback(Helper.FullTrack("uri1"));
|
||||
@ -167,7 +169,8 @@ namespace Selector.Tests
|
||||
{
|
||||
var timeline = new PlayerTimeline()
|
||||
{
|
||||
SortOnBackDate = false
|
||||
SortOnBackDate = false,
|
||||
EqualityChecker = null
|
||||
};
|
||||
|
||||
var earlier = Helper.CurrentPlayback(Helper.FullTrack("uri1"));
|
||||
|
@ -38,7 +38,7 @@ namespace Selector.Tests
|
||||
spotMock.Setup(s => s.GetCurrentPlayback(It.IsAny<CancellationToken>()).Result)
|
||||
.Returns(playingQueue.Dequeue);
|
||||
|
||||
var watcher = new SpotifyPlayerWatcher(spotMock.Object, eq);
|
||||
var watcher = new SpotifyPlayerWatcher(spotMock.Object, eq) { Id = "test", SpotifyUsername = "test" };
|
||||
|
||||
for (var i = 0; i < playing.Count; i++)
|
||||
{
|
||||
@ -265,7 +265,7 @@ namespace Selector.Tests
|
||||
s => s.GetCurrentPlayback(It.IsAny<CancellationToken>()).Result
|
||||
).Returns(playingQueue.Dequeue);
|
||||
|
||||
var watcher = new SpotifyPlayerWatcher(spotMock.Object, eq);
|
||||
var watcher = new SpotifyPlayerWatcher(spotMock.Object, eq) { Id = "test", SpotifyUsername = "test" };
|
||||
using var monitoredWatcher = watcher.Monitor();
|
||||
|
||||
for (var i = 0; i < playing.Count; i++)
|
||||
@ -287,7 +287,9 @@ namespace Selector.Tests
|
||||
var eq = new UriEqual();
|
||||
var watch = new SpotifyPlayerWatcher(spotMock.Object, eq)
|
||||
{
|
||||
PollPeriod = pollPeriod
|
||||
PollPeriod = pollPeriod,
|
||||
Id = "test",
|
||||
SpotifyUsername = "test"
|
||||
};
|
||||
|
||||
var tokenSource = new CancellationTokenSource();
|
||||
|
@ -38,7 +38,7 @@ namespace Selector.Tests
|
||||
.Returns(playlistDequeue.Dequeue);
|
||||
|
||||
var config = new PlaylistWatcherConfig() { PlaylistId = "spotify:playlist:test" };
|
||||
var watcher = new PlaylistWatcher(config, spotMock.Object);
|
||||
var watcher = new PlaylistWatcher(config, spotMock.Object) { Id = "test", SpotifyUsername = "test" };
|
||||
|
||||
for (var i = 0; i < playing.Count; i++)
|
||||
{
|
||||
@ -92,7 +92,7 @@ namespace Selector.Tests
|
||||
.Returns(playlistDequeue.Dequeue);
|
||||
|
||||
var config = new PlaylistWatcherConfig() { PlaylistId = "spotify:playlist:test" };
|
||||
var watcher = new PlaylistWatcher(config, spotMock.Object);
|
||||
var watcher = new PlaylistWatcher(config, spotMock.Object) { Id = "test", SpotifyUsername = "test" };
|
||||
|
||||
using var monitoredWatcher = watcher.Monitor();
|
||||
|
||||
|
@ -2,7 +2,6 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using IF.Lastfm.Core.Objects;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Selector.Cache;
|
||||
@ -42,9 +41,9 @@ namespace Selector.Web.Hubs
|
||||
pastOptions = options;
|
||||
}
|
||||
|
||||
public async Task OnConnected()
|
||||
public Task OnConnected()
|
||||
{
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private static IEnumerable<string> AlbumSuffixes = new[]
|
||||
@ -63,8 +62,12 @@ namespace Selector.Web.Hubs
|
||||
param.Album = string.IsNullOrWhiteSpace(param.Album) ? null : param.Album;
|
||||
param.Artist = string.IsNullOrWhiteSpace(param.Artist) ? null : param.Artist;
|
||||
|
||||
DateTime? from = param.From is string f && DateTime.TryParse(f, out var fromDate) ? fromDate.ToUniversalTime() : null;
|
||||
DateTime? to = param.To is string t && DateTime.TryParse(t, out var toDate) ? toDate.ToUniversalTime() : null;
|
||||
DateTime? from = param.From is string f && DateTime.TryParse(f, out var fromDate)
|
||||
? fromDate.ToUniversalTime()
|
||||
: null;
|
||||
DateTime? to = param.To is string t && DateTime.TryParse(t, out var toDate)
|
||||
? toDate.ToUniversalTime()
|
||||
: null;
|
||||
|
||||
var listenQuery = ListenRepository.GetAll(
|
||||
userId: Context.UserIdentifier,
|
||||
|
@ -4,6 +4,8 @@
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<LangVersion>latest</LangVersion>
|
||||
<TypeScriptCompileBlocked>true</TypeScriptCompileBlocked>
|
||||
<IsAotCompatible>true</IsAotCompatible>
|
||||
<EnableConfigurationBindingGenerator>true</EnableConfigurationBindingGenerator>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
@ -96,7 +96,8 @@ namespace Selector.Web
|
||||
);
|
||||
services.AddDBPlayCountPuller();
|
||||
services.AddTransient<IScrobbleRepository, ScrobbleRepository>()
|
||||
.AddTransient<ISpotifyListenRepository, SpotifyListenRepository>();
|
||||
.AddTransient<ISpotifyListenRepository, SpotifyListenRepository>()
|
||||
.AddTransient<IAppleListenRepository, AppleListenRepository>();
|
||||
|
||||
services.AddTransient<IListenRepository, MetaListenRepository>();
|
||||
//services.AddTransient<IListenRepository, SpotifyListenRepository>();
|
||||
|
@ -1,12 +1,9 @@
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Selector
|
||||
namespace Selector
|
||||
{
|
||||
public interface IConsumer
|
||||
{
|
||||
public void Subscribe(IWatcher watch = null);
|
||||
public void Unsubscribe(IWatcher watch = null);
|
||||
public void Subscribe(IWatcher? watch = null);
|
||||
public void Unsubscribe(IWatcher? watch = null);
|
||||
}
|
||||
|
||||
public interface IProcessingConsumer
|
||||
|
@ -1,18 +1,15 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Selector
|
||||
namespace Selector
|
||||
{
|
||||
public class Equal : IEqual
|
||||
{
|
||||
protected Dictionary<Type, object> comps;
|
||||
protected Dictionary<Type, object>? comps;
|
||||
|
||||
public bool IsEqual<T>(T item, T other)
|
||||
{
|
||||
if (item is null && other is null) return true;
|
||||
if (item is null ^ other is null) return false;
|
||||
|
||||
if (comps.ContainsKey(typeof(T)))
|
||||
if (comps?.ContainsKey(typeof(T)) ?? false)
|
||||
{
|
||||
var comp = (IEqualityComparer<T>)comps[typeof(T)];
|
||||
return comp.Equals(item, other);
|
||||
|
@ -1,5 +1,3 @@
|
||||
using System;
|
||||
|
||||
namespace Selector;
|
||||
|
||||
public class ListeningChangeEventArgs : EventArgs
|
||||
@ -8,5 +6,5 @@ public class ListeningChangeEventArgs : EventArgs
|
||||
/// String Id for watcher, used to hold user Db Id
|
||||
/// </summary>
|
||||
/// <value></value>
|
||||
public string Id { get; set; }
|
||||
public required string Id { get; set; }
|
||||
}
|
@ -1,13 +1,10 @@
|
||||
using System;
|
||||
namespace Selector;
|
||||
|
||||
namespace Selector;
|
||||
|
||||
public class Listen: IListen
|
||||
public class Listen : IListen
|
||||
{
|
||||
public string TrackName { get; set; }
|
||||
public string AlbumName { get; set; }
|
||||
public string ArtistName { get; set; }
|
||||
public required string TrackName { get; set; }
|
||||
public required string AlbumName { get; set; }
|
||||
public required string ArtistName { get; set; }
|
||||
|
||||
public DateTime Timestamp { get; set; }
|
||||
}
|
||||
|
||||
}
|
@ -1,8 +1,4 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace Selector
|
||||
namespace Selector
|
||||
{
|
||||
public record struct CountSample
|
||||
{
|
||||
@ -98,8 +94,8 @@ namespace Selector
|
||||
}
|
||||
|
||||
while (sortedScrobblesIter.MoveNext()
|
||||
&& sortedScrobblesIter.Current.Timestamp.Year == counter.Year
|
||||
&& sortedScrobblesIter.Current.Timestamp.Month == counter.Month)
|
||||
&& sortedScrobblesIter.Current?.Timestamp.Year == counter.Year
|
||||
&& sortedScrobblesIter.Current?.Timestamp.Month == counter.Month)
|
||||
{
|
||||
count++;
|
||||
}
|
||||
|
@ -1,20 +1,13 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Collections.Concurrent;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Selector.Operations
|
||||
{
|
||||
{
|
||||
public class BatchingOperation<T> where T : IOperation
|
||||
{
|
||||
protected ILogger<BatchingOperation<T>> logger;
|
||||
protected CancellationToken _token;
|
||||
protected Task aggregateNetworkTask;
|
||||
|
||||
public ConcurrentQueue<T> WaitingRequests { get; private set; } = new();
|
||||
public ConcurrentQueue<T> DoneRequests { get; private set; } = new();
|
||||
@ -23,14 +16,15 @@ namespace Selector.Operations
|
||||
private TimeSpan timeout;
|
||||
private int simultaneousRequests;
|
||||
|
||||
public BatchingOperation(TimeSpan _interRequestDelay, TimeSpan _timeout, int _simultaneous, IEnumerable<T> requests, ILogger<BatchingOperation<T>> _logger = null)
|
||||
public BatchingOperation(TimeSpan _interRequestDelay, TimeSpan _timeout, int _simultaneous,
|
||||
IEnumerable<T> requests, ILogger<BatchingOperation<T>>? _logger = null)
|
||||
{
|
||||
interRequestDelay = _interRequestDelay;
|
||||
timeout = _timeout;
|
||||
simultaneousRequests = _simultaneous;
|
||||
logger = _logger ?? NullLogger<BatchingOperation<T>>.Instance;
|
||||
|
||||
foreach(var request in requests)
|
||||
foreach (var request in requests)
|
||||
{
|
||||
WaitingRequests.Enqueue(request);
|
||||
}
|
||||
@ -38,7 +32,7 @@ namespace Selector.Operations
|
||||
|
||||
public bool TimedOut { get; private set; } = false;
|
||||
|
||||
private async void HandleSuccessfulRequest(object o, EventArgs e)
|
||||
private async void HandleSuccessfulRequest(object? o, EventArgs e)
|
||||
{
|
||||
await Task.Delay(interRequestDelay, _token);
|
||||
TransitionRequest();
|
||||
@ -52,7 +46,8 @@ namespace Selector.Operations
|
||||
_ = request.Execute();
|
||||
DoneRequests.Enqueue(request);
|
||||
|
||||
logger.LogInformation("Executing request {} of {}", DoneRequests.Count, WaitingRequests.Count + DoneRequests.Count);
|
||||
logger.LogInformation("Executing request {} of {}", DoneRequests.Count,
|
||||
WaitingRequests.Count + DoneRequests.Count);
|
||||
}
|
||||
}
|
||||
|
||||
@ -71,4 +66,4 @@ namespace Selector.Operations
|
||||
TimedOut = firstToFinish == timeoutTask;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -2,9 +2,12 @@
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>net9.0</TargetFrameworks>
|
||||
<TargetFrameworks Condition="$([MSBuild]::IsOSPlatform('OSX'))">$(TargetFrameworks);net9.0-ios;net9.0-maccatalyst</TargetFrameworks>
|
||||
<EnableDefaultCompileItems>true</EnableDefaultCompileItems>
|
||||
<LangVersion>latest</LangVersion>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<IsAotCompatible>true</IsAotCompatible>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
@ -1,13 +1,11 @@
|
||||
using System;
|
||||
|
||||
namespace Selector
|
||||
namespace Selector
|
||||
{
|
||||
public interface ITimeline<T>
|
||||
{
|
||||
public int Count { get; }
|
||||
public T Get(DateTime at);
|
||||
public T? Get(DateTime at);
|
||||
public T Get();
|
||||
public void Add(T item, DateTime timestamp);
|
||||
public void Clear();
|
||||
}
|
||||
}
|
||||
}
|
@ -1,26 +1,29 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Collections;
|
||||
|
||||
namespace Selector
|
||||
{
|
||||
public class Timeline<T> : ITimeline<T>, IEnumerable<TimelineItem<T>> where T : class
|
||||
{
|
||||
protected List<TimelineItem<T>> Recent = new();
|
||||
public int Count { get => Recent.Count; }
|
||||
|
||||
public int Count
|
||||
{
|
||||
get => Recent.Count;
|
||||
}
|
||||
|
||||
public void Clear() => Recent.Clear();
|
||||
public bool SortOnBackDate { get; set; } = true;
|
||||
|
||||
private int? max = 1000;
|
||||
|
||||
public int? MaxSize
|
||||
{
|
||||
get => max;
|
||||
set => max = value is null ? value : Math.Max(1, (int) value);
|
||||
set => max = value is null ? value : Math.Max(1, (int)value);
|
||||
}
|
||||
|
||||
public virtual void Add(T item) => Add(item, DateTime.UtcNow);
|
||||
|
||||
public virtual void Add(T item, DateTime timestamp)
|
||||
{
|
||||
Recent.Add(TimelineItem<T>.From(item, timestamp));
|
||||
@ -32,7 +35,7 @@ namespace Selector
|
||||
|
||||
CheckSize();
|
||||
}
|
||||
|
||||
|
||||
public void Sort()
|
||||
{
|
||||
Recent = Recent.OrderBy(i => i.Time).ToList();
|
||||
@ -49,13 +52,13 @@ namespace Selector
|
||||
public T Get()
|
||||
=> Recent.Last().Item;
|
||||
|
||||
public T Get(DateTime at)
|
||||
=> GetTimelineItem(at).Item;
|
||||
public TimelineItem<T> GetTimelineItem(DateTime at)
|
||||
=> Recent
|
||||
.Where(i => i.Time <= at).LastOrDefault();
|
||||
public T? Get(DateTime at)
|
||||
=> GetTimelineItem(at)?.Item;
|
||||
|
||||
public TimelineItem<T>? GetTimelineItem(DateTime at)
|
||||
=> Recent.LastOrDefault(i => i.Time <= at);
|
||||
|
||||
public IEnumerator<TimelineItem<T>> GetEnumerator() => Recent.GetEnumerator();
|
||||
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
|
||||
}
|
||||
}
|
||||
}
|
@ -1,10 +1,8 @@
|
||||
using System;
|
||||
|
||||
namespace Selector
|
||||
namespace Selector
|
||||
{
|
||||
public class TimelineItem<T>: ITimelineItem<T>
|
||||
public class TimelineItem<T> : ITimelineItem<T>
|
||||
{
|
||||
public T Item { get; set; }
|
||||
public required T Item { get; set; }
|
||||
public DateTime Time { get; set; }
|
||||
|
||||
public static TimelineItem<TT> From<TT>(TT item, DateTime time)
|
||||
@ -16,4 +14,4 @@ namespace Selector
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,17 +1,16 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Selector;
|
||||
|
||||
public abstract class BaseParallelPlayerConsumer<TWatcher, TArgs>(
|
||||
TWatcher watcher,
|
||||
ILogger<BaseParallelPlayerConsumer<TWatcher, TArgs>> logger) : IConsumer<TArgs>
|
||||
TWatcher? watcher,
|
||||
ILogger<BaseParallelPlayerConsumer<TWatcher, TArgs>> logger)
|
||||
: BasePlayerConsumer<TWatcher, TArgs>(watcher, logger), IConsumer<TArgs>
|
||||
where TWatcher : IWatcher<TArgs>
|
||||
{
|
||||
protected readonly ILogger<BaseParallelPlayerConsumer<TWatcher, TArgs>> Logger = logger;
|
||||
protected new readonly ILogger<BaseParallelPlayerConsumer<TWatcher, TArgs>> Logger = logger;
|
||||
|
||||
public void Callback(object sender, TArgs e)
|
||||
public override void Callback(object? sender, TArgs e)
|
||||
{
|
||||
Task.Run(async () =>
|
||||
{
|
||||
@ -25,34 +24,4 @@ public abstract class BaseParallelPlayerConsumer<TWatcher, TArgs>(
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected abstract Task ProcessEvent(TArgs e);
|
||||
|
||||
public void Subscribe(IWatcher? watch = null)
|
||||
{
|
||||
var watcher1 = watch ?? watcher ?? throw new ArgumentNullException("No watcher provided");
|
||||
|
||||
if (watcher1 is TWatcher watcherCast)
|
||||
{
|
||||
watcherCast.ItemChange += Callback;
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new ArgumentException("Provided watcher is not a PlayerWatcher");
|
||||
}
|
||||
}
|
||||
|
||||
public void Unsubscribe(IWatcher? watch = null)
|
||||
{
|
||||
var watcher1 = watch ?? watcher ?? throw new ArgumentNullException("No watcher provided");
|
||||
|
||||
if (watcher1 is TWatcher watcherCast)
|
||||
{
|
||||
watcherCast.ItemChange -= Callback;
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new ArgumentException("Provided watcher is not a PlayerWatcher");
|
||||
}
|
||||
}
|
||||
}
|
42
Selector/Watcher/BasePlayerConsumer.cs
Normal file
42
Selector/Watcher/BasePlayerConsumer.cs
Normal file
@ -0,0 +1,42 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Selector;
|
||||
|
||||
public abstract class BasePlayerConsumer<TWatcher, TArgs>(
|
||||
TWatcher? watcher,
|
||||
ILogger<BasePlayerConsumer<TWatcher, TArgs>> logger) : IConsumer<TArgs>
|
||||
where TWatcher : IWatcher<TArgs>
|
||||
{
|
||||
protected readonly ILogger<BasePlayerConsumer<TWatcher, TArgs>> Logger = logger;
|
||||
|
||||
public abstract void Callback(object? sender, TArgs e);
|
||||
protected abstract Task ProcessEvent(TArgs e);
|
||||
|
||||
public void Subscribe(IWatcher? watch = null)
|
||||
{
|
||||
var watcher1 = watch ?? watcher ?? throw new ArgumentNullException("No watcher provided");
|
||||
|
||||
if (watcher1 is TWatcher watcherCast)
|
||||
{
|
||||
watcherCast.ItemChange += Callback;
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new ArgumentException("Provided watcher is not a PlayerWatcher");
|
||||
}
|
||||
}
|
||||
|
||||
public void Unsubscribe(IWatcher? watch = null)
|
||||
{
|
||||
var watcher1 = watch ?? watcher ?? throw new ArgumentNullException("No watcher provided");
|
||||
|
||||
if (watcher1 is TWatcher watcherCast)
|
||||
{
|
||||
watcherCast.ItemChange -= Callback;
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new ArgumentException("Provided watcher is not a PlayerWatcher");
|
||||
}
|
||||
}
|
||||
}
|
@ -1,21 +1,19 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Channels;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Selector;
|
||||
|
||||
public abstract class BaseSequentialPlayerConsumer<TWatcher, TArgs>(
|
||||
TWatcher watcher,
|
||||
ILogger<BaseSequentialPlayerConsumer<TWatcher, TArgs>> logger) : IProcessingConsumer<TArgs>
|
||||
TWatcher? watcher,
|
||||
ILogger<BaseSequentialPlayerConsumer<TWatcher, TArgs>> logger)
|
||||
: BasePlayerConsumer<TWatcher, TArgs>(watcher, logger), IProcessingConsumer<TArgs>
|
||||
where TWatcher : IWatcher<TArgs>
|
||||
{
|
||||
protected readonly ILogger<BaseSequentialPlayerConsumer<TWatcher, TArgs>> Logger = logger;
|
||||
protected new readonly ILogger<BaseSequentialPlayerConsumer<TWatcher, TArgs>> Logger = logger;
|
||||
|
||||
private readonly Channel<TArgs> _events = Channel.CreateUnbounded<TArgs>();
|
||||
|
||||
public void Callback(object sender, TArgs e)
|
||||
public override void Callback(object? sender, TArgs e)
|
||||
{
|
||||
if (!_events.Writer.TryWrite(e))
|
||||
{
|
||||
@ -41,34 +39,4 @@ public abstract class BaseSequentialPlayerConsumer<TWatcher, TArgs>(
|
||||
}
|
||||
}, cancellationToken: token);
|
||||
}
|
||||
|
||||
protected abstract Task ProcessEvent(TArgs e);
|
||||
|
||||
public void Subscribe(IWatcher? watch = null)
|
||||
{
|
||||
var watcher1 = watch ?? watcher ?? throw new ArgumentNullException("No watcher provided");
|
||||
|
||||
if (watcher1 is TWatcher watcherCast)
|
||||
{
|
||||
watcherCast.ItemChange += Callback;
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new ArgumentException("Provided watcher is not a PlayerWatcher");
|
||||
}
|
||||
}
|
||||
|
||||
public void Unsubscribe(IWatcher? watch = null)
|
||||
{
|
||||
var watcher1 = watch ?? watcher ?? throw new ArgumentNullException("No watcher provided");
|
||||
|
||||
if (watcher1 is TWatcher watcherCast)
|
||||
{
|
||||
watcherCast.ItemChange -= Callback;
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new ArgumentException("Provided watcher is not a PlayerWatcher");
|
||||
}
|
||||
}
|
||||
}
|
@ -1,8 +1,4 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Diagnostics;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
|
||||
@ -11,10 +7,10 @@ namespace Selector
|
||||
public abstract class BaseWatcher : IWatcher
|
||||
{
|
||||
protected readonly ILogger<BaseWatcher> Logger;
|
||||
public string Id { get; set; }
|
||||
public required string Id { get; set; }
|
||||
private Stopwatch ExecutionTimer { get; set; }
|
||||
|
||||
public BaseWatcher(ILogger<BaseWatcher> logger = null)
|
||||
public BaseWatcher(ILogger<BaseWatcher>? logger = null)
|
||||
{
|
||||
Logger = logger ?? NullLogger<BaseWatcher>.Instance;
|
||||
ExecutionTimer = new Stopwatch();
|
||||
|
@ -1,9 +1,4 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
|
||||
@ -15,7 +10,7 @@ namespace Selector
|
||||
public bool IsRunning { get; private set; } = false;
|
||||
private List<WatcherContext> Watchers { get; set; } = new();
|
||||
|
||||
public WatcherCollection(ILogger<WatcherCollection> logger = null)
|
||||
public WatcherCollection(ILogger<WatcherCollection>? logger = null)
|
||||
{
|
||||
Logger = logger ?? NullLogger<WatcherCollection>.Instance;
|
||||
}
|
||||
@ -30,9 +25,10 @@ namespace Selector
|
||||
public IEnumerable<Task> Tasks
|
||||
=> Watchers
|
||||
.Select(w => w.Task)
|
||||
.Where(t => t is not null);
|
||||
.Where(t => t is not null)
|
||||
.Select(t => t!);
|
||||
|
||||
public IEnumerable<CancellationTokenSource> TokenSources
|
||||
public IEnumerable<CancellationTokenSource?> TokenSources
|
||||
=> Watchers
|
||||
.Select(w => w.TokenSource)
|
||||
.Where(t => t is not null);
|
||||
|
@ -1,9 +1,3 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Selector
|
||||
{
|
||||
public class WatcherContext : IDisposable, IWatcherContext
|
||||
@ -16,9 +10,9 @@ namespace Selector
|
||||
/// Reference to Watcher.Watch() task when running
|
||||
/// </summary>
|
||||
/// <value></value>
|
||||
public Task Task { get; set; }
|
||||
public Task? Task { get; private set; }
|
||||
|
||||
public CancellationTokenSource TokenSource { get; set; }
|
||||
public CancellationTokenSource? TokenSource { get; private set; }
|
||||
|
||||
public WatcherContext(IWatcher watcher)
|
||||
{
|
||||
@ -57,9 +51,9 @@ namespace Selector
|
||||
IsRunning = true;
|
||||
TokenSource = new();
|
||||
|
||||
Consumers.ForEach(c => c.Subscribe(Watcher));
|
||||
foreach (var consumer in Consumers)
|
||||
{
|
||||
consumer.Subscribe(Watcher);
|
||||
if (consumer is IProcessingConsumer c)
|
||||
{
|
||||
c.ProcessQueue(TokenSource.Token);
|
||||
@ -73,7 +67,7 @@ namespace Selector
|
||||
{
|
||||
if (Task is not null && !Task.IsCompleted)
|
||||
{
|
||||
TokenSource.Cancel();
|
||||
TokenSource?.Cancel();
|
||||
}
|
||||
|
||||
TokenSource = new();
|
||||
@ -91,15 +85,15 @@ namespace Selector
|
||||
{
|
||||
Consumers.ForEach(c => c.Unsubscribe(Watcher));
|
||||
|
||||
TokenSource.Cancel();
|
||||
TokenSource?.Cancel();
|
||||
IsRunning = false;
|
||||
}
|
||||
|
||||
private void Clear()
|
||||
{
|
||||
if (IsRunning
|
||||
|| Task.Status == TaskStatus.Running
|
||||
|| Task.Status == TaskStatus.WaitingToRun)
|
||||
|| Task?.Status == TaskStatus.Running
|
||||
|| Task?.Status == TaskStatus.WaitingToRun)
|
||||
{
|
||||
Stop();
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user