UNPKG

andrade-soulseek-downloader

Version:

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

255 lines 12 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 __param = (this && this.__param) || function (paramIndex, decorator) { return function (target, key) { decorator(target, key, paramIndex); } }; Object.defineProperty(exports, "__esModule", { value: true }); exports.DownloadTrackUseCase = void 0; const inversify_1 = require("inversify"); const domain_1 = require("../../domain"); /** * Application use case for downloading tracks. * Orchestrates domain logic with retry strategy and rate limiting. */ let DownloadTrackUseCase = class DownloadTrackUseCase { trackRepository; selectionService; racingService; logger; rateLimiter; youtubeDownloader; /** * @param trackRepository - Repository for track operations * @param selectionService - Service for track quality selection * @param racingService - Service for concurrent download racing * @param logger - Logger for operation tracking * @param rateLimiter - Rate limiter to prevent bans * @param youtubeDownloader - YouTube downloader for fallback */ constructor(trackRepository, selectionService, racingService, logger, rateLimiter, youtubeDownloader) { this.trackRepository = trackRepository; this.selectionService = selectionService; this.racingService = racingService; this.logger = logger; this.rateLimiter = rateLimiter; this.youtubeDownloader = youtubeDownloader; } /** * Executes track download with quality-first strategy. * @param request - Download parameters * @returns Download result with file path or error */ async execute(request) { try { this.validateRequest(request); const minBitrate = new domain_1.Bitrate(request.minBitrate || 128); const maxBitrate = request.maxBitrate ? new domain_1.Bitrate(request.maxBitrate) : undefined; // Log quality range const qualityRange = maxBitrate ? `${minBitrate.getValue()}-${maxBitrate.getValue()} kbps` : `${minBitrate.getValue()}+ kbps`; this.logger.info(`Searching for ${request.artist} - ${request.title} (${qualityRange})`); // Search for tracks with rate limiting const tracks = await this.rateLimiter.executeWithDelay(() => this.trackRepository.search(request.artist, request.title, minBitrate.getValue(), maxBitrate?.getValue(), request.searchTimeout), 'search'); if (tracks.length === 0) { // No tracks found on Soulseek, try YouTube as fallback this.logger.warning('No tracks found on Soulseek, attempting YouTube download as fallback'); const youtubeResult = await this.youtubeDownloader.download({ artist: request.artist, title: request.title, label: request.label, destinationDir: request.destinationDir }); if (youtubeResult) { this.logger.success(`Successfully downloaded from YouTube: ${youtubeResult}`); return { success: true, filePath: youtubeResult, attemptsMade: 1, quality: 192, // YouTube downloads are at 192kbps source: 'YouTube' }; } return { success: false, error: 'No tracks found on Soulseek or YouTube', attemptsMade: 1 }; } // Select best tracks for download with racing strategy const enableRacing = request.enableDownloadRacing !== false; // Default to true const concurrentDownloads = request.concurrentDownloads || 3; if (enableRacing) { return await this.executeWithRacing(tracks, request, concurrentDownloads); } else { return await this.executeSequentially(tracks, request); } return { success: false, error: 'All download attempts failed', attemptsMade: 0 }; } catch (error) { this.logger.error(`Download use case error: ${error}`); return { success: false, error: error instanceof Error ? error.message : 'Unknown error', attemptsMade: 0 }; } } /** Validates request parameters */ validateRequest(request) { if (!request.artist || !request.title) { throw new Error('Artist and title are required'); } if (!request.destinationDir) { throw new Error('Destination directory is required'); } } /** * Executes download with racing strategy - concurrent downloads from different users */ async executeWithRacing(tracks, request, concurrentDownloads) { // Group tracks for racing const trackGroups = this.racingService.groupTracksForRacing(tracks, concurrentDownloads); this.racingService.logRacingStrategy(trackGroups); let totalAttempts = 0; // Try each quality group with racing for (const group of trackGroups) { totalAttempts += group.length; // Create download function for racing const downloadFn = async (track) => { const destinationPath = this.buildDestinationPath(request.destinationDir, request.artist, request.title, track.getFilePath()); // Check if already exists if (await this.trackRepository.exists(destinationPath)) { this.logger.info(`File already exists: ${destinationPath}`); return destinationPath; } // Download with rate limiting return await this.rateLimiter.executeWithDelay(() => this.trackRepository.download(track, destinationPath), 'download'); }; // Race downloads in this quality group const result = await this.racingService.raceDownloads(group, downloadFn); if (result) { this.logger.success(`Racing completed successfully with ${result.winner.getUser()}`); return { success: true, filePath: result.result, attemptsMade: totalAttempts, quality: result.winner.getBitrate().getValue() }; } } // All Soulseek racing attempts failed, try YouTube as last resort this.logger.warning('All Soulseek racing attempts failed, trying YouTube as fallback'); const youtubeResult = await this.youtubeDownloader.download({ artist: request.artist, title: request.title, label: request.label, destinationDir: request.destinationDir }); if (youtubeResult) { this.logger.success(`Successfully downloaded from YouTube: ${youtubeResult}`); return { success: true, filePath: youtubeResult, attemptsMade: totalAttempts + 1, quality: 192, source: 'YouTube' }; } return { success: false, error: 'All Soulseek and YouTube attempts failed', attemptsMade: totalAttempts + 1 }; } /** * Executes download sequentially - traditional one-by-one approach */ async executeSequentially(tracks, request) { const selectedTracks = this.selectionService.selectBestTracks(tracks, request.maxAttempts || 10); this.logger.info(`Found ${tracks.length} tracks, selected ${selectedTracks.length} for sequential download`); let attemptsMade = 0; for (const track of selectedTracks) { attemptsMade++; const destinationPath = this.buildDestinationPath(request.destinationDir, request.artist, request.title, track.getFilePath()); // Check if file already exists if (await this.trackRepository.exists(destinationPath)) { this.logger.info(`File already exists: ${destinationPath}`); return { success: true, filePath: destinationPath, attemptsMade, quality: track.getBitrate().getValue() }; } // Attempt download with rate limiting this.logger.info(`Attempting download from ${track.getUser()} (${track.getBitrate().toString()})`); const filePath = await this.rateLimiter.executeWithDelay(() => this.trackRepository.download(track, destinationPath), 'download'); if (filePath) { this.logger.success(`Successfully downloaded at ${track.getBitrate().toString()}`); return { success: true, filePath, attemptsMade, quality: track.getBitrate().getValue() }; } this.logger.warning(`Failed to download from ${track.getUser()}`); } // All sequential Soulseek attempts failed, try YouTube as last resort this.logger.warning('All sequential Soulseek attempts failed, trying YouTube as fallback'); const youtubeResult = await this.youtubeDownloader.download({ artist: request.artist, title: request.title, label: request.label, destinationDir: request.destinationDir }); if (youtubeResult) { this.logger.success(`Successfully downloaded from YouTube: ${youtubeResult}`); return { success: true, filePath: youtubeResult, attemptsMade: attemptsMade + 1, quality: 192, source: 'YouTube' }; } return { success: false, error: 'All Soulseek and YouTube attempts failed', attemptsMade: attemptsMade + 1 }; } /** Builds safe file path from metadata */ buildDestinationPath(dir, artist, title, originalPath) { const ext = originalPath.substring(originalPath.lastIndexOf('.')); const filename = `${artist} - ${title}`.replace(/[^\w\s-]/g, '').replace(/\s+/g, '_'); return `${dir}/${filename}${ext}`; } }; exports.DownloadTrackUseCase = DownloadTrackUseCase; exports.DownloadTrackUseCase = DownloadTrackUseCase = __decorate([ (0, inversify_1.injectable)(), __param(0, (0, inversify_1.inject)('ITrackRepository')), __param(1, (0, inversify_1.inject)('TrackSelectionService')), __param(2, (0, inversify_1.inject)('DownloadRacingService')), __param(3, (0, inversify_1.inject)('ILogger')), __param(4, (0, inversify_1.inject)('IRateLimiter')), __param(5, (0, inversify_1.inject)('YouTubeDownloader')), __metadata("design:paramtypes", [Object, domain_1.TrackSelectionService, domain_1.DownloadRacingService, Object, Object, Function]) ], DownloadTrackUseCase); //# sourceMappingURL=download-track-use-case.js.map