Update release notes and deploy workspace
This commit is contained in:
@@ -29,8 +29,9 @@ public sealed class AwardsDbContext(DbContextOptions<AwardsDbContext> options) :
|
||||
{
|
||||
entity.HasIndex(item => item.Year).IsUnique();
|
||||
entity.Property(item => item.Name).HasMaxLength(160);
|
||||
entity.Property(item => item.ShowStreamUrl).HasMaxLength(400);
|
||||
entity.Property(item => item.IsDemo).HasDefaultValue(false);
|
||||
entity.Property(item => item.CurrentPhase).HasMaxLength(60);
|
||||
entity.Property(item => item.WinnersPublishedByTwitchId).HasMaxLength(120);
|
||||
entity.Property(item => item.SubcategoryTemplatesJson).HasDefaultValue("[]");
|
||||
entity.Property(item => item.WorkflowRulesJson).HasDefaultValue("[]");
|
||||
});
|
||||
@@ -47,6 +48,15 @@ public sealed class AwardsDbContext(DbContextOptions<AwardsDbContext> options) :
|
||||
entity.Property(item => item.SponsorsUrl).HasMaxLength(400);
|
||||
entity.Property(item => item.ShowactsUrl).HasMaxLength(400);
|
||||
entity.Property(item => item.ShowactsContent).HasDefaultValue(string.Empty);
|
||||
entity.Property(item => item.StreamBannerEyebrow).HasMaxLength(120);
|
||||
entity.Property(item => item.StreamBannerTitle).HasMaxLength(160);
|
||||
entity.Property(item => item.StreamBannerLiveButtonLabel).HasMaxLength(120);
|
||||
entity.Property(item => item.StreamBannerLiveButtonUrl).HasMaxLength(400);
|
||||
entity.Property(item => item.StreamBannerLockedButtonLabel).HasMaxLength(120);
|
||||
entity.Property(item => item.StreamBannerCompletedEyebrow).HasMaxLength(120);
|
||||
entity.Property(item => item.StreamBannerCompletedTitle).HasMaxLength(160);
|
||||
entity.Property(item => item.StreamBannerCompletedButtonLabel).HasMaxLength(120);
|
||||
entity.Property(item => item.StreamBannerCompletedButtonUrl).HasMaxLength(400);
|
||||
entity.Property(item => item.DemoLoginEmail).HasMaxLength(180);
|
||||
entity.Property(item => item.DemoLoginPasswordHash).HasMaxLength(120);
|
||||
entity.Property(item => item.DemoLoginPasswordSalt).HasMaxLength(80);
|
||||
@@ -225,6 +235,10 @@ public sealed class AwardsDbContext(DbContextOptions<AwardsDbContext> options) :
|
||||
entity.Property(item => item.CreatedFromIp).HasMaxLength(80);
|
||||
entity.HasIndex(item => new { item.SeasonId, item.Status });
|
||||
entity.HasIndex(item => item.CandidateId);
|
||||
entity.HasOne<Season>()
|
||||
.WithMany()
|
||||
.HasForeignKey(item => item.SeasonId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
entity.HasOne(item => item.Candidate)
|
||||
.WithMany()
|
||||
.HasForeignKey(item => item.CandidateId)
|
||||
@@ -258,7 +272,5 @@ public sealed class AwardsDbContext(DbContextOptions<AwardsDbContext> options) :
|
||||
entity.Property(item => item.Tier).HasMaxLength(80);
|
||||
entity.HasIndex(item => new { item.SeasonId, item.IsVisible, item.SortOrder });
|
||||
});
|
||||
|
||||
SeedData.Apply(modelBuilder);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,471 +0,0 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Backend.Data;
|
||||
|
||||
public static class OperationalTablesBootstrapper
|
||||
{
|
||||
public static Task EnsureAsync(AwardsDbContext db) =>
|
||||
db.Database.ExecuteSqlRawAsync(
|
||||
"""
|
||||
ALTER TABLE "UserSessions"
|
||||
ADD COLUMN IF NOT EXISTS "CreatedFromIp" character varying(80) NOT NULL DEFAULT '';
|
||||
|
||||
ALTER TABLE "UserSessions"
|
||||
ADD COLUMN IF NOT EXISTS "UserAgent" character varying(400) NOT NULL DEFAULT '';
|
||||
|
||||
ALTER TABLE "SiteSettings"
|
||||
ADD COLUMN IF NOT EXISTS "RiskRulesJson" text NOT NULL DEFAULT '[]';
|
||||
|
||||
ALTER TABLE "SiteSettings"
|
||||
ADD COLUMN IF NOT EXISTS "WorkflowRulesJson" text NOT NULL DEFAULT '[]';
|
||||
|
||||
ALTER TABLE "SiteSettings"
|
||||
ADD COLUMN IF NOT EXISTS "TrackingRulesJson" text NOT NULL DEFAULT '[]';
|
||||
|
||||
ALTER TABLE "SiteSettings"
|
||||
ADD COLUMN IF NOT EXISTS "ViewerStatsProviderBaseUrl" character varying(400) NOT NULL DEFAULT '';
|
||||
|
||||
ALTER TABLE "SiteSettings"
|
||||
ADD COLUMN IF NOT EXISTS "TrackingReviewNotes" text NOT NULL DEFAULT '';
|
||||
|
||||
ALTER TABLE "SiteSettings"
|
||||
ADD COLUMN IF NOT EXISTS "NominationLinkBlacklistJson" text NOT NULL DEFAULT '[]';
|
||||
|
||||
ALTER TABLE "SiteSettings"
|
||||
ADD COLUMN IF NOT EXISTS "ClipSubmissionsEnabled" boolean NOT NULL DEFAULT false;
|
||||
|
||||
ALTER TABLE "SiteSettings"
|
||||
ADD COLUMN IF NOT EXISTS "ClipReviewEnabled" boolean NOT NULL DEFAULT true;
|
||||
|
||||
ALTER TABLE "SiteSettings"
|
||||
ADD COLUMN IF NOT EXISTS "ClipSubmissionDisabledMessage" character varying(240) NOT NULL DEFAULT 'Clip-Einreichungen sind aktuell geschlossen.';
|
||||
|
||||
ALTER TABLE "SiteSettings"
|
||||
ADD COLUMN IF NOT EXISTS "ShowactApplicationsEnabled" boolean NOT NULL DEFAULT false;
|
||||
|
||||
ALTER TABLE "SiteSettings"
|
||||
ADD COLUMN IF NOT EXISTS "ShowactApplicationStartsAt" date NULL;
|
||||
|
||||
ALTER TABLE "SiteSettings"
|
||||
ADD COLUMN IF NOT EXISTS "ShowactApplicationEndsAt" date NULL;
|
||||
|
||||
ALTER TABLE "SiteSettings"
|
||||
ADD COLUMN IF NOT EXISTS "ShowactApplicationDisabledMessage" character varying(240) NOT NULL DEFAULT 'Showact-Bewerbungen sind aktuell geschlossen.';
|
||||
|
||||
ALTER TABLE "SiteSettings"
|
||||
ADD COLUMN IF NOT EXISTS "SponsorsVisible" boolean NOT NULL DEFAULT true;
|
||||
|
||||
ALTER TABLE "Nominations"
|
||||
ADD COLUMN IF NOT EXISTS "HoursStreamed" integer NULL;
|
||||
|
||||
ALTER TABLE "Nominations"
|
||||
ADD COLUMN IF NOT EXISTS "HoursWatched" integer NULL;
|
||||
|
||||
ALTER TABLE "Nominations"
|
||||
ADD COLUMN IF NOT EXISTS "PeakViewers" integer NULL;
|
||||
|
||||
ALTER TABLE "Nominations"
|
||||
ADD COLUMN IF NOT EXISTS "FollowersGained" integer NULL;
|
||||
|
||||
ALTER TABLE "SiteSettings"
|
||||
ADD COLUMN IF NOT EXISTS "TwitchAuthManagedByDatabase" boolean NOT NULL DEFAULT false;
|
||||
|
||||
ALTER TABLE "SiteSettings"
|
||||
ADD COLUMN IF NOT EXISTS "TwitchClientId" character varying(120) NOT NULL DEFAULT '';
|
||||
|
||||
ALTER TABLE "SiteSettings"
|
||||
ADD COLUMN IF NOT EXISTS "TwitchClientSecret" character varying(180) NOT NULL DEFAULT '';
|
||||
|
||||
ALTER TABLE "SiteSettings"
|
||||
ADD COLUMN IF NOT EXISTS "TwitchRedirectUri" character varying(400) NOT NULL DEFAULT '';
|
||||
|
||||
ALTER TABLE "SiteSettings"
|
||||
ADD COLUMN IF NOT EXISTS "TwitchScope" character varying(300) NOT NULL DEFAULT '';
|
||||
|
||||
ALTER TABLE "SiteSettings"
|
||||
ADD COLUMN IF NOT EXISTS "SessionIdleTimeoutHours" integer NOT NULL DEFAULT 3;
|
||||
|
||||
ALTER TABLE "SiteSettings"
|
||||
ADD COLUMN IF NOT EXISTS "ImprintContent" text NOT NULL DEFAULT '';
|
||||
|
||||
ALTER TABLE "SiteSettings"
|
||||
ADD COLUMN IF NOT EXISTS "ContactContent" text NOT NULL DEFAULT '';
|
||||
|
||||
ALTER TABLE "SiteSettings"
|
||||
ADD COLUMN IF NOT EXISTS "SponsorsContent" text NOT NULL DEFAULT '';
|
||||
|
||||
ALTER TABLE "SiteSettings"
|
||||
ADD COLUMN IF NOT EXISTS "ShowactsUrl" character varying(400) NOT NULL DEFAULT '';
|
||||
|
||||
ALTER TABLE "SiteSettings"
|
||||
ADD COLUMN IF NOT EXISTS "ShowactsContent" text NOT NULL DEFAULT '';
|
||||
|
||||
ALTER TABLE "Seasons"
|
||||
ADD COLUMN IF NOT EXISTS "SubcategoryTemplatesJson" text NOT NULL DEFAULT '[]';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "RiskFlags" (
|
||||
"Id" integer GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||||
"SeasonId" integer NULL,
|
||||
"TwitchUserId" character varying(120) NULL,
|
||||
"Source" character varying(80) NOT NULL,
|
||||
"Type" character varying(80) NOT NULL,
|
||||
"Severity" character varying(20) NOT NULL,
|
||||
"Status" character varying(20) NOT NULL,
|
||||
"Summary" character varying(240) NOT NULL,
|
||||
"CreatedFromIp" character varying(80) NOT NULL,
|
||||
"UserAgent" character varying(400) NOT NULL,
|
||||
"MetadataJson" text NOT NULL,
|
||||
"ReviewNote" character varying(500) NULL,
|
||||
"ReviewedByTwitchId" character varying(120) NULL,
|
||||
"CreatedAt" timestamp with time zone NOT NULL,
|
||||
"ReviewedAt" timestamp with time zone NULL
|
||||
);
|
||||
|
||||
ALTER TABLE "RiskFlags"
|
||||
ADD COLUMN IF NOT EXISTS "ReviewNote" character varying(500) NULL;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS "IX_RiskFlags_Status_CreatedAt"
|
||||
ON "RiskFlags" ("Status", "CreatedAt" DESC);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS "IX_RiskFlags_SeasonId"
|
||||
ON "RiskFlags" ("SeasonId");
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "AdminAuditEntries" (
|
||||
"Id" integer GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||||
"AdminTwitchUserId" character varying(120) NOT NULL,
|
||||
"ActionType" character varying(80) NOT NULL,
|
||||
"EntityType" character varying(80) NOT NULL,
|
||||
"EntityId" character varying(120) NOT NULL,
|
||||
"Summary" character varying(240) NOT NULL,
|
||||
"MetadataJson" text NOT NULL,
|
||||
"CreatedFromIp" character varying(80) NOT NULL DEFAULT '',
|
||||
"UserAgent" character varying(400) NOT NULL DEFAULT '',
|
||||
"CreatedAt" timestamp with time zone NOT NULL
|
||||
);
|
||||
|
||||
ALTER TABLE "AdminAuditEntries"
|
||||
ADD COLUMN IF NOT EXISTS "CreatedFromIp" character varying(80) NOT NULL DEFAULT '';
|
||||
|
||||
ALTER TABLE "AdminAuditEntries"
|
||||
ADD COLUMN IF NOT EXISTS "UserAgent" character varying(400) NOT NULL DEFAULT '';
|
||||
|
||||
CREATE INDEX IF NOT EXISTS "IX_AdminAuditEntries_CreatedAt"
|
||||
ON "AdminAuditEntries" ("CreatedAt" DESC);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "ClipSubmissions" (
|
||||
"Id" integer GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||||
"SeasonId" integer NOT NULL,
|
||||
"CategoryId" integer NULL,
|
||||
"SubmittedByTwitchId" character varying(120) NOT NULL,
|
||||
"ClipUrl" character varying(500) NOT NULL,
|
||||
"Title" character varying(200) NOT NULL,
|
||||
"Creator" character varying(120) NOT NULL,
|
||||
"Platform" character varying(40) NOT NULL,
|
||||
"Status" character varying(20) NOT NULL,
|
||||
"CreatedFromIp" character varying(80) NOT NULL,
|
||||
"CreatedAt" timestamp with time zone NOT NULL
|
||||
);
|
||||
|
||||
ALTER TABLE "ClipSubmissions"
|
||||
ADD COLUMN IF NOT EXISTS "CandidateId" integer NULL;
|
||||
|
||||
ALTER TABLE "ClipSubmissions"
|
||||
ADD COLUMN IF NOT EXISTS "ReviewNote" character varying(500) NULL;
|
||||
|
||||
ALTER TABLE "ClipSubmissions"
|
||||
ADD COLUMN IF NOT EXISTS "ReviewedByTwitchId" character varying(120) NULL;
|
||||
|
||||
ALTER TABLE "ClipSubmissions"
|
||||
ADD COLUMN IF NOT EXISTS "ReviewedAt" timestamp with time zone NULL;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS "IX_ClipSubmissions_SeasonId_Status"
|
||||
ON "ClipSubmissions" ("SeasonId", "Status");
|
||||
|
||||
CREATE INDEX IF NOT EXISTS "IX_ClipSubmissions_CandidateId"
|
||||
ON "ClipSubmissions" ("CandidateId");
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "ShowactApplications" (
|
||||
"Id" integer GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||||
"SeasonId" integer NOT NULL,
|
||||
"ArtistName" character varying(120) NOT NULL,
|
||||
"ContactEmail" character varying(180) NOT NULL,
|
||||
"ContactDiscord" character varying(120) NOT NULL,
|
||||
"PlatformUrl" character varying(500) NOT NULL,
|
||||
"PerformanceType" character varying(80) NOT NULL,
|
||||
"Description" character varying(1000) NOT NULL,
|
||||
"TechnicalNotes" character varying(1000) NOT NULL,
|
||||
"ReferenceUrl" character varying(500) NOT NULL,
|
||||
"Status" character varying(20) NOT NULL,
|
||||
"ReviewNote" character varying(500) NULL,
|
||||
"ReviewedByTwitchId" character varying(120) NULL,
|
||||
"CreatedFromIp" character varying(80) NOT NULL,
|
||||
"UserAgent" character varying(400) NOT NULL,
|
||||
"CreatedAt" timestamp with time zone NOT NULL,
|
||||
"ReviewedAt" timestamp with time zone NULL
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS "IX_ShowactApplications_SeasonId_Status"
|
||||
ON "ShowactApplications" ("SeasonId", "Status");
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "Sponsors" (
|
||||
"Id" integer GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||||
"SeasonId" integer NOT NULL,
|
||||
"Name" character varying(120) NOT NULL,
|
||||
"WebsiteUrl" character varying(500) NOT NULL,
|
||||
"LogoUrl" character varying(500) NOT NULL,
|
||||
"Description" character varying(500) NOT NULL,
|
||||
"Tier" character varying(80) NOT NULL,
|
||||
"SortOrder" integer NOT NULL,
|
||||
"IsVisible" boolean NOT NULL,
|
||||
"CreatedAt" timestamp with time zone NOT NULL,
|
||||
"UpdatedAt" timestamp with time zone NULL
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS "IX_Sponsors_SeasonId_IsVisible_SortOrder"
|
||||
ON "Sponsors" ("SeasonId", "IsVisible", "SortOrder");
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM pg_constraint
|
||||
WHERE conname = 'FK_ShowactApplications_Seasons_SeasonId'
|
||||
) THEN
|
||||
ALTER TABLE "ShowactApplications"
|
||||
ADD CONSTRAINT "FK_ShowactApplications_Seasons_SeasonId"
|
||||
FOREIGN KEY ("SeasonId") REFERENCES "Seasons" ("Id")
|
||||
ON DELETE CASCADE;
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM pg_constraint
|
||||
WHERE conname = 'FK_Sponsors_Seasons_SeasonId'
|
||||
) THEN
|
||||
ALTER TABLE "Sponsors"
|
||||
ADD CONSTRAINT "FK_Sponsors_Seasons_SeasonId"
|
||||
FOREIGN KEY ("SeasonId") REFERENCES "Seasons" ("Id")
|
||||
ON DELETE CASCADE;
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM pg_constraint
|
||||
WHERE conname = 'FK_ClipSubmissions_Candidates_CandidateId'
|
||||
) THEN
|
||||
ALTER TABLE "ClipSubmissions"
|
||||
ADD CONSTRAINT "FK_ClipSubmissions_Candidates_CandidateId"
|
||||
FOREIGN KEY ("CandidateId") REFERENCES "Candidates" ("Id")
|
||||
ON DELETE SET NULL;
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
ALTER TABLE "Nominations"
|
||||
ADD COLUMN IF NOT EXISTS "StreamUrl" character varying(300) NULL;
|
||||
|
||||
ALTER TABLE "Candidates"
|
||||
ADD COLUMN IF NOT EXISTS "AcceptanceStatus" character varying(30) NOT NULL DEFAULT 'open';
|
||||
|
||||
ALTER TABLE "Candidates"
|
||||
ADD COLUMN IF NOT EXISTS "AcceptanceNote" character varying(500) NULL;
|
||||
|
||||
ALTER TABLE "Candidates"
|
||||
ADD COLUMN IF NOT EXISTS "ClipCompilationUrl" character varying(500) NULL;
|
||||
|
||||
ALTER TABLE "Candidates"
|
||||
ADD COLUMN IF NOT EXISTS "ClipCompilationTitle" character varying(200) NULL;
|
||||
|
||||
ALTER TABLE "Candidates"
|
||||
ADD COLUMN IF NOT EXISTS "ClipCompilationPlatform" character varying(40) NULL;
|
||||
|
||||
ALTER TABLE "Candidates"
|
||||
ADD COLUMN IF NOT EXISTS "ClipEmbedStatus" character varying(30) NOT NULL DEFAULT 'unchecked';
|
||||
|
||||
ALTER TABLE "Categories"
|
||||
ADD COLUMN IF NOT EXISTS "ViewerRangeMin" integer NULL;
|
||||
|
||||
ALTER TABLE "Categories"
|
||||
ADD COLUMN IF NOT EXISTS "ViewerRangeMax" integer NULL;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "StreamerIdentities" (
|
||||
"Id" integer GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||||
"Platform" character varying(40) NOT NULL,
|
||||
"Login" character varying(120) NOT NULL,
|
||||
"NormalizedKey" character varying(180) NOT NULL,
|
||||
"DisplayName" character varying(120) NOT NULL,
|
||||
"ProfileUrl" character varying(500) NULL,
|
||||
"LastResolvedAt" timestamp with time zone NULL
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS "IX_StreamerIdentities_NormalizedKey"
|
||||
ON "StreamerIdentities" ("NormalizedKey");
|
||||
|
||||
ALTER TABLE "Candidates"
|
||||
ADD COLUMN IF NOT EXISTS "StreamerIdentityId" integer NULL;
|
||||
|
||||
ALTER TABLE "Candidates"
|
||||
ADD COLUMN IF NOT EXISTS "NominationTally" integer NOT NULL DEFAULT 0;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS "IX_Candidates_StreamerIdentityId"
|
||||
ON "Candidates" ("StreamerIdentityId");
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM pg_constraint
|
||||
WHERE conname = 'FK_Candidates_StreamerIdentities_StreamerIdentityId'
|
||||
) THEN
|
||||
ALTER TABLE "Candidates"
|
||||
ADD CONSTRAINT "FK_Candidates_StreamerIdentities_StreamerIdentityId"
|
||||
FOREIGN KEY ("StreamerIdentityId") REFERENCES "StreamerIdentities" ("Id");
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
ALTER TABLE "Nominations"
|
||||
ALTER COLUMN "CategoryId" DROP NOT NULL;
|
||||
|
||||
ALTER TABLE "Nominations"
|
||||
ADD COLUMN IF NOT EXISTS "CategoryGroupName" character varying(80) NOT NULL DEFAULT '';
|
||||
|
||||
UPDATE "Nominations" n
|
||||
SET "CategoryGroupName" = c."GroupName"
|
||||
FROM "Categories" c
|
||||
WHERE n."CategoryId" = c."Id"
|
||||
AND COALESCE(n."CategoryGroupName", '') = '';
|
||||
|
||||
ALTER TABLE "Nominations"
|
||||
ADD COLUMN IF NOT EXISTS "StreamerIdentityId" integer NULL;
|
||||
|
||||
ALTER TABLE "Nominations"
|
||||
ADD COLUMN IF NOT EXISTS "SuggestedCategoryId" integer NULL;
|
||||
|
||||
ALTER TABLE "Nominations"
|
||||
ADD COLUMN IF NOT EXISTS "ResolvedChannel" character varying(120) NULL;
|
||||
|
||||
ALTER TABLE "Nominations"
|
||||
ADD COLUMN IF NOT EXISTS "ResolvedPlatform" character varying(40) NULL;
|
||||
|
||||
ALTER TABLE "Nominations"
|
||||
ADD COLUMN IF NOT EXISTS "AvgViewers" integer NULL;
|
||||
|
||||
ALTER TABLE "Nominations"
|
||||
ADD COLUMN IF NOT EXISTS "TrackerStatus" character varying(40) NOT NULL DEFAULT 'pending';
|
||||
|
||||
ALTER TABLE "Nominations"
|
||||
ADD COLUMN IF NOT EXISTS "TrackerCheckedAt" timestamp with time zone NULL;
|
||||
|
||||
ALTER TABLE "Nominations"
|
||||
ADD COLUMN IF NOT EXISTS "TrackingReviewStatus" character varying(30) NOT NULL DEFAULT 'clear';
|
||||
|
||||
ALTER TABLE "Nominations"
|
||||
ADD COLUMN IF NOT EXISTS "TrackingFlagsJson" text NOT NULL DEFAULT '[]';
|
||||
|
||||
ALTER TABLE "Nominations"
|
||||
ADD COLUMN IF NOT EXISTS "TrackingReviewNote" character varying(1000) NULL;
|
||||
|
||||
ALTER TABLE "Nominations"
|
||||
ADD COLUMN IF NOT EXISTS "TrackingReviewedByTwitchId" character varying(120) NULL;
|
||||
|
||||
ALTER TABLE "Nominations"
|
||||
ADD COLUMN IF NOT EXISTS "TrackingReviewedAt" timestamp with time zone NULL;
|
||||
|
||||
ALTER TABLE "Nominations"
|
||||
ADD COLUMN IF NOT EXISTS "Status" character varying(20) NOT NULL DEFAULT 'pending';
|
||||
|
||||
ALTER TABLE "Nominations"
|
||||
ADD COLUMN IF NOT EXISTS "ReviewNote" character varying(500) NULL;
|
||||
|
||||
ALTER TABLE "Nominations"
|
||||
ADD COLUMN IF NOT EXISTS "ReviewedByTwitchId" character varying(120) NULL;
|
||||
|
||||
ALTER TABLE "Nominations"
|
||||
ADD COLUMN IF NOT EXISTS "ReviewedAt" timestamp with time zone NULL;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS "IX_Nominations_SeasonId_Status"
|
||||
ON "Nominations" ("SeasonId", "Status");
|
||||
|
||||
CREATE INDEX IF NOT EXISTS "IX_Nominations_SeasonId_CategoryGroupName_Status"
|
||||
ON "Nominations" ("SeasonId", "CategoryGroupName", "Status");
|
||||
|
||||
CREATE INDEX IF NOT EXISTS "IX_Nominations_SeasonId_StreamerIdentityId_CategoryGroupName"
|
||||
ON "Nominations" ("SeasonId", "StreamerIdentityId", "CategoryGroupName");
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM pg_constraint
|
||||
WHERE conname = 'FK_Nominations_StreamerIdentities_StreamerIdentityId'
|
||||
) THEN
|
||||
ALTER TABLE "Nominations"
|
||||
ADD CONSTRAINT "FK_Nominations_StreamerIdentities_StreamerIdentityId"
|
||||
FOREIGN KEY ("StreamerIdentityId") REFERENCES "StreamerIdentities" ("Id");
|
||||
END IF;
|
||||
|
||||
IF NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM pg_constraint
|
||||
WHERE conname = 'FK_Nominations_Categories_SuggestedCategoryId'
|
||||
) THEN
|
||||
ALTER TABLE "Nominations"
|
||||
ADD CONSTRAINT "FK_Nominations_Categories_SuggestedCategoryId"
|
||||
FOREIGN KEY ("SuggestedCategoryId") REFERENCES "Categories" ("Id")
|
||||
ON DELETE SET NULL;
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "TeamMembers" (
|
||||
"Id" integer GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||||
"Login" character varying(80) NOT NULL,
|
||||
"DisplayName" character varying(120) NOT NULL,
|
||||
"Role" character varying(40) NOT NULL,
|
||||
"PasswordHash" character varying(120) NOT NULL,
|
||||
"PasswordSalt" character varying(80) NOT NULL,
|
||||
"BoundTwitchUserId" character varying(120) NULL,
|
||||
"BoundTwitchDisplayName" character varying(120) NULL,
|
||||
"MustChangePassword" boolean NOT NULL,
|
||||
"IsActive" boolean NOT NULL,
|
||||
"CreatedByTwitchId" character varying(120) NOT NULL,
|
||||
"UpdatedByTwitchId" character varying(120) NULL,
|
||||
"CreatedAt" timestamp with time zone NOT NULL,
|
||||
"UpdatedAt" timestamp with time zone NULL,
|
||||
"LastLoginAt" timestamp with time zone NULL,
|
||||
"TwitchBoundAt" timestamp with time zone NULL,
|
||||
"PasswordResetAt" timestamp with time zone NULL
|
||||
);
|
||||
|
||||
ALTER TABLE "TeamMembers"
|
||||
ADD COLUMN IF NOT EXISTS "BoundTwitchUserId" character varying(120) NULL;
|
||||
|
||||
ALTER TABLE "TeamMembers"
|
||||
ADD COLUMN IF NOT EXISTS "BoundTwitchDisplayName" character varying(120) NULL;
|
||||
|
||||
ALTER TABLE "TeamMembers"
|
||||
ADD COLUMN IF NOT EXISTS "TwitchBoundAt" timestamp with time zone NULL;
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS "IX_TeamMembers_Login"
|
||||
ON "TeamMembers" ("Login");
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS "IX_TeamMembers_BoundTwitchUserId"
|
||||
ON "TeamMembers" ("BoundTwitchUserId")
|
||||
WHERE "BoundTwitchUserId" IS NOT NULL;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "TeamRolePermissions" (
|
||||
"Id" integer GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||||
"Role" character varying(40) NOT NULL,
|
||||
"PermissionsJson" text NOT NULL,
|
||||
"UpdatedByTwitchId" character varying(120) NOT NULL,
|
||||
"UpdatedAt" timestamp with time zone NOT NULL
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS "IX_TeamRolePermissions_Role"
|
||||
ON "TeamRolePermissions" ("Role");
|
||||
""");
|
||||
}
|
||||
@@ -1,232 +0,0 @@
|
||||
using Backend.Domain;
|
||||
using Backend.Services;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Backend.Data;
|
||||
|
||||
public static partial class SeedDataBootstrapper
|
||||
{
|
||||
private static async Task EnsureCategoriesAsync(AwardsDbContext db, Season season)
|
||||
{
|
||||
var categories = await db.Categories
|
||||
.Where(item => item.SeasonId == season.Id)
|
||||
.ToListAsync();
|
||||
var templates = SeedCatalog.DefaultSubcategoryTemplates
|
||||
.Select((item, index) => item with { SortOrder = index + 1 })
|
||||
.ToArray();
|
||||
var usedCategories = new HashSet<Category>();
|
||||
|
||||
season.SubcategoryTemplatesJson = SeasonSubcategoryTemplateSettings.Serialize(templates);
|
||||
|
||||
var nextSortOrder = 1;
|
||||
foreach (var award in SeedCatalog.AwardCategorySeeds.OrderBy(item => item.SortOrder))
|
||||
{
|
||||
foreach (var template in templates)
|
||||
{
|
||||
var category = FindReusableCategory(categories, award, template, usedCategories)
|
||||
?? new Category { SeasonId = season.Id };
|
||||
usedCategories.Add(category);
|
||||
|
||||
category.GroupName = award.Name;
|
||||
category.Name = template.Name;
|
||||
category.Slug = BuildCategorySlug(award.Slug, template.Slug);
|
||||
category.Description = award.Description;
|
||||
category.SortOrder = nextSortOrder++;
|
||||
category.MaxNomineesPerUser = 3;
|
||||
category.ViewerRangeMin = template.ViewerRangeMin;
|
||||
category.ViewerRangeMax = template.ViewerRangeMax;
|
||||
|
||||
if (category.Id == 0 && !db.Categories.Local.Any(item => ReferenceEquals(item, category)))
|
||||
{
|
||||
db.Categories.Add(category);
|
||||
categories.Add(category);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var staleCategories = categories
|
||||
.Where(item => item.Id > 0 && !usedCategories.Contains(item))
|
||||
.ToArray();
|
||||
await RemoveStaleCategoryDataAsync(db, staleCategories);
|
||||
await db.SaveChangesAsync();
|
||||
}
|
||||
|
||||
private static async Task RemoveStaleCategoryDataAsync(AwardsDbContext db, Category[] staleCategories)
|
||||
{
|
||||
if (staleCategories.Length == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var staleCategoryIds = staleCategories.Select(item => item.Id).ToArray();
|
||||
var staleCandidateIds = await db.Candidates
|
||||
.Where(item => staleCategoryIds.Contains(item.CategoryId))
|
||||
.Select(item => item.Id)
|
||||
.ToArrayAsync();
|
||||
|
||||
var staleVoteEntries = await db.VoteEntries
|
||||
.Where(item => staleCategoryIds.Contains(item.CategoryId) || staleCandidateIds.Contains(item.CandidateId))
|
||||
.ToArrayAsync();
|
||||
db.VoteEntries.RemoveRange(staleVoteEntries);
|
||||
|
||||
var staleResults = await db.Results
|
||||
.Where(item => staleCategoryIds.Contains(item.CategoryId) || staleCandidateIds.Contains(item.CandidateId))
|
||||
.ToArrayAsync();
|
||||
db.Results.RemoveRange(staleResults);
|
||||
|
||||
var affectedClips = await db.ClipSubmissions
|
||||
.Where(item =>
|
||||
(item.CategoryId != null && staleCategoryIds.Contains(item.CategoryId.Value))
|
||||
|| (item.CandidateId != null && staleCandidateIds.Contains(item.CandidateId.Value)))
|
||||
.ToArrayAsync();
|
||||
foreach (var clip in affectedClips)
|
||||
{
|
||||
if (clip.CategoryId != null && staleCategoryIds.Contains(clip.CategoryId.Value))
|
||||
{
|
||||
clip.CategoryId = null;
|
||||
}
|
||||
|
||||
if (clip.CandidateId != null && staleCandidateIds.Contains(clip.CandidateId.Value))
|
||||
{
|
||||
clip.CandidateId = null;
|
||||
}
|
||||
}
|
||||
|
||||
var affectedNominations = await db.Nominations
|
||||
.Where(item =>
|
||||
(item.CategoryId != null && staleCategoryIds.Contains(item.CategoryId.Value))
|
||||
|| (item.SuggestedCategoryId != null && staleCategoryIds.Contains(item.SuggestedCategoryId.Value))
|
||||
|| (item.CandidateId != null && staleCandidateIds.Contains(item.CandidateId.Value)))
|
||||
.ToArrayAsync();
|
||||
foreach (var nomination in affectedNominations)
|
||||
{
|
||||
if (nomination.CategoryId != null && staleCategoryIds.Contains(nomination.CategoryId.Value))
|
||||
{
|
||||
nomination.CategoryId = null;
|
||||
}
|
||||
|
||||
if (nomination.SuggestedCategoryId != null && staleCategoryIds.Contains(nomination.SuggestedCategoryId.Value))
|
||||
{
|
||||
nomination.SuggestedCategoryId = null;
|
||||
}
|
||||
|
||||
if (nomination.CandidateId != null && staleCandidateIds.Contains(nomination.CandidateId.Value))
|
||||
{
|
||||
nomination.CandidateId = null;
|
||||
}
|
||||
}
|
||||
|
||||
var staleCandidates = await db.Candidates
|
||||
.Where(item => staleCandidateIds.Contains(item.Id))
|
||||
.ToArrayAsync();
|
||||
db.Candidates.RemoveRange(staleCandidates);
|
||||
db.Categories.RemoveRange(staleCategories);
|
||||
}
|
||||
|
||||
private static Category? FindReusableCategory(
|
||||
List<Category> categories,
|
||||
AwardCategorySeed award,
|
||||
SeasonSubcategoryTemplateSetting template,
|
||||
HashSet<Category> usedCategories)
|
||||
{
|
||||
var targetSlug = BuildCategorySlug(award.Slug, template.Slug);
|
||||
|
||||
return categories.FirstOrDefault(item => !usedCategories.Contains(item)
|
||||
&& string.Equals(item.Slug, targetSlug, StringComparison.OrdinalIgnoreCase))
|
||||
?? categories.FirstOrDefault(item => !usedCategories.Contains(item)
|
||||
&& string.Equals(item.GroupName, award.Name, StringComparison.OrdinalIgnoreCase)
|
||||
&& string.Equals(item.Name, template.Name, StringComparison.OrdinalIgnoreCase))
|
||||
?? categories.FirstOrDefault(item => !usedCategories.Contains(item)
|
||||
&& string.Equals(item.GroupName, award.LegacyGroupName, StringComparison.OrdinalIgnoreCase)
|
||||
&& string.Equals(item.Name, template.Name, StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
private static async Task EnsureCandidatesAsync(AwardsDbContext db, Season season, CandidateSeed[] seeds)
|
||||
{
|
||||
var categories = await db.Categories
|
||||
.Where(item => item.SeasonId == season.Id)
|
||||
.ToDictionaryAsync(item => item.Slug, StringComparer.OrdinalIgnoreCase);
|
||||
var existing = await db.Candidates
|
||||
.Where(item => item.SeasonId == season.Id)
|
||||
.Select(item => new { item.CategoryId, item.DisplayName, item.ChannelSlug })
|
||||
.ToArrayAsync();
|
||||
var existingKeys = existing
|
||||
.Select(item => $"{item.CategoryId}|{item.DisplayName}|{item.ChannelSlug}".ToLowerInvariant())
|
||||
.ToHashSet();
|
||||
|
||||
foreach (var seed in seeds)
|
||||
{
|
||||
if (!categories.TryGetValue(seed.CategorySlug, out var category))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var key = $"{category.Id}|{seed.DisplayName}|{seed.ChannelSlug}".ToLowerInvariant();
|
||||
if (existingKeys.Contains(key))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
db.Candidates.Add(new Candidate
|
||||
{
|
||||
SeasonId = season.Id,
|
||||
CategoryId = category.Id,
|
||||
DisplayName = seed.DisplayName,
|
||||
ChannelSlug = seed.ChannelSlug,
|
||||
Platform = seed.Platform,
|
||||
});
|
||||
}
|
||||
|
||||
await db.SaveChangesAsync();
|
||||
}
|
||||
|
||||
private static async Task EnsureWinnersAsync(AwardsDbContext db, Season season, WinnerSeed[] seeds)
|
||||
{
|
||||
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 existingResults = await db.Results
|
||||
.Where(item => item.SeasonId == season.Id)
|
||||
.ToArrayAsync();
|
||||
foreach (var result in existingResults)
|
||||
{
|
||||
if (categories.Values.FirstOrDefault(item => item.Id == result.CategoryId) is { } category
|
||||
&& result.CategoryName != category.Name)
|
||||
{
|
||||
result.CategoryName = category.Name;
|
||||
}
|
||||
}
|
||||
|
||||
var existingResultCategoryIds = existingResults.Select(item => item.CategoryId).ToHashSet();
|
||||
|
||||
foreach (var seed in seeds)
|
||||
{
|
||||
if (!categories.TryGetValue(seed.CategorySlug, out var category) || existingResultCategoryIds.Contains(category.Id))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var candidate = candidates.FirstOrDefault(item =>
|
||||
item.CategoryId == category.Id
|
||||
&& string.Equals(item.DisplayName, seed.DisplayName, StringComparison.OrdinalIgnoreCase));
|
||||
if (candidate is null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
db.Results.Add(new AwardResult
|
||||
{
|
||||
SeasonId = season.Id,
|
||||
CategoryId = category.Id,
|
||||
CandidateId = candidate.Id,
|
||||
CategoryName = category.Name,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private static string BuildCategorySlug(string awardSlug, string templateSlug) =>
|
||||
$"{SeasonSubcategoryTemplateSettings.Slugify(awardSlug)}-{SeasonSubcategoryTemplateSettings.Slugify(templateSlug)}";
|
||||
}
|
||||
@@ -1,184 +0,0 @@
|
||||
namespace Backend.Data;
|
||||
|
||||
using Backend.Services;
|
||||
|
||||
internal sealed record AwardCategorySeed(string LegacyGroupName, string Name, string Slug, string Description, int SortOrder);
|
||||
internal sealed record CandidateSeed(string CategorySlug, string DisplayName, string ChannelSlug, string Platform);
|
||||
internal sealed record WinnerSeed(int Year, string CategorySlug, string DisplayName, string ChannelSlug, string Platform);
|
||||
internal sealed record SiteFaqSeed(string Question, string Answer);
|
||||
internal sealed record SiteSocialSeed(string Label, string Platform, string Url, string Icon);
|
||||
internal sealed record SponsorSeed(string Name, string WebsiteUrl, string LogoUrl, string Description, string Tier, int SortOrder);
|
||||
|
||||
internal static class SeedCatalog
|
||||
{
|
||||
internal static readonly SeasonSubcategoryTemplateSetting[] DefaultSubcategoryTemplates =
|
||||
[
|
||||
new("Hidden Star", "hidden-star", 1, 1, 20),
|
||||
new("Rising Star", "rising-star", 2, 21, 60),
|
||||
new("Shining Star", "shining-star", 3, 61, null),
|
||||
];
|
||||
|
||||
internal static readonly AwardCategorySeed[] AwardCategorySeeds =
|
||||
[
|
||||
new("Main Awards", "VTuber des Jahres", "vtuber-des-jahres", "Die größte Auszeichnung des Jahres.", 1),
|
||||
new("Discovery", "Best Newcomer", "best-newcomer", "Neue Stimmen, neue Welten und frische Energie für die Szene.", 2),
|
||||
new("Creative", "Model & Design", "model-design", "Live2D, 3D, Outfit, Rigging und visuelle Identitaet.", 3),
|
||||
new("Performance", "Gesang & Musik", "gesang-musik", "Songs, Covers, Konzerte und musikalische Highlights.", 4),
|
||||
new("Gaming", "Best Gaming", "best-gaming", "Gameplay, Skill, Chaos und legendaere Gaming-Momente.", 5),
|
||||
new("Entertainment", "Best Variety", "best-variety", "Talk, Comedy, Watchalongs und kreative Streamformate.", 6),
|
||||
new("Community", "Community Liebling", "community-liebling", "Creator:innen, die ihre Community besonders stark verbinden.", 7),
|
||||
new("Collab", "Best Collab & Duo", "best-collab-duo", "Gemeinsame Streams, Projekte und Duo-Dynamik.", 8),
|
||||
];
|
||||
|
||||
internal static readonly Dictionary<string, string> LegacyCategorySlugMap = new(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
["bestes-live-event"] = "best-newcomer",
|
||||
["clip-des-jahres"] = "model-design",
|
||||
["beste-community"] = "gesang-musik",
|
||||
};
|
||||
|
||||
internal static readonly SiteFaqSeed[] SiteFaqSeeds =
|
||||
[
|
||||
new(
|
||||
"Wer darf nominiert werden?",
|
||||
"Jede:r aktive deutschsprachige VTuber kann nominiert werden — unabhaengig von Follower-Zahl oder Plattform. Die Community schlaegt in der Nominierungsphase ihre Favorit:innen vor."),
|
||||
new(
|
||||
"Wie funktioniert das Voting?",
|
||||
"Du meldest dich ausschließlich mit deinem Twitch-Account an — nur so kannst du teilnehmen. Das hält das Voting fair und bot-frei. Danach vergibst du pro Kategorie eine Stimme, änderbar bis zum Ende der Phase."),
|
||||
new(
|
||||
"Was kostet die Teilnahme?",
|
||||
"Gar nichts. Nominieren, voten und die Show schauen ist komplett kostenlos — der VTuber Star Award ist ein Community-Event von Fans für Fans."),
|
||||
new(
|
||||
"Wann und wo findet die Award-Show statt?",
|
||||
"Die große Live-Show wird von Jayuhime gehostet und auf Twitch & YouTube gestreamt. Den genauen Termin findest du im Countdown oben — sei live dabei, wenn die Stars gekürt werden!"),
|
||||
new(
|
||||
"Ich wurde nominiert — was nun?",
|
||||
"Glückwunsch! Du erhältst eine Benachrichtigung mit allen Infos. Teile deine Nominierung gern mit deiner Community und ruf zum Voten auf — jede Stimme zählt."),
|
||||
];
|
||||
|
||||
internal static readonly SiteSocialSeed[] SiteSocialSeeds =
|
||||
[
|
||||
new("Twitch", "twitch", "https://twitch.tv/jayuhime", "twitch"),
|
||||
new("YouTube", "youtube", "https://youtube.com/c/Jayuhime", "youtube"),
|
||||
new("X", "x", "https://x.com/jayuhime", "x"),
|
||||
new("Instagram", "instagram", "https://instagram.com/jayuhime", "instagram"),
|
||||
new("Discord", "discord", "https://discord.gg/jayuhime", "discord"),
|
||||
];
|
||||
|
||||
internal const string DefaultImprintContent = """
|
||||
Anbieter
|
||||
VTuber Star Awards, vertreten durch Jayuhime.
|
||||
|
||||
Kontakt
|
||||
Nutze für organisatorische Fragen bitte die Kontaktseite oder die hinterlegte Kontaktadresse.
|
||||
|
||||
Hinweis
|
||||
Dieses Impressum ist ein redaktioneller Platzhalter und sollte vor dem Livegang mit den tatsächlichen Anbieterangaben ersetzt werden.
|
||||
""";
|
||||
|
||||
internal const string DefaultContactContent = """
|
||||
Kontakt zum Award-Team
|
||||
Du hast Fragen zur Nominierung, zum Voting, zu Clips oder zur Show? Schreib dem Team über die hinterlegte Kontaktseite.
|
||||
|
||||
Datenschutzfragen
|
||||
Für Datenschutzanfragen nutze bitte die Datenschutz-E-Mail aus diesem Footer.
|
||||
|
||||
Community & Kooperationen
|
||||
Social Links und Partneranfragen werden vom Admin-Team gepflegt und koordiniert.
|
||||
""";
|
||||
|
||||
internal const string DefaultSponsorsContent = """
|
||||
Sponsoren & Partner
|
||||
Hier können Sponsor:innen, Medienpartner, Community-Partner und Supporter des VTuber Star Awards vorgestellt werden.
|
||||
|
||||
Partner werden im Rahmen der Show und auf den öffentlichen Kontaktflächen genannt, sobald sie final bestätigt sind.
|
||||
""";
|
||||
|
||||
internal static readonly CandidateSeed[] CurrentCandidateSeeds =
|
||||
[
|
||||
new("vtuber-des-jahres-shining-star", "Hoshimi Miyu", "@hoshimimiyu", "Twitch"),
|
||||
new("vtuber-des-jahres-shining-star", "Kurainu", "@kurainu", "Twitch"),
|
||||
new("vtuber-des-jahres-shining-star", "Shiro Ch.", "@shiroch", "Twitch"),
|
||||
new("best-newcomer-hidden-star", "Aoi Sakura", "@aoisakura", "YouTube"),
|
||||
new("best-newcomer-rising-star", "Nox Live", "@noxlive", "Twitch"),
|
||||
new("model-design-shining-star", "Velvet Rei", "@velvetrei", "Twitch"),
|
||||
new("model-design-rising-star", "Mochi Atelier", "@mochiatelier", "Cake"),
|
||||
new("gesang-musik-shining-star", "Melo Diva", "@melodiva", "YouTube"),
|
||||
new("gesang-musik-rising-star", "Yuki Stern", "@yukistern", "Twitch"),
|
||||
new("best-gaming-shining-star", "Kurainu", "@kurainu", "Twitch"),
|
||||
new("best-gaming-rising-star", "PixelPunk", "@pixelpunk", "Twitch"),
|
||||
new("best-variety-shining-star", "Taro Chaos", "@tarochaos", "Twitch"),
|
||||
new("best-variety-rising-star", "Kotaro Plays", "@kotaroplays", "YouTube"),
|
||||
new("community-liebling-shining-star", "Shiro Ch.", "@shiroch", "Twitch"),
|
||||
new("community-liebling-hidden-star", "Lumi", "@lumi_vt", "Cake"),
|
||||
new("best-collab-duo-shining-star", "Akari & Nox", "@akari_vt", "Twitch"),
|
||||
new("best-collab-duo-rising-star", "Mochi & Hana", "@mochi_mochi", "YouTube"),
|
||||
];
|
||||
|
||||
internal static readonly WinnerSeed[] WinnerSeeds =
|
||||
[
|
||||
new(2025, "vtuber-des-jahres-shining-star", "Hoshimi Miyu", "@hoshimimiyu", "Twitch"),
|
||||
new(2025, "best-newcomer-hidden-star", "Aoi Sakura", "@aoisakura", "YouTube"),
|
||||
new(2025, "model-design-shining-star", "Velvet Rei", "@velvetrei", "Twitch"),
|
||||
new(2025, "gesang-musik-shining-star", "Melo Diva", "@melodiva", "YouTube"),
|
||||
new(2025, "best-gaming-shining-star", "Kurainu", "@kurainu", "Twitch"),
|
||||
new(2025, "best-variety-shining-star", "Taro Chaos", "@tarochaos", "Twitch"),
|
||||
new(2025, "community-liebling-shining-star", "Shiro Ch.", "@shiroch", "Twitch"),
|
||||
new(2025, "best-collab-duo-shining-star", "Akari & Nox", "@akari_vt", "Cake"),
|
||||
new(2024, "vtuber-des-jahres-shining-star", "Aoi Sakura", "@aoisakura", "YouTube"),
|
||||
new(2024, "best-newcomer-hidden-star", "Lumi", "@lumi_vt", "Cake"),
|
||||
new(2024, "model-design-rising-star", "Mochi Atelier", "@mochiatelier", "Cake"),
|
||||
new(2024, "gesang-musik-rising-star", "Yuki Stern", "@yukistern", "Twitch"),
|
||||
new(2024, "best-gaming-shining-star", "Starbyte", "@starbyte", "Twitch"),
|
||||
new(2024, "best-variety-rising-star", "Kotaro Plays", "@kotaroplays", "YouTube"),
|
||||
new(2024, "community-liebling-rising-star", "Moonrelay", "@moonrelay", "Twitch"),
|
||||
new(2024, "best-collab-duo-rising-star", "Pixel & Kotaro", "@pixelpunk", "Twitch"),
|
||||
new(2023, "vtuber-des-jahres-shining-star", "Akari Nova", "@akarinova", "Twitch"),
|
||||
new(2023, "best-newcomer-rising-star", "Nox Live", "@noxlive", "Twitch"),
|
||||
new(2023, "model-design-shining-star", "Rei Velvet", "@reivelvet", "YouTube"),
|
||||
new(2023, "gesang-musik-shining-star", "Tenshi Vox", "@tenshivox", "Twitch"),
|
||||
new(2023, "best-gaming-rising-star", "Bit Knight", "@bitknight", "Twitch"),
|
||||
new(2023, "best-variety-hidden-star", "Hana Hearts", "@hanahearts", "Cake"),
|
||||
new(2023, "community-liebling-rising-star", "Sora Blau", "@sorablau", "YouTube"),
|
||||
new(2023, "best-collab-duo-rising-star", "Yuki & Melo", "@yukistern", "Twitch"),
|
||||
];
|
||||
|
||||
internal static readonly SponsorSeed[] DemoSponsorSeeds =
|
||||
[
|
||||
new(
|
||||
"HoshiForge Studio",
|
||||
"https://hoshiforge.example",
|
||||
"/demo/sponsors/hoshiforge-studio.svg",
|
||||
"Branding-, Overlay- und Debuet-Visuals fuer VTuber-Projekte und Community-Events.",
|
||||
"Presenting Sponsor",
|
||||
10),
|
||||
new(
|
||||
"NekoPixel Energy",
|
||||
"https://nekopixel.example",
|
||||
"/demo/sponsors/nekopixel-energy.svg",
|
||||
"Community-fokussierter Drink-Partner fuer lange Showabende, Watchpartys und Creator-Collabs.",
|
||||
"Gold Partner",
|
||||
20),
|
||||
new(
|
||||
"PrismLoop Audio",
|
||||
"https://prismloop.example",
|
||||
"/demo/sponsors/prismloop-audio.svg",
|
||||
"Audio-Tools, Intro-Packs und Stream-Sounddesign fuer Live-Shows und Highlight-Clips.",
|
||||
"Gold Partner",
|
||||
30),
|
||||
new(
|
||||
"CloudBeacon Hosting",
|
||||
"https://cloudbeacon.example",
|
||||
"/demo/sponsors/cloudbeacon-hosting.svg",
|
||||
"Skalierbares Hosting fuer Voting, Landingpages und Event-Traffic rund um Showtage.",
|
||||
"Tech Partner",
|
||||
40),
|
||||
new(
|
||||
"ChibiCanvas Market",
|
||||
"https://chibicanvas.example",
|
||||
"/demo/sponsors/chibicanvas-market.svg",
|
||||
"Merch-, Sticker- und Artist-Marketplace mit Fokus auf VTuber, Emotes und Fanartikel.",
|
||||
"Community Partner",
|
||||
50),
|
||||
];
|
||||
}
|
||||
@@ -1,211 +0,0 @@
|
||||
using Backend.Domain;
|
||||
using Backend.Services;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace Backend.Data;
|
||||
|
||||
public static class SeedData
|
||||
{
|
||||
public static void Apply(ModelBuilder modelBuilder)
|
||||
{
|
||||
modelBuilder.Entity<SiteSettings>().HasData(
|
||||
new SiteSettings
|
||||
{
|
||||
Id = 1,
|
||||
HostDisplayName = "Jayuhime",
|
||||
HostTagline = "VTuber & Award Host",
|
||||
NewsletterUrl = "https://vtuber-star-awards.de/newsletter",
|
||||
PrivacyEmail = "datenschutz@vtuber-star-awards.de",
|
||||
PrivacyPolicyContent = """
|
||||
Verantwortliche:r
|
||||
VTuber Star Awards, vertreten durch Jayuhime. Kontakt: datenschutz@vtuber-star-awards.de
|
||||
|
||||
Welche Daten wir verarbeiten
|
||||
Bei Teilnahme (Voting, Nominierung, Clip-Einreichung) verarbeiten wir ausschließlich deine Twitch-User-ID sowie den Zeitstempel deiner Aktion.
|
||||
Für Show-Erinnerungen speichern wir optional deine E-Mail-Adresse.
|
||||
|
||||
Rechtsgrundlage
|
||||
Verarbeitung auf Basis von Art. 6 Abs. 1 lit. b DSGVO. Für optionale Erinnerungen gilt Art. 6 Abs. 1 lit. a DSGVO.
|
||||
|
||||
Zweck der Verarbeitung
|
||||
Durchführung des VTuber Star Awards, Sicherstellung fairer Abstimmung, Spam-Prävention sowie Admin-Review von Einreichungen.
|
||||
|
||||
Löschfristen
|
||||
Alle Teilnahmedaten werden spätestens 6 Monate nach der Award-Show automatisch gelöscht. E-Mail-Adressen werden nach der Show entfernt.
|
||||
|
||||
Deine Rechte
|
||||
Du hast das Recht auf Auskunft, Löschung, Berichtigung, Widerspruch und Beschwerde bei der zuständigen Aufsichtsbehörde.
|
||||
|
||||
Weitergabe an Dritte
|
||||
Keine Weitergabe zu Werbezwecken. Technische Dienstleister verarbeiten Daten ausschließlich im Rahmen der Auftragsverarbeitung.
|
||||
""",
|
||||
PrivacyPolicyUpdatedBy = "seed",
|
||||
PrivacyPolicyUpdatedAt = new DateTimeOffset(2026, 6, 23, 0, 0, 0, TimeSpan.Zero),
|
||||
ImprintUrl = "https://vtuber-star-awards.de/impressum",
|
||||
ImprintContent = SeedCatalog.DefaultImprintContent,
|
||||
ContactUrl = "https://vtuber-star-awards.de/kontakt",
|
||||
ContactContent = SeedCatalog.DefaultContactContent,
|
||||
SponsorsUrl = "https://vtuber-star-awards.de/partner",
|
||||
SponsorsContent = SeedCatalog.DefaultSponsorsContent,
|
||||
ShowactsUrl = "https://vtuber-star-awards.de/showacts",
|
||||
ShowactsContent = "Informationen zu Showact-Bewerbungen, Ablauf und Kontakt für die Award-Show.",
|
||||
RiskRulesJson = RiskRuleSettings.Serialize(RiskRuleSettings.Defaults),
|
||||
WorkflowRulesJson = WorkflowRuleSettings.Serialize(WorkflowRuleSettings.Defaults),
|
||||
TrackingRulesJson = TrackingRulesSettings.Serialize(new TrackingRulesConfiguration(
|
||||
TrackingRulesSettings.DefaultSource,
|
||||
TrackingRulesSettings.DefaultImportantMetrics,
|
||||
TrackingRulesSettings.DefaultOptionalMetrics,
|
||||
TrackingRulesSettings.DefaultFlags)),
|
||||
ViewerStatsProviderBaseUrl = TrackingRulesSettings.DefaultBaseUrl,
|
||||
TrackingReviewNotes = "Fallback-Quellen für manuelle Reviews:\\n- SullyGnome\\n- Twitch-Kanal direkt\\n\\nNutze diese Notizen für Edge Cases und manuelle Tier-Entscheidungen.",
|
||||
NominationLinkBlacklistJson = NominationLinkBlacklistSettings.Serialize(NominationLinkBlacklistSettings.Defaults),
|
||||
ClipSubmissionsEnabled = false,
|
||||
ClipReviewEnabled = true,
|
||||
ClipSubmissionDisabledMessage = "Clip-Einreichungen sind aktuell geschlossen.",
|
||||
ShowactApplicationsEnabled = false,
|
||||
ShowactApplicationDisabledMessage = "Showact-Bewerbungen sind aktuell geschlossen.",
|
||||
SessionIdleTimeoutHours = 3,
|
||||
SponsorsVisible = true,
|
||||
SocialLinksJson = JsonSerializer.Serialize(new[]
|
||||
{
|
||||
new { label = "Twitch", platform = "twitch", url = "https://twitch.tv/jayuhime", icon = "twitch", showOnHost = true, showOnCommunity = true },
|
||||
new { label = "YouTube", platform = "youtube", url = "https://youtube.com/c/Jayuhime", icon = "youtube", showOnHost = true, showOnCommunity = true },
|
||||
new { label = "X", platform = "x", url = "https://x.com/jayuhime", icon = "x", showOnHost = true, showOnCommunity = true },
|
||||
new { label = "Instagram", platform = "instagram", url = "https://instagram.com/jayuhime", icon = "instagram", showOnHost = true, showOnCommunity = true },
|
||||
new { label = "Discord", platform = "discord", url = "https://discord.gg/jayuhime", icon = "discord", showOnHost = true, showOnCommunity = true },
|
||||
}),
|
||||
FaqJson = JsonSerializer.Serialize(new[]
|
||||
{
|
||||
new { question = "Wer darf nominiert werden?", answer = "Jede:r aktive deutschsprachige VTuber kann nominiert werden — unabhängig von Follower-Zahl oder Plattform. Die Community schlägt in der Nominierungsphase ihre Favorit:innen vor." },
|
||||
new { question = "Wie funktioniert das Voting?", answer = "Du meldest dich ausschließlich mit deinem Twitch-Account an — nur so kannst du teilnehmen. Das hält das Voting fair und bot-frei. Danach vergibst du pro Kategorie eine Stimme, änderbar bis zum Ende der Phase." },
|
||||
new { question = "Was kostet die Teilnahme?", answer = "Gar nichts. Nominieren, voten und die Show schauen ist komplett kostenlos — der VTuber Star Award ist ein Community-Event von Fans für Fans." },
|
||||
new { question = "Wann und wo findet die Award-Show statt?", answer = "Die große Live-Show wird von Jayuhime gehostet und auf Twitch & YouTube gestreamt. Den genauen Termin findest du im Countdown oben — sei live dabei, wenn die Stars gekürt werden!" },
|
||||
new { question = "Ich wurde nominiert — was nun?", answer = "Glückwunsch! Du erhältst eine Benachrichtigung mit allen Infos. Teile deine Nominierung gern mit deiner Community und ruf zum Voten auf — jede Stimme zählt." },
|
||||
}),
|
||||
});
|
||||
|
||||
modelBuilder.Entity<Season>().HasData(
|
||||
new Season
|
||||
{
|
||||
Id = 1,
|
||||
Year = 2026,
|
||||
Name = "VTuber Star Awards 2026",
|
||||
ShowStreamUrl = "https://twitch.tv/jayuhime",
|
||||
IsCurrent = true,
|
||||
IsCommunityOnly = true,
|
||||
CurrentPhase = "Community Voting",
|
||||
NominationStartsAt = new DateOnly(2026, 5, 1),
|
||||
NominationEndsAt = new DateOnly(2026, 5, 31),
|
||||
VotingStartsAt = new DateOnly(2026, 6, 1),
|
||||
VotingEndsAt = new DateOnly(2026, 6, 30),
|
||||
ReviewStartsAt = new DateOnly(2026, 7, 1),
|
||||
ReviewEndsAt = new DateOnly(2026, 7, 10),
|
||||
ShowDate = new DateOnly(2026, 7, 20),
|
||||
ShowStartsAt = new TimeOnly(20, 0),
|
||||
},
|
||||
new Season
|
||||
{
|
||||
Id = 2,
|
||||
Year = 2025,
|
||||
Name = "VTuber Star Awards 2025",
|
||||
ShowStreamUrl = "https://twitch.tv/jayuhime",
|
||||
IsCurrent = false,
|
||||
IsCommunityOnly = true,
|
||||
CurrentPhase = "Archived",
|
||||
NominationStartsAt = new DateOnly(2025, 5, 1),
|
||||
NominationEndsAt = new DateOnly(2025, 5, 31),
|
||||
VotingStartsAt = new DateOnly(2025, 6, 1),
|
||||
VotingEndsAt = new DateOnly(2025, 6, 30),
|
||||
ReviewStartsAt = new DateOnly(2025, 7, 1),
|
||||
ReviewEndsAt = new DateOnly(2025, 7, 10),
|
||||
ShowDate = new DateOnly(2025, 7, 20),
|
||||
ShowStartsAt = new TimeOnly(20, 0),
|
||||
},
|
||||
new Season
|
||||
{
|
||||
Id = 3,
|
||||
Year = 2024,
|
||||
Name = "VTuber Star Awards 2024",
|
||||
ShowStreamUrl = "https://youtube.com/c/Jayuhime",
|
||||
IsCurrent = false,
|
||||
IsCommunityOnly = true,
|
||||
CurrentPhase = "Archived",
|
||||
NominationStartsAt = new DateOnly(2024, 5, 1),
|
||||
NominationEndsAt = new DateOnly(2024, 5, 31),
|
||||
VotingStartsAt = new DateOnly(2024, 6, 1),
|
||||
VotingEndsAt = new DateOnly(2024, 6, 30),
|
||||
ReviewStartsAt = new DateOnly(2024, 7, 1),
|
||||
ReviewEndsAt = new DateOnly(2024, 7, 10),
|
||||
ShowDate = new DateOnly(2024, 7, 20),
|
||||
ShowStartsAt = new TimeOnly(20, 0),
|
||||
},
|
||||
new Season
|
||||
{
|
||||
Id = 4,
|
||||
Year = 2023,
|
||||
Name = "VTuber Star Awards 2023",
|
||||
ShowStreamUrl = "https://twitch.tv/jayuhime",
|
||||
IsCurrent = false,
|
||||
IsCommunityOnly = true,
|
||||
CurrentPhase = "Archived",
|
||||
NominationStartsAt = new DateOnly(2023, 5, 1),
|
||||
NominationEndsAt = new DateOnly(2023, 5, 31),
|
||||
VotingStartsAt = new DateOnly(2023, 6, 1),
|
||||
VotingEndsAt = new DateOnly(2023, 6, 30),
|
||||
ReviewStartsAt = new DateOnly(2023, 7, 1),
|
||||
ReviewEndsAt = new DateOnly(2023, 7, 10),
|
||||
ShowDate = new DateOnly(2023, 7, 20),
|
||||
ShowStartsAt = new TimeOnly(20, 0),
|
||||
});
|
||||
|
||||
modelBuilder.Entity<Category>().HasData(
|
||||
new Category { Id = 1, SeasonId = 1, GroupName = "Main Awards", Name = "VTuber des Jahres", Slug = "vtuber-des-jahres", Description = "Die größte Auszeichnung des Jahres.", SortOrder = 1, MaxNomineesPerUser = 3 },
|
||||
new Category { Id = 2, SeasonId = 1, GroupName = "Performance", Name = "Bestes Live Event", Slug = "bestes-live-event", Description = "Events, Konzerte und 3D-Shows.", SortOrder = 2, MaxNomineesPerUser = 3 },
|
||||
new Category { Id = 3, SeasonId = 1, GroupName = "Clips & Highlights", Name = "Clip des Jahres", Slug = "clip-des-jahres", Description = "Der lustigste oder emotionalste Clip des Jahres.", SortOrder = 3, MaxNomineesPerUser = 3 },
|
||||
new Category { Id = 4, SeasonId = 1, GroupName = "Main Awards", Name = "Beste Community", Slug = "beste-community", Description = "Die aktivste und freundlichste Community.", SortOrder = 4, MaxNomineesPerUser = 3 },
|
||||
new Category { Id = 5, SeasonId = 2, GroupName = "Main Awards", Name = "VTuber des Jahres", Slug = "vtuber-des-jahres", Description = "Archivkategorie 2025.", SortOrder = 1, MaxNomineesPerUser = 3 },
|
||||
new Category { Id = 6, SeasonId = 2, GroupName = "Performance", Name = "Bestes Live Event", Slug = "bestes-live-event", Description = "Archivkategorie 2025.", SortOrder = 2, MaxNomineesPerUser = 3 },
|
||||
new Category { Id = 7, SeasonId = 2, GroupName = "Clips & Highlights", Name = "Clip des Jahres", Slug = "clip-des-jahres", Description = "Archivkategorie 2025.", SortOrder = 3, MaxNomineesPerUser = 3 },
|
||||
new Category { Id = 8, SeasonId = 3, GroupName = "Main Awards", Name = "VTuber des Jahres", Slug = "vtuber-des-jahres", Description = "Archivkategorie 2024.", SortOrder = 1, MaxNomineesPerUser = 3 },
|
||||
new Category { Id = 9, SeasonId = 3, GroupName = "Clips & Highlights", Name = "Clip des Jahres", Slug = "clip-des-jahres", Description = "Archivkategorie 2024.", SortOrder = 2, MaxNomineesPerUser = 3 },
|
||||
new Category { Id = 10, SeasonId = 4, GroupName = "Main Awards", Name = "VTuber des Jahres", Slug = "vtuber-des-jahres", Description = "Archivkategorie 2023.", SortOrder = 1, MaxNomineesPerUser = 3 });
|
||||
|
||||
modelBuilder.Entity<Candidate>().HasData(
|
||||
new Candidate { Id = 1, SeasonId = 1, CategoryId = 1, DisplayName = "Hoshimi Miyu", ChannelSlug = "@hoshimimiyu", Platform = "Twitch" },
|
||||
new Candidate { Id = 2, SeasonId = 1, CategoryId = 1, DisplayName = "Kurainu", ChannelSlug = "@kurainu", Platform = "Twitch" },
|
||||
new Candidate { Id = 3, SeasonId = 1, CategoryId = 1, DisplayName = "Shiro Ch.", ChannelSlug = "@shiroch", Platform = "Twitch" },
|
||||
new Candidate { Id = 4, SeasonId = 1, CategoryId = 2, DisplayName = "Kurainu 3D Live", ChannelSlug = "@kurainu", Platform = "Twitch" },
|
||||
new Candidate { Id = 5, SeasonId = 1, CategoryId = 2, DisplayName = "Aoi Sakura Showcase", ChannelSlug = "@aoisakura", Platform = "YouTube" },
|
||||
new Candidate { Id = 6, SeasonId = 1, CategoryId = 3, DisplayName = "Pyonkichi Kingdom", ChannelSlug = "@pyonkichikingdom", Platform = "Twitch" },
|
||||
new Candidate { Id = 7, SeasonId = 1, CategoryId = 4, DisplayName = "Moonrelay", ChannelSlug = "@moonrelay", Platform = "Twitch" },
|
||||
new Candidate { Id = 8, SeasonId = 2, CategoryId = 5, DisplayName = "Hoshimi Miyu", ChannelSlug = "@hoshimimiyu", Platform = "Twitch" },
|
||||
new Candidate { Id = 9, SeasonId = 2, CategoryId = 6, DisplayName = "Kurainu 3D Live", ChannelSlug = "@kurainu", Platform = "Twitch" },
|
||||
new Candidate { Id = 10, SeasonId = 2, CategoryId = 7, DisplayName = "Pyonkichi Kingdom", ChannelSlug = "@pyonkichikingdom", Platform = "Twitch" },
|
||||
new Candidate { Id = 11, SeasonId = 3, CategoryId = 8, DisplayName = "Aoi Sakura", ChannelSlug = "@aoisakura", Platform = "YouTube" },
|
||||
new Candidate { Id = 12, SeasonId = 3, CategoryId = 9, DisplayName = "Starbyte", ChannelSlug = "@starbyte", Platform = "Twitch" },
|
||||
new Candidate { Id = 13, SeasonId = 4, CategoryId = 10, DisplayName = "Tenshi Vox", ChannelSlug = "@tenshivox", Platform = "Twitch" });
|
||||
|
||||
modelBuilder.Entity<AwardResult>().HasData(
|
||||
new AwardResult { Id = 1, SeasonId = 2, CategoryId = 5, CandidateId = 8, CategoryName = "VTuber des Jahres" },
|
||||
new AwardResult { Id = 2, SeasonId = 2, CategoryId = 6, CandidateId = 9, CategoryName = "Bestes Live Event" },
|
||||
new AwardResult { Id = 3, SeasonId = 2, CategoryId = 7, CandidateId = 10, CategoryName = "Clip des Jahres" },
|
||||
new AwardResult { Id = 4, SeasonId = 3, CategoryId = 8, CandidateId = 11, CategoryName = "VTuber des Jahres" },
|
||||
new AwardResult { Id = 5, SeasonId = 3, CategoryId = 9, CandidateId = 12, CategoryName = "Clip des Jahres" },
|
||||
new AwardResult { Id = 6, SeasonId = 4, CategoryId = 10, CandidateId = 13, CategoryName = "VTuber des Jahres" });
|
||||
|
||||
modelBuilder.Entity<Nomination>().HasData(
|
||||
new Nomination { Id = 1, SeasonId = 1, CategoryId = 1, SubmittedByTwitchId = "twitch_hoshi", CandidateText = "Hoshimi Miyu", CreatedAt = new DateTimeOffset(2026, 6, 10, 13, 0, 0, TimeSpan.Zero) },
|
||||
new Nomination { Id = 2, SeasonId = 1, CategoryId = 2, SubmittedByTwitchId = "twitch_kurainu", CandidateText = "Kurainu 3D Live", CreatedAt = new DateTimeOffset(2026, 6, 10, 14, 0, 0, TimeSpan.Zero) });
|
||||
|
||||
modelBuilder.Entity<VoteBallot>().HasData(
|
||||
new VoteBallot { Id = 1, SeasonId = 1, SubmittedByTwitchId = "twitch_vote_1", Status = "submitted", SubmittedAt = new DateTimeOffset(2026, 6, 11, 12, 0, 0, TimeSpan.Zero) },
|
||||
new VoteBallot { Id = 2, SeasonId = 1, SubmittedByTwitchId = "twitch_vote_2", Status = "submitted", SubmittedAt = new DateTimeOffset(2026, 6, 11, 12, 5, 0, TimeSpan.Zero) });
|
||||
|
||||
modelBuilder.Entity<VoteEntry>().HasData(
|
||||
new VoteEntry { Id = 1, BallotId = 1, CategoryId = 1, CandidateId = 1 },
|
||||
new VoteEntry { Id = 2, BallotId = 1, CategoryId = 2, CandidateId = 4 },
|
||||
new VoteEntry { Id = 3, BallotId = 2, CategoryId = 1, CandidateId = 2 },
|
||||
new VoteEntry { Id = 4, BallotId = 2, CategoryId = 3, CandidateId = 6 });
|
||||
}
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Backend.Data;
|
||||
|
||||
public static partial class SeedDataBootstrapper
|
||||
{
|
||||
public static async Task EnsureAsync(AwardsDbContext db)
|
||||
{
|
||||
await EnsureSiteSettingsAsync(db);
|
||||
|
||||
var seasons = await db.Seasons.ToDictionaryAsync(item => item.Year);
|
||||
if (seasons.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var season in seasons.Values)
|
||||
{
|
||||
await EnsureCategoriesAsync(db, season);
|
||||
}
|
||||
|
||||
if (seasons.TryGetValue(2026, out var currentSeason))
|
||||
{
|
||||
await EnsureCandidatesAsync(db, currentSeason, SeedCatalog.CurrentCandidateSeeds);
|
||||
await EnsureSponsorsAsync(db, currentSeason);
|
||||
await EnsureSeedOperationalDataAsync(db, currentSeason);
|
||||
}
|
||||
|
||||
foreach (var year in new[] { 2025, 2024, 2023 })
|
||||
{
|
||||
if (!seasons.TryGetValue(year, out var season))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var winners = SeedCatalog.WinnerSeeds.Where(item => item.Year == year).ToArray();
|
||||
await EnsureCandidatesAsync(db, season, winners.Select(item => new CandidateSeed(item.CategorySlug, item.DisplayName, item.ChannelSlug, item.Platform)).ToArray());
|
||||
await EnsureWinnersAsync(db, season, winners);
|
||||
}
|
||||
|
||||
await db.SaveChangesAsync();
|
||||
}
|
||||
}
|
||||
@@ -1,554 +0,0 @@
|
||||
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);
|
||||
await EnsureSeedReviewNominationsAsync(db, season, categories, candidates);
|
||||
|
||||
if (!await db.ClipSubmissions.AnyAsync(item => item.SeasonId == season.Id))
|
||||
{
|
||||
db.ClipSubmissions.AddRange(
|
||||
new ClipSubmission
|
||||
{
|
||||
SeasonId = season.Id,
|
||||
CategoryId = ResolveCategoryId(categories, "vtuber-des-jahres-shining-star"),
|
||||
CandidateId = ResolveCandidateId(categories, candidates, "vtuber-des-jahres-shining-star", "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 für 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-shining-star"),
|
||||
CandidateId = ResolveCandidateId(categories, candidates, "vtuber-des-jahres-shining-star", "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 für 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-shining-star"),
|
||||
CandidateId = ResolveCandidateId(categories, candidates, "best-gaming-shining-star", "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-shining-star"),
|
||||
CandidateId = ResolveCandidateId(categories, candidates, "gesang-musik-shining-star", "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 für 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 { awardCategories = SeedCatalog.AwardCategorySeeds.Length, subcategories = SeedCatalog.DefaultSubcategoryTemplates.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 static async Task EnsureSeedReviewNominationsAsync(
|
||||
AwardsDbContext db,
|
||||
Season season,
|
||||
IReadOnlyDictionary<string, Category> categories,
|
||||
Candidate[] candidates)
|
||||
{
|
||||
var staleSeedNominations = await db.Nominations
|
||||
.Where(item =>
|
||||
item.SeasonId == season.Id
|
||||
&& (
|
||||
item.SubmittedByTwitchId.StartsWith("seed_review_")
|
||||
|| item.SubmittedByTwitchId == "twitch_hoshi"
|
||||
|| item.SubmittedByTwitchId == "twitch_kurainu"
|
||||
|| item.SubmittedByTwitchId.StartsWith("demo_user")
|
||||
|| item.SubmittedByTwitchId.StartsWith("local_user")
|
||||
))
|
||||
.ToArrayAsync();
|
||||
|
||||
if (staleSeedNominations.Length > 0)
|
||||
{
|
||||
db.Nominations.RemoveRange(staleSeedNominations);
|
||||
await db.SaveChangesAsync();
|
||||
}
|
||||
|
||||
var orderedCategories = categories.Values
|
||||
.OrderBy(item => item.SortOrder)
|
||||
.ThenBy(item => item.Name)
|
||||
.ToArray();
|
||||
var candidatesByCategoryId = candidates
|
||||
.GroupBy(item => item.CategoryId)
|
||||
.ToDictionary(
|
||||
grouping => grouping.Key,
|
||||
grouping => grouping.OrderBy(item => item.DisplayName).ToArray());
|
||||
|
||||
var seedNominations = new List<Nomination>();
|
||||
var createdAt = new DateTimeOffset(2026, 6, 22, 12, 0, 0, TimeSpan.Zero);
|
||||
|
||||
foreach (var category in orderedCategories)
|
||||
{
|
||||
candidatesByCategoryId.TryGetValue(category.Id, out var categoryCandidates);
|
||||
var existingCandidate = categoryCandidates?.FirstOrDefault();
|
||||
|
||||
seedNominations.AddRange(BuildPendingSeedGroup(
|
||||
category,
|
||||
existingCandidate,
|
||||
groupKey: "existing",
|
||||
firstSubmitter: $"seed_review_{category.Slug}_existing_a",
|
||||
secondSubmitter: $"seed_review_{category.Slug}_existing_b",
|
||||
createdAt,
|
||||
useSuggestedCategory: true,
|
||||
trackerStatus: "resolved"));
|
||||
createdAt = createdAt.AddMinutes(8);
|
||||
|
||||
seedNominations.AddRange(BuildPendingSeedGroup(
|
||||
category,
|
||||
existingCandidate: null,
|
||||
groupKey: "fresh",
|
||||
firstSubmitter: $"seed_review_{category.Slug}_fresh_a",
|
||||
secondSubmitter: $"seed_review_{category.Slug}_fresh_b",
|
||||
createdAt,
|
||||
useSuggestedCategory: false,
|
||||
trackerStatus: "unsupported_platform"));
|
||||
createdAt = createdAt.AddMinutes(8);
|
||||
}
|
||||
|
||||
foreach (var category in orderedCategories.Take(2))
|
||||
{
|
||||
seedNominations.AddRange(BuildReviewedSeedGroup(
|
||||
category,
|
||||
status: "rejected",
|
||||
displayName: $"{category.GroupName} Review Return",
|
||||
submittedByPrefix: $"seed_review_{category.Slug}_rejected",
|
||||
createdAt,
|
||||
reviewedByTwitchId: "jayuhime_admin",
|
||||
candidateId: null,
|
||||
candidateDisplayName: null));
|
||||
createdAt = createdAt.AddMinutes(10);
|
||||
}
|
||||
|
||||
foreach (var category in orderedCategories.Skip(2).Take(2))
|
||||
{
|
||||
candidatesByCategoryId.TryGetValue(category.Id, out var categoryCandidates);
|
||||
var candidate = categoryCandidates?.FirstOrDefault();
|
||||
seedNominations.AddRange(BuildReviewedSeedGroup(
|
||||
category,
|
||||
status: "approved",
|
||||
displayName: candidate?.DisplayName ?? $"{category.GroupName} Approved Pick",
|
||||
submittedByPrefix: $"seed_review_{category.Slug}_approved",
|
||||
createdAt,
|
||||
reviewedByTwitchId: "jayuhime_admin",
|
||||
candidateId: candidate?.Id,
|
||||
candidateDisplayName: candidate?.DisplayName));
|
||||
createdAt = createdAt.AddMinutes(10);
|
||||
}
|
||||
|
||||
db.Nominations.AddRange(seedNominations);
|
||||
await db.SaveChangesAsync();
|
||||
}
|
||||
|
||||
private static IEnumerable<Nomination> BuildPendingSeedGroup(
|
||||
Category category,
|
||||
Candidate? existingCandidate,
|
||||
string groupKey,
|
||||
string firstSubmitter,
|
||||
string secondSubmitter,
|
||||
DateTimeOffset createdAt,
|
||||
bool useSuggestedCategory,
|
||||
string trackerStatus)
|
||||
{
|
||||
var displayName = existingCandidate?.DisplayName ?? BuildFreshSeedName(category, groupKey);
|
||||
var platform = existingCandidate?.Platform ?? "YouTube";
|
||||
var channelSlug = existingCandidate?.ChannelSlug ?? BuildSeedChannelSlug(category, groupKey);
|
||||
var streamUrl = BuildSeedStreamUrl(platform, channelSlug);
|
||||
var avgViewers = ResolveSeedViewerValue(category);
|
||||
int? suggestedCategoryId = useSuggestedCategory ? category.Id : null;
|
||||
|
||||
yield return new Nomination
|
||||
{
|
||||
SeasonId = category.SeasonId,
|
||||
CategoryId = category.Id,
|
||||
CategoryGroupName = category.GroupName,
|
||||
SubmittedByTwitchId = firstSubmitter,
|
||||
CandidateText = displayName,
|
||||
StreamUrl = streamUrl,
|
||||
ResolvedChannel = channelSlug.TrimStart('@'),
|
||||
ResolvedPlatform = platform,
|
||||
AvgViewers = avgViewers,
|
||||
SuggestedCategoryId = suggestedCategoryId,
|
||||
TrackerStatus = trackerStatus,
|
||||
TrackerCheckedAt = createdAt.AddMinutes(2),
|
||||
TrackingReviewStatus = "clear",
|
||||
Status = "pending",
|
||||
CreatedAt = createdAt,
|
||||
};
|
||||
|
||||
yield return new Nomination
|
||||
{
|
||||
SeasonId = category.SeasonId,
|
||||
CategoryId = category.Id,
|
||||
CategoryGroupName = category.GroupName,
|
||||
SubmittedByTwitchId = secondSubmitter,
|
||||
CandidateText = displayName,
|
||||
StreamUrl = streamUrl,
|
||||
ResolvedChannel = channelSlug.TrimStart('@'),
|
||||
ResolvedPlatform = platform,
|
||||
AvgViewers = avgViewers,
|
||||
SuggestedCategoryId = suggestedCategoryId,
|
||||
TrackerStatus = trackerStatus,
|
||||
TrackerCheckedAt = createdAt.AddMinutes(3),
|
||||
TrackingReviewStatus = "clear",
|
||||
Status = "pending",
|
||||
CreatedAt = createdAt.AddMinutes(1),
|
||||
};
|
||||
}
|
||||
|
||||
private static IEnumerable<Nomination> BuildReviewedSeedGroup(
|
||||
Category category,
|
||||
string status,
|
||||
string displayName,
|
||||
string submittedByPrefix,
|
||||
DateTimeOffset createdAt,
|
||||
string reviewedByTwitchId,
|
||||
int? candidateId,
|
||||
string? candidateDisplayName)
|
||||
{
|
||||
var channelSlug = BuildSeedChannelSlug(category, $"{status}_{displayName}");
|
||||
var streamUrl = BuildSeedStreamUrl("Twitch", channelSlug);
|
||||
var reviewNote = status == "approved"
|
||||
? "Seed-Datensatz: bereits als Kandidat übernommen."
|
||||
: "Seed-Datensatz: bewusst verworfen für Undo-Tests.";
|
||||
|
||||
yield return new Nomination
|
||||
{
|
||||
SeasonId = category.SeasonId,
|
||||
CategoryId = category.Id,
|
||||
CategoryGroupName = category.GroupName,
|
||||
SubmittedByTwitchId = $"{submittedByPrefix}_a",
|
||||
CandidateId = candidateId,
|
||||
CandidateText = displayName,
|
||||
StreamUrl = streamUrl,
|
||||
ResolvedChannel = channelSlug.TrimStart('@'),
|
||||
ResolvedPlatform = "Twitch",
|
||||
AvgViewers = ResolveSeedViewerValue(category),
|
||||
SuggestedCategoryId = category.Id,
|
||||
TrackerStatus = "resolved",
|
||||
TrackerCheckedAt = createdAt.AddMinutes(2),
|
||||
TrackingReviewStatus = "reviewed",
|
||||
TrackingReviewNote = reviewNote,
|
||||
TrackingReviewedByTwitchId = reviewedByTwitchId,
|
||||
TrackingReviewedAt = createdAt.AddMinutes(4),
|
||||
Status = status,
|
||||
ReviewNote = reviewNote,
|
||||
ReviewedByTwitchId = reviewedByTwitchId,
|
||||
CreatedAt = createdAt,
|
||||
ReviewedAt = createdAt.AddMinutes(4),
|
||||
};
|
||||
|
||||
yield return new Nomination
|
||||
{
|
||||
SeasonId = category.SeasonId,
|
||||
CategoryId = category.Id,
|
||||
CategoryGroupName = category.GroupName,
|
||||
SubmittedByTwitchId = $"{submittedByPrefix}_b",
|
||||
CandidateId = candidateId,
|
||||
CandidateText = displayName,
|
||||
StreamUrl = streamUrl,
|
||||
ResolvedChannel = channelSlug.TrimStart('@'),
|
||||
ResolvedPlatform = "Twitch",
|
||||
AvgViewers = ResolveSeedViewerValue(category),
|
||||
SuggestedCategoryId = category.Id,
|
||||
TrackerStatus = "resolved",
|
||||
TrackerCheckedAt = createdAt.AddMinutes(3),
|
||||
TrackingReviewStatus = "reviewed",
|
||||
TrackingReviewNote = reviewNote,
|
||||
TrackingReviewedByTwitchId = reviewedByTwitchId,
|
||||
TrackingReviewedAt = createdAt.AddMinutes(5),
|
||||
Status = status,
|
||||
ReviewNote = reviewNote,
|
||||
ReviewedByTwitchId = reviewedByTwitchId,
|
||||
CreatedAt = createdAt.AddMinutes(1),
|
||||
ReviewedAt = createdAt.AddMinutes(5),
|
||||
};
|
||||
}
|
||||
|
||||
private static string BuildFreshSeedName(Category category, string groupKey) =>
|
||||
groupKey switch
|
||||
{
|
||||
"fresh" => $"{category.GroupName} Spotlight {category.Name}",
|
||||
_ => $"{category.GroupName} {category.Name} Pick",
|
||||
};
|
||||
|
||||
private static string BuildSeedChannelSlug(Category category, string suffix)
|
||||
{
|
||||
var raw = $"{category.Slug}-{suffix}"
|
||||
.Trim()
|
||||
.TrimStart('@')
|
||||
.ToLowerInvariant();
|
||||
|
||||
return new string(raw.Where(char.IsLetterOrDigit).ToArray());
|
||||
}
|
||||
|
||||
private static string BuildSeedStreamUrl(string platform, string channelSlug) =>
|
||||
platform.Trim().ToLowerInvariant() switch
|
||||
{
|
||||
"youtube" => $"https://www.youtube.com/@{channelSlug}",
|
||||
"kick" => $"https://kick.com/{channelSlug}",
|
||||
"cake" => $"https://cake.gg/{channelSlug}",
|
||||
_ => $"https://www.twitch.tv/{channelSlug}",
|
||||
};
|
||||
|
||||
private static int ResolveSeedViewerValue(Category category)
|
||||
{
|
||||
if (category.ViewerRangeMin is int min && category.ViewerRangeMax is int max)
|
||||
{
|
||||
return min + ((max - min) / 2);
|
||||
}
|
||||
|
||||
if (category.ViewerRangeMin is int lowerBound)
|
||||
{
|
||||
return lowerBound + 12;
|
||||
}
|
||||
|
||||
if (category.ViewerRangeMax is int upperBound)
|
||||
{
|
||||
return Math.Max(1, upperBound - 5);
|
||||
}
|
||||
|
||||
return 25;
|
||||
}
|
||||
|
||||
private sealed record LegacySeedState(bool HasRiskSeed, bool HasAuditSeed);
|
||||
}
|
||||
@@ -1,187 +0,0 @@
|
||||
using System.Text.Json;
|
||||
using Backend.Services;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Backend.Data;
|
||||
|
||||
public static partial class SeedDataBootstrapper
|
||||
{
|
||||
private static async Task EnsureSiteSettingsAsync(AwardsDbContext db)
|
||||
{
|
||||
var settings = await db.SiteSettings.FirstOrDefaultAsync(item => item.Id == 1);
|
||||
if (settings is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!HasValidSiteArray(settings.FaqJson, "question", "answer"))
|
||||
{
|
||||
settings.FaqJson = JsonSerializer.Serialize(SeedCatalog.SiteFaqSeeds.Select(item => new
|
||||
{
|
||||
question = item.Question,
|
||||
answer = item.Answer,
|
||||
}));
|
||||
}
|
||||
|
||||
if (!HasValidSiteArray(settings.SocialLinksJson, "label", "platform", "url"))
|
||||
{
|
||||
settings.SocialLinksJson = JsonSerializer.Serialize(SeedCatalog.SiteSocialSeeds.Select(item => new
|
||||
{
|
||||
label = item.Label,
|
||||
platform = item.Platform,
|
||||
url = item.Url,
|
||||
icon = item.Icon,
|
||||
showOnHost = true,
|
||||
showOnCommunity = true,
|
||||
}));
|
||||
}
|
||||
|
||||
if (!HasValidRiskRules(settings.RiskRulesJson))
|
||||
{
|
||||
settings.RiskRulesJson = RiskRuleSettings.Serialize(RiskRuleSettings.Defaults);
|
||||
}
|
||||
|
||||
if (!HasValidWorkflowRules(settings.WorkflowRulesJson))
|
||||
{
|
||||
settings.WorkflowRulesJson = WorkflowRuleSettings.Serialize(WorkflowRuleSettings.Defaults);
|
||||
}
|
||||
|
||||
if (!HasValidTrackingRules(settings.TrackingRulesJson))
|
||||
{
|
||||
settings.TrackingRulesJson = TrackingRulesSettings.Serialize(new TrackingRulesConfiguration(
|
||||
TrackingRulesSettings.DefaultSource,
|
||||
TrackingRulesSettings.DefaultImportantMetrics,
|
||||
TrackingRulesSettings.DefaultOptionalMetrics,
|
||||
TrackingRulesSettings.DefaultFlags));
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(settings.ViewerStatsProviderBaseUrl))
|
||||
{
|
||||
settings.ViewerStatsProviderBaseUrl = TrackingRulesSettings.DefaultBaseUrl;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(settings.TrackingReviewNotes))
|
||||
{
|
||||
settings.TrackingReviewNotes = """
|
||||
Fallback-Quellen für manuelle Reviews:
|
||||
- SullyGnome Channel Summary
|
||||
- Offizieller Twitch-Kanal
|
||||
|
||||
Prüfe bei Edge Cases:
|
||||
- passt der Kanal wirklich zur Unterkategorie?
|
||||
- fehlen TwitchTracker-Daten nur temporär?
|
||||
- braucht der Fall eine manuelle Team-Notiz?
|
||||
""";
|
||||
}
|
||||
|
||||
if (!HasValidNominationLinkBlacklist(settings.NominationLinkBlacklistJson))
|
||||
{
|
||||
settings.NominationLinkBlacklistJson = NominationLinkBlacklistSettings.Serialize(NominationLinkBlacklistSettings.Defaults);
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(settings.ClipSubmissionDisabledMessage))
|
||||
{
|
||||
settings.ClipSubmissionDisabledMessage = "Clip-Einreichungen sind aktuell geschlossen.";
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(settings.ShowactApplicationDisabledMessage))
|
||||
{
|
||||
settings.ShowactApplicationDisabledMessage = "Showact-Bewerbungen sind aktuell geschlossen.";
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(settings.ImprintContent))
|
||||
{
|
||||
settings.ImprintContent = SeedCatalog.DefaultImprintContent;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(settings.ContactContent))
|
||||
{
|
||||
settings.ContactContent = SeedCatalog.DefaultContactContent;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(settings.SponsorsContent))
|
||||
{
|
||||
settings.SponsorsContent = SeedCatalog.DefaultSponsorsContent;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(settings.ShowactsUrl))
|
||||
{
|
||||
settings.ShowactsUrl = "https://vtuber-star-awards.de/showacts";
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(settings.ShowactsContent))
|
||||
{
|
||||
settings.ShowactsContent = "Informationen zu Showact-Bewerbungen, Ablauf und Kontakt für die Award-Show.";
|
||||
}
|
||||
|
||||
if (settings.SessionIdleTimeoutHours < 3)
|
||||
{
|
||||
settings.SessionIdleTimeoutHours = 3;
|
||||
}
|
||||
}
|
||||
|
||||
private static bool HasValidSiteArray(string? json, params string[] requiredKeys)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(json))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
using var document = JsonDocument.Parse(json);
|
||||
if (document.RootElement.ValueKind != JsonValueKind.Array)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return document.RootElement.EnumerateArray().Any(item =>
|
||||
item.ValueKind == JsonValueKind.Object
|
||||
&& requiredKeys.All(key =>
|
||||
item.TryGetProperty(key, out var value)
|
||||
&& value.ValueKind == JsonValueKind.String
|
||||
&& !string.IsNullOrWhiteSpace(value.GetString())));
|
||||
}
|
||||
catch (JsonException)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static bool HasValidRiskRules(string? json) =>
|
||||
RiskRuleSettings.Read(new Backend.Domain.SiteSettings { RiskRulesJson = json ?? string.Empty }).Length == RiskRuleSettings.Defaults.Length;
|
||||
|
||||
private static bool HasValidWorkflowRules(string? json) =>
|
||||
WorkflowRuleSettings.Read(new Backend.Domain.SiteSettings { WorkflowRulesJson = json ?? string.Empty }).Length == WorkflowRuleSettings.Defaults.Length;
|
||||
|
||||
private static bool HasValidTrackingRules(string? json)
|
||||
{
|
||||
var rules = TrackingRulesSettings.Read(new Backend.Domain.SiteSettings { TrackingRulesJson = json ?? string.Empty });
|
||||
return rules.ImportantMetrics.Length == TrackingRulesSettings.DefaultImportantMetrics.Length
|
||||
&& rules.OptionalMetrics.Length == TrackingRulesSettings.DefaultOptionalMetrics.Length
|
||||
&& rules.Flags.Length == TrackingRulesSettings.DefaultFlags.Length;
|
||||
}
|
||||
|
||||
private static bool HasValidNominationLinkBlacklist(string? json)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(json))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
using var document = JsonDocument.Parse(json);
|
||||
return document.RootElement.ValueKind == JsonValueKind.Array
|
||||
&& document.RootElement.EnumerateArray().Any(item =>
|
||||
item.ValueKind == JsonValueKind.Object
|
||||
&& item.TryGetProperty("Url", out var url)
|
||||
&& url.ValueKind == JsonValueKind.String
|
||||
&& !string.IsNullOrWhiteSpace(url.GetString()));
|
||||
}
|
||||
catch (JsonException)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
using Backend.Domain;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Backend.Data;
|
||||
|
||||
public static partial class SeedDataBootstrapper
|
||||
{
|
||||
private static async Task EnsureSponsorsAsync(AwardsDbContext db, Season season)
|
||||
{
|
||||
var existingSponsors = await db.Sponsors
|
||||
.Where(item => item.SeasonId == season.Id)
|
||||
.ToListAsync();
|
||||
|
||||
foreach (var seed in SeedCatalog.DemoSponsorSeeds)
|
||||
{
|
||||
var sponsor = existingSponsors.FirstOrDefault(item =>
|
||||
string.Equals(item.Name, seed.Name, StringComparison.OrdinalIgnoreCase))
|
||||
?? new Sponsor
|
||||
{
|
||||
SeasonId = season.Id,
|
||||
CreatedAt = DateTimeOffset.UtcNow,
|
||||
};
|
||||
|
||||
sponsor.Name = seed.Name;
|
||||
sponsor.WebsiteUrl = seed.WebsiteUrl;
|
||||
sponsor.LogoUrl = seed.LogoUrl;
|
||||
sponsor.Description = seed.Description;
|
||||
sponsor.Tier = seed.Tier;
|
||||
sponsor.SortOrder = seed.SortOrder;
|
||||
sponsor.IsVisible = true;
|
||||
sponsor.UpdatedAt = DateTimeOffset.UtcNow;
|
||||
|
||||
if (sponsor.Id == 0)
|
||||
{
|
||||
db.Sponsors.Add(sponsor);
|
||||
existingSponsors.Add(sponsor);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Backend.Data;
|
||||
|
||||
public static class SessionBootstrapper
|
||||
{
|
||||
public static Task EnsureAsync(AwardsDbContext db) =>
|
||||
db.Database.ExecuteSqlRawAsync(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS "UserSessions" (
|
||||
"Id" uuid NOT NULL PRIMARY KEY,
|
||||
"SessionToken" character varying(120) NOT NULL,
|
||||
"TwitchUserId" character varying(120) NOT NULL,
|
||||
"DisplayName" character varying(120) NOT NULL,
|
||||
"Role" character varying(40) NOT NULL,
|
||||
"CreatedAt" timestamp with time zone NOT NULL,
|
||||
"LastSeenAt" timestamp with time zone NOT NULL,
|
||||
"IsActive" boolean NOT NULL
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS "IX_UserSessions_SessionToken"
|
||||
ON "UserSessions" ("SessionToken");
|
||||
""");
|
||||
}
|
||||
Reference in New Issue
Block a user