UNPKG

andrade-soulseek-downloader

Version:

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

212 lines 10.6 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.YouTubeDownloader = void 0; const ytdl_core_1 = __importDefault(require("@distube/ytdl-core")); const ytsr_1 = __importDefault(require("@distube/ytsr")); const fs_1 = __importDefault(require("fs")); const path_1 = __importDefault(require("path")); const fluent_ffmpeg_1 = __importDefault(require("fluent-ffmpeg")); const ffmpeg_1 = __importDefault(require("@ffmpeg-installer/ffmpeg")); const inversify_1 = require("inversify"); let YouTubeDownloader = class YouTubeDownloader { constructor() { // Set ffmpeg path fluent_ffmpeg_1.default.setFfmpegPath(ffmpeg_1.default.path); } calculateRelevanceScore(searchQuery, videoTitle, videoDuration, views) { const queryWords = searchQuery.toLowerCase().split(' '); const titleWords = videoTitle.toLowerCase(); let score = 0; // Check for exact matches if (titleWords.includes(searchQuery.toLowerCase())) { score += 50; } // Check for individual word matches queryWords.forEach(word => { if (titleWords.includes(word)) { score += 10; } }); // Prefer videos with reasonable duration (3-10 minutes) const durationParts = videoDuration.split(':'); const totalSeconds = durationParts.reduce((acc, part, index) => { return acc + parseInt(part) * Math.pow(60, durationParts.length - 1 - index); }, 0); if (totalSeconds >= 180 && totalSeconds <= 600) { score += 20; } // Consider view count (logarithmic scale) score += Math.log10(views + 1) * 2; return score; } async searchYouTube(options) { const searchQuery = `${options.artist} ${options.title} ${options.label || ''}`.trim(); try { const searchResults = await (0, ytsr_1.default)(searchQuery, { limit: 10 }); const videos = searchResults.items .filter((item) => item.type === 'video') .map((video) => { const viewsStr = typeof video.views === 'string' ? video.views.replace(/[^\d]/g, '') : (video.views || '0').toString(); const viewCount = parseInt(viewsStr || '0'); const videoTitle = video.name || video.title || ''; return { url: video.url, title: videoTitle, duration: video.duration || '0:00', views: viewCount, relevanceScore: this.calculateRelevanceScore(searchQuery, videoTitle, video.duration || '0:00', viewCount) }; }); // Sort by relevance score videos.sort((a, b) => b.relevanceScore - a.relevanceScore); return videos; } catch (error) { throw new Error(`Failed to search YouTube: ${error}`); } } async convertToMp3(inputPath, outputPath) { return new Promise((resolve, reject) => { console.log('[YouTube] 🎵 Converting to MP3 (192kbps)...'); (0, fluent_ffmpeg_1.default)(inputPath) .toFormat('mp3') .audioBitrate('192k') .on('progress', (progress) => { process.stdout.write(`\r[YouTube] Converting: ${Math.round(progress.percent || 0)}%`); }) .on('end', () => { console.log('\n[YouTube] ✅ Conversion completed'); // Delete the temporary WebM file fs_1.default.unlinkSync(inputPath); resolve(); }) .on('error', (err) => { console.error('[YouTube] ❌ Conversion error:', err); reject(err); }) .save(outputPath); }); } async downloadVideo(url, outputPath) { return new Promise(async (resolve, reject) => { try { // Get video info first const info = await ytdl_core_1.default.getInfo(url); console.log('[YouTube] 🎦 Video title:', info.videoDetails.title); const lengthSeconds = parseInt(info.videoDetails.lengthSeconds); console.log('[YouTube] ⏱️ Duration:', Math.floor(lengthSeconds / 60), 'minutes', lengthSeconds % 60, 'seconds'); // Get the best audio format const audioFormats = ytdl_core_1.default.filterFormats(info.formats, 'audioonly'); if (audioFormats.length === 0) { throw new Error('No audio formats found'); } console.log(`[YouTube] 🎧 Found ${audioFormats.length} audio formats`); const bestAudio = audioFormats[0]; console.log('[YouTube] 🎶 Using format:', bestAudio.mimeType, '| Audio bitrate:', bestAudio.audioBitrate || 'unknown'); // Download with the specific format const stream = (0, ytdl_core_1.default)(url, { format: bestAudio, quality: 'highestaudio' }); const writeStream = fs_1.default.createWriteStream(outputPath); let downloadedBytes = 0; const totalBytes = parseInt(bestAudio.contentLength || '0'); stream.on('progress', (chunkLength, downloaded, total) => { downloadedBytes = downloaded; const percent = ((downloaded / total) * 100).toFixed(1); const downloadedMB = (downloaded / 1024 / 1024).toFixed(2); const totalMB = (total / 1024 / 1024).toFixed(2); process.stdout.write(`\r[YouTube] ⬇️ Downloading: ${percent}% (${downloadedMB} MB / ${totalMB} MB)`); }); stream.on('error', (error) => { console.error('\n[YouTube] ❌ Stream error:', error.message); reject(error); }); stream.on('end', () => { console.log('\n[YouTube] 🏁 Download stream completed'); }); writeStream.on('error', (error) => { console.error('[YouTube] ❌ Write error:', error.message); reject(error); }); writeStream.on('finish', () => { console.log('[YouTube] 💾 File write completed'); resolve(); }); stream.pipe(writeStream); } catch (error) { console.error('[YouTube] ❌ Setup error:', error); reject(error); } }); } 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 { // Search for videos console.log(`[YouTube] 🔍 Searching for: "${options.artist} - ${options.title}"`); const searchResults = await this.searchYouTube(options); if (searchResults.length === 0) { console.error('[YouTube] ❌ No search results found'); return null; } // Select the most relevant result const bestMatch = searchResults[0]; console.log(`[YouTube] ✨ Found ${searchResults.length} results`); console.log(`[YouTube] 🏆 Best match: "${bestMatch.title}"`); console.log(`[YouTube] 🔗 URL: ${bestMatch.url}`); console.log(`[YouTube] 📊 Relevance score: ${bestMatch.relevanceScore.toFixed(1)} | Duration: ${bestMatch.duration} | Views: ${bestMatch.views.toLocaleString()}`); // Generate filenames const baseFilename = options.customFileName || `${options.artist} - ${options.title}`; const tempFilename = `${baseFilename}_temp.webm`.replace(/[<>:"/\\|?*]/g, '_'); const finalFilename = `${baseFilename}.mp3`.replace(/[<>:"/\\|?*]/g, '_'); const tempPath = path_1.default.join(downloadDir, tempFilename); const finalPath = path_1.default.join(downloadDir, finalFilename); // Check if file already exists if (fs_1.default.existsSync(finalPath)) { console.log(`[YouTube] 📁 File already exists: ${finalPath}`); return finalPath; } // Download the video to temporary WebM file console.log('[YouTube] 🎯 Starting download process...'); await this.downloadVideo(bestMatch.url, tempPath); // Convert to MP3 await this.convertToMp3(tempPath, finalPath); console.log(`[YouTube] ✅ Downloaded successfully!`); console.log(`[YouTube] 📍 Saved to: ${finalPath}`); return finalPath; } catch (error) { console.error(`[YouTube] ❌ Download failed: ${error.message || error}`); if (error.message?.includes('410')) { console.error('[YouTube] 🚨 Video may be age-restricted or unavailable'); } return null; } } }; exports.YouTubeDownloader = YouTubeDownloader; exports.YouTubeDownloader = YouTubeDownloader = __decorate([ (0, inversify_1.injectable)(), __metadata("design:paramtypes", []) ], YouTubeDownloader); //# sourceMappingURL=youtube-downloader.js.map