118 lines
3.6 KiB
TypeScript
118 lines
3.6 KiB
TypeScript
import { promises as fs } from 'fs'
|
|
import { join } 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 for debugging proxy issues
|
|
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}`)
|
|
console.log('Original filename bytes:', [...filename].map(c => c.charCodeAt(0)))
|
|
|
|
// Decode the filename
|
|
try {
|
|
filename = decodeURIComponent(filename)
|
|
console.log('Decoded filename:', filename)
|
|
console.log('Decoded filename bytes:', [...filename].map(c => c.charCodeAt(0)))
|
|
} catch (error) {
|
|
console.error('Error decoding filename:', error)
|
|
// If decoding fails, use original filename
|
|
}
|
|
|
|
try {
|
|
const config = useRuntimeConfig()
|
|
const defaultPublicPath = config.public?.musicPath || '/music'
|
|
const publicRel = defaultPublicPath.replace(/^\//, '')
|
|
const musicDir = process.env.MUSIC_DIR || join(process.cwd(), 'public', publicRel)
|
|
const filePath = join(musicDir, filename)
|
|
|
|
// Security check: ensure the file is within the music directory
|
|
if (!filePath.startsWith(musicDir)) {
|
|
throw createError({
|
|
statusCode: 403,
|
|
statusMessage: 'Access denied'
|
|
})
|
|
}
|
|
|
|
// Check if file exists
|
|
try {
|
|
await fs.access(filePath)
|
|
console.log('File found successfully:', filePath)
|
|
} catch (error) {
|
|
console.log('File NOT found:', filePath)
|
|
console.log('Directory contents:')
|
|
try {
|
|
const files = await fs.readdir(musicDir)
|
|
files.forEach(file => console.log(' -', file))
|
|
} catch (e) {
|
|
console.log('Cannot read directory:', e)
|
|
}
|
|
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'
|
|
})
|
|
}
|
|
})
|