#!/usr/bin/env python3 """ Audio library scanner CLI tool. Scans a directory for audio files and adds them to the database. """ import os import sys import argparse from pathlib import Path from typing import List # Add parent directory to path for imports sys.path.insert(0, str(Path(__file__).parent.parent.parent)) from src.core.audio_processor import extract_all_features from src.core.essentia_classifier import EssentiaClassifier from src.core.transcoder import AudioTranscoder from src.core.waveform_generator import save_waveform_to_file from src.models.database import SessionLocal from src.models.schema import AudioTrack from src.utils.logging import get_logger logger = get_logger(__name__) # Supported audio formats AUDIO_EXTENSIONS = {'.mp3', '.wav', '.flac', '.m4a', '.aac', '.ogg', '.wma'} def find_audio_files(directory: str) -> List[Path]: """Find all audio files in directory and subdirectories. Args: directory: Root directory to scan Returns: List of paths to audio files """ audio_files = [] directory_path = Path(directory) if not directory_path.exists(): logger.error(f"Directory does not exist: {directory}") return [] logger.info(f"Scanning directory: {directory}") for root, dirs, files in os.walk(directory_path): # Skip transcoded and waveforms directories dirs[:] = [d for d in dirs if d not in ['transcoded', 'waveforms', '.transcoded', '.waveforms']] for file in files: file_path = Path(root) / file if file_path.suffix.lower() in AUDIO_EXTENSIONS: audio_files.append(file_path) logger.info(f"Found {len(audio_files)} audio files") return audio_files def analyze_and_store(file_path: Path, classifier: EssentiaClassifier, transcoder: AudioTranscoder, db) -> bool: """Analyze an audio file and store it in the database. Args: file_path: Path to audio file classifier: Essentia classifier instance transcoder: Audio transcoder instance db: Database session Returns: True if successful, False otherwise """ try: logger.info(f"Processing: {file_path}") # Check if already in database existing = db.query(AudioTrack).filter(AudioTrack.filepath == str(file_path)).first() if existing: logger.info(f"Already in database, skipping: {file_path}") return True # Extract basic features with librosa features = extract_all_features(str(file_path)) # Get genre classification genre_result = classifier.predict_genre(str(file_path)) # Get mood classification mood_result = classifier.predict_mood(str(file_path)) # Get instruments instruments = classifier.predict_instruments(str(file_path)) # Transcode to MP3 128kbps for streaming logger.info(" → Transcoding to MP3 128kbps for streaming...") stream_path = transcoder.transcode_to_mp3( str(file_path), bitrate="128k", overwrite=False ) # Pre-compute waveform logger.info(" → Generating waveform...") waveform_dir = file_path.parent / "waveforms" waveform_dir.mkdir(parents=True, exist_ok=True) waveform_path = waveform_dir / f"{file_path.stem}.waveform.json" waveform_success = save_waveform_to_file( str(file_path), str(waveform_path), num_peaks=800 ) # Create track record track = AudioTrack( filepath=str(file_path), stream_filepath=stream_path, waveform_filepath=str(waveform_path) if waveform_success else None, filename=file_path.name, duration_seconds=features['duration_seconds'], tempo_bpm=features['tempo_bpm'], key=features['key'], time_signature=features['time_signature'], energy=features['energy'], danceability=features['danceability'], valence=features['valence'], loudness_lufs=features['loudness_lufs'], spectral_centroid=features['spectral_centroid'], zero_crossing_rate=features['zero_crossing_rate'], genre_primary=genre_result['primary'], genre_secondary=genre_result['secondary'], genre_confidence=genre_result['confidence'], mood_primary=mood_result['primary'], mood_secondary=mood_result['secondary'], mood_arousal=mood_result['arousal'], mood_valence=mood_result['valence'], instruments=[i['name'] for i in instruments[:5]], # Top 5 ) db.add(track) db.commit() logger.info(f"✓ Added to database: {file_path.name}") logger.info(f" Genre: {genre_result['primary']}, Mood: {mood_result['primary']}, " f"Tempo: {features['tempo_bpm']:.1f} BPM") logger.info(f" Stream: {stream_path}") logger.info(f" Waveform: {'✓' if waveform_success else '✗'}") return True except Exception as e: logger.error(f"Failed to process {file_path}: {e}") db.rollback() return False def main(): """Main scanner function.""" parser = argparse.ArgumentParser( description='Scan audio library and add tracks to database' ) parser.add_argument( 'directory', help='Directory to scan for audio files' ) parser.add_argument( '--workers', type=int, default=1, help='Number of parallel workers (default: 1)' ) args = parser.parse_args() # Find audio files audio_files = find_audio_files(args.directory) if not audio_files: logger.warning("No audio files found!") return # Initialize classifier logger.info("Initializing Essentia classifier...") classifier = EssentiaClassifier() # Initialize transcoder logger.info("Initializing audio transcoder...") transcoder = AudioTranscoder() # Check FFmpeg availability if not transcoder.check_ffmpeg_available(): logger.error("FFmpeg is required for transcoding. Please install FFmpeg and try again.") return # Process files db = SessionLocal() success_count = 0 error_count = 0 try: for i, file_path in enumerate(audio_files, 1): logger.info(f"[{i}/{len(audio_files)}] Processing...") if analyze_and_store(file_path, classifier, transcoder, db): success_count += 1 else: error_count += 1 finally: db.close() # Summary logger.info("") logger.info("=" * 60) logger.info(f"Scan complete!") logger.info(f" Total files: {len(audio_files)}") logger.info(f" Successfully processed: {success_count}") logger.info(f" Errors: {error_count}") logger.info("=" * 60) if __name__ == '__main__': main()