21945cc54f
由 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>
128 lines
4.2 KiB
TypeScript
128 lines
4.2 KiB
TypeScript
// 应用配置:读取根目录 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 });
|