first commit
This commit is contained in:
@@ -0,0 +1,215 @@
|
||||
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";
|
||||
}
|
||||
Reference in New Issue
Block a user