Deploying Black Candy
Black Candy is a self-hosted music streaming server that transforms your personal music library into a private, personal music center. Built with Ruby on Rails and modern web technologies, Black Candy enables you to stream your music collection across devices without reliance on commercial streaming services. Whether you’re building a personal music platform, managing a family music library, or creating a dedicated audio streaming system, Black Candy provides a complete, feature-rich music streaming solution under your complete control.
Why Black Candy?
Black Candy stands out as the premier choice for self-hosted music streaming with exceptional features and user experience:
- Complete Privacy: Your music stays on your servers, no tracking or data collection
- Self-Hosted: Full control over music library and platform infrastructure
- No Subscription Required: One-time setup, no recurring streaming fees
- Web-Based Interface: Modern, responsive web interface accessible from any browser
- Mobile Apps: Native iOS and Android apps for seamless mobile streaming
- Music Library Management: Organize, tag, and manage your complete music collection
- Playlist Creation: Create and manage custom playlists from your library
- Multi-User Support: Create accounts for family members and friends
- User Roles and Permissions: Admin and user-level access control
- Metadata Management: Auto-fetch artist and album artwork via Discogs integration
- Audio Format Support: Support for MP3, FLAC, WAV, OGG, AAC, and more
- High-Quality Streaming: Stream your music at the quality you have stored
- Smart Collections: Album, artist, and genre browsing with search functionality
- Recently Played Tracking: Keep track of recently played songs and albums
- Cross-Device Support: Sync playback across devices and resume where you left off
- No Bandwidth Concerns: Stream unlimited amounts without data caps
- Open Source: MIT licensed with transparent, auditable code
- Lightweight Installation: Minimal resource requirements for small to medium libraries
- FFmpeg Integration: Automatic audio format transcoding and processing
Black Candy is ideal for music enthusiasts seeking privacy, families wanting shared music access, audiophiles preferring lossless formats, and anyone desiring complete control over their music streaming platform. With persistent storage on Klutch.sh, your music library is permanently secure and accessible.
Prerequisites
Before deploying Black Candy, ensure you have:
- A Klutch.sh account
- A GitHub repository with your Black Candy deployment configuration
- Basic familiarity with Docker and Git
- A custom domain name (recommended for production streaming)
- Sufficient storage for your music library
- Music files in supported formats (MP3, FLAC, WAV, OGG, AAC, etc.)
- Optional: Discogs account for album artwork integration
Important Considerations
Deploying Black Candy
Create a New Project
Log in to your Klutch.sh dashboard and create a new project for your Black Candy deployment.
Prepare Your Repository
Create a GitHub repository with the following structure for your Black Candy deployment:
black-candy-deploy/├─ Dockerfile├─ .env.example├─ docker-entrypoint.sh├─ .gitignore└─ README.mdHere’s a Dockerfile for Black Candy:
FROM ghcr.io/blackcandy-org/blackcandy:latest# Install additional utilitiesRUN apt-get update && apt-get install -y \curl \ca-certificates \&& rm -rf /var/lib/apt/lists/*# Set working directoryWORKDIR /app# Create necessary directoriesRUN mkdir -p /app/storage \/app/media \/app/logs && \chmod -R 755 /app# Copy entrypoint scriptCOPY docker-entrypoint.sh /RUN chmod +x /docker-entrypoint.sh# Expose portEXPOSE 3000# Health checkHEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=3 \CMD curl -f http://localhost:3000 || exit 1# Run entrypointENTRYPOINT ["/docker-entrypoint.sh"]CMD ["start"]Create a
docker-entrypoint.shfile:#!/bin/bashset -e# Create necessary directoriesmkdir -p /app/storage/uploads \/app/storage/cache \/app/media \/app/logs# Set proper permissionschmod -R 755 /app/storagechmod -R 755 /app/media# Initialize database if needed (SQLite)if [ ! -f /app/storage/db.sqlite3 ]; thenecho "Initializing database..."if [ "$DB_ADAPTER" = "postgresql" ]; then# PostgreSQL will be handled by the applicationecho "PostgreSQL configured"elseecho "SQLite configured"fifi# Check database connection if using PostgreSQLif [ "$DB_ADAPTER" = "postgresql" ] && [ -n "$DB_URL" ]; thenecho "Waiting for database connection..."MAX_ATTEMPTS=30ATTEMPTS=0while [ $ATTEMPTS -lt $MAX_ATTEMPTS ]; doif nc -z $(echo $DB_URL | cut -d@ -f2 | cut -d: -f1) \$(echo $DB_URL | cut -d: -f4 | cut -d/ -f1) 2>/dev/null || \pg_isready -h $(echo $DB_URL | cut -d@ -f2 | cut -d: -f1) \-p $(echo $DB_URL | cut -d: -f4 | cut -d/ -f1) 2>/dev/null; thenecho "Database is ready!"breakfiATTEMPTS=$((ATTEMPTS + 1))sleep 1donefiif [ "$1" = "start" ]; then# Start the applicationexec bundle exec rails server -b 0.0.0.0 -p 3000elseexec "$@"fiCreate a
.env.examplefile:Terminal window # Black Candy ConfigurationRAILS_ENV=productionRACK_ENV=productionPORT=3000# Database Configuration (choose one)DB_ADAPTER=sqlite# For PostgreSQL, uncomment below and update with your connection# DB_ADAPTER=postgresql# DB_URL=postgresql://username:password@host:5432/blackcandy# CABLE_DB_URL=postgresql://username:password@host:5432/blackcandy_cable# QUEUE_DB_URL=postgresql://username:password@host:5432/blackcandy_queue# CACHE_DB_URL=postgresql://username:password@host:5432/blackcandy_cache# SecuritySECRET_KEY_BASE=generate-your-secure-key-hereFORCE_SSL=true# Media ConfigurationMEDIA_PATH=/app/mediaMAX_UPLOAD_SIZE=536870912# User ConfigurationDEFAULT_ADMIN_EMAIL=admin@yourdomain.comDEFAULT_ADMIN_PASSWORD=secure_password_here# API ConfigurationAPI_ENABLED=true# LoggingLOG_LEVEL=infoRAILS_LOG_TO_STDOUT=true# Discogs Integration (Optional)DISCOGS_API_TOKEN=your-discogs-token-hereENABLE_DISCOGS_LOOKUP=true# Demo ModeDEMO_MODE=false# FeaturesENABLE_USER_REGISTRATION=trueENABLE_PLAYLIST_SHARING=trueENABLE_SCROBBLING=falseCommit and push to your GitHub repository:
Terminal window git initgit add .git commit -m "Initial Black Candy deployment"git remote add origin https://github.com/yourusername/black-candy-deploy.gitgit push -u origin mainCreate a New App
In the Klutch.sh dashboard:
- Click “Create New App”
- Select your GitHub repository containing the Dockerfile
- Choose the branch (typically
mainormaster) - Klutch.sh will automatically detect the Dockerfile in the root directory
Configure Environment Variables
Set up these essential environment variables in your Klutch.sh dashboard:
Variable Description Example RAILS_ENVRails environment productionPORTApplication port 3000SECRET_KEY_BASERails secret key (generate secure random) secure-random-stringDB_ADAPTERDatabase type (sqlite or postgresql) sqliteDB_URLPostgreSQL connection string (if using PostgreSQL) postgresql://user:pass@host/dbCABLE_DB_URLWebSocket database URL (PostgreSQL only) postgresql://user:pass@host/dbQUEUE_DB_URLBackground job database URL (PostgreSQL only) postgresql://user:pass@host/dbCACHE_DB_URLCache database URL (PostgreSQL only) postgresql://user:pass@host/dbMEDIA_PATHPath to media files /app/mediaDEFAULT_ADMIN_EMAILInitial admin email admin@yourdomain.comDEFAULT_ADMIN_PASSWORDInitial admin password secure_passwordFORCE_SSLForce HTTPS connections trueDISCOGS_API_TOKENDiscogs API token for artwork your-tokenENABLE_DISCOGS_LOOKUPEnable album artwork lookup trueLOG_LEVELLogging verbosity infoAPI_ENABLEDEnable API access trueConfigure Persistent Storage
Black Candy requires persistent storage for music files and application data. Add persistent volumes:
Mount Path Description Recommended Size /app/mediaMusic files and library 500GB+ (based on collection size) /app/storageDatabase, cache, and application data 50GB /app/logsApplication logs 20GB In the Klutch.sh dashboard:
- Navigate to your app settings
- Go to the “Volumes” section
- Click “Add Volume” for each mount path
- Set mount paths and sizes as specified above
- Size media volume according to your music library
Set Network Configuration
Configure your app’s network settings:
- Select traffic type: HTTP (Black Candy uses standard web ports)
- Recommended internal port: 3000 (as specified in Dockerfile)
- Klutch.sh will automatically handle HTTPS termination via reverse proxy
- Ensure ports 80 and 443 are accessible from your domain
Configure Custom Domain
Black Candy works best with a custom domain for professional music streaming:
- Navigate to your app’s “Domains” section in Klutch.sh
- Click “Add Custom Domain”
- Enter your domain (e.g.,
music.yourdomain.com) - Configure DNS with a CNAME record to point to your Klutch.sh app
- Klutch.sh will automatically provision SSL certificates
- Update your domain reference in configuration if needed
Deploy Your App
- Review all settings and environment variables
- Verify all persistent volumes are properly configured
- Click “Deploy”
- Klutch.sh will build the Docker image and start your Black Candy instance
- Wait for the deployment to complete (typically 5-10 minutes)
- Access your Black Candy instance at your configured domain
- Log in with admin credentials to configure your music library
Initial Setup and Configuration
After deployment completes, access your Black Candy instance to complete setup.
Accessing Black Candy
Navigate to your domain: https://music.yourdomain.com
Log in with the admin credentials you configured in environment variables.
Uploading Your Music Library
Configure Black Candy to recognize your music files:
- Log in to your Black Candy instance
- Navigate to “Settings” → “Library”
- Set media path:
/app/media(if not already set) - Black Candy will automatically scan for music files
- Supported formats: MP3, FLAC, WAV, OGG, AAC, M4A
- Wait for library scan to complete (may take several minutes for large libraries)
Adding Music Files
Upload or mount music files to your Black Candy instance:
- Via SFTP/SCP: Connect to persistent volume and transfer files
- Via Web Interface: Some implementations support direct upload
- Via Mounted Folder: Place files in
/app/mediaand rescan library - Organize by: Artist/Album folder structure recommended
- Example structure:
/app/media/├─ Artist Name/│ ├─ Album 1/│ │ ├─ 01 - Song.mp3│ │ ├─ 02 - Song.mp3│ ├─ Album 2/
Creating User Accounts
Set up accounts for family members and friends:
- Navigate to “Settings” → “Users”
- Click “Add New User”
- Enter email and password
- Set user role (Admin or User)
- Send login credentials to user
- User can change password on first login
Creating Playlists
Build custom playlists from your music library:
- Navigate to “Library” or “Playlists”
- Search for songs or browse by artist/album
- Create new playlist: “My Favorite Songs”
- Add songs by dragging or clicking add button
- Organize songs within playlist
- Share playlist with other users (if enabled)
Enabling Discogs Integration
Auto-fetch album artwork and metadata:
- Create a Discogs Developer Account
- Generate API token from Discogs account settings
- In Black Candy settings, enter your Discogs token
- Enable “Auto-fetch Artwork”
- Library will automatically fetch missing artwork
Mobile App Setup
Access Black Candy from iOS or Android:
- Download Black Candy app from App Store or F-Droid
- Launch app and add server:
- Server URL:
https://music.yourdomain.com - Email: Your login email
- Password: Your password
- Server URL:
- Sync music library
- Browse and stream from mobile device
Environment Variable Examples
Basic Configuration (SQLite)
RAILS_ENV=productionPORT=3000SECRET_KEY_BASE=your-secure-keyDB_ADAPTER=sqliteMEDIA_PATH=/app/mediaDEFAULT_ADMIN_EMAIL=admin@yourdomain.comDEFAULT_ADMIN_PASSWORD=secure_passwordFORCE_SSL=truePostgreSQL Configuration
RAILS_ENV=productionPORT=3000SECRET_KEY_BASE=your-secure-keyDB_ADAPTER=postgresqlDB_URL=postgresql://blackcandy:password@db:5432/blackcandyCABLE_DB_URL=postgresql://blackcandy:password@db:5432/blackcandy_cableQUEUE_DB_URL=postgresql://blackcandy:password@db:5432/blackcandy_queueCACHE_DB_URL=postgresql://blackcandy:password@db:5432/blackcandy_cacheMEDIA_PATH=/app/mediaDEFAULT_ADMIN_EMAIL=admin@yourdomain.comDEFAULT_ADMIN_PASSWORD=secure_passwordFORCE_SSL=trueComplete Production Configuration
# Rails ConfigurationRAILS_ENV=productionRACK_ENV=productionPORT=3000SECRET_KEY_BASE=generate-very-secure-random-key-64-chars
# Database ConfigurationDB_ADAPTER=sqlite# Uncomment below for PostgreSQL# DB_ADAPTER=postgresql# DB_URL=postgresql://blackcandy_user:secure_password@postgres:5432/blackcandy# CABLE_DB_URL=postgresql://blackcandy_user:secure_password@postgres:5432/blackcandy_cable# QUEUE_DB_URL=postgresql://blackcandy_user:secure_password@postgres:5432/blackcandy_queue# CACHE_DB_URL=postgresql://blackcandy_user:secure_password@postgres:5432/blackcandy_cache
# Media and StorageMEDIA_PATH=/app/mediaMAX_UPLOAD_SIZE=536870912STORAGE_PATH=/app/storage
# User ConfigurationDEFAULT_ADMIN_EMAIL=admin@yourdomain.comDEFAULT_ADMIN_PASSWORD=secure_admin_passwordENABLE_USER_REGISTRATION=true
# SecurityFORCE_SSL=trueSECURE_COOKIES=trueSESSION_TIMEOUT_MINUTES=1440
# Metadata and ArtworkDISCOGS_API_TOKEN=your-discogs-api-tokenENABLE_DISCOGS_LOOKUP=trueAUTO_FETCH_ARTWORK=true
# FeaturesAPI_ENABLED=trueENABLE_PLAYLIST_SHARING=trueENABLE_SCROBBLING=false
# Logging and PerformanceLOG_LEVEL=infoRAILS_LOG_TO_STDOUT=trueRAILS_SERVE_STATIC_FILES=true
# Demo Mode (only for testing)DEMO_MODE=falseSample Code and Getting Started
Python - Black Candy API Integration
import requestsimport jsonfrom typing import Optional, List, Dict
class BlackCandyClient: def __init__(self, base_url: str, email: str, password: str): self.base_url = base_url.rstrip('/') self.api_url = f'{self.base_url}/api' self.email = email self.password = password self.access_token = None self.login()
def login(self) -> bool: """Authenticate with Black Candy API""" try: response = requests.post( f'{self.api_url}/auth/login', json={'email': self.email, 'password': self.password} ) response.raise_for_status() data = response.json() self.access_token = data.get('access_token') return bool(self.access_token) except requests.exceptions.RequestException as e: print(f"Login failed: {e}") return False
def get_headers(self) -> Dict: """Get request headers with authentication""" return { 'Content-Type': 'application/json', 'Authorization': f'Bearer {self.access_token}' }
def get_library_stats(self) -> Optional[Dict]: """Get library statistics""" try: response = requests.get( f'{self.api_url}/library/stats', headers=self.get_headers() ) response.raise_for_status() return response.json() except requests.exceptions.RequestException as e: print(f"Error getting library stats: {e}") return None
def list_artists(self, limit: int = 50, offset: int = 0) -> Optional[List[Dict]]: """List all artists in library""" try: response = requests.get( f'{self.api_url}/artists', headers=self.get_headers(), params={'limit': limit, 'offset': offset} ) response.raise_for_status() return response.json().get('data', []) except requests.exceptions.RequestException as e: print(f"Error listing artists: {e}") return None
def get_artist_albums(self, artist_id: str) -> Optional[List[Dict]]: """Get albums by artist""" try: response = requests.get( f'{self.api_url}/artists/{artist_id}/albums', headers=self.get_headers() ) response.raise_for_status() return response.json().get('data', []) except requests.exceptions.RequestException as e: print(f"Error getting artist albums: {e}") return None
def list_albums(self, limit: int = 50, offset: int = 0) -> Optional[List[Dict]]: """List all albums in library""" try: response = requests.get( f'{self.api_url}/albums', headers=self.get_headers(), params={'limit': limit, 'offset': offset} ) response.raise_for_status() return response.json().get('data', []) except requests.exceptions.RequestException as e: print(f"Error listing albums: {e}") return None
def get_album_songs(self, album_id: str) -> Optional[List[Dict]]: """Get songs in an album""" try: response = requests.get( f'{self.api_url}/albums/{album_id}/songs', headers=self.get_headers() ) response.raise_for_status() return response.json().get('data', []) except requests.exceptions.RequestException as e: print(f"Error getting album songs: {e}") return None
def search_library(self, query: str, search_type: str = 'all') -> Optional[Dict]: """Search library for songs, artists, or albums""" try: response = requests.get( f'{self.api_url}/search', headers=self.get_headers(), params={'q': query, 'type': search_type} ) response.raise_for_status() return response.json() except requests.exceptions.RequestException as e: print(f"Error searching library: {e}") return None
def create_playlist(self, name: str, description: str = '') -> Optional[Dict]: """Create a new playlist""" try: payload = { 'name': name, 'description': description } response = requests.post( f'{self.api_url}/playlists', headers=self.get_headers(), json=payload ) response.raise_for_status() return response.json() except requests.exceptions.RequestException as e: print(f"Error creating playlist: {e}") return None
def add_song_to_playlist(self, playlist_id: str, song_id: str) -> bool: """Add song to playlist""" try: payload = {'song_id': song_id} response = requests.post( f'{self.api_url}/playlists/{playlist_id}/songs', headers=self.get_headers(), json=payload ) response.raise_for_status() return True except requests.exceptions.RequestException as e: print(f"Error adding song to playlist: {e}") return False
def list_playlists(self) -> Optional[List[Dict]]: """Get all user playlists""" try: response = requests.get( f'{self.api_url}/playlists', headers=self.get_headers() ) response.raise_for_status() return response.json().get('data', []) except requests.exceptions.RequestException as e: print(f"Error listing playlists: {e}") return None
def get_recently_played(self, limit: int = 20) -> Optional[List[Dict]]: """Get recently played songs""" try: response = requests.get( f'{self.api_url}/recently-played', headers=self.get_headers(), params={'limit': limit} ) response.raise_for_status() return response.json().get('data', []) except requests.exceptions.RequestException as e: print(f"Error getting recently played: {e}") return None
# Usage exampleif __name__ == "__main__": client = BlackCandyClient('https://music.yourdomain.com', 'admin@yourdomain.com', 'password')
# Get library statistics stats = client.get_library_stats() if stats: print(f"Library contains {stats['total_songs']} songs") print(f"Total artists: {stats['total_artists']}") print(f"Total albums: {stats['total_albums']}")
# List artists artists = client.list_artists() if artists: print(f"\nFirst 10 artists:") for artist in artists[:10]: print(f"- {artist['name']}")
# Search library results = client.search_library('jazz') print(f"\nSearch results for 'jazz': {results}")
# Create playlist playlist = client.create_playlist('My Favorites', 'My favorite songs') if playlist: playlist_id = playlist['id'] print(f"\nPlaylist created: {playlist_id}")
# Add songs to playlist albums = client.list_albums(limit=1) if albums: songs = client.get_album_songs(albums[0]['id']) if songs: for song in songs[:5]: client.add_song_to_playlist(playlist_id, song['id']) print("Added songs to playlist")
# Get recently played recent = client.get_recently_played(limit=10) if recent: print(f"\nRecently played songs: {len(recent)}")JavaScript - Black Candy Web Client
const MUSIC_API_URL = 'https://music.yourdomain.com/api';
class BlackCandyPlayer { constructor(email, password) { this.email = email; this.password = password; this.accessToken = null; this.currentPlaylist = []; this.currentSongIndex = 0; this.isPlaying = false; }
async login() { try { const response = await fetch(`${MUSIC_API_URL}/auth/login`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email: this.email, password: this.password }) });
if (!response.ok) throw new Error('Login failed'); const data = await response.json(); this.accessToken = data.access_token; return true; } catch (error) { console.error('Login error:', error); return false; } }
getHeaders() { return { 'Authorization': `Bearer ${this.accessToken}`, 'Content-Type': 'application/json' }; }
async searchLibrary(query) { try { const response = await fetch( `${MUSIC_API_URL}/search?q=${encodeURIComponent(query)}`, { headers: this.getHeaders() } );
if (!response.ok) throw new Error('Search failed'); return await response.json(); } catch (error) { console.error('Search error:', error); return { data: [] }; } }
async getArtists() { try { const response = await fetch( `${MUSIC_API_URL}/artists`, { headers: this.getHeaders() } );
if (!response.ok) throw new Error('Failed to fetch artists'); return await response.json(); } catch (error) { console.error('Artists fetch error:', error); return { data: [] }; } }
async getAlbums(artistId) { try { const response = await fetch( `${MUSIC_API_URL}/artists/${artistId}/albums`, { headers: this.getHeaders() } );
if (!response.ok) throw new Error('Failed to fetch albums'); return await response.json(); } catch (error) { console.error('Albums fetch error:', error); return { data: [] }; } }
async getSongs(albumId) { try { const response = await fetch( `${MUSIC_API_URL}/albums/${albumId}/songs`, { headers: this.getHeaders() } );
if (!response.ok) throw new Error('Failed to fetch songs'); return await response.json(); } catch (error) { console.error('Songs fetch error:', error); return { data: [] }; } }
async createPlaylist(name, description) { try { const response = await fetch(`${MUSIC_API_URL}/playlists`, { method: 'POST', headers: this.getHeaders(), body: JSON.stringify({ name, description }) });
if (!response.ok) throw new Error('Failed to create playlist'); return await response.json(); } catch (error) { console.error('Playlist creation error:', error); return null; } }
async addSongToPlaylist(playlistId, songId) { try { const response = await fetch( `${MUSIC_API_URL}/playlists/${playlistId}/songs`, { method: 'POST', headers: this.getHeaders(), body: JSON.stringify({ song_id: songId }) } );
return response.ok; } catch (error) { console.error('Add to playlist error:', error); return false; } }
async loadPlaylist(songs) { this.currentPlaylist = songs; this.currentSongIndex = 0; }
async playSong(songId) { try { const audioElement = document.getElementById('music-player'); audioElement.src = `${MUSIC_API_URL}/songs/${songId}/stream`; audioElement.play(); this.isPlaying = true; return true; } catch (error) { console.error('Play error:', error); return false; } }
async getRecentlyPlayed() { try { const response = await fetch( `${MUSIC_API_URL}/recently-played?limit=20`, { headers: this.getHeaders() } );
if (!response.ok) throw new Error('Failed to fetch recently played'); return await response.json(); } catch (error) { console.error('Recently played error:', error); return { data: [] }; } }}
// Usage example(async () => { const player = new BlackCandyPlayer('admin@yourdomain.com', 'password');
// Login const loggedIn = await player.login(); if (!loggedIn) { console.error('Failed to login'); return; }
// Search for songs const searchResults = await player.searchLibrary('jazz'); console.log('Search results:', searchResults);
// Get artists const artists = await player.getArtists(); console.log('Artists:', artists.data);
// Get albums for first artist if (artists.data.length > 0) { const albums = await player.getAlbums(artists.data[0].id); console.log('Albums:', albums.data);
// Get songs for first album if (albums.data.length > 0) { const songs = await player.getSongs(albums.data[0].id); console.log('Songs:', songs.data);
// Load and play first song if (songs.data.length > 0) { await player.loadPlaylist(songs.data); await player.playSong(songs.data[0].id); } } }
// Get recently played const recent = await player.getRecentlyPlayed(); console.log('Recently played:', recent.data);})();cURL - API Integration Examples
# Login to Black Candycurl -X POST https://music.yourdomain.com/api/auth/login \ -H "Content-Type: application/json" \ -d '{ "email": "admin@yourdomain.com", "password": "your-password" }'
# Get library statisticscurl -X GET https://music.yourdomain.com/api/library/stats \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN"
# List artists in librarycurl -X GET "https://music.yourdomain.com/api/artists?limit=50" \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN"
# Get albums by artistcurl -X GET https://music.yourdomain.com/api/artists/{artist-id}/albums \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN"
# Get songs in albumcurl -X GET https://music.yourdomain.com/api/albums/{album-id}/songs \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN"
# Search librarycurl -X GET "https://music.yourdomain.com/api/search?q=jazz&type=all" \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN"
# Create new playlistcurl -X POST https://music.yourdomain.com/api/playlists \ -H "Content-Type: application/json" \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ -d '{ "name": "My Favorite Songs", "description": "A collection of my favorite songs" }'
# Add song to playlistcurl -X POST https://music.yourdomain.com/api/playlists/{playlist-id}/songs \ -H "Content-Type: application/json" \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ -d '{ "song_id": "song-id" }'
# Get all playlistscurl -X GET https://music.yourdomain.com/api/playlists \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN"
# Get recently played songscurl -X GET "https://music.yourdomain.com/api/recently-played?limit=20" \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN"
# Stream song (direct link)curl -X GET https://music.yourdomain.com/api/songs/{song-id}/stream \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ -o song.mp3Bash - Music Library Backup Script
#!/bin/bash
# Black Candy Music Library Backup ScriptBACKUP_DIR="/backups/blackcandy"TIMESTAMP=$(date +%Y%m%d_%H%M%S)RETENTION_DAYS=30
# Create backup directorymkdir -p $BACKUP_DIR
echo "Starting Black Candy backup..."
# Backup database (SQLite)if [ -f "/app/storage/db.sqlite3" ]; then echo "Backing up SQLite database..." cp /app/storage/db.sqlite3 $BACKUP_DIR/blackcandy_db_$TIMESTAMP.sqlite3 gzip $BACKUP_DIR/blackcandy_db_$TIMESTAMP.sqlite3fi
# Backup application dataecho "Backing up application data..."tar -czf $BACKUP_DIR/blackcandy_data_$TIMESTAMP.tar.gz \ /app/storage/config \ /app/storage/cache
# Optional: Backup entire music library (comment out if too large)# echo "Backing up music library..."# tar -czf $BACKUP_DIR/blackcandy_music_$TIMESTAMP.tar.gz /app/media
# Create backup manifestcat > $BACKUP_DIR/backup_manifest_$TIMESTAMP.txt << EOFBackup Date: $TIMESTAMPDatabase: blackcandy_db_$TIMESTAMP.sqlite3.gzData: blackcandy_data_$TIMESTAMP.tar.gzMusic Library: blackcandy_music_$TIMESTAMP.tar.gz (optional)EOF
# Cleanup old backupsecho "Cleaning up old backups..."find $BACKUP_DIR -name "blackcandy_*" -mtime +$RETENTION_DAYS -delete
# Backup summaryecho "Backup completed: $TIMESTAMP"ls -lh $BACKUP_DIR | tail -10
# Optional: Upload to cloud storage (S3 example)# aws s3 cp $BACKUP_DIR/blackcandy_db_$TIMESTAMP.sqlite3.gz s3://your-bucket/backups/# aws s3 cp $BACKUP_DIR/blackcandy_data_$TIMESTAMP.tar.gz s3://your-bucket/backups/Docker Compose for Local Development
For local testing before deploying to Klutch.sh:
version: '3.8'
services: black-candy: image: ghcr.io/blackcandy-org/blackcandy:latest container_name: black-candy-app environment: RAILS_ENV: development PORT: 3000 SECRET_KEY_BASE: dev-secret-key-change-in-production DB_ADAPTER: sqlite MEDIA_PATH: /app/media DEFAULT_ADMIN_EMAIL: admin@localhost DEFAULT_ADMIN_PASSWORD: admin123 DISCOGS_API_TOKEN: "" ENABLE_DISCOGS_LOOKUP: "false" ports: - "3000:3000" volumes: - ./media:/app/media - ./storage:/app/storage - ./logs:/app/logs restart: unless-stoppedTo run locally:
docker-compose up -dAccess Black Candy at http://localhost:3000
Music Library Management
Organizing Your Music Files
Proper folder structure ensures efficient library scanning:
-
Artist-based Organization (Recommended):
/app/media/├─ Artist Name/│ ├─ Album Name/│ │ ├─ 01 - Song Title.mp3│ │ ├─ 02 - Song Title.mp3 -
Flat Structure:
/app/media/├─ Song1.mp3├─ Song2.mp3├─ Song3.mp3 -
Genre-based Organization:
/app/media/├─ Jazz/│ ├─ Artist/│ │ ├─ Album/│ │ │ ├─ songs...
Supported Audio Formats
Black Candy supports multiple audio formats:
- MP3: Wide compatibility, smaller file size
- FLAC: Lossless, larger file size, high quality
- WAV: Uncompressed, very large files
- OGG Vorbis: Open format, good compression
- AAC: iTunes standard, good quality
- M4A: Apple format, good quality
- Opus: Modern codec, excellent quality
Metadata Management
Configure metadata for your library:
- Log in to Black Candy
- Navigate to “Settings” → “Metadata”
- Enable Discogs integration with API token
- Auto-fetch album artwork for missing items
- Update artist information automatically
- Rescan library to apply metadata changes
Large Library Considerations
For music libraries exceeding 100GB:
- Use PostgreSQL for better performance
- Increase memory allocation to container
- Optimize indexes in database
- Enable caching for faster browsing
- Consider splitting into multiple volumes
- Monitor performance during scanning
User Management
Creating New Users
Add family members and friends to your music library:
- In settings, navigate to “Users”
- Click “Add New User”
- Enter email and set password
- Assign role: Admin or User
- User receives login credentials
- User can change password on first login
User Roles and Permissions
Configure access levels:
| Role | Permissions |
|---|---|
| Admin | Full access, manage users, settings |
| User | Browse and stream music, create playlists |
Managing User Access
Control what users can access:
- Admin users can see all songs and playlists
- Regular users see only allowed content
- Each user has separate playlist collection
- Recently played tracking per user
- User preferences stored separately
Advanced Features
Enabling Scrobbling (Optional)
Track your listening activity:
- Set
ENABLE_SCROBBLING=truein environment variables - Configure scrobbling service (if applicable)
- Music plays are automatically recorded
- View listening statistics and charts
Playlist Sharing
Share playlists with other users:
- Set
ENABLE_PLAYLIST_SHARING=true - Create playlist normally
- Share playlist with specific users
- Shared users can view but not modify
- Owner controls sharing permissions
API Access
Use Black Candy API for integrations:
- Ensure
API_ENABLED=true - Generate API token for authentication
- Use token in API requests
- Build custom applications
- Integrate with other services
Security and Performance
Password Security
Ensure strong user passwords:
- Minimum 8 characters
- Mix of uppercase, lowercase, numbers
- Avoid common passwords
- Regular password changes recommended
- Force password reset on first login
Network Security
Secure your Black Candy deployment:
- HTTPS enforced via FORCE_SSL
- Strong SSL certificates (automatic via Klutch.sh)
- Firewall rules limiting access
- Rate limiting on authentication
- Regular security updates
Performance Optimization
Optimize for large music libraries:
-- Create indexes for common queriesCREATE INDEX idx_song_artist ON songs(artist_id);CREATE INDEX idx_song_album ON songs(album_id);CREATE INDEX idx_album_artist ON albums(artist_id);CREATE INDEX idx_playlist_user ON playlists(user_id);CREATE INDEX idx_played_song ON played_songs(song_id);Database Optimization
Switch to PostgreSQL for better performance:
- Create PostgreSQL database
- Set
DB_ADAPTER=postgresql - Configure DB_URL and related variables
- Migrate data from SQLite
- Monitor performance improvements
Monitoring and Maintenance
Regular Backups
Schedule automated backups:
- Database backups weekly
- Application data backups daily
- Music library backup monthly (if changed)
- Test restore procedures regularly
- Store backups in secure location
Logging and Monitoring
Monitor application health:
# Check application logsdocker logs black-candy-app
# Monitor system resourcesdocker stats black-candy-app
# Check disk usagedf -h /app/media /app/storageDatabase Maintenance
Regular database optimization:
# Vacuum SQLite database (defragment)sqlite3 /app/storage/db.sqlite3 VACUUM;
# Analyze query performancesqlite3 /app/storage/db.sqlite3 ANALYZE;Troubleshooting
Common Issues and Solutions
Issue: Music files not appearing in library
Solutions:
- Verify files are in
/app/mediadirectory - Check file formats are supported
- Ensure proper read permissions on files
- Try manual library rescan from settings
- Check application logs for errors
- Verify database is not corrupted
Issue: Slow library browsing
Troubleshooting:
- Use PostgreSQL instead of SQLite
- Enable caching in settings
- Optimize database indexes
- Check system resource usage
- Monitor network connectivity
- Consider reducing simultaneous users
Issue: Album artwork not loading
Solutions:
- Verify Discogs API token is valid
- Check internet connectivity
- Enable ENABLE_DISCOGS_LOOKUP
- Manually trigger metadata update
- Check API rate limits
- Try manual artwork upload
Issue: Login failures
Solutions:
- Verify email and password are correct
- Check if user account is created
- Review application logs
- Verify database connectivity
- Check session configuration
- Restart application container
Updating Black Candy
To update Black Candy to a newer version:
-
Update your Dockerfile:
FROM ghcr.io/blackcandy-org/blackcandy:latest -
Read the Upgrade Guide carefully
-
Commit and push to GitHub
-
Klutch.sh will automatically rebuild
-
Always backup database before upgrading
-
Test in development first
-
Monitor logs after upgrade
Use Cases
Personal Music Streaming
- Stream personal music collection from any device
- Create custom playlists for different moods
- High-quality audio streaming at home
Family Music Sharing
- Share music library with family members
- Each family member has separate account
- Control access to different content
- Create shared playlists for family
Audiophile Music Platform
- Stream lossless FLAC audio files
- Maintain full quality without compression
- Complete library control
- No bandwidth limitations
Private Music Curation
- Build music discovery without algorithms
- Maintain complete privacy
- No tracking or data collection
- Full ownership of listening data
Additional Resources
- Black Candy GitHub Repository - Source code and documentation
- Black Candy Upgrade Guide - Version upgrade instructions
- Black Candy iOS App - Apple App Store
- Black Candy Android App - F-Droid Store
- Discogs API Documentation - Metadata and artwork integration
- Klutch.sh Getting Started Guide
- Klutch.sh Volumes Documentation
- Klutch.sh Custom Domains Guide
Conclusion
Deploying Black Candy on Klutch.sh provides you with a complete, self-hosted music streaming platform that maintains full privacy and control over your music library. With comprehensive music library management, multi-user support, playlist creation, mobile app access, and optional metadata integration, Black Candy enables you to stream your entire music collection without limitations or subscriptions. Klutch.sh’s managed infrastructure ensures your music platform is always available, secure, and performant, allowing you to focus on enjoying your music.
Start hosting your personal music streaming server today by deploying Black Candy on Klutch.sh and take complete control of your audio experience.