# Jarri Spooty Self-hosted Spotify playlist and track downloader built with NestJS + Angular. Jarri Spooty does not download audio from Spotify itself. It retrieves metadata from Spotify and locates matching audio on YouTube. ![Jarri Spooty Demo](docs/images/jarri-spooty-demo.png) Deterministic Spotify metadata ingestion with duration-aware YouTube candidate scoring, paced queue execution, hardened Docker deployment, and local archival workflows. This hardened branch focuses on: - Large playlist support (>100 tracks) - Spotify OAuth login flow - Better YouTube pacing and throttling resistance - Improved Docker deployment - Safer credential handling - More resilient cover-art embedding - Better queue stability - Deterministic YouTube fallback handling - Explicit yt-dlp CLI execution - Automatic failed-candidate rejection - Improved age-gated video handling - Persistent SQLite state across container restarts - Deterministic Docker config persistence - Improved operational observability - Reduced unused dependency surface - Hardened filename, cover-art, subprocess, and websocket boundaries --- # Features - Download Spotify playlists - Download individual Spotify tracks - Playlist auto-subscription support - Automatic YouTube matching - MP3 tagging and embedded cover art - Docker deployment - Queue-based download system - Spotify OAuth integration - Large playlist pagination support - Download pacing controls - YouTube cookie support - Automatic YouTube retry/fallback handling - Failed YouTube candidate rejection memory - Deterministic yt-dlp error classification - Hardened cover-art validation and embedding - Explicit client-facing websocket payload shaping --- # Important Notice Use this software responsibly. Only download music you legally own or are permitted to access. The maintainers are not responsible for misuse. --- # Supported URLs - Spotify playlists - Spotify tracks Example: https://open.spotify.com/playlist/... https://open.spotify.com/track/... --- # Quick Start (Recommended) ## 1. Create Spotify Developer App Go to: https://developer.spotify.com/dashboard Create an application. Add this Redirect URI: http://127.0.0.1:3000/api/spotify/callback Copy: - Client ID - Client Secret --- ## 2. Store Credentials Outside Repository Create a secure env file: ~~~bash sudo mkdir -p /etc/tokens sudo tee /etc/tokens/spotify.env > /dev/null <<'EOT' SPOTIFY_CLIENT_ID=your_client_id SPOTIFY_CLIENT_SECRET=your_client_secret EOT sudo chown root:$USER /etc/tokens/spotify.env sudo chmod 640 /etc/tokens/spotify.env ~~~ Never commit this file. --- ## 3. Export YouTube Cookies (Recommended) YouTube increasingly rate-limits or age-gates anonymous downloads. Export a Netscape-format `cookies.txt` from a logged-in browser session. Recommended storage: ~~~bash sudo cp cookies.txt /etc/tokens/youtube.cookies.txt sudo chown root:$USER /etc/tokens/youtube.cookies.txt sudo chmod 640 /etc/tokens/youtube.cookies.txt ~~~ --- # Docker Run ~~~bash docker run --rm -p 3000:3000 \ --env-file /etc/tokens/spotify.env \ -e SPOTIFY_REDIRECT_URI='http://127.0.0.1:3000/api/spotify/callback' \ -e AUTH_ENABLED=true \ -e SPOOTY_AUTH_TOKEN=change_this_token \ -e YT_SEARCH_DELAY_MS=7000 \ -e YT_DOWNLOADS_PER_MINUTE=6 \ -e YT_COOKIES_FILE=/spooty/config/youtube.cookies.txt \ -v "$PWD/downloads:/spooty/backend/downloads" \ -v "$PWD/spooty-config:/spooty/backend/config" \ -v "/etc/tokens/youtube.cookies.txt:/spooty/config/youtube.cookies.txt:ro" \ jarri-spooty:local ~~~ Open: http://127.0.0.1:3000/?token=change_this_token Then: 1. Click "Connect Spotify" 2. Login to Spotify 3. Approve access 4. Paste playlist URL 5. Download --- # Deterministic YouTube Fallback Handling The hardened branch now uses direct `yt-dlp` CLI execution rather than relying entirely on wrapper abstractions. This provides: - Explicit stderr visibility - Better Docker compatibility - Deterministic retry handling - Automatic failed-candidate rejection - Better age-gated video handling - Improved operational observability If a YouTube candidate fails: 1. The failed URL is recorded 2. The candidate is rejected 3. A new YouTube search is performed 4. The next-best valid candidate is attempted automatically This prevents infinite retry loops against dead or restricted videos. --- # Queue Pacing Aggressive YouTube access can trigger: - HTTP 302 loops - CAPTCHA - temporary throttling - incomplete downloads Recommended safe pacing: ~~~bash -e YT_SEARCH_DELAY_MS=7000 -e YT_DOWNLOADS_PER_MINUTE=6 ~~~ The hardened branch also includes additional internal pacing and retry coordination to reduce: - repeated failed candidate loops - aggressive retry bursts - queue collisions - YouTube anti-bot triggers --- # Security Notes Never commit: - Spotify secrets - OAuth tokens - cookies.txt - downloaded music - local databases - spooty-config/ Recommended `.gitignore` additions: ~~~gitignore downloads/ config/ spooty-config/ *.sqlite cookies.txt .env .env.local ~~~ --- # License MIT