const express = require('express'); const path = require('path'); const multer = require('multer'); const { v4: uuidv4 } = require('uuid'); const fs = require('fs'); const app = express(); const PORT = process.env.PORT || 16031; // 中间件 app.use(express.json()); app.use(express.urlencoded({ extended: true })); app.use(express.static(path.join(__dirname, 'public'))); app.use('/uploads', express.static(path.join(__dirname, 'uploads'))); // 文件上传配置 const storage = multer.diskStorage({ destination: (req, file, cb) => { if (file.fieldname === 'music') { cb(null, path.join(__dirname, 'uploads/music')); } else { cb(null, path.join(__dirname, 'uploads/covers')); } }, filename: (req, file, cb) => { const ext = path.extname(file.originalname); cb(null, uuidv4() + ext); } }); const upload = multer({ storage, limits: { fileSize: 50 * 1024 * 1024 } }); // ============ JSON 数据库 ============ const DB_PATH = path.join(__dirname, 'data.json'); function loadData() { if (fs.existsSync(DB_PATH)) { return JSON.parse(fs.readFileSync(DB_PATH, 'utf8')); } return { artists: [], albums: [], songs: [], playlists: [], playlist_songs: [], banners: [], _counters: { artists: 0, albums: 0, songs: 0, playlists: 0, banners: 0 } }; } function saveData(data) { fs.writeFileSync(DB_PATH, JSON.stringify(data, null, 2)); } function nextId(data, table) { data._counters[table] = (data._counters[table] || 0) + 1; return data._counters[table]; } // 初始化示例数据 function initSampleData() { const data = loadData(); if (data.artists.length > 0) return; // 歌手 const artists = [ { name: '周杰伦', avatar: '/images/artist1.jpg', bio: '华语流行男歌手、音乐人、制作人' }, { name: '林俊杰', avatar: '/images/artist2.jpg', bio: '新加坡华语流行男歌手、词曲创作者' }, { name: '邓紫棋', avatar: '/images/artist3.jpg', bio: '中国香港女歌手、词曲作者' }, { name: '薛之谦', avatar: '/images/artist4.jpg', bio: '华语流行男歌手、音乐制作人' }, { name: 'Taylor Swift', avatar: '/images/artist5.jpg', bio: 'American singer-songwriter' } ]; artists.forEach(a => { const id = nextId(data, 'artists'); data.artists.push({ id, ...a, created_at: new Date().toISOString() }); }); // 专辑 const albums = [ { title: '范特西', artist_id: 1, cover: '/covers/fantasy.jpg', description: '周杰伦第二张录音室专辑', release_date: '2001-09-14' }, { title: '叶惠美', artist_id: 1, cover: '/covers/yehuimei.jpg', description: '周杰伦第四张录音室专辑', release_date: '2003-07-31' }, { title: '学不会', artist_id: 2, cover: '/covers/learn.jpg', description: '林俊杰专辑', release_date: '2011-12-31' }, { title: '新的心跳', artist_id: 3, cover: '/covers/newheart.jpg', description: '邓紫棋专辑', release_date: '2015-11-06' } ]; albums.forEach(a => { const id = nextId(data, 'albums'); data.albums.push({ id, ...a, created_at: new Date().toISOString() }); }); // 歌曲 const songs = [ { title: '晴天', artist_id: 1, album_id: 1, file_path: '/music/qingtian.mp3', cover: '/covers/qingtian.jpg', duration: 269, play_count: 1580000, lyrics: `[ti:晴天] [ar:周杰伦] [al:范特西] [00:00.00]晴天 - 周杰伦 [00:05.00]词:周杰伦 [00:10.00]曲:周杰伦 [00:15.00]故事的小黄花 [00:18.00]从出生那年就飘着 [00:22.00]童年的荡秋千 [00:25.00]随记忆一直晃到现在 [00:30.00]Re So So Si Do Si La [00:34.00]So La Si Si Si Si La Si La So [00:40.00]吹着前奏望着天空 [00:44.00]我想起花瓣试着掉落 [00:50.00]为你翘课的那一天 [00:53.00]花落的那一天 [00:55.00]教室的那一间 [00:57.00]我怎么看不见 [01:00.00]消失的雨天 [01:02.00]我好想再淋一次 [01:06.00]没想到失去的勇气我还留着 [01:12.00]好想再问一遍 [01:15.00]你会等待还是离开` }, { title: '七里香', artist_id: 1, album_id: 2, file_path: '/music/qilixiang.mp3', cover: '/covers/qilixiang.jpg', duration: 299, play_count: 1200000, lyrics: `[ti:七里香] [ar:周杰伦] [al:叶惠美] [00:00.00]七里香 - 周杰伦 [00:05.00]词:方文山 [00:10.00]曲:周杰伦 [00:15.00]窗外的麻雀 [00:18.00]在电线杆上多嘴 [00:22.00]你说这一句 [00:25.00]很有夏天的感觉 [00:30.00]手中的铅笔 [00:33.00]在纸上来回自在 [00:37.00]我用几行字形容你是我的谁 [00:44.00]秋刀鱼的滋味 [00:47.00]猫跟你都想了解 [00:50.00]初恋的香味就这样被我们寻回 [00:57.00]那温暖的阳光 [01:00.00]像刚摘的鲜艳草莓 [01:04.00]你说你舍不得吃掉这一种感觉` }, { title: '江南', artist_id: 2, album_id: 3, file_path: '/music/jiangnan.mp3', cover: '/covers/jiangnan.jpg', duration: 274, play_count: 980000, lyrics: `[ti:江南] [ar:林俊杰] [al:学不会] [00:00.00]江南 - 林俊杰 [00:05.00]词:李瑞洵 [00:10.00]曲:林俊杰 [00:15.00]风到这里就是粘 [00:19.00]粘住过客的思念 [00:23.00]雨到了这里缠成线 [00:27.00]缠着我们流连人世间 [00:32.00]你在身边就是缘 [00:36.00]思念写在被子上 [00:40.00]等待秋风吹天天 [00:44.00]等你等到烟火灭` }, { title: '光年之外', artist_id: 3, album_id: 4, file_path: '/music/guangnian.mp3', cover: '/covers/guangnian.jpg', duration: 235, play_count: 1350000, lyrics: `[ti:光年之外] [ar:邓紫棋] [al:新的心跳] [00:00.00]光年之外 - 邓紫棋 [00:05.00]词:邓紫棋 [00:10.00]曲:邓紫棋 [00:15.00]感受停在我发端的指尖 [00:19.00]如何瞬间冻结时间 [00:23.00]记住望着我坚定的双眼 [00:27.00]也许已经没有明天 [00:32.00]面对浩瀚的星海 [00:36.00]彼此微小的依赖 [00:40.00]看着宇宙在旋转 [00:44.00]我们只身漂流外` } ]; songs.forEach(s => { const id = nextId(data, 'songs'); data.songs.push({ id, ...s, created_at: new Date().toISOString() }); }); // 歌单 const playlists = [ { title: '华语经典', description: '精选华语乐坛经典好歌', cover: '/covers/playlist1.jpg', is_featured: 1 }, { title: '流行热歌', description: '最新最热的流行歌曲', cover: '/covers/playlist2.jpg', is_featured: 1 }, { title: '治愈系', description: '温暖你的每一首歌', cover: '/covers/playlist3.jpg', is_featured: 1 }, { title: '欧美金曲', description: '经典欧美流行音乐', cover: '/covers/playlist4.jpg', is_featured: 0 } ]; playlists.forEach(p => { const id = nextId(data, 'playlists'); data.playlists.push({ id, ...p, created_at: new Date().toISOString() }); }); // 歌单歌曲关联 data.playlist_songs = [ { playlist_id: 1, song_id: 1, sort_order: 1 }, { playlist_id: 1, song_id: 2, sort_order: 2 }, { playlist_id: 1, song_id: 3, sort_order: 3 }, { playlist_id: 2, song_id: 4, sort_order: 1 }, { playlist_id: 2, song_id: 1, sort_order: 2 } ]; // 轮播图 const banners = [ { title: '新歌首发', image: '/images/banner1.jpg', link: '/song/1', sort_order: 1, is_active: 1 }, { title: '热门歌单推荐', image: '/images/banner2.jpg', link: '/playlist/1', sort_order: 2, is_active: 1 }, { title: '经典回顾', image: '/images/banner3.jpg', link: '/album/1', sort_order: 3, is_active: 1 } ]; banners.forEach(b => { const id = nextId(data, 'banners'); data.banners.push({ id, ...b, created_at: new Date().toISOString() }); }); saveData(data); } initSampleData(); // ============ 辅助函数 ============ function getArtistName(artist_id) { const data = loadData(); const artist = data.artists.find(a => a.id === artist_id); return artist ? artist.name : null; } function getAlbumTitle(album_id) { const data = loadData(); const album = data.albums.find(a => a.id === album_id); return album ? album.title : null; } function enrichSong(song) { return { ...song, artist_name: getArtistName(song.artist_id), album_title: getAlbumTitle(song.album_id) }; } // ============ API 路由 ============ // 获取首页数据 app.get('/api/home', (req, res) => { const data = loadData(); const banners = data.banners.filter(b => b.is_active === 1).sort((a, b) => a.sort_order - b.sort_order); const featuredPlaylists = data.playlists.filter(p => p.is_featured === 1).slice(0, 6); const hotSongs = [...data.songs].sort((a, b) => b.play_count - a.play_count).slice(0, 20).map(enrichSong); const newSongs = [...data.songs].sort((a, b) => new Date(b.created_at) - new Date(a.created_at)).slice(0, 10).map(enrichSong); const artists = data.artists.slice(0, 8); const albums = data.albums.map(a => ({ ...a, artist_name: getArtistName(a.artist_id) })).sort((a, b) => new Date(b.release_date) - new Date(a.release_date)).slice(0, 8); res.json({ banners, featuredPlaylists, hotSongs, newSongs, artists, albums }); }); // 获取歌曲列表 app.get('/api/songs', (req, res) => { const data = loadData(); const { page = 1, limit = 20, artist_id, album_id } = req.query; let songs = data.songs; if (artist_id) songs = songs.filter(s => s.artist_id === parseInt(artist_id)); if (album_id) songs = songs.filter(s => s.album_id === parseInt(album_id)); songs = songs.sort((a, b) => new Date(b.created_at) - new Date(a.created_at)); const total = songs.length; const offset = (page - 1) * limit; songs = songs.slice(offset, offset + parseInt(limit)).map(enrichSong); res.json({ songs, total, page: parseInt(page), limit: parseInt(limit) }); }); // 获取单曲详情 app.get('/api/songs/:id', (req, res) => { const data = loadData(); const song = data.songs.find(s => s.id === parseInt(req.params.id)); if (!song) return res.status(404).json({ error: '歌曲不存在' }); res.json(enrichSong(song)); }); // 增加播放次数 app.post('/api/songs/:id/play', (req, res) => { const data = loadData(); const song = data.songs.find(s => s.id === parseInt(req.params.id)); if (song) { song.play_count = (song.play_count || 0) + 1; saveData(data); } res.json({ success: true }); }); // 获取歌手列表 app.get('/api/artists', (req, res) => { const data = loadData(); res.json(data.artists.sort((a, b) => a.name.localeCompare(b.name))); }); // 获取歌手详情 app.get('/api/artists/:id', (req, res) => { const data = loadData(); const artist = data.artists.find(a => a.id === parseInt(req.params.id)); if (!artist) return res.status(404).json({ error: '歌手不存在' }); const songs = data.songs.filter(s => s.artist_id === artist.id).map(enrichSong); const albums = data.albums.filter(a => a.artist_id === artist.id).map(a => ({ ...a, artist_name: artist.name })); res.json({ ...artist, songs, albums }); }); // 获取专辑列表 app.get('/api/albums', (req, res) => { const data = loadData(); const albums = data.albums.map(a => ({ ...a, artist_name: getArtistName(a.artist_id) })); res.json(albums.sort((a, b) => new Date(b.created_at) - new Date(a.created_at))); }); // 获取专辑详情 app.get('/api/albums/:id', (req, res) => { const data = loadData(); const album = data.albums.find(a => a.id === parseInt(req.params.id)); if (!album) return res.status(404).json({ error: '专辑不存在' }); const songs = data.songs.filter(s => s.album_id === album.id).map(enrichSong); const artist_name = getArtistName(album.artist_id); res.json({ ...album, artist_name, songs }); }); // 获取歌单列表 app.get('/api/playlists', (req, res) => { const data = loadData(); res.json(data.playlists.sort((a, b) => new Date(b.created_at) - new Date(a.created_at))); }); // 获取歌单详情 app.get('/api/playlists/:id', (req, res) => { const data = loadData(); const playlist = data.playlists.find(p => p.id === parseInt(req.params.id)); if (!playlist) return res.status(404).json({ error: '歌单不存在' }); const songIds = data.playlist_songs.filter(ps => ps.playlist_id === playlist.id).sort((a, b) => a.sort_order - b.sort_order).map(ps => ps.song_id); const songs = songIds.map(id => data.songs.find(s => s.id === id)).filter(Boolean).map(enrichSong); res.json({ ...playlist, songs }); }); // 搜索 app.get('/api/search', (req, res) => { const data = loadData(); const { q } = req.query; if (!q) return res.json({ songs: [], artists: [], albums: [] }); const query = q.toLowerCase(); const songs = data.songs.filter(s => { const artistName = getArtistName(s.artist_id) || ''; return s.title.toLowerCase().includes(query) || artistName.toLowerCase().includes(query); }).slice(0, 20).map(enrichSong); const artists = data.artists.filter(a => a.name.toLowerCase().includes(query)).slice(0, 10); const albums = data.albums.filter(a => { const artistName = getArtistName(a.artist_id) || ''; return a.title.toLowerCase().includes(query) || artistName.toLowerCase().includes(query); }).slice(0, 10).map(a => ({ ...a, artist_name: getArtistName(a.artist_id) })); res.json({ songs, artists, albums }); }); // ============ 后台管理 API ============ // 上传音乐文件 app.post('/api/admin/upload/music', upload.single('music'), (req, res) => { if (!req.file) return res.status(400).json({ error: '未选择文件' }); res.json({ path: '/uploads/music/' + req.file.filename }); }); // 上传封面图片 app.post('/api/admin/upload/cover', upload.single('cover'), (req, res) => { if (!req.file) return res.status(400).json({ error: '未选择文件' }); res.json({ path: '/uploads/covers/' + req.file.filename }); }); // 管理 - 歌曲 CRUD app.get('/api/admin/songs', (req, res) => { const data = loadData(); res.json(data.songs.map(enrichSong).sort((a, b) => new Date(b.created_at) - new Date(a.created_at))); }); app.post('/api/admin/songs', (req, res) => { const data = loadData(); const { title, artist_id, album_id, file_path, cover, duration, lyrics } = req.body; const id = nextId(data, 'songs'); data.songs.push({ id, title, artist_id: artist_id ? parseInt(artist_id) : null, album_id: album_id ? parseInt(album_id) : null, file_path, cover: cover || '', duration: parseFloat(duration) || 0, lyrics: lyrics || '', play_count: 0, created_at: new Date().toISOString() }); saveData(data); res.json({ id }); }); app.put('/api/admin/songs/:id', (req, res) => { const data = loadData(); const { title, artist_id, album_id, file_path, cover, duration, lyrics } = req.body; const song = data.songs.find(s => s.id === parseInt(req.params.id)); if (song) { song.title = title; song.artist_id = artist_id ? parseInt(artist_id) : null; song.album_id = album_id ? parseInt(album_id) : null; song.file_path = file_path; song.cover = cover || ''; song.duration = parseFloat(duration) || 0; song.lyrics = lyrics || ''; saveData(data); } res.json({ success: true }); }); app.delete('/api/admin/songs/:id', (req, res) => { const data = loadData(); data.songs = data.songs.filter(s => s.id !== parseInt(req.params.id)); saveData(data); res.json({ success: true }); }); // 管理 - 歌手 CRUD app.get('/api/admin/artists', (req, res) => { const data = loadData(); res.json(data.artists.sort((a, b) => new Date(b.created_at) - new Date(a.created_at))); }); app.post('/api/admin/artists', (req, res) => { const data = loadData(); const { name, avatar, bio } = req.body; const id = nextId(data, 'artists'); data.artists.push({ id, name, avatar: avatar || '', bio: bio || '', created_at: new Date().toISOString() }); saveData(data); res.json({ id }); }); app.put('/api/admin/artists/:id', (req, res) => { const data = loadData(); const { name, avatar, bio } = req.body; const artist = data.artists.find(a => a.id === parseInt(req.params.id)); if (artist) { artist.name = name; artist.avatar = avatar || ''; artist.bio = bio || ''; saveData(data); } res.json({ success: true }); }); app.delete('/api/admin/artists/:id', (req, res) => { const data = loadData(); data.artists = data.artists.filter(a => a.id !== parseInt(req.params.id)); saveData(data); res.json({ success: true }); }); // 管理 - 专辑 CRUD app.get('/api/admin/albums', (req, res) => { const data = loadData(); res.json(data.albums.map(a => ({ ...a, artist_name: getArtistName(a.artist_id) })).sort((a, b) => new Date(b.created_at) - new Date(a.created_at))); }); app.post('/api/admin/albums', (req, res) => { const data = loadData(); const { title, artist_id, cover, description, release_date } = req.body; const id = nextId(data, 'albums'); data.albums.push({ id, title, artist_id: artist_id ? parseInt(artist_id) : null, cover: cover || '', description: description || '', release_date: release_date || '', created_at: new Date().toISOString() }); saveData(data); res.json({ id }); }); app.put('/api/admin/albums/:id', (req, res) => { const data = loadData(); const { title, artist_id, cover, description, release_date } = req.body; const album = data.albums.find(a => a.id === parseInt(req.params.id)); if (album) { album.title = title; album.artist_id = artist_id ? parseInt(artist_id) : null; album.cover = cover || ''; album.description = description || ''; album.release_date = release_date || ''; saveData(data); } res.json({ success: true }); }); app.delete('/api/admin/albums/:id', (req, res) => { const data = loadData(); data.albums = data.albums.filter(a => a.id !== parseInt(req.params.id)); saveData(data); res.json({ success: true }); }); // 管理 - 歌单 CRUD app.get('/api/admin/playlists', (req, res) => { const data = loadData(); res.json(data.playlists.sort((a, b) => new Date(b.created_at) - new Date(a.created_at))); }); app.post('/api/admin/playlists', (req, res) => { const data = loadData(); const { title, description, cover, is_featured } = req.body; const id = nextId(data, 'playlists'); data.playlists.push({ id, title, description: description || '', cover: cover || '', is_featured: is_featured ? 1 : 0, created_at: new Date().toISOString() }); saveData(data); res.json({ id }); }); app.put('/api/admin/playlists/:id', (req, res) => { const data = loadData(); const { title, description, cover, is_featured } = req.body; const playlist = data.playlists.find(p => p.id === parseInt(req.params.id)); if (playlist) { playlist.title = title; playlist.description = description || ''; playlist.cover = cover || ''; playlist.is_featured = is_featured ? 1 : 0; saveData(data); } res.json({ success: true }); }); app.delete('/api/admin/playlists/:id', (req, res) => { const data = loadData(); data.playlists = data.playlists.filter(p => p.id !== parseInt(req.params.id)); saveData(data); res.json({ success: true }); }); // 歌单添加歌曲 app.post('/api/admin/playlists/:id/songs', (req, res) => { const data = loadData(); const { song_ids } = req.body; song_ids.forEach((id, idx) => { const exists = data.playlist_songs.find(ps => ps.playlist_id === parseInt(req.params.id) && ps.song_id === id); if (!exists) { data.playlist_songs.push({ playlist_id: parseInt(req.params.id), song_id: id, sort_order: idx + 1 }); } }); saveData(data); res.json({ success: true }); }); // 管理 - 轮播图 CRUD app.get('/api/admin/banners', (req, res) => { const data = loadData(); res.json(data.banners.sort((a, b) => a.sort_order - b.sort_order)); }); app.post('/api/admin/banners', (req, res) => { const data = loadData(); const { title, image, link, sort_order, is_active } = req.body; const id = nextId(data, 'banners'); data.banners.push({ id, title, image, link: link || '', sort_order: parseInt(sort_order) || 0, is_active: is_active !== undefined ? is_active : 1, created_at: new Date().toISOString() }); saveData(data); res.json({ id }); }); app.put('/api/admin/banners/:id', (req, res) => { const data = loadData(); const { title, image, link, sort_order, is_active } = req.body; const banner = data.banners.find(b => b.id === parseInt(req.params.id)); if (banner) { banner.title = title; banner.image = image; banner.link = link || ''; banner.sort_order = parseInt(sort_order) || 0; banner.is_active = is_active; saveData(data); } res.json({ success: true }); }); app.delete('/api/admin/banners/:id', (req, res) => { const data = loadData(); data.banners = data.banners.filter(b => b.id !== parseInt(req.params.id)); saveData(data); res.json({ success: true }); }); // 统计信息 app.get('/api/admin/stats', (req, res) => { const data = loadData(); const songCount = data.songs.length; const artistCount = data.artists.length; const albumCount = data.albums.length; const playlistCount = data.playlists.length; const totalPlays = data.songs.reduce((sum, s) => sum + (s.play_count || 0), 0); res.json({ songCount, artistCount, albumCount, playlistCount, totalPlays }); }); // SPA 路由 - 所有未匹配的路由返回 index.html app.get('*', (req, res) => { if (req.path.startsWith('/admin')) { res.sendFile(path.join(__dirname, 'public', 'admin.html')); } else { res.sendFile(path.join(__dirname, 'public', 'index.html')); } }); app.listen(PORT, '0.0.0.0', () => { console.log(`🎵 音乐平台已启动: http://0.0.0.0:${PORT}`); console.log(`📋 后台管理: http://0.0.0.0:${PORT}/admin`); });