Fix music file serving path to match Docker mount point
This commit is contained in:
103
server/api/music/[filename].get.ts
Normal file
103
server/api/music/[filename].get.ts
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
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}`)
|
||||||
|
|
||||||
|
// Decode the filename
|
||||||
|
try {
|
||||||
|
filename = decodeURIComponent(filename)
|
||||||
|
console.log('Decoded filename:', filename)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error decoding filename:', error)
|
||||||
|
// If decoding fails, use original filename
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const musicDir = join(process.cwd(), 'public', 'music')
|
||||||
|
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)
|
||||||
|
} catch {
|
||||||
|
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'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user