feat: 本地音乐播放器

This commit is contained in:
熊潇 2025-02-25 23:32:01 +08:00
parent 9eb68a69d7
commit 4cd8cfa523
12 changed files with 252 additions and 21 deletions

View File

@ -1,3 +1,4 @@
{ {
port: 3000, port: 3000,
musicDir: '/Users/xion/dev/router-template/music'
} }

View File

@ -1,5 +1,5 @@
{ {
"name": "router", "name": "music-server",
"version": "0.0.1", "version": "0.0.1",
"description": "", "description": "",
"main": "index.js", "main": "index.js",
@ -9,12 +9,12 @@
"test": "tsx test/**/*.ts", "test": "tsx test/**/*.ts",
"dev:watch": "cross-env NODE_ENV=development concurrently -n \"Watch,Dev\" -c \"green,blue\" \"npm run watch\" \"sleep 1 && npm run dev\" ", "dev:watch": "cross-env NODE_ENV=development concurrently -n \"Watch,Dev\" -c \"green,blue\" \"npm run watch\" \"sleep 1 && npm run dev\" ",
"build": "rimraf dist && rollup -c rollup.config.mjs", "build": "rimraf dist && rollup -c rollup.config.mjs",
"deploy": "rsync -avz --delete ./dist/ --exclude='app.config.json5' light:~/apps/router/dist", "deploy": "rsync -avz --delete ./dist/ --exclude='app.config.json5' light:~/apps/music-server/dist",
"clean": "rm -rf dist", "clean": "rm -rf dist",
"reload": "ssh light pm2 restart router", "reload": "ssh light pm2 restart music-server",
"pub": "npm run build && npm run deploy && npm run reload", "pub": "npm run build && npm run deploy && npm run reload",
"deploy:nova": "rsync -avz --delete ./dist/ --exclude='app.config.json5' nova:~/apps/router/dist", "deploy:nova": "rsync -avz --delete ./dist/ --exclude='app.config.json5' nova:~/apps/music-server/dist",
"start": "pm2 start dist/app.mjs --name router" "start": "pm2 start dist/app.mjs --name music-server"
}, },
"keywords": [], "keywords": [],
"author": "abearxiong <xiongxiao@xiongxiao.me>", "author": "abearxiong <xiongxiao@xiongxiao.me>",
@ -27,7 +27,7 @@
"src" "src"
], ],
"dependencies": { "dependencies": {
"@kevisual/router": "^0.0.6-alpha-5", "@kevisual/router": "^0.0.6",
"dayjs": "^1.11.13", "dayjs": "^1.11.13",
"formidable": "^3.5.2", "formidable": "^3.5.2",
"json5": "^2.2.3", "json5": "^2.2.3",
@ -44,7 +44,7 @@
"@types/crypto-js": "^4.2.2", "@types/crypto-js": "^4.2.2",
"@types/formidable": "^3.4.5", "@types/formidable": "^3.4.5",
"@types/lodash-es": "^4.17.12", "@types/lodash-es": "^4.17.12",
"@types/node": "^22.13.4", "@types/node": "^22.13.5",
"concurrently": "^9.1.2", "concurrently": "^9.1.2",
"cross-env": "^7.0.3", "cross-env": "^7.0.3",
"nodemon": "^3.1.9", "nodemon": "^3.1.9",

28
pnpm-lock.yaml generated
View File

@ -9,8 +9,8 @@ importers:
.: .:
dependencies: dependencies:
'@kevisual/router': '@kevisual/router':
specifier: ^0.0.6-alpha-5 specifier: ^0.0.6
version: 0.0.6-alpha-5 version: 0.0.6
dayjs: dayjs:
specifier: ^1.11.13 specifier: ^1.11.13
version: 1.11.13 version: 1.11.13
@ -55,8 +55,8 @@ importers:
specifier: ^4.17.12 specifier: ^4.17.12
version: 4.17.12 version: 4.17.12
'@types/node': '@types/node':
specifier: ^22.13.4 specifier: ^22.13.5
version: 22.13.4 version: 22.13.5
concurrently: concurrently:
specifier: ^9.1.2 specifier: ^9.1.2
version: 9.1.2 version: 9.1.2
@ -261,8 +261,8 @@ packages:
'@jridgewell/sourcemap-codec@1.5.0': '@jridgewell/sourcemap-codec@1.5.0':
resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==} resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==}
'@kevisual/router@0.0.6-alpha-5': '@kevisual/router@0.0.6':
resolution: {integrity: sha512-YT9cxzzFKjWyE05MYlvhuAp16ymgmwThSMHrr2PNbmnZiYgUqm3O4j8cny40lOhZB4Jy/4nQb9Ql2laL+mZ4zg==} resolution: {integrity: sha512-7FQUY87Zy5A4V30OAggRbGpO/Asd7SUpnhHv8mlxnSFFTto25xpXmjHYp12mu/HJTsHM7RTaxVEyD1DeP44D2A==}
'@kevisual/use-config@1.0.7': '@kevisual/use-config@1.0.7':
resolution: {integrity: sha512-2W1iXdiypugQVgjAz8AGWDVUIcBtegdzLV0FPKq1Rm065yB1EWcI0u0d6qFaAw1RWqtT8o0GT3sR3tzg7nWdjA==} resolution: {integrity: sha512-2W1iXdiypugQVgjAz8AGWDVUIcBtegdzLV0FPKq1Rm065yB1EWcI0u0d6qFaAw1RWqtT8o0GT3sR3tzg7nWdjA==}
@ -493,8 +493,8 @@ packages:
'@types/node-forge@1.3.11': '@types/node-forge@1.3.11':
resolution: {integrity: sha512-FQx220y22OKNTqaByeBGqHWYz4cl94tpcxeFdvBo3wjG6XPBuZ0BNgNZRV5J5TFmmcsJ4IzsLkmGRiQbnYsBEQ==} resolution: {integrity: sha512-FQx220y22OKNTqaByeBGqHWYz4cl94tpcxeFdvBo3wjG6XPBuZ0BNgNZRV5J5TFmmcsJ4IzsLkmGRiQbnYsBEQ==}
'@types/node@22.13.4': '@types/node@22.13.5':
resolution: {integrity: sha512-ywP2X0DYtX3y08eFVx5fNIw7/uIv8hYUKgXoK8oayJlLnKcRfEYCxWMVE1XagUdVtCJlZT1AU4LXEABW+L1Peg==} resolution: {integrity: sha512-+lTU0PxZXn0Dr1NBtC7Y8cR21AJr87dLLU953CWA6pMxxv/UDc7jYAY90upcrie1nRcD6XNG5HOYEDtgW5TxAg==}
'@types/resolve@1.20.2': '@types/resolve@1.20.2':
resolution: {integrity: sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==} resolution: {integrity: sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==}
@ -1930,7 +1930,7 @@ snapshots:
'@jridgewell/sourcemap-codec@1.5.0': {} '@jridgewell/sourcemap-codec@1.5.0': {}
'@kevisual/router@0.0.6-alpha-5': '@kevisual/router@0.0.6':
dependencies: dependencies:
path-to-regexp: 8.2.0 path-to-regexp: 8.2.0
selfsigned: 2.4.1 selfsigned: 2.4.1
@ -2134,16 +2134,16 @@ snapshots:
'@types/formidable@3.4.5': '@types/formidable@3.4.5':
dependencies: dependencies:
'@types/node': 22.13.4 '@types/node': 22.13.5
'@types/fs-extra@8.1.5': '@types/fs-extra@8.1.5':
dependencies: dependencies:
'@types/node': 22.13.4 '@types/node': 22.13.5
'@types/glob@7.2.0': '@types/glob@7.2.0':
dependencies: dependencies:
'@types/minimatch': 5.1.2 '@types/minimatch': 5.1.2
'@types/node': 22.13.4 '@types/node': 22.13.5
'@types/lodash-es@4.17.12': '@types/lodash-es@4.17.12':
dependencies: dependencies:
@ -2155,9 +2155,9 @@ snapshots:
'@types/node-forge@1.3.11': '@types/node-forge@1.3.11':
dependencies: dependencies:
'@types/node': 22.13.4 '@types/node': 22.13.5
'@types/node@22.13.4': '@types/node@22.13.5':
dependencies: dependencies:
undici-types: 6.20.0 undici-types: 6.20.0

