246 lines
7.9 KiB
TypeScript
246 lines
7.9 KiB
TypeScript
import { PrismaClient } from '@prisma/client';
|
|
import { getTodayISOString } from './dateUtils';
|
|
|
|
const prisma = new PrismaClient();
|
|
|
|
export async function getOrCreateDailyPuzzle(genreName: string | null = null) {
|
|
try {
|
|
const today = getTodayISOString();
|
|
let genreId: number | null = null;
|
|
|
|
if (genreName) {
|
|
const genre = await prisma.genre.findUnique({
|
|
where: { name: genreName }
|
|
});
|
|
if (genre) {
|
|
genreId = genre.id;
|
|
} else {
|
|
return null; // Genre not found
|
|
}
|
|
}
|
|
|
|
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: {
|
|
where: { genreId: genreId }
|
|
},
|
|
},
|
|
});
|
|
|
|
if (allSongs.length === 0) {
|
|
console.log(`[Daily Puzzle] No songs available for genre: ${genreName || 'Global'}`);
|
|
return null;
|
|
}
|
|
|
|
// Calculate weights
|
|
const weightedSongs = allSongs.map(song => ({
|
|
song,
|
|
weight: 1.0 / (song.puzzles.length + 1),
|
|
}));
|
|
|
|
// Calculate total weight
|
|
const totalWeight = weightedSongs.reduce((sum, item) => sum + item.weight, 0);
|
|
|
|
// Pick a random song based on weights
|
|
let random = Math.random() * totalWeight;
|
|
let selectedSong = weightedSongs[0].song;
|
|
|
|
for (const item of weightedSongs) {
|
|
random -= item.weight;
|
|
if (random <= 0) {
|
|
selectedSong = item.song;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// 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: ${genreName || '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: genreName
|
|
};
|
|
|
|
} catch (error) {
|
|
console.error('Error in getOrCreateDailyPuzzle:', error);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
export async function getOrCreateSpecialPuzzle(specialName: string) {
|
|
try {
|
|
const today = getTodayISOString();
|
|
|
|
const special = await prisma.special.findUnique({
|
|
where: { name: specialName }
|
|
});
|
|
|
|
if (!special) return null;
|
|
|
|
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 }
|
|
}
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
if (specialSongs.length === 0) return null;
|
|
|
|
// Calculate weights
|
|
const weightedSongs = specialSongs.map(specialSong => ({
|
|
specialSong,
|
|
weight: 1.0 / (specialSong.song.puzzles.length + 1),
|
|
}));
|
|
|
|
const totalWeight = weightedSongs.reduce((sum, item) => sum + item.weight, 0);
|
|
let random = Math.random() * totalWeight;
|
|
let selectedSpecialSong = weightedSongs[0].specialSong;
|
|
|
|
for (const item of weightedSongs) {
|
|
random -= item.weight;
|
|
if (random <= 0) {
|
|
selectedSpecialSong = item.specialSong;
|
|
break;
|
|
}
|
|
}
|
|
|
|
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: specialName,
|
|
maxAttempts: special.maxAttempts,
|
|
unlockSteps: JSON.parse(special.unlockSteps),
|
|
startTime: specialSong?.startTime || 0
|
|
};
|
|
|
|
} catch (error) {
|
|
console.error('Error in getOrCreateSpecialPuzzle:', error);
|
|
return null;
|
|
}
|
|
}
|