UNPKG

andrade-soulseek-downloader

Version:

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

331 lines 14.8 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.InteractiveSoulseek = void 0; exports.runInteractiveSoulseek = runInteractiveSoulseek; const dotenv = __importStar(require("dotenv")); const prompts_1 = __importDefault(require("prompts")); const core_1 = require("../../core"); const utils_1 = require("../../utils"); const chalk_1 = __importDefault(require("chalk")); dotenv.config(); /** * Interactive Soulseek-only CLI mode. * Allows user to search, select files manually, and retry on failures. */ class InteractiveSoulseek { downloader; logger; isRunning = true; constructor() { this.logger = new utils_1.Logger(); this.downloader = new core_1.SoulseekDownloader({ searchTimeout: parseInt(process.env.SOULSEEK_SEARCH_TIMEOUT || '30000'), downloadTimeout: parseInt(process.env.SOULSEEK_DOWNLOAD_TIMEOUT || '120000'), searchDelay: parseInt(process.env.SOULSEEK_SEARCH_DELAY || '5000'), downloadDelay: parseInt(process.env.SOULSEEK_DOWNLOAD_DELAY || '3000'), }); // Handle Ctrl+C gracefully prompts_1.default.override({ cancel: () => { this.isRunning = false; } }); } /** * Checks if user wants to quit. */ isQuitCommand(value) { if (typeof value === 'string') { const lower = value.toLowerCase(); return lower === 'quit' || lower === 'q'; } return false; } /** * Displays welcome header. */ displayWelcome() { console.log('\n' + chalk_1.default.cyan.bold('═══════════════════════════════════════════════════')); console.log(chalk_1.default.cyan.bold(' 🎵 Interactive Soulseek Download Mode 🎵')); console.log(chalk_1.default.cyan.bold('═══════════════════════════════════════════════════')); console.log(chalk_1.default.gray('Type "quit" or "q" at any time to exit\n')); } /** * Gets search query from user. */ async getSearchQuery() { const response = await (0, prompts_1.default)({ type: 'text', name: 'query', message: '🔍 Enter search query (artist + title):', validate: (value) => value.length > 0 || 'Search query cannot be empty', }); if (!response.query || this.isQuitCommand(response.query)) { return null; } return response.query; } /** * Formats a result for display in the selection menu. */ formatResult(result) { const fileName = result.file.split(/[/\\]/).pop() || result.file; const fileExt = fileName.slice(fileName.lastIndexOf('.')).toLowerCase(); const sizeMB = (result.size / 1024 / 1024).toFixed(2); const speedMBs = (result.speed / 1024 / 1024).toFixed(1); // Format quality indicator let qualityIcon = '●'; if (fileExt === '.flac' || fileExt === '.wav' || fileExt === '.aif' || fileExt === '.aiff') { qualityIcon = '♦'; } else if (result.bitrate >= 320) { qualityIcon = '⭐'; } // Truncate filename if too long const displayName = fileName.length > 50 ? fileName.substring(0, 47) + '...' : fileName; return `${qualityIcon} ${displayName}${result.user}${result.bitrate}kbps │ ${sizeMB}MB │ ${speedMBs}MB/s`; } /** * Gets user file selection with arrow keys. */ async getUserSelection(results) { const choices = results.map((result, index) => ({ title: this.formatResult(result), value: result, })); // Add refresh and quit options choices.push({ title: chalk_1.default.cyan('🔄 Refresh search results'), value: 'refresh', }); const response = await (0, prompts_1.default)({ type: 'select', name: 'file', message: '📥 Select file to download:', choices, initial: 0, }); if (!response.file) { return null; // User cancelled } if (response.file === 'refresh') { return 'refresh'; } return response.file; } /** * Performs search with user query. */ async performSearch(query) { try { console.log(chalk_1.default.cyan(`\n🔍 Searching for: "${query}"...\n`)); // Connect if not connected await this.downloader.connect(); // Parse query (simple split by space, user can provide "artist title") const parts = query.split(' '); const artist = parts.slice(0, Math.ceil(parts.length / 2)).join(' '); const title = parts.slice(Math.ceil(parts.length / 2)).join(' '); // Search with relaxed matching to get all results const results = await this.downloader.search({ artist: artist || query, title: title || '', minBitrate: parseInt(process.env.SOULSEEK_MIN_QUALITY_BITRATE || '96'), maxBitrate: process.env.SOULSEEK_MAX_QUALITY_BITRATE ? parseInt(process.env.SOULSEEK_MAX_QUALITY_BITRATE) : undefined, timeout: parseInt(process.env.SOULSEEK_SEARCH_TIMEOUT || '30000'), maxResults: 100, strictMatching: false, // Relaxed to get more results }); if (results.length === 0) { console.log(chalk_1.default.red('❌ No results found. Try a different query.')); return null; } // Filter out results with no available slots const availableResults = results.filter(result => result.slots); if (availableResults.length === 0) { console.log(chalk_1.default.red('❌ No results with available slots found.')); console.log(chalk_1.default.yellow(` Found ${results.length} results but all users have no slots available.`)); console.log(chalk_1.default.gray(' Try a different query or wait for users to free up slots.')); return null; } if (availableResults.length < results.length) { console.log(chalk_1.default.yellow(`ℹ Filtered out ${results.length - availableResults.length} results with no available slots`)); } return availableResults; } catch (error) { console.log(chalk_1.default.red(`❌ Search error: ${error.message}`)); return null; } } /** * Attempts to download selected file. */ async attemptDownload(result, query) { try { const fileName = result.file.split(/[/\\]/).pop() || 'download'; const downloadDir = process.env.SOULSEEK_DOWNLOAD_DIR || './downloads'; console.log(chalk_1.default.cyan('\n📥 Starting download...')); console.log(chalk_1.default.gray(`File: ${fileName}`)); console.log(chalk_1.default.gray(`User: ${result.user}`)); console.log(chalk_1.default.gray(`Size: ${(result.size / 1024 / 1024).toFixed(2)} MB`)); console.log(chalk_1.default.gray(`Bitrate: ${result.bitrate} kbps\n`)); const downloadResult = await this.downloader.download(result, query, // Use query as artist '', // No specific title undefined, // No folder query.replace(/[^\p{L}\p{N} ]/gu, '').replace(/\s+/g, '_') // Sanitized filename ); if (downloadResult.path) { console.log('\n' + chalk_1.default.green.bold('✅ Download successful!')); console.log(chalk_1.default.green(`📁 Saved to: ${downloadResult.path}\n`)); return true; } else if (downloadResult.timeout) { console.log(chalk_1.default.red('\n❌ Download timed out')); return false; } else { console.log(chalk_1.default.red('\n❌ Download failed')); return false; } } catch (error) { console.log(chalk_1.default.red(`\n❌ Download error: ${error.message}`)); return false; } } /** * Main interactive loop. */ async run() { this.displayWelcome(); try { while (this.isRunning) { // Get search query const query = await this.getSearchQuery(); if (!query) { console.log(chalk_1.default.yellow('\n👋 Exiting...')); break; } // Perform search let results = await this.performSearch(query); if (!results || results.length === 0) { continue; // Ask for new query } // Download loop for current search let downloadSuccess = false; while (!downloadSuccess && this.isRunning) { console.log(chalk_1.default.cyan(`\n📊 Found ${results.length} files with available slots`)); // Get user selection with arrow keys const selection = await this.getUserSelection(results); if (!selection) { // User cancelled (Ctrl+C) console.log(chalk_1.default.yellow('\n👋 Exiting...')); this.isRunning = false; break; } if (selection === 'refresh') { console.log(chalk_1.default.cyan('\n🔄 Refreshing search results...')); results = await this.performSearch(query); if (!results || results.length === 0) { break; // Exit download loop, will ask for new query } continue; } // selection is a SoulseekSearchResult downloadSuccess = await this.attemptDownload(selection, query); if (downloadSuccess) { // Ask if user wants to download another file const response = await (0, prompts_1.default)({ type: 'confirm', name: 'continue', message: '🎵 Download another track?', initial: false, }); if (response.continue) { downloadSuccess = false; // Reset to exit download loop and get new query break; // Exit download loop to get new query } else { this.isRunning = false; console.log(chalk_1.default.yellow('\n👋 Goodbye!')); } } else { // Download failed, ask what to do const response = await (0, prompts_1.default)({ type: 'select', name: 'action', message: '🔄 What would you like to do?', choices: [ { title: 'Try another file from results', value: 'retry' }, { title: 'Refresh search results', value: 'refresh' }, { title: 'Quit', value: 'quit' }, ], initial: 0, }); if (!response.action || response.action === 'quit') { this.isRunning = false; console.log(chalk_1.default.yellow('\n👋 Exiting...')); break; } else if (response.action === 'refresh') { console.log(chalk_1.default.cyan('\n🔄 Refreshing search results...')); results = await this.performSearch(query); if (!results || results.length === 0) { break; } } // Otherwise continue loop to select another file (action === 'retry') } } } } finally { await this.downloader.disconnect(); console.log(chalk_1.default.gray('\n✨ Disconnected from Soulseek network\n')); } } } exports.InteractiveSoulseek = InteractiveSoulseek; /** * Entry point for interactive mode. */ async function runInteractiveSoulseek() { const interactive = new InteractiveSoulseek(); await interactive.run(); } //# sourceMappingURL=interactive-soulseek.js.map