Files
music-platform/server.js
hz4th_coder 595670b6ce feat: 音乐展示平台 v1.0.0
- 响应式设计,支持桌面端和手机端自适应
- 首页展示轮播图、推荐歌单、热门歌曲、新碟上架、热门歌手
- 歌曲/歌手/专辑/歌单详情页
- 底部播放器:播放/暂停、上下首、进度拖动、音量控制
- 歌词同步滚动显示
- 搜索功能
- 后台管理 /admin
- JSON文件存储
2026-07-07 23:57:31 +08:00

607 lines
21 KiB
JavaScript

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`);
});