feat(upload): allow selecting target S3 folder and auto-add uploaded files to 'To Be Scanned' playlist via stub songs for XML export

This commit is contained in:
Geert Rademakes 2025-08-13 17:00:59 +02:00
parent 6c879987bf
commit aae57ec55f
3 changed files with 76 additions and 4 deletions

View File

@ -74,9 +74,11 @@ router.post('/upload', upload.single('file'), async (req, res) => {
} }
const { buffer, originalname, mimetype } = req.file; 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 // Upload to S3
const uploadResult = await s3Service.uploadFile(buffer, originalname, mimetype); const uploadResult = await s3Service.uploadFile(buffer, originalname, mimetype, targetFolder);
// Extract audio metadata // Extract audio metadata
const metadata = await audioMetadataService.extractMetadata(buffer, originalname); const metadata = await audioMetadataService.extractMetadata(buffer, originalname);
@ -93,6 +95,59 @@ router.post('/upload', upload.single('file'), async (req, res) => {
await musicFile.save(); 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({ res.json({
message: 'File uploaded successfully', message: 'File uploaded successfully',
musicFile, musicFile,

View File

@ -79,10 +79,12 @@ export class S3Service {
async uploadFile( async uploadFile(
file: Buffer, file: Buffer,
originalName: string, originalName: string,
contentType: string contentType: string,
targetFolder?: string
): Promise<UploadResult> { ): Promise<UploadResult> {
const fileExtension = originalName.split('.').pop(); 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({ const command = new PutObjectCommand({
Bucket: this.bucketName, Bucket: this.bucketName,

View File

@ -12,6 +12,8 @@ import {
AlertTitle, AlertTitle,
AlertDescription, AlertDescription,
Icon, Icon,
Input,
Checkbox,
useToast, useToast,
} from '@chakra-ui/react'; } from '@chakra-ui/react';
import { FiUpload, FiMusic, FiCheck, FiX } from 'react-icons/fi'; import { FiUpload, FiMusic, FiCheck, FiX } from 'react-icons/fi';
@ -30,6 +32,8 @@ interface MusicUploadProps {
export const MusicUpload: React.FC<MusicUploadProps> = ({ onUploadComplete }) => { export const MusicUpload: React.FC<MusicUploadProps> = ({ onUploadComplete }) => {
const [uploadProgress, setUploadProgress] = useState<UploadProgress[]>([]); const [uploadProgress, setUploadProgress] = useState<UploadProgress[]>([]);
const [isUploading, setIsUploading] = useState(false); const [isUploading, setIsUploading] = useState(false);
const [targetFolder, setTargetFolder] = useState<string>('uploads');
const [markForScan, setMarkForScan] = useState<boolean>(true);
const toast = useToast(); const toast = useToast();
const onDrop = useCallback(async (acceptedFiles: File[]) => { const onDrop = useCallback(async (acceptedFiles: File[]) => {
@ -50,6 +54,8 @@ export const MusicUpload: React.FC<MusicUploadProps> = ({ onUploadComplete }) =>
try { try {
const formData = new FormData(); const formData = new FormData();
formData.append('file', file); formData.append('file', file);
formData.append('targetFolder', targetFolder);
formData.append('markForScan', String(markForScan));
const response = await fetch('/api/music/upload', { const response = await fetch('/api/music/upload', {
method: 'POST', method: 'POST',
@ -120,6 +126,15 @@ export const MusicUpload: React.FC<MusicUploadProps> = ({ onUploadComplete }) =>
return ( return (
<VStack spacing={4} align="stretch" w="full"> <VStack spacing={4} align="stretch" w="full">
<HStack>
<Box flex={1}>
<Text color="gray.300" fontSize="sm" mb={1}>Target S3 folder</Text>
<Input value={targetFolder} onChange={(e) => setTargetFolder(e.target.value)} placeholder="e.g. Prep/ToScan" bg="gray.800" borderColor="gray.700" />
</Box>
<Checkbox isChecked={markForScan} onChange={(e) => setMarkForScan(e.target.checked)} colorScheme="blue" alignSelf="end">
Add to "To Be Scanned"
</Checkbox>
</HStack>
<Box <Box
{...getRootProps()} {...getRootProps()}
border="2px dashed" border="2px dashed"