#!/usr/bin/env python3 """ Fix artist name formatting for Let's Sing Karaoke channel. This script specifically targets the "Last Name, First Name" format and converts it to "First Name Last Name" format in ID3 tags. It only processes entries where there is exactly one comma followed by exactly 2 words, to avoid affecting multi-artist entries. Usage: python fix_artist_name_format_simple.py --preview # Show what would be changed python fix_artist_name_format_simple.py --apply # Actually make the changes python fix_artist_name_format_simple.py --external "D:\Karaoke\Karaoke\MP4\Let's Sing Karaoke" # Use external directory """ import json import os import re import shutil import argparse from pathlib import Path from typing import Dict, List, Tuple, Optional # Try to import mutagen for ID3 tag manipulation try: from mutagen.mp4 import MP4 MUTAGEN_AVAILABLE = True except ImportError: MUTAGEN_AVAILABLE = False print("WARNING: mutagen not available - install with: pip install mutagen") def is_lastname_firstname_format(artist_name: str) -> bool: """ Check if artist name is in "Last Name, First Name" format. Args: artist_name: The artist name to check Returns: True if the name matches "Last Name, First Name" format with exactly 1 or 2 words after comma """ if ',' not in artist_name: return False # Split by comma parts = artist_name.split(',', 1) if len(parts) != 2: return False last_name = parts[0].strip() first_name_part = parts[1].strip() # Check if there are exactly 1 or 2 words after the comma words_after_comma = first_name_part.split() if len(words_after_comma) not in [1, 2]: return False # Additional check: make sure it's not a multi-artist entry # If there are more than 4 words total in the artist name, it might be multi-artist total_words = len(artist_name.split()) if total_words > 4: # Last, First Name (4 words max for single artist) return False return True def convert_lastname_firstname(artist_name: str) -> str: """ Convert "Last Name, First Name" to "First Name Last Name". Args: artist_name: The artist name to convert Returns: The converted artist name """ if ',' not in artist_name: return artist_name parts = artist_name.split(',', 1) if len(parts) != 2: return artist_name last_name = parts[0].strip() first_name = parts[1].strip() return f"{first_name} {last_name}" def process_artist_name(artist_name: str) -> str: """ Process an artist name, handling both single artists and multiple artists separated by "&". Args: artist_name: The artist name to process Returns: The processed artist name """ if '&' in artist_name: # Split by "&" and process each artist individually artists = [artist.strip() for artist in artist_name.split('&')] processed_artists = [] for artist in artists: if is_lastname_firstname_format(artist): processed_artist = convert_lastname_firstname(artist) processed_artists.append(processed_artist) else: processed_artists.append(artist) # Rejoin with "&" return ' & '.join(processed_artists) else: # Single artist if is_lastname_firstname_format(artist_name): return convert_lastname_firstname(artist_name) else: return artist_name def update_id3_tags(file_path: str, new_artist: str, apply_changes: bool = False) -> bool: """ Update the ID3 tags in an MP4 file. Args: file_path: Path to the MP4 file new_artist: New artist name to set apply_changes: Whether to actually apply changes or just preview Returns: True if successful, False otherwise """ if not MUTAGEN_AVAILABLE: print(f"WARNING: mutagen not available - cannot update ID3 tags for {file_path}") return False try: mp4 = MP4(file_path) if apply_changes: # Update the artist tag mp4["\xa9ART"] = new_artist mp4.save() print(f"UPDATED ID3 tag: {os.path.basename(file_path)} -> Artist: '{new_artist}'") else: # Just preview what would be changed current_artist = mp4.get("\xa9ART", ["Unknown"])[0] if "\xa9ART" in mp4 else "Unknown" print(f"WOULD UPDATE ID3 tag: {os.path.basename(file_path)} -> Artist: '{current_artist}' -> '{new_artist}'") return True except Exception as e: print(f"ERROR: Failed to update ID3 tags for {file_path}: {e}") return False def scan_external_directory(directory_path: str, debug: bool = False) -> List[Dict]: """ Scan external directory for MP4 files with "Last Name, First Name" format in ID3 tags. Args: directory_path: Path to the external directory debug: Whether to show debug information Returns: List of files that need ID3 tag updates """ if not os.path.exists(directory_path): print(f"ERROR: Directory not found: {directory_path}") return [] if not MUTAGEN_AVAILABLE: print("ERROR: mutagen not available - cannot scan ID3 tags") return [] files_to_update = [] total_files = 0 files_with_artist_tags = 0 # Scan for MP4 files for file_path in Path(directory_path).glob("*.mp4"): total_files += 1 try: mp4 = MP4(str(file_path)) current_artist = mp4.get("\xa9ART", ["Unknown"])[0] if "\xa9ART" in mp4 else "Unknown" if current_artist != "Unknown": files_with_artist_tags += 1 if debug: print(f"DEBUG: {file_path.name} -> Artist: '{current_artist}'") # Process the artist name to handle multiple artists processed_artist = process_artist_name(current_artist) if processed_artist != current_artist: files_to_update.append({ 'file_path': str(file_path), 'filename': file_path.name, 'old_artist': current_artist, 'new_artist': processed_artist }) if debug: print(f"DEBUG: MATCH FOUND - {file_path.name}: '{current_artist}' -> '{processed_artist}'") except Exception as e: if debug: print(f"WARNING: Could not read ID3 tags from {file_path.name}: {e}") print(f"INFO: Scanned {total_files} MP4 files, {files_with_artist_tags} had artist tags, {len(files_to_update)} need updates") return files_to_update def update_id3_tags_for_files(files_to_update: List[Dict], apply_changes: bool = False) -> int: """ Update ID3 tags for a list of files. Args: files_to_update: List of files to update apply_changes: Whether to actually apply changes or just preview Returns: Number of files successfully updated """ updated_count = 0 for file_info in files_to_update: file_path = file_info['file_path'] new_artist = file_info['new_artist'] if update_id3_tags(file_path, new_artist, apply_changes): updated_count += 1 return updated_count def main(): """Main function to run the artist name fix script.""" parser = argparse.ArgumentParser(description="Fix artist name formatting in ID3 tags for Let's Sing Karaoke") parser.add_argument('--preview', action='store_true', help='Show what would be changed without making changes') parser.add_argument('--apply', action='store_true', help='Actually apply the changes') parser.add_argument('--external', type=str, help='Path to external karaoke directory') parser.add_argument('--debug', action='store_true', help='Show debug information') args = parser.parse_args() # Default to preview mode if no action specified if not args.preview and not args.apply: args.preview = True print("Artist Name Format Fix Script (ID3 Tags Only)") print("=" * 60) print("This script will fix 'Last Name, First Name' format to 'First Name Last Name'") print("Only targeting Let's Sing Karaoke channel to avoid affecting other channels.") print("Focusing on ID3 tags only - filenames will not be changed.") print() if not MUTAGEN_AVAILABLE: print("ERROR: mutagen library not available!") print("Please install it with: pip install mutagen") return if args.preview: print("PREVIEW MODE - No changes will be made") else: print("APPLY MODE - Changes will be made") print() # Process external directory if specified if args.external: print(f"Scanning external directory: {args.external}") external_files = scan_external_directory(args.external, debug=args.debug) if external_files: print(f"\nFound {len(external_files)} files with 'Last Name, First Name' format in ID3 tags:") for file_info in external_files: print(f" * {file_info['filename']}: '{file_info['old_artist']}' -> '{file_info['new_artist']}'") if args.apply: print(f"\nUpdating ID3 tags in external files...") updated_count = update_id3_tags_for_files(external_files, apply_changes=True) print(f"SUCCESS: Updated ID3 tags in {updated_count} external files") else: print(f"\nWould update ID3 tags in {len(external_files)} external files") else: print("SUCCESS: No files with 'Last Name, First Name' format found in ID3 tags") print("\n" + "=" * 60) print("Summary complete.") if __name__ == "__main__": main()