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