feat: 声匣 —— 网易云 / QQ 音乐自托管下载服务
由 163-music-dl CLI 重构为 Web 服务: - 下载流水线、歌词与标签逻辑沿用旧版(node-id3 / ffmpeg) - 网易云(chksz 接口 + music.163.com 元数据)与 QQ 音乐(VIP Cookie)provider - QQ 扫码登录与 musickey 自动续期 - SQLite 持久化的下载队列,SSE 实时进度 - 曲库工具(M3U / ZIP / 回收站 / 补全标签)、历史统计与导出、访问密码 - Vite 8 + Vue 3 前端,浅色 / 深色 - Docker 部署配置(飞牛 NAS) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,127 @@
|
||||
// 应用配置:读取根目录 config.json,再用环境变量覆盖(Docker 部署时主要靠环境变量)
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { isQualityKey, type QualityKey } from './core/quality.ts';
|
||||
|
||||
export const ROOT_DIR = path.resolve(import.meta.dirname, '../..');
|
||||
|
||||
export type Layout = 'album' | 'flat';
|
||||
|
||||
export interface DownloadDefaults {
|
||||
quality: QualityKey;
|
||||
layout: Layout;
|
||||
retries: number;
|
||||
lyric: boolean;
|
||||
translatedLyric: boolean;
|
||||
embedTags: boolean;
|
||||
albumCover: boolean;
|
||||
}
|
||||
|
||||
export interface AppConfig {
|
||||
port: number;
|
||||
musicDir: string;
|
||||
dataDir: string;
|
||||
legacyHistoryFile: string | null;
|
||||
/** QQ 音乐 Cookie 文件;比已保存的新时自动导入 */
|
||||
qqCookieFile: string | null;
|
||||
netease: {
|
||||
apikey: string;
|
||||
music: string;
|
||||
lyric: string;
|
||||
search: string;
|
||||
playlist: string;
|
||||
baseUrl: string;
|
||||
};
|
||||
defaults: DownloadDefaults;
|
||||
}
|
||||
|
||||
const DEFAULTS: AppConfig = {
|
||||
port: 8066,
|
||||
musicDir: 'music',
|
||||
dataDir: 'data',
|
||||
legacyHistoryFile: null,
|
||||
qqCookieFile: null,
|
||||
netease: {
|
||||
apikey: 'chksz_RGXIby7J6ok-GPa6nMrFgHgQtwEGD-hILAhZXpIa72Q',
|
||||
music: 'https://api.chksz.com/api/163_music',
|
||||
lyric: 'https://api.chksz.com/api/163_lyric',
|
||||
search: 'https://api.chksz.com/api/163_search',
|
||||
playlist: 'https://api.chksz.com/api/163_playlist',
|
||||
baseUrl: 'https://music.163.com',
|
||||
},
|
||||
defaults: {
|
||||
quality: 'master',
|
||||
layout: 'album',
|
||||
retries: 3,
|
||||
lyric: true,
|
||||
translatedLyric: true,
|
||||
embedTags: true,
|
||||
albumCover: true,
|
||||
},
|
||||
};
|
||||
|
||||
function readJson(file: string): Record<string, any> {
|
||||
try {
|
||||
return JSON.parse(fs.readFileSync(file, 'utf-8'));
|
||||
} catch (e) {
|
||||
if ((e as NodeJS.ErrnoException).code !== 'ENOENT') {
|
||||
console.warn(`⚠️ 配置文件读取失败,使用默认配置: ${(e as Error).message}`);
|
||||
}
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
function str(value: unknown, fallback: string): string {
|
||||
return typeof value === 'string' && value.trim() ? value.trim() : fallback;
|
||||
}
|
||||
|
||||
function bool(value: unknown, fallback: boolean): boolean {
|
||||
if (typeof value === 'boolean') return value;
|
||||
if (value === 'true' || value === '1') return true;
|
||||
if (value === 'false' || value === '0') return false;
|
||||
return fallback;
|
||||
}
|
||||
|
||||
function resolvePath(value: string): string {
|
||||
return path.isAbsolute(value) ? value : path.join(ROOT_DIR, value);
|
||||
}
|
||||
|
||||
function loadConfig(): AppConfig {
|
||||
const file = readJson(path.join(ROOT_DIR, 'config.json'));
|
||||
const env = process.env;
|
||||
const ne = { ...DEFAULTS.netease, ...(file.netease || {}) };
|
||||
const df = { ...DEFAULTS.defaults, ...(file.defaults || {}) };
|
||||
const quality = str(env.DEFAULT_QUALITY, df.quality);
|
||||
const layout = str(env.DEFAULT_LAYOUT, df.layout);
|
||||
const retries = Number(env.DEFAULT_RETRIES ?? df.retries);
|
||||
|
||||
return {
|
||||
port: Number(env.PORT || file.server?.port || DEFAULTS.port),
|
||||
musicDir: resolvePath(str(env.MUSIC_DIR, str(file.paths?.musicDir, DEFAULTS.musicDir))),
|
||||
dataDir: resolvePath(str(env.DATA_DIR, str(file.paths?.dataDir, DEFAULTS.dataDir))),
|
||||
legacyHistoryFile: env.LEGACY_HISTORY_FILE ? resolvePath(env.LEGACY_HISTORY_FILE) : null,
|
||||
qqCookieFile: env.QQ_COOKIE_FILE || file.qq?.cookieFile ? resolvePath(str(env.QQ_COOKIE_FILE, file.qq?.cookieFile)) : null,
|
||||
netease: {
|
||||
apikey: str(env.CHKSZ_API_KEY, str(ne.apikey, DEFAULTS.netease.apikey)),
|
||||
music: str(ne.music, DEFAULTS.netease.music),
|
||||
lyric: str(ne.lyric, DEFAULTS.netease.lyric),
|
||||
search: str(ne.search, DEFAULTS.netease.search),
|
||||
playlist: str(ne.playlist, DEFAULTS.netease.playlist),
|
||||
baseUrl: str(ne.baseUrl, DEFAULTS.netease.baseUrl).replace(/\/$/, ''),
|
||||
},
|
||||
defaults: {
|
||||
quality: isQualityKey(quality) ? quality : DEFAULTS.defaults.quality,
|
||||
layout: layout === 'flat' ? 'flat' : 'album',
|
||||
retries: Number.isInteger(retries) && retries > 0 ? retries : DEFAULTS.defaults.retries,
|
||||
lyric: bool(df.lyric, true),
|
||||
translatedLyric: bool(df.translatedLyric, true),
|
||||
embedTags: bool(df.embedTags, true),
|
||||
albumCover: bool(df.albumCover, true),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export const config = loadConfig();
|
||||
|
||||
fs.mkdirSync(config.musicDir, { recursive: true });
|
||||
fs.mkdirSync(config.dataDir, { recursive: true });
|
||||
Reference in New Issue
Block a user