diff --git a/Netease-sync/server.js b/Netease-sync/server.js index e821531..93008c9 100644 --- a/Netease-sync/server.js +++ b/Netease-sync/server.js @@ -232,13 +232,13 @@ function sanitizeFilename(str) { .substring(0, 200); // Limit length to avoid path too long errors } -function downloadFile(url, dest) { +function downloadFile(url, dest, headers = {}) { return new Promise((resolve, reject) => { const protocol = url.startsWith('https') ? https : http; - protocol.get(url, (res) => { + protocol.get(url, { headers }, (res) => { // Handle Redirects if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) { - downloadFile(res.headers.location, dest).then(resolve).catch(reject); + downloadFile(res.headers.location, dest, headers).then(resolve).catch(reject); return; } if (res.statusCode !== 200) { @@ -261,6 +261,49 @@ function downloadFile(url, dest) { }); } +function buildDownloadHeaders(source) { + const headers = { + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36' + }; + if (source === 'netease') { + headers['Referer'] = 'https://music.163.com/'; + } + return headers; +} + +function getPreferredQualities(types) { + const preference = ['flac24bit', 'flac', '320k', '128k']; + if (!Array.isArray(types) || types.length === 0) { + return ['320k', '128k']; + } + const normalized = types.map(t => t?.toString().toLowerCase()).filter(Boolean); + const available = preference.filter(q => normalized.includes(q)); + return available.length > 0 ? available : ['320k', '128k']; +} + +async function resolveTuneHubAudioUrl(apiUrl) { + try { + const response = await apiClient.get(apiUrl, { + maxRedirects: 0, + validateStatus: (status) => status >= 200 && status < 400 + }); + + if (response.status >= 300 && response.status < 400 && response.headers?.location) { + return response.headers.location; + } + + if (response.status === 200 && response.data) { + const data = response.data.data || response.data; + if (typeof data === 'object' && data.url) { + return data.url; + } + } + return null; + } catch (error) { + return null; + } +} + function writeMetadata(inputPath, outputPath, metadata) { return new Promise((resolve, reject) => { const args = ['-i', inputPath]; @@ -321,8 +364,7 @@ function writeMetadata(inputPath, outputPath, metadata) { }); } -function processSong(song) { - return new Promise((resolve) => { +async function processSong(song) { const safeName = sanitizeFilename(song.name); const safeArtist = sanitizeFilename(song.artist); const source = song.platform || song.source; @@ -350,107 +392,97 @@ function processSong(song) { if (exists) { // console.log(`[Sync] Skipped (Exists): ${song.name}`); - resolve(true); // Exists - return; + return true; // Exists } } catch (e) { console.error('Error reading music dir:', e); } - // Get URL (prefer FLAC) - const apiUrl = `${TUNEHUB_API_URL}/api/?source=${source}&id=${song.id}&type=url&br=flac`; - - https.get(apiUrl, (res) => { - const handleDownload = (url) => { - let ext = 'mp3'; - if (url.includes('.flac')) ext = 'flac'; - else if (url.includes('.m4a')) ext = 'm4a'; - else if (url.includes('.ogg')) ext = 'ogg'; - else if (url.includes('.wav')) ext = 'wav'; + const downloadHeaders = buildDownloadHeaders(source); - const tempFile = path.join(MUSIC_DIR, `temp_${Date.now()}_${song.id}.${ext}`); - const finalFile = path.join(MUSIC_DIR, `${baseName}.${ext}`); + const candidates = []; + if (song.url) { + candidates.push({ label: 'playlist_url', apiUrl: song.url }); + } - downloadFile(url, tempFile) - .then(async () => { - 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); - } 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); - resolve(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); - } - resolve(true); // Downloaded (fallback) - } - }) - .catch(err => { - console.error(`[Sync] Download failed for ${song.name}:`, err.message); - if (fs.existsSync(tempFile)) fs.unlinkSync(tempFile); - resolve(false); // Failed - }); - }; - - // Handle 302 Redirect (Standard API behavior) - if (res.statusCode === 302 && res.headers.location) { - res.resume(); - handleDownload(res.headers.location); - return; - } - - if (res.statusCode !== 200) { - console.error(`[Sync] API Request Failed for ${song.name}: Status ${res.statusCode}`); - res.resume(); - resolve(false); - return; - } - - let data = ''; - res.on('data', chunk => data += chunk); - res.on('end', () => { - try { - const apiRes = JSON.parse(data); - const url = apiRes.url; - - if (!url) { - console.log(`[Sync] No URL found for ${song.name}`); - resolve(false); - return; - } - handleDownload(url); - } catch (e) { - console.error('[Sync] API Parse Error:', e); - resolve(false); - } + 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}` }); - }).on('error', (e) => { - console.error(`[Sync] API Request Error: ${e.message}`); - resolve(false); + } + + 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); + } + } + + return false; // Failed +} } async function searchSongInNavidrome(song) { @@ -616,7 +648,9 @@ async function syncPlaylist(playlist, cachedInfo = null) { artist: track.ar?.[0]?.name || track.artist || 'Unknown', album: track.al?.name || track.album || 'Unknown', platform: 'netease', - source: 'netease' + source: 'netease', + url: track.url || '', + types: track.types || [] })); // --- Integrated Download Logic ---