UNPKG

andrade-soulseek-downloader

Version:

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

1,335 lines (1,034 loc) โ€ข 43.4 kB
# ๐ŸŽต Soulseek Downloader <div align="center"> ![npm version](https://img.shields.io/npm/v/andrade-soulseek-downloader) ![npm downloads](https://img.shields.io/npm/dm/andrade-soulseek-downloader) ![license](https://img.shields.io/npm/l/andrade-soulseek-downloader) ![node version](https://img.shields.io/node/v/andrade-soulseek-downloader) [![TypeScript](https://img.shields.io/badge/TypeScript-100%25-blue.svg)](https://www.typescriptlang.org/) [![Tests](https://img.shields.io/badge/Tests-42%20passing-green.svg)](#testing) [![Architecture](https://img.shields.io/badge/Architecture-Hexagonal-purple.svg)](#architecture) [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](http://makeapullrequest.com) **๐Ÿš€ Enterprise-grade Soulseek downloader with YouTube fallback, automatic quality selection, ban protection, and clean architecture** [Installation](#-installation) โ€ข [Quick Start](#-quick-start) โ€ข [Features](#-features) โ€ข [API](#-api-reference) โ€ข [Architecture](#-architecture) โ€ข [Examples](#-examples) </div> --- ## ๐Ÿ†• Recent Updates (v1.0.22 - v1.0.27) ### ๐ŸŽฎ Interactive Mode (v1.0.26+) ```bash npm run soulseek # New interactive mode! ``` - **Arrow key navigation** (โ†‘โ†“) to browse files - **Visual selection menu** with file details - **Auto-filters locked files** - only shows available downloads - **Refresh & retry** options on failures - Perfect for browsing specific versions/releases ### ๐ŸŽต Three-Tier Fallback System (v1.0.25+) **Soulseek โ†’ Juno.co.uk โ†’ YouTube** 1. **Soulseek**: Full quality tracks (FLAC/320kbps priority) 2. **Juno.co.uk**: Preview snippets (1-2 min) if not on Soulseek 3. **YouTube**: Full tracks as last resort Files tagged with `_juno.mp3` or `_youtube.mp3` suffixes. ### โฑ๏ธ Smart Timeout for Large Files (v1.0.27) - **Inactivity-based timeout** instead of absolute time - 100MB+ files work perfectly at any speed - Timeout only triggers if NO data received for timeout period - Download a 200MB FLAC at 50 KB/s? No problem! โœ… ### ๐ŸŽฏ Enhanced Quality Selection (v1.0.24-26) - **Lossless priority**: FLAC/WAV/AIF/ALAC preferred when no max quality set - **Exhaustive search**: Tries ALL Soulseek results before fallback - **Low quality fallback**: Searches 96kbps+ as last resort - **Bitrate suffix**: Low quality files tagged (e.g., `track_128kbps.mp3`) ### ๐ŸŽฌ YouTube Improvements (v1.0.22) - **yt-dlp support**: Auto-detects and uses for better success rates - **Smart retry**: 3 attempts per video, tries 3 different videos - **Version detection**: Warns if yt-dlp needs updating - **Cookie support**: Optional `YOUTUBE_COOKIES_FILE` for authentication ### ๐Ÿ›ก๏ธ Stability Improvements - **Error handling**: Prevents "Token not found" crashes - **Progress tracking**: Better download progress indicators - **Slot filtering**: Auto-skips unavailable files [๐Ÿ“ See full changelog](#-changelog) --- ## ๐Ÿ“– Table of Contents - [๐Ÿ†• Recent Updates](#-recent-updates-v1022---v1027) - [โšก Key Features](#-key-features) - [๐Ÿ—๏ธ Architecture Highlights](#๏ธ-architecture-highlights) - [๐Ÿ›ก๏ธ Ban Protection](#๏ธ-ban-protection) - [๐Ÿ“ฆ Installation](#-installation) - [๐Ÿš€ Quick Start](#-quick-start) - [๐ŸŽฏ How It Works](#-how-it-works) - [๐Ÿ“š API Reference](#-api-reference) - [โš™๏ธ Configuration](#๏ธ-configuration) - [๐Ÿ’ก Examples](#-examples) - [๐Ÿ“ File Organization](#-file-organization) - [๐ŸŽต Juno.co.uk Fallback](#-junocouk-fallback) - [๐ŸŽฌ YouTube Fallback](#-youtube-fallback) - [๐ŸŽจ Quality Selection](#-quality-selection-algorithm) - [๐Ÿ“Š Architecture](#-architecture) - [๐Ÿงช Testing](#-testing) - [๐Ÿ”ง Advanced Usage](#-advanced-usage) - [๐Ÿ“ TypeScript Support](#-typescript-support) - [โ“ FAQ](#-faq) - [๐Ÿ› Troubleshooting](#-troubleshooting) - [๐Ÿ“ Changelog](#-changelog) - [๐Ÿค Contributing](#-contributing) ## โšก Key Features | Feature | Description | |---------|-------------| | ๐ŸŽฏ **Quality-First Downloads** | Automatically downloads the highest quality available (FLAC โ†’ 320kbps โ†’ 256kbps โ†’ 192kbps) | | ๐ŸŽฎ **Interactive Mode** | Manual file selection with browse, search, and retry capabilities (`npm run soulseek`) | | ๐ŸŽต **Juno.co.uk Fallback** | Downloads audio snippets from Juno.co.uk if track not found on Soulseek | | ๐ŸŽฌ **YouTube Fallback** | Automatically downloads from YouTube if track not found on Soulseek or Juno | | ๐Ÿ›ก๏ธ **Ban Protection** | Built-in rate limiting and queue management to prevent Soulseek bans | | ๐Ÿ“Š **Smart Selection** | Intelligent file selection based on bitrate, availability, speed, and filename match | | ๐Ÿ”„ **Auto-Retry Logic** | Tries multiple sources with progressive quality fallback | | ๐ŸŽจ **Beautiful UI** | Colored output, progress bars, and status indicators | | โšก **Simple API** | Just one function: `soulseekDownload(artist, title)` | | ๐Ÿ” **Safe Defaults** | Conservative rate limits and single connection by default | | ๐Ÿ“ˆ **Progress Tracking** | Real-time download progress with speed indicators | | ๐Ÿ—๏ธ **Clean Architecture** | Hexagonal architecture with SOLID principles | | ๐Ÿ”ท **TypeScript First** | Full TypeScript support with comprehensive type definitions | | ๐Ÿงช **Well Tested** | 49 comprehensive unit tests with high coverage | | ๐Ÿ“ฆ **Barrel Exports** | Clean imports with organized module structure | ## ๐Ÿ—๏ธ Architecture Highlights This project follows **enterprise-grade software architecture principles**: ### ๐Ÿ”ท **Hexagonal Architecture (Ports & Adapters)** - **Domain Layer**: Pure business logic with entities and services - **Application Layer**: Use cases and orchestration - **Infrastructure Layer**: External adapters (Soulseek, filesystem, logging) - **Presentation Layer**: CLI and API interfaces ### ๐Ÿ”ง **SOLID Principles Applied** - **S**ingle Responsibility: Each class has one clear purpose - **O**pen/Closed: Extensible through interfaces - **L**iskov Substitution: Proper inheritance and polymorphism - **I**nterface Segregation: Focused, minimal interfaces - **D**ependency Inversion: Depend on abstractions, not concretions ### ๐Ÿญ **Dependency Injection** - Uses InversifyJS for clean dependency management - Testable and mockable components - Easy to extend and modify ### ๐Ÿ“‚ **Clean File Organization** - **One symbol per file** for maximum clarity - **kebab-case naming** following Node.js conventions - **Barrel files** for clean imports - **Structured by layer** not by feature ## ๐Ÿ›ก๏ธ Ban Protection This library includes **automatic protection** against Soulseek bans: ```mermaid graph LR A[Search Request] --> B{Rate Limiter} B -->|Wait 5s| C[Execute Search] C --> D[Download Request] D --> E{Rate Limiter} E -->|Wait 3s| F[Execute Download] F --> G{Success?} G -->|No| H[Error Cooldown 10s] H --> D G -->|Yes| I[Complete] style B fill:#ff9999 style E fill:#ff9999 style H fill:#ffcc99 ``` ### Protection Features: - โฑ๏ธ **5-second delay** between searches - โฑ๏ธ **3-second delay** between downloads - โฑ๏ธ **10-second cooldown** after errors - ๐Ÿ”’ **Single connection** limit - ๐Ÿ“ฆ **Queue management** for multiple requests - ๐Ÿ”„ **Progressive backoff** on repeated failures ## ๐Ÿ“ฆ Installation ```bash # pnpm (recommended) pnpm add andrade-soulseek-downloader # npm npm install andrade-soulseek-downloader # yarn yarn add andrade-soulseek-downloader ``` ### TypeScript Support TypeScript definitions are **included automatically** - no need for separate `@types` packages! ## ๐Ÿš€ Quick Start ### 1๏ธโƒฃ Set up environment variables Create a `.env` file in your project root: ```env # Required SOULSEEK_USER=your_username SOULSEEK_PASSWORD=your_password SOULSEEK_SHARED_MUSIC_DIR=/path/to/shared/music SOULSEEK_DOWNLOAD_DIR=/path/to/downloads # Optional (defaults shown) SOULSEEK_MIN_QUALITY_BITRATE=128 # Minimum acceptable quality SOULSEEK_MAX_QUALITY_BITRATE=320 # Maximum quality (optional - avoids FLAC/WAV) SOULSEEK_SEARCH_DELAY=5000 # ms between searches SOULSEEK_DOWNLOAD_DELAY=3000 # ms between downloads SOULSEEK_MAX_ATTEMPTS=10 # Max download attempts SOULSEEK_DOWNLOAD_TIMEOUT=120000 # Download timeout in ms ``` ### 2๏ธโƒฃ Use the Simple API ```typescript import { soulseekDownload } from 'andrade-soulseek-downloader'; async function downloadTrack() { const filePath = await soulseekDownload('Daft Punk', 'One More Time'); if (filePath) { console.log(`โœ… Downloaded to: ${filePath}`); } else { console.log('โŒ Download failed'); } } downloadTrack(); ``` ### 3๏ธโƒฃ Or use CommonJS ```javascript const { soulseekDownload } = require('andrade-soulseek-downloader'); async function downloadTrack() { const filePath = await soulseekDownload('Daft Punk', 'One More Time'); if (filePath) { console.log(`โœ… Downloaded to: ${filePath}`); } else { console.log('โŒ Download failed'); } } downloadTrack(); ``` ### 4๏ธโƒฃ Interactive Soulseek Mode (Manual Selection) For manual file selection and browsing, use the interactive CLI: ```bash npm run soulseek # or for production build npm run soulseek:prod ``` **Features:** - ๐Ÿ” **Search prompt**: Enter any search query - ๐Ÿ“Š **Browse results**: See all files with bitrate, size, speed (only shows files with available slots) - โฌ†๏ธโฌ‡๏ธ **Arrow key selection**: Navigate files with arrow keys (up/down) - ๐ŸŽฏ **Manual selection**: Choose exactly which file to download - ๐Ÿ”„ **Retry & refresh**: If download fails, try another file or refresh search - ๐Ÿšช **Quit anytime**: Press Ctrl+C or select quit option - ๐ŸŽต **Soulseek-only**: No fallback to Juno/YouTube **Example session:** ``` ๐Ÿ” Enter search query: daft punk one more time ๐Ÿ“Š Found 3 files with available slots ๐Ÿ“ฅ Select file to download: (Use arrow keys โ†‘โ†“) โฏ โญ Daft Punk - One More Time.mp3 โ”‚ djuser123 โ”‚ 320kbps โ”‚ 8.5MB โ”‚ 2.3MB/s โ™ฆ Daft Punk - One More Time.flac โ”‚ audiophile โ”‚ 1411kbps โ”‚ 35.2MB โ”‚ 5.1MB/s โญ One More Time (Original Mix).mp3 โ”‚ musicfan โ”‚ 256kbps โ”‚ 6.8MB โ”‚ 1.8MB/s ๐Ÿ”„ Refresh search results ๐Ÿ“ฅ Starting download... โœ… Download successful! ๐Ÿ“ Saved to: /path/to/downloads/daft_punk_one_more_time.flac ๐ŸŽต Download another track? (Y/n): n ๐Ÿ‘‹ Goodbye! ``` ## ๐ŸŽฏ How It Works ```mermaid flowchart TD Start([User calls soulseekDownload]) --> Connect{Connected?} Connect -->|No| Connect2[Connect to Soulseek] Connect -->|Yes| Search[Search for files] Connect2 --> Search Search --> Found{Files found?} Found -->|No| Juno[๐ŸŽต Try Juno.co.uk] Found -->|Yes| Filter[Filter by min bitrate] Juno -->|Success| Done[โœ… Return file path] Juno -->|Failed| YouTube[๐ŸŽฌ Try YouTube] YouTube -->|Success| Done YouTube -->|Failed| Failed[โŒ Return null] Filter --> Group[Group by quality] Group --> Sort[Sort each group by:<br/>1. Slot availability<br/>2. Connection speed<br/>3. Filename match] Sort --> Try320[Try 320kbps files] Try320 -->|Success| Done Try320 -->|All failed| Try256[Try 256kbps files] Try256 -->|Success| Done Try256 -->|All failed| Try192[Try 192kbps files] Try192 -->|Success| Done Try192 -->|All failed| Try128[Try 128kbps files] Try128 -->|Success| Done Try128 -->|All failed| Juno2[๐ŸŽต Try Juno.co.uk as fallback] Juno2 -->|Success| Done Juno2 -->|Failed| YouTube2[๐ŸŽฌ Try YouTube as fallback] YouTube2 -->|Success| Done YouTube2 -->|Failed| Failed style Done fill:#90EE90 style Failed fill:#FFB6C1 style Juno fill:#E6E6FA style Juno2 fill:#E6E6FA style YouTube fill:#FFE5B4 style YouTube2 fill:#FFE5B4 ``` ## ๐Ÿ“š API Reference ### Simple API (Recommended) ```typescript // Download with optional folder and filename customization soulseekDownload( artist: string, title: string, folderName?: string, // Optional: subfolder in SOULSEEK_DOWNLOAD_DIR customFileName?: string // Optional: custom filename (without extension) ): Promise<string | null> // Clean up when done soulseekDisconnect(): Promise<void> ``` #### Parameters | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `artist` | string | โœ… | Artist name to search for | | `title` | string | โœ… | Track title to search for | | `folderName` | string | โŒ | Optional subfolder path. Supports nested paths like "Genre/Artist" | | `customFileName` | string | โŒ | Optional custom filename (extension added automatically) | #### Examples ```typescript // Basic usage await soulseekDownload('Daft Punk', 'One More Time'); // โ†’ "/downloads/Daft Punk - One More Time.mp3" // With folder await soulseekDownload('Daft Punk', 'One More Time', 'Electronic'); // โ†’ "/downloads/Electronic/Daft Punk - One More Time.mp3" // With custom filename await soulseekDownload('Daft Punk', 'One More Time', null, 'daft_punk_01'); // โ†’ "/downloads/daft_punk_01.mp3" // With both await soulseekDownload('Daft Punk', 'One More Time', 'Electronic/Daft_Punk', 'track_01'); // โ†’ "/downloads/Electronic/Daft_Punk/track_01.mp3" ``` ### Advanced API ```typescript import { SoulseekDownloader, DownloadConfig, SearchOptions, SoulseekSearchResult } from 'andrade-soulseek-downloader'; const downloader = new SoulseekDownloader({ maxAttempts: 10, downloadTimeout: 120000, preferSlotsAvailable: true, minSpeed: 100000, searchDelay: 5000, downloadDelay: 3000, maxConcurrent: 1, cooldownAfterError: 10000 }); // Manual control await downloader.connect(); const results = await downloader.search(options); const filePath = await downloader.download(result, artist, title); await downloader.disconnect(); ``` ## โš™๏ธ Configuration ### Environment Variables | Variable | Default | Description | |----------|---------|-------------| | `SOULSEEK_USER` | - | Your Soulseek username **(required)** | | `SOULSEEK_PASSWORD` | - | Your Soulseek password **(required)** | | `SOULSEEK_SHARED_MUSIC_DIR` | - | Path to your shared music folder **(required)** | | `SOULSEEK_DOWNLOAD_DIR` | - | Where to save downloads **(required)** | | `SOULSEEK_MIN_QUALITY_BITRATE` | `128` | Minimum acceptable bitrate (kbps) | | `SOULSEEK_MAX_QUALITY_BITRATE` | *(none)* | Maximum acceptable bitrate (kbps) - prevents large FLAC/WAV files | | `SOULSEEK_SEARCH_DELAY` | `5000` | Delay between searches (ms) | | `SOULSEEK_DOWNLOAD_DELAY` | `3000` | Delay between downloads (ms) | | `SOULSEEK_MAX_ATTEMPTS` | `10` | Maximum download attempts | | `SOULSEEK_DOWNLOAD_TIMEOUT` | `120000` | Download timeout (ms) | | `SOULSEEK_MAX_CONCURRENT` | `1` | Max concurrent operations **(keep at 1!)** | | `SOULSEEK_ERROR_COOLDOWN` | `10000` | Cooldown after errors (ms) | ### Programmatic Configuration ```typescript const config: DownloadConfig = { maxAttempts: 15, // Try up to 15 different sources downloadTimeout: 180000, // 3 minute timeout per download preferSlotsAvailable: true, // Prefer users with open slots minSpeed: 500000, // Minimum 500kb/s connection minQualityBitrate: 192, // Minimum quality maxQualityBitrate: 320, // Maximum quality (avoid FLAC/WAV) searchDelay: 3000, // 3 second delay between searches downloadDelay: 2000, // 2 second delay between downloads maxConcurrent: 1, // Always 1 to prevent bans cooldownAfterError: 15000 // 15 second cooldown after errors }; ``` ### Quality Control Examples ```typescript // Example 1: High quality only (FLAC/WAV allowed) process.env.SOULSEEK_MIN_QUALITY_BITRATE = '320'; // No max limit = allows FLAC/WAV downloads // Example 2: Avoid large files (cap at 320kbps) process.env.SOULSEEK_MIN_QUALITY_BITRATE = '256'; process.env.SOULSEEK_MAX_QUALITY_BITRATE = '320'; // Example 3: Specific quality only (exactly 320kbps) process.env.SOULSEEK_MIN_QUALITY_BITRATE = '320'; process.env.SOULSEEK_MAX_QUALITY_BITRATE = '320'; // Example 4: Mobile-friendly sizes (avoid large files) process.env.SOULSEEK_MIN_QUALITY_BITRATE = '128'; process.env.SOULSEEK_MAX_QUALITY_BITRATE = '256'; ``` ## ๐Ÿ’ก Examples ### Basic Usage ```typescript import { soulseekDownload, soulseekDisconnect } from 'andrade-soulseek-downloader'; // Basic download const file1 = await soulseekDownload('The Beatles', 'Hey Jude'); // Download to a specific folder const file2 = await soulseekDownload('Pink Floyd', 'Comfortably Numb', 'Classic Rock'); // Download with custom folder and filename const file3 = await soulseekDownload( 'Led Zeppelin', 'Stairway to Heaven', 'Rock/Led Zeppelin', // Creates nested folders 'stairway_to_heaven_remastered' // Custom filename (extension added automatically) ); // Clean up when done await soulseekDisconnect(); ``` ### Batch Downloads ```typescript import { soulseekDownload, soulseekDisconnect } from 'andrade-soulseek-downloader'; const tracks = [ { artist: 'The Beatles', title: 'Hey Jude' }, { artist: 'Pink Floyd', title: 'Comfortably Numb' }, { artist: 'Led Zeppelin', title: 'Stairway to Heaven' } ]; // Download to organized folders for (const track of tracks) { console.log(`Downloading ${track.artist} - ${track.title}...`); // Downloads each track to an artist-specific folder const filePath = await soulseekDownload( track.artist, track.title, track.artist.replace(/[^a-zA-Z0-9]/g, '_') // Artist folder ); if (filePath) { console.log(`โœ… Downloaded: ${filePath}`); } else { console.log(`โŒ Failed: ${track.artist} - ${track.title}`); } } await soulseekDisconnect(); ``` ### Advanced Configuration ```typescript import { SoulseekDownloader, DownloadConfig, SearchOptions } from 'andrade-soulseek-downloader'; const config: DownloadConfig = { maxAttempts: 20, downloadTimeout: 300000, // 5 minutes preferSlotsAvailable: true, minSpeed: 1000000, // 1 MB/s minimum searchDelay: 2000, downloadDelay: 1000 }; const downloader = new SoulseekDownloader(config); try { await downloader.connect(); const searchOptions: SearchOptions = { artist: 'Daft Punk', title: 'One More Time', minBitrate: 320, // Only high quality timeout: 60000, maxResults: 50, strictMatching: true }; const results = await downloader.search(searchOptions); console.log(`Found ${results.length} high-quality results`); if (results.length > 0) { const bestResult = results[0]; // Already sorted by quality // Download with custom folder and filename const filePath = await downloader.download( bestResult, 'Daft Punk', 'One More Time', 'Electronic/Daft_Punk', // Custom folder 'one_more_time_hq' // Custom filename ); console.log(`Downloaded: ${filePath}`); } } finally { await downloader.disconnect(); } ``` ### Custom Folders and Filenames ```typescript import { soulseekDownload } from 'andrade-soulseek-downloader'; // Organize downloads by genre const genres = { 'The Beatles': 'Rock/Classic', 'Miles Davis': 'Jazz', 'Daft Punk': 'Electronic/House' }; for (const [artist, genre] of Object.entries(genres)) { // Downloads to genre-specific folders await soulseekDownload( artist, 'Greatest Hits', genre, // Creates folder structure `${artist.toLowerCase().replace(/ /g, '_')}_greatest_hits` // Custom filename ); } // Create a compilation folder const compilationTracks = [ { artist: 'Queen', title: 'Bohemian Rhapsody', filename: '01_queen_bohemian_rhapsody' }, { artist: 'Led Zeppelin', title: 'Stairway to Heaven', filename: '02_led_zeppelin_stairway' }, { artist: 'Pink Floyd', title: 'Comfortably Numb', filename: '03_pink_floyd_comfortably_numb' } ]; for (const track of compilationTracks) { await soulseekDownload( track.artist, track.title, 'Compilations/Rock_Classics_2024', // Nested folder structure track.filename // Sequential naming for playlist ); } ``` ### CLI Usage ```bash # Global installation pnpm install -g andrade-soulseek-downloader # Command line usage (basic - no custom params via CLI yet) soulseek-download "The Beatles" "Hey Jude" # Or run directly with pnpm pnpm start "The Beatles" "Hey Jude" ``` ### Error Handling ```javascript try { const filePath = await soulseekDownload('Artist', 'Title'); if (!filePath) { // Download failed but no error thrown console.log('Could not find or download the track'); } } catch (error) { // Connection or configuration error console.error('Fatal error:', error.message); } ``` ### Custom Download Handler ```javascript async function downloadWithMetadata(artist, title) { console.log(`๐ŸŽต Searching for ${artist} - ${title}`); const startTime = Date.now(); const filePath = await soulseekDownload(artist, title); if (filePath) { const duration = (Date.now() - startTime) / 1000; const stats = require('fs').statSync(filePath); return { success: true, path: filePath, size: stats.size, duration: duration, timestamp: new Date().toISOString() }; } return { success: false, artist, title }; } ``` ## ๐Ÿ“ File Organization Organize your music library with custom folders and filenames: ### Folder Structure Examples ```typescript // Organize by genre await soulseekDownload('Pink Floyd', 'Comfortably Numb', 'Rock/Progressive'); await soulseekDownload('Miles Davis', 'So What', 'Jazz/Modal'); await soulseekDownload('Daft Punk', 'One More Time', 'Electronic/House'); // Organize by year await soulseekDownload('The Beatles', 'Hey Jude', '1960s/1968'); await soulseekDownload('Nirvana', 'Smells Like Teen Spirit', '1990s/1991'); // Create compilations const tracks = ['track1', 'track2', 'track3']; for (let i = 0; i < tracks.length; i++) { await soulseekDownload( artist[i], tracks[i], 'Compilations/Summer_2024', `${String(i+1).padStart(2, '0')}_${artist[i].toLowerCase()}` ); } ``` ### Custom Filename Patterns ```typescript // Sequential numbering for playlists await soulseekDownload('Artist', 'Title', 'Playlist', '01_intro'); await soulseekDownload('Artist', 'Title', 'Playlist', '02_main_theme'); await soulseekDownload('Artist', 'Title', 'Playlist', '03_outro'); // Include metadata in filename const safeArtist = artist.replace(/[^a-zA-Z0-9]/g, '_'); const safeTitle = title.replace(/[^a-zA-Z0-9]/g, '_'); await soulseekDownload( artist, title, 'Library', `${safeArtist}-${safeTitle}-${bitrate}kbps` ); // Date-based organization const date = new Date().toISOString().split('T')[0]; await soulseekDownload(artist, title, date, `${date}_${safeTitle}`); ``` ### Automatic Features - **Directory Creation**: Folders are created automatically if they don't exist - **Nested Paths**: Supports multi-level folder structures (e.g., "Genre/Artist/Album") - **Extension Handling**: File extensions are added automatically based on source - **Special Characters**: Handles special characters safely in paths - **YouTube Support**: Custom folders and filenames work with YouTube fallback too - **Juno Support**: Custom folders and filenames work with Juno.co.uk fallback too ## ๐ŸŽต Juno.co.uk Fallback The library includes **Juno.co.uk snippet fallback** as an intermediate option between Soulseek and YouTube: ### How Juno Fallback Works 1. **Primary Search**: First searches Soulseek network for the track 2. **Juno Fallback**: If no results on Soulseek, automatically searches Juno.co.uk for audio snippets 3. **Web Scraping**: Uses Puppeteer (headless Chrome) to scrape Juno.co.uk search results 4. **Snippet Download**: Downloads the first available MP3 snippet preview from search results 5. **YouTube Fallback**: If Juno fails, falls back to YouTube as the final option 6. **Same Location**: Saves to the same download directory as Soulseek files ### Juno Features - ๐Ÿ” **Intelligent Search**: Searches Juno.co.uk with artist + title query - ๐ŸŽถ **Audio Snippets**: Downloads preview snippets (usually 1-2 minutes) - ๐Ÿค– **Automated Scraping**: Uses Puppeteer for reliable web scraping - ๐Ÿ“Š **Progress Tracking**: Shows search and download progress - ๐ŸŽฏ **First Match**: Takes the first available snippet from search results - ๐Ÿ“ **Organized Storage**: Saves with `_juno.mp3` suffix for easy identification - ๐Ÿ”„ **Fallback Chain**: Soulseek โ†’ Juno โ†’ YouTube ### When Juno is Used - **No Soulseek Results**: When search returns no files on Soulseek - **All Downloads Failed**: After all Soulseek download attempts fail - **Before YouTube**: Tries Juno before falling back to YouTube - **Seamless Integration**: Works automatically without configuration ### Example Output ```bash โŒ No results found on Soulseek network ๐ŸŽต Juno.co.uk Fallback ๐ŸŽถ Attempting to download snippet from Juno.co.uk... ๐Ÿ” Searching Juno for: "Artist - Title" [Juno] ๐Ÿ” Searching: https://www.juno.co.uk/search/?q[all][]=Artist+Title&show_out_of_stock=1 [Juno] โœ… Found snippet: https://www.juno.co.uk/MP3/... [Juno] โฌ‡๏ธ Downloading from: https://www.juno.co.uk/MP3/... [Juno] ๐Ÿ“ฅ Progress: 100% [Juno] โœ… Download completed [Juno] โœ… Downloaded successfully! [Juno] ๐Ÿ“ Saved to: /downloads/Artist - Title_juno.mp3 โœ… Successfully downloaded snippet from Juno! ๐Ÿ“ File saved to: /downloads/Artist - Title_juno.mp3 ๐Ÿ’ก Note: This is a preview snippet from Juno.co.uk ``` **Note:** Juno downloads are automatically suffixed with `_juno` before the file extension to distinguish them from Soulseek and YouTube downloads. ### Requirements Juno fallback uses **Puppeteer** which is automatically installed as a dependency. Puppeteer downloads a headless Chrome browser on first install (~170MB). ### Filename Suffixes - **Soulseek (high quality)**: `Artist - Title.mp3` - **Soulseek (low quality)**: `Artist - Title_128kbps.mp3` - **Juno snippet**: `Artist - Title_juno.mp3` - **YouTube fallback**: `Artist - Title_youtube.mp3` This makes it easy to identify the source and quality of each file in your library. ## ๐ŸŽฌ YouTube Fallback The library includes **robust YouTube fallback** with advanced error handling when tracks are not found on Soulseek: ### How YouTube Fallback Works 1. **Primary Search**: First searches Soulseek network for the track 2. **Automatic Fallback**: If no results on Soulseek, automatically searches YouTube 3. **Multiple Attempts**: Tries up to 3 alternative search results if the first fails 4. **Retry Logic**: Each video gets 3 retry attempts with exponential backoff 5. **Dual Methods**: Uses yt-dlp if available, falls back to ytdl-core 6. **Quality Download**: Downloads audio in 192kbps MP3 format 7. **Smart Conversion**: Automatically converts to MP3 using ffmpeg (when needed) 8. **Same Location**: Saves to the same download directory as Soulseek files ### YouTube Features - ๐Ÿ” **Intelligent Search**: Uses relevance scoring to find the best match - ๐ŸŽต **Audio Only**: Downloads only audio stream for efficiency - ๐Ÿ”„ **Auto Conversion**: Converts to MP3 format automatically - ๐Ÿ“Š **Progress Tracking**: Shows download and conversion progress - ๐ŸŽฏ **Best Match**: Selects most relevant video based on title, duration, and views - ๐Ÿ” **Retry Logic**: 3 attempts per video with exponential backoff (2s, 4s, 6s delays) - ๐ŸŽฌ **Fallback Results**: Tries up to 3 different videos from search results - ๐Ÿš€ **yt-dlp Support**: Uses yt-dlp when available for better reliability ### Required: Install and Update yt-dlp For YouTube downloads to work, you **must have yt-dlp installed and up-to-date**: ```bash # Install yt-dlp (if not already installed) pip install yt-dlp # IMPORTANT: Update to the latest version (required!) pip install --upgrade yt-dlp # Alternative: Homebrew (macOS) brew install yt-dlp brew upgrade yt-dlp # Verify version (should be 2025.9.26 or newer) yt-dlp --version ``` **โš ๏ธ CRITICAL: YouTube regularly updates anti-bot measures. An outdated yt-dlp will fail with 403 errors!** **Why yt-dlp is essential:** - โœ… Handles YouTube's latest anti-bot protections (as of January 2025) - โœ… Automatic fallback to working player clients (tvhtml5, web safari) - โœ… Regular updates to handle YouTube changes (update weekly!) - โœ… Built-in retry and fragment recovery - โœ… Much more reliable than ytdl-core The library automatically detects and uses yt-dlp if available. Without it, YouTube fallback **will not work**. ### When YouTube is Used - **No Soulseek Results**: When search returns no files on Soulseek - **All Downloads Failed**: After all Soulseek download attempts fail - **Seamless Integration**: Works automatically without configuration ### Example Output ```bash โš  No results found on Soulseek, trying YouTube as fallback... [YouTube] โœ“ yt-dlp found - will use for better reliability [YouTube] ๐Ÿ” Searching for: Artist - Title [YouTube] โœจ Found 10 results [YouTube] ๐Ÿ† Best match: "Artist - Title (Official Audio)" [YouTube] ๐Ÿ”— URL: https://www.youtube.com/watch?v=... [YouTube] ๐Ÿ“Š Relevance score: 85.3 | Duration: 3:45 | Views: 1,234,567 [YouTube] ๐ŸŽฏ Starting download process... [YouTube] ๐Ÿš€ Downloading with yt-dlp... [YouTube] โฌ‡๏ธ Progress: 100% [YouTube] โœ… Download completed [YouTube] โœ… Downloaded successfully! [YouTube] ๐Ÿ“ Saved to: /downloads/Artist - Title_youtube.mp3 ``` **Note:** YouTube downloads are automatically suffixed with `_youtube` before the file extension to distinguish them from Soulseek downloads. ### Low Quality Fallback If no files are found matching your minimum quality requirement (`SOULSEEK_MIN_QUALITY_BITRATE`), the library will automatically search for lower quality files as a last resort before falling back to YouTube: ```bash # User has SOULSEEK_MIN_QUALITY_BITRATE=320 set โš ๏ธ No results found at 320+ kbps, searching for lower quality files... ๐Ÿ“Š Found files with bitrates: 128 kbps โš ๏ธ Low quality file (128kbps < 320kbps requested) - adding bitrate suffix โœ… Downloaded: Artist - Title_128kbps.mp3 ``` **Filename Suffixes:** - High quality (meets minimum): `Artist - Title.mp3` - Low quality fallback: `Artist - Title_128kbps.mp3` (or _96kbps, _192kbps, etc.) - Juno snippet: `Artist - Title_juno.mp3` - YouTube fallback: `Artist - Title_youtube.mp3` This makes it easy to identify which files need to be replaced with higher quality versions later. ### Handling YouTube Errors If you see frequent 403 errors: ```bash [YouTube] โŒ Stream error: Status code: 403 [YouTube] ๐Ÿ”„ Retry attempt 2/3... [YouTube] ๐Ÿ”„ Trying alternative result #2: "Artist - Title (Lyric Video)" ``` **Solutions:** 1. **Update yt-dlp** (most common fix): `pip install --upgrade yt-dlp` 2. **Install yt-dlp** (if not installed): `pip install yt-dlp` 3. **Verify version**: `yt-dlp --version` (should be 2025.9.26 or newer) 4. **Check region restrictions**: Some videos may be blocked in your region 5. **Try again later**: YouTube rate limiting may be temporary **Most 403 errors are fixed by updating yt-dlp to the latest version!** ## ๐ŸŽจ Quality Selection Algorithm The library uses a sophisticated scoring system to select the best file: ```mermaid pie title Quality Score Components (100 points total) "Bitrate (50pts)" : 50 "Slot Availability (25pts)" : 25 "Connection Speed (15pts)" : 15 "Filename Match (10pts)" : 10 ``` ### Quality Score Calculation ```javascript Quality Score = (Bitrate/320 ร— 50) + (Slots ร— 25) + (Speed/5MB ร— 15) + (Match ร— 10) ``` ### Priority Order 1. **Highest Bitrate First** - FLAC/WAV โ†’ 320kbps โ†’ 256kbps โ†’ 192kbps โ†’ 128kbps 2. **Within each bitrate:** - Users with available slots - Faster connection speeds - Better filename matches ## ๐Ÿ“Š Architecture This project demonstrates **professional software architecture** patterns: ```mermaid graph TB subgraph "Presentation Layer" CLI[CLI Handler] API[API Handler] end subgraph "Application Layer" UC[Use Cases] DTO[DTOs] end subgraph "Domain Layer" E[Entities] VO[Value Objects] DS[Domain Services] R[Repository Interfaces] end subgraph "Infrastructure Layer" REPO[Soulseek Repository] LOG[Console Logger] RL[Rate Limiter] DI[DI Container] end CLI --> UC API --> UC UC --> DS UC --> R DS --> E DS --> VO R --> REPO UC --> LOG UC --> RL style CLI fill:#e3f2fd style API fill:#e3f2fd style UC fill:#e8f5e8 style E fill:#fff3e0 style VO fill:#fff3e0 style DS fill:#fff3e0 style REPO fill:#fce4ec ``` ### Directory Structure ``` src/ โ”œโ”€โ”€ presentation/ # User interfaces โ”‚ โ”œโ”€โ”€ api/ # HTTP/Function API โ”‚ โ””โ”€โ”€ cli/ # Command line interface โ”œโ”€โ”€ application/ # Use cases & orchestration โ”‚ โ”œโ”€โ”€ use-cases/ # Business workflows โ”‚ โ””โ”€โ”€ dto/ # Data transfer objects โ”œโ”€โ”€ domain/ # Core business logic โ”‚ โ”œโ”€โ”€ entities/ # Business objects โ”‚ โ”œโ”€โ”€ value-objects/ # Immutable values โ”‚ โ”œโ”€โ”€ services/ # Domain services โ”‚ โ””โ”€โ”€ repositories/ # Repository interfaces (ports) โ”œโ”€โ”€ infrastructure/ # External concerns โ”‚ โ”œโ”€โ”€ repositories/ # Repository implementations (adapters) โ”‚ โ”œโ”€โ”€ services/ # External services โ”‚ โ””โ”€โ”€ container/ # Dependency injection โ”œโ”€โ”€ shared/ # Shared utilities โ”‚ โ”œโ”€โ”€ interfaces/ # Common interfaces โ”‚ โ””โ”€โ”€ types/ # Type definitions โ””โ”€โ”€ core/ # Core components ``` ### Benefits of This Architecture - ๐Ÿงช **Highly Testable**: Easy to mock and test each layer - ๐Ÿ”ง **Maintainable**: Clear separation of concerns - ๐Ÿ”„ **Flexible**: Easy to swap implementations - ๐Ÿ“ˆ **Scalable**: Can grow with your needs - ๐Ÿ›ก๏ธ **Robust**: Handles errors gracefully - ๐Ÿ“ **Self-Documenting**: Clear intent and structure ## ๐Ÿงช Testing The project includes **comprehensive test coverage**: ```bash # Run all tests pnpm test # Run tests with coverage pnpm test:coverage # Run specific test suites pnpm test:unit pnpm test:integration # Watch mode for development pnpm test:watch ``` ### Test Statistics - **49 Tests** across all layers - **4 Test Suites** covering domain, application, and infrastructure - **High Coverage** on critical business logic - **Fast Execution** (< 1 second) ### Test Architecture - **Unit Tests**: Domain entities, value objects, services - **Integration Tests**: Use cases with mocked dependencies - **Mocking**: Comprehensive mocks for external dependencies - **Test Utilities**: Shared test factories and helpers ## ๐Ÿ“ TypeScript Support ### Built-in Type Definitions Full TypeScript support is **included by default**: ```typescript import { soulseekDownload, // Function SoulseekDownloader, // Class DownloadConfig, // Interface SearchOptions, // Interface SoulseekSearchResult, // Interface RateLimiter // Class } from 'andrade-soulseek-downloader'; // All types are automatically available const config: DownloadConfig = { maxAttempts: 10, downloadTimeout: 120000 // TypeScript will provide IntelliSense here }; ``` ### Type Safety Features - **Comprehensive Interfaces**: All public APIs are fully typed - **Generic Support**: Type-safe generic functions where applicable - **Strict Null Checks**: Proper handling of nullable values - **IntelliSense Support**: Full autocomplete in supported editors - **Compile-time Safety**: Catch errors before runtime ### Import Options ```typescript // Barrel imports (recommended) import { Track, Bitrate } from 'andrade-soulseek-downloader/domain'; import { DownloadTrackUseCase } from 'andrade-soulseek-downloader/application'; // Specific imports import { soulseekDownload } from 'andrade-soulseek-downloader/presentation/api'; import { SoulseekDownloader } from 'andrade-soulseek-downloader/core'; // Root imports (simple) import { soulseekDownload, SoulseekDownloader } from 'andrade-soulseek-downloader'; ``` ## ๐Ÿ”ง Advanced Usage ### Custom Rate Limiting ```typescript import { RateLimiter, RateLimitConfig } from 'andrade-soulseek-downloader'; const customConfig: RateLimitConfig = { searchDelay: 2000, // Faster searches (be careful!) downloadDelay: 4000, // Slower downloads (safer) maxConcurrent: 1, // Always 1 for safety cooldownAfterError: 20000 // Longer cooldown }; const rateLimiter = RateLimiter.getInstance(customConfig); // Use with your own functions const results = await rateLimiter.executeSearch(async () => { return await customSearchFunction(); }); ``` ### Extending the Domain ```typescript import { Track, TrackSelectionService } from 'andrade-soulseek-downloader/domain'; class CustomTrackSelectionService extends TrackSelectionService { selectBestTracks(tracks: Track[], maxPerBitrate: number = 5): Track[] { // Your custom selection logic const filtered = tracks.filter(track => track.getBitrate().getValue() >= 256 && track.hasAvailableSlots() ); return super.selectBestTracks(filtered, maxPerBitrate); } } ``` ### Custom Logging ```typescript import { ILogger } from 'andrade-soulseek-downloader/shared'; class CustomLogger implements ILogger { info(message: string): void { // Send to your logging service console.log(`[INFO] ${new Date().toISOString()} ${message}`); } success(message: string): void { // Custom success handling console.log(`[SUCCESS] ${message}`); } // ... implement other methods } // Use with dependency injection container.bind<ILogger>('ILogger').to(CustomLogger); ``` ## โ“ FAQ ### **Q: Is this safe to use? Will I get banned?** A: Yes, it's designed with safety first. The built-in rate limiting prevents bans by enforcing conservative delays between operations. ### **Q: What audio quality can I expect?** A: The library automatically finds the highest quality available on Soulseek, preferring lossless formats (FLAC) when possible, then falling back to 320kbps, 256kbps, etc. If the track is not found on Soulseek, it will download from YouTube at 192kbps MP3. ### **Q: Can I use this in production?** A: Yes! The architecture is enterprise-grade with proper error handling, logging, and testing. However, always respect Soulseek's terms of service. ### **Q: Does it work with TypeScript?** A: Absolutely! Full TypeScript support is built-in with comprehensive type definitions. ### **Q: Can I customize the download behavior?** A: Yes, the architecture is designed for extensibility. You can inject custom services, modify selection algorithms, or add your own retry logic. ### **Q: Does it download from YouTube automatically?** A: Yes! If a track is not found on Soulseek or all download attempts fail, the library automatically tries to download from YouTube as a fallback option. ### **Q: How do I report issues?** A: Please open an issue on GitHub with detailed information about your problem, including logs and environment details. ## ๐Ÿ› Troubleshooting ### Common Issues #### **Connection Problems** ```bash Error: Failed to connect to Soulseek ``` **Solution**: Check your username/password and network connection. #### **No Results Found** ```bash No tracks found ``` **Solutions**: - Try broader search terms - Lower the minimum bitrate requirement - Check if the artist/track exists on Soulseek #### **Download Timeouts** ```bash Download timeout for user X ``` **Solutions**: - Increase `SOULSEEK_DOWNLOAD_TIMEOUT` - The library will automatically try other sources #### **Rate Limit Warnings** ```bash Warning: Multiple SoulseekDownloader instances detected! ``` **Solution**: Use only one instance of SoulseekDownloader, or use the simple API functions. ### Debug Mode Enable debug logging: ```bash DEBUG=true pnpm start "Artist" "Title" ``` Or programmatically: ```typescript process.env.DEBUG = 'true'; import { soulseekDownload } from 'andrade-soulseek-downloader'; ``` ## ๐Ÿ“ Changelog For a detailed list of changes, improvements, and bug fixes, see [CHANGELOG.md](./CHANGELOG.md). **Recent versions:** - **v1.0.27**: Smart timeout logic for large files - **v1.0.26**: Interactive mode with arrow keys, filter locked files - **v1.0.25**: Juno.co.uk fallback, error handling improvements - **v1.0.24**: Low quality fallback with bitrate suffix - **v1.0.23**: YouTube filename suffix - **v1.0.22**: yt-dlp support, YouTube improvements --- ## ๐Ÿค Contributing We welcome contributions! Here's how to get started: ### Development Setup ```bash # Clone the repository git clone https://github.com/andrade/soulseek-downloader.git cd soulseek-downloader # Install dependencies pnpm install # Run tests pnpm test # Build the project pnpm build # Run in development pnpm dev ``` ### Code Standards - **TypeScript**: All code must be in TypeScript - **Architecture**: Follow hexagonal architecture patterns - **Testing**: Maintain high test coverage - **Formatting**: Code is automatically formatted - **Conventions**: Use kebab-case for files, PascalCase for classes ### Submitting Changes 1. Fork the repository 2. Create a feature branch: `git checkout -b feature/amazing-feature` 3. Make your changes following the architecture patterns 4. Add tests for new functionality 5. Ensure all tests pass: `pnpm test` 6. Build successfully: `pnpm build` 7. Commit your changes: `git commit -m 'Add amazing feature'` 8. Push to the branch: `git push origin feature/amazing-feature` 9. Open a Pull Request ### Architecture Guidelines When contributing, please: - Keep domain logic pure (no external dependencies) - Use dependency injection for external concerns - Follow the single responsibility principle - Add comprehensive tests for new features - Update documentation for API changes --- ## ๐Ÿ“ Changelog ### v1.0.14 (Latest) - ๐ŸŽฏ Added optional `folderName` parameter for organizing downloads into custom folders - ๐Ÿ“ Added optional `customFileName` parameter for custom file naming - ๐Ÿ“ Support for nested folder structures (e.g., "Genre/Artist/Album") - ๐ŸŽฌ Custom parameters work with YouTube fallback - ๐Ÿงช Added comprehensive tests for new features ### v1.0.13 - ๐Ÿ”ง Fixed dependency issue: moved `inversify` and `reflect-metadata` to dependencies - ๐Ÿ“Š Enhanced logging for Soulseek failures and YouTube fallback - ๐ŸŽฌ Clear indication when YouTube is used as fallback - ๐Ÿ“ Improved error messages and user guidance ### v1.0.12 - ๐ŸŽฌ Added automatic YouTube fallback when Soulseek fails - ๐Ÿ” Improved search algorithms - ๐Ÿ›ก๏ธ Enhanced rate limiting --- ## ๐Ÿ“„ License MIT ยฉ [andrade](https://github.com/andrade) --- <div align="center"> **โญ Star this repo if you found it helpful!** [Report Bug](https://github.com/andrade/soulseek-downloader/issues) โ€ข [Request Feature](https://github.com/andrade/soulseek-downloader/issues) โ€ข [Contribute](https://github.com/andrade/soulseek-downloader/pulls) </div>