216 lines
7.8 KiB
C#
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";
|
|
}
|