Skip to content

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

  1. Create a New Project

    Log in to your Klutch.sh dashboard and create a new project for your Black Candy deployment.

  2. 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.md

    Here’s a Dockerfile for Black Candy:

    FROM ghcr.io/blackcandy-org/blackcandy:latest
    # Install additional utilities
    RUN apt-get update && apt-get install -y \
    curl \
    ca-certificates \
    && rm -rf /var/lib/apt/lists/*
    # Set working directory
    WORKDIR /app
    # Create necessary directories
    RUN mkdir -p /app/storage \
    /app/media \
    /app/logs && \
    chmod -R 755 /app
    # Copy entrypoint script
    COPY docker-entrypoint.sh /
    RUN chmod +x /docker-entrypoint.sh
    # Expose port
    EXPOSE 3000
    # Health check
    HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=3 \
    CMD curl -f http://localhost:3000 || exit 1
    # Run entrypoint
    ENTRYPOINT ["/docker-entrypoint.sh"]
    CMD ["start"]

    Create a docker-entrypoint.sh file:

    #!/bin/bash
    set -e
    # Create necessary directories
    mkdir -p /app/storage/uploads \
    /app/storage/cache \
    /app/media \
    /app/logs
    # Set proper permissions
    chmod -R 755 /app/storage
    chmod -R 755 /app/media
    # Initialize database if needed (SQLite)
    if [ ! -f /app/storage/db.sqlite3 ]; then
    echo "Initializing database..."
    if [ "$DB_ADAPTER" = "postgresql" ]; then
    # PostgreSQL will be handled by the application
    echo "PostgreSQL configured"
    else
    echo "SQLite configured"
    fi
    fi
    # Check database connection if using PostgreSQL
    if [ "$DB_ADAPTER" = "postgresql" ] && [ -n "$DB_URL" ]; then
    echo "Waiting for database connection..."
    MAX_ATTEMPTS=30
    ATTEMPTS=0
    while [ $ATTEMPTS -lt $MAX_ATTEMPTS ]; do
    if 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; then
    echo "Database is ready!"
    break
    fi
    ATTEMPTS=$((ATTEMPTS + 1))
    sleep 1
    done
    fi
    if [ "$1" = "start" ]; then
    # Start the application
    exec bundle exec rails server -b 0.0.0.0 -p 3000
    else
    exec "$@"
    fi

    Create a .env.example file:

    Terminal window
    # Black Candy Configuration
    RAILS_ENV=production
    RACK_ENV=production
    PORT=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
    # Security
    SECRET_KEY_BASE=generate-your-secure-key-here
    FORCE_SSL=true
    # Media Configuration
    MEDIA_PATH=/app/media
    MAX_UPLOAD_SIZE=536870912
    # User Configuration
    DEFAULT_ADMIN_EMAIL=admin@yourdomain.com
    DEFAULT_ADMIN_PASSWORD=secure_password_here
    # API Configuration
    API_ENABLED=true
    # Logging
    LOG_LEVEL=info
    RAILS_LOG_TO_STDOUT=true
    # Discogs Integration (Optional)
    DISCOGS_API_TOKEN=your-discogs-token-here
    ENABLE_DISCOGS_LOOKUP=true
    # Demo Mode
    DEMO_MODE=false
    # Features
    ENABLE_USER_REGISTRATION=true
    ENABLE_PLAYLIST_SHARING=true
    ENABLE_SCROBBLING=false

    Commit and push to your GitHub repository:

    Terminal window
    git init
    git add .
    git commit -m "Initial Black Candy deployment"
    git remote add origin https://github.com/yourusername/black-candy-deploy.git
    git push -u origin main
  3. Create a New App

    In the Klutch.sh dashboard:

    • Click “Create New App”
    • Select your GitHub repository containing the Dockerfile
    • Choose the branch (typically main or master)
    • Klutch.sh will automatically detect the Dockerfile in the root directory
  4. Configure Environment Variables

    Set up these essential environment variables in your Klutch.sh dashboard:

    VariableDescriptionExample
    RAILS_ENVRails environmentproduction
    PORTApplication port3000
    SECRET_KEY_BASERails secret key (generate secure random)secure-random-string
    DB_ADAPTERDatabase type (sqlite or postgresql)sqlite
    DB_URLPostgreSQL connection string (if using PostgreSQL)postgresql://user:pass@host/db
    CABLE_DB_URLWebSocket database URL (PostgreSQL only)postgresql://user:pass@host/db
    QUEUE_DB_URLBackground job database URL (PostgreSQL only)postgresql://user:pass@host/db
    CACHE_DB_URLCache database URL (PostgreSQL only)postgresql://user:pass@host/db
    MEDIA_PATHPath to media files/app/media
    DEFAULT_ADMIN_EMAILInitial admin emailadmin@yourdomain.com
    DEFAULT_ADMIN_PASSWORDInitial admin passwordsecure_password
    FORCE_SSLForce HTTPS connectionstrue
    DISCOGS_API_TOKENDiscogs API token for artworkyour-token
    ENABLE_DISCOGS_LOOKUPEnable album artwork lookuptrue
    LOG_LEVELLogging verbosityinfo
    API_ENABLEDEnable API accesstrue
  5. Configure Persistent Storage

    Black Candy requires persistent storage for music files and application data. Add persistent volumes:

    Mount PathDescriptionRecommended Size
    /app/mediaMusic files and library500GB+ (based on collection size)
    /app/storageDatabase, cache, and application data50GB
    /app/logsApplication logs20GB

    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
  6. 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
  7. 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
  8. 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:

  1. Log in to your Black Candy instance
  2. Navigate to “Settings” → “Library”
  3. Set media path: /app/media (if not already set)
  4. Black Candy will automatically scan for music files
  5. Supported formats: MP3, FLAC, WAV, OGG, AAC, M4A
  6. 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:

  1. Via SFTP/SCP: Connect to persistent volume and transfer files
  2. Via Web Interface: Some implementations support direct upload
  3. Via Mounted Folder: Place files in /app/media and rescan library
  4. Organize by: Artist/Album folder structure recommended
  5. 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:

  1. Navigate to “Settings” → “Users”
  2. Click “Add New User”
  3. Enter email and password
  4. Set user role (Admin or User)
  5. Send login credentials to user
  6. User can change password on first login

Creating Playlists

Build custom playlists from your music library:

  1. Navigate to “Library” or “Playlists”
  2. Search for songs or browse by artist/album
  3. Create new playlist: “My Favorite Songs”
  4. Add songs by dragging or clicking add button
  5. Organize songs within playlist
  6. Share playlist with other users (if enabled)

Enabling Discogs Integration

Auto-fetch album artwork and metadata:

  1. Create a Discogs Developer Account
  2. Generate API token from Discogs account settings
  3. In Black Candy settings, enter your Discogs token
  4. Enable “Auto-fetch Artwork”
  5. Library will automatically fetch missing artwork

Mobile App Setup

Access Black Candy from iOS or Android:

  1. Download Black Candy app from App Store or F-Droid
  2. Launch app and add server:
    • Server URL: https://music.yourdomain.com
    • Email: Your login email
    • Password: Your password
  3. Sync music library
  4. Browse and stream from mobile device

Environment Variable Examples

Basic Configuration (SQLite)

Terminal window
RAILS_ENV=production
PORT=3000
SECRET_KEY_BASE=your-secure-key
DB_ADAPTER=sqlite
MEDIA_PATH=/app/media
DEFAULT_ADMIN_EMAIL=admin@yourdomain.com
DEFAULT_ADMIN_PASSWORD=secure_password
FORCE_SSL=true

PostgreSQL Configuration

Terminal window
RAILS_ENV=production
PORT=3000
SECRET_KEY_BASE=your-secure-key
DB_ADAPTER=postgresql
DB_URL=postgresql://blackcandy:password@db:5432/blackcandy
CABLE_DB_URL=postgresql://blackcandy:password@db:5432/blackcandy_cable
QUEUE_DB_URL=postgresql://blackcandy:password@db:5432/blackcandy_queue
CACHE_DB_URL=postgresql://blackcandy:password@db:5432/blackcandy_cache
MEDIA_PATH=/app/media
DEFAULT_ADMIN_EMAIL=admin@yourdomain.com
DEFAULT_ADMIN_PASSWORD=secure_password
FORCE_SSL=true

Complete Production Configuration

Terminal window
# Rails Configuration
RAILS_ENV=production
RACK_ENV=production
PORT=3000
SECRET_KEY_BASE=generate-very-secure-random-key-64-chars
# Database Configuration
DB_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 Storage
MEDIA_PATH=/app/media
MAX_UPLOAD_SIZE=536870912
STORAGE_PATH=/app/storage
# User Configuration
DEFAULT_ADMIN_EMAIL=admin@yourdomain.com
DEFAULT_ADMIN_PASSWORD=secure_admin_password
ENABLE_USER_REGISTRATION=true
# Security
FORCE_SSL=true
SECURE_COOKIES=true
SESSION_TIMEOUT_MINUTES=1440
# Metadata and Artwork
DISCOGS_API_TOKEN=your-discogs-api-token
ENABLE_DISCOGS_LOOKUP=true
AUTO_FETCH_ARTWORK=true
# Features
API_ENABLED=true
ENABLE_PLAYLIST_SHARING=true
ENABLE_SCROBBLING=false
# Logging and Performance
LOG_LEVEL=info
RAILS_LOG_TO_STDOUT=true
RAILS_SERVE_STATIC_FILES=true
# Demo Mode (only for testing)
DEMO_MODE=false

Sample Code and Getting Started

Python - Black Candy API Integration

import requests
import json
from 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 example
if __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

Terminal window
# Login to Black Candy
curl -X POST https://music.yourdomain.com/api/auth/login \
-H "Content-Type: application/json" \
-d '{
"email": "admin@yourdomain.com",
"password": "your-password"
}'
# Get library statistics
curl -X GET https://music.yourdomain.com/api/library/stats \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN"
# List artists in library
curl -X GET "https://music.yourdomain.com/api/artists?limit=50" \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN"
# Get albums by artist
curl -X GET https://music.yourdomain.com/api/artists/{artist-id}/albums \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN"
# Get songs in album
curl -X GET https://music.yourdomain.com/api/albums/{album-id}/songs \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN"
# Search library
curl -X GET "https://music.yourdomain.com/api/search?q=jazz&type=all" \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN"
# Create new playlist
curl -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 playlist
curl -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 playlists
curl -X GET https://music.yourdomain.com/api/playlists \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN"
# Get recently played songs
curl -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.mp3

Bash - Music Library Backup Script

#!/bin/bash
# Black Candy Music Library Backup Script
BACKUP_DIR="/backups/blackcandy"
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
RETENTION_DAYS=30
# Create backup directory
mkdir -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.sqlite3
fi
# Backup application data
echo "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 manifest
cat > $BACKUP_DIR/backup_manifest_$TIMESTAMP.txt << EOF
Backup Date: $TIMESTAMP
Database: blackcandy_db_$TIMESTAMP.sqlite3.gz
Data: blackcandy_data_$TIMESTAMP.tar.gz
Music Library: blackcandy_music_$TIMESTAMP.tar.gz (optional)
EOF
# Cleanup old backups
echo "Cleaning up old backups..."
find $BACKUP_DIR -name "blackcandy_*" -mtime +$RETENTION_DAYS -delete
# Backup summary
echo "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-stopped

To run locally:

Terminal window
docker-compose up -d

Access Black Candy at http://localhost:3000

Music Library Management

Organizing Your Music Files

Proper folder structure ensures efficient library scanning:

  1. Artist-based Organization (Recommended):

    /app/media/
    ├─ Artist Name/
    │ ├─ Album Name/
    │ │ ├─ 01 - Song Title.mp3
    │ │ ├─ 02 - Song Title.mp3
  2. Flat Structure:

    /app/media/
    ├─ Song1.mp3
    ├─ Song2.mp3
    ├─ Song3.mp3
  3. 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:

  1. Log in to Black Candy
  2. Navigate to “Settings” → “Metadata”
  3. Enable Discogs integration with API token
  4. Auto-fetch album artwork for missing items
  5. Update artist information automatically
  6. Rescan library to apply metadata changes

Large Library Considerations

For music libraries exceeding 100GB:

  1. Use PostgreSQL for better performance
  2. Increase memory allocation to container
  3. Optimize indexes in database
  4. Enable caching for faster browsing
  5. Consider splitting into multiple volumes
  6. Monitor performance during scanning

User Management

Creating New Users

Add family members and friends to your music library:

  1. In settings, navigate to “Users”
  2. Click “Add New User”
  3. Enter email and set password
  4. Assign role: Admin or User
  5. User receives login credentials
  6. User can change password on first login

User Roles and Permissions

Configure access levels:

RolePermissions
AdminFull access, manage users, settings
UserBrowse and stream music, create playlists

Managing User Access

Control what users can access:

  1. Admin users can see all songs and playlists
  2. Regular users see only allowed content
  3. Each user has separate playlist collection
  4. Recently played tracking per user
  5. User preferences stored separately

Advanced Features

Enabling Scrobbling (Optional)

Track your listening activity:

  1. Set ENABLE_SCROBBLING=true in environment variables
  2. Configure scrobbling service (if applicable)
  3. Music plays are automatically recorded
  4. View listening statistics and charts

Playlist Sharing

Share playlists with other users:

  1. Set ENABLE_PLAYLIST_SHARING=true
  2. Create playlist normally
  3. Share playlist with specific users
  4. Shared users can view but not modify
  5. Owner controls sharing permissions

API Access

Use Black Candy API for integrations:

  1. Ensure API_ENABLED=true
  2. Generate API token for authentication
  3. Use token in API requests
  4. Build custom applications
  5. 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 queries
CREATE 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:

  1. Create PostgreSQL database
  2. Set DB_ADAPTER=postgresql
  3. Configure DB_URL and related variables
  4. Migrate data from SQLite
  5. 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:

Terminal window
# Check application logs
docker logs black-candy-app
# Monitor system resources
docker stats black-candy-app
# Check disk usage
df -h /app/media /app/storage

Database Maintenance

Regular database optimization:

Terminal window
# Vacuum SQLite database (defragment)
sqlite3 /app/storage/db.sqlite3 VACUUM;
# Analyze query performance
sqlite3 /app/storage/db.sqlite3 ANALYZE;

Troubleshooting

Common Issues and Solutions

Issue: Music files not appearing in library

Solutions:

  • Verify files are in /app/media directory
  • 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:

  1. Update your Dockerfile:

    FROM ghcr.io/blackcandy-org/blackcandy:latest
  2. Read the Upgrade Guide carefully

  3. Commit and push to GitHub

  4. Klutch.sh will automatically rebuild

  5. Always backup database before upgrading

  6. Test in development first

  7. 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

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.