View File

@ -1,8 +1,10 @@
import { App } from '@kevisual/router'; import { App } from '@kevisual/router';
import { useContextKey, useContext } from '@kevisual/use-config/context'; import { useContextKey, useContext } from '@kevisual/use-config/context';
import { MusicService } from './services/index.ts';
const init = () => { const init = () => {
return new App(); return new App();
}; };
export const musicService = new MusicService();
export const app = useContextKey('app', init); export const app = useContextKey('app', init);

View File

@ -1,8 +1,11 @@
import { app } from './app.ts'; import { app } from './app.ts';
import { useConfig } from '@kevisual/use-config'; import { useConfig } from '@kevisual/use-config';
import './demo-route.ts'; import './demo-route.ts';
import './routes/index.ts';
import { middleware } from './simple-routes/upload.ts';
const config = useConfig(); const config = useConfig();
app.listen(config.port, () => { app.listen(config.port, () => {
console.log(`server is running at http://localhost:${config.port}`); console.log(`server is running at http://localhost:${config.port}`);
}); });
app.server.on(middleware);

View File

@ -0,0 +1,38 @@
import { exec } from 'child_process';
import path from 'path';
export function playMusic(musicPath: string): void {
const musicFilePath = path.resolve(musicPath);
let command = '';
switch (process.platform) {
case 'win32':
command = `start ${musicFilePath}`; // Windows
break;
case 'darwin':
command = `afplay ${musicFilePath}`; // macOS
break;
case 'linux':
command = `mpg123 ${musicFilePath}`; // Linux
break;
default:
throw new Error('Unsupported platform');
}
exec(command);
}
export function stopMusic(): void {
switch (process.platform) {
case 'win32':
exec('taskkill /IM mpg123.exe /F'); // Windows
break;
case 'darwin':
exec('killall afplay'); // macOS
break;
case 'linux':
exec('killall mpg123'); // Linux
break;
default:
throw new Error('Unsupported platform');
}
}

