From c6fb745b8584acd713725c3ff1cda7a9b68d4384 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=8F=B2=E6=82=A6?= Date: Tue, 13 Jan 2026 09:09:58 +0800 Subject: [PATCH] =?UTF-8?q?feat(=E5=90=8C=E6=AD=A5=E5=8A=9F=E8=83=BD):=20?= =?UTF-8?q?=E6=94=B9=E8=BF=9B=E7=BD=91=E6=98=93=E4=BA=91=E9=9F=B3=E4=B9=90?= =?UTF-8?q?=E6=AD=8C=E6=9B=B2=E5=9C=A8Navidrome=E4=B8=AD=E7=9A=84=E6=90=9C?= =?UTF-8?q?=E7=B4=A2=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 重构searchSongInNavidrome函数,支持多种查询组合和重试机制 - 新增对歌曲名、艺术家、专辑的多维度查询 - 添加基于ID令牌的精确匹配 - 实现最多2次的重试机制 - 优化匹配逻辑,提高搜索成功率 --- Netease-sync/server.js | 77 +++++++++++++++++++++++++++++++++--------- 1 file changed, 61 insertions(+), 16 deletions(-) diff --git a/Netease-sync/server.js b/Netease-sync/server.js index f51ccb8..d6f5c17 100644 --- a/Netease-sync/server.js +++ b/Netease-sync/server.js @@ -443,20 +443,69 @@ function processSong(song) { }); } -async function searchSongInNavidrome(filename) { +async function searchSongInNavidrome(song) { try { - const result = await callSubsonicAPI('search3', { query: filename }); - if (result.searchResult3?.song) { - const songs = Array.isArray(result.searchResult3.song) - ? result.searchResult3.song - : [result.searchResult3.song]; - - for (const song of songs) { - if (song.title && song.path) { - return song.id; + const rawName = (song?.name || '').trim(); + const rawArtist = (song?.artist || '').trim(); + const rawAlbum = (song?.album || '').trim(); + const idToken = song?.id ? `netease_${song.id}` : ''; + + const normalize = (str) => (str || '').toString().trim().toLowerCase(); + const safeName = sanitizeFilename(rawName); + const safeArtist = sanitizeFilename(rawArtist); + + const queries = []; + if (rawName && rawArtist) queries.push(`${rawName} ${rawArtist}`); + if (rawName) queries.push(rawName); + if (rawArtist && rawAlbum) queries.push(`${rawArtist} ${rawAlbum}`); + if (safeName) { + const fallbackName = safeArtist ? `${safeArtist} - ${safeName}` : safeName; + queries.push(idToken ? `${fallbackName} [${idToken}]` : fallbackName); + } + + const seen = new Set(); + const uniqueQueries = queries.filter(q => { + const key = normalize(q); + if (!key || seen.has(key)) return false; + seen.add(key); + return true; + }); + + const MAX_ATTEMPTS = 2; + + for (const query of uniqueQueries) { + for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) { + const result = await callSubsonicAPI('search3', { query }); + if (result.searchResult3?.song) { + const songs = Array.isArray(result.searchResult3.song) + ? result.searchResult3.song + : [result.searchResult3.song]; + + if (idToken) { + const byPath = songs.find(s => typeof s.path === 'string' && s.path.includes(idToken)); + 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) { + await new Promise(resolve => setTimeout(resolve, 1500)); } } } + return null; } catch (error) { console.error('Search song error:', error.message); @@ -591,12 +640,8 @@ async function syncPlaylist(playlist, cachedInfo = null) { if (playlist.songMapping && playlist.songMapping[neteaseSongId]) { navidromeSongId = playlist.songMapping[neteaseSongId]; } else { - const safeName = sanitizeFilename(song.name); - const safeArtist = sanitizeFilename(song.artist); - const filename = `${safeArtist} - ${safeName} [netease_${neteaseSongId}]`; - - navidromeSongId = await searchSongInNavidrome(filename); - + navidromeSongId = await searchSongInNavidrome(song); + if (navidromeSongId) { newSongIds.push(navidromeSongId);