// 应用配置:读取根目录 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 { 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 });