"""CLI command for NFO management. This script provides command-line interface for creating, updating, and checking NFO metadata files. """ import asyncio import logging import sys from pathlib import Path # Add src to path sys.path.insert(0, str(Path(__file__).parent.parent.parent)) from src.config.settings import settings from src.core.services.series_manager_service import SeriesManagerService logger = logging.getLogger(__name__) async def scan_and_create_nfo(): """Scan all series and create missing NFO files.""" logger.info("%s", "=" * 70) logger.info("NFO Auto-Creation Tool") logger.info("%s", "=" * 70) if not settings.tmdb_api_key: logger.error("TMDB_API_KEY not configured") logger.error("Set TMDB_API_KEY in .env file or environment") logger.error("Get API key from: https://www.themoviedb.org/settings/api") return 1 if not settings.anime_directory: logger.error("ANIME_DIRECTORY not configured") return 1 logger.info("Anime Directory: %s", settings.anime_directory) logger.info("Auto-create NFO: %s", settings.nfo_auto_create) logger.info("Update on scan: %s", settings.nfo_update_on_scan) logger.info("Download poster: %s", settings.nfo_download_poster) logger.info("Download logo: %s", settings.nfo_download_logo) logger.info("Download fanart: %s", settings.nfo_download_fanart) if not settings.nfo_auto_create: logger.warning("NFO_AUTO_CREATE is set to False") logger.warning("Enable it in .env to auto-create NFO files") logger.info("Continuing anyway to demonstrate functionality...") # Override for demonstration settings.nfo_auto_create = True logger.info("Initializing series manager...") manager = SeriesManagerService.from_settings() # Get series list first serie_list = manager.get_serie_list() all_series = serie_list.get_all() logger.info("Found %d series in directory", len(all_series)) if not all_series: logger.warning("No series found. Add some anime series first.") return 0 # Show series without NFO series_without_nfo = [] for serie in all_series: if not serie.has_nfo(): series_without_nfo.append(serie) if series_without_nfo: logger.info("Series without NFO: %d", len(series_without_nfo)) for serie in series_without_nfo[:5]: # Show first 5 logger.debug("Missing NFO: %s (%s)", serie.name, serie.folder) if len(series_without_nfo) > 5: logger.info("... and %d more", len(series_without_nfo) - 5) else: logger.info("All series already have NFO files") if not settings.nfo_update_on_scan: logger.info("Nothing to do. Enable NFO_UPDATE_ON_SCAN to update existing NFOs.") return 0 logger.info("Processing NFO files...") logger.info("This may take a while depending on the number of series") try: await manager.scan_and_process_nfo() logger.info("NFO processing complete") # Show updated stats serie_list.load_series() # Reload to get updated stats all_series = serie_list.get_all() series_with_nfo = [s for s in all_series if s.has_nfo()] series_with_poster = [s for s in all_series if s.has_poster()] series_with_logo = [s for s in all_series if s.has_logo()] series_with_fanart = [s for s in all_series if s.has_fanart()] logger.info("Final statistics", extra={ "total_series": len(all_series), "with_nfo": len(series_with_nfo), "with_poster": len(series_with_poster), "with_logo": len(series_with_logo), "with_fanart": len(series_with_fanart), }) except Exception: logger.exception("Failed to process NFO files") return 1 finally: await manager.close() return 0 async def check_nfo_status(): """Check NFO status for all series.""" logger.info("%s", "=" * 70) logger.info("NFO Status Check") logger.info("%s", "=" * 70) if not settings.anime_directory: logger.error("ANIME_DIRECTORY not configured") return 1 logger.info("Anime Directory: %s", settings.anime_directory) # Create series list (no NFO service needed for status check) from src.core.entities.SerieList import SerieList serie_list = SerieList(settings.anime_directory) all_series = serie_list.get_all() if not all_series: logger.warning("No series found") return 0 logger.info("Total series: %d", len(all_series)) # Categorize series with_nfo = [] without_nfo = [] for serie in all_series: if serie.has_nfo(): with_nfo.append(serie) else: without_nfo.append(serie) logger.info( "Series NFO coverage", extra={ "with_nfo": len(with_nfo), "without_nfo": len(without_nfo), "total": len(all_series), }, ) if without_nfo: logger.info("Series missing NFO: %d", len(without_nfo)) for serie in without_nfo[:10]: logger.debug("Missing NFO: %s (%s)", serie.name, serie.folder) if len(without_nfo) > 10: logger.info("... and %d more", len(without_nfo) - 10) # Media file statistics with_poster = sum(1 for s in all_series if s.has_poster()) with_logo = sum(1 for s in all_series if s.has_logo()) with_fanart = sum(1 for s in all_series if s.has_fanart()) logger.info( "Media file coverage", extra={ "posters": with_poster, "logos": with_logo, "fanart": with_fanart, "total": len(all_series), }, ) return 0 async def update_nfo_files(): """Update existing NFO files with fresh data from TMDB.""" logger.info("%s", "=" * 70) logger.info("NFO Update Tool") logger.info("%s", "=" * 70) if not settings.tmdb_api_key: logger.error("TMDB_API_KEY not configured") logger.error("Set TMDB_API_KEY in .env file or environment") logger.error("Get API key from: https://www.themoviedb.org/settings/api") return 1 if not settings.anime_directory: logger.error("ANIME_DIRECTORY not configured") return 1 logger.info("Anime Directory: %s", settings.anime_directory) logger.info( "Download media: %s", settings.nfo_download_poster or settings.nfo_download_logo or settings.nfo_download_fanart, ) # Get series with NFO from src.core.entities.SerieList import SerieList serie_list = SerieList(settings.anime_directory) all_series = serie_list.get_all() series_with_nfo = [s for s in all_series if s.has_nfo()] if not series_with_nfo: logger.warning("No series with NFO files found") logger.info("Run 'scan' command first to create NFO files") return 0 logger.info("Found %d series with NFO files", len(series_with_nfo)) logger.info("Updating NFO files with fresh data from TMDB...") logger.info("This may take a while") # Initialize NFO service using factory from src.core.services.nfo_factory import create_nfo_service try: nfo_service = create_nfo_service() except ValueError as e: logger.error("Error creating NFO service: %s", e) return 1 success_count = 0 error_count = 0 try: for i, serie in enumerate(series_with_nfo, 1): logger.info("[%d/%d] Updating: %s", i, len(series_with_nfo), serie.name) try: await nfo_service.update_tvshow_nfo( serie_folder=serie.folder, download_media=( settings.nfo_download_poster or settings.nfo_download_logo or settings.nfo_download_fanart ), ) logger.info("Updated successfully: %s", serie.name) success_count += 1 # Small delay to respect API rate limits await asyncio.sleep(0.5) except Exception as e: logger.exception("Failed to update NFO for %s", serie.name) error_count += 1 logger.info("%s", "=" * 70) logger.info("Update complete") logger.info("Success: %d", success_count) logger.info("Errors: %d", error_count) except Exception: logger.exception("Fatal error during NFO update") return 1 finally: await nfo_service.close() return 0 def main(): """Main CLI entry point.""" logging.basicConfig(level=logging.INFO, format="%(message)s") if len(sys.argv) < 2: logger.info("NFO Management Tool") logger.info("\nUsage:") logger.info(" python -m src.cli.nfo_cli scan # Scan and create missing NFO files") logger.info(" python -m src.cli.nfo_cli status # Check NFO status for all series") logger.info(" python -m src.cli.nfo_cli update # Update existing NFO files with fresh data") logger.info("\nConfiguration:") logger.info(" Set TMDB_API_KEY in .env file") logger.info(" Set NFO_AUTO_CREATE=true to enable auto-creation") logger.info(" Set NFO_UPDATE_ON_SCAN=true to update existing NFOs during scan") return 1 command = sys.argv[1].lower() if command == "scan": return asyncio.run(scan_and_create_nfo()) elif command == "status": return asyncio.run(check_nfo_status()) elif command == "update": return asyncio.run(update_nfo_files()) else: logger.error("Unknown command: %s", command) logger.info("Use 'scan', 'status', or 'update'") return 1 if __name__ == "__main__": sys.exit(main())