同步时会下载音乐
This commit is contained in:
@@ -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}`);
|
||||
});
|
||||
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);
|
||||
});
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user