229 lines
8.2 KiB
TypeScript
229 lines
8.2 KiB
TypeScript
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;
|
|
}
|
|
}
|