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 { 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,
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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"
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user