#!/usr/bin/env python3 """Migration script to populate year for existing series from folder names. This script: 1. Finds all series in the database with year=NULL 2. Extracts year from their folder names using the same pattern as SerieScanner 3. Updates the database records Usage: python scripts/migrate_populate_year_from_folder.py [--dry-run] """ import argparse import re import sys from pathlib import Path # Add project root to path sys.path.insert(0, str(Path(__file__).parent.parent)) from sqlalchemy import select, update from src.server.database.models import AnimeSeries from src.server.database.service import DatabaseSession def extract_year_from_folder_name(folder_name: str) -> int | None: """Extract year from folder name if present. Same logic as SerieScanner._extract_year_from_folder_name. Args: folder_name: The folder name to check Returns: int or None: Year if found, None otherwise """ if not folder_name: return None # Look for year in format (YYYY) - typically at end of name match = re.search(r'\((\d{4})\)', folder_name) if match: try: year = int(match.group(1)) # Validate year is reasonable (between 1900 and 2100) if 1900 <= year <= 2100: return year except ValueError: pass return None async def migrate_year_from_folder(dry_run: bool = True) -> tuple[int, int]: """Migrate year field for existing series. Args: dry_run: If True, only report what would be changed Returns: Tuple of (updated_count, skipped_count) """ updated_count = 0 skipped_count = 0 async with DatabaseSession() as db: # Find all series with NULL year result = await db.execute( select(AnimeSeries).where(AnimeSeries.year.is_(None)) ) series_list = result.scalars().all() print(f"Found {len(series_list)} series with year=NULL") for series in series_list: year_from_folder = extract_year_from_folder_name(series.folder) if year_from_folder: print(f" {series.folder} -> {year_from_folder}") if not dry_run: await db.execute( update(AnimeSeries) .where(AnimeSeries.id == series.id) .values(year=year_from_folder) ) updated_count += 1 else: print(f" {series.folder} -> (no year found)") skipped_count += 1 return updated_count, skipped_count def main(): parser = argparse.ArgumentParser(description="Migrate year from folder name") parser.add_argument( "--dry-run", action="store_true", default=True, help="Show what would be changed without making changes" ) parser.add_argument( "--execute", action="store_true", help="Actually execute the migration (disabled by default)" ) args = parser.parse_args() dry_run = not args.execute if dry_run: print("=== DRY RUN MODE ===") print("No changes will be made. Use --execute to apply changes.\n") import asyncio try: updated, skipped = asyncio.run(migrate_year_from_folder(dry_run=dry_run)) print(f"\n{'Would update' if dry_run else 'Updated'}: {updated} series") print(f"Skipped (no year in folder): {skipped} series") if dry_run: print("\nRun with --execute to apply these changes.") return 0 except Exception as e: print(f"Error: {e}", file=sys.stderr) return 1 if __name__ == "__main__": sys.exit(main())