import { PrismaClient, Genre, Special } from '@prisma/client'; import { getTodayISOString } from './dateUtils'; const prisma = new PrismaClient(); export async function getOrCreateDailyPuzzle(genre: Genre | null = null) { try { const today = getTodayISOString(); let genreId: number | null = null; if (genre) { genreId = genre.id; } let dailyPuzzle = await prisma.dailyPuzzle.findFirst({ where: { date: today, genreId: genreId }, include: { song: true }, }); if (!dailyPuzzle) { // Get songs available for this genre const whereClause = genreId ? { genres: { some: { id: genreId } } } : { excludeFromGlobal: false }; // Global puzzle picks from ALL songs (except excluded) const allSongs = await prisma.song.findMany({ where: whereClause, include: { puzzles: true, // Load ALL puzzles, not just for this genre (to use total activations) }, }); if (allSongs.length === 0) { console.log(`[Daily Puzzle] No songs available for genre: ${genre ? JSON.stringify(genre.name) : 'Global'}`); return null; } // Find songs with the minimum number of activations (all puzzles, not just for this genre) // Only select from songs with the fewest activations to ensure fair distribution const songsWithActivations = allSongs.map(song => ({ song, activations: song.puzzles.length, })); // Find minimum activations const minActivations = Math.min(...songsWithActivations.map(item => item.activations)); // Filter to only songs with minimum activations const songsWithMinActivations = songsWithActivations .filter(item => item.activations === minActivations) .map(item => item.song); // Randomly select from songs with minimum activations const randomIndex = Math.floor(Math.random() * songsWithMinActivations.length); const selectedSong = songsWithMinActivations[randomIndex]; // Create the daily puzzle try { dailyPuzzle = await prisma.dailyPuzzle.create({ data: { date: today, songId: selectedSong.id, genreId: genreId }, include: { song: true }, }); console.log(`[Daily Puzzle] Created new puzzle for ${today} (Genre: ${genre ? JSON.stringify(genre.name) : 'Global'}) with song: ${selectedSong.title}`); } catch (e) { // Handle race condition console.log('[Daily Puzzle] Creation failed, trying to fetch again (likely race condition)'); dailyPuzzle = await prisma.dailyPuzzle.findFirst({ where: { date: today, genreId: genreId }, include: { song: true }, }); } } if (!dailyPuzzle) return null; // Calculate puzzle number (sequential day count) const whereClause = genreId ? { genreId: genreId } : { genreId: null, specialId: null }; const puzzleCount = await prisma.dailyPuzzle.count({ where: { ...whereClause, date: { lte: dailyPuzzle.date } } }); return { id: dailyPuzzle.id, puzzleNumber: puzzleCount, audioUrl: `/api/audio/${dailyPuzzle.song.filename}`, songId: dailyPuzzle.songId, title: dailyPuzzle.song.title, artist: dailyPuzzle.song.artist, coverImage: dailyPuzzle.song.coverImage ? `/uploads/covers/${dailyPuzzle.song.coverImage}` : null, releaseYear: dailyPuzzle.song.releaseYear, genre: genre ? genre.name : null }; } catch (error) { console.error('Error in getOrCreateDailyPuzzle:', error); return null; } } export async function getOrCreateSpecialPuzzle(special: Special) { try { const today = getTodayISOString(); let dailyPuzzle = await prisma.dailyPuzzle.findFirst({ where: { date: today, specialId: special.id }, include: { song: true }, }); if (!dailyPuzzle) { // Get songs available for this special through SpecialSong const specialSongs = await prisma.specialSong.findMany({ where: { specialId: special.id }, include: { song: { include: { puzzles: { where: { specialId: special.id } // For specials, only count puzzles within this special } } } } }); if (specialSongs.length === 0) return null; // Find songs with the minimum number of activations within this special // Note: For specials, we only count puzzles within the special (not all puzzles), // since specials are curated, separate lists const songsWithActivations = specialSongs.map(specialSong => ({ specialSong, activations: specialSong.song.puzzles.length, })); // Find minimum activations const minActivations = Math.min(...songsWithActivations.map(item => item.activations)); // Filter to only songs with minimum activations const songsWithMinActivations = songsWithActivations .filter(item => item.activations === minActivations) .map(item => item.specialSong); // Randomly select from songs with minimum activations const randomIndex = Math.floor(Math.random() * songsWithMinActivations.length); const selectedSpecialSong = songsWithMinActivations[randomIndex]; try { dailyPuzzle = await prisma.dailyPuzzle.create({ data: { date: today, songId: selectedSpecialSong.songId, specialId: special.id }, include: { song: true }, }); } catch (e) { dailyPuzzle = await prisma.dailyPuzzle.findFirst({ where: { date: today, specialId: special.id }, include: { song: true }, }); } } if (!dailyPuzzle) return null; // Fetch the startTime from SpecialSong const specialSong = await prisma.specialSong.findUnique({ where: { specialId_songId: { specialId: special.id, songId: dailyPuzzle.songId } } }); // Calculate puzzle number const puzzleCount = await prisma.dailyPuzzle.count({ where: { specialId: special.id, date: { lte: dailyPuzzle.date } } }); return { id: dailyPuzzle.id, puzzleNumber: puzzleCount, audioUrl: `/api/audio/${dailyPuzzle.song.filename}`, songId: dailyPuzzle.songId, title: dailyPuzzle.song.title, artist: dailyPuzzle.song.artist, coverImage: dailyPuzzle.song.coverImage ? `/uploads/covers/${dailyPuzzle.song.coverImage}` : null, releaseYear: dailyPuzzle.song.releaseYear, special: special.name, maxAttempts: special.maxAttempts, unlockSteps: JSON.parse(special.unlockSteps), startTime: specialSong?.startTime || 0 }; } catch (error) { console.error('Error in getOrCreateSpecialPuzzle:', error); return null; } }