andrade-soulseek-downloader
Version:
Simple, safe Soulseek download library with built-in rate limiting to prevent bans
189 lines • 8.58 kB
JavaScript
;
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