UNPKG

andrade-soulseek-downloader

Version:

Simple, safe Soulseek download library with built-in rate limiting to prevent bans

189 lines 8.58 kB
"use strict"; var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; return c > 3 && r && Object.defineProperty(target, key, r), r; }; var __metadata = (this && this.__metadata) || function (k, v) { if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v); }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.JunoDownloader = void 0; const puppeteer_1 = __importDefault(require("puppeteer")); const fs_1 = __importDefault(require("fs")); const path_1 = __importDefault(require("path")); const https_1 = __importDefault(require("https")); const http_1 = __importDefault(require("http")); const inversify_1 = require("inversify"); let JunoDownloader = class JunoDownloader { baseUrl = 'https://www.juno.co.uk'; userAgent = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'; constructor() { console.log('[Juno] 🎵 Juno downloader initialized'); } /** * Searches Juno.co.uk for a track and returns the first snippet URL found */ async searchJuno(artist, title) { const browser = await puppeteer_1.default.launch({ headless: true, args: ['--no-sandbox', '--disable-setuid-sandbox'] }); try { const page = await browser.newPage(); // Set user agent await page.setUserAgent(this.userAgent); // Build search URL const searchQuery = `${artist} ${title}`.trim(); const searchUrl = `${this.baseUrl}/search/?q[all][]=${encodeURIComponent(searchQuery)}&show_out_of_stock=1`; console.log(`[Juno] 🔍 Searching: ${searchUrl}`); // Navigate to search results await page.goto(searchUrl, { waitUntil: 'networkidle2', timeout: 30000 }); // Wait for results to load await page.waitForSelector('.vi-tracklist, .product-list', { timeout: 10000 }).catch(() => { console.log('[Juno] ⚠️ No search results container found'); }); // Extract snippet URLs from .btn-play-sm elements // The function passed to page.evaluate runs in browser context, not Node.js const snippetUrl = await page.evaluate(() => { // Look for play buttons with audio snippets const doc = globalThis.document; const playButtons = doc.querySelectorAll('.btn-play-sm'); for (const button of playButtons) { const href = button.getAttribute('href'); if (href && href.includes('.mp3')) { return href; } } return null; }); if (snippetUrl) { console.log(`[Juno] ✅ Found snippet: ${snippetUrl}`); return snippetUrl; } console.log('[Juno] ❌ No snippet found in search results'); return null; } catch (error) { console.error(`[Juno] ❌ Search error: ${error.message}`); return null; } finally { await browser.close(); } } /** * Downloads a file from a URL */ async downloadFile(url, outputPath) { return new Promise((resolve, reject) => { const file = fs_1.default.createWriteStream(outputPath); const protocol = url.startsWith('https') ? https_1.default : http_1.default; console.log(`[Juno] ⬇️ Downloading from: ${url}`); const request = protocol.get(url, { headers: { 'User-Agent': this.userAgent, 'Referer': this.baseUrl } }, (response) => { // Handle redirects if (response.statusCode === 301 || response.statusCode === 302) { const redirectUrl = response.headers.location; if (redirectUrl) { console.log(`[Juno] 🔄 Following redirect to: ${redirectUrl}`); file.close(); fs_1.default.unlinkSync(outputPath); this.downloadFile(redirectUrl, outputPath).then(resolve).catch(reject); return; } } if (response.statusCode !== 200) { file.close(); fs_1.default.unlinkSync(outputPath); reject(new Error(`Failed to download: HTTP ${response.statusCode}`)); return; } const totalBytes = parseInt(response.headers['content-length'] || '0', 10); let downloadedBytes = 0; response.on('data', (chunk) => { downloadedBytes += chunk.length; if (totalBytes > 0) { const percent = ((downloadedBytes / totalBytes) * 100).toFixed(1); process.stdout.write(`\r[Juno] 📥 Progress: ${percent}%`); } }); response.pipe(file); file.on('finish', () => { file.close(); console.log('\n[Juno] ✅ Download completed'); resolve(); }); }); request.on('error', (err) => { file.close(); if (fs_1.default.existsSync(outputPath)) { fs_1.default.unlinkSync(outputPath); } reject(err); }); file.on('error', (err) => { file.close(); if (fs_1.default.existsSync(outputPath)) { fs_1.default.unlinkSync(outputPath); } reject(err); }); }); } /** * Downloads a snippet from Juno.co.uk */ async download(options) { const downloadDir = options.destinationDir || process.env.DOWNLOAD_DIR || './downloads'; // Ensure download directory exists if (!fs_1.default.existsSync(downloadDir)) { fs_1.default.mkdirSync(downloadDir, { recursive: true }); } try { console.log(`[Juno] 🔍 Searching for: "${options.artist} - ${options.title}"`); // Search for snippet const snippetUrl = await this.searchJuno(options.artist, options.title); if (!snippetUrl) { console.error('[Juno] ❌ No snippet found'); return null; } // Generate filename with _juno suffix const baseFilename = options.customFileName || `${options.artist} - ${options.title}`; const finalFilename = `${baseFilename}_juno.mp3`.replace(/[<>:"/\\|?*]/g, '_'); const finalPath = path_1.default.join(downloadDir, finalFilename); // Check if file already exists if (fs_1.default.existsSync(finalPath)) { console.log(`[Juno] 📁 File already exists: ${finalPath}`); return finalPath; } // Download snippet console.log('[Juno] 🎯 Starting download process...'); await this.downloadFile(snippetUrl, finalPath); console.log(`[Juno] ✅ Downloaded successfully!`); console.log(`[Juno] 📍 Saved to: ${finalPath}`); return finalPath; } catch (error) { console.error(`[Juno] ❌ Download failed: ${error.message || error}`); return null; } } }; exports.JunoDownloader = JunoDownloader; exports.JunoDownloader = JunoDownloader = __decorate([ (0, inversify_1.injectable)(), __metadata("design:paramtypes", []) ], JunoDownloader); //# sourceMappingURL=juno-downloader.js.map