andrade-soulseek-downloader
Version:
Simple, safe Soulseek download library with built-in rate limiting to prevent bans
255 lines • 12 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 __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