181 lines
5.4 KiB
TypeScript
181 lines
5.4 KiB
TypeScript
import { Router, Response } from 'express';
|
|
import multer from 'multer';
|
|
import path from 'path';
|
|
import { pool } from '../db/pool.js';
|
|
import { authMiddleware, AuthenticatedRequest } from '../middleware/auth.js';
|
|
import { getStorageProvider } from '../services/storage/factory.js';
|
|
|
|
const router = Router();
|
|
|
|
const upload = multer({
|
|
storage: multer.memoryStorage(),
|
|
limits: { fileSize: 50 * 1024 * 1024 }, // 50MB max
|
|
fileFilter: (_req, file, cb) => {
|
|
const allowedTypes = ['audio/mpeg', 'audio/wav', 'audio/flac', 'audio/mp3', 'audio/x-wav', 'audio/x-flac'];
|
|
if (allowedTypes.includes(file.mimetype) || file.originalname.match(/\.(mp3|wav|flac)$/i)) {
|
|
cb(null, true);
|
|
} else {
|
|
cb(new Error('Invalid file type. Only MP3, WAV, and FLAC are allowed.'));
|
|
}
|
|
}
|
|
});
|
|
|
|
// Get user's reference tracks
|
|
router.get('/', authMiddleware, async (req: AuthenticatedRequest, res: Response) => {
|
|
try {
|
|
const result = await pool.query(
|
|
`SELECT id, filename, storage_key, duration, file_size_bytes, tags, created_at
|
|
FROM reference_tracks
|
|
WHERE user_id = $1
|
|
ORDER BY created_at DESC`,
|
|
[req.user!.id]
|
|
);
|
|
|
|
const storage = getStorageProvider();
|
|
const tracks = result.rows.map(row => ({
|
|
...row,
|
|
audio_url: storage.getPublicUrl(row.storage_key)
|
|
}));
|
|
|
|
res.json({ tracks });
|
|
} catch (error) {
|
|
console.error('Get reference tracks error:', error);
|
|
res.status(500).json({ error: 'Internal server error' });
|
|
}
|
|
});
|
|
|
|
// Upload a new reference track
|
|
router.post('/', authMiddleware, upload.single('audio'), async (req: AuthenticatedRequest, res: Response) => {
|
|
try {
|
|
if (!req.file) {
|
|
res.status(400).json({ error: 'No file uploaded' });
|
|
return;
|
|
}
|
|
|
|
const userId = req.user!.id;
|
|
const originalFilename = req.file.originalname;
|
|
const ext = path.extname(originalFilename) || '.mp3';
|
|
const timestamp = Date.now();
|
|
const key = `reference-tracks/${userId}/${timestamp}${ext}`;
|
|
|
|
const storage = getStorageProvider();
|
|
await storage.upload(key, req.file.buffer, req.file.mimetype);
|
|
const audioUrl = storage.getPublicUrl(key);
|
|
|
|
// Parse tags from request body if provided
|
|
const tags = req.body.tags ? JSON.parse(req.body.tags) : null;
|
|
|
|
const result = await pool.query(
|
|
`INSERT INTO reference_tracks (user_id, filename, storage_key, file_size_bytes, tags)
|
|
VALUES ($1, $2, $3, $4, $5)
|
|
RETURNING *`,
|
|
[userId, originalFilename, key, req.file.size, tags]
|
|
);
|
|
|
|
res.status(201).json({
|
|
track: {
|
|
...result.rows[0],
|
|
audio_url: audioUrl
|
|
}
|
|
});
|
|
} catch (error) {
|
|
console.error('Upload reference track error:', error);
|
|
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
res.status(500).json({ error: 'Failed to upload reference track', details: errorMessage });
|
|
}
|
|
});
|
|
|
|
// Update reference track (duration, tags)
|
|
router.patch('/:id', authMiddleware, async (req: AuthenticatedRequest, res: Response) => {
|
|
try {
|
|
// Verify ownership
|
|
const check = await pool.query(
|
|
'SELECT user_id FROM reference_tracks WHERE id = $1',
|
|
[req.params.id]
|
|
);
|
|
if (check.rows.length === 0) {
|
|
res.status(404).json({ error: 'Track not found' });
|
|
return;
|
|
}
|
|
if (check.rows[0].user_id !== req.user!.id) {
|
|
res.status(403).json({ error: 'Access denied' });
|
|
return;
|
|
}
|
|
|
|
const { duration, tags } = req.body;
|
|
const updates: string[] = [];
|
|
const values: unknown[] = [];
|
|
let paramCount = 1;
|
|
|
|
if (duration !== undefined) {
|
|
updates.push(`duration = $${paramCount}`);
|
|
values.push(duration);
|
|
paramCount++;
|
|
}
|
|
if (tags !== undefined) {
|
|
updates.push(`tags = $${paramCount}`);
|
|
values.push(tags);
|
|
paramCount++;
|
|
}
|
|
|
|
if (updates.length === 0) {
|
|
res.status(400).json({ error: 'No fields to update' });
|
|
return;
|
|
}
|
|
|
|
values.push(req.params.id);
|
|
const result = await pool.query(
|
|
`UPDATE reference_tracks SET ${updates.join(', ')} WHERE id = $${paramCount} RETURNING *`,
|
|
values
|
|
);
|
|
|
|
const storage = getStorageProvider();
|
|
res.json({
|
|
track: {
|
|
...result.rows[0],
|
|
audio_url: storage.getPublicUrl(result.rows[0].storage_key)
|
|
}
|
|
});
|
|
} catch (error) {
|
|
console.error('Update reference track error:', error);
|
|
res.status(500).json({ error: 'Internal server error' });
|
|
}
|
|
});
|
|
|
|
// Delete a reference track
|
|
router.delete('/:id', authMiddleware, async (req: AuthenticatedRequest, res: Response) => {
|
|
try {
|
|
// Verify ownership
|
|
const check = await pool.query(
|
|
'SELECT user_id, storage_key FROM reference_tracks WHERE id = $1',
|
|
[req.params.id]
|
|
);
|
|
if (check.rows.length === 0) {
|
|
res.status(404).json({ error: 'Track not found' });
|
|
return;
|
|
}
|
|
if (check.rows[0].user_id !== req.user!.id) {
|
|
res.status(403).json({ error: 'Access denied' });
|
|
return;
|
|
}
|
|
|
|
// Delete from storage
|
|
const storage = getStorageProvider();
|
|
try {
|
|
await storage.delete(check.rows[0].storage_key);
|
|
} catch (storageError) {
|
|
console.error('Failed to delete from storage:', storageError);
|
|
}
|
|
|
|
// Delete from database
|
|
await pool.query('DELETE FROM reference_tracks WHERE id = $1', [req.params.id]);
|
|
|
|
res.json({ success: true });
|
|
} catch (error) {
|
|
console.error('Delete reference track error:', error);
|
|
res.status(500).json({ error: 'Internal server error' });
|
|
}
|
|
});
|
|
|
|
export default router;
|