KaraokeVideoDownloader/karaoke_downloader/songlist_generator.py

261 lines
9.6 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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