Quality & CachingΒΆ
The quality and caching system allows clients to request different audio quality levels while the server manages transcoded file caching for optimal performance.
π― PurposeΒΆ
Quality selection enables:
- Bandwidth optimization for mobile/cellular connections
- User preference for quality vs. data usage
- Faster streaming on slower connections
- Battery savings on mobile devices
Caching system provides:
- Pre-transcoded files for instant delivery
- Reduced server CPU load (no real-time transcoding)
- Faster response times
- Lower latency for quality switching
π Quality LevelsΒΆ
Available QualitiesΒΆ
| Quality | Bitrate | Format | Use Case |
|---|---|---|---|
original |
Varies | Original format (FLAC, MP3, etc.) | WiFi, audiophiles, local playback |
high |
256 kbps | MP3 | High-quality wireless, good connections |
medium |
192 kbps | MP3 | Default, balanced quality/bandwidth |
low |
128 kbps | MP3 | Cellular, bandwidth-constrained |
Quality ParameterΒΆ
URL format:
Examples:
/play/jazz/miles-davis/so-what.flac # original (no param)
/play/jazz/miles-davis/so-what.flac?quality=original # original (explicit)
/play/jazz/miles-davis/so-what.flac?quality=high # 256k MP3
/play/jazz/miles-davis/so-what.flac?quality=medium # 192k MP3
/play/jazz/miles-davis/so-what.flac?quality=low # 128k MP3
Default: original (when parameter omitted)
Frontend IntegrationΒΆ
JavaScript:
const QUALITY_LEVELS = {
high: { label: 'High (256k)', bandwidth: 'high' },
medium: { label: 'Medium (192k)', bandwidth: 'medium' },
low: { label: 'Low (128k)', bandwidth: 'low' },
original: { label: 'Original', bandwidth: 'highest' }
};
// Get from localStorage
let currentQuality = localStorage.getItem('audioQuality') || 'medium';
// Build URL
const url = `/play/${track.path}?quality=${currentQuality}`;
See: Player Controls
π§ ImplementationΒΆ
_get_serving_path()ΒΆ
Purpose: Select which file to serve (original or cached)
Signature:
def _get_serving_path(
original_path: Path,
quality: str,
cache: AudioCache,
logger: Logger
) -> Path
Flow diagram:
flowchart TD
Start[_get_serving_path called]
CheckQuality{quality == 'original'?}
NeedTranscode{File needs transcoding?}
CheckCache{Cached version exists?}
ReturnOriginal[Return original path]
ReturnCached[Return cached path]
LogHit[Log cache hit]
LogMiss[Log cache miss]
Start --> CheckQuality
CheckQuality -->|Yes| ReturnOriginal
CheckQuality -->|No| NeedTranscode
NeedTranscode -->|No| ReturnOriginal
NeedTranscode -->|Yes| CheckCache
CheckCache -->|Yes| LogHit
CheckCache -->|No| LogMiss
LogHit --> ReturnCached
LogMiss --> ReturnOriginal
style ReturnCached fill:#4a8c5f,color:#fff
style LogHit fill:#4a8c5f,color:#fff
style LogMiss fill:#c65d5d,color:#fff
Implementation:
def _get_serving_path(
original_path: Path,
quality: str,
cache: AudioCache,
logger: Logger
) -> Path:
"""
Select file to serve: original or cached transcoded version
Logic:
1. If quality='original' β return original
2. If file doesn't need transcoding (already MP3) β return original
3. If cached version exists β return cached path
4. Otherwise β return original (cache miss)
"""
# Original quality requested
if quality == "original":
logger.debug(f"Serving original: {original_path.name}")
return original_path
# Check if file needs transcoding
if not cache.should_transcode(original_path):
logger.debug(
f"File already compatible: {original_path.name} "
f"(no transcode needed)"
)
return original_path
# Check if cached version exists
if cache.is_cached(original_path, quality):
cached_path = cache.get_cache_path(original_path, quality)
logger.info(
f"Cache hit: {original_path.name} β {quality} quality"
)
return cached_path
# Cache miss - serve original
logger.warning(
f"Cache miss: {original_path.name} ({quality}) - "
f"serving original"
)
return original_path
Transcoding DecisionΒΆ
Files that need transcoding:
- FLAC β MP3
- WAV β MP3
- OGG β MP3
- M4A β MP3
Files that don't need transcoding:
- MP3 β MP3 (already compressed)
- AAC β AAC (already compressed, different use case)
Implementation:
# In AudioCache class
def should_transcode(self, file_path: Path) -> bool:
"""Check if file should be transcoded"""
extension = file_path.suffix.lower()
return extension in {'.flac', '.wav', '.ogg', '.m4a'}
πΎ Cache StructureΒΆ
Directory LayoutΒΆ
cache/audio/
βββ <relative_path>/
βββ <filename>_high.mp3 # 256k version
βββ <filename>_medium.mp3 # 192k version
βββ <filename>_low.mp3 # 128k version
Example:
Original: /music/jazz/miles-davis/so-what.flac
Cache:
cache/audio/jazz/miles-davis/
βββ so-what_high.mp3 # 256kbps MP3
βββ so-what_medium.mp3 # 192kbps MP3
βββ so-what_low.mp3 # 128kbps MP3
Cache Path GenerationΒΆ
Method:
Implementation:
def get_cache_path(self, original_path: Path, quality: str) -> Path:
"""
Generate cache file path for transcoded version
Example:
/music/jazz/album/song.flac + 'medium'
β /cache/audio/jazz/album/song_medium.mp3
"""
# Get relative path from MUSIC_ROOT
relative = original_path.relative_to(MUSIC_ROOT)
# Build cache path
cache_dir = CACHE_DIR / relative.parent
cache_file = f"{relative.stem}_{quality}.mp3"
return cache_dir / cache_file
π Cache PopulationΒΆ
Pre-caching StrategyΒΆ
Background job: Periodically transcode popular/recent tracks
# Example background job
def precache_mixtapes():
"""Pre-populate cache for all mixtapes"""
mixtapes = mixtape_manager.get_all()
for mixtape in mixtapes:
for track in mixtape['tracks']:
for quality in ['high', 'medium', 'low']:
original = Path(MUSIC_ROOT) / track['file_path']
# Check if needs caching
if cache.should_transcode(original):
if not cache.is_cached(original, quality):
# Transcode in background
cache.transcode(original, quality)
Triggers:
- New mixtape created
- Track added to mixtape
- Scheduled background task (nightly)
See: Audio Caching for transcoding implementation
On-Demand TranscodingΒΆ
Current status: Commented out (prepared for future use)
# Future: On-demand transcoding
# if not cache.is_cached(original_path, quality):
# cache.transcode(original_path, quality)
# cached_path = cache.get_cache_path(original_path, quality)
Why disabled:
- Real-time transcoding adds latency
- CPU-intensive operation
- Better to pre-cache popular tracks
When to enable:
- Low storage constraints
- Powerful server hardware
- Infrequent cache misses acceptable
π Cache ManagementΒΆ
Admin EndpointsΒΆ
GET /admin/cache/statsΒΆ
Purpose: Get cache statistics
Response:
{
"cache_size_bytes": 1234567890,
"cache_size_mb": 1177.38,
"cached_files": 1523,
"cache_dir": "/app/cache/audio"
}
Implementation:
@play.route("/admin/cache/stats")
def cache_stats():
"""Return cache statistics"""
cache = current_app.audio_cache
size_bytes = cache.get_cache_size()
size_mb = round(size_bytes / (1024 * 1024), 2)
file_count = cache.count_cached_files()
return jsonify({
"cache_size_bytes": size_bytes,
"cache_size_mb": size_mb,
"cached_files": file_count,
"cache_dir": str(cache.cache_dir)
})
POST /admin/cache/clearΒΆ
Purpose: Clear cached files
Query parameters:
older_than_days(optional) - Only delete files older than N days
Request:
Request with filter:
Response:
Implementation:
@play.route("/admin/cache/clear", methods=["POST"])
def clear_cache():
"""Clear audio cache"""
cache = current_app.audio_cache
older_than_days = request.args.get("older_than_days", type=int)
deleted_count = cache.clear_cache(older_than_days=older_than_days)
if older_than_days:
message = f"Cleared {deleted_count} cached files older than {older_than_days} days"
else:
message = f"Cleared all {deleted_count} cached files"
logger.info(message)
return jsonify({
"deleted_files": deleted_count,
"message": message
})
π Performance OptimizationΒΆ
Cache Hit RateΒΆ
Metric: Percentage of requests served from cache
Monitoring:
# Log cache performance
logger.info(f"Cache hit: {file_path} β {quality}") # Hit
logger.warning(f"Cache miss: {file_path} ({quality})") # Miss
Analyze logs:
# Count cache hits vs misses
grep "Cache hit" logs/app.log | wc -l # Hits
grep "Cache miss" logs/app.log | wc -l # Misses
Target hit rate: 80%+ for optimal performance
Cache WarmingΒΆ
Strategy: Pre-cache frequently accessed tracks
def warm_cache():
"""Pre-cache popular tracks"""
# Get recently played tracks
popular_tracks = get_popular_tracks(limit=100)
for track in popular_tracks:
for quality in ['medium', 'high']:
if cache.should_transcode(track.path):
if not cache.is_cached(track.path, quality):
cache.transcode(track.path, quality)
When to run:
- Server startup
- Low-traffic periods (2-4 AM)
- After adding new mixtapes
Storage ManagementΒΆ
Monitor disk usage:
Set cache size limits:
# Example: Limit cache to 10GB
MAX_CACHE_SIZE = 10 * 1024 * 1024 * 1024 # 10GB
if cache.get_cache_size() > MAX_CACHE_SIZE:
# Clear oldest files
cache.clear_cache(older_than_days=90)
Cleanup strategies:
- Delete files older than X days
- Delete least recently accessed files
- Delete least popular quality levels
- Keep only medium quality for space savings
π§ͺ TestingΒΆ
Manual TestingΒΆ
Test original quality:
Test medium quality:
curl -I http://localhost:5000/play/artist/album/song.flac?quality=medium
# Should serve cached MP3 (if cached) or original
Test cache stats:
Test cache clear:
Check Cache DirectoryΒΆ
View cached files:
Expected:
Check file sizes:
π ConfigurationΒΆ
Environment VariablesΒΆ
# Audio cache directory
AUDIO_CACHE_DIR=/app/cache/audio
# Enable/disable caching
AUDIO_CACHE_ENABLED=true
# Pre-cache on mixtape upload
AUDIO_CACHE_PRECACHE_ON_UPLOAD=true
Docker Volume MappingΒΆ
# docker-compose.yml
services:
mixtape:
volumes:
- mixtape_data:/app/collection-data
# Cache is inside collection-data:
# /app/collection-data/cache/audio/
Persistence: Cache survives container restarts
See: Docker Deployment
π Related DocumentationΒΆ
- Audio Streaming - Uses _get_serving_path()
- Audio Caching - Transcoding implementation
- Player Controls - Quality selector UI
- Configuration - Cache directory settings
π‘ Best PracticesΒΆ
For UsersΒΆ
WiFi:
- Use
highororiginalquality - Better audio quality
- No bandwidth concerns
Cellular:
- Use
mediumorlowquality - Save data usage
- Faster streaming
Storage-constrained:
- Use
lowquality - Smallest cache size
- Adequate quality for most cases
For AdministratorsΒΆ
Cache management:
- Monitor cache size regularly
- Clear old files periodically
- Pre-cache popular mixtapes
- Balance quality vs storage
Performance:
- Warm cache on startup
- Schedule cleanup during low-traffic hours
- Monitor hit/miss ratio
- Adjust cache strategy based on usage
Implementation: src/routes/play.py::_get_serving_path() and src/audio_cache/
