295 lines
10 KiB
Python
295 lines
10 KiB
Python
#!/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() |