feat: Add NoGlobal feature to exclude songs from Global Daily Puzzle
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect, useRef } from 'react';
|
||||||
|
|
||||||
|
|
||||||
interface Special {
|
interface Special {
|
||||||
@@ -47,6 +47,7 @@ interface Song {
|
|||||||
specials: Special[];
|
specials: Special[];
|
||||||
averageRating: number;
|
averageRating: number;
|
||||||
ratingCount: number;
|
ratingCount: number;
|
||||||
|
excludeFromGlobal: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
type SortField = 'id' | 'title' | 'artist' | 'createdAt' | 'releaseYear';
|
type SortField = 'id' | 'title' | 'artist' | 'createdAt' | 'releaseYear';
|
||||||
@@ -95,10 +96,12 @@ export default function AdminPage() {
|
|||||||
const [editReleaseYear, setEditReleaseYear] = useState<number | ''>('');
|
const [editReleaseYear, setEditReleaseYear] = useState<number | ''>('');
|
||||||
const [editGenreIds, setEditGenreIds] = useState<number[]>([]);
|
const [editGenreIds, setEditGenreIds] = useState<number[]>([]);
|
||||||
const [editSpecialIds, setEditSpecialIds] = useState<number[]>([]);
|
const [editSpecialIds, setEditSpecialIds] = useState<number[]>([]);
|
||||||
|
const [editExcludeFromGlobal, setEditExcludeFromGlobal] = useState(false);
|
||||||
|
|
||||||
// Post-upload state
|
// Post-upload state
|
||||||
const [uploadedSong, setUploadedSong] = useState<Song | null>(null);
|
const [uploadedSong, setUploadedSong] = useState<Song | null>(null);
|
||||||
const [uploadGenreIds, setUploadGenreIds] = useState<number[]>([]);
|
const [uploadGenreIds, setUploadGenreIds] = useState<number[]>([]);
|
||||||
|
const [uploadExcludeFromGlobal, setUploadExcludeFromGlobal] = useState(false);
|
||||||
|
|
||||||
// AI Categorization state
|
// AI Categorization state
|
||||||
const [isCategorizing, setIsCategorizing] = useState(false);
|
const [isCategorizing, setIsCategorizing] = useState(false);
|
||||||
@@ -123,6 +126,7 @@ export default function AdminPage() {
|
|||||||
const [dailyPuzzles, setDailyPuzzles] = useState<any[]>([]);
|
const [dailyPuzzles, setDailyPuzzles] = useState<any[]>([]);
|
||||||
const [playingPuzzleId, setPlayingPuzzleId] = useState<number | null>(null);
|
const [playingPuzzleId, setPlayingPuzzleId] = useState<number | null>(null);
|
||||||
const [showDailyPuzzles, setShowDailyPuzzles] = useState(false);
|
const [showDailyPuzzles, setShowDailyPuzzles] = useState(false);
|
||||||
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
// Check for existing auth on mount
|
// Check for existing auth on mount
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -478,8 +482,11 @@ export default function AdminPage() {
|
|||||||
setUploadProgress({ current: i + 1, total: files.length });
|
setUploadProgress({ current: i + 1, total: files.length });
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
console.log(`Uploading file ${i + 1}/${files.length}: ${file.name} (${(file.size / 1024 / 1024).toFixed(2)}MB)`);
|
||||||
|
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append('file', file);
|
formData.append('file', file);
|
||||||
|
formData.append('excludeFromGlobal', String(uploadExcludeFromGlobal));
|
||||||
|
|
||||||
const res = await fetch('/api/songs', {
|
const res = await fetch('/api/songs', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@@ -487,8 +494,11 @@ export default function AdminPage() {
|
|||||||
body: formData,
|
body: formData,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
console.log(`Response status for ${file.name}: ${res.status}`);
|
||||||
|
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
|
console.log(`Upload successful for ${file.name}:`, data);
|
||||||
results.push({
|
results.push({
|
||||||
filename: file.name,
|
filename: file.name,
|
||||||
success: true,
|
success: true,
|
||||||
@@ -498,6 +508,7 @@ export default function AdminPage() {
|
|||||||
} else if (res.status === 409) {
|
} else if (res.status === 409) {
|
||||||
// Duplicate detected
|
// Duplicate detected
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
|
console.log(`Duplicate detected for ${file.name}:`, data);
|
||||||
results.push({
|
results.push({
|
||||||
filename: file.name,
|
filename: file.name,
|
||||||
success: false,
|
success: false,
|
||||||
@@ -506,17 +517,20 @@ export default function AdminPage() {
|
|||||||
error: `Duplicate: Already exists as "${data.duplicate.title}" by "${data.duplicate.artist}"`
|
error: `Duplicate: Already exists as "${data.duplicate.title}" by "${data.duplicate.artist}"`
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
|
const errorText = await res.text();
|
||||||
|
console.error(`Upload failed for ${file.name} (${res.status}):`, errorText);
|
||||||
results.push({
|
results.push({
|
||||||
filename: file.name,
|
filename: file.name,
|
||||||
success: false,
|
success: false,
|
||||||
error: 'Upload failed'
|
error: `Upload failed (${res.status}): ${errorText.substring(0, 100)}`
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
console.error(`Network error for ${file.name}:`, error);
|
||||||
results.push({
|
results.push({
|
||||||
filename: file.name,
|
filename: file.name,
|
||||||
success: false,
|
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) => {
|
const handleDragOver = (e: React.DragEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setIsDragging(true);
|
e.stopPropagation();
|
||||||
|
e.dataTransfer.dropEffect = 'copy';
|
||||||
|
if (!isDragging) setIsDragging(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDragLeave = (e: React.DragEvent) => {
|
const handleDragLeave = (e: React.DragEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
|
||||||
|
// Prevent flickering when dragging over children
|
||||||
|
if (e.currentTarget.contains(e.relatedTarget as Node)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
setIsDragging(false);
|
setIsDragging(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDrop = (e: React.DragEvent) => {
|
const handleDrop = (e: React.DragEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
setIsDragging(false);
|
setIsDragging(false);
|
||||||
|
|
||||||
const droppedFiles = Array.from(e.dataTransfer.files).filter(
|
const droppedFiles = Array.from(e.dataTransfer.files);
|
||||||
file => file.type === 'audio/mpeg' || file.name.endsWith('.mp3')
|
|
||||||
);
|
|
||||||
|
|
||||||
if (droppedFiles.length > 0) {
|
// Validate file types
|
||||||
setFiles(droppedFiles);
|
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<HTMLInputElement>) => {
|
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
if (e.target.files) {
|
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 || '');
|
setEditReleaseYear(song.releaseYear || '');
|
||||||
setEditGenreIds(song.genres.map(g => g.id));
|
setEditGenreIds(song.genres.map(g => g.id));
|
||||||
setEditSpecialIds(song.specials ? song.specials.map(s => s.id) : []);
|
setEditSpecialIds(song.specials ? song.specials.map(s => s.id) : []);
|
||||||
|
setEditExcludeFromGlobal(song.excludeFromGlobal || false);
|
||||||
};
|
};
|
||||||
|
|
||||||
const cancelEditing = () => {
|
const cancelEditing = () => {
|
||||||
@@ -624,6 +688,7 @@ export default function AdminPage() {
|
|||||||
setEditReleaseYear('');
|
setEditReleaseYear('');
|
||||||
setEditGenreIds([]);
|
setEditGenreIds([]);
|
||||||
setEditSpecialIds([]);
|
setEditSpecialIds([]);
|
||||||
|
setEditExcludeFromGlobal(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
const saveEditing = async (id: number) => {
|
const saveEditing = async (id: number) => {
|
||||||
@@ -636,7 +701,8 @@ export default function AdminPage() {
|
|||||||
artist: editArtist,
|
artist: editArtist,
|
||||||
releaseYear: editReleaseYear === '' ? null : Number(editReleaseYear),
|
releaseYear: editReleaseYear === '' ? null : Number(editReleaseYear),
|
||||||
genreIds: editGenreIds,
|
genreIds: editGenreIds,
|
||||||
specialIds: editSpecialIds
|
specialIds: editSpecialIds,
|
||||||
|
excludeFromGlobal: editExcludeFromGlobal
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -739,6 +805,8 @@ export default function AdminPage() {
|
|||||||
} else if (selectedGenreFilter === 'daily') {
|
} else if (selectedGenreFilter === 'daily') {
|
||||||
const today = new Date().toISOString().split('T')[0];
|
const today = new Date().toISOString().split('T')[0];
|
||||||
matchesFilter = song.puzzles?.some(p => p.date === today) || false;
|
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() {
|
|||||||
<form onSubmit={handleBatchUpload}>
|
<form onSubmit={handleBatchUpload}>
|
||||||
{/* Drag & Drop Zone */}
|
{/* Drag & Drop Zone */}
|
||||||
<div
|
<div
|
||||||
|
onDragEnter={handleDragEnter}
|
||||||
onDragOver={handleDragOver}
|
onDragOver={handleDragOver}
|
||||||
onDragLeave={handleDragLeave}
|
onDragLeave={handleDragLeave}
|
||||||
onDrop={handleDrop}
|
onDrop={handleDrop}
|
||||||
@@ -1065,7 +1134,7 @@ export default function AdminPage() {
|
|||||||
cursor: 'pointer',
|
cursor: 'pointer',
|
||||||
transition: 'all 0.2s'
|
transition: 'all 0.2s'
|
||||||
}}
|
}}
|
||||||
onClick={() => document.getElementById('file-input')?.click()}
|
onClick={() => fileInputRef.current?.click()}
|
||||||
>
|
>
|
||||||
<div style={{ fontSize: '3rem', marginBottom: '0.5rem' }}>📁</div>
|
<div style={{ fontSize: '3rem', marginBottom: '0.5rem' }}>📁</div>
|
||||||
<p style={{ fontWeight: 'bold', marginBottom: '0.25rem' }}>
|
<p style={{ fontWeight: 'bold', marginBottom: '0.25rem' }}>
|
||||||
@@ -1075,7 +1144,7 @@ export default function AdminPage() {
|
|||||||
or click to browse
|
or click to browse
|
||||||
</p>
|
</p>
|
||||||
<input
|
<input
|
||||||
id="file-input"
|
ref={fileInputRef}
|
||||||
type="file"
|
type="file"
|
||||||
accept="audio/mpeg"
|
accept="audio/mpeg"
|
||||||
multiple
|
multiple
|
||||||
@@ -1115,6 +1184,21 @@ export default function AdminPage() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
<div style={{ marginBottom: '1rem' }}>
|
||||||
|
<label style={{ display: 'flex', alignItems: 'center', gap: '0.5rem', cursor: 'pointer' }}>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={uploadExcludeFromGlobal}
|
||||||
|
onChange={e => setUploadExcludeFromGlobal(e.target.checked)}
|
||||||
|
style={{ width: '1.25rem', height: '1.25rem' }}
|
||||||
|
/>
|
||||||
|
<span style={{ fontWeight: '500' }}>Exclude from Global Daily Puzzle</span>
|
||||||
|
</label>
|
||||||
|
<p style={{ fontSize: '0.875rem', color: '#666', marginLeft: '1.75rem', marginTop: '0.25rem' }}>
|
||||||
|
If checked, these songs will only appear in Genre or Special puzzles.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
className="btn-primary"
|
className="btn-primary"
|
||||||
@@ -1235,6 +1319,7 @@ export default function AdminPage() {
|
|||||||
>
|
>
|
||||||
<option value="">All Content</option>
|
<option value="">All Content</option>
|
||||||
<option value="daily">📅 Song of the Day</option>
|
<option value="daily">📅 Song of the Day</option>
|
||||||
|
<option value="no-global">🚫 No Global</option>
|
||||||
<optgroup label="Genres">
|
<optgroup label="Genres">
|
||||||
<option value="genre:-1">No Genre</option>
|
<option value="genre:-1">No Genre</option>
|
||||||
{genres.map(genre => (
|
{genres.map(genre => (
|
||||||
@@ -1377,6 +1462,16 @@ export default function AdminPage() {
|
|||||||
</label>
|
</label>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
<div style={{ marginTop: '0.5rem', borderTop: '1px dashed #eee', paddingTop: '0.5rem' }}>
|
||||||
|
<label style={{ display: 'flex', alignItems: 'center', gap: '0.5rem', fontSize: '0.75rem', cursor: 'pointer', color: '#b91c1c' }}>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={editExcludeFromGlobal}
|
||||||
|
onChange={e => setEditExcludeFromGlobal(e.target.checked)}
|
||||||
|
/>
|
||||||
|
Exclude from Global
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td style={{ padding: '0.75rem', color: '#666', fontSize: '0.75rem' }}>
|
<td style={{ padding: '0.75rem', color: '#666', fontSize: '0.75rem' }}>
|
||||||
{new Date(song.createdAt).toLocaleDateString('de-DE')}
|
{new Date(song.createdAt).toLocaleDateString('de-DE')}
|
||||||
@@ -1416,6 +1511,24 @@ export default function AdminPage() {
|
|||||||
<div style={{ fontWeight: 'bold', color: '#111827' }}>{song.title}</div>
|
<div style={{ fontWeight: 'bold', color: '#111827' }}>{song.title}</div>
|
||||||
<div style={{ fontSize: '0.875rem', color: '#6b7280' }}>{song.artist}</div>
|
<div style={{ fontSize: '0.875rem', color: '#6b7280' }}>{song.artist}</div>
|
||||||
|
|
||||||
|
{song.excludeFromGlobal && (
|
||||||
|
<div style={{ marginTop: '0.25rem' }}>
|
||||||
|
<span style={{
|
||||||
|
background: '#fee2e2',
|
||||||
|
color: '#991b1b',
|
||||||
|
padding: '0.1rem 0.4rem',
|
||||||
|
borderRadius: '0.25rem',
|
||||||
|
fontSize: '0.7rem',
|
||||||
|
border: '1px solid #fecaca',
|
||||||
|
display: 'inline-flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: '0.25rem'
|
||||||
|
}}>
|
||||||
|
🚫 No Global
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Daily Puzzle Badges */}
|
{/* Daily Puzzle Badges */}
|
||||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '0.25rem', marginTop: '0.25rem' }}>
|
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '0.25rem', marginTop: '0.25rem' }}>
|
||||||
{song.puzzles?.filter(p => p.date === new Date().toISOString().split('T')[0]).map(p => {
|
{song.puzzles?.filter(p => p.date === new Date().toISOString().split('T')[0]).map(p => {
|
||||||
|
|||||||
@@ -8,6 +8,10 @@ import { requireAdminAuth } from '@/lib/auth';
|
|||||||
|
|
||||||
const prisma = new PrismaClient();
|
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() {
|
export async function GET() {
|
||||||
const songs = await prisma.song.findMany({
|
const songs = await prisma.song.findMany({
|
||||||
orderBy: { createdAt: 'desc' },
|
orderBy: { createdAt: 'desc' },
|
||||||
@@ -37,23 +41,35 @@ export async function GET() {
|
|||||||
specials: song.specials.map(ss => ss.special),
|
specials: song.specials.map(ss => ss.special),
|
||||||
averageRating: song.averageRating,
|
averageRating: song.averageRating,
|
||||||
ratingCount: song.ratingCount,
|
ratingCount: song.ratingCount,
|
||||||
|
excludeFromGlobal: song.excludeFromGlobal,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
return NextResponse.json(songsWithActivations);
|
return NextResponse.json(songsWithActivations);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function POST(request: Request) {
|
export async function POST(request: Request) {
|
||||||
|
console.log('[UPLOAD] Starting song upload request');
|
||||||
|
|
||||||
// Check authentication
|
// Check authentication
|
||||||
const authError = await requireAdminAuth(request as any);
|
const authError = await requireAdminAuth(request as any);
|
||||||
if (authError) return authError;
|
if (authError) {
|
||||||
|
console.log('[UPLOAD] Authentication failed');
|
||||||
|
return authError;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
console.log('[UPLOAD] Parsing form data...');
|
||||||
const formData = await request.formData();
|
const formData = await request.formData();
|
||||||
const file = formData.get('file') as File;
|
const file = formData.get('file') as File;
|
||||||
let title = '';
|
let title = '';
|
||||||
let artist = '';
|
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) {
|
if (!file) {
|
||||||
|
console.error('[UPLOAD] No file provided');
|
||||||
return NextResponse.json({ error: 'No file provided' }, { status: 400 });
|
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());
|
const buffer = Buffer.from(await file.arrayBuffer());
|
||||||
|
console.log('[UPLOAD] Buffer created, size:', buffer.length, 'bytes');
|
||||||
|
|
||||||
// Validate and extract metadata from file
|
// Validate and extract metadata from file
|
||||||
let metadata;
|
let metadata;
|
||||||
@@ -208,10 +225,9 @@ export async function POST(request: Request) {
|
|||||||
console.error('Failed to extract cover image:', e);
|
console.error('Failed to extract cover image:', e);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fetch release year (iTunes first, then MusicBrainz)
|
// Fetch release year from iTunes
|
||||||
let releaseYear = null;
|
let releaseYear = null;
|
||||||
try {
|
try {
|
||||||
// Try iTunes first
|
|
||||||
const { getReleaseYearFromItunes } = await import('@/lib/itunes');
|
const { getReleaseYearFromItunes } = await import('@/lib/itunes');
|
||||||
releaseYear = await getReleaseYearFromItunes(artist, title);
|
releaseYear = await getReleaseYearFromItunes(artist, title);
|
||||||
|
|
||||||
@@ -229,6 +245,7 @@ export async function POST(request: Request) {
|
|||||||
filename,
|
filename,
|
||||||
coverImage,
|
coverImage,
|
||||||
releaseYear,
|
releaseYear,
|
||||||
|
excludeFromGlobal,
|
||||||
},
|
},
|
||||||
include: { genres: true, specials: true }
|
include: { genres: true, specials: true }
|
||||||
});
|
});
|
||||||
@@ -249,7 +266,7 @@ export async function PUT(request: Request) {
|
|||||||
if (authError) return authError;
|
if (authError) return authError;
|
||||||
|
|
||||||
try {
|
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) {
|
if (!id || !title || !artist) {
|
||||||
return NextResponse.json({ error: 'Missing fields' }, { status: 400 });
|
return NextResponse.json({ error: 'Missing fields' }, { status: 400 });
|
||||||
@@ -262,6 +279,10 @@ export async function PUT(request: Request) {
|
|||||||
data.releaseYear = releaseYear;
|
data.releaseYear = releaseYear;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (excludeFromGlobal !== undefined) {
|
||||||
|
data.excludeFromGlobal = excludeFromGlobal;
|
||||||
|
}
|
||||||
|
|
||||||
if (genreIds) {
|
if (genreIds) {
|
||||||
data.genres = {
|
data.genres = {
|
||||||
set: genreIds.map((gId: number) => ({ id: gId }))
|
set: genreIds.map((gId: number) => ({ id: gId }))
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ export async function getOrCreateDailyPuzzle(genreName: string | null = null) {
|
|||||||
// Get songs available for this genre
|
// Get songs available for this genre
|
||||||
const whereClause = genreId
|
const whereClause = genreId
|
||||||
? { genres: { some: { id: 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({
|
const allSongs = await prisma.song.findMany({
|
||||||
where: whereClause,
|
where: whereClause,
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ const nextConfig: NextConfig = {
|
|||||||
serverActions: {
|
serverActions: {
|
||||||
bodySizeLimit: '50mb',
|
bodySizeLimit: '50mb',
|
||||||
},
|
},
|
||||||
|
middlewareClientMaxBodySize: '50mb',
|
||||||
},
|
},
|
||||||
env: {
|
env: {
|
||||||
TZ: process.env.TZ || 'Europe/Berlin',
|
TZ: process.env.TZ || 'Europe/Berlin',
|
||||||
|
|||||||
BIN
prisma/dev.db.bak
Normal file
BIN
prisma/dev.db.bak
Normal file
Binary file not shown.
@@ -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;
|
||||||
@@ -23,6 +23,7 @@ model Song {
|
|||||||
specials SpecialSong[]
|
specials SpecialSong[]
|
||||||
averageRating Float @default(0)
|
averageRating Float @default(0)
|
||||||
ratingCount Int @default(0)
|
ratingCount Int @default(0)
|
||||||
|
excludeFromGlobal Boolean @default(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
model Genre {
|
model Genre {
|
||||||
|
|||||||
Reference in New Issue
Block a user