Compare commits
2 Commits
cbf541d81e
...
60b2fc0578
| Author | SHA1 | Date | |
|---|---|---|---|
| 60b2fc0578 | |||
| 839ec8eb2e |
@@ -18,3 +18,4 @@
|
||||
*.tgz
|
||||
.claude/settings.local.json
|
||||
download_history.json
|
||||
-e "download_history.json\nMusic/"
|
||||
|
||||
+5
-1
@@ -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) {
|
||||
|
||||
+9
-4
@@ -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);
|
||||
|
||||
+2
-1
@@ -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]) {
|
||||
|
||||
+10
-1
@@ -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,
|
||||
|
||||
+17
-14
@@ -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');
|
||||
|
||||
+5
-1
@@ -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 };
|
||||
|
||||
Reference in New Issue
Block a user