add gif-Seeder and gif-Templates:

Zusammenfassung der Änderungen

  Template-GIF Seeding implementiert:

  1. DemoSeeder.cs aktualisiert:
  - SeedTemplateGifsAsync() - Öffentliche Methode für externes Seeding
  - SeedTemplateGifsInternalAsync() - Kopiert GIFs vom Template-Ordner und erstellt DB-Einträge
  - DeleteClubGifsAsync() - Löscht GIF-Dateien und DB-Einträge beim Reset
  - ResetDemoClubAsync() - Löscht nun auch GIFs und seedet sie neu
  - SeedAsync() - Seedet Template-GIFs beim Demo-Club-Setup

  2. IClubGifService.cs erweitert:
  - SeedTemplateGifsAsync(Guid clubId) - Neue Interface-Methode

  3. ClubGifService.cs erweitert:
  - SeedTemplateGifsAsync() implementiert - Liest giftemplates.json, kopiert Dateien und erstellt ClubGif-Einträge

  4. ClubService.cs aktualisiert:
  - Injiziert IClubGifService
  - CreateAsync() seedet automatisch Template-GIFs für neue Clubs

  Ablauf:

  1. Neuer Club: Template-GIFs werden automatisch kopiert nach wwwroot/club-media/{LoginName}/gifs/
  2. Demo-Reset: Alte GIFs werden gelöscht, Templates neu kopiert
  3. Demo-Seed: Template-GIFs werden geseedet falls noch keine vorhanden

  Template-Struktur:

  wwwroot/club-template/gifs/
  ├── giftemplates.json        # Definition der GIFs
  ├── 44f5bc11-...-d1215.gif   # Strike GIF
  └── 297970f3-...-a2a93.gif   # NoWood GIF
This commit is contained in:
beo3000 2025-12-30 18:00:11 +01:00
parent a3a5c288a1
commit 9aee6b50c0
7 changed files with 278 additions and 4 deletions

View File

@ -36,4 +36,11 @@ public interface IClubGifService
// Anonymous Submission
Task<ClubGifDto> SubmitAnonymousAsync(string token, IFormFile file, string name, CancellationToken ct = default);
// Template Seeding
/// <summary>
/// Seeds template GIFs from club-template folder to a club.
/// Should be called after creating a new club.
/// </summary>
Task<int> SeedTemplateGifsAsync(Guid clubId, CancellationToken ct = default);
}

View File

