diff --git a/Netease-sync/server.js b/Netease-sync/server.js index a4bb693..b06a5df 100644 --- a/Netease-sync/server.js +++ b/Netease-sync/server.js @@ -281,6 +281,145 @@ function getPreferredQualities(types) { return available.length > 0 ? available : ['320k', '128k']; } +function normalizeText(str) { + return (str || '') + .toString() + .trim() + .toLowerCase() + .replace(/[\s\-_[\]().·()、]+/g, ''); +} + +function pickBestSearchResult(results, songName, songArtist) { + if (!Array.isArray(results) || results.length === 0) return null; + + const targetName = normalizeText(songName); + const targetArtist = normalizeText(songArtist); + + let best = null; + let bestScore = -1; + + for (const item of results) { + const name = normalizeText(item.name); + const artist = normalizeText(item.artist); + let score = 0; + if (name && targetName && name === targetName) score += 100; + if (artist && targetArtist && artist === targetArtist) score += 60; + if (name && targetName && name.includes(targetName)) score += 30; + if (artist && targetArtist && artist.includes(targetArtist)) score += 20; + if (score > bestScore) { + bestScore = score; + best = item; + } + } + + return best || results[0]; +} + +async function searchSongOnTuneHub(songName, songArtist, source) { + try { + const keyword = songArtist ? `${songName} ${songArtist}` : songName; + if (!keyword) return null; + const url = `${TUNEHUB_API_URL}/api/?source=${source}&type=search&keyword=${encodeURIComponent(keyword)}&limit=20`; + const response = await apiClient.get(url, { retry: 2, retryDelay: 1000 }); + if (response.data?.code !== 200) return null; + const data = response.data.data || {}; + const results = data.results || []; + return pickBestSearchResult(results, songName, songArtist); + } catch (error) { + console.warn('[Sync] Search fallback failed:', error.message); + return null; + } +} + +function buildDownloadCandidates(source, songId, songUrl, types) { + const candidates = []; + if (songUrl) { + candidates.push({ label: 'playlist_url', apiUrl: songUrl }); + } + + const qualities = getPreferredQualities(types); + for (const q of qualities) { + candidates.push({ + label: `br=${q}`, + apiUrl: `${TUNEHUB_API_URL}/api/?source=${source}&id=${songId}&type=url&br=${q}` + }); + } + + candidates.push({ + label: 'br=default', + apiUrl: `${TUNEHUB_API_URL}/api/?source=${source}&id=${songId}&type=url` + }); + + return candidates; +} + +async function tryDownloadWithCandidates(song, source, baseName, candidates, downloadHeaders) { + const tried = new Set(); + + for (const candidate of candidates) { + if (!candidate.apiUrl || tried.has(candidate.apiUrl)) continue; + tried.add(candidate.apiUrl); + + const directUrl = await resolveTuneHubAudioUrl(candidate.apiUrl); + if (!directUrl) { + console.warn(`[Sync] Failed to resolve audio url for ${song.name} (${candidate.label})`); + continue; + } + + let ext = 'mp3'; + if (directUrl.includes('.flac')) ext = 'flac'; + else if (directUrl.includes('.m4a')) ext = 'm4a'; + else if (directUrl.includes('.ogg')) ext = 'ogg'; + else if (directUrl.includes('.wav')) ext = 'wav'; + + const tempFile = path.join(MUSIC_DIR, `temp_${Date.now()}_${song.id}.${ext}`); + const finalFile = path.join(MUSIC_DIR, `${baseName}.${ext}`); + + try { + await downloadFile(directUrl, tempFile, downloadHeaders); + + try { + // Try to download cover image + let coverPath = null; + try { + const coverUrl = `${TUNEHUB_API_URL}/api/?source=${source}&id=${song.id}&type=pic`; + coverPath = path.join(MUSIC_DIR, `temp_cover_${Date.now()}_${song.id}.jpg`); + await downloadFile(coverUrl, coverPath, downloadHeaders); + } catch (e) { + console.warn(`[Sync] Failed to download cover for ${song.name}:`, e.message); + } + + // Write Metadata (Title, Artist, Album, Cover) + await writeMetadata(tempFile, finalFile, { + title: song.name, + artist: song.artist, + album: song.album || song.name, + cover: coverPath + }); + + console.log(`[Sync] Downloaded & Tagged: ${baseName}.${ext}`); + + // Cleanup temp files + if (fs.existsSync(tempFile)) fs.unlinkSync(tempFile); + if (coverPath && fs.existsSync(coverPath)) fs.unlinkSync(coverPath); + return true; // Downloaded + } catch (err) { + console.error(`[Sync] Metadata Error for ${song.name}:`, err.message); + // Fallback: Just rename temp to final + if (!fs.existsSync(finalFile)) { + fs.renameSync(tempFile, finalFile); + } + return true; // Downloaded (fallback) + } + } catch (err) { + console.error(`[Sync] Download failed for ${song.name} (${candidate.label}):`, err.message); + if (fs.existsSync(tempFile)) fs.unlinkSync(tempFile); + } + } + + return false; +} + async function resolveTuneHubAudioUrl(apiUrl) { try { const response = await apiClient.get(apiUrl, { @@ -399,86 +538,17 @@ async function processSong(song) { } const downloadHeaders = buildDownloadHeaders(source); + const candidates = buildDownloadCandidates(source, song.id, song.url, song.types); - const candidates = []; - if (song.url) { - candidates.push({ label: 'playlist_url', apiUrl: song.url }); - } + const primaryOk = await tryDownloadWithCandidates(song, source, baseName, candidates, downloadHeaders); + if (primaryOk) return true; - const qualities = getPreferredQualities(song.types); - for (const q of qualities) { - candidates.push({ - label: `br=${q}`, - apiUrl: `${TUNEHUB_API_URL}/api/?source=${source}&id=${song.id}&type=url&br=${q}` - }); - } - - candidates.push({ - label: 'br=default', - apiUrl: `${TUNEHUB_API_URL}/api/?source=${source}&id=${song.id}&type=url` - }); - - const tried = new Set(); - - for (const candidate of candidates) { - if (!candidate.apiUrl || tried.has(candidate.apiUrl)) continue; - tried.add(candidate.apiUrl); - - const directUrl = await resolveTuneHubAudioUrl(candidate.apiUrl); - if (!directUrl) { - console.warn(`[Sync] Failed to resolve audio url for ${song.name} (${candidate.label})`); - continue; - } - - let ext = 'mp3'; - if (directUrl.includes('.flac')) ext = 'flac'; - else if (directUrl.includes('.m4a')) ext = 'm4a'; - else if (directUrl.includes('.ogg')) ext = 'ogg'; - else if (directUrl.includes('.wav')) ext = 'wav'; - - const tempFile = path.join(MUSIC_DIR, `temp_${Date.now()}_${song.id}.${ext}`); - const finalFile = path.join(MUSIC_DIR, `${baseName}.${ext}`); - - try { - await downloadFile(directUrl, tempFile, downloadHeaders); - - try { - // Try to download cover image - let coverPath = null; - try { - const coverUrl = `${TUNEHUB_API_URL}/api/?source=${source}&id=${song.id}&type=pic`; - coverPath = path.join(MUSIC_DIR, `temp_cover_${Date.now()}_${song.id}.jpg`); - await downloadFile(coverUrl, coverPath, downloadHeaders); - } catch (e) { - console.warn(`[Sync] Failed to download cover for ${song.name}:`, e.message); - } - - // Write Metadata (Title, Artist, Album, Cover) - await writeMetadata(tempFile, finalFile, { - title: song.name, - artist: song.artist, - album: song.album || song.name, - cover: coverPath - }); - - console.log(`[Sync] Downloaded & Tagged: ${baseName}.${ext}`); - - // Cleanup temp files - if (fs.existsSync(tempFile)) fs.unlinkSync(tempFile); - if (coverPath && fs.existsSync(coverPath)) fs.unlinkSync(coverPath); - return true; // Downloaded - } catch (err) { - console.error(`[Sync] Metadata Error for ${song.name}:`, err.message); - // Fallback: Just rename temp to final - if (!fs.existsSync(finalFile)) { - fs.renameSync(tempFile, finalFile); - } - return true; // Downloaded (fallback) - } - } catch (err) { - console.error(`[Sync] Download failed for ${song.name} (${candidate.label}):`, err.message); - if (fs.existsSync(tempFile)) fs.unlinkSync(tempFile); - } + // 回退:搜索拿到可用歌曲 ID 再尝试下载 + const searchHit = await searchSongOnTuneHub(song.name, song.artist, source); + if (searchHit?.id) { + const fallbackCandidates = buildDownloadCandidates(source, searchHit.id, searchHit.url, searchHit.types); + const fallbackOk = await tryDownloadWithCandidates(song, source, baseName, fallbackCandidates, downloadHeaders); + if (fallbackOk) return true; } return false; // Failed