import { promises as fs } from 'fs' import { join, resolve, sep, isAbsolute } from 'path' import { createReadStream } from 'fs' export default defineEventHandler(async (event) => { let filename = getRouterParam(event, 'filename') if (!filename) { throw createError({ statusCode: 400, statusMessage: 'Filename is required' }) } // Log incoming request const headers = getHeaders(event) const realIP = headers['x-real-ip'] || headers['x-forwarded-for'] || 'unknown' console.log(`[MUSIC API] Request from ${realIP} for file: ${filename}`) // Decode the filename try { filename = decodeURIComponent(filename) } catch (error) { console.error('Error decoding filename:', error) // If decoding fails, use original filename } try { const config = useRuntimeConfig() // Determine the music directory path let musicDir: string if (process.env.MUSIC_DIR) { // Resolve env to absolute musicDir = isAbsolute(process.env.MUSIC_DIR) ? process.env.MUSIC_DIR : resolve(process.cwd(), process.env.MUSIC_DIR) } else { // Fallback to public/music const defaultPublicPath = config.public?.musicPath || '/music' const publicRel = defaultPublicPath.replace(/^\//, '') musicDir = resolve(process.cwd(), 'public', publicRel) } // Resolve the full file path const filePath = resolve(musicDir, filename) // Security check: ensure the file is within the music directory // Normalize both paths and add separator to ensure exact directory match const normalizedMusicDir = musicDir.endsWith(sep) ? musicDir : musicDir + sep if (!filePath.startsWith(normalizedMusicDir)) { throw createError({ statusCode: 403, statusMessage: 'Access denied' }) } // Check if file exists try { await fs.access(filePath) } catch (error) { throw createError({ statusCode: 404, statusMessage: 'File not found' }) } // Get file stats const stats = await fs.stat(filePath) // Set appropriate headers const headers = getHeaders(event) // Handle range requests for audio streaming const range = headers.range if (range) { const parts = range.replace(/bytes=/, "").split("-") const start = parseInt(parts[0], 10) const end = parts[1] ? parseInt(parts[1], 10) : stats.size - 1 const chunksize = (end - start) + 1 setHeader(event, 'Content-Range', `bytes ${start}-${end}/${stats.size}`) setHeader(event, 'Accept-Ranges', 'bytes') setHeader(event, 'Content-Length', chunksize.toString()) setResponseStatus(event, 206) return sendStream(event, createReadStream(filePath, { start, end })) } else { // Send entire file setHeader(event, 'Content-Length', stats.size.toString()) setHeader(event, 'Accept-Ranges', 'bytes') // Set content type based on file extension const ext = filename.toLowerCase().split('.').pop() const contentTypes = { 'mp3': 'audio/mpeg', 'wav': 'audio/wav', 'flac': 'audio/flac', 'm4a': 'audio/mp4', 'ogg': 'audio/ogg', 'aac': 'audio/aac' } setHeader(event, 'Content-Type', contentTypes[ext] || 'audio/mpeg') return sendStream(event, createReadStream(filePath)) } } catch (error) { if (error.statusCode) { throw error } console.error('Error serving music file:', error) throw createError({ statusCode: 500, statusMessage: 'Failed to serve music file' }) } })