PlotDirector/PlotLine/Services/ManuscriptScanPreviewStore.cs

196 lines
8.1 KiB
C#

using System.Collections.Concurrent;
using PlotLine.Models;
namespace PlotLine.Services;
public interface IManuscriptScanPreviewStore
{
Task<ManuscriptScanState> StartAsync(int userId, int onboardingId, int projectId, int bookId);
Task<ManuscriptScanState> ProgressAsync(int userId, ManuscriptScanProgress progress);
Task<ManuscriptScanState> CompleteAsync(int userId, ManuscriptScanPreview preview);
Task<ManuscriptScanState> FailAsync(int userId, int onboardingId, string message);
Task<IReadOnlyList<ManuscriptScanState>> FailRunningForUserAsync(int userId, string message);
Task<ManuscriptScanState> GetStateAsync(int userId, int onboardingId);
Task<ManuscriptScanPreview?> GetLatestPreviewAsync(int userId, int onboardingId);
Task<ManuscriptScanPreview?> GetPreviewAsync(int userId, Guid previewId);
}
public sealed class ManuscriptScanPreviewStore : IManuscriptScanPreviewStore
{
private readonly ConcurrentDictionary<(int UserId, int OnboardingId), ScanSession> sessions = new();
private readonly ConcurrentDictionary<Guid, ManuscriptScanPreview> previews = new();
public Task<ManuscriptScanState> StartAsync(int userId, int onboardingId, int projectId, int bookId)
{
var session = new ScanSession
{
UserID = userId,
OnboardingID = onboardingId,
ProjectID = projectId,
BookID = bookId,
Status = ManuscriptScanStatuses.Running,
Message = "Preparing document scan",
PercentComplete = 0,
UpdatedUtc = DateTime.UtcNow
};
sessions[(userId, onboardingId)] = session;
return Task.FromResult(ToState(session));
}
public Task<ManuscriptScanState> ProgressAsync(int userId, ManuscriptScanProgress progress)
{
var key = (userId, progress.OnboardingID);
var session = sessions.GetOrAdd(key, _ => new ScanSession
{
UserID = userId,
OnboardingID = progress.OnboardingID,
Status = ManuscriptScanStatuses.Running
});
session.Status = ManuscriptScanStatuses.Running;
session.Message = Clean(progress.Message, "Scanning manuscript");
session.PercentComplete = progress.PercentComplete;
session.ChapterCount = progress.ChapterCount;
session.SceneCount = progress.SceneCount;
session.CharacterCandidateCount = progress.CharacterCandidateCount;
session.TotalWordCount = progress.TotalWordCount;
session.UpdatedUtc = DateTime.UtcNow;
return Task.FromResult(ToState(session));
}
public Task<ManuscriptScanState> CompleteAsync(int userId, ManuscriptScanPreview preview)
{
var stored = preview.WithUser(userId);
previews[stored.PreviewID] = stored;
var session = sessions.GetOrAdd((userId, stored.OnboardingID), _ => new ScanSession
{
UserID = userId,
OnboardingID = stored.OnboardingID,
ProjectID = stored.ProjectID,
BookID = stored.BookID
});
session.Status = ManuscriptScanStatuses.Complete;
session.Message = "Scan complete";
session.PercentComplete = 100;
session.ChapterCount = stored.ChapterCount;
session.SceneCount = stored.SceneCount;
session.CharacterCandidateCount = stored.CharacterCandidateCount;
session.TotalWordCount = stored.TotalWordCount;
session.PreviewID = stored.PreviewID;
session.UpdatedUtc = stored.CreatedUtc;
return Task.FromResult(ToState(session));
}
public Task<ManuscriptScanState> FailAsync(int userId, int onboardingId, string message)
{
var session = sessions.GetOrAdd((userId, onboardingId), _ => new ScanSession
{
UserID = userId,
OnboardingID = onboardingId
});
session.Status = ManuscriptScanStatuses.Failed;
session.Message = Clean(message, "The scan could not finish. Try reconnecting the Word Companion.");
session.UpdatedUtc = DateTime.UtcNow;
return Task.FromResult(ToState(session));
}
public Task<IReadOnlyList<ManuscriptScanState>> FailRunningForUserAsync(int userId, string message)
{
var failed = new List<ManuscriptScanState>();
foreach (var pair in sessions)
{
if (pair.Key.UserId != userId || !string.Equals(pair.Value.Status, ManuscriptScanStatuses.Running, StringComparison.Ordinal))
{
continue;
}
pair.Value.Status = ManuscriptScanStatuses.Failed;
pair.Value.Message = Clean(message, "The Word Companion disconnected before the scan finished. Reopen Word and try again.");
pair.Value.UpdatedUtc = DateTime.UtcNow;
failed.Add(ToState(pair.Value));
}
return Task.FromResult<IReadOnlyList<ManuscriptScanState>>(failed);
}
public Task<ManuscriptScanState> GetStateAsync(int userId, int onboardingId)
=> Task.FromResult(sessions.TryGetValue((userId, onboardingId), out var session)
? ToState(session)
: new ManuscriptScanState());
public Task<ManuscriptScanPreview?> GetLatestPreviewAsync(int userId, int onboardingId)
{
if (!sessions.TryGetValue((userId, onboardingId), out var session) || !session.PreviewID.HasValue)
{
return Task.FromResult<ManuscriptScanPreview?>(null);
}
return GetPreviewAsync(userId, session.PreviewID.Value);
}
public Task<ManuscriptScanPreview?> GetPreviewAsync(int userId, Guid previewId)
=> Task.FromResult(previews.TryGetValue(previewId, out var preview) && preview.UserID == userId ? preview : null);
private static ManuscriptScanState ToState(ScanSession session)
=> new()
{
Status = session.Status,
Message = session.Message,
PercentComplete = session.PercentComplete,
ChapterCount = session.ChapterCount,
SceneCount = session.SceneCount,
CharacterCandidateCount = session.CharacterCandidateCount,
TotalWordCount = session.TotalWordCount,
PreviewID = session.PreviewID,
UpdatedUtc = session.UpdatedUtc
};
private static string Clean(string? value, string fallback)
{
var cleaned = (value ?? string.Empty).Trim();
return string.IsNullOrWhiteSpace(cleaned) ? fallback : cleaned;
}
private sealed class ScanSession
{
public int UserID { get; init; }
public int OnboardingID { get; init; }
public int ProjectID { get; init; }
public int BookID { get; init; }
public string Status { get; set; } = ManuscriptScanStatuses.NotStarted;
public string Message { get; set; } = string.Empty;
public int? PercentComplete { get; set; }
public int ChapterCount { get; set; }
public int SceneCount { get; set; }
public int CharacterCandidateCount { get; set; }
public int TotalWordCount { get; set; }
public Guid? PreviewID { get; set; }
public DateTime? UpdatedUtc { get; set; }
}
}
file static class ManuscriptScanPreviewExtensions
{
public static ManuscriptScanPreview WithUser(this ManuscriptScanPreview preview, int userId)
=> new()
{
PreviewID = preview.PreviewID == Guid.Empty ? Guid.NewGuid() : preview.PreviewID,
UserID = userId,
OnboardingID = preview.OnboardingID,
ProjectID = preview.ProjectID,
BookID = preview.BookID,
Source = preview.Source,
DocumentTitle = preview.DocumentTitle,
CompanionDocumentIdentifier = preview.CompanionDocumentIdentifier,
TotalWordCount = preview.TotalWordCount,
ChapterCount = preview.ChapterCount,
SceneCount = preview.SceneCount,
CharacterCandidateCount = preview.CharacterCandidateCount,
CreatedUtc = preview.CreatedUtc == default ? DateTime.UtcNow : preview.CreatedUtc,
Chapters = preview.Chapters,
Scenes = preview.Scenes,
CharacterCandidates = preview.CharacterCandidates
};
}