From aae57ec55f512a60760bb02ba4e9355d0a15ebb9 Mon Sep 17 00:00:00 2001 From: Geert Rademakes Date: Wed, 13 Aug 2025 17:00:59 +0200 Subject: [PATCH] feat(upload): allow selecting target S3 folder and auto-add uploaded files to 'To Be Scanned' playlist via stub songs for XML export --- packages/backend/src/routes/music.ts | 57 ++++++++++++++++++- packages/backend/src/services/s3Service.ts | 6 +- .../frontend/src/components/MusicUpload.tsx | 17 +++++- 3 files changed, 76 insertions(+), 4 deletions(-) diff --git a/packages/backend/src/routes/music.ts b/packages/backend/src/routes/music.ts index 118452d..dce125e 100644 --- a/packages/backend/src/routes/music.ts +++ b/packages/backend/src/routes/music.ts @@ -74,9 +74,11 @@ router.post('/upload', upload.single('file'), async (req, res) => { } const { buffer, originalname, mimetype } = req.file; + const targetFolder = typeof req.body?.targetFolder === 'string' ? req.body.targetFolder : undefined; + const markForScan = String(req.body?.markForScan || '').toLowerCase() === 'true'; // Upload to S3 - const uploadResult = await s3Service.uploadFile(buffer, originalname, mimetype); + const uploadResult = await s3Service.uploadFile(buffer, originalname, mimetype, targetFolder); // Extract audio metadata const metadata = await audioMetadataService.extractMetadata(buffer, originalname); @@ -93,6 +95,59 @@ router.post('/upload', upload.single('file'), async (req, res) => { await musicFile.save(); + // Optionally add to a special playlist for scanning + if (markForScan) { + try { + const { Playlist } = await import('../models/Playlist.js'); + // Ensure root exists or create simple structure + let root = await Playlist.findOne({ name: 'ROOT' }); + if (!root) { + root = new Playlist({ name: 'ROOT', type: 'folder', children: [] }); + await root.save(); + } + // Find or create the "To Be Scanned" playlist at root level + const findNode = (node: any, name: string): any => { + if (!node) return null; + if (node.type === 'playlist' && node.name === name) return node; + if (Array.isArray(node.children)) { + for (const child of node.children) { + const found = findNode(child, name); + if (found) return found; + } + } + return null; + }; + + let toScan = findNode(root, 'To Be Scanned'); + if (!toScan) { + toScan = { id: 'to-be-scanned', name: 'To Be Scanned', type: 'playlist', tracks: [] } as any; + root.children = [...(root.children || []), toScan]; + } + // Add by songId? We don't have a Song yet; add by MusicFile ObjectId to track later + // Instead, we will create a stub Song entry if none exists so XML export can include it + const { Song } = await import('../models/Song.js'); + // Create stub song with temporary id if needed + const tempId = `stub-${musicFile._id.toString()}`; + let existingStub = await Song.findOne({ id: tempId }); + if (!existingStub) { + const stub = new Song({ + id: tempId, + title: musicFile.title || musicFile.originalName, + artist: musicFile.artist || '', + album: musicFile.album || '', + totalTime: musicFile.duration ? String(Math.round(musicFile.duration / 1000)) : '', + location: '', + }); + await stub.save(); + } + // Push stub id into playlist + toScan.tracks = Array.from(new Set([...(toScan.tracks || []), tempId])); + await root.save(); + } catch (e) { + console.warn('Failed to mark uploaded file for scanning:', e); + } + } + res.json({ message: 'File uploaded successfully', musicFile, diff --git a/packages/backend/src/services/s3Service.ts b/packages/backend/src/services/s3Service.ts index 7420989..17a5c52 100644 --- a/packages/backend/src/services/s3Service.ts +++ b/packages/backend/src/services/s3Service.ts @@ -79,10 +79,12 @@ export class S3Service { async uploadFile( file: Buffer, originalName: string, - contentType: string + contentType: string, + targetFolder?: string ): Promise { const fileExtension = originalName.split('.').pop(); - const key = `music/${uuidv4()}.${fileExtension}`; + const safeFolder = (targetFolder || 'music').replace(/^\/+|\/+$/g, ''); + const key = `${safeFolder}/${uuidv4()}.${fileExtension}`; const command = new PutObjectCommand({ Bucket: this.bucketName, diff --git a/packages/frontend/src/components/MusicUpload.tsx b/packages/frontend/src/components/MusicUpload.tsx index 6b35b27..8fa924a 100644 --- a/packages/frontend/src/components/MusicUpload.tsx +++ b/packages/frontend/src/components/MusicUpload.tsx @@ -12,6 +12,8 @@ import { AlertTitle, AlertDescription, Icon, + Input, + Checkbox, useToast, } from '@chakra-ui/react'; import { FiUpload, FiMusic, FiCheck, FiX } from 'react-icons/fi'; @@ -30,6 +32,8 @@ interface MusicUploadProps { export const MusicUpload: React.FC = ({ onUploadComplete }) => { const [uploadProgress, setUploadProgress] = useState([]); const [isUploading, setIsUploading] = useState(false); + const [targetFolder, setTargetFolder] = useState('uploads'); + const [markForScan, setMarkForScan] = useState(true); const toast = useToast(); const onDrop = useCallback(async (acceptedFiles: File[]) => { @@ -44,12 +48,14 @@ export const MusicUpload: React.FC = ({ onUploadComplete }) => const results = []; - for (let i = 0; i < acceptedFiles.length; i++) { + for (let i = 0; i < acceptedFiles.length; i++) { const file = acceptedFiles[i]; try { const formData = new FormData(); formData.append('file', file); + formData.append('targetFolder', targetFolder); + formData.append('markForScan', String(markForScan)); const response = await fetch('/api/music/upload', { method: 'POST', @@ -120,6 +126,15 @@ export const MusicUpload: React.FC = ({ onUploadComplete }) => return ( + + + Target S3 folder + setTargetFolder(e.target.value)} placeholder="e.g. Prep/ToScan" bg="gray.800" borderColor="gray.700" /> + + setMarkForScan(e.target.checked)} colorScheme="blue" alignSelf="end"> + Add to "To Be Scanned" + +