feat: 歌手头像 —— 下载时写入 artist.jpg、曲库批量补全、无头像时用专辑封面拼图

- 按「歌手/专辑」下载时,歌手目录没有图片则保存歌手头像(合辑不写)
- 曲库页列出缺少头像的歌手文件夹,一键从网易云 / QQ 音乐按名字精确匹配补全
- 网易云头像缩到 600×600,避免原图数 MB
- 仍无头像的文件夹显示前 4 张专辑封面拼图

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Jocay
2026-10-10 23:15:31 +08:00
parent 7a3de9afba
commit 4a8fe455f3
11 changed files with 302 additions and 10 deletions
+20
View File
@@ -23,6 +23,17 @@ export interface DownloadOptions {
const TAG_OVERHEAD_MAX = 8 * 1024 * 1024;
/** 文件夹封面 / 歌手头像的常见文件名(曲库页按此顺序取图) */
export const FOLDER_IMAGES = ['artist.jpg', 'folder.jpg', 'cover.jpg', 'cover.png', 'artist.png'];
export async function hasFolderImage(dir: string): Promise<boolean> {
for (const name of FOLDER_IMAGES) {
const stat = await fs.promises.stat(path.join(dir, name)).catch(() => null);
if (stat?.size) return true;
}
return false;
}
export type Stage ='resolve' | 'metadata' | 'download' | 'lyric' | 'cover' | 'tag';
export interface Reporter {
@@ -151,6 +162,15 @@ async function downloadOnce(provider: MusicProvider, id: string, opts: DownloadO
if (!stat?.size) await fs.promises.writeFile(coverPath, cover);
}
// 歌手目录还没有头像时写入 artist.jpg(合辑「群星」不写)
if (wantAlbumCover && meta?.albumArtistImage && !meta.compilation) {
const artistDir = path.dirname(target.dir);
if (!(await hasFolderImage(artistDir))) {
const avatar = await fetchBuffer(meta.albumArtistImage, { signal }).catch(() => null);
if (avatar?.length) await fs.promises.writeFile(path.join(artistDir, 'artist.jpg'), avatar);
}
}
if (opts.embedTags) {
reporter?.stage?.('tag', '写入标签');
try {