261 lines
9.6 KiB
Python
261 lines
9.6 KiB
Python
import json
|
||
import os
|
||
from pathlib import Path
|
||
from typing import List, Dict, Any, Optional
|
||
from mutagen.mp4 import MP4
|
||
|
||
|
||
class SongListGenerator:
|
||
"""Utility class for generating song lists from MP4 files with ID3 tags."""
|
||
|
||
def __init__(self, songlist_path: str = "data/songList.json"):
|
||
self.songlist_path = Path(songlist_path)
|
||
self.songlist_path.parent.mkdir(parents=True, exist_ok=True)
|
||
|
||
def read_existing_songlist(self) -> List[Dict[str, Any]]:
|
||
"""Read existing song list from JSON file."""
|
||
if self.songlist_path.exists():
|
||
try:
|
||
with open(self.songlist_path, 'r', encoding='utf-8') as f:
|
||
return json.load(f)
|
||
except (json.JSONDecodeError, IOError) as e:
|
||
print(f"⚠️ Warning: Could not read existing songlist: {e}")
|
||
return []
|
||
return []
|
||
|
||
def save_songlist(self, songlist: List[Dict[str, Any]]) -> None:
|
||
"""Save song list to JSON file."""
|
||
try:
|
||
with open(self.songlist_path, 'w', encoding='utf-8') as f:
|
||
json.dump(songlist, f, indent=2, ensure_ascii=False)
|
||
print(f"✅ Song list saved to {self.songlist_path}")
|
||
except IOError as e:
|
||
print(f"❌ Error saving song list: {e}")
|
||
raise
|
||
|
||
def extract_id3_tags(self, mp4_path: Path) -> Optional[Dict[str, str]]:
|
||
"""Extract ID3 tags from MP4 file."""
|
||
try:
|
||
mp4 = MP4(str(mp4_path))
|
||
|
||
# Extract artist and title from ID3 tags
|
||
artist = mp4.get("\xa9ART", ["Unknown Artist"])[0] if "\xa9ART" in mp4 else "Unknown Artist"
|
||
title = mp4.get("\xa9nam", ["Unknown Title"])[0] if "\xa9nam" in mp4 else "Unknown Title"
|
||
|
||
return {
|
||
"artist": artist,
|
||
"title": title
|
||
}
|
||
except Exception as e:
|
||
print(f"⚠️ Warning: Could not extract ID3 tags from {mp4_path.name}: {e}")
|
||
return None
|
||
|
||
def scan_directory_for_mp4_files(self, directory_path: str) -> List[Path]:
|
||
"""Scan directory for MP4 files."""
|
||
directory = Path(directory_path)
|
||
if not directory.exists():
|
||
raise FileNotFoundError(f"Directory not found: {directory_path}")
|
||
|
||
if not directory.is_dir():
|
||
raise ValueError(f"Path is not a directory: {directory_path}")
|
||
|
||
mp4_files = list(directory.glob("*.mp4"))
|
||
if not mp4_files:
|
||
print(f"⚠️ No MP4 files found in {directory_path}")
|
||
return []
|
||
|
||
print(f"📁 Found {len(mp4_files)} MP4 files in {directory.name}")
|
||
return sorted(mp4_files)
|
||
|
||
def generate_songlist_from_directory(self, directory_path: str, append: bool = True) -> Dict[str, Any]:
|
||
"""Generate a song list from MP4 files in a directory."""
|
||
directory = Path(directory_path)
|
||
directory_name = directory.name
|
||
|
||
# Scan for MP4 files
|
||
mp4_files = self.scan_directory_for_mp4_files(directory_path)
|
||
if not mp4_files:
|
||
return {}
|
||
|
||
# Extract ID3 tags and create songs list
|
||
songs = []
|
||
for index, mp4_file in enumerate(mp4_files, start=1):
|
||
id3_tags = self.extract_id3_tags(mp4_file)
|
||
if id3_tags:
|
||
song = {
|
||
"position": index,
|
||
"title": id3_tags["title"],
|
||
"artist": id3_tags["artist"]
|
||
}
|
||
songs.append(song)
|
||
print(f" {index:3d}. {id3_tags['artist']} - {id3_tags['title']}")
|
||
|
||
if not songs:
|
||
print("❌ No valid ID3 tags found in any MP4 files")
|
||
return {}
|
||
|
||
# Create the song list entry
|
||
songlist_entry = {
|
||
"title": directory_name,
|
||
"songs": songs
|
||
}
|
||
|
||
# Handle appending to existing song list
|
||
if append:
|
||
existing_songlist = self.read_existing_songlist()
|
||
|
||
# Check if a playlist with this title already exists
|
||
existing_index = None
|
||
for i, entry in enumerate(existing_songlist):
|
||
if entry.get("title") == directory_name:
|
||
existing_index = i
|
||
break
|
||
|
||
if existing_index is not None:
|
||
# Replace existing entry
|
||
print(f"🔄 Replacing existing playlist: {directory_name}")
|
||
existing_songlist[existing_index] = songlist_entry
|
||
else:
|
||
# Add new entry to the beginning of the list
|
||
print(f"➕ Adding new playlist: {directory_name}")
|
||
existing_songlist.insert(0, songlist_entry)
|
||
|
||
self.save_songlist(existing_songlist)
|
||
else:
|
||
# Create new song list with just this entry
|
||
print(f"📝 Creating new song list with playlist: {directory_name}")
|
||
self.save_songlist([songlist_entry])
|
||
|
||
return songlist_entry
|
||
|
||
def generate_songlist_from_multiple_directories(self, directory_paths: List[str], append: bool = True) -> List[Dict[str, Any]]:
|
||
"""Generate song lists from multiple directories."""
|
||
results = []
|
||
errors = []
|
||
|
||
# Read existing song list once at the beginning
|
||
existing_songlist = self.read_existing_songlist() if append else []
|
||
|
||
for directory_path in directory_paths:
|
||
try:
|
||
print(f"\n📂 Processing directory: {directory_path}")
|
||
directory = Path(directory_path)
|
||
directory_name = directory.name
|
||
|
||
# Scan for MP4 files
|
||
mp4_files = self.scan_directory_for_mp4_files(directory_path)
|
||
if not mp4_files:
|
||
continue
|
||
|
||
# Extract ID3 tags and create songs list
|
||
songs = []
|
||
for index, mp4_file in enumerate(mp4_files, start=1):
|
||
id3_tags = self.extract_id3_tags(mp4_file)
|
||
if id3_tags:
|
||
song = {
|
||
"position": index,
|
||
"title": id3_tags["title"],
|
||
"artist": id3_tags["artist"]
|
||
}
|
||
songs.append(song)
|
||
print(f" {index:3d}. {id3_tags['artist']} - {id3_tags['title']}")
|
||
|
||
if not songs:
|
||
print("❌ No valid ID3 tags found in any MP4 files")
|
||
continue
|
||
|
||
# Create the song list entry
|
||
songlist_entry = {
|
||
"title": directory_name,
|
||
"songs": songs
|
||
}
|
||
|
||
# Check if a playlist with this title already exists
|
||
existing_index = None
|
||
for i, entry in enumerate(existing_songlist):
|
||
if entry.get("title") == directory_name:
|
||
existing_index = i
|
||
break
|
||
|
||
if existing_index is not None:
|
||
# Replace existing entry
|
||
print(f"🔄 Replacing existing playlist: {directory_name}")
|
||
existing_songlist[existing_index] = songlist_entry
|
||
else:
|
||
# Add new entry to the beginning of the list
|
||
print(f"➕ Adding new playlist: {directory_name}")
|
||
existing_songlist.insert(0, songlist_entry)
|
||
|
||
results.append(songlist_entry)
|
||
|
||
except Exception as e:
|
||
error_msg = f"Error processing {directory_path}: {e}"
|
||
print(f"❌ {error_msg}")
|
||
errors.append(error_msg)
|
||
|
||
# Save the final song list
|
||
if results:
|
||
if append:
|
||
# Save the updated existing song list
|
||
self.save_songlist(existing_songlist)
|
||
else:
|
||
# Create new song list with just the results
|
||
self.save_songlist(results)
|
||
|
||
# If there were any errors, raise an exception
|
||
if errors:
|
||
raise Exception(f"Failed to process {len(errors)} directories: {'; '.join(errors)}")
|
||
|
||
return results
|
||
|
||
|
||
def main():
|
||
"""CLI entry point for song list generation."""
|
||
import argparse
|
||
import sys
|
||
|
||
parser = argparse.ArgumentParser(
|
||
description="Generate song lists from MP4 files with ID3 tags",
|
||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||
epilog="""
|
||
Examples:
|
||
python -m karaoke_downloader.songlist_generator /path/to/mp4/directory
|
||
python -m karaoke_downloader.songlist_generator /path/to/dir1 /path/to/dir2 --no-append
|
||
python -m karaoke_downloader.songlist_generator /path/to/dir --songlist-path custom_songlist.json
|
||
"""
|
||
)
|
||
|
||
parser.add_argument(
|
||
"directories",
|
||
nargs="+",
|
||
help="Directory paths containing MP4 files with ID3 tags"
|
||
)
|
||
|
||
parser.add_argument(
|
||
"--no-append",
|
||
action="store_true",
|
||
help="Create a new song list instead of appending to existing one"
|
||
)
|
||
|
||
parser.add_argument(
|
||
"--songlist-path",
|
||
default="data/songList.json",
|
||
help="Path to the song list JSON file (default: data/songList.json)"
|
||
)
|
||
|
||
args = parser.parse_args()
|
||
|
||
try:
|
||
generator = SongListGenerator(args.songlist_path)
|
||
generator.generate_songlist_from_multiple_directories(
|
||
args.directories,
|
||
append=not args.no_append
|
||
)
|
||
print("\n✅ Song list generation completed successfully!")
|
||
except Exception as e:
|
||
print(f"\n❌ Error: {e}")
|
||
sys.exit(1)
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main() |