From 562c119e1801e2ddb37ffccc9865008c0b1e00ba Mon Sep 17 00:00:00 2001 From: Andy Pack Date: Sun, 22 Jan 2023 10:28:52 +0000 Subject: [PATCH] adding signalr project --- Selector.Core.sln | 6 ++ Selector.SignalR/BaseSignalRClient.cs | 40 ++++++++ Selector.SignalR/INow.cs | 24 +++++ Selector.SignalR/IPast.cs | 14 +++ Selector.SignalR/Models/ICard.cs | 6 ++ Selector.SignalR/Models/IChartEntry.cs | 7 ++ Selector.SignalR/Models/IPastParams.cs | 10 ++ Selector.SignalR/Models/IRankResult.cs | 12 +++ Selector.SignalR/NowHubClient.cs | 98 +++++++++++++++++++ Selector.SignalR/Selector.SignalR.csproj | 25 +++++ Selector.Web/Extensions/ServiceExtensions.cs | 1 + Selector.Web/Hubs/NowPlayingHub.cs | 43 +++----- Selector.Web/Hubs/PastHub.cs | 16 ++- Selector.Web/NowPlaying/Card.cs | 11 ++- Selector.Web/Past/ChartEntry.cs | 3 +- Selector.Web/Past/PastParams.cs | 3 +- Selector.Web/Past/RankResult.cs | 9 +- Selector.Web/Selector.Web.csproj | 3 + .../EventMappings/NowPlayingHubMapping.cs | 1 + Selector.sln | 6 ++ 20 files changed, 290 insertions(+), 48 deletions(-) create mode 100644 Selector.SignalR/BaseSignalRClient.cs create mode 100644 Selector.SignalR/INow.cs create mode 100644 Selector.SignalR/IPast.cs create mode 100644 Selector.SignalR/Models/ICard.cs create mode 100644 Selector.SignalR/Models/IChartEntry.cs create mode 100644 Selector.SignalR/Models/IPastParams.cs create mode 100644 Selector.SignalR/Models/IRankResult.cs create mode 100644 Selector.SignalR/NowHubClient.cs create mode 100644 Selector.SignalR/Selector.SignalR.csproj diff --git a/Selector.Core.sln b/Selector.Core.sln index e143bf2..edf83f8 100644 --- a/Selector.Core.sln +++ b/Selector.Core.sln @@ -19,6 +19,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Selector.Event", "Selector. EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Selector.Data", "Selector.Data\Selector.Data.csproj", "{CB62ACCB-94F1-4B78-A195-8B108B9E800D}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Selector.SignalR", "Selector.SignalR\Selector.SignalR.csproj", "{089C9DE8-2B73-4341-BA17-572CD6BAD14D}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -57,6 +59,10 @@ Global {CB62ACCB-94F1-4B78-A195-8B108B9E800D}.Debug|Any CPU.Build.0 = Debug|Any CPU {CB62ACCB-94F1-4B78-A195-8B108B9E800D}.Release|Any CPU.ActiveCfg = Release|Any CPU {CB62ACCB-94F1-4B78-A195-8B108B9E800D}.Release|Any CPU.Build.0 = Release|Any CPU + {089C9DE8-2B73-4341-BA17-572CD6BAD14D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {089C9DE8-2B73-4341-BA17-572CD6BAD14D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {089C9DE8-2B73-4341-BA17-572CD6BAD14D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {089C9DE8-2B73-4341-BA17-572CD6BAD14D}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/Selector.SignalR/BaseSignalRClient.cs b/Selector.SignalR/BaseSignalRClient.cs new file mode 100644 index 0000000..ff0f493 --- /dev/null +++ b/Selector.SignalR/BaseSignalRClient.cs @@ -0,0 +1,40 @@ +using System; +using Microsoft.AspNetCore.SignalR.Client; + +namespace Selector.SignalR; + +public abstract class BaseSignalRClient: IAsyncDisposable +{ + private readonly string _baseUrl; + protected HubConnection hubConnection; + + public BaseSignalRClient(string path) + { + var baseOverride = Environment.GetEnvironmentVariable("SELECTOR_BASE_URL"); + + if (!string.IsNullOrWhiteSpace(baseOverride)) + { + _baseUrl = baseOverride; + } + else + { + _baseUrl = "https://selector.sarsoo.xyz"; + } + + hubConnection = new HubConnectionBuilder() + .WithUrl(_baseUrl + "/" + path) + .WithAutomaticReconnect() + .Build(); + } + + public ValueTask DisposeAsync() + { + return ((IAsyncDisposable)hubConnection).DisposeAsync(); + } + + public async Task StartAsync() + { + await hubConnection.StartAsync(); + } +} + diff --git a/Selector.SignalR/INow.cs b/Selector.SignalR/INow.cs new file mode 100644 index 0000000..295dd80 --- /dev/null +++ b/Selector.SignalR/INow.cs @@ -0,0 +1,24 @@ +using System; +using SpotifyAPI.Web; +using System.Threading.Tasks; + +namespace Selector.SignalR; + +public interface INowPlayingHubClient +{ + public Task OnNewPlaying(CurrentlyPlayingDTO context); + public Task OnNewAudioFeature(TrackAudioFeatures features); + public Task OnNewPlayCount(PlayCount playCount); + public Task OnNewCard(ICard card); +} + +public interface INowPlayingHub +{ + Task OnConnected(); + Task PlayDensityFacts(string track, string artist, string album, string albumArtist); + Task SendAudioFeatures(string trackId); + Task SendFacts(string track, string artist, string album, string albumArtist); + Task SendNewPlaying(); + Task SendPlayCount(string track, string artist, string album, string albumArtist); +} + diff --git a/Selector.SignalR/IPast.cs b/Selector.SignalR/IPast.cs new file mode 100644 index 0000000..8f365c6 --- /dev/null +++ b/Selector.SignalR/IPast.cs @@ -0,0 +1,14 @@ +using System.Threading.Tasks; + +namespace Selector.SignalR; + +public interface IPastHub +{ + Task OnConnected(); + Task OnSubmitted(IPastParams param); +} + +public interface IPastHubClient +{ + public Task OnRankResult(IRankResult result); +} \ No newline at end of file diff --git a/Selector.SignalR/Models/ICard.cs b/Selector.SignalR/Models/ICard.cs new file mode 100644 index 0000000..1f98294 --- /dev/null +++ b/Selector.SignalR/Models/ICard.cs @@ -0,0 +1,6 @@ +namespace Selector.SignalR; + +public interface ICard +{ + string Content { get; set; } +} \ No newline at end of file diff --git a/Selector.SignalR/Models/IChartEntry.cs b/Selector.SignalR/Models/IChartEntry.cs new file mode 100644 index 0000000..5e1c36f --- /dev/null +++ b/Selector.SignalR/Models/IChartEntry.cs @@ -0,0 +1,7 @@ +namespace Selector.SignalR; + +public interface IChartEntry +{ + string Name { get; set; } + int Value { get; set; } +} \ No newline at end of file diff --git a/Selector.SignalR/Models/IPastParams.cs b/Selector.SignalR/Models/IPastParams.cs new file mode 100644 index 0000000..f2e7750 --- /dev/null +++ b/Selector.SignalR/Models/IPastParams.cs @@ -0,0 +1,10 @@ +namespace Selector.SignalR; + +public interface IPastParams +{ + string Track { get; set; } + string Album { get; set; } + string Artist { get; set; } + string From { get; set; } + string To { get; set; } +} \ No newline at end of file diff --git a/Selector.SignalR/Models/IRankResult.cs b/Selector.SignalR/Models/IRankResult.cs new file mode 100644 index 0000000..d28e1c9 --- /dev/null +++ b/Selector.SignalR/Models/IRankResult.cs @@ -0,0 +1,12 @@ +using System.Collections.Generic; + +namespace Selector.SignalR; + +public interface IRankResult +{ + IEnumerable TrackEntries { get; set; } + IEnumerable AlbumEntries { get; set; } + IEnumerable ArtistEntries { get; set; } + IEnumerable ResampledSeries { get; set; } + int TotalCount { get; set; } +} \ No newline at end of file diff --git a/Selector.SignalR/NowHubClient.cs b/Selector.SignalR/NowHubClient.cs new file mode 100644 index 0000000..9885c00 --- /dev/null +++ b/Selector.SignalR/NowHubClient.cs @@ -0,0 +1,98 @@ +using System; +using System.Threading.Tasks; +using Microsoft.AspNetCore.SignalR.Client; +using SpotifyAPI.Web; + +namespace Selector.SignalR; + +public class NowHubClient: BaseSignalRClient, INowPlayingHub, IDisposable +{ + private List NewPlayingCallbacks = new(); + private List NewAudioFeatureCallbacks = new(); + private List NewPlayCountCallbacks = new(); + private List NewCardCallbacks = new(); + private bool disposedValue; + + public NowHubClient(): base("nowhub") + { + } + + public void OnNewPlaying(Action action) + { + NewPlayingCallbacks.Add(hubConnection.On(nameof(OnNewPlaying), action)); + } + + public void OnNewAudioFeature(Action action) + { + NewAudioFeatureCallbacks.Add(hubConnection.On(nameof(OnNewAudioFeature), action)); + } + + public void OnNewPlayCount(Action action) + { + NewPlayCountCallbacks.Add(hubConnection.On(nameof(OnNewPlayCount), action)); + } + + public void OnNewCard(Action action) + { + NewCardCallbacks.Add(hubConnection.On(nameof(OnNewCard), action)); + } + + public Task OnConnected() + { + return hubConnection.InvokeAsync(nameof(OnConnected)); + } + + public Task PlayDensityFacts(string track, string artist, string album, string albumArtist) + { + return hubConnection.InvokeAsync(nameof(PlayDensityFacts), track, artist, album, albumArtist); + } + + public Task SendAudioFeatures(string trackId) + { + return hubConnection.InvokeAsync(nameof(SendAudioFeatures), trackId); + } + + public Task SendFacts(string track, string artist, string album, string albumArtist) + { + return hubConnection.InvokeAsync(nameof(SendFacts), track, artist, album, albumArtist); + } + + public Task SendNewPlaying() + { + return hubConnection.InvokeAsync(nameof(SendNewPlaying)); + } + + public Task SendPlayCount(string track, string artist, string album, string albumArtist) + { + return hubConnection.InvokeAsync(nameof(SendPlayCount), track, artist, album, albumArtist); + } + + protected virtual void Dispose(bool disposing) + { + if (!disposedValue) + { + if (disposing) + { + foreach(var callback in NewPlayingCallbacks + .Concat(NewAudioFeatureCallbacks) + .Concat(NewPlayCountCallbacks) + .Concat(NewCardCallbacks)) + { + callback.Dispose(); + } + + base.DisposeAsync(); + } + + disposedValue = true; + } + } + + public void Dispose() + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(disposing: true); + GC.SuppressFinalize(this); + } +} + diff --git a/Selector.SignalR/Selector.SignalR.csproj b/Selector.SignalR/Selector.SignalR.csproj new file mode 100644 index 0000000..16be311 --- /dev/null +++ b/Selector.SignalR/Selector.SignalR.csproj @@ -0,0 +1,25 @@ + + + + net7.0 + enable + enable + + + + + + + + + + + + + + + + + diff --git a/Selector.Web/Extensions/ServiceExtensions.cs b/Selector.Web/Extensions/ServiceExtensions.cs index c4dcfad..204e0b8 100644 --- a/Selector.Web/Extensions/ServiceExtensions.cs +++ b/Selector.Web/Extensions/ServiceExtensions.cs @@ -1,6 +1,7 @@ using Microsoft.Extensions.DependencyInjection; using Selector.Web.Service; using Selector.Web.Hubs; +using Selector.SignalR; namespace Selector.Web.Extensions { diff --git a/Selector.Web/Hubs/NowPlayingHub.cs b/Selector.Web/Hubs/NowPlayingHub.cs index 1f5ba26..97f2a35 100644 --- a/Selector.Web/Hubs/NowPlayingHub.cs +++ b/Selector.Web/Hubs/NowPlayingHub.cs @@ -11,21 +11,14 @@ using Microsoft.Extensions.Options; using Selector.Cache; using Selector.Model; using Selector.Model.Extensions; +using Selector.SignalR; using Selector.Web.NowPlaying; using SpotifyAPI.Web; using StackExchange.Redis; namespace Selector.Web.Hubs { - public interface INowPlayingHubClient - { - public Task OnNewPlaying(CurrentlyPlayingDTO context); - public Task OnNewAudioFeature(TrackAudioFeatures features); - public Task OnNewPlayCount(PlayCount playCount); - public Task OnNewCard(Card card); - } - - public class NowPlayingHub: Hub + public class NowPlayingHub : Hub, INowPlayingHub { private readonly IDatabaseAsync Cache; private readonly AudioFeaturePuller AudioFeaturePuller; @@ -37,8 +30,8 @@ namespace Selector.Web.Hubs private readonly IOptions nowOptions; public NowPlayingHub( - IDatabaseAsync cache, - AudioFeaturePuller featurePuller, + IDatabaseAsync cache, + AudioFeaturePuller featurePuller, ApplicationDbContext db, IScrobbleRepository scrobbleRepository, IOptions options, @@ -77,15 +70,15 @@ namespace Selector.Web.Hubs var user = Db.Users .AsNoTracking() .Where(u => u.Id == Context.UserIdentifier) - .SingleOrDefault() + .SingleOrDefault() ?? throw new SqlNullValueException("No user returned"); var watcher = Db.Watcher .AsNoTracking() .Where(w => w.UserId == Context.UserIdentifier && w.Type == WatcherType.Player) - .SingleOrDefault() + .SingleOrDefault() ?? throw new SqlNullValueException($"No player watcher found for [{user.UserName}]"); - + var feature = await AudioFeaturePuller.Get(user.SpotifyRefreshToken, trackId); if (feature is not null) @@ -96,7 +89,7 @@ namespace Selector.Web.Hubs public async Task SendPlayCount(string track, string artist, string album, string albumArtist) { - if(PlayCountPuller is not null) + if (PlayCountPuller is not null) { var user = Db.Users .AsNoTracking() @@ -124,17 +117,13 @@ namespace Selector.Web.Hubs public async Task SendFacts(string track, string artist, string album, string albumArtist) { - var user = Db.Users - .AsNoTracking() - .Where(u => u.Id == Context.UserIdentifier) - .SingleOrDefault() - ?? throw new SqlNullValueException("No user returned"); - - await PlayDensityFacts(user, track, artist, album, albumArtist); + await PlayDensityFacts(track, artist, album, albumArtist); } - public async Task PlayDensityFacts(ApplicationUser user, string track, string artist, string album, string albumArtist) + public async Task PlayDensityFacts(string track, string artist, string album, string albumArtist) { + var user = await Db.Users.AsNoTracking().FirstOrDefaultAsync(u => u.Id == Context.UserIdentifier); + if (user.ScrobbleSavingEnabled()) { var artistScrobbles = ScrobbleRepository.GetAll(userId: user.Id, artistName: artist, from: GetMaximumWindow()).ToArray(); @@ -144,7 +133,7 @@ namespace Selector.Web.Hubs if (artistDensity > nowOptions.Value.ArtistDensityThreshold) { - tasks.Add(Clients.Caller.OnNewCard(new() + tasks.Add(Clients.Caller.OnNewCard(new Card() { Content = $"You're on a {artist} binge! {artistDensity} plays/day recently" })); @@ -154,7 +143,7 @@ namespace Selector.Web.Hubs if (albumDensity > nowOptions.Value.AlbumDensityThreshold) { - tasks.Add(Clients.Caller.OnNewCard(new() + tasks.Add(Clients.Caller.OnNewCard(new Card() { Content = $"You're on a {album} binge! {albumDensity} plays/day recently" })); @@ -164,13 +153,13 @@ namespace Selector.Web.Hubs if (albumDensity > nowOptions.Value.TrackDensityThreshold) { - tasks.Add(Clients.Caller.OnNewCard(new() + tasks.Add(Clients.Caller.OnNewCard(new Card() { Content = $"You're on a {track} binge! {trackDensity} plays/day recently" })); } - if(tasks.Any()) + if (tasks.Any()) { await Task.WhenAll(tasks); } diff --git a/Selector.Web/Hubs/PastHub.cs b/Selector.Web/Hubs/PastHub.cs index 2b5b16b..4efe2c1 100644 --- a/Selector.Web/Hubs/PastHub.cs +++ b/Selector.Web/Hubs/PastHub.cs @@ -7,16 +7,12 @@ using Microsoft.AspNetCore.SignalR; using Microsoft.Extensions.Options; using Selector.Cache; using Selector.Model; +using Selector.SignalR; using StackExchange.Redis; namespace Selector.Web.Hubs { - public interface IPastHubClient - { - public Task OnRankResult(RankResult result); - } - - public class PastHub: Hub + public class PastHub : Hub, IPastHub { private readonly IDatabaseAsync Cache; private readonly AudioFeaturePuller AudioFeaturePuller; @@ -28,8 +24,8 @@ namespace Selector.Web.Hubs private readonly IOptions pastOptions; public PastHub( - IDatabaseAsync cache, - AudioFeaturePuller featurePuller, + IDatabaseAsync cache, + AudioFeaturePuller featurePuller, ApplicationDbContext db, IListenRepository listenRepository, IOptions options, @@ -61,7 +57,7 @@ namespace Selector.Web.Hubs " (Expanded Edition)", }; - public async Task OnSubmitted(PastParams param) + public async Task OnSubmitted(IPastParams param) { param.Track = string.IsNullOrWhiteSpace(param.Track) ? null : param.Track; param.Album = string.IsNullOrWhiteSpace(param.Album) ? null : param.Album; @@ -111,7 +107,7 @@ namespace Selector.Web.Hubs .Take(pastOptions.Value.RankingCount) .ToArray(); - await Clients.Caller.OnRankResult(new() + await Clients.Caller.OnRankResult(new RankResult() { TrackEntries = trackGrouped.Select(x => new ChartEntry() { diff --git a/Selector.Web/NowPlaying/Card.cs b/Selector.Web/NowPlaying/Card.cs index 7041cd7..90f28e8 100644 --- a/Selector.Web/NowPlaying/Card.cs +++ b/Selector.Web/NowPlaying/Card.cs @@ -1,9 +1,10 @@ using System; -namespace Selector.Web.NowPlaying +using Selector.SignalR; + +namespace Selector.Web.NowPlaying; + +public class Card : ICard { - public class Card - { - public string Content { get; set; } - } + public string Content { get; set; } } diff --git a/Selector.Web/Past/ChartEntry.cs b/Selector.Web/Past/ChartEntry.cs index af3fed8..1579a9e 100644 --- a/Selector.Web/Past/ChartEntry.cs +++ b/Selector.Web/Past/ChartEntry.cs @@ -1,8 +1,9 @@ using System; +using Selector.SignalR; namespace Selector.Web; -public class ChartEntry +public class ChartEntry : IChartEntry { public string Name { get; set; } public int Value { get; set; } diff --git a/Selector.Web/Past/PastParams.cs b/Selector.Web/Past/PastParams.cs index ae5993d..40f811e 100644 --- a/Selector.Web/Past/PastParams.cs +++ b/Selector.Web/Past/PastParams.cs @@ -1,8 +1,9 @@ using System; +using Selector.SignalR; namespace Selector.Web; -public class PastParams +public class PastParams : IPastParams { public string Track { get; set; } public string Album { get; set; } diff --git a/Selector.Web/Past/RankResult.cs b/Selector.Web/Past/RankResult.cs index 984dd5c..5027be1 100644 --- a/Selector.Web/Past/RankResult.cs +++ b/Selector.Web/Past/RankResult.cs @@ -1,13 +1,14 @@ using System; using System.Collections.Generic; +using Selector.SignalR; namespace Selector.Web; -public class RankResult +public class RankResult : IRankResult { - public IEnumerable TrackEntries { get; set; } - public IEnumerable AlbumEntries { get; set; } - public IEnumerable ArtistEntries { get; set; } + public IEnumerable TrackEntries { get; set; } + public IEnumerable AlbumEntries { get; set; } + public IEnumerable ArtistEntries { get; set; } public IEnumerable ResampledSeries { get; set; } diff --git a/Selector.Web/Selector.Web.csproj b/Selector.Web/Selector.Web.csproj index 758d185..beb6fa3 100644 --- a/Selector.Web/Selector.Web.csproj +++ b/Selector.Web/Selector.Web.csproj @@ -11,6 +11,9 @@ + + + diff --git a/Selector.Web/Services/EventMappings/NowPlayingHubMapping.cs b/Selector.Web/Services/EventMappings/NowPlayingHubMapping.cs index 3e3a4fb..835637f 100644 --- a/Selector.Web/Services/EventMappings/NowPlayingHubMapping.cs +++ b/Selector.Web/Services/EventMappings/NowPlayingHubMapping.cs @@ -4,6 +4,7 @@ using Microsoft.Extensions.Logging; using Selector.Web.Hubs; using Selector.Events; +using Selector.SignalR; namespace Selector.Web.Service { diff --git a/Selector.sln b/Selector.sln index 311df8b..f024903 100644 --- a/Selector.sln +++ b/Selector.sln @@ -21,6 +21,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Selector.Data", "Selector.D EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Selector.MAUI", "Selector.MAUI\Selector.MAUI.csproj", "{090ADE89-4119-43D7-B108-3357B7D676FC}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Selector.SignalR", "Selector.SignalR\Selector.SignalR.csproj", "{F41D98F2-7684-4786-969C-BFC8DF7FB489}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -63,6 +65,10 @@ Global {090ADE89-4119-43D7-B108-3357B7D676FC}.Debug|Any CPU.Build.0 = Debug|Any CPU {090ADE89-4119-43D7-B108-3357B7D676FC}.Release|Any CPU.ActiveCfg = Release|Any CPU {090ADE89-4119-43D7-B108-3357B7D676FC}.Release|Any CPU.Build.0 = Release|Any CPU + {F41D98F2-7684-4786-969C-BFC8DF7FB489}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F41D98F2-7684-4786-969C-BFC8DF7FB489}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F41D98F2-7684-4786-969C-BFC8DF7FB489}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F41D98F2-7684-4786-969C-BFC8DF7FB489}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE