From 67cf85dc22f9b441fb2458217f01e762f70f3d97 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=B6rdle=20Bot?= Date: Mon, 24 Nov 2025 19:59:47 +0100 Subject: [PATCH] feat(song): add option to exclude songs from global visibility and improve admin upload validation --- app/admin/page.tsx | 139 ++++++++++++++++-- app/api/songs/route.ts | 29 +++- lib/dailyPuzzle.ts | 2 +- next.config.ts | 1 + prisma/dev.db.bak | Bin 0 -> 90112 bytes .../migration.sql | 20 +++ prisma/schema.prisma | 1 + 7 files changed, 174 insertions(+), 18 deletions(-) create mode 100644 prisma/dev.db.bak create mode 100644 prisma/migrations/20251124182259_add_exclude_from_global/migration.sql diff --git a/app/admin/page.tsx b/app/admin/page.tsx index a68cdf5..83a9836 100644 --- a/app/admin/page.tsx +++ b/app/admin/page.tsx @@ -1,6 +1,6 @@ 'use client'; -import { useState, useEffect } from 'react'; +import { useState, useEffect, useRef } from 'react'; interface Special { @@ -47,6 +47,7 @@ interface Song { specials: Special[]; averageRating: number; ratingCount: number; + excludeFromGlobal: boolean; } type SortField = 'id' | 'title' | 'artist' | 'createdAt' | 'releaseYear'; @@ -95,10 +96,12 @@ export default function AdminPage() { const [editReleaseYear, setEditReleaseYear] = useState(''); const [editGenreIds, setEditGenreIds] = useState([]); const [editSpecialIds, setEditSpecialIds] = useState([]); + const [editExcludeFromGlobal, setEditExcludeFromGlobal] = useState(false); // Post-upload state const [uploadedSong, setUploadedSong] = useState(null); const [uploadGenreIds, setUploadGenreIds] = useState([]); + const [uploadExcludeFromGlobal, setUploadExcludeFromGlobal] = useState(false); // AI Categorization state const [isCategorizing, setIsCategorizing] = useState(false); @@ -123,6 +126,7 @@ export default function AdminPage() { const [dailyPuzzles, setDailyPuzzles] = useState([]); const [playingPuzzleId, setPlayingPuzzleId] = useState(null); const [showDailyPuzzles, setShowDailyPuzzles] = useState(false); + const fileInputRef = useRef(null); // Check for existing auth on mount useEffect(() => { @@ -478,8 +482,11 @@ export default function AdminPage() { setUploadProgress({ current: i + 1, total: files.length }); try { + console.log(`Uploading file ${i + 1}/${files.length}: ${file.name} (${(file.size / 1024 / 1024).toFixed(2)}MB)`); + const formData = new FormData(); formData.append('file', file); + formData.append('excludeFromGlobal', String(uploadExcludeFromGlobal)); const res = await fetch('/api/songs', { method: 'POST', @@ -487,8 +494,11 @@ export default function AdminPage() { body: formData, }); + console.log(`Response status for ${file.name}: ${res.status}`); + if (res.ok) { const data = await res.json(); + console.log(`Upload successful for ${file.name}:`, data); results.push({ filename: file.name, success: true, @@ -498,6 +508,7 @@ export default function AdminPage() { } else if (res.status === 409) { // Duplicate detected const data = await res.json(); + console.log(`Duplicate detected for ${file.name}:`, data); results.push({ filename: file.name, success: false, @@ -506,17 +517,20 @@ export default function AdminPage() { error: `Duplicate: Already exists as "${data.duplicate.title}" by "${data.duplicate.artist}"` }); } else { + const errorText = await res.text(); + console.error(`Upload failed for ${file.name} (${res.status}):`, errorText); results.push({ filename: file.name, success: false, - error: 'Upload failed' + error: `Upload failed (${res.status}): ${errorText.substring(0, 100)}` }); } } catch (error) { + console.error(`Network error for ${file.name}:`, error); results.push({ filename: file.name, success: false, - error: 'Network error' + error: `Network error: ${error instanceof Error ? error.message : 'Unknown error'}` }); } } @@ -553,32 +567,81 @@ export default function AdminPage() { } }; + const handleDragEnter = (e: React.DragEvent) => { + e.preventDefault(); + e.stopPropagation(); + setIsDragging(true); + }; + const handleDragOver = (e: React.DragEvent) => { e.preventDefault(); - setIsDragging(true); + e.stopPropagation(); + e.dataTransfer.dropEffect = 'copy'; + if (!isDragging) setIsDragging(true); }; const handleDragLeave = (e: React.DragEvent) => { e.preventDefault(); + e.stopPropagation(); + + // Prevent flickering when dragging over children + if (e.currentTarget.contains(e.relatedTarget as Node)) { + return; + } setIsDragging(false); }; const handleDrop = (e: React.DragEvent) => { e.preventDefault(); + e.stopPropagation(); setIsDragging(false); - const droppedFiles = Array.from(e.dataTransfer.files).filter( - file => file.type === 'audio/mpeg' || file.name.endsWith('.mp3') - ); + const droppedFiles = Array.from(e.dataTransfer.files); - if (droppedFiles.length > 0) { - setFiles(droppedFiles); + // Validate file types + const validFiles: File[] = []; + const invalidFiles: string[] = []; + + droppedFiles.forEach(file => { + if (file.type === 'audio/mpeg' || file.name.toLowerCase().endsWith('.mp3')) { + validFiles.push(file); + } else { + invalidFiles.push(`${file.name} (${file.type || 'unknown type'})`); + } + }); + + if (invalidFiles.length > 0) { + alert(`⚠️ The following files are not supported:\n\n${invalidFiles.join('\n')}\n\nOnly MP3 files are allowed.`); + } + + if (validFiles.length > 0) { + setFiles(validFiles); } }; const handleFileChange = (e: React.ChangeEvent) => { if (e.target.files) { - setFiles(Array.from(e.target.files)); + const selectedFiles = Array.from(e.target.files); + + // Validate file types + const validFiles: File[] = []; + const invalidFiles: string[] = []; + + selectedFiles.forEach(file => { + if (file.type === 'audio/mpeg' || file.name.toLowerCase().endsWith('.mp3')) { + validFiles.push(file); + } else { + invalidFiles.push(`${file.name} (${file.type || 'unknown type'})`); + } + }); + + if (invalidFiles.length > 0) { + alert(`⚠️ The following files are not supported:\n\n${invalidFiles.join('\n')}\n\nOnly MP3 files are allowed.`); + } + + if (validFiles.length > 0) { + setFiles(validFiles); + } } }; @@ -615,6 +678,7 @@ export default function AdminPage() { setEditReleaseYear(song.releaseYear || ''); setEditGenreIds(song.genres.map(g => g.id)); setEditSpecialIds(song.specials ? song.specials.map(s => s.id) : []); + setEditExcludeFromGlobal(song.excludeFromGlobal || false); }; const cancelEditing = () => { @@ -624,6 +688,7 @@ export default function AdminPage() { setEditReleaseYear(''); setEditGenreIds([]); setEditSpecialIds([]); + setEditExcludeFromGlobal(false); }; const saveEditing = async (id: number) => { @@ -636,7 +701,8 @@ export default function AdminPage() { artist: editArtist, releaseYear: editReleaseYear === '' ? null : Number(editReleaseYear), genreIds: editGenreIds, - specialIds: editSpecialIds + specialIds: editSpecialIds, + excludeFromGlobal: editExcludeFromGlobal }), }); @@ -739,6 +805,8 @@ export default function AdminPage() { } else if (selectedGenreFilter === 'daily') { const today = new Date().toISOString().split('T')[0]; matchesFilter = song.puzzles?.some(p => p.date === today) || false; + } else if (selectedGenreFilter === 'no-global') { + matchesFilter = song.excludeFromGlobal === true; } } @@ -1052,6 +1120,7 @@ export default function AdminPage() {
{/* Drag & Drop Zone */}
document.getElementById('file-input')?.click()} + onClick={() => fileInputRef.current?.click()} >
📁

@@ -1075,7 +1144,7 @@ export default function AdminPage() { or click to browse

)} +
+ +

+ If checked, these songs will only appear in Genre or Special puzzles. +

+
+
+
+ +
{new Date(song.createdAt).toLocaleDateString('de-DE')} @@ -1416,6 +1511,24 @@ export default function AdminPage() {
{song.title}
{song.artist}
+ {song.excludeFromGlobal && ( +
+ + 🚫 No Global + +
+ )} + {/* Daily Puzzle Badges */}
{song.puzzles?.filter(p => p.date === new Date().toISOString().split('T')[0]).map(p => { diff --git a/app/api/songs/route.ts b/app/api/songs/route.ts index 5a832fb..2bd4900 100644 --- a/app/api/songs/route.ts +++ b/app/api/songs/route.ts @@ -8,6 +8,10 @@ import { requireAdminAuth } from '@/lib/auth'; const prisma = new PrismaClient(); +// Configure route to handle large file uploads +export const runtime = 'nodejs'; +export const maxDuration = 60; // 60 seconds timeout for uploads + export async function GET() { const songs = await prisma.song.findMany({ orderBy: { createdAt: 'desc' }, @@ -37,23 +41,35 @@ export async function GET() { specials: song.specials.map(ss => ss.special), averageRating: song.averageRating, ratingCount: song.ratingCount, + excludeFromGlobal: song.excludeFromGlobal, })); return NextResponse.json(songsWithActivations); } export async function POST(request: Request) { + console.log('[UPLOAD] Starting song upload request'); + // Check authentication const authError = await requireAdminAuth(request as any); - if (authError) return authError; + if (authError) { + console.log('[UPLOAD] Authentication failed'); + return authError; + } try { + console.log('[UPLOAD] Parsing form data...'); const formData = await request.formData(); const file = formData.get('file') as File; let title = ''; let artist = ''; + const excludeFromGlobal = formData.get('excludeFromGlobal') === 'true'; + + console.log('[UPLOAD] Received file:', file?.name, 'Size:', file?.size, 'Type:', file?.type); + console.log('[UPLOAD] excludeFromGlobal:', excludeFromGlobal); if (!file) { + console.error('[UPLOAD] No file provided'); return NextResponse.json({ error: 'No file provided' }, { status: 400 }); } @@ -81,6 +97,7 @@ export async function POST(request: Request) { } const buffer = Buffer.from(await file.arrayBuffer()); + console.log('[UPLOAD] Buffer created, size:', buffer.length, 'bytes'); // Validate and extract metadata from file let metadata; @@ -208,10 +225,9 @@ export async function POST(request: Request) { console.error('Failed to extract cover image:', e); } - // Fetch release year (iTunes first, then MusicBrainz) + // Fetch release year from iTunes let releaseYear = null; try { - // Try iTunes first const { getReleaseYearFromItunes } = await import('@/lib/itunes'); releaseYear = await getReleaseYearFromItunes(artist, title); @@ -229,6 +245,7 @@ export async function POST(request: Request) { filename, coverImage, releaseYear, + excludeFromGlobal, }, include: { genres: true, specials: true } }); @@ -249,7 +266,7 @@ export async function PUT(request: Request) { if (authError) return authError; try { - const { id, title, artist, releaseYear, genreIds, specialIds } = await request.json(); + const { id, title, artist, releaseYear, genreIds, specialIds, excludeFromGlobal } = await request.json(); if (!id || !title || !artist) { return NextResponse.json({ error: 'Missing fields' }, { status: 400 }); @@ -262,6 +279,10 @@ export async function PUT(request: Request) { data.releaseYear = releaseYear; } + if (excludeFromGlobal !== undefined) { + data.excludeFromGlobal = excludeFromGlobal; + } + if (genreIds) { data.genres = { set: genreIds.map((gId: number) => ({ id: gId })) diff --git a/lib/dailyPuzzle.ts b/lib/dailyPuzzle.ts index f7b8a41..8e46d21 100644 --- a/lib/dailyPuzzle.ts +++ b/lib/dailyPuzzle.ts @@ -33,7 +33,7 @@ export async function getOrCreateDailyPuzzle(genreName: string | null = null) { // Get songs available for this genre const whereClause = genreId ? { genres: { some: { id: genreId } } } - : {}; // Global puzzle picks from ALL songs + : { excludeFromGlobal: false }; // Global puzzle picks from ALL songs (except excluded) const allSongs = await prisma.song.findMany({ where: whereClause, diff --git a/next.config.ts b/next.config.ts index 9f2fc56..9b78294 100644 --- a/next.config.ts +++ b/next.config.ts @@ -8,6 +8,7 @@ const nextConfig: NextConfig = { serverActions: { bodySizeLimit: '50mb', }, + middlewareClientMaxBodySize: '50mb', }, env: { TZ: process.env.TZ || 'Europe/Berlin', diff --git a/prisma/dev.db.bak b/prisma/dev.db.bak new file mode 100644 index 0000000000000000000000000000000000000000..c124225970a640c7fd9675a778db23de012f5d5b GIT binary patch literal 90112 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 literal 0 HcmV?d00001 diff --git a/prisma/migrations/20251124182259_add_exclude_from_global/migration.sql b/prisma/migrations/20251124182259_add_exclude_from_global/migration.sql new file mode 100644 index 0000000..036677c --- /dev/null +++ b/prisma/migrations/20251124182259_add_exclude_from_global/migration.sql @@ -0,0 +1,20 @@ +-- RedefineTables +PRAGMA defer_foreign_keys=ON; +PRAGMA foreign_keys=OFF; +CREATE TABLE "new_Song" ( + "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + "title" TEXT NOT NULL, + "artist" TEXT NOT NULL, + "filename" TEXT NOT NULL, + "coverImage" TEXT, + "releaseYear" INTEGER, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "averageRating" REAL NOT NULL DEFAULT 0, + "ratingCount" INTEGER NOT NULL DEFAULT 0, + "excludeFromGlobal" BOOLEAN NOT NULL DEFAULT false +); +INSERT INTO "new_Song" ("artist", "averageRating", "coverImage", "createdAt", "filename", "id", "ratingCount", "releaseYear", "title") SELECT "artist", "averageRating", "coverImage", "createdAt", "filename", "id", "ratingCount", "releaseYear", "title" FROM "Song"; +DROP TABLE "Song"; +ALTER TABLE "new_Song" RENAME TO "Song"; +PRAGMA foreign_keys=ON; +PRAGMA defer_foreign_keys=OFF; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 5f441e2..6b398c2 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -23,6 +23,7 @@ model Song { specials SpecialSong[] averageRating Float @default(0) ratingCount Int @default(0) + excludeFromGlobal Boolean @default(false) } model Genre {