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