#!/usr/bin/env python3 """ Add Artist Labels to JSON Filenames Renames individual JSON files to include detected artist types in the filename. Based on the accent filename script but adapted for famous artists. """ import json import shutil from pathlib import Path import argparse import logging from typing import Dict, List def setup_logging(verbose: bool = False): """Setup logging configuration""" level = logging.DEBUG if verbose else logging.INFO logging.basicConfig(level=level, format="%(asctime)s - %(levelname)s - %(message)s") def get_artist_types_from_record(record: Dict) -> List[str]: """ Extract artist types from record's artist analysis Args: record: JSON record with _artist_analysis field Returns: List of detected artist types """ artist_analysis = record.get("_artist_analysis", {}) found_artists = artist_analysis.get("found_artists", {}) return list(found_artists.keys()) def format_artist_string(artists: List[str]) -> str: """ Format list of artists into a clean filename-safe string Args: artists: List of artist names Returns: Formatted artist string for filename """ if not artists: return "unknown_artist" # Clean and format artist names formatted_artists = [] for artist in artists: # Convert underscores to hyphens and use title case clean_artist = artist.replace("_", "-").title() # Handle special cases for readability clean_artist = clean_artist.replace("-The-", "-the-") clean_artist = clean_artist.replace("-And-", "-and-") clean_artist = clean_artist.replace("-Of-", "-of-") clean_artist = clean_artist.replace("-At-", "-at-") clean_artist = clean_artist.replace("-N-", "-n-") formatted_artists.append(clean_artist) # Join multiple artists with plus sign return "+".join(formatted_artists) def rename_files_with_artists( input_dir: str, output_dir: str = None, copy_mode: bool = True, dry_run: bool = False ) -> Dict[str, int]: """ Rename JSON files to include detected artist types Args: input_dir: Directory containing JSON files with artist analysis output_dir: Output directory (None = rename in place) copy_mode: If True, copy files; if False, move files dry_run: If True, show what would be done without actually doing it Returns: Dictionary with statistics """ input_path = Path(input_dir) if not input_path.exists(): raise FileNotFoundError(f"Input directory not found: {input_dir}") # Setup output directory if output_dir: output_path = Path(output_dir) output_path.mkdir(parents=True, exist_ok=True) else: output_path = input_path copy_mode = False # Must move if same directory logging.info(f"Processing files in: {input_dir}") logging.info(f"Output directory: {output_path}") logging.info(f"Mode: {'COPY' if copy_mode else 'MOVE'}") logging.info(f"Dry run: {dry_run}") # Statistics stats = {"processed": 0, "renamed": 0, "no_artists": 0, "errors": 0, "multiple_artists": 0} # Find all JSON files json_files = list(input_path.glob("*.json")) if not json_files: logging.warning(f"No JSON files found in {input_dir}") return stats logging.info(f"Found {len(json_files)} JSON files to process") for json_file in json_files: try: stats["processed"] += 1 # Read and parse JSON with open(json_file, "r") as f: record = json.load(f) # Extract artist types artists = get_artist_types_from_record(record) if not artists: stats["no_artists"] += 1 logging.debug(f"No artists found in: {json_file.name}") continue if len(artists) > 1: stats["multiple_artists"] += 1 # Format artist string artist_string = format_artist_string(artists) # Create new filename original_stem = json_file.stem # Remove existing prefix if present (e.g., "artist_test_001_") if original_stem.startswith("artist_test_"): parts = original_stem.split("_", 3) # ['artist', 'test', '001', 'record_id'] if len(parts) >= 4: clean_stem = parts[3] # Just the record ID index_part = parts[2] # The sequence number else: clean_stem = original_stem index_part = "000" else: clean_stem = original_stem index_part = f"{stats['processed']:03d}" # Build new filename with artist new_filename = f"artist_{index_part}_{artist_string}_{clean_stem}.json" new_file_path = output_path / new_filename # Log the operation operation = "COPY" if copy_mode else "MOVE" logging.info(f"{operation}: {json_file.name} -> {new_filename}") logging.debug(f" Detected artists: {', '.join(artists)}") if not dry_run: if copy_mode: shutil.copy2(json_file, new_file_path) else: shutil.move(str(json_file), str(new_file_path)) stats["renamed"] += 1 except Exception as e: stats["errors"] += 1 logging.error(f"Error processing {json_file.name}: {e}") continue # Print summary logging.info("\n" + "=" * 50) logging.info("RENAMING SUMMARY") logging.info("=" * 50) logging.info(f"Files processed: {stats['processed']}") logging.info(f"Files renamed: {stats['renamed']}") logging.info(f"Files with no artists: {stats['no_artists']}") logging.info(f"Files with multiple artists: {stats['multiple_artists']}") logging.info(f"Errors: {stats['errors']}") return stats def main(): parser = argparse.ArgumentParser( description="Rename JSON files to include detected artist types in filename", formatter_class=argparse.RawDescriptionHelpFormatter, epilog=""" Examples: # Rename files in place (move) python add_artists_to_filenames.py --input-dir /path/to/artist_files/ # Copy files to new directory with artist names python add_artists_to_filenames.py --input-dir /path/to/source/ --output-dir /path/to/renamed/ --copy # Preview what would be done (dry run) python add_artists_to_filenames.py --input-dir /path/to/files/ --dry-run Filename Format: artist_001_Taylor-Swift_record_id.json artist_002_Kanye-West+Eminem_record_id.json (multiple artists) artist_003_Ariana-Grande_record_id.json """, ) parser.add_argument( "--input-dir", "-i", type=str, required=True, help="Directory containing JSON files with artist analysis", ) parser.add_argument( "--output-dir", "-o", type=str, default=None, help="Output directory for renamed files (default: rename in place)", ) parser.add_argument( "--copy", action="store_true", help="Copy files instead of moving them (default: move)" ) parser.add_argument( "--dry-run", action="store_true", help="Show what would be done without actually doing it" ) parser.add_argument("--verbose", "-v", action="store_true", help="Verbose logging") args = parser.parse_args() setup_logging(args.verbose) try: stats = rename_files_with_artists( input_dir=args.input_dir, output_dir=args.output_dir, copy_mode=args.copy, dry_run=args.dry_run, ) if args.dry_run: print(f"\nšŸ” DRY RUN COMPLETE - No files were actually modified") else: print(f"\nāœ… Successfully processed {stats['processed']} files") print(f"šŸ“ Renamed {stats['renamed']} files with artist labels") if stats["errors"] > 0: print(f"āš ļø {stats['errors']} errors occurred") return 1 return 0 except Exception as e: logging.error(f"Script failed: {e}") return 1 if __name__ == "__main__": exit(main())