From 1af86ed6a674058b825b6d69b515f8c07e4af6be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=8F=B2=E6=82=A6?= Date: Tue, 6 Jan 2026 15:03:06 +0800 Subject: [PATCH] =?UTF-8?q?=E5=90=8C=E6=AD=A5=E6=97=B6=E4=BC=9A=E4=B8=8B?= =?UTF-8?q?=E8=BD=BD=E9=9F=B3=E4=B9=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docker-compose.yml | 1 + index.html | 8 ++- sync-server/server.js | 153 +++++++++++++++++++++++++++++++++++++++++- 3 files changed, 158 insertions(+), 4 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 4687484..f358b8a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -13,4 +13,5 @@ services: - "7482:3001" volumes: - ./data:/app/data + - ./music:/app/music restart: unless-stopped \ No newline at end of file diff --git a/index.html b/index.html index 67a93ea..b2b2b44 100644 --- a/index.html +++ b/index.html @@ -851,9 +851,10 @@ const handleSync = async () => { if (!syncToken) return; + + // Sync Favorites const cloudFavorites = await syncService.get('favorites', syncToken); if (cloudFavorites && Array.isArray(cloudFavorites)) { - // Merge strategy: Combine unique songs by ID setFavorites(prev => { const combined = [...prev]; cloudFavorites.forEach(cloudSong => { @@ -864,6 +865,11 @@ return combined; }); } + + // Also push current favorites to cloud to ensure sync + if (favorites.length > 0) { + syncService.set('favorites', favorites, syncToken); + } }; useEffect(() => { diff --git a/sync-server/server.js b/sync-server/server.js index 536c24b..9bdc994 100644 --- a/sync-server/server.js +++ b/sync-server/server.js @@ -1,15 +1,20 @@ const http = require('http'); +const https = require('https'); const fs = require('fs'); const path = require('path'); const url = require('url'); const DATA_DIR = process.env.DATA_DIR || './data'; +const MUSIC_DIR = process.env.MUSIC_DIR || './music'; const PORT = process.env.PORT || 3001; -// Ensure data directory exists +// Ensure directories exist if (!fs.existsSync(DATA_DIR)) { fs.mkdirSync(DATA_DIR, { recursive: true }); } +if (!fs.existsSync(MUSIC_DIR)) { + fs.mkdirSync(MUSIC_DIR, { recursive: true }); +} const server = http.createServer((req, res) => { // CORS headers @@ -64,11 +69,17 @@ const server = http.createServer((req, res) => { req.on('end', () => { try { // Validate JSON - JSON.parse(body); + const data = JSON.parse(body); fs.writeFileSync(filePath, body); + + // Try to handle background download if data looks like a song list + // Execute in background + tryDownloadSongs(data).catch(err => console.error('Download Manager Error:', err)); + res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ success: true })); } catch (e) { + console.error(e); res.writeHead(400, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: 'Invalid JSON' })); } @@ -82,4 +93,140 @@ const server = http.createServer((req, res) => { server.listen(PORT, () => { console.log(`Sync Server running on port ${PORT}`); console.log(`Data directory: ${DATA_DIR}`); -}); \ No newline at end of file + console.log(`Music directory: ${MUSIC_DIR}`); +}); + +// --- Download Manager --- + +async function tryDownloadSongs(data) { + // Detect if data is a song list + // Must be an array, and items must have id, name, and (source or platform) + if (!Array.isArray(data) || data.length === 0) return; + + const isSongList = data.every(item => + item && + typeof item === 'object' && + item.id && + item.name && + (item.source || item.platform) + ); + + if (!isSongList) return; + + console.log(`[Sync] Detected song list. Starting background processing for ${data.length} songs...`); + + for (const song of data) { + try { + await processSong(song); + } catch (e) { + console.error(`[Sync] Error processing ${song.name}:`, e.message); + } + } +} + +function processSong(song) { + return new Promise((resolve) => { + // Basic sanitization + const safeName = (song.name || 'unknown').replace(/[\\/:*?"<>|]/g, '_'); + const safeArtist = (song.artist || 'unknown').replace(/[\\/:*?"<>|]/g, '_'); + const source = song.platform || song.source; + // Filename: Artist - Name [source_id] + const baseName = `${safeArtist} - ${safeName} [${source}_${song.id}]`; + + // Check if file exists (fuzzy match for extension) + try { + const files = fs.readdirSync(MUSIC_DIR); + // Check for standard format OR manual rename (Song Name.ext) + const exists = files.some(f => { + // 1. Standard format: Artist - Name [source_id].ext + if (f.startsWith(baseName)) return true; + + // 2. Manual rename: Name.ext (ignoring extension) + // We check if the file starts with the song name followed by a dot + if (f.startsWith(`${safeName}.`)) return true; + + return false; + }); + + if (exists) { + resolve(); + return; + } + } catch (e) { + console.error('Error reading music dir:', e); + } + + // Get URL (prefer FLAC) + const apiUrl = `https://music-dl.sayqz.com/api/?source=${source}&id=${song.id}&type=url&br=flac`; + + https.get(apiUrl, (res) => { + 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(); + return; + } + + // Determine extension + 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 dest = path.join(MUSIC_DIR, `${baseName}.${ext}`); + downloadFile(url, dest) + .then(() => { + console.log(`[Sync] Downloaded: ${baseName}.${ext}`); + resolve(); + }) + .catch(err => { + console.error(`[Sync] Download failed for ${song.name}:`, err.message); + resolve(); + }); + } catch (e) { + console.error('[Sync] API Parse Error:', e); + resolve(); + } + }); + }).on('error', (e) => { + console.error(`[Sync] API Request Error: ${e.message}`); + resolve(); + }); + }); +} + +function downloadFile(url, dest) { + return new Promise((resolve, reject) => { + const protocol = url.startsWith('https') ? https : http; + protocol.get(url, (res) => { + // Handle Redirects + if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) { + downloadFile(res.headers.location, dest).then(resolve).catch(reject); + return; + } + if (res.statusCode !== 200) { + reject(new Error(`Status code ${res.statusCode}`)); + return; + } + const file = fs.createWriteStream(dest); + res.pipe(file); + file.on('finish', () => { + file.close(() => resolve()); + }); + file.on('error', (err) => { + fs.unlink(dest, () => {}); + reject(err); + }); + }).on('error', (err) => { + fs.unlink(dest, () => {}); + reject(err); + }); + }); +} \ No newline at end of file