main
  1"""
  2Rename series episodes in Sonarr with interactive confirmation.
  3
  4This script:
  51. Fetches all series from Sonarr API
  62. Checks which series have episodes that need renaming
  73. Previews the rename changes for each series
  84. Asks for confirmation before applying renames
  9"""
 10
 11from typing import Any, Dict, List
 12
 13from lib import (
 14    ArrClient,
 15    CommandContext,
 16    get_confirmation_decision,
 17    print_final_summary,
 18    print_item_list,
 19    print_section_header,
 20)
 21
 22
 23def get_rename_preview(
 24    client: ArrClient, series_id: int
 25) -> List[Dict[str, Any]]:
 26    """Get preview of files that will be renamed for a series."""
 27    return client.get("/api/v3/rename", params={"seriesId": series_id})
 28
 29
 30def execute_rename(
 31    client: ArrClient, series_id: int, file_ids: List[int]
 32) -> Dict[str, Any]:
 33    """Execute rename operation for a series."""
 34    payload = {
 35        "name": "RenameFiles",
 36        "seriesId": series_id,
 37        "files": file_ids,
 38    }
 39    return client.post("/api/v3/command", payload)
 40
 41
 42def run(url: str, api_key: str, dry_run: bool, no_confirm: bool):
 43    """Execute the sonarr rename command."""
 44    # Create client and context
 45    client = ArrClient(url, api_key)
 46    ctx = CommandContext(dry_run, no_confirm)
 47
 48    print(f"Fetching series from {client.base_url}...")
 49    all_series = client.get("/api/v3/series")
 50    print(f"Found {len(all_series)} series\n")
 51
 52    series_with_renames = []
 53    series_without_renames = []
 54
 55    # Check each series for rename candidates
 56    print("Checking which series need renaming...")
 57    for series in all_series:
 58        series_id = series.get("id")
 59        series_title = series.get("title", "Unknown")
 60
 61        rename_preview = get_rename_preview(client, series_id)
 62
 63        if rename_preview:
 64            series_with_renames.append(
 65                (series_id, series_title, rename_preview)
 66            )
 67        else:
 68            series_without_renames.append(series_title)
 69
 70    # Print summary
 71    print_section_header("SUMMARY")
 72    print_item_list(series_without_renames, "✓ No renames needed")
 73
 74    if series_with_renames:
 75        count = len(series_with_renames)
 76        print(f"\n→ Series with renames needed: {count}")
 77
 78    if not series_with_renames:
 79        print("\nNo series need renaming!")
 80        return
 81
 82    # Process each series that needs renaming
 83    print_section_header("RENAME PREVIEW")
 84
 85    renamed_count = 0
 86    skipped_count = 0
 87
 88    for series_id, series_title, rename_preview in series_with_renames:
 89        print(f"\n{'=' * 80}")
 90        print(f"Series: {series_title}")
 91        print(f"Episodes to rename: {len(rename_preview)}")
 92        print("=" * 80)
 93
 94        # Show preview of renames (limit to first 10)
 95        display_limit = 10
 96        for i, item in enumerate(rename_preview[:display_limit]):
 97            existing_path = item.get("existingPath", "Unknown")
 98            new_path = item.get("newPath", "Unknown")
 99            print(f"\n  Episode {i + 1}:")
100            print(f"    FROM: {existing_path}")
101            print(f"    TO:   {new_path}")
102
103        if len(rename_preview) > display_limit:
104            remaining = len(rename_preview) - display_limit
105            print(f"\n  ... and {remaining} more episodes")
106
107        # Ask for confirmation
108        prompt = (
109            f"\nRename {len(rename_preview)} episodes "
110            f"for '{series_title}'?"
111        )
112        should_rename = get_confirmation_decision(ctx, prompt)
113
114        if should_rename:
115            print("Executing rename...")
116            # Extract episode file IDs from preview
117            file_ids = [item.get("episodeFileId") for item in rename_preview]
118            file_ids = [fid for fid in file_ids if fid is not None]
119
120            if file_ids:
121                result = execute_rename(client, series_id, file_ids)
122                if result:
123                    print("✓ Rename command queued successfully")
124                    renamed_count += 1
125                else:
126                    print("✗ Failed to queue rename command")
127                    skipped_count += 1
128            else:
129                print("✗ No valid file IDs found")
130                skipped_count += 1
131        else:
132            if not ctx.dry_run:
133                print("Skipped")
134            skipped_count += 1
135
136    # Final summary
137    if ctx.dry_run:
138        print_section_header("FINAL SUMMARY")
139        print(
140            f"\n[DRY RUN] Found {len(series_with_renames)} series "
141            "that need renaming"
142        )
143        print("No changes were made. Remove --dry-run to apply renames.")
144    else:
145        print_final_summary(
146            len(series_with_renames),
147            renamed_count,
148            skipped_count,
149            "Renamed",
150        )