EpisodeMatcher/episode_matcher.py
Jarian Cottingham 03b03d676f Initial Commit
2025-08-07 21:15:00 -05:00

228 lines
7.7 KiB
Python
Executable File

#!/usr/bin/env python3
"""
Episode Matcher - Main script for classifying and renaming TV show episode files.
This tool takes a folder of MKV files and:
1. Classifies them as episodes or extras based on file size and duration
2. Moves extras to an "extras" subfolder
3. Renames episodes using Jellyfin naming convention
4. Uses TVDB data to validate episode matching
Usage:
python episode_matcher.py <folder_path> <show_name> <season_number> [--api-key <tvdb_api_key>]
Examples:
python episode_matcher.py "/path/to/episodes" "Game of Thrones" 1
python episode_matcher.py "/path/to/episodes" "Breaking Bad" 2 --api-key your_tvdb_api_key
"""
import argparse
import sys
import os
from pathlib import Path
# Add src to path so we can import our modules
sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'src'))
from TVDBProvider import TVDBClient
from TVDBProvider.tvdb_client import MockTVDBClient
from Matcher import FileClassifier, EpisodeRenamer
from config import config_manager
def main():
"""Main function to run the episode matcher."""
parser = argparse.ArgumentParser(
description="Classify and rename TV show episode files",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog=__doc__
)
parser.add_argument(
"folder_path",
help="Path to folder containing MKV files"
)
parser.add_argument(
"show_name",
help="Name of the TV show (e.g., 'Game of Thrones')"
)
parser.add_argument(
"season_number",
type=int,
help="Season number (e.g., 1, 2, 3)"
)
parser.add_argument(
"--api-key",
help="TVDB API key (optional, will check config.json first, then use mock data if not found)"
)
parser.add_argument(
"--dry-run",
action="store_true",
help="Show what would be done without actually renaming/moving files"
)
parser.add_argument(
"--verbose", "-v",
action="store_true",
help="Show detailed analysis information"
)
args = parser.parse_args()
# Validate inputs
folder_path = Path(args.folder_path)
if not folder_path.exists():
print(f"Error: Folder not found: {folder_path}")
sys.exit(1)
if not folder_path.is_dir():
print(f"Error: Path is not a directory: {folder_path}")
sys.exit(1)
if args.season_number < 1:
print(f"Error: Season number must be positive, got: {args.season_number}")
sys.exit(1)
print(f"=== Episode Matcher ===")
print(f"Folder: {folder_path}")
print(f"Show: {args.show_name}")
print(f"Season: {args.season_number}")
print(f"Mode: {'DRY RUN' if args.dry_run else 'LIVE'}")
# Show config status
if args.verbose:
print("\n=== Configuration ===")
config_manager.print_config_status()
print()
# Get API key from config or command line argument
api_key = args.api_key or config_manager.get_tvdb_api_key()
# Initialize TVDB client
if api_key:
print("Using TVDB API with configured key...")
tvdb_client = TVDBClient()
if not tvdb_client.authenticate(api_key):
print("TVDB authentication failed, falling back to mock data")
tvdb_client = MockTVDBClient()
tvdb_client.authenticate()
else:
print("No API key found in config.json or command line, using mock TVDB data...")
print("To use real TVDB data, add your API key to config.json")
tvdb_client = MockTVDBClient()
tvdb_client.authenticate()
# Get episode information from TVDB
print(f"Fetching episode data for '{args.show_name}' season {args.season_number}...")
tvdb_episodes = tvdb_client.get_episode_durations(args.show_name, args.season_number)
if tvdb_episodes:
print(f"Found {len(tvdb_episodes)} episodes in TVDB data")
if args.verbose:
print("\nTVDB Episodes:")
for ep in tvdb_episodes:
print(f" Episode {ep['episode_number']}: {ep['name']} ({ep['runtime']} min)")
else:
print("Warning: Could not fetch episode data from TVDB")
tvdb_episodes = []
# Initialize file classifier
try:
classifier = FileClassifier(str(folder_path))
except FileNotFoundError as e:
print(f"Error: {e}")
sys.exit(1)
if args.verbose:
classifier.print_analysis()
# Classify files
expected_episode_count = len(tvdb_episodes) if tvdb_episodes else None
episodes, extras = classifier.classify_files(expected_episode_count, tvdb_episodes)
print(f"\n=== Classification Results ===")
print(f"Episodes: {len(episodes)}")
print(f"Extras: {len(extras)}")
if not episodes:
print("Error: No episodes found. Check your files and try again.")
sys.exit(1)
if args.verbose:
print("\nEpisodes:")
for ep in episodes:
print(f" - {ep['name']} ({ep['size_gb']:.2f} GB, {ep['duration_minutes']:.1f} min)")
if extras:
print("\nExtras:")
for ex in extras:
print(f" - {ex['name']} ({ex['size_gb']:.2f} GB, {ex['duration_minutes']:.1f} min)")
# Initialize renamer
renamer = EpisodeRenamer(str(folder_path), args.show_name, args.season_number)
if args.dry_run:
print(f"\n=== DRY RUN - No files will be modified ===")
if extras:
print(f"\nWould move {len(extras)} files to extras folder:")
for extra in extras:
print(f" - {extra['name']}")
print(f"\nWould rename {len(episodes)} episode files:")
# Simulate the matching process
if tvdb_episodes:
matched_episodes = renamer._match_episodes_by_duration(episodes, tvdb_episodes)
else:
sorted_episodes = sorted(episodes, key=lambda x: x['size_bytes'], reverse=True)
matched_episodes = []
for i, episode in enumerate(sorted_episodes, 1):
matched_episodes.append({
'file_info': episode,
'episode_number': i,
'tvdb_info': None
})
for match in matched_episodes:
episode_number = match['episode_number']
file_info = match['file_info']
original_name = file_info['name'] # This should work now
new_name = renamer._generate_episode_filename(episode_number, Path(original_name).suffix)
print(f" Episode {episode_number:2d}: {original_name}{new_name}")
else:
# Move extras to folder
if extras:
print(f"\n=== Moving {len(extras)} extras to subfolder ===")
moved_extras = renamer.move_extras_to_folder(extras)
print(f"Successfully moved {len(moved_extras)} files to extras folder")
# Rename episodes
print(f"\n=== Renaming {len(episodes)} episodes ===")
renamed_files = renamer.rename_episodes(episodes, tvdb_episodes)
if renamed_files:
renamer.print_rename_summary(renamed_files)
# Validate that all episodes were processed
expected_count = len(tvdb_episodes) if tvdb_episodes else len(episodes)
success = renamer.validate_season_complete(renamed_files, expected_count)
if success:
print(f"\n✓ Episode matching completed successfully!")
else:
print(f"\n⚠ Episode matching completed with warnings")
sys.exit(1)
else:
print("Error: No episodes were renamed")
sys.exit(1)
if __name__ == "__main__":
main()