From d18ac54476a438d8906ae3b89b90f4abd4dc43be Mon Sep 17 00:00:00 2001 From: mbrucedogs Date: Mon, 28 Jul 2025 14:09:07 -0500 Subject: [PATCH] Signed-off-by: mbrucedogs --- fix_artist_name_format.py | 465 +++++++++++++++++++++++++++++++ fix_artist_name_format_simple.py | 295 ++++++++++++++++++++ 2 files changed, 760 insertions(+) create mode 100644 fix_artist_name_format.py create mode 100644 fix_artist_name_format_simple.py diff --git a/fix_artist_name_format.py b/fix_artist_name_format.py new file mode 100644 index 0000000..9e6b586 --- /dev/null +++ b/fix_artist_name_format.py @@ -0,0 +1,465 @@ +#!/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.py --preview # Show what would be changed + python fix_artist_name_format.py --apply # Actually make the changes + python fix_artist_name_format.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("āš ļø 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 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 2 words after the comma + words_after_comma = first_name_part.split() + if len(words_after_comma) != 2: + return False + + # Additional check: make sure it's not a multi-artist entry + # If there are more than 2 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_to_firstname_lastname(artist_name: str) -> str: + """ + Convert "Last Name, First Name" to "First Name Last Name". + + Args: + artist_name: Artist name in "Last Name, First Name" format + + Returns: + Artist name in "First Name Last Name" format + """ + parts = artist_name.split(',', 1) + last_name = parts[0].strip() + first_name_part = parts[1].strip() + + # Split the first name part into words + words = first_name_part.split() + if len(words) == 2: + first_name = words[0] + middle_name = words[1] + return f"{first_name} {middle_name} {last_name}" + else: + # Fallback - just reverse the parts + return f"{first_name_part} {last_name}" + + +def extract_artist_title_from_filename(filename: str) -> Tuple[str, str]: + """ + Extract artist and title from a filename. + + Args: + filename: MP4 filename (without extension) + + Returns: + Tuple of (artist, title) + """ + # Remove .mp4 extension + if filename.endswith('.mp4'): + filename = filename[:-4] + + # Look for " - " separator + if " - " in filename: + parts = filename.split(" - ", 1) + return parts[0].strip(), parts[1].strip() + + return "", filename + + +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"āš ļø 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"āŒ Failed to update ID3 tags for {file_path}: {e}") + return False + + +def scan_external_directory(directory_path: str) -> 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 + + Returns: + List of files that need ID3 tag updates + """ + if not os.path.exists(directory_path): + print(f"āŒ Directory not found: {directory_path}") + return [] + + if not MUTAGEN_AVAILABLE: + print("āŒ mutagen not available - cannot scan ID3 tags") + return [] + + files_to_update = [] + + # Scan for MP4 files + for file_path in Path(directory_path).glob("*.mp4"): + try: + mp4 = MP4(str(file_path)) + current_artist = mp4.get("\xa9ART", ["Unknown"])[0] if "\xa9ART" in mp4 else "Unknown" + + if current_artist and is_lastname_firstname_format(current_artist): + new_artist = convert_to_firstname_lastname(current_artist) + + files_to_update.append({ + 'file_path': str(file_path), + 'filename': file_path.name, + 'old_artist': current_artist, + 'new_artist': new_artist + }) + + except Exception as e: + print(f"āš ļø Could not read ID3 tags from {file_path.name}: {e}") + + return files_to_update + + +def update_tracking_file(tracking_file: str, channel_name: str = "Let's Sing Karaoke", apply_changes: bool = False) -> Tuple[int, List[Dict]]: + """ + Update the karaoke tracking file to fix artist name formatting. + + Args: + tracking_file: Path to the tracking JSON file + channel_name: Channel name to target (default: Let's Sing Karaoke) + apply_changes: Whether to actually apply changes or just preview + + Returns: + Tuple of (number of changes made, list of changed entries) + """ + if not os.path.exists(tracking_file): + print(f"āŒ Tracking file not found: {tracking_file}") + return 0, [] + + # Load the tracking data + with open(tracking_file, 'r', encoding='utf-8') as f: + data = json.load(f) + + changes_made = 0 + changed_entries = [] + + # Process songs + for song_key, song_data in data.get('songs', {}).items(): + if song_data.get('channel_name') != channel_name: + continue + + artist = song_data.get('artist', '') + if not artist or not is_lastname_firstname_format(artist): + continue + + # Convert the artist name + new_artist = convert_to_firstname_lastname(artist) + + if apply_changes: + # Update the tracking data + song_data['artist'] = new_artist + + # Update the video title if it exists and contains the old artist name + video_title = song_data.get('video_title', '') + if video_title and artist in video_title: + song_data['video_title'] = video_title.replace(artist, new_artist) + + # Update the file path if it exists + file_path = song_data.get('file_path', '') + if file_path and artist in file_path: + song_data['file_path'] = file_path.replace(artist, new_artist) + + changes_made += 1 + changed_entries.append({ + 'song_key': song_key, + 'old_artist': artist, + 'new_artist': new_artist, + 'title': song_data.get('title', ''), + 'file_path': song_data.get('file_path', '') + }) + + print(f"šŸ”„ {'Updated' if apply_changes else 'Would update'}: '{artist}' → '{new_artist}' ({song_data.get('title', '')})") + + # Save the updated data + if apply_changes and changes_made > 0: + # Create backup + backup_file = f"{tracking_file}.backup" + shutil.copy2(tracking_file, backup_file) + print(f"šŸ’¾ Created backup: {backup_file}") + + # Save updated file + with open(tracking_file, 'w', encoding='utf-8') as f: + json.dump(data, f, indent=2, ensure_ascii=False) + print(f"šŸ’¾ Updated tracking file: {tracking_file}") + + return changes_made, changed_entries + + +def update_songlist_tracking(songlist_file: str, channel_name: str = "Let's Sing Karaoke", apply_changes: bool = False) -> Tuple[int, List[Dict]]: + """ + Update the songlist tracking file to fix artist name formatting. + + Args: + songlist_file: Path to the songlist tracking JSON file + channel_name: Channel name to target (default: Let's Sing Karaoke) + apply_changes: Whether to actually apply changes or just preview + + Returns: + Tuple of (number of changes made, list of changed entries) + """ + if not os.path.exists(songlist_file): + print(f"āŒ Songlist tracking file not found: {songlist_file}") + return 0, [] + + # Load the songlist data + with open(songlist_file, 'r', encoding='utf-8') as f: + data = json.load(f) + + changes_made = 0 + changed_entries = [] + + # Process songlist entries + for song_key, song_data in data.items(): + artist = song_data.get('artist', '') + if not artist or not is_lastname_firstname_format(artist): + continue + + # Convert the artist name + new_artist = convert_to_firstname_lastname(artist) + + if apply_changes: + # Update the songlist data + song_data['artist'] = new_artist + + changes_made += 1 + changed_entries.append({ + 'song_key': song_key, + 'old_artist': artist, + 'new_artist': new_artist, + 'title': song_data.get('title', '') + }) + + print(f"šŸ”„ {'Updated' if apply_changes else 'Would update'} songlist: '{artist}' → '{new_artist}' ({song_data.get('title', '')})") + + # Save the updated data + if apply_changes and changes_made > 0: + # Create backup + backup_file = f"{songlist_file}.backup" + shutil.copy2(songlist_file, backup_file) + print(f"šŸ’¾ Created backup: {backup_file}") + + # Save updated file + with open(songlist_file, 'w', encoding='utf-8') as f: + json.dump(data, f, indent=2, ensure_ascii=False) + print(f"šŸ’¾ Updated songlist file: {songlist_file}") + + return changes_made, changed_entries + + +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') + + 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("āŒ 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() + + # File paths + tracking_file = "data/karaoke_tracking.json" + songlist_file = "data/songlist_tracking.json" + + # Process external directory if specified + if args.external: + print(f"šŸ“ Scanning external directory: {args.external}") + external_files = scan_external_directory(args.external) + + if external_files: + print(f"\nšŸ“‹ Found {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"\nšŸ“ Updating ID3 tags in external files...") + updated_count = update_id3_tags_for_files(external_files, apply_changes=True) + print(f"āœ… Updated ID3 tags in {updated_count} external files") + else: + print(f"\nšŸ“ Would update ID3 tags in {len(external_files)} external files") + else: + print("āœ… No files with 'Last Name, First Name' format found in ID3 tags") + + # Process tracking files (only if they exist in current project) + if os.path.exists(tracking_file): + print(f"\nšŸ“Š Processing karaoke tracking file...") + tracking_changes, tracking_entries = update_tracking_file(tracking_file, apply_changes=args.apply) + else: + print(f"\nāš ļø Tracking file not found: {tracking_file}") + tracking_changes = 0 + + if os.path.exists(songlist_file): + print(f"\nšŸ“Š Processing songlist tracking file...") + songlist_changes, songlist_entries = update_songlist_tracking(songlist_file, apply_changes=args.apply) + else: + print(f"\nāš ļø Songlist tracking file not found: {songlist_file}") + songlist_changes = 0 + + # Process local downloads directory ID3 tags + downloads_dir = "downloads" + local_id3_updates = 0 + if os.path.exists(downloads_dir) and tracking_changes > 0: + print(f"\nšŸ“ Processing ID3 tags in local downloads directory...") + # Scan local downloads for files that need ID3 tag updates + local_files = [] + for entry in tracking_entries: + file_path = entry.get('file_path', '') + if file_path and os.path.exists(file_path.replace('\\', '/')): + local_files.append({ + 'file_path': file_path.replace('\\', '/'), + 'filename': os.path.basename(file_path), + 'old_artist': entry['old_artist'], + 'new_artist': entry['new_artist'] + }) + + if local_files: + local_id3_updates = update_id3_tags_for_files(local_files, apply_changes=args.apply) + + total_changes = tracking_changes + songlist_changes + + print("\n" + "=" * 60) + print("šŸ“‹ Summary:") + print(f" • Tracking file changes: {tracking_changes}") + print(f" • Songlist file changes: {songlist_changes}") + print(f" • Local ID3 tag updates: {local_id3_updates}") + print(f" • Total changes: {total_changes}") + + if args.external: + external_count = len(scan_external_directory(args.external)) if args.preview else len(external_files) + print(f" • External ID3 tag updates: {external_count}") + + if total_changes > 0 or (args.external and external_count > 0): + if args.apply: + print("\nāœ… Artist name formatting in ID3 tags has been fixed!") + print("šŸ’¾ Backups have been created for all modified files.") + print("šŸ”„ You may need to re-run your karaoke downloader to update any cached data.") + else: + print("\nšŸ” Preview complete. Use --apply to make these changes.") + else: + print("\nāœ… No changes needed! All artist names are already in the correct format.") + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/fix_artist_name_format_simple.py b/fix_artist_name_format_simple.py new file mode 100644 index 0000000..f943a9b --- /dev/null +++ b/fix_artist_name_format_simple.py @@ -0,0 +1,295 @@ +#!/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() \ No newline at end of file