@ -1,3 +1,4 @@
using System.Text.Json;
using Koogle.Application.DTOs;
using Koogle.Application.Interfaces;
using Koogle.Domain.Entities;
@ -17,17 +18,25 @@ public class ClubGifService : IClubGifService
private readonly IClubRepository _clubRepository;
private readonly IMediaStorageService _mediaStorage;
private readonly ICurrentClubContext _clubContext;
private readonly IWebHostEnvironment _env;
/// <summary>
/// DTO for deserializing GIF template entries from giftemplates.json.
/// </summary>
private record GifTemplateEntry(string Name, string Filename, string ThrowEventType, string? Description);
public ClubGifService(
IClubGifRepository repository,
IClubRepository clubRepository,
IMediaStorageService mediaStorage,
ICurrentClubContext clubContext)
ICurrentClubContext clubContext,
IWebHostEnvironment env)
{
_repository = repository;
_clubRepository = clubRepository;
_mediaStorage = mediaStorage;
_clubContext = clubContext;
_env = env;
}
public async Task<ClubGifDto?> GetByIdAsync(Guid id, CancellationToken ct = default)
@ -276,6 +285,92 @@ public class ClubGifService : IClubGifService
return MapToDto(gif, club.LoginName);
}
public async Task<int> SeedTemplateGifsAsync(Guid clubId, CancellationToken ct = default)
{
var club = await _clubRepository.GetByIdAsync(clubId, ct)
?? throw new ArgumentException("Club not found");
var templatePath = Path.Combine(_env.WebRootPath, "club-template", "gifs");
var templateJsonPath = Path.Combine(templatePath, "giftemplates.json");
if (!File.Exists(templateJsonPath))
{
return 0; // No template file, skip seeding
}
// Read template definitions
var jsonContent = await File.ReadAllTextAsync(templateJsonPath, ct);
var templates = JsonSerializer.Deserialize<List<GifTemplateEntry>>(jsonContent, new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true
});
if (templates == null || templates.Count == 0)
{
return 0;
}
// Ensure target directory exists
_mediaStorage.EnsureClubDirectoryExists(club.LoginName);
var seededCount = 0;
foreach (var template in templates)
{
var sourceFile = Path.Combine(templatePath, template.Filename);
if (!File.Exists(sourceFile))
{
continue; // Skip if source file doesn't exist
}
// Parse event type
if (!Enum.TryParse<ThrowEventType>(template.ThrowEventType, out var eventType))
{
eventType = ThrowEventType.None;
}
// Determine content type
var extension = Path.GetExtension(template.Filename).ToLowerInvariant();
var contentType = extension switch
{
".gif" => "image/gif",
".mp4" => "video/mp4",
".webm" => "video/webm",
_ => "application/octet-stream"
};
// Copy file via media storage service
await using var sourceStream = new FileStream(sourceFile, FileMode.Open, FileAccess.Read);
var newFileName = await _mediaStorage.SaveGifAsync(club.LoginName, sourceStream, template.Filename, contentType, ct);
// Get file size
var targetFile = Path.Combine(_env.WebRootPath, "club-media", club.LoginName, "gifs", newFileName);
var fileInfo = new FileInfo(targetFile);
// Create database entry
var gif = new ClubGif
{
ClubId = clubId,
Name = template.Name,
FileName = newFileName,
OriginalFileName = template.Filename,
ContentType = contentType,
FileSizeBytes = fileInfo.Length,
AssignedEvents = eventType,
Description = template.Description,
IsEnabled = true,
IsPendingApproval = false,
RatingScore = 0,
RatingCount = 0
};
await _repository.AddAsync(gif, ct);
seededCount++;
}
return seededCount;
}
private static ClubGif? SelectWeightedRandom(List<ClubGif> gifs)
{
if (gifs.Count == 0) return null;

View File

@ -18,6 +18,7 @@ public class ClubService : IClubService
private readonly IClubRepository _clubRepository;
private readonly IMapper _mapper;
private readonly AppDbContext _appDb;
private readonly IClubGifService _gifService;
/// <summary>
/// Initializes a new instance of the <see cref="ClubService"/> class.
@ -25,11 +26,13 @@ public class ClubService : IClubService
/// <param name="clubRepository">The club repository.</param>
/// <param name="mapper">The AutoMapper instance.</param>
/// <param name="appDb">The application database context.</param>
public ClubService(IClubRepository clubRepository, IMapper mapper, AppDbContext appDb)
/// <param name="gifService">The GIF service for seeding templates.</param>
public ClubService(IClubRepository clubRepository, IMapper mapper, AppDbContext appDb, IClubGifService gifService)
{
_clubRepository = clubRepository;
_mapper = mapper;
_appDb = appDb;
_gifService = gifService;
}
/// <inheritdoc />
@ -72,6 +75,17 @@ public class ClubService : IClubService
};
var created = await _clubRepository.AddAsync(entity, ct);
// Seed template GIFs for new club
try
{
await _gifService.SeedTemplateGifsAsync(created.Id, ct);
}
catch
{
// Don't fail club creation if GIF seeding fails
}
return _mapper.Map<ClubDto>(created);
}

View File

