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 )