Implement Specials feature, Admin UI enhancements, and Database Rebuild tool
This commit is contained in:
95
app/api/admin/rebuild/route.ts
Normal file
95
app/api/admin/rebuild/route.ts
Normal file
@@ -0,0 +1,95 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
import { parseFile } from 'music-metadata';
|
||||
import path from 'path';
|
||||
import fs from 'fs/promises';
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
export async function POST() {
|
||||
try {
|
||||
console.log('[Rebuild] Starting database rebuild...');
|
||||
|
||||
// 1. Clear Database
|
||||
// Delete in order to respect foreign keys
|
||||
await prisma.dailyPuzzle.deleteMany();
|
||||
// We need to clear the many-to-many relations first implicitly by deleting songs/genres/specials
|
||||
// But explicit deletion of join tables isn't needed with Prisma's cascading deletes usually,
|
||||
// but let's be safe and delete main entities.
|
||||
await prisma.song.deleteMany();
|
||||
await prisma.genre.deleteMany();
|
||||
await prisma.special.deleteMany();
|
||||
|
||||
console.log('[Rebuild] Database cleared.');
|
||||
|
||||
// 2. Clear Covers Directory
|
||||
const coversDir = path.join(process.cwd(), 'public/uploads/covers');
|
||||
try {
|
||||
const coverFiles = await fs.readdir(coversDir);
|
||||
for (const file of coverFiles) {
|
||||
if (file !== '.gitkeep') { // Preserve .gitkeep if it exists
|
||||
await fs.unlink(path.join(coversDir, file));
|
||||
}
|
||||
}
|
||||
console.log('[Rebuild] Covers directory cleared.');
|
||||
} catch (e) {
|
||||
console.log('[Rebuild] Covers directory might not exist or empty, creating it.');
|
||||
await fs.mkdir(coversDir, { recursive: true });
|
||||
}
|
||||
|
||||
// 3. Re-import Songs
|
||||
const uploadsDir = path.join(process.cwd(), 'public/uploads');
|
||||
const files = await fs.readdir(uploadsDir);
|
||||
const mp3Files = files.filter(f => f.endsWith('.mp3'));
|
||||
|
||||
console.log(`[Rebuild] Found ${mp3Files.length} MP3 files to import.`);
|
||||
|
||||
let importedCount = 0;
|
||||
|
||||
for (const filename of mp3Files) {
|
||||
const filePath = path.join(uploadsDir, filename);
|
||||
|
||||
try {
|
||||
const metadata = await parseFile(filePath);
|
||||
|
||||
const title = metadata.common.title || 'Unknown Title';
|
||||
const artist = metadata.common.artist || 'Unknown Artist';
|
||||
|
||||
let coverImage = null;
|
||||
const picture = metadata.common.picture?.[0];
|
||||
|
||||
if (picture) {
|
||||
const extension = picture.format.split('/')[1] || 'jpg';
|
||||
const coverFilename = `cover-${Date.now()}-${Math.random().toString(36).substring(7)}.${extension}`;
|
||||
const coverPath = path.join(coversDir, coverFilename);
|
||||
|
||||
await fs.writeFile(coverPath, picture.data);
|
||||
coverImage = coverFilename;
|
||||
}
|
||||
|
||||
await prisma.song.create({
|
||||
data: {
|
||||
title,
|
||||
artist,
|
||||
filename,
|
||||
coverImage
|
||||
}
|
||||
});
|
||||
importedCount++;
|
||||
} catch (e) {
|
||||
console.error(`[Rebuild] Failed to process ${filename}:`, e);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`[Rebuild] Successfully imported ${importedCount} songs.`);
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: `Database rebuilt. Imported ${importedCount} songs.`
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('[Rebuild] Error:', error);
|
||||
return NextResponse.json({ error: 'Internal Server Error' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,7 @@ export async function GET() {
|
||||
include: {
|
||||
puzzles: true,
|
||||
genres: true,
|
||||
specials: true,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -25,6 +26,7 @@ export async function GET() {
|
||||
coverImage: song.coverImage,
|
||||
activations: song.puzzles.length,
|
||||
genres: song.genres,
|
||||
specials: song.specials,
|
||||
}));
|
||||
|
||||
return NextResponse.json(songsWithActivations);
|
||||
@@ -146,7 +148,7 @@ export async function POST(request: Request) {
|
||||
filename,
|
||||
coverImage,
|
||||
},
|
||||
include: { genres: true }
|
||||
include: { genres: true, specials: true }
|
||||
});
|
||||
|
||||
return NextResponse.json({
|
||||
@@ -161,7 +163,7 @@ export async function POST(request: Request) {
|
||||
|
||||
export async function PUT(request: Request) {
|
||||
try {
|
||||
const { id, title, artist, genreIds } = await request.json();
|
||||
const { id, title, artist, genreIds, specialIds } = await request.json();
|
||||
|
||||
if (!id || !title || !artist) {
|
||||
return NextResponse.json({ error: 'Missing fields' }, { status: 400 });
|
||||
@@ -175,10 +177,16 @@ export async function PUT(request: Request) {
|
||||
};
|
||||
}
|
||||
|
||||
if (specialIds) {
|
||||
data.specials = {
|
||||
set: specialIds.map((sId: number) => ({ id: sId }))
|
||||
};
|
||||
}
|
||||
|
||||
const updatedSong = await prisma.song.update({
|
||||
where: { id: Number(id) },
|
||||
data,
|
||||
include: { genres: true }
|
||||
include: { genres: true, specials: true }
|
||||
});
|
||||
|
||||
return NextResponse.json(updatedSong);
|
||||
|
||||
51
app/api/specials/route.ts
Normal file
51
app/api/specials/route.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import { PrismaClient, Special } from '@prisma/client';
|
||||
import { NextResponse } from 'next/server';
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
export async function GET() {
|
||||
const specials = await prisma.special.findMany({
|
||||
orderBy: { name: 'asc' },
|
||||
});
|
||||
return NextResponse.json(specials);
|
||||
}
|
||||
|
||||
export async function POST(request: Request) {
|
||||
const { name, maxAttempts = 7, unlockSteps = '[2,4,7,11,16,30,60]' } = await request.json();
|
||||
if (!name) {
|
||||
return NextResponse.json({ error: 'Name is required' }, { status: 400 });
|
||||
}
|
||||
const special = await prisma.special.create({
|
||||
data: {
|
||||
name,
|
||||
maxAttempts: Number(maxAttempts),
|
||||
unlockSteps,
|
||||
},
|
||||
});
|
||||
return NextResponse.json(special);
|
||||
}
|
||||
|
||||
export async function DELETE(request: Request) {
|
||||
const { id } = await request.json();
|
||||
if (!id) {
|
||||
return NextResponse.json({ error: 'ID required' }, { status: 400 });
|
||||
}
|
||||
await prisma.special.delete({ where: { id: Number(id) } });
|
||||
return NextResponse.json({ success: true });
|
||||
}
|
||||
|
||||
export async function PUT(request: Request) {
|
||||
const { id, name, maxAttempts, unlockSteps } = await request.json();
|
||||
if (!id) {
|
||||
return NextResponse.json({ error: 'ID required' }, { status: 400 });
|
||||
}
|
||||
const updated = await prisma.special.update({
|
||||
where: { id: Number(id) },
|
||||
data: {
|
||||
...(name && { name }),
|
||||
...(maxAttempts && { maxAttempts: Number(maxAttempts) }),
|
||||
...(unlockSteps && { unlockSteps }),
|
||||
},
|
||||
});
|
||||
return NextResponse.json(updated);
|
||||
}
|
||||
Reference in New Issue
Block a user