adding signalr project

This commit is contained in:
Andy Pack 2023-01-22 10:28:52 +00:00
parent b9cc6d5d45
commit 562c119e18
Signed by: sarsoo
GPG Key ID: A55BA3536A5E0ED7
20 changed files with 290 additions and 48 deletions

View File

@ -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

View File

@ -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();
}
}

24
Selector.SignalR/INow.cs Normal file
View File

@ -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);
}

14
Selector.SignalR/IPast.cs Normal file
View File

@ -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);
}

View File

@ -0,0 +1,6 @@
namespace Selector.SignalR;
public interface ICard
{
string Content { get; set; }
}

View File

@ -0,0 +1,7 @@
namespace Selector.SignalR;
public interface IChartEntry
{
string Name { get; set; }
int Value { get; set; }
}

View File

@ -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; }
}

View File

@ -0,0 +1,12 @@
using System.Collections.Generic;
namespace Selector.SignalR;
public interface IRankResult
{
IEnumerable<IChartEntry> TrackEntries { get; set; }
IEnumerable<IChartEntry> AlbumEntries { get; set; }
IEnumerable<IChartEntry> ArtistEntries { get; set; }
IEnumerable<CountSample> ResampledSeries { get; set; }
int TotalCount { get; set; }
}

View File

@ -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<IDisposable> NewPlayingCallbacks = new();
private List<IDisposable> NewAudioFeatureCallbacks = new();
private List<IDisposable> NewPlayCountCallbacks = new();
private List<IDisposable> NewCardCallbacks = new();
private bool disposedValue;
public NowHubClient(): base("nowhub")
{
}
public void OnNewPlaying(Action<CurrentlyPlayingDTO> action)
{
NewPlayingCallbacks.Add(hubConnection.On(nameof(OnNewPlaying), action));
}
public void OnNewAudioFeature(Action<TrackAudioFeatures> action)
{
NewAudioFeatureCallbacks.Add(hubConnection.On(nameof(OnNewAudioFeature), action));
}
public void OnNewPlayCount(Action<PlayCount> action)
{
NewPlayCountCallbacks.Add(hubConnection.On(nameof(OnNewPlayCount), action));
}
public void OnNewCard(Action<ICard> 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);
}
}

View File

@ -0,0 +1,25 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<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="7.0.2" />
</ItemGroup>
<ItemGroup>
<None Remove="Models\" />
</ItemGroup>
<ItemGroup>
<Folder Include="Models\" />
</ItemGroup>
</Project>

View File

@ -1,6 +1,7 @@
using Microsoft.Extensions.DependencyInjection;
using Selector.Web.Service;
using Selector.Web.Hubs;
using Selector.SignalR;
namespace Selector.Web.Extensions
{

View File

@ -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<INowPlayingHubClient>
public class NowPlayingHub : Hub<INowPlayingHubClient>, INowPlayingHub
{
private readonly IDatabaseAsync Cache;
private readonly AudioFeaturePuller AudioFeaturePuller;
@ -37,8 +30,8 @@ namespace Selector.Web.Hubs
private readonly IOptions<NowPlayingOptions> nowOptions;
public NowPlayingHub(
IDatabaseAsync cache,
AudioFeaturePuller featurePuller,
IDatabaseAsync cache,
AudioFeaturePuller featurePuller,
ApplicationDbContext db,
IScrobbleRepository scrobbleRepository,
IOptions<NowPlayingOptions> 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);
}

View File

@ -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<IPastHubClient>
public class PastHub : Hub<IPastHubClient>, IPastHub
{
private readonly IDatabaseAsync Cache;
private readonly AudioFeaturePuller AudioFeaturePuller;
@ -28,8 +24,8 @@ namespace Selector.Web.Hubs
private readonly IOptions<PastOptions> pastOptions;
public PastHub(
IDatabaseAsync cache,
AudioFeaturePuller featurePuller,
IDatabaseAsync cache,
AudioFeaturePuller featurePuller,
ApplicationDbContext db,
IListenRepository listenRepository,
IOptions<PastOptions> 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()
{

View File

@ -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; }
}

View File

@ -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; }

View File

@ -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; }

View File

@ -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<ChartEntry> TrackEntries { get; set; }
public IEnumerable<ChartEntry> AlbumEntries { get; set; }
public IEnumerable<ChartEntry> ArtistEntries { get; set; }
public IEnumerable<IChartEntry> TrackEntries { get; set; }
public IEnumerable<IChartEntry> AlbumEntries { get; set; }
public IEnumerable<IChartEntry> ArtistEntries { get; set; }
public IEnumerable<CountSample> ResampledSeries { get; set; }

View File

@ -11,6 +11,9 @@
<ProjectReference Include="..\Selector.Model\Selector.Model.csproj" />
<ProjectReference Include="..\Selector.Cache\Selector.Cache.csproj" />
<ProjectReference Include="..\Selector.Event\Selector.Event.csproj" />
<ProjectReference Include="..\Selector.SignalR\Selector.SignalR.csproj">
<GlobalPropertiesToRemove></GlobalPropertiesToRemove>
</ProjectReference>
</ItemGroup>
<ItemGroup>

View File

@ -4,6 +4,7 @@ using Microsoft.Extensions.Logging;
using Selector.Web.Hubs;
using Selector.Events;
using Selector.SignalR;
namespace Selector.Web.Service
{

View File

@ -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