104 lines
3.2 KiB
JavaScript
104 lines
3.2 KiB
JavaScript
import express from 'express';
|
|
import path from 'path';
|
|
import fs from 'fs/promises';
|
|
import * as mm from 'music-metadata';
|
|
import cors from 'cors';
|
|
|
|
const app = express();
|
|
app.use(cors()); // adjust for production if needed
|
|
|
|
// Base music directory
|
|
const MUSIC_DIR = path.resolve(__dirname, 'music');
|
|
const SUPPORTED = ['.flac', '.mp3', '.wav', '.m4a', '.ogg'];
|
|
|
|
// Serve static music files
|
|
app.use('/music', express.static(MUSIC_DIR, {
|
|
setHeaders: (res) => {
|
|
res.setHeader('Cache-Control', 'public, max-age=86400');
|
|
}
|
|
}));
|
|
|
|
// Recursive scan for all music files
|
|
async function scanDir(dir) {
|
|
let results = [];
|
|
const files = await fs.readdir(dir, { withFileTypes: true });
|
|
|
|
for (const file of files) {
|
|
const fullPath = path.join(dir, file.name);
|
|
if (file.isDirectory()) {
|
|
results = results.concat(await scanDir(fullPath));
|
|
} else if (SUPPORTED.includes(path.extname(file.name).toLowerCase())) {
|
|
results.push(fullPath);
|
|
}
|
|
}
|
|
return results;
|
|
}
|
|
|
|
// Parse filename into metadata
|
|
function parseFilename(filename) {
|
|
// Expected: Title__Movie__Genre__YYYY-MM-DD.ext
|
|
const base = path.basename(filename);
|
|
const noExt = base.replace(/\.[^.]+$/, '');
|
|
const parts = noExt.split('__');
|
|
|
|
return {
|
|
title: parts[0] || 'Unknown',
|
|
movie: parts[1] && parts[1] !== 'None' ? parts[1] : null,
|
|
genre: parts[2] || null,
|
|
date: parts[3] || null,
|
|
filename: base,
|
|
};
|
|
}
|
|
|
|
// API endpoint: list all tracks
|
|
app.get('/api/tracks', async (req, res) => {
|
|
try {
|
|
const files = await scanDir(MUSIC_DIR);
|
|
const tracks = [];
|
|
|
|
for (const f of files) {
|
|
const stat = await fs.stat(f);
|
|
const metadata = parseFilename(f);
|
|
|
|
metadata.path = `/music/${encodeURIComponent(path.relative(MUSIC_DIR, f))}`;
|
|
metadata.size = stat.size;
|
|
metadata.ext = path.extname(f).toLowerCase();
|
|
metadata.duration = null;
|
|
metadata.tags = {};
|
|
|
|
// Try reading embedded metadata (optional)
|
|
try {
|
|
const mmData = await mm.parseFile(f, { duration: true });
|
|
if (mmData.format.duration) metadata.duration = Math.round(mmData.format.duration);
|
|
if (mmData.common) {
|
|
metadata.tags = mmData.common;
|
|
metadata.title = mmData.common.title || metadata.title;
|
|
metadata.movie = mmData.common.album || metadata.movie;
|
|
if (!metadata.genre && mmData.common.genre) {
|
|
metadata.genre = Array.isArray(mmData.common.genre) ? mmData.common.genre.join(', ') : mmData.common.genre;
|
|
}
|
|
if (!metadata.date && mmData.common.date) metadata.date = mmData.common.date;
|
|
}
|
|
} catch (err) {
|
|
// ignore metadata errors
|
|
}
|
|
|
|
tracks.push(metadata);
|
|
}
|
|
|
|
// Sort: first by date (desc), then by filename
|
|
tracks.sort((a, b) => {
|
|
if (a.date && b.date) return String(b.date).localeCompare(String(a.date));
|
|
return a.filename.localeCompare(b.filename);
|
|
});
|
|
|
|
res.json({ tracks });
|
|
} catch (err) {
|
|
console.error(err);
|
|
res.status(500).json({ error: 'Failed to read music folder' });
|
|
}
|
|
});
|
|
|
|
// Start server
|
|
const PORT = process.env.PORT || 3001;
|
|
app.listen(PORT, () => console.log(`Music API server running at http://localhost:${PORT}`)); |