37
src/libs/music-lib.ts Normal file
View File

@ -0,0 +1,37 @@
import { exec } from 'child_process';
import path from 'path';
export function playMusic(musicPath: string): void {
const musicFilePath = path.resolve(musicPath);
let command = '';
switch (process.platform) {
case 'win32':
command = `start ${musicFilePath}`; // Windows
break;
case 'darwin':
command = `afplay ${musicFilePath}`; // macOS
break;
case 'linux':
command = `mpg123 ${musicFilePath}`; // Linux
break;
default:
throw new Error('Unsupported platform');
}
exec(command);
}
export function stopMusic(): void {
switch (process.platform) {
case 'win32':
exec('taskkill /IM mpg123.exe /F'); // Windows
break;
case 'darwin':
exec('killall afplay'); // macOS
break;
case 'linux':
exec('killall mpg123'); // Linux
break;
default:
throw new Error('Unsupported platform');
}
}

16
src/modules/config.ts Normal file
View File

@ -0,0 +1,16 @@
import path from 'path';
import { useConfig } from '@kevisual/use-config';
import fs from 'fs';
export const configPath = path.join(process.cwd(), 'app.config.json5');
export const saveConfig = (config: any) => {
fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
};
export const getConfig = () => {
return useConfig();
};
export const setConfig = (config: any) => {
saveConfig(config);
};

