From 839ec8eb2e0286e102ce2f64687e278c352b6f97 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=90=B4=E7=92=A8?= <1579649885@qq.com> Date: Tue, 4 Aug 2026 09:40:15 +0800 Subject: [PATCH] =?UTF-8?q?'=E6=9B=B4=E6=96=B0'?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- 163_music_downloader.js | 18 +++++++++--------- download_history.json | 11 +++++++++++ lib/catalog.js | 6 +++++- lib/downloader.js | 13 +++++++++---- lib/lyrics.js | 3 ++- lib/metadata.js | 11 ++++++++++- lib/tagging.js | 31 +++++++++++++++++-------------- lib/utils.js | 6 +++++- 8 files changed, 68 insertions(+), 31 deletions(-) diff --git a/163_music_downloader.js b/163_music_downloader.js index 07a4955..2216a45 100644 --- a/163_music_downloader.js +++ b/163_music_downloader.js @@ -121,7 +121,7 @@ async function main() { const track = album.tracks[i]; const target = getAlbumDownloadTarget(album, track, i); await waitIfPaused(); - const artists = track.ar?.map(a => a.name).join('/') || album.artist?.name || '未知'; + const artists = track.ar?.map(a => a.name).join(' / ') || album.artist?.name || '未知'; console.log(`[${i + 1}/${album.tracks.length}] ${track.name} - ${artists}`); try { await downloadSong(track.id, level, { @@ -144,7 +144,7 @@ async function main() { for (let i = 0; i < pl.tracks.length; i++) { const t = pl.tracks[i]; await waitIfPaused(); - console.log(`[${i + 1}/${pl.tracks.length}] ${t.name} - ${t.ar?.map(a => a.name).join('/')}`); + console.log(`[${i + 1}/${pl.tracks.length}] ${t.name} - ${t.ar?.map(a => a.name).join(' / ')}`); try { await downloadSong(t.id, level, { downloadLyric: batchLyric, embedCoverArt: batchCover, maxRetries }); } catch (e) { console.error(` ❌ 出错: ${e.message}`); } if (i < pl.tracks.length - 1) await sleep(500); @@ -242,7 +242,7 @@ async function main() { for (let i = 0; i < pl.tracks.length; i++) { const t = pl.tracks[i]; await waitIfPaused(); - console.log(`\n[${i + 1}/${pl.tracks.length}] ${t.name} - ${t.ar?.map(a => a.name).join('/')}`); + console.log(`\n[${i + 1}/${pl.tracks.length}] ${t.name} - ${t.ar?.map(a => a.name).join(' / ')}`); try { await downloadSong(t.id, level); } catch (e) { console.error(` ❌ 出错: ${e.message}`); } if (i < pl.tracks.length - 1) await sleep(500); @@ -268,7 +268,7 @@ async function main() { pl.tracks.forEach((t, i) => { const num = String(i + 1).padStart(3); const name = (t.name || '').slice(0, 22).padEnd(24); - const artist = (t.ar?.map(a => a.name).join('/') || '').slice(0, 18).padEnd(20); + const artist = (t.ar?.map(a => a.name).join(' / ') || '').slice(0, 18).padEnd(20); const album = (t.al?.name || '').slice(0, 18); console.log(` ${num} ${name} ${artist} ${album}`); }); @@ -309,7 +309,7 @@ async function main() { console.log(`\n📥 开始下载 ${tracksToDownload.length} 首歌曲...`); for (let i = 0; i < tracksToDownload.length; i++) { const t = tracksToDownload[i]; - console.log(`\n[${i + 1}/${tracksToDownload.length}] ${t.name} - ${t.ar?.map(a => a.name).join('/')}`); + console.log(`\n[${i + 1}/${tracksToDownload.length}] ${t.name} - ${t.ar?.map(a => a.name).join(' / ')}`); try { await downloadSong(t.id, level); } catch (e) { console.error(` ❌ 出错: ${e.message}`); } if (i < tracksToDownload.length - 1) await sleep(500); @@ -958,7 +958,7 @@ async function main() { console.log(`\n💿 ${album.name} - ${album.artist?.name || '未知歌手'}`); console.log(` 共 ${album.tracks.length} 首歌曲`); album.tracks.forEach((track, i) => { - const artists = track.ar?.map(a => a.name).join('/') || album.artist?.name || '未知'; + const artists = track.ar?.map(a => a.name).join(' / ') || album.artist?.name || '未知'; console.log(` ${String(i + 1).padStart(2)}. ${track.name} - ${artists}`); }); @@ -971,7 +971,7 @@ async function main() { for (let i = 0; i < album.tracks.length; i++) { const track = album.tracks[i]; - const artists = track.ar?.map(a => a.name).join('/') || album.artist?.name || '未知'; + const artists = track.ar?.map(a => a.name).join(' / ') || album.artist?.name || '未知'; const target = getAlbumDownloadTarget(album, track, i); await waitIfPaused(); console.log(`\n[${i + 1}/${album.tracks.length}] ${track.name} - ${artists}`); @@ -1459,7 +1459,7 @@ async function main() { // 生成 M3U 内容 const m3uLines = ['#EXTM3U', '']; for (const t of pl.tracks) { - const artist = t.ar?.map(a => a.name).join('/') || '未知'; + const artist = t.ar?.map(a => a.name).join(' / ') || '未知'; const title = t.name || '未知'; const duration = Math.round((t.dt || 0) / 1000) || -1; m3uLines.push(`#EXTINF:${duration},${artist} - ${title}`); @@ -1648,7 +1648,7 @@ async function main() { for (let i = 0; i < pl.tracks.length; i++) { const t = pl.tracks[i]; - const artist = t.ar?.map(a => a.name).join('/') || '未知'; + const artist = t.ar?.map(a => a.name).join(' / ') || '未知'; const title = t.name || '未知'; console.log(`[${i + 1}/${pl.tracks.length}] ${title} - ${artist}`); diff --git a/download_history.json b/download_history.json index 2afeed8..6eaf564 100644 --- a/download_history.json +++ b/download_history.json @@ -1,4 +1,15 @@ [ + { + "id": 1431373779, + "name": "想要你在身边", + "artist": "音阙诗听/王梓钰/锦零", + "album": "听", + "level": "lossless", + "bitrate": 1411000, + "size": 27191185, + "file": "音阙诗听_王梓钰_锦零 - 想要你在身边.flac", + "timestamp": "2026-08-03T03:19:47.520Z" + }, { "id": "3342981041", "name": "铁花飞", diff --git a/lib/catalog.js b/lib/catalog.js index 415d094..ad881fc 100644 --- a/lib/catalog.js +++ b/lib/catalog.js @@ -1,5 +1,6 @@ const { API } = require('./config'); const { fetchJSON } = require('./network'); +const { formatPersonNames } = require('./utils'); async function searchSongs(keyword, limit = 10) { const res = await fetchJSON(`${API.search}?keyword=${encodeURIComponent(keyword)}&limit=${limit}`); @@ -9,7 +10,10 @@ async function searchSongs(keyword, limit = 10) { } const songs = Array.isArray(res.data) ? res.data : (res.data?.songs || []); if (!songs.length) console.log('❌ 搜索无结果'); - return songs; + return songs.map(song => ({ + ...song, + artists: formatPersonNames(song.artists), + })); } async function interactiveSearch(rl, ask) { diff --git a/lib/downloader.js b/lib/downloader.js index c498212..7c0ec22 100644 --- a/lib/downloader.js +++ b/lib/downloader.js @@ -6,7 +6,7 @@ const { fetchLyric, mergeLrc, parseCredits } = require('./lyrics'); const { fetchNeteaseAlbumDetail, fetchNeteaseSongDetail } = require('./metadata'); const { downloadFile, fetchJSON } = require('./network'); const { embedCover } = require('./tagging'); -const { sanitize, sleep } = require('./utils'); +const { sanitize, formatPersonNames, sleep } = require('./utils'); async function downloadSong(songId, level, { downloadLyric = true, @@ -42,7 +42,8 @@ async function downloadSongOnce(songId, level, { const result = await fetchJSON(`${API.music}?id=${songId}&level=${level}`); if (result.code !== 200 || !result.data?.url) throw new Error(result.msg || 'API 返回错误'); - const { name, artist, album, url: audioUrl, br, size, level: actualLevel, picUrl } = result.data; + const { name, artist: rawArtist, album, url: audioUrl, br, size, level: actualLevel, picUrl } = result.data; + const artist = formatPersonNames(rawArtist); const ext = audioUrl.match(/\.(mp3|flac|m4a|wav|aac)(\?|$)/i)?.[1] || 'mp3'; const baseName = sanitize(outputBaseName || `${artist} - ${name}`); const fileName = `${baseName}.${ext}`; @@ -105,10 +106,10 @@ async function downloadSongOnce(songId, level, { if (detail) { if (detail.trackNo) tagMeta.trackNo = detail.trackNo; if (detail.disc) tagMeta.disc = detail.disc; - const albumArtist = detail.artists?.map(item => item.name).join(' / '); - if (albumArtist) tagMeta.albumArtist = albumArtist; if (detail.album?.id) { const albumDetail = await fetchNeteaseAlbumDetail(detail.album.id); + if (albumDetail?.albumArtist) tagMeta.albumArtist = albumDetail.albumArtist; + if (albumDetail?.isCompilation) tagMeta.compilation = true; if (albumDetail?.publishTime) { const date = new Date(albumDetail.publishTime); tagMeta.year = date.getFullYear(); @@ -118,6 +119,10 @@ async function downloadSongOnce(songId, level, { if (albumDetail?.company) tagMeta.publisher = albumDetail.company; if (tagMeta.trackNo && albumDetail?.size) tagMeta.trackNo = `${tagMeta.trackNo}/${albumDetail.size}`; } + if (!tagMeta.albumArtist) { + const fallbackAlbumArtist = detail.artists?.map(item => item.name).join(' / '); + if (fallbackAlbumArtist) tagMeta.albumArtist = fallbackAlbumArtist; + } } } catch {} await embedCover(dest, picUrl, tagMeta); diff --git a/lib/lyrics.js b/lib/lyrics.js index 79a0876..4fcf6c2 100644 --- a/lib/lyrics.js +++ b/lib/lyrics.js @@ -2,6 +2,7 @@ const fs = require('fs'); const path = require('path'); const { API, DOWNLOAD_DIR } = require('./config'); const { fetchJSON } = require('./network'); +const { formatPersonNames } = require('./utils'); async function fetchLyric(songId) { try { @@ -47,7 +48,7 @@ function parseCredits(lrcText) { const match = line.match(/^([^::]{1,12})\s*[::]\s*(.+)$/); if (!match) continue; const label = match[1].trim(); - const value = match[2].trim().replace(/\s*\/\s*/g, '/'); + const value = formatPersonNames(match[2].trim()); if (!value) continue; for (const [key, pattern] of Object.entries(labels)) { if (pattern.test(label) && !credits[key]) { diff --git a/lib/metadata.js b/lib/metadata.js index 02676cb..c796778 100644 --- a/lib/metadata.js +++ b/lib/metadata.js @@ -61,6 +61,12 @@ async function fetchNeteaseAlbumDetail(albumId) { const data = await requestNetease(`/api/v1/album/${albumId}`); if (!data) return null; const album = data.album || data; + const albumArtists = (album.artists?.length ? album.artists : [album.artist]) + .filter(artist => artist?.name) + .map(artist => ({ id: artist.id, name: artist.name, trans: artist.trans })); + const albumArtist = albumArtists.map(artist => artist.name).join(' / '); + const normalizedAlbumArtist = albumArtist.toLowerCase().replace(/[.\s]/g, ''); + const isCompilation = ['群星', 'variousartists', 'variousartist', 'va'].includes(normalizedAlbumArtist); return { id: album.id, name: album.name, @@ -70,7 +76,10 @@ async function fetchNeteaseAlbumDetail(albumId) { tags: album.tags || [], type: album.type || '', size: album.size || 0, - artist: album.artist ? { id: album.artist.id, name: album.artist.name, trans: album.artist.trans } : null, + artist: albumArtists[0] || null, + artists: albumArtists, + albumArtist, + isCompilation, picUrl: album.picUrl || '', tracks: (data.songs || album.songs || []).map(song => ({ id: song.id, diff --git a/lib/tagging.js b/lib/tagging.js index 5baf8cb..7cb43bb 100644 --- a/lib/tagging.js +++ b/lib/tagging.js @@ -2,6 +2,7 @@ const fs = require('fs'); const path = require('path'); const { execSync, execFileSync } = require('child_process'); const { downloadBuffer } = require('./network'); +const { formatPersonNames } = require('./utils'); function hasFFmpeg() { try { execSync('ffmpeg -version', { stdio: 'ignore' }); return true; } catch { return false; } @@ -23,24 +24,25 @@ async function embedCover(audioPath, coverUrl, meta) { const NodeID3 = require('node-id3'); const tags = { title: meta.name, - artist: meta.artist, + artist: formatPersonNames(meta.artist), album: meta.album, image: coverPath, }; - if (meta.albumArtist) tags.performerInfo = meta.albumArtist; + if (meta.albumArtist) tags.performerInfo = formatPersonNames(meta.albumArtist); + if (meta.compilation) tags.TCMP = '1'; if (meta.year) tags.year = String(meta.year); if (meta.date) tags.date = String(meta.date); if (meta.trackNo) tags.trackNumber = String(meta.trackNo); if (meta.disc) tags.partOfSet = String(meta.disc); if (meta.genre) tags.genre = meta.genre; - if (meta.composer) tags.composer = meta.composer; - if (meta.lyricist) tags.textWriter = meta.lyricist; + if (meta.composer) tags.composer = formatPersonNames(meta.composer); + if (meta.lyricist) tags.textWriter = formatPersonNames(meta.lyricist); if (meta.publisher) tags.publisher = meta.publisher; const userTexts = []; - if (meta.arranger) userTexts.push({ description: 'ARRANGER', value: meta.arranger }); - if (meta.producer) userTexts.push({ description: 'PRODUCER', value: meta.producer }); - if (meta.mixer) userTexts.push({ description: 'MIXER', value: meta.mixer }); + if (meta.arranger) userTexts.push({ description: 'ARRANGER', value: formatPersonNames(meta.arranger) }); + if (meta.producer) userTexts.push({ description: 'PRODUCER', value: formatPersonNames(meta.producer) }); + if (meta.mixer) userTexts.push({ description: 'MIXER', value: formatPersonNames(meta.mixer) }); if (userTexts.length) tags.userDefinedText = userTexts; if (meta.lyrics) tags.unsynchronisedLyrics = { language: 'chi', text: meta.lyrics }; const ok = NodeID3.write(tags, audioPath); @@ -63,19 +65,20 @@ async function embedCover(audioPath, coverUrl, meta) { } }; addMeta('title', meta.name); - addMeta('artist', meta.artist); + addMeta('artist', formatPersonNames(meta.artist)); addMeta('album', meta.album); - addMeta('album_artist', meta.albumArtist || meta.artist); + addMeta('album_artist', formatPersonNames(meta.albumArtist || meta.artist)); + addMeta('compilation', meta.compilation ? '1' : undefined); addMeta('date', meta.date || meta.year); addMeta('track', meta.trackNo); addMeta('disc', meta.disc); addMeta('genre', meta.genre); - addMeta('composer', meta.composer); + addMeta('composer', formatPersonNames(meta.composer)); addMeta('publisher', meta.publisher); - addMeta('lyricist', meta.lyricist); - addMeta('arranger', meta.arranger); - addMeta('producer', meta.producer); - addMeta('mixer', meta.mixer); + addMeta('lyricist', formatPersonNames(meta.lyricist)); + addMeta('arranger', formatPersonNames(meta.arranger)); + addMeta('producer', formatPersonNames(meta.producer)); + addMeta('mixer', formatPersonNames(meta.mixer)); if (meta.lyrics && meta.lyrics.length <= 8000) addMeta('lyrics', meta.lyrics); args.push('-metadata:s:v', 'title=Album cover', '-metadata:s:v', 'comment=Cover (front)'); if (ext === '.mp3') args.push('-id3v2_version', '3'); diff --git a/lib/utils.js b/lib/utils.js index 591428c..1602b11 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -10,6 +10,10 @@ function sanitizePathSegment(name, fallback) { return value && value !== '.' && value !== '..' ? value : fallback; } +function formatPersonNames(value) { + return typeof value === 'string' ? value.replace(/\s*\/\s*/g, ' / ') : value; +} + function sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } @@ -26,4 +30,4 @@ function getDirSize(dirPath) { return size; } -module.exports = { sanitize, sanitizePathSegment, sleep, getDirSize }; +module.exports = { sanitize, sanitizePathSegment, formatPersonNames, sleep, getDirSize };