cache hub proxy working

This commit is contained in:
andy 2021-11-05 07:58:48 +00:00
parent 3867ea8fad
commit 0d74257b89
13 changed files with 175 additions and 60 deletions

6
.vscode/launch.json vendored
View File

@ -8,10 +8,10 @@
"name": "Selector.Web", "name": "Selector.Web",
"type": "coreclr", "type": "coreclr",
"request": "launch", "request": "launch",
"preLaunchTask": "build", "preLaunchTask": "buildWeb",
"program": "${workspaceFolder}/Selector.Web/bin/Debug/net5.0/Selector.Web.dll", "program": "${workspaceFolder}/Selector.Web/bin/Debug/net5.0/Selector.Web.dll",
"args": [], "args": [],
"cwd": "${workspaceFolder}", "cwd": "${workspaceFolder}/Selector.Web",
"stopAtEntry": false, "stopAtEntry": false,
"serverReadyAction": { "serverReadyAction": {
"action": "openExternally", "action": "openExternally",
@ -29,7 +29,7 @@
"name": "Selector.CLI", "name": "Selector.CLI",
"type": "coreclr", "type": "coreclr",
"request": "launch", "request": "launch",
"preLaunchTask": "build", "preLaunchTask": "buildCLI",
"program": "${workspaceFolder}/Selector.CLI/bin/Debug/net5.0/Selector.CLI.dll", "program": "${workspaceFolder}/Selector.CLI/bin/Debug/net5.0/Selector.CLI.dll",
"env": { "env": {
"DOTNET_ENVIRONMENT": "Development" "DOTNET_ENVIRONMENT": "Development"

14
.vscode/tasks.json vendored
View File

@ -2,7 +2,19 @@
"version": "2.0.0", "version": "2.0.0",
"tasks": [ "tasks": [
{ {
"label": "build", "label": "buildWeb",
"command": "dotnet",
"type": "process",
"args": [
"build",
"${workspaceFolder}/Selector.Web/Selector.Web.csproj",
"/property:GenerateFullPaths=true",
"/consoleloggerparameters:NoSummary"
],
"problemMatcher": "$msCompile"
},
{
"label": "buildCLI",
"command": "dotnet", "command": "dotnet",
"type": "process", "type": "process",
"args": [ "args": [

View File

@ -43,6 +43,7 @@ namespace Selector.Cache
Logger.LogTrace($"Publishing current for [{e.Username}]"); Logger.LogTrace($"Publishing current for [{e.Username}]");
// TODO: currently using spotify username for cache key, use db username
var receivers = await Subscriber.PublishAsync(Key.CurrentlyPlaying(e.Username), payload); var receivers = await Subscriber.PublishAsync(Key.CurrentlyPlaying(e.Username), payload);
Logger.LogDebug($"Published current for [{e.Username}], {receivers} receivers"); Logger.LogDebug($"Published current for [{e.Username}], {receivers} receivers");

View File

@ -5,7 +5,7 @@ using SpotifyAPI.Web;
namespace Selector.Cache { namespace Selector.Cache {
public class CurrentlyPlayingDTO { public class CurrentlyPlayingDTO {
public CurrentlyPlayingContext Context { get; set; } public CurrentlyPlayingContextDTO Context { get; set; }
public string Username { get; set; } public string Username { get; set; }
public FullTrack Track { get; set; } public FullTrack Track { get; set; }
@ -36,5 +36,38 @@ namespace Selector.Cache {
throw new ArgumentException("Unknown item item"); throw new ArgumentException("Unknown item item");
} }
} }
public override string ToString() => $"[{Username}] [{Context}]";
}
public class CurrentlyPlayingContextDTO
{
public Device Device { get; set; }
public string RepeatState { get; set; }
public bool ShuffleState { get; set; }
public Context Context { get; set; }
public long Timestamp { get; set; }
public int ProgressMs { get; set; }
public bool IsPlaying { get; set; }
public string CurrentlyPlayingType { get; set; }
public Actions Actions { get; set; }
public static implicit operator CurrentlyPlayingContextDTO(CurrentlyPlayingContext context)
{
return new CurrentlyPlayingContextDTO {
Device = context.Device,
RepeatState = context.RepeatState,
ShuffleState = context.ShuffleState,
Context = context.Context,
Timestamp = context.Timestamp,
ProgressMs = context.ProgressMs,
IsPlaying = context.IsPlaying,
CurrentlyPlayingType = context.CurrentlyPlayingType,
Actions = context.Actions
};
}
public override string ToString() => $"{IsPlaying}, {Device?.DisplayString()}";
} }
} }

View File

@ -35,7 +35,7 @@ namespace Selector.Web.Pages
public void OnGet() public void OnGet()
{ {
HubProxy.FormMapping(MappingFactory.Get(UserManager.GetUserId(User)));
} }
} }
} }

View File

@ -17,11 +17,11 @@ namespace Selector.Web.Service
{ {
private readonly ILogger<CacheHubProxy> Logger; private readonly ILogger<CacheHubProxy> Logger;
private readonly ISubscriber Subscriber; private readonly ISubscriber Subscriber;
private readonly ServiceProvider Services; private readonly IServiceProvider Services;
public CacheHubProxy(ILogger<CacheHubProxy> logger, public CacheHubProxy(ILogger<CacheHubProxy> logger,
ISubscriber subscriber, ISubscriber subscriber,
ServiceProvider services IServiceProvider services
) )
{ {
Logger = logger; Logger = logger;
@ -35,37 +35,4 @@ namespace Selector.Web.Service
mapping.ConstructMapping(Subscriber, context); mapping.ConstructMapping(Subscriber, context);
} }
} }
// public class CacheHubProxy<THub, T>
// where THub: Hub<T>
// where T: class
// {
// private readonly ILogger<CacheHubProxy<THub, T>> Logger;
// private readonly ISubscriber Subscriber;
// private readonly IHubContext<THub, T> HubContext;
// private readonly List<ICacheHubMapping<THub, T>> Mappings;
// public CacheHubProxy(ILogger<CacheHubProxy<THub, T>> logger,
// ISubscriber subscriber,
// IHubContext<THub, T> hubContext,
// IEnumerable<ICacheHubMapping<THub, T>> mappings
// )
// {
// Logger = logger;
// Subscriber = subscriber;
// HubContext = hubContext;
// Mappings = mappings.ToList();
// }
// public void FormMapping(ICacheHubMapping<THub, T> mapping)
// {
// mapping.ConstructMapping(Subscriber, HubContext);
// }
// public void AddMapping(ICacheHubMapping<THub, T> mapping)
// {
// Mappings.Add(mapping);
// FormMapping(mapping);
// }
// }
} }

View File

@ -1,6 +1,8 @@
using System; using System;
using System.Collections.Generic;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
@ -10,19 +12,31 @@ namespace Selector.Web.Service
{ {
private readonly ILogger<CacheHubProxyService> Logger; private readonly ILogger<CacheHubProxyService> Logger;
private readonly CacheHubProxy Proxy; private readonly CacheHubProxy Proxy;
private readonly IServiceScopeFactory ScopeFactory;
public CacheHubProxyService( public CacheHubProxyService(
ILogger<CacheHubProxyService> logger, ILogger<CacheHubProxyService> logger,
CacheHubProxy proxy CacheHubProxy proxy,
IServiceScopeFactory scopeFactory
) )
{ {
Logger = logger; Logger = logger;
Proxy = proxy; Proxy = proxy;
ScopeFactory = scopeFactory;
} }
public Task StartAsync(CancellationToken cancellationToken) public Task StartAsync(CancellationToken cancellationToken)
{ {
Logger.LogInformation("Starting cache hub proxy"); Logger.LogInformation("Starting cache hub proxy");
using(var scope = ScopeFactory.CreateScope())
{
foreach(var mapping in scope.ServiceProvider.GetServices<IUserMapping>())
{
mapping.FormAll();
}
}
return Task.CompletedTask; return Task.CompletedTask;
} }

View File

@ -14,19 +14,32 @@ namespace Selector.Web.Service
{ {
public class NowPlayingMapping : ICacheHubMapping<NowPlayingHub, INowPlayingHubClient> public class NowPlayingMapping : ICacheHubMapping<NowPlayingHub, INowPlayingHubClient>
{ {
private readonly ILogger<NowPlayingMapping> Logger;
private readonly string UserId; private readonly string UserId;
private readonly string Username;
public NowPlayingMapping(ILogger<NowPlayingMapping> logger, string userId) public NowPlayingMapping(ILogger<NowPlayingMapping> logger, string userId, string username)
{ {
Logger = logger;
UserId = userId; UserId = userId;
Username = username;
} }
public async Task ConstructMapping(ISubscriber subscriber, IHubContext<NowPlayingHub, INowPlayingHubClient> hub) public async Task ConstructMapping(ISubscriber subscriber, IHubContext<NowPlayingHub, INowPlayingHubClient> hub)
{ {
(await subscriber.SubscribeAsync(Key.CurrentlyPlaying(UserId))).OnMessage(async message => { var key = Key.CurrentlyPlaying(Username);
(await subscriber.SubscribeAsync(key)).OnMessage(async message => {
var deserialised = JsonSerializer.Deserialize<CurrentlyPlayingDTO>(message.ToString()); try{
await hub.Clients.User(UserId).OnNewPlaying(deserialised); var trimmedMessage = message.ToString().Substring(key.Length + 1);
var deserialised = JsonSerializer.Deserialize<CurrentlyPlayingDTO>(trimmedMessage);
Logger.LogDebug($"Received new currently playing [{deserialised.Username}] [{deserialised.Username}]");
await hub.Clients.User(UserId).OnNewPlaying(deserialised);
}
catch(Exception e)
{
Logger.LogError(e, $"Error parsing new currently playing [{message}]");
}
}); });
} }
} }

View File

@ -1,18 +1,22 @@
using System; using System;
using System.Collections.Generic;
using System.Text;
using System.Threading.Tasks;
using Microsoft.AspNetCore.SignalR; using Microsoft.AspNetCore.SignalR;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using SpotifyAPI.Web; using Selector.Web.Hubs;
namespace Selector.Web.Service namespace Selector.Web.Service
{ {
public interface INowPlayingMappingFactory { public interface IUserMappingFactory<out TMap, THub, T>
public NowPlayingMapping Get(string userId); where TMap : ICacheHubMapping<THub, T>
where THub : Hub<T>
where T : class
{
public TMap Get(string userId, string username);
} }
public interface INowPlayingMappingFactory: IUserMappingFactory<NowPlayingMapping, NowPlayingHub, INowPlayingHubClient>
{ }
public class NowPlayingMappingFactory : INowPlayingMappingFactory { public class NowPlayingMappingFactory : INowPlayingMappingFactory {
private readonly ILoggerFactory LoggerFactory; private readonly ILoggerFactory LoggerFactory;
@ -22,11 +26,12 @@ namespace Selector.Web.Service
LoggerFactory = loggerFactory; LoggerFactory = loggerFactory;
} }
public NowPlayingMapping Get(string userId) public NowPlayingMapping Get(string userId, string username)
{ {
return new NowPlayingMapping( return new NowPlayingMapping(
LoggerFactory?.CreateLogger<NowPlayingMapping>(), LoggerFactory?.CreateLogger<NowPlayingMapping>(),
userId userId,
username
); );
} }
} }

View File

@ -0,0 +1,37 @@
using System;
using System.Linq;
using Selector.Model;
namespace Selector.Web.Service
{
public interface IUserMapping {
public void FormAll();
}
public class NowPlayingUserMapping: IUserMapping
{
private readonly ApplicationDbContext Db;
private readonly CacheHubProxy Proxy;
private readonly INowPlayingMappingFactory NowPlayingMappingFactory;
public NowPlayingUserMapping(
ApplicationDbContext db,
CacheHubProxy proxy,
INowPlayingMappingFactory nowPlayingMappingFactory
)
{
Db = db;
Proxy = proxy;
NowPlayingMappingFactory = nowPlayingMappingFactory;
}
public void FormAll()
{
foreach(var user in Db.Users)
{
Proxy.FormMapping(NowPlayingMappingFactory.Get(user.Id, user.UserName));
}
}
}
}

View File

@ -117,9 +117,11 @@ namespace Selector.Web
services.AddTransient<ISubscriber>(services => services.GetService<ConnectionMultiplexer>().GetSubscriber()); services.AddTransient<ISubscriber>(services => services.GetService<ConnectionMultiplexer>().GetSubscriber());
} }
// HOSTED
services.AddSingleton<CacheHubProxy>(); services.AddSingleton<CacheHubProxy>();
services.AddSingleton<INowPlayingMappingFactory, NowPlayingMappingFactory>(); services.AddHostedService<CacheHubProxyService>();
services.AddTransient<INowPlayingMappingFactory, NowPlayingMappingFactory>();
services.AddScoped<IUserMapping, NowPlayingUserMapping>();
} }
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline. // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.

View File

@ -1,6 +1,25 @@
export interface SignalR {
nowPlayingHub: nowPlayingProxy;
}
export interface nowPlayingProxy {
client: NowPlayingHubClient;
server: NowPlayingHub;
}
export interface NowPlayingHubClient {
OnNewPlaying: (context: CurrentlyPlayingDTO) => void;
}
export interface NowPlayingHub { export interface NowPlayingHub {
SendNewPlaying(context: CurrentlyPlayingDTO): void;
}
export interface CurrentlyPlayingDTO {
context: CurrentlyPlayingContextDTO;
username: string;
track: FullTrack;
episode: FullEpisode;
} }
export interface ListeningChangeEventArgs { export interface ListeningChangeEventArgs {
@ -9,6 +28,18 @@ export interface ListeningChangeEventArgs {
username: string; username: string;
} }
export interface CurrentlyPlayingContextDTO {
device: Device;
repeatState: string;
shuffleState: boolean;
context: Context;
timestamp: number;
progressMs: number;
isPlaying: boolean;
currentlyPlayingType: string;
actions: Actions;
}
export interface CurrentlyPlayingContext { export interface CurrentlyPlayingContext {
device: Device; device: Device;
repeatState: string; repeatState: string;

View File

@ -1,11 +1,11 @@
import * as signalR from "@microsoft/signalr"; import * as signalR from "@microsoft/signalr";
import { FullTrack } from "./HubInterfaces"; import { FullTrack, CurrentlyPlayingDTO } from "./HubInterfaces";
const connection = new signalR.HubConnectionBuilder() const connection = new signalR.HubConnectionBuilder()
.withUrl("/hub") .withUrl("/hub")
.build(); .build();
connection.on("OnNewPlaying", context => console.log(context)); connection.on("OnNewPlaying", (context: CurrentlyPlayingDTO) => console.log(context));
connection.start().catch(err => console.error(err)); connection.start().catch(err => console.error(err));