diff --git a/packages/backend/src/routes/music.ts b/packages/backend/src/routes/music.ts index 446d029..c78c80f 100644 --- a/packages/backend/src/routes/music.ts +++ b/packages/backend/src/routes/music.ts @@ -95,6 +95,9 @@ router.post('/upload', upload.single('file'), async (req, res) => { await musicFile.save(); + // Invalidate folder cache since we added a new file + invalidateFolderCache(); + // Optionally add to a special playlist for scanning if (markForScan) { try { @@ -191,6 +194,9 @@ router.post('/batch-upload', upload.array('files', 10), async (req, res) => { await musicFile.save(); results.push({ success: true, musicFile }); + + // Invalidate folder cache since we added a new file + invalidateFolderCache(); } catch (error) { console.error(`Error uploading ${file.originalname}:`, error); results.push({ success: false, fileName: file.originalname, error: (error as Error).message }); @@ -223,17 +229,51 @@ router.get('/files', async (req, res) => { /** * List folders in the S3 bucket for folder selection */ +// Cache for folder listing to improve performance +let folderCache: { folders: string[]; timestamp: number } | null = null; +const CACHE_DURATION = 5 * 60 * 1000; // 5 minutes + +// Function to invalidate folder cache +const invalidateFolderCache = () => { + folderCache = null; +}; + router.get('/folders', async (req, res) => { try { + // Check if we have a valid cached result + if (folderCache && (Date.now() - folderCache.timestamp) < CACHE_DURATION) { + return res.json({ folders: folderCache.folders }); + } + const folders = await s3Service.listAllFolders(''); - // Include root option as empty string - res.json({ folders: ['', ...folders] }); + const result = ['', ...folders]; + + // Cache the result + folderCache = { + folders: result, + timestamp: Date.now() + }; + + res.json({ folders: result }); } catch (error) { console.error('Error fetching S3 folders:', error); res.status(500).json({ error: 'Failed to fetch S3 folders' }); } }); +/** + * Force refresh folder cache + */ +router.post('/folders/refresh', async (req, res) => { + try { + invalidateFolderCache(); + res.json({ message: 'Folder cache refreshed successfully' }); + } catch (error) { + console.error('Error refreshing folder cache:', error); + res.status(500).json({ error: 'Failed to refresh folder cache' }); + } +}); + /** * Sync S3 files with database - now uses background job system */ @@ -256,6 +296,9 @@ router.post('/sync-s3', async (req, res) => { status: 'started' }); + // Invalidate folder cache since sync might change folder structure + invalidateFolderCache(); + } catch (error) { console.error('❌ Error starting S3 sync job:', error); res.status(500).json({ error: 'Failed to start S3 sync job' }); @@ -396,6 +439,9 @@ router.delete('/:id', async (req, res) => { // Delete from database await MusicFile.findByIdAndDelete(req.params.id); + // Invalidate folder cache since we removed a file + invalidateFolderCache(); + res.json({ message: 'Music file deleted successfully' }); } catch (error) { console.error('Delete error:', error); diff --git a/packages/frontend/src/components/FolderBrowser.tsx b/packages/frontend/src/components/FolderBrowser.tsx index 4d59c84..e24bbe7 100644 --- a/packages/frontend/src/components/FolderBrowser.tsx +++ b/packages/frontend/src/components/FolderBrowser.tsx @@ -135,6 +135,9 @@ const FolderBrowser: React.FC = ({ toggleFolder(node.path); }} aria-label={expanded ? 'Collapse folder' : 'Expand folder'} + color="gray.300" + _hover={{ bg: 'gray.700', color: 'white' }} + _active={{ bg: 'gray.600' }} /> ) : ( diff --git a/packages/frontend/src/components/MusicUpload.tsx b/packages/frontend/src/components/MusicUpload.tsx index 5b5defb..37f5556 100644 --- a/packages/frontend/src/components/MusicUpload.tsx +++ b/packages/frontend/src/components/MusicUpload.tsx @@ -14,6 +14,7 @@ import { Icon, Input, Checkbox, + Spinner, useToast, } from '@chakra-ui/react'; import { FiUpload, FiMusic, FiCheck, FiX } from 'react-icons/fi'; @@ -35,6 +36,7 @@ export const MusicUpload: React.FC = ({ onUploadComplete }) => const [isUploading, setIsUploading] = useState(false); const [targetFolder, setTargetFolder] = useState('uploads'); const [folders, setFolders] = useState([]); + const [isLoadingFolders, setIsLoadingFolders] = useState(true); const [markForScan, setMarkForScan] = useState(true); const toast = useToast(); @@ -126,6 +128,7 @@ export const MusicUpload: React.FC = ({ onUploadComplete }) => React.useEffect(() => { (async () => { try { + setIsLoadingFolders(true); const res = await fetch('/api/music/folders'); if (!res.ok) throw new Error('Failed to load folders'); const data = await res.json(); @@ -138,6 +141,8 @@ export const MusicUpload: React.FC = ({ onUploadComplete }) => } } catch (e) { // ignore, keep text input fallback + } finally { + setIsLoadingFolders(false); } })(); }, []); @@ -150,7 +155,19 @@ export const MusicUpload: React.FC = ({ onUploadComplete }) => Target S3 folder - {folders.length > 0 ? ( + {isLoadingFolders ? ( + + + Loading folder structure... + + ) : folders.length > 0 ? (