@ -1,8 +1,10 @@
using System.Text.Json;
using Koogle.Domain.Entities;
using Koogle.Domain.Enums;
using Koogle.Domain.Interfaces;
using Koogle.Infrastructure.Identity;
using KoogleApp.Data;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
@ -24,6 +26,11 @@ public static class DemoSeeder
private static readonly string[] GuestNames = { "Stefan Gast", "Thomas Besucher" };
/// <summary>
/// DTO for deserializing GIF template entries from giftemplates.json.
/// </summary>
private record GifTemplateEntry(string Name, string Filename, string ThrowEventType, string? Description);
/// <summary>
/// Seeds demo user, club membership, persons, expenses, triggers and sample days.
/// </summary>
@ -130,6 +137,14 @@ public static class DemoSeeder
{
await SeedDemoDataAsync(appDb, triggerRepo, club.Id, demoUser.Id);
}
// 7) Seed template GIFs if not already present
var webEnv = scope.ServiceProvider.GetRequiredService<IWebHostEnvironment>();
var hasGifs = await appDb.ClubGifs.AnyAsync(g => g.ClubId == club.Id && !g.IsDeleted);
if (!hasGifs)
{
await SeedTemplateGifsInternalAsync(appDb, webEnv, club.Id, club.LoginName);
}
}
/// <summary>
@ -140,19 +155,24 @@ public static class DemoSeeder
using var scope = services.CreateScope();
var appDb = scope.ServiceProvider.GetRequiredService<AppDbContext>();
var triggerRepo = scope.ServiceProvider.GetRequiredService<ITriggerRepository>();
var env = scope.ServiceProvider.GetRequiredService<IWebHostEnvironment>();
// Find creator ID for seeding
// Find creator ID and club for seeding
var club = await appDb.Clubs.FindAsync(clubId);
if (club == null) return;
var membership = await appDb.UserProfileClubs.FirstOrDefaultAsync(m => m.ClubId == clubId);
var creatorId = membership?.AssignedById ?? Guid.Empty;
// Hard delete all club data
// Hard delete all club data including GIFs
await DeleteClubGifsAsync(appDb, env, clubId, club.LoginName);
await HardDeleteClubDataAsync(appDb, clubId);
// Re-seed demo data
await SeedDemoDataAsync(appDb, triggerRepo, clubId, creatorId);
// Re-seed template GIFs
await SeedTemplateGifsInternalAsync(appDb, env, clubId, club.LoginName);
}
private static async Task HardDeleteClubDataAsync(AppDbContext db, Guid clubId)
@ -404,4 +424,128 @@ public static class DemoSeeder
}
await db.SaveChangesAsync();
}
/// <summary>
/// Seeds template GIFs for a club by copying files and creating database entries.
/// Called during demo reset and when creating new clubs.
/// </summary>
/// <param name="services">Service provider.</param>
/// <param name="clubId">Club ID to seed GIFs for.</param>
/// <param name="clubLoginName">Club's login name for folder path.</param>
public static async Task SeedTemplateGifsAsync(IServiceProvider services, Guid clubId, string clubLoginName)
{
using var scope = services.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
var env = scope.ServiceProvider.GetRequiredService<IWebHostEnvironment>();
await SeedTemplateGifsInternalAsync(db, env, clubId, clubLoginName);
}
private static async Task SeedTemplateGifsInternalAsync(
AppDbContext db,
IWebHostEnvironment env,
Guid clubId,
string clubLoginName)
{
var templatePath = Path.Combine(env.WebRootPath, "club-template", "gifs");
var templateJsonPath = Path.Combine(templatePath, "giftemplates.json");
if (!File.Exists(templateJsonPath))
{
return; // No template file, skip seeding
}
// Read template definitions
var jsonContent = await File.ReadAllTextAsync(templateJsonPath);
var templates = JsonSerializer.Deserialize<List<GifTemplateEntry>>(jsonContent, new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true
});
if (templates == null || templates.Count == 0)
{
return;
}
// Create target directory
var targetPath = Path.Combine(env.WebRootPath, "club-media", clubLoginName, "gifs");
Directory.CreateDirectory(targetPath);
var now = DateTime.UtcNow;
foreach (var template in templates)
{
var sourceFile = Path.Combine(templatePath, template.Filename);
if (!File.Exists(sourceFile))
{
continue; // Skip if source file doesn't exist
}
// Generate new filename for club
var extension = Path.GetExtension(template.Filename);
var newFileName = $"{Guid.NewGuid()}{extension}";
var targetFile = Path.Combine(targetPath, newFileName);
// Copy file
File.Copy(sourceFile, targetFile, overwrite: true);
// Parse event type
if (!Enum.TryParse<ThrowEventType>(template.ThrowEventType, out var eventType))
{
eventType = ThrowEventType.None;
}
// Get file info
var fileInfo = new FileInfo(targetFile);
// Determine content type
var contentType = extension.ToLowerInvariant() switch
{
".gif" => "image/gif",
".mp4" => "video/mp4",
".webm" => "video/webm",
_ => "application/octet-stream"
};
// Create database entry
var gif = new ClubGif
{
ClubId = clubId,
Name = template.Name,
FileName = newFileName,
OriginalFileName = template.Filename,
ContentType = contentType,
FileSizeBytes = fileInfo.Length,
AssignedEvents = eventType,
Description = template.Description,
IsEnabled = true,
IsPendingApproval = false,
RatingScore = 0,
RatingCount = 0,
CreatedAt = now
};
db.ClubGifs.Add(gif);
}
await db.SaveChangesAsync();
}
/// <summary>
/// Deletes all GIF files and database entries for a club.
/// </summary>
private static async Task DeleteClubGifsAsync(AppDbContext db, IWebHostEnvironment env, Guid clubId, string clubLoginName)
{
// Delete database entries
await db.Database.ExecuteSqlAsync($"DELETE FROM app.ClubGifRatings WHERE ClubGifId IN (SELECT Id FROM app.ClubGifs WHERE ClubId = {clubId})");
await db.Database.ExecuteSqlAsync($"DELETE FROM app.ClubGifs WHERE ClubId = {clubId}");
await db.Database.ExecuteSqlAsync($"DELETE FROM app.GifSubmissionTokens WHERE ClubId = {clubId}");
// Delete files
var gifPath = Path.Combine(env.WebRootPath, "club-media", clubLoginName, "gifs");
if (Directory.Exists(gifPath))
{
Directory.Delete(gifPath, recursive: true);
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1016 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 MiB

View File

@ -0,0 +1,14 @@
[
{
"name": "alle Neune 1",
"filename": "44f5bc11-8471-4ca9-b764-58e3929d1215.gif",
"ThrowEventType": "Strike",
"description": "Animation, alle Kegel werden getroffen"
},
{
"name": "kein Holz 1",
"filename": "297970f3-1bf1-4090-aa5e-d0f5cea42a93.gif",
"ThrowEventType": "NoWood",
"description": "Animation, Kegel springen zur Seite"
}
]