Files
fragalytics/src/Fragalytics.Web/Services/QueryService.cs
T
2026-04-20 18:15:08 +00:00

216 lines
7.8 KiB
C#

using Fragalytics.Web.Infrastructure;
using Fragalytics.Web.Models;
using Microsoft.AspNetCore.StaticFiles;
namespace Fragalytics.Web.Services;
public sealed class QueryService
{
private readonly JsonAppStore _store;
public QueryService(JsonAppStore store)
{
_store = store;
}
public Task<AppState> GetSnapshotAsync(CancellationToken cancellationToken = default) => _store.GetSnapshotAsync(cancellationToken);
public async Task<DashboardViewModel> GetDashboardAsync(CancellationToken cancellationToken = default)
{
var state = await _store.GetSnapshotAsync(cancellationToken);
return new DashboardViewModel
{
DemoCount = state.Demos.Count,
MatchCount = state.Matches.Count,
HighlightCount = state.Highlights.Count,
ClipJobCount = state.ClipJobs.Count,
Demos = state.Demos.OrderByDescending(item => item.UploadedAtUtc).Take(20).ToList(),
ClipJobs = state.ClipJobs.OrderByDescending(item => item.CreatedAtUtc).Take(12).ToList()
};
}
public async Task<DemoDetailsViewModel?> GetDemoDetailsAsync(int id, CancellationToken cancellationToken = default)
{
var state = await _store.GetSnapshotAsync(cancellationToken);
var demo = state.Demos.FirstOrDefault(item => item.Id == id);
if (demo is null)
{
return null;
}
MatchRecord? match = null;
List<HighlightDetailsRow> highlights = [];
if (demo.MatchId is int matchId)
{
match = state.Matches.FirstOrDefault(item => item.Id == matchId);
highlights = BuildHighlightRows(state, matchId).OrderByDescending(item => item.Highlight.Score).ToList();
}
return new DemoDetailsViewModel
{
Demo = demo,
Match = match,
Highlights = highlights
};
}
public async Task<MatchDetailsViewModel?> GetMatchDetailsAsync(int matchId, CancellationToken cancellationToken = default)
{
var state = await _store.GetSnapshotAsync(cancellationToken);
var match = state.Matches.FirstOrDefault(item => item.Id == matchId);
if (match is null)
{
return null;
}
return new MatchDetailsViewModel
{
Match = match,
Players = state.Players.Where(item => item.MatchId == matchId).OrderBy(item => item.Name).ToList(),
Rounds = state.Rounds.Where(item => item.MatchId == matchId).OrderBy(item => item.Number).ToList(),
Highlights = BuildHighlightRows(state, matchId).OrderByDescending(item => item.Highlight.Score).ToList()
};
}
public async Task<List<ClipJobWithHighlightViewModel>> GetClipJobsAsync(CancellationToken cancellationToken = default)
{
var state = await _store.GetSnapshotAsync(cancellationToken);
return state.ClipJobs
.OrderByDescending(item => item.CreatedAtUtc)
.Select(job => new ClipJobWithHighlightViewModel
{
ClipJob = job,
Highlight = state.Highlights.FirstOrDefault(highlight => highlight.Id == job.HighlightId),
ClipAsset = state.ClipAssets.FirstOrDefault(asset => asset.Id == job.ClipAssetId)
})
.ToList();
}
public async Task<ClipAssetDownloadViewModel?> GetClipAssetAsync(int clipJobId, CancellationToken cancellationToken = default)
{
var state = await _store.GetSnapshotAsync(cancellationToken);
var clipJob = state.ClipJobs.FirstOrDefault(item => item.Id == clipJobId);
if (clipJob?.ClipAssetId is not int assetId)
{
return null;
}
var asset = state.ClipAssets.FirstOrDefault(item => item.Id == assetId);
if (asset is null || !File.Exists(asset.StoredPath))
{
return null;
}
var provider = new FileExtensionContentTypeProvider();
var contentType = asset.ContentType;
if (string.IsNullOrWhiteSpace(contentType) && !provider.TryGetContentType(asset.FileName, out contentType))
{
contentType = "application/octet-stream";
}
return new ClipAssetDownloadViewModel
{
Asset = asset,
ContentType = contentType
};
}
private static IEnumerable<HighlightDetailsRow> BuildHighlightRows(AppState state, int matchId)
{
var playersById = state.Players.Where(player => player.MatchId == matchId).ToDictionary(player => player.Id);
var roundsById = state.Rounds.Where(round => round.MatchId == matchId).ToDictionary(round => round.Id);
var eventsById = state.Events.Where(@event => @event.MatchId == matchId).ToDictionary(@event => @event.Id);
var clipJobsByHighlightId = state.ClipJobs.ToDictionary(job => job.HighlightId);
var clipAssetsById = state.ClipAssets.ToDictionary(asset => asset.Id);
foreach (var highlight in state.Highlights.Where(item => item.MatchId == matchId))
{
eventsById.TryGetValue(highlight.SourceEventId, out var sourceEvent);
if (sourceEvent is null)
{
continue;
}
PlayerRecord? actor = null;
PlayerRecord? target = null;
RoundRecord? round = null;
if (sourceEvent.ActorPlayerId is int actorId)
{
playersById.TryGetValue(actorId, out actor);
}
if (sourceEvent.TargetPlayerId is int targetId)
{
playersById.TryGetValue(targetId, out target);
}
if (sourceEvent.RoundId is int roundId)
{
roundsById.TryGetValue(roundId, out round);
}
yield return new HighlightDetailsRow
{
Highlight = highlight,
Event = sourceEvent,
Actor = actor,
Target = target,
Round = round,
ClipJob = clipJobsByHighlightId.GetValueOrDefault(highlight.Id),
ClipAsset = clipJobsByHighlightId.TryGetValue(highlight.Id, out var clipJob) && clipJob.ClipAssetId is int assetId
? clipAssetsById.GetValueOrDefault(assetId)
: null
};
}
}
}
public sealed class DashboardViewModel
{
public int DemoCount { get; set; }
public int MatchCount { get; set; }
public int HighlightCount { get; set; }
public int ClipJobCount { get; set; }
public List<DemoRecord> Demos { get; set; } = [];
public List<ClipJobRecord> ClipJobs { get; set; } = [];
}
public sealed class DemoDetailsViewModel
{
public DemoRecord Demo { get; set; } = new();
public MatchRecord? Match { get; set; }
public List<HighlightDetailsRow> Highlights { get; set; } = [];
}
public sealed class MatchDetailsViewModel
{
public MatchRecord Match { get; set; } = new();
public List<PlayerRecord> Players { get; set; } = [];
public List<RoundRecord> Rounds { get; set; } = [];
public List<HighlightDetailsRow> Highlights { get; set; } = [];
}
public sealed class HighlightDetailsRow
{
public HighlightRecord Highlight { get; set; } = new();
public EventRecord Event { get; set; } = new();
public PlayerRecord? Actor { get; set; }
public PlayerRecord? Target { get; set; }
public RoundRecord? Round { get; set; }
public ClipJobRecord? ClipJob { get; set; }
public ClipAssetRecord? ClipAsset { get; set; }
}
public sealed class ClipJobWithHighlightViewModel
{
public ClipJobRecord ClipJob { get; set; } = new();
public HighlightRecord? Highlight { get; set; }
public ClipAssetRecord? ClipAsset { get; set; }
}
public sealed class ClipAssetDownloadViewModel
{
public ClipAssetRecord Asset { get; set; } = new();
public string ContentType { get; set; } = "application/octet-stream";
}