Signed-off-by: mbrucedogs <mbrucedogs@gmail.com>

This commit is contained in:
mbrucedogs 2025-07-28 14:09:07 -05:00
parent c48c1d3696
commit d18ac54476
2 changed files with 760 additions and 0 deletions

465
fix_artist_name_format.py Normal file
View File

@ -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()

View File

@ -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()