1
src/routes/index.ts Normal file
View File

@ -0,0 +1 @@
import './music/list.ts';

39
src/routes/music/list.ts Normal file
View File

@ -0,0 +1,39 @@
import { app, musicService } from '../../app.ts';
// 配置音乐文件夹
// 获取音乐列表
console.log(`http://localhost:3000/api/router?path=music&key=musicDirSet&musicDir="C:\\Users\\Administrator\\Music"`);
app
.route({
path: 'music',
key: 'musicDirSet',
})
.define(async (ctx) => {
const musicDir = ctx.query.musicDir;
console.log('musicDir', musicDir);
const res = musicService.setMusicDir(musicDir);
if (res.code === 200) {
ctx.body = 'success';
} else {
throw ctx.throw(res.code, res.message);
}
})
.addTo(app);
// 获取音乐列表
console.log(`http://localhost:3000/api/router?path=music&key=list`);
app
.route({
path: 'music',
key: 'list',
})
.define(async (ctx) => {
const musicList = await musicService.getMusicList();
ctx.body = {
musicDir: musicService.musicDir,
musicList,
};
})
.addTo(app);

55
src/services/index.ts Normal file
View File

@ -0,0 +1,55 @@
import { setConfig } from '@/modules/config.ts';
import { useConfig, fileIsExist } from '@kevisual/use-config';
import fs from 'fs';
import path from 'path';
// 只获取文件列表
const getMusicList = async (musicDir: string) => {
const musicList = fs.readdirSync(musicDir);
// 只获取文件列表
const fileList = musicList.filter((item) => {
return fs.statSync(path.join(musicDir, item)).isFile();
});
return fileList;
};
const config = useConfig();
export class MusicService {
musicDir: string;
constructor() {
this.musicDir = config.musicDir || '';
}
setMusicDir(musicDir: string) {
musicDir = musicDir.replace(/"/g, '');
const _musicDir = path.resolve(musicDir);
if (fileIsExist(_musicDir)) {
this.musicDir = musicDir;
setConfig({
musicDir: _musicDir,
});
return {
code: 200,
message: 'musicDir is set success',
};
} else {
return {
code: 400,
message: 'musicDir is not exist',
};
}
}
async getMusicList() {
const musicList = await getMusicList(this.musicDir);
return musicList;
}
getRealPath(file: string) {
// file 如果有空格,则需要转义
// 如果有"" 则移除
const realPath = path.join(this.musicDir, file.replace(/"/g, ''));
console.log('realPath', realPath);
if (fileIsExist(realPath)) {
return realPath;
}
return null;
}
}

View File

@ -0,0 +1,39 @@
import { musicService } from '@/app.ts';
import { SimpleRouter } from '@kevisual/router/simple';
import fs from 'fs';
import http from 'http';
export const simpleRouter = new SimpleRouter();
// http://localhost:3000/files?file="蔡依林 - 倒带.mp3"
simpleRouter.get('/files', async (req, res) => {
const url = req.url;
const urlObj = new URL(url, 'http://localhost:3000');
const file = urlObj.searchParams.get('file');
console.log(file);
if (!file) {
res.writeHead(500, { 'Content-Type': 'text/html' });
res.write('File params is required\n');
res.end();
return;
}
if (!musicService.musicDir) {
res.writeHead(500, { 'Content-Type': 'text/html' });
res.write('Music dir is not set\n');
res.end();
return;
}
const realPath = musicService.getRealPath(file);
if (realPath) {
res.writeHead(200, { 'Content-Type': 'audio/mpeg' });
const readStream = fs.createReadStream(realPath);
readStream.pipe(res);
} else {
res.writeHead(500, { 'Content-Type': 'text/html' });
res.write('File is not exist\n');
res.end();
}
});
export const middleware = async (req: http.IncomingMessage, res: http.ServerResponse) => {
return simpleRouter.parse(req, res);
};