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:
parent
6c879987bf
commit
aae57ec55f
@ -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,
|
||||
|
||||
@ -79,10 +79,12 @@ export class S3Service {
|
||||
async uploadFile(
|
||||
file: Buffer,
|
||||
originalName: string,
|
||||
contentType: string
|
||||
contentType: string,
|
||||
targetFolder?: string
|
||||
): Promise<UploadResult> {
|
||||
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,
|
||||
|
||||
@ -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<MusicUploadProps> = ({ onUploadComplete }) => {
|
||||
const [uploadProgress, setUploadProgress] = useState<UploadProgress[]>([]);
|
||||
const [isUploading, setIsUploading] = useState(false);
|
||||
const [targetFolder, setTargetFolder] = useState<string>('uploads');
|
||||
const [markForScan, setMarkForScan] = useState<boolean>(true);
|
||||
const toast = useToast();
|
||||
|
||||
const onDrop = useCallback(async (acceptedFiles: File[]) => {
|
||||
@ -44,12 +48,14 @@ export const MusicUpload: React.FC<MusicUploadProps> = ({ 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<MusicUploadProps> = ({ onUploadComplete }) =>
|
||||
|
||||
return (
|
||||
<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
|
||||
{...getRootProps()}
|
||||
border="2px dashed"
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user