Files
hoerdle/lib/dailyPuzzle.ts

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;
}
}