feat: 下载沿用曲库已有的歌手 / 专辑文件夹写法
歌手或专辑文件夹名与曲库已有文件夹只差空格、大小写、全半角时(如 「G.E.M.邓紫棋」与「G.E.M. 邓紫棋」),放进已有文件夹,并把歌手、 专辑歌手标签统一成该写法,避免同一歌手被拆成两个目录 / 两个艺人。 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,8 @@
|
||||
// 输出目录与文件名规则
|
||||
// album: {音乐目录}/{第一位专辑艺人}/{专辑}/{NN}. {歌名}.ext —— 与 NAS 现有曲库结构一致
|
||||
// flat : {音乐目录}/{歌手} - {歌名}.ext —— 旧版单曲下载格式
|
||||
// 曲库里已有只差空格 / 大小写 / 全半角的歌手或专辑文件夹时,沿用已有文件夹名,避免同一歌手拆成两个目录
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import type { Layout } from '../config.ts';
|
||||
import { sanitize, sanitizePathSegment } from './util.ts';
|
||||
@@ -18,6 +20,26 @@ export interface Target {
|
||||
dir: string;
|
||||
baseName: string; // 不含扩展名
|
||||
isAlbumDir: boolean;
|
||||
/** 歌手名被替换为曲库已有写法时:{ from: 平台写法, to: 曲库写法 } */
|
||||
artistAlias?: { from: string; to: string };
|
||||
}
|
||||
|
||||
/** 判断两个文件夹名是否「同一个」:忽略空白、大小写、全半角 */
|
||||
export function folderKey(name: string): string {
|
||||
return name.normalize('NFKC').toLowerCase().replace(/\s+/g, '');
|
||||
}
|
||||
|
||||
/** 在 parent 下找与 name 等价的已有文件夹,找到返回已有的名字 */
|
||||
function existingFolder(parent: string, name: string): string {
|
||||
let entries: fs.Dirent[];
|
||||
try {
|
||||
entries = fs.readdirSync(parent, { withFileTypes: true });
|
||||
} catch {
|
||||
return name;
|
||||
}
|
||||
if (entries.some((e) => e.isDirectory() && e.name === name)) return name;
|
||||
const key = folderKey(name);
|
||||
return entries.find((e) => e.isDirectory() && folderKey(e.name) === key)?.name ?? name;
|
||||
}
|
||||
|
||||
export function resolveTarget(musicDir: string, layout: Layout, input: LayoutInput): Target {
|
||||
@@ -25,13 +47,29 @@ export function resolveTarget(musicDir: string, layout: Layout, input: LayoutInp
|
||||
if (layout === 'flat') {
|
||||
return { dir: musicDir, baseName: sanitize(`${input.artists} - ${title}`), isAlbumDir: false };
|
||||
}
|
||||
const firstArtist = input.artists.split(' / ')[0];
|
||||
const artistDir = sanitizePathSegment(input.albumArtistPrimary || firstArtist, '未知歌手');
|
||||
const albumDir = sanitizePathSegment(input.album, '未知专辑');
|
||||
const primary = input.albumArtistPrimary || input.artists.split(' / ')[0] || '';
|
||||
const wantArtistDir = sanitizePathSegment(primary, '未知歌手');
|
||||
const artistDir = existingFolder(musicDir, wantArtistDir);
|
||||
const albumDir = existingFolder(path.join(musicDir, artistDir), sanitizePathSegment(input.album, '未知专辑'));
|
||||
let baseName = title;
|
||||
if (input.trackNo) {
|
||||
const width = Math.max(2, String(input.trackTotal || 0).length);
|
||||
baseName = `${String(input.trackNo).padStart(width, '0')}. ${title}`;
|
||||
}
|
||||
return { dir: path.join(musicDir, artistDir, albumDir), baseName: sanitize(baseName), isAlbumDir: true };
|
||||
return {
|
||||
dir: path.join(musicDir, artistDir, albumDir),
|
||||
baseName: sanitize(baseName),
|
||||
isAlbumDir: true,
|
||||
// 只有原名本身就是合法文件夹名时才回写标签(含 / 等被替换过的名字不处理)
|
||||
artistAlias: artistDir !== wantArtistDir && primary === wantArtistDir ? { from: primary, to: artistDir } : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
/** 把 "A / B" 形式的歌手串中等于 from 的那一位替换为 to */
|
||||
export function applyArtistAlias(artists: string | undefined, alias: Target['artistAlias']): string | undefined {
|
||||
if (!artists || !alias) return artists;
|
||||
return artists
|
||||
.split(' / ')
|
||||
.map((a) => (a === alias.from ? alias.to : a))
|
||||
.join(' / ');
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import type { Layout } from '../config.ts';
|
||||
import { resolveTarget } from './layout.ts';
|
||||
import { applyArtistAlias, resolveTarget } from './layout.ts';
|
||||
import { mergeLrc, parseCredits } from './lyrics.ts';
|
||||
import { downloadToFile, fetchBuffer, type Progress } from './net/http.ts';
|
||||
import type { QualityKey } from './quality.ts';
|
||||
@@ -100,7 +100,7 @@ async function downloadOnce(provider: MusicProvider, id: string, opts: DownloadO
|
||||
reporter?.stage?.('metadata', '获取元数据');
|
||||
const meta = await provider.getTrackMeta(id).catch(() => null);
|
||||
const name = audio.name || meta?.name || id;
|
||||
const artists = audio.artists || meta?.artists || '未知歌手';
|
||||
let artists = audio.artists || meta?.artists || '未知歌手';
|
||||
const album = audio.album || meta?.album || '';
|
||||
|
||||
const target = resolveTarget(ctx.musicDir, opts.layout, {
|
||||
@@ -111,6 +111,15 @@ async function downloadOnce(provider: MusicProvider, id: string, opts: DownloadO
|
||||
trackNo: meta?.trackNo,
|
||||
trackTotal: meta?.trackTotal,
|
||||
});
|
||||
// 沿用了曲库已有的歌手文件夹名(如「G.E.M. 邓紫棋」)时,标签里的歌手名也统一成该写法
|
||||
if (target.artistAlias) {
|
||||
artists = applyArtistAlias(artists, target.artistAlias)!;
|
||||
if (meta) {
|
||||
meta.albumArtist = applyArtistAlias(meta.albumArtist, target.artistAlias);
|
||||
meta.albumArtistPrimary = target.artistAlias.to;
|
||||
}
|
||||
reporter?.log?.('info', `歌手名统一为曲库已有写法:${target.artistAlias.to}`);
|
||||
}
|
||||
await fs.promises.mkdir(target.dir, { recursive: true });
|
||||
const dest = path.join(target.dir, `${target.baseName}.${audio.ext}`);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user