改动点
- searchSongInNavidrome 增加:
- 去括号标题(去掉 (Live) / (十年荣耀版) 等)
- 按歌手进行兜底搜索,再用 path/idToken/模糊标题匹配
- 优先 path 命中(含真实文件名或 netease_id)
This commit is contained in:
@@ -548,25 +548,38 @@ async function processSong(song) {
|
|||||||
// Check if file exists (fuzzy match for extension)
|
// Check if file exists (fuzzy match for extension)
|
||||||
try {
|
try {
|
||||||
const files = fs.readdirSync(MUSIC_DIR);
|
const files = fs.readdirSync(MUSIC_DIR);
|
||||||
|
let matchedFile = null;
|
||||||
const exists = files.some(f => {
|
const exists = files.some(f => {
|
||||||
// 1. Exact Match: Artist - Name [source_id].ext
|
// 1. Exact Match: Artist - Name [source_id].ext
|
||||||
if (f.startsWith(baseName)) return true;
|
if (f.startsWith(baseName)) {
|
||||||
|
matchedFile = f;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
// 2. Simple Format: Artist - Name.ext
|
// 2. Simple Format: Artist - Name.ext
|
||||||
if (f.startsWith(`${safeArtist} - ${safeName}.`)) return true;
|
if (f.startsWith(`${safeArtist} - ${safeName}.`)) {
|
||||||
|
matchedFile = f;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
// 3. Simple Format w/o Artist: Name.ext
|
// 3. Simple Format w/o Artist: Name.ext
|
||||||
if (f.startsWith(`${safeName}.`)) return true;
|
if (f.startsWith(`${safeName}.`)) {
|
||||||
|
matchedFile = f;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
// 4. Match prefix ignoring ID: Artist - Name [
|
// 4. Match prefix ignoring ID: Artist - Name [
|
||||||
if (f.startsWith(`${safeArtist} - ${safeName} [`)) return true;
|
if (f.startsWith(`${safeArtist} - ${safeName} [`)) {
|
||||||
|
matchedFile = f;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
});
|
});
|
||||||
|
|
||||||
if (exists) {
|
if (exists) {
|
||||||
// console.log(`[Sync] Skipped (Exists): ${song.name}`);
|
// console.log(`[Sync] Skipped (Exists): ${song.name}`);
|
||||||
return true; // Exists
|
return { ok: true, existed: true, filename: matchedFile || null }; // Exists
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Error reading music dir:', e);
|
console.error('Error reading music dir:', e);
|
||||||
@@ -576,14 +589,14 @@ async function processSong(song) {
|
|||||||
const candidates = buildDownloadCandidates(source, song.id, song.url, song.types);
|
const candidates = buildDownloadCandidates(source, song.id, song.url, song.types);
|
||||||
|
|
||||||
const primaryOk = await tryDownloadWithCandidates(song, source, baseName, candidates, downloadHeaders);
|
const primaryOk = await tryDownloadWithCandidates(song, source, baseName, candidates, downloadHeaders);
|
||||||
if (primaryOk) return true;
|
if (primaryOk) return { ok: true, existed: false, filename: `${baseName}` };
|
||||||
|
|
||||||
// 回退:搜索拿到可用歌曲 ID 再尝试下载
|
// 回退:搜索拿到可用歌曲 ID 再尝试下载
|
||||||
const searchHit = await searchSongOnTuneHub(song.name, song.artist, source);
|
const searchHit = await searchSongOnTuneHub(song.name, song.artist, source);
|
||||||
if (searchHit?.id) {
|
if (searchHit?.id) {
|
||||||
const fallbackCandidates = buildDownloadCandidates(source, searchHit.id, searchHit.url, searchHit.types);
|
const fallbackCandidates = buildDownloadCandidates(source, searchHit.id, searchHit.url, searchHit.types);
|
||||||
const fallbackOk = await tryDownloadWithCandidates(song, source, baseName, fallbackCandidates, downloadHeaders);
|
const fallbackOk = await tryDownloadWithCandidates(song, source, baseName, fallbackCandidates, downloadHeaders);
|
||||||
if (fallbackOk) return true;
|
if (fallbackOk) return { ok: true, existed: false, filename: `${baseName}` };
|
||||||
}
|
}
|
||||||
|
|
||||||
// 换源回退:kuwo -> qq
|
// 换源回退:kuwo -> qq
|
||||||
@@ -611,13 +624,59 @@ async function processSong(song) {
|
|||||||
fallbackCandidates,
|
fallbackCandidates,
|
||||||
fallbackHeaders
|
fallbackHeaders
|
||||||
);
|
);
|
||||||
if (fallbackOk) return true;
|
if (fallbackOk) return { ok: true, existed: false, filename: `${baseName}` };
|
||||||
}
|
}
|
||||||
|
|
||||||
return false; // Failed
|
return { ok: false, existed: false, filename: null }; // Failed
|
||||||
}
|
}
|
||||||
|
|
||||||
async function searchSongInNavidrome(song) {
|
function normalizeMatchText(str) {
|
||||||
|
return (str || '')
|
||||||
|
.toString()
|
||||||
|
.toLowerCase()
|
||||||
|
.replace(/[\s\-_[\]().·()、]+/g, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
function stripBracketed(str) {
|
||||||
|
return (str || '')
|
||||||
|
.toString()
|
||||||
|
.replace(/[\(\(\[\【].*?[\)\)\]\】]/g, '')
|
||||||
|
.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
function pickSongByPathOrFuzzy(songs, filenameHint, idToken, rawName, rawArtist) {
|
||||||
|
if (!Array.isArray(songs) || songs.length === 0) return null;
|
||||||
|
|
||||||
|
if (filenameHint) {
|
||||||
|
const byPath = songs.find(s => typeof s.path === 'string' && s.path.includes(filenameHint));
|
||||||
|
if (byPath) return byPath.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (idToken) {
|
||||||
|
const byIdToken = songs.find(s => typeof s.path === 'string' && s.path.includes(idToken));
|
||||||
|
if (byIdToken) return byIdToken.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
const targetName = normalizeMatchText(stripBracketed(rawName));
|
||||||
|
const targetArtist = normalizeMatchText(rawArtist);
|
||||||
|
|
||||||
|
const byTitleArtist = songs.find(s =>
|
||||||
|
normalizeMatchText(stripBracketed(s.title)) === targetName &&
|
||||||
|
normalizeMatchText(s.artist) === targetArtist
|
||||||
|
);
|
||||||
|
if (byTitleArtist) return byTitleArtist.id;
|
||||||
|
|
||||||
|
if (songs.length === 1) {
|
||||||
|
const only = songs[0];
|
||||||
|
if (normalizeMatchText(stripBracketed(only.title)) === targetName) {
|
||||||
|
return only.id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function searchSongInNavidrome(song, filenameHint = null) {
|
||||||
try {
|
try {
|
||||||
const rawName = (song?.name || '').trim();
|
const rawName = (song?.name || '').trim();
|
||||||
const rawArtist = (song?.artist || '').trim();
|
const rawArtist = (song?.artist || '').trim();
|
||||||
@@ -629,8 +688,14 @@ async function searchSongInNavidrome(song) {
|
|||||||
const safeArtist = sanitizeFilename(rawArtist);
|
const safeArtist = sanitizeFilename(rawArtist);
|
||||||
|
|
||||||
const queries = [];
|
const queries = [];
|
||||||
|
if (filenameHint) {
|
||||||
|
const base = filenameHint.replace(/\.[^.]+$/, '');
|
||||||
|
queries.push(base);
|
||||||
|
}
|
||||||
if (rawName && rawArtist) queries.push(`${rawName} ${rawArtist}`);
|
if (rawName && rawArtist) queries.push(`${rawName} ${rawArtist}`);
|
||||||
if (rawName) queries.push(rawName);
|
if (rawName) queries.push(rawName);
|
||||||
|
const strippedName = stripBracketed(rawName);
|
||||||
|
if (strippedName && strippedName !== rawName) queries.push(strippedName);
|
||||||
if (rawArtist && rawAlbum) queries.push(`${rawArtist} ${rawAlbum}`);
|
if (rawArtist && rawAlbum) queries.push(`${rawArtist} ${rawAlbum}`);
|
||||||
if (safeName) {
|
if (safeName) {
|
||||||
const fallbackName = safeArtist ? `${safeArtist} - ${safeName}` : safeName;
|
const fallbackName = safeArtist ? `${safeArtist} - ${safeName}` : safeName;
|
||||||
@@ -655,23 +720,8 @@ async function searchSongInNavidrome(song) {
|
|||||||
? result.searchResult3.song
|
? result.searchResult3.song
|
||||||
: [result.searchResult3.song];
|
: [result.searchResult3.song];
|
||||||
|
|
||||||
if (idToken) {
|
const picked = pickSongByPathOrFuzzy(songs, filenameHint, idToken, rawName, rawArtist);
|
||||||
const byPath = songs.find(s => typeof s.path === 'string' && s.path.includes(idToken));
|
if (picked) return picked;
|
||||||
if (byPath) return byPath.id;
|
|
||||||
}
|
|
||||||
|
|
||||||
const byTitleArtist = songs.find(s =>
|
|
||||||
normalize(s.title) === normalize(rawName) &&
|
|
||||||
normalize(s.artist) === normalize(rawArtist)
|
|
||||||
);
|
|
||||||
if (byTitleArtist) return byTitleArtist.id;
|
|
||||||
|
|
||||||
if (songs.length === 1) {
|
|
||||||
const only = songs[0];
|
|
||||||
if (normalize(only.title) === normalize(rawName)) {
|
|
||||||
return only.id;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (attempt < MAX_ATTEMPTS) {
|
if (attempt < MAX_ATTEMPTS) {
|
||||||
@@ -680,6 +730,18 @@ async function searchSongInNavidrome(song) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 兜底:用歌手进行宽松搜索,再按路径或模糊标题匹配
|
||||||
|
if (rawArtist) {
|
||||||
|
const fallbackResult = await callSubsonicAPI('search3', { query: rawArtist });
|
||||||
|
if (fallbackResult.searchResult3?.song) {
|
||||||
|
const songs = Array.isArray(fallbackResult.searchResult3.song)
|
||||||
|
? fallbackResult.searchResult3.song
|
||||||
|
: [fallbackResult.searchResult3.song];
|
||||||
|
const picked = pickSongByPathOrFuzzy(songs, filenameHint, idToken, rawName, rawArtist);
|
||||||
|
if (picked) return picked;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Search song error:', error.message);
|
console.error('Search song error:', error.message);
|
||||||
@@ -772,6 +834,7 @@ async function syncPlaylist(playlist, cachedInfo = null) {
|
|||||||
let syncedCount = 0;
|
let syncedCount = 0;
|
||||||
let failedCount = 0;
|
let failedCount = 0;
|
||||||
const downloadStatus = {};
|
const downloadStatus = {};
|
||||||
|
const downloadInfo = {};
|
||||||
const unmatchedSongs = [];
|
const unmatchedSongs = [];
|
||||||
|
|
||||||
syncStatus[playlistId] = { status: 'syncing', progress: 10, message: `获取到 ${totalTracks} 首歌曲` };
|
syncStatus[playlistId] = { status: 'syncing', progress: 10, message: `获取到 ${totalTracks} 首歌曲` };
|
||||||
@@ -798,8 +861,12 @@ async function syncPlaylist(playlist, cachedInfo = null) {
|
|||||||
const batch = songs.slice(i, i + BATCH_SIZE);
|
const batch = songs.slice(i, i + BATCH_SIZE);
|
||||||
await Promise.all(batch.map(async (song) => {
|
await Promise.all(batch.map(async (song) => {
|
||||||
try {
|
try {
|
||||||
const ok = await processSong(song);
|
const result = await processSong(song);
|
||||||
|
const ok = typeof result === 'object' ? result.ok : !!result;
|
||||||
downloadStatus[song.id] = ok;
|
downloadStatus[song.id] = ok;
|
||||||
|
if (typeof result === 'object') {
|
||||||
|
downloadInfo[song.id] = result;
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(`[Sync] Failed to process song ${song.name}:`, e);
|
console.error(`[Sync] Failed to process song ${song.name}:`, e);
|
||||||
downloadStatus[song.id] = false;
|
downloadStatus[song.id] = false;
|
||||||
@@ -833,7 +900,8 @@ async function syncPlaylist(playlist, cachedInfo = null) {
|
|||||||
if (playlist.songMapping && playlist.songMapping[neteaseSongId]) {
|
if (playlist.songMapping && playlist.songMapping[neteaseSongId]) {
|
||||||
navidromeSongId = playlist.songMapping[neteaseSongId];
|
navidromeSongId = playlist.songMapping[neteaseSongId];
|
||||||
} else {
|
} else {
|
||||||
navidromeSongId = await searchSongInNavidrome(song);
|
const fileHint = downloadInfo[neteaseSongId]?.filename || null;
|
||||||
|
navidromeSongId = await searchSongInNavidrome(song, fileHint);
|
||||||
|
|
||||||
if (navidromeSongId) {
|
if (navidromeSongId) {
|
||||||
newSongIds.push(navidromeSongId);
|
newSongIds.push(navidromeSongId);
|
||||||
@@ -855,7 +923,8 @@ async function syncPlaylist(playlist, cachedInfo = null) {
|
|||||||
name: song.name,
|
name: song.name,
|
||||||
artist: song.artist,
|
artist: song.artist,
|
||||||
album: song.album,
|
album: song.album,
|
||||||
downloaded: downloadStatus[neteaseSongId] === true
|
downloaded: downloadStatus[neteaseSongId] === true,
|
||||||
|
filename: downloadInfo[neteaseSongId]?.filename || null
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -872,7 +941,7 @@ async function syncPlaylist(playlist, cachedInfo = null) {
|
|||||||
if (unmatchedSongs.length > 0) {
|
if (unmatchedSongs.length > 0) {
|
||||||
const maxLog = 20;
|
const maxLog = 20;
|
||||||
const preview = unmatchedSongs.slice(0, maxLog)
|
const preview = unmatchedSongs.slice(0, maxLog)
|
||||||
.map(s => `${s.name} - ${s.artist} (id=${s.id}, downloaded=${s.downloaded})`)
|
.map(s => `${s.name} - ${s.artist} (id=${s.id}, downloaded=${s.downloaded}, file=${s.filename || 'n/a'})`)
|
||||||
.join(' | ');
|
.join(' | ');
|
||||||
console.warn(`[Sync] Unmatched songs (${unmatchedSongs.length}): ${preview}${unmatchedSongs.length > maxLog ? ' ...' : ''}`);
|
console.warn(`[Sync] Unmatched songs (${unmatchedSongs.length}): ${preview}${unmatchedSongs.length > maxLog ? ' ...' : ''}`);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user