diff --git a/src/Koogle.Application/Interfaces/IClubGifService.cs b/src/Koogle.Application/Interfaces/IClubGifService.cs index c80a9ef..3e5c88a 100644 --- a/src/Koogle.Application/Interfaces/IClubGifService.cs +++ b/src/Koogle.Application/Interfaces/IClubGifService.cs @@ -36,4 +36,11 @@ public interface IClubGifService // Anonymous Submission Task SubmitAnonymousAsync(string token, IFormFile file, string name, CancellationToken ct = default); + + // Template Seeding + /// + /// Seeds template GIFs from club-template folder to a club. + /// Should be called after creating a new club. + /// + Task SeedTemplateGifsAsync(Guid clubId, CancellationToken ct = default); } diff --git a/src/Koogle.Application/Services/ClubGifService.cs b/src/Koogle.Application/Services/ClubGifService.cs index 8d64d63..2a3ff0e 100644 --- a/src/Koogle.Application/Services/ClubGifService.cs +++ b/src/Koogle.Application/Services/ClubGifService.cs @@ -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; + + /// + /// DTO for deserializing GIF template entries from giftemplates.json. + /// + 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 GetByIdAsync(Guid id, CancellationToken ct = default) @@ -276,6 +285,92 @@ public class ClubGifService : IClubGifService return MapToDto(gif, club.LoginName); } + public async Task 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>(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(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 gifs) { if (gifs.Count == 0) return null; diff --git a/src/Koogle.Application/Services/ClubService.cs b/src/Koogle.Application/Services/ClubService.cs index 54c40c9..2415989 100644 --- a/src/Koogle.Application/Services/ClubService.cs +++ b/src/Koogle.Application/Services/ClubService.cs @@ -18,6 +18,7 @@ public class ClubService : IClubService private readonly IClubRepository _clubRepository; private readonly IMapper _mapper; private readonly AppDbContext _appDb; + private readonly IClubGifService _gifService; /// /// Initializes a new instance of the class. @@ -25,11 +26,13 @@ public class ClubService : IClubService /// The club repository. /// The AutoMapper instance. /// The application database context. - public ClubService(IClubRepository clubRepository, IMapper mapper, AppDbContext appDb) + /// The GIF service for seeding templates. + public ClubService(IClubRepository clubRepository, IMapper mapper, AppDbContext appDb, IClubGifService gifService) { _clubRepository = clubRepository; _mapper = mapper; _appDb = appDb; + _gifService = gifService; } /// @@ -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(created); } diff --git a/src/Koogle.Infrastructure/Data/DemoSeeder.cs b/src/Koogle.Infrastructure/Data/DemoSeeder.cs index 6432661..2308c5c 100644 --- a/src/Koogle.Infrastructure/Data/DemoSeeder.cs +++ b/src/Koogle.Infrastructure/Data/DemoSeeder.cs @@ -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" }; + /// + /// DTO for deserializing GIF template entries from giftemplates.json. + /// + private record GifTemplateEntry(string Name, string Filename, string ThrowEventType, string? Description); + /// /// Seeds demo user, club membership, persons, expenses, triggers and sample days. /// @@ -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(); + var hasGifs = await appDb.ClubGifs.AnyAsync(g => g.ClubId == club.Id && !g.IsDeleted); + if (!hasGifs) + { + await SeedTemplateGifsInternalAsync(appDb, webEnv, club.Id, club.LoginName); + } } /// @@ -140,19 +155,24 @@ public static class DemoSeeder using var scope = services.CreateScope(); var appDb = scope.ServiceProvider.GetRequiredService(); var triggerRepo = scope.ServiceProvider.GetRequiredService(); + var env = scope.ServiceProvider.GetRequiredService(); - // 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(); } + + /// + /// Seeds template GIFs for a club by copying files and creating database entries. + /// Called during demo reset and when creating new clubs. + /// + /// Service provider. + /// Club ID to seed GIFs for. + /// Club's login name for folder path. + public static async Task SeedTemplateGifsAsync(IServiceProvider services, Guid clubId, string clubLoginName) + { + using var scope = services.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService(); + var env = scope.ServiceProvider.GetRequiredService(); + + 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>(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(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(); + } + + /// + /// Deletes all GIF files and database entries for a club. + /// + 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); + } + } } diff --git a/src/Koogle.Web/wwwroot/club-template/gifs/297970f3-1bf1-4090-aa5e-d0f5cea42a93.gif b/src/Koogle.Web/wwwroot/club-template/gifs/297970f3-1bf1-4090-aa5e-d0f5cea42a93.gif new file mode 100644 index 0000000..169e2a7 Binary files /dev/null and b/src/Koogle.Web/wwwroot/club-template/gifs/297970f3-1bf1-4090-aa5e-d0f5cea42a93.gif differ diff --git a/src/Koogle.Web/wwwroot/club-template/gifs/44f5bc11-8471-4ca9-b764-58e3929d1215.gif b/src/Koogle.Web/wwwroot/club-template/gifs/44f5bc11-8471-4ca9-b764-58e3929d1215.gif new file mode 100644 index 0000000..4bd66d6 Binary files /dev/null and b/src/Koogle.Web/wwwroot/club-template/gifs/44f5bc11-8471-4ca9-b764-58e3929d1215.gif differ diff --git a/src/Koogle.Web/wwwroot/club-template/gifs/giftemplates.json b/src/Koogle.Web/wwwroot/club-template/gifs/giftemplates.json new file mode 100644 index 0000000..043f32e --- /dev/null +++ b/src/Koogle.Web/wwwroot/club-template/gifs/giftemplates.json @@ -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" + } +] \ No newline at end of file