Files
vtuber-awards/Backend/Data/SeedOperationalDataBootstrapper.cs
T

284 lines
12 KiB
C#

using System.Text.Json;
using Backend.Domain;
using Microsoft.EntityFrameworkCore;
namespace Backend.Data;
public static partial class SeedDataBootstrapper
{
private static async Task EnsureSeedOperationalDataAsync(AwardsDbContext db, Season season)
{
var categories = await db.Categories
.Where(item => item.SeasonId == season.Id)
.ToDictionaryAsync(item => item.Slug, StringComparer.OrdinalIgnoreCase);
var candidates = await db.Candidates
.Where(item => item.SeasonId == season.Id)
.ToArrayAsync();
var normalizedLegacyState = await NormalizeLegacyDemoLabelsAsync(db);
if (!await db.ClipSubmissions.AnyAsync(item => item.SeasonId == season.Id))
{
db.ClipSubmissions.AddRange(
new ClipSubmission
{
SeasonId = season.Id,
CategoryId = ResolveCategoryId(categories, "vtuber-des-jahres"),
CandidateId = ResolveCandidateId(categories, candidates, "vtuber-des-jahres", "Hoshimi Miyu"),
SubmittedByTwitchId = "local_user_3",
ClipUrl = "https://clips.twitch.tv/StarlitDebutMoment",
Title = "Starlight Debut Moment",
Creator = "Hoshimi Miyu",
Platform = "Twitch",
Status = "approved",
ReviewNote = "Geprüfter Clip fuer Voting-Vorschau.",
ReviewedByTwitchId = "jayuhime_admin",
CreatedFromIp = "127.0.0.1",
CreatedAt = new DateTimeOffset(2026, 6, 18, 8, 15, 0, TimeSpan.Zero),
ReviewedAt = new DateTimeOffset(2026, 6, 18, 12, 5, 0, TimeSpan.Zero),
},
new ClipSubmission
{
SeasonId = season.Id,
CategoryId = ResolveCategoryId(categories, "vtuber-des-jahres"),
CandidateId = ResolveCandidateId(categories, candidates, "vtuber-des-jahres", "Kurainu"),
SubmittedByTwitchId = "local_user_4",
ClipUrl = "https://clips.twitch.tv/KurainuFinaleHype",
Title = "Finale-Hype mit Chat-Chaos",
Creator = "Kurainu",
Platform = "Twitch",
Status = "approved",
ReviewNote = "Geprüfter Clip fuer Voting-Vorschau.",
ReviewedByTwitchId = "jayuhime_admin",
CreatedFromIp = "127.0.0.1",
CreatedAt = new DateTimeOffset(2026, 6, 18, 8, 35, 0, TimeSpan.Zero),
ReviewedAt = new DateTimeOffset(2026, 6, 18, 12, 10, 0, TimeSpan.Zero),
},
new ClipSubmission
{
SeasonId = season.Id,
CategoryId = ResolveCategoryId(categories, "best-gaming"),
CandidateId = ResolveCandidateId(categories, candidates, "best-gaming", "Kurainu"),
SubmittedByTwitchId = "local_user",
ClipUrl = "https://clips.twitch.tv/EpicGamingMoment",
Title = "Epischer Clutch im Finale",
Creator = "Kurainu",
Platform = "Twitch",
Status = "pending",
CreatedFromIp = "127.0.0.1",
CreatedAt = new DateTimeOffset(2026, 6, 17, 9, 10, 0, TimeSpan.Zero),
},
new ClipSubmission
{
SeasonId = season.Id,
CategoryId = ResolveCategoryId(categories, "gesang-musik"),
CandidateId = ResolveCandidateId(categories, candidates, "gesang-musik", "Melo Diva"),
SubmittedByTwitchId = "local_user_2",
ClipUrl = "https://www.youtube.com/watch?v=liveCoverMoment",
Title = "Live-Cover mit Gänsehaut",
Creator = "Melo Diva",
Platform = "YouTube",
Status = "approved",
ReviewNote = "Geprüfter Clip fuer Review-Workflow.",
ReviewedByTwitchId = "jayuhime_admin",
CreatedFromIp = "127.0.0.1",
CreatedAt = new DateTimeOffset(2026, 6, 18, 10, 30, 0, TimeSpan.Zero),
ReviewedAt = new DateTimeOffset(2026, 6, 18, 12, 0, 0, TimeSpan.Zero),
});
}
if (!normalizedLegacyState.HasRiskSeed && !await db.RiskFlags.AnyAsync(item => item.Source == "seed"))
{
db.RiskFlags.Add(new RiskFlag
{
SeasonId = season.Id,
TwitchUserId = "sample_user",
Source = "seed",
Type = "rapid_vote_updates",
Severity = "medium",
Status = "open",
Summary = "Mehrere Voting-Aenderungen in kurzer Zeit erkannt.",
CreatedFromIp = "127.0.0.1",
UserAgent = "seed-bootstrap",
MetadataJson = JsonSerializer.Serialize(new { recentVoteSubmissions = 3 }),
CreatedAt = new DateTimeOffset(2026, 6, 17, 8, 40, 0, TimeSpan.Zero),
});
}
if (!normalizedLegacyState.HasAuditSeed && !await db.AdminAuditEntries.AnyAsync(item => item.ActionType == "seed.initialize"))
{
db.AdminAuditEntries.Add(new AdminAuditEntry
{
AdminTwitchUserId = "system",
ActionType = "seed.initialize",
EntityType = "database",
EntityId = season.Year.ToString(),
Summary = "Startinhalte wurden in der Datenbank bereitgestellt.",
MetadataJson = JsonSerializer.Serialize(new { categories = SeedCatalog.CategorySeeds.Length }),
CreatedFromIp = "seed",
UserAgent = "seed-bootstrap",
CreatedAt = new DateTimeOffset(2026, 6, 17, 8, 32, 0, TimeSpan.Zero),
});
}
}
private static int? ResolveCategoryId(IReadOnlyDictionary<string, Category> categories, string slug) =>
categories.TryGetValue(slug, out var category) ? category.Id : null;
private static int? ResolveCandidateId(
IReadOnlyDictionary<string, Category> categories,
IEnumerable<Candidate> candidates,
string categorySlug,
string displayName)
{
var categoryId = ResolveCategoryId(categories, categorySlug);
return categoryId is int resolvedCategoryId
? candidates.FirstOrDefault(item =>
item.CategoryId == resolvedCategoryId
&& string.Equals(item.DisplayName, displayName, StringComparison.OrdinalIgnoreCase))?.Id
: null;
}
private static async Task<LegacySeedState> NormalizeLegacyDemoLabelsAsync(AwardsDbContext db)
{
var legacySessions = await db.UserSessions
.Where(item => item.TwitchUserId == "admin_demo" || item.TwitchUserId == "jayuhime_demo" || item.TwitchUserId == "demo_user")
.ToArrayAsync();
foreach (var session in legacySessions)
{
session.TwitchUserId = session.TwitchUserId switch
{
"admin_demo" => "jayuhime_admin",
"jayuhime_demo" => "jayuhime_viewer",
"demo_user" => "local_user",
_ => session.TwitchUserId,
};
session.DisplayName = session.DisplayName switch
{
"Admin Demo" => "Jayuhime Admin",
"Demo User" => "Local User",
_ => session.DisplayName,
};
}
var legacyClipSubmissions = await db.ClipSubmissions
.Where(item =>
item.SubmittedByTwitchId == "demo_user" ||
item.SubmittedByTwitchId == "demo_user_2" ||
item.ClipUrl.Contains("Demo") ||
item.ClipUrl.Contains("demo") ||
(item.ReviewNote != null && item.ReviewNote.Contains("Demo-Clip")))
.ToArrayAsync();
foreach (var clip in legacyClipSubmissions)
{
clip.SubmittedByTwitchId = clip.SubmittedByTwitchId switch
{
"demo_user" => "local_user",
"demo_user_2" => "local_user_2",
_ => clip.SubmittedByTwitchId,
};
clip.ClipUrl = clip.ClipUrl
.Replace("DemoGamingMoment", "EpicGamingMoment")
.Replace("demoSong", "liveCoverMoment");
clip.ReviewNote = clip.ReviewNote?.Replace("Demo-Clip", "Geprüfter Clip");
}
await LinkExistingClipsToCandidatesAsync(db);
var legacyRiskFlags = await db.RiskFlags
.Where(item =>
item.Source == "demo" ||
item.Summary.StartsWith("Demo:") ||
item.TwitchUserId == "jayuhime_demo" ||
item.TwitchUserId == "demo_user")
.ToArrayAsync();
foreach (var flag in legacyRiskFlags)
{
flag.Source = "seed";
flag.TwitchUserId = flag.TwitchUserId switch
{
"demo_user" => "local_user",
"jayuhime_demo" => "jayuhime_viewer",
_ => flag.TwitchUserId,
};
flag.Summary = flag.Summary.Replace("Demo: ", string.Empty);
flag.UserAgent = flag.UserAgent == "demo-seed" ? "seed-bootstrap" : flag.UserAgent;
}
var legacyAuditEntries = await db.AdminAuditEntries
.Where(item =>
item.ActionType == "demo.seed" ||
item.Summary.Contains("Demo-Inhalte") ||
item.AdminTwitchUserId == "admin_demo" ||
item.AdminTwitchUserId == "jayuhime_demo")
.ToArrayAsync();
foreach (var entry in legacyAuditEntries)
{
entry.AdminTwitchUserId = entry.AdminTwitchUserId switch
{
"admin_demo" => "jayuhime_admin",
"jayuhime_demo" => "jayuhime_viewer",
_ => entry.AdminTwitchUserId,
};
if (entry.ActionType == "demo.seed")
{
entry.ActionType = "seed.initialize";
}
if (entry.Summary.Contains("Demo-Inhalte"))
{
entry.Summary = "Startinhalte wurden in der Datenbank bereitgestellt.";
}
}
return new LegacySeedState(
legacyRiskFlags.Length > 0 || await db.RiskFlags.AnyAsync(item => item.Source == "seed"),
legacyAuditEntries.Length > 0 || await db.AdminAuditEntries.AnyAsync(item => item.ActionType == "seed.initialize"));
}
private static async Task LinkExistingClipsToCandidatesAsync(AwardsDbContext db)
{
var clips = await db.ClipSubmissions
.Where(item => item.CandidateId == null && item.CategoryId != null && item.Creator != string.Empty)
.ToArrayAsync();
if (clips.Length == 0)
{
return;
}
var seasonIds = clips.Select(item => item.SeasonId).Distinct().ToArray();
var categoryIds = clips.Select(item => item.CategoryId!.Value).Distinct().ToArray();
var candidates = await db.Candidates
.Where(item => seasonIds.Contains(item.SeasonId) && categoryIds.Contains(item.CategoryId))
.ToArrayAsync();
foreach (var clip in clips)
{
var creatorKey = NormalizeSeedCandidateKey(clip.Creator);
var candidate = candidates.FirstOrDefault(item =>
item.SeasonId == clip.SeasonId
&& item.CategoryId == clip.CategoryId
&& (NormalizeSeedCandidateKey(item.DisplayName) == creatorKey
|| NormalizeSeedCandidateKey(item.ChannelSlug) == creatorKey));
if (candidate is not null)
{
clip.CandidateId = candidate.Id;
}
}
}
private static string NormalizeSeedCandidateKey(string value) =>
new(
value
.Trim()
.TrimStart('@')
.ToLowerInvariant()
.Where(char.IsLetterOrDigit)
.ToArray());
private sealed record LegacySeedState(bool HasRiskSeed, bool HasAuditSeed);
}