feat: Implement senior audio injection with 5 fallback methods

- Add _cmd_create_arrangement_audio_pattern with 5-method fallback chain
- Method 1: track.insert_arrangement_clip() [Live 12+]
- Method 2: track.create_audio_clip() [Live 11+]
- Method 3: arrangement_clips.add_new_clip() [Live 12+]
- Method 4: Session->duplicate_clip_to_arrangement [Legacy]
- Method 5: Session->Recording [Universal]

- Add _cmd_duplicate_clip_to_arrangement for session-to-arrangement workflow
- Update skills documentation
- Verified: 3 clips created at positions [0, 4, 8] in Arrangement View

Closes: Audio injection in Arrangement View
This commit is contained in:
OpenCode Agent
2026-04-12 14:02:32 -03:00
commit 5ce8187c65
118 changed files with 55075 additions and 0 deletions

View File

@@ -0,0 +1,899 @@
"""
Batch Migration Script for Sample Library
Scans the libreria/reggaeton/ directory, analyzes all audio files,
and stores metadata in SQLite database with progress tracking.
Usage:
python migrate_library.py # Run migration with defaults
python migrate_library.py --force # Force re-analyze all samples
python migrate_library.py --dry-run # Scan only, don't save to DB
python migrate_library.py --status # Show current DB statistics
"""
import os
import sys
import sqlite3
import argparse
from pathlib import Path
from dataclasses import dataclass, asdict
from typing import List, Dict, Optional, Any, Tuple
from datetime import datetime
# Audio analysis libraries (optional)
try:
import numpy as np
import librosa
import librosa.feature
LIBROSA_AVAILABLE = True
except ImportError:
LIBROSA_AVAILABLE = False
np = None
try:
import wave
import struct
WAVE_AVAILABLE = True
except ImportError:
WAVE_AVAILABLE = False
# Constants
DEFAULT_LIBRARY_PATH = Path(
r"C:\ProgramData\Ableton\Live 12 Suite\Resources\MIDI Remote Scripts\libreria\reggaeton"
)
DEFAULT_DB_PATH = Path(
r"C:\ProgramData\Ableton\Live 12 Suite\Resources\MIDI Remote Scripts\AbletonMCP_AI\mcp_server\data\samples.db"
)
SUPPORTED_EXTENSIONS = {'.wav', '.aif', '.aiff', '.mp3', '.flac'}
# Role mapping for categorization
ROLE_MAPPING = {
'kick': 'kick',
'snare': 'snare',
'bass': 'bass',
'fx': 'fx',
'drumloops': 'drum_loop',
'drumloop': 'drum_loop',
'hi-hat': 'hat_closed',
'hihat': 'hat_closed',
'hat': 'hat_closed',
'oneshots': 'oneshot',
'oneshot': 'oneshot',
'perc loop': 'perc_loop',
'perc_loop': 'perc_loop',
'reggaeton 3': 'synth',
'sentimientolatino2025': 'multi',
'sounds presets': 'preset',
'extra': 'extra',
'flp': 'project',
}
@dataclass
class SampleFeatures:
"""Complete feature set for a sample."""
# File info
path: str
name: str
pack: str
role: str
# Audio properties
duration: float = 0.0
sample_rate: int = 44100
channels: int = 1
# Musical properties
bpm: float = 0.0
key: str = ""
# Spectral features
rms: float = 0.0
spectral_centroid: float = 0.0
spectral_rolloff: float = 0.0
zero_crossing_rate: float = 0.0
# Advanced features
mfccs: str = "" # JSON string of list
onset_strength: float = 0.0
# Analysis metadata
analysis_type: str = "partial" # "full" or "partial"
analyzed_at: str = ""
file_size: int = 0
file_modified: float = 0.0
def scan_library(library_path: Path) -> List[Path]:
"""
Scan library directory for all audio files.
Args:
library_path: Root directory to scan
Returns:
List of paths to audio files
"""
samples = []
if not library_path.exists():
print(f"[ERROR] Library path not found: {library_path}")
return samples
for ext in SUPPORTED_EXTENSIONS:
samples.extend(library_path.rglob(f"*{ext}"))
samples.extend(library_path.rglob(f"*{ext.upper()}"))
# Remove duplicates and sort
seen = set()
unique_samples = []
for s in samples:
resolved = s.resolve()
if resolved not in seen:
seen.add(resolved)
unique_samples.append(s)
return sorted(unique_samples)
def detect_role(file_path: Path) -> str:
"""Detect sample role based on folder and filename."""
path_parts = [p.lower() for p in file_path.parts]
filename = file_path.name.lower()
for part in path_parts:
clean_part = part.replace(' ', '_').replace('-', '_').replace('(', '').replace(')', '')
if part in ROLE_MAPPING:
return ROLE_MAPPING[part]
if clean_part in ROLE_MAPPING:
return ROLE_MAPPING[clean_part]
for key, role in ROLE_MAPPING.items():
if key in part or key in clean_part:
return role
# Check filename
if 'kick' in filename:
return 'kick'
if 'snare' in filename:
return 'snare'
if 'clap' in filename:
return 'clap'
if 'hat' in filename or 'hihat' in filename:
return 'hat_closed'
if 'bass' in filename:
return 'bass'
if 'fx' in filename:
return 'fx'
if 'perc' in filename:
return 'perc'
return 'unknown'
def get_pack_name(file_path: Path, library_path: Path) -> str:
"""Get the pack/folder name relative to library root."""
try:
rel_path = file_path.relative_to(library_path)
return rel_path.parts[0] if rel_path.parts else 'root'
except ValueError:
return file_path.parent.name or 'unknown'
def analyze_sample_librosa(sample_path: Path) -> Optional[Dict[str, Any]]:
"""
Analyze sample using librosa (full analysis).
Args:
sample_path: Path to audio file
Returns:
Dictionary with audio features or None on error
"""
if not LIBROSA_AVAILABLE:
return None
try:
# Load audio
y, sr = librosa.load(str(sample_path), sr=None, mono=True)
# Duration
duration = librosa.get_duration(y=y, sr=sr)
# RMS (energy)
rms = float(np.mean(librosa.feature.rms(y=y)))
rms_db = 20 * np.log10(rms + 1e-10)
# Spectral features
spectral_centroid = float(np.mean(librosa.feature.spectral_centroid(y=y, sr=sr)))
spectral_rolloff = float(np.mean(librosa.feature.spectral_rolloff(y=y, sr=sr)))
zcr = float(np.mean(librosa.feature.zero_crossing_rate(y)))
# MFCCs
mfccs = librosa.feature.mfcc(y=y, sr=sr, n_mfcc=13)
mfccs_mean = [float(np.mean(coef)) for coef in mfccs]
# Onset strength
onset_env = librosa.onset.onset_strength(y=y, sr=sr)
onset_strength = float(np.mean(onset_env))
# BPM detection
try:
tempo, _ = librosa.beat.beat_track(y=y, sr=sr)
bpm = float(tempo) if isinstance(tempo, (int, float, np.number)) else float(tempo[0])
except:
bpm = 0.0
# Key detection
try:
chromagram = librosa.feature.chroma_cqt(y=y, sr=sr)
chroma_avg = np.sum(chromagram, axis=1)
notes = ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B']
key_index = np.argmax(chroma_avg)
key = notes[key_index]
# Detect minor
minor_third_idx = (key_index + 3) % 12
if chroma_avg[minor_third_idx] > chroma_avg[(key_index + 4) % 12]:
key += 'm'
except:
key = ""
# Detect original channels
try:
y_orig, _ = librosa.load(str(sample_path), sr=None, mono=False)
channels = y_orig.shape[0] if len(y_orig.shape) > 1 else 1
except:
channels = 1
return {
"rms": round(rms_db, 2),
"spectral_centroid": round(spectral_centroid, 2),
"spectral_rolloff": round(spectral_rolloff, 2),
"zero_crossing_rate": round(zcr, 4),
"mfccs": mfccs_mean,
"onset_strength": round(onset_strength, 4),
"duration": round(duration, 3),
"sample_rate": sr,
"channels": channels,
"bpm": round(bpm, 1) if bpm > 0 else 0,
"key": key,
"analysis_type": "full"
}
except Exception as e:
print(f" [WARN] Librosa analysis failed for {sample_path.name}: {e}")
return None
def analyze_sample_wave(sample_path: Path) -> Optional[Dict[str, Any]]:
"""
Analyze sample using wave module (basic info for WAV files).
Args:
sample_path: Path to audio file
Returns:
Dictionary with basic audio features or None on error
"""
if not WAVE_AVAILABLE:
return None
try:
# Only works for WAV files
if sample_path.suffix.lower() != '.wav':
return None
with wave.open(str(sample_path), 'rb') as wav_file:
channels = wav_file.getnchannels()
sample_rate = wav_file.getframerate()
sample_width = wav_file.getsampwidth()
n_frames = wav_file.getnframes()
duration = n_frames / sample_rate
# Try to calculate RMS from samples
rms_db = 0.0
try:
# Read a portion of the file for RMS calculation
frames_to_read = min(n_frames, int(sample_rate * 1)) # Max 1 second
raw_data = wav_file.readframes(frames_to_read)
if sample_width == 1:
fmt = f"{len(raw_data)}B"
samples = struct.unpack(fmt, raw_data)
samples = [(s - 128) / 128.0 for s in samples]
elif sample_width == 2:
fmt = f"{len(raw_data) // 2}h"
samples = struct.unpack(fmt, raw_data)
samples = [s / 32768.0 for s in samples]
elif sample_width == 4:
fmt = f"{len(raw_data) // 4}i"
samples = struct.unpack(fmt, raw_data)
samples = [s / 2147483648.0 for s in samples]
else:
samples = []
if samples:
# Calculate RMS
if channels > 1:
# Interleaved channels - convert to mono
mono_samples = []
for i in range(0, len(samples) - channels + 1, channels):
mono_samples.append(sum(samples[i:i+channels]) / channels)
samples = mono_samples
rms = (sum(s**2 for s in samples) / len(samples)) ** 0.5
rms_db = 20 * (rms + 1e-10).bit_length() # Approximate
except Exception:
pass
return {
"rms": round(rms_db, 2),
"spectral_centroid": 0.0,
"spectral_rolloff": 0.0,
"zero_crossing_rate": 0.0,
"mfccs": [],
"onset_strength": 0.0,
"duration": round(duration, 3),
"sample_rate": sample_rate,
"channels": channels,
"bpm": 0,
"key": "",
"analysis_type": "partial"
}
except Exception as e:
return None
def create_placeholder_metadata(sample_path: Path) -> Dict[str, Any]:
"""
Create basic metadata without audio analysis (fallback).
Args:
sample_path: Path to audio file
Returns:
Dictionary with file info and placeholder audio features
"""
# Try wave module first
wave_data = analyze_sample_wave(sample_path)
if wave_data:
return wave_data
# Ultimate fallback - just file info
stat = sample_path.stat()
return {
"rms": 0.0,
"spectral_centroid": 0.0,
"spectral_rolloff": 0.0,
"zero_crossing_rate": 0.0,
"mfccs": [],
"onset_strength": 0.0,
"duration": 0.0,
"sample_rate": 44100,
"channels": 1,
"bpm": 0,
"key": "",
"analysis_type": "partial"
}
def analyze_sample(sample_path: Path, library_path: Path) -> Optional[SampleFeatures]:
"""
Analyze a sample and return complete features.
Tries librosa first, falls back to wave module, then placeholder.
Args:
sample_path: Path to audio file
library_path: Root library path for pack detection
Returns:
SampleFeatures object or None on error
"""
# Get file info
stat = sample_path.stat()
# Detect role and pack
role = detect_role(sample_path)
pack = get_pack_name(sample_path, library_path)
# Try analysis methods in order of preference
audio_features = None
if LIBROSA_AVAILABLE:
audio_features = analyze_sample_librosa(sample_path)
if audio_features is None:
audio_features = create_placeholder_metadata(sample_path)
if audio_features is None:
return None
# Build SampleFeatures
return SampleFeatures(
path=str(sample_path.resolve()),
name=sample_path.name,
pack=pack,
role=role,
duration=audio_features.get("duration", 0.0),
sample_rate=audio_features.get("sample_rate", 44100),
channels=audio_features.get("channels", 1),
bpm=audio_features.get("bpm", 0.0),
key=audio_features.get("key", ""),
rms=audio_features.get("rms", 0.0),
spectral_centroid=audio_features.get("spectral_centroid", 0.0),
spectral_rolloff=audio_features.get("spectral_rolloff", 0.0),
zero_crossing_rate=audio_features.get("zero_crossing_rate", 0.0),
mfccs=str(audio_features.get("mfccs", [])),
onset_strength=audio_features.get("onset_strength", 0.0),
analysis_type=audio_features.get("analysis_type", "partial"),
analyzed_at=datetime.now().isoformat(),
file_size=stat.st_size,
file_modified=stat.st_mtime
)
def init_database(db_path: Path) -> sqlite3.Connection:
"""
Initialize SQLite database with schema.
Args:
db_path: Path to database file
Returns:
Database connection
"""
# Ensure directory exists
db_path.parent.mkdir(parents=True, exist_ok=True)
conn = sqlite3.connect(str(db_path))
cursor = conn.cursor()
# Create samples table
cursor.execute("""
CREATE TABLE IF NOT EXISTS samples (
id INTEGER PRIMARY KEY AUTOINCREMENT,
path TEXT UNIQUE NOT NULL,
name TEXT NOT NULL,
pack TEXT,
role TEXT,
duration REAL DEFAULT 0.0,
sample_rate INTEGER DEFAULT 44100,
channels INTEGER DEFAULT 1,
bpm REAL DEFAULT 0.0,
key TEXT,
rms REAL DEFAULT 0.0,
spectral_centroid REAL DEFAULT 0.0,
spectral_rolloff REAL DEFAULT 0.0,
zero_crossing_rate REAL DEFAULT 0.0,
mfccs TEXT,
onset_strength REAL DEFAULT 0.0,
analysis_type TEXT DEFAULT 'partial',
analyzed_at TEXT,
file_size INTEGER DEFAULT 0,
file_modified REAL DEFAULT 0.0,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
""")
# Create indexes
cursor.execute("CREATE INDEX IF NOT EXISTS idx_role ON samples(role)")
cursor.execute("CREATE INDEX IF NOT EXISTS idx_pack ON samples(pack)")
cursor.execute("CREATE INDEX IF NOT EXISTS idx_key ON samples(key)")
cursor.execute("CREATE INDEX IF NOT EXISTS idx_bpm ON samples(bpm)")
cursor.execute("CREATE INDEX IF NOT EXISTS idx_analysis ON samples(analysis_type)")
# Create migration log table
cursor.execute("""
CREATE TABLE IF NOT EXISTS migration_log (
id INTEGER PRIMARY KEY AUTOINCREMENT,
started_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
completed_at TIMESTAMP,
total_samples INTEGER DEFAULT 0,
analyzed_full INTEGER DEFAULT 0,
analyzed_partial INTEGER DEFAULT 0,
errors INTEGER DEFAULT 0,
duration_seconds REAL DEFAULT 0.0
)
""")
conn.commit()
return conn
def sample_exists(conn: sqlite3.Connection, sample_path: str) -> bool:
"""Check if a sample already exists in database."""
cursor = conn.cursor()
cursor.execute("SELECT 1 FROM samples WHERE path = ?", (sample_path,))
return cursor.fetchone() is not None
def save_sample(conn: sqlite3.Connection, features: SampleFeatures) -> bool:
"""
Save or update sample features in database.
Args:
conn: Database connection
features: SampleFeatures to save
Returns:
True on success
"""
cursor = conn.cursor()
data = asdict(features)
cursor.execute("""
INSERT OR REPLACE INTO samples (
path, name, pack, role, duration, sample_rate, channels,
bpm, key, rms, spectral_centroid, spectral_rolloff,
zero_crossing_rate, mfccs, onset_strength, analysis_type,
analyzed_at, file_size, file_modified
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""", (
data['path'], data['name'], data['pack'], data['role'],
data['duration'], data['sample_rate'], data['channels'],
data['bpm'], data['key'], data['rms'], data['spectral_centroid'],
data['spectral_rolloff'], data['zero_crossing_rate'], data['mfccs'],
data['onset_strength'], data['analysis_type'], data['analyzed_at'],
data['file_size'], data['file_modified']
))
conn.commit()
return True
def migrate_library(
library_path: Path,
db_path: Path,
force_reanalyze: bool = False,
dry_run: bool = False
) -> Dict[str, Any]:
"""
Migrate all samples from library to SQLite database.
Args:
library_path: Path to sample library
db_path: Path to SQLite database
force_reanalyze: Re-analyze samples even if already in DB
dry_run: Scan only, don't save to database
Returns:
Migration statistics
"""
start_time = datetime.now()
# Scan for samples
print(f"[MIGRATE] Scanning library: {library_path}")
samples = scan_library(library_path)
total = len(samples)
if total == 0:
print("[MIGRATE] No samples found!")
return {"total": 0, "analyzed": 0, "errors": 0, "skipped": 0}
print(f"[MIGRATE] Found {total} samples")
if dry_run:
print("[MIGRATE] Dry run - not saving to database")
for i, sample in enumerate(samples, 1):
print(f" {i}/{total}: {sample.name}")
return {"total": total, "dry_run": True}
# Initialize database
conn = init_database(db_path)
# Start migration log
cursor = conn.cursor()
cursor.execute("INSERT INTO migration_log (started_at) VALUES (CURRENT_TIMESTAMP)")
migration_id = cursor.lastrowid
conn.commit()
# Process samples
analyzed_full = 0
analyzed_partial = 0
errors = 0
skipped = 0
for i, sample_path in enumerate(samples, 1):
abs_path = str(sample_path.resolve())
# Check if already analyzed
if not force_reanalyze and sample_exists(conn, abs_path):
skipped += 1
print(f"\r[MIGRATE] {i}/{total}: {sample_path.name} (skipped - already in DB)", end="")
continue
print(f"\r[MIGRATE] {i}/{total}: {sample_path.name}", end="")
sys.stdout.flush()
try:
features = analyze_sample(sample_path, library_path)
if features:
save_sample(conn, features)
if features.analysis_type == "full":
analyzed_full += 1
else:
analyzed_partial += 1
else:
errors += 1
print(f"\n [ERROR] Failed to analyze: {sample_path.name}")
except Exception as e:
errors += 1
print(f"\n [ERROR] Exception analyzing {sample_path.name}: {e}")
print() # New line after progress
# Update migration log
duration = (datetime.now() - start_time).total_seconds()
cursor.execute("""
UPDATE migration_log
SET completed_at = CURRENT_TIMESTAMP,
total_samples = ?,
analyzed_full = ?,
analyzed_partial = ?,
errors = ?,
duration_seconds = ?
WHERE id = ?
""", (total, analyzed_full, analyzed_partial, errors, duration, migration_id))
conn.commit()
conn.close()
return {
"total": total,
"analyzed_full": analyzed_full,
"analyzed_partial": analyzed_partial,
"errors": errors,
"skipped": skipped,
"duration_seconds": duration,
"db_path": str(db_path)
}
def get_migration_status(db_path: Path) -> Dict[str, Any]:
"""
Get current database statistics.
Args:
db_path: Path to SQLite database
Returns:
Statistics dictionary
"""
if not db_path.exists():
return {"error": "Database not found", "db_path": str(db_path)}
conn = sqlite3.connect(str(db_path))
cursor = conn.cursor()
# Total samples
cursor.execute("SELECT COUNT(*) FROM samples")
total = cursor.fetchone()[0]
# By role
cursor.execute("SELECT role, COUNT(*) FROM samples GROUP BY role")
by_role = {row[0]: row[1] for row in cursor.fetchall()}
# By analysis type
cursor.execute("SELECT analysis_type, COUNT(*) FROM samples GROUP BY analysis_type")
by_analysis = {row[0]: row[1] for row in cursor.fetchall()}
# By pack
cursor.execute("SELECT pack, COUNT(*) FROM samples GROUP BY pack")
by_pack = {row[0]: row[1] for row in cursor.fetchall()}
# Averages
cursor.execute("""
SELECT
AVG(duration),
AVG(bpm),
AVG(rms),
AVG(spectral_centroid)
FROM samples
""")
avg_row = cursor.fetchone()
# Last migration
cursor.execute("""
SELECT started_at, completed_at, total_samples, errors, duration_seconds
FROM migration_log
ORDER BY id DESC
LIMIT 1
""")
last_migration = cursor.fetchone()
conn.close()
return {
"total_samples": total,
"by_role": by_role,
"by_analysis_type": by_analysis,
"by_pack": by_pack,
"averages": {
"duration": round(avg_row[0], 3) if avg_row[0] else 0,
"bpm": round(avg_row[1], 1) if avg_row[1] else 0,
"rms": round(avg_row[2], 2) if avg_row[2] else 0,
"spectral_centroid": round(avg_row[3], 2) if avg_row[3] else 0,
},
"last_migration": {
"started": last_migration[0] if last_migration else None,
"completed": last_migration[1] if last_migration else None,
"total_samples": last_migration[2] if last_migration else 0,
"errors": last_migration[3] if last_migration else 0,
"duration_seconds": last_migration[4] if last_migration else 0,
} if last_migration else None,
"db_path": str(db_path),
"db_size_mb": round(db_path.stat().st_size / (1024 * 1024), 2)
}
def print_report(stats: Dict[str, Any]):
"""Print formatted migration report."""
print("\n" + "=" * 60)
print("MIGRATION REPORT")
print("=" * 60)
if "error" in stats:
print(f"Error: {stats['error']}")
return
print(f"\nTotal samples: {stats['total']}")
if stats.get('dry_run'):
print("Mode: Dry run (no changes saved)")
return
print(f"Full analysis: {stats.get('analyzed_full', 0)}")
print(f"Partial analysis: {stats.get('analyzed_partial', 0)}")
print(f"Skipped (already in DB): {stats.get('skipped', 0)}")
print(f"Errors: {stats.get('errors', 0)}")
print(f"Duration: {stats.get('duration_seconds', 0):.1f} seconds")
print(f"Database: {stats.get('db_path', 'N/A')}")
print("\n" + "=" * 60)
def print_status(status: Dict[str, Any]):
"""Print database status report."""
print("\n" + "=" * 60)
print("DATABASE STATUS")
print("=" * 60)
if "error" in status:
print(f"Error: {status['error']}")
return
print(f"\nTotal samples: {status['total_samples']}")
print(f"Database size: {status['db_size_mb']} MB")
print(f"Database path: {status['db_path']}")
print("\nBy Role:")
for role, count in sorted(status['by_role'].items()):
print(f" {role}: {count}")
print("\nBy Analysis Type:")
for atype, count in status['by_analysis_type'].items():
print(f" {atype}: {count}")
print("\nAverages:")
avg = status['averages']
print(f" Duration: {avg['duration']}s")
print(f" BPM: {avg['bpm']}")
print(f" RMS: {avg['rms']} dB")
print(f" Spectral Centroid: {avg['spectral_centroid']} Hz")
if status.get('last_migration'):
lm = status['last_migration']
print(f"\nLast Migration:")
print(f" Started: {lm['started']}")
print(f" Completed: {lm['completed']}")
print(f" Samples: {lm['total_samples']}")
print(f" Errors: {lm['errors']}")
print(f" Duration: {lm['duration_seconds']:.1f}s")
print("\n" + "=" * 60)
def main():
"""Command-line interface for migration script."""
parser = argparse.ArgumentParser(
description="Migrate sample library to SQLite database",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Examples:
python migrate_library.py # Run migration
python migrate_library.py --force # Force re-analyze all
python migrate_library.py --dry-run # Scan only
python migrate_library.py --status # Show database stats
"""
)
parser.add_argument(
"--library",
type=str,
default=str(DEFAULT_LIBRARY_PATH),
help=f"Path to sample library (default: {DEFAULT_LIBRARY_PATH})"
)
parser.add_argument(
"--db",
type=str,
default=str(DEFAULT_DB_PATH),
help=f"Path to SQLite database (default: {DEFAULT_DB_PATH})"
)
parser.add_argument(
"--force",
action="store_true",
help="Force re-analysis of all samples"
)
parser.add_argument(
"--dry-run",
action="store_true",
help="Scan only, don't save to database"
)
parser.add_argument(
"--status",
action="store_true",
help="Show database status and exit"
)
parser.add_argument(
"--reset",
action="store_true",
help="Delete database and start fresh"
)
args = parser.parse_args()
library_path = Path(args.library)
db_path = Path(args.db)
# Handle reset
if args.reset:
if db_path.exists():
print(f"[RESET] Deleting database: {db_path}")
db_path.unlink()
else:
print("[RESET] Database does not exist")
# Show status
if args.status:
status = get_migration_status(db_path)
print_status(status)
return
# Run migration
print(f"[MIGRATE] Library: {library_path}")
print(f"[MIGRATE] Database: {db_path}")
print(f"[MIGRATE] Librosa available: {LIBROSA_AVAILABLE}")
stats = migrate_library(
library_path=library_path,
db_path=db_path,
force_reanalyze=args.force,
dry_run=args.dry_run
)
print_report(stats)
# Show final status
if not args.dry_run:
status = get_migration_status(db_path)
print_status(status)
if __name__ == "__main__":
main()