system-manager-wakasu
1#!/usr/bin/env -S uv run --quiet --script
2# /// script
3# dependencies = [
4# "requests",
5# ]
6# ///
7"""
8Rename albums in Lidarr with interactive confirmation.
9
10This script:
111. Fetches all artists from Lidarr API
122. Checks which artists have albums with files that need renaming
133. Previews the rename changes for each album
144. Asks for confirmation before applying renames
15
16Usage:
17 ./lidarr-rename-albums.py <lidarr_url> <api_key>
18
19Example:
20 ./lidarr-rename-albums.py http://localhost:8686 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_artist_albums(
36 client: ArrClient, artist_id: int
37) -> List[Dict[str, Any]]:
38 """Fetch all albums for a specific artist."""
39 return client.get("/api/v1/album", params={"artistId": artist_id})
40
41
42def get_rename_preview(
43 client: ArrClient, artist_id: int
44) -> List[Dict[str, Any]]:
45 """Get preview of files that will be renamed for an artist."""
46 return client.get("/api/v1/rename", params={"artistId": artist_id})
47
48
49def execute_rename(
50 client: ArrClient, artist_id: int, file_ids: List[int]
51) -> Dict[str, Any]:
52 """Execute rename operation for an artist."""
53 payload = {
54 "name": "RenameFiles",
55 "artistId": artist_id,
56 "files": file_ids,
57 }
58 return client.post("/api/v1/command", payload)
59
60
61def main():
62 parser = create_arr_parser(
63 "Lidarr", "Rename Lidarr albums with confirmation", 8686
64 )
65 args = parser.parse_args()
66
67 # Create client
68 client = ArrClient(args.lidarr_url, args.api_key)
69
70 print(f"Fetching artists from {client.base_url}...")
71 all_artists = client.get("/api/v1/artist")
72 print(f"Found {len(all_artists)} artists\n")
73
74 artists_with_renames = []
75 artists_without_renames = []
76
77 # Check each artist for rename candidates
78 print("Checking which artists have albums needing renaming...")
79 for artist in all_artists:
80 artist_id = artist.get("id")
81 artist_name = artist.get("artistName", "Unknown")
82
83 rename_preview = get_rename_preview(client, artist_id)
84
85 if rename_preview:
86 # Get album count for this artist
87 albums = get_artist_albums(client, artist_id)
88 album_count = len(albums) if albums else 0
89
90 artists_with_renames.append(
91 (artist_id, artist_name, album_count, rename_preview)
92 )
93 else:
94 artists_without_renames.append(artist_name)
95
96 # Print summary
97 print_section_header("SUMMARY")
98 print_item_list(artists_without_renames, "✓ No renames needed")
99
100 if artists_with_renames:
101 count = len(artists_with_renames)
102 print(f"\n→ Artists with renames needed: {count}")
103
104 if not artists_with_renames:
105 print("\nNo artists need renaming!")
106 return
107
108 # Process each artist that needs renaming
109 print_section_header("RENAME PREVIEW")
110
111 renamed_count = 0
112 skipped_count = 0
113
114 for artist_id, artist_name, album_count, rename_preview in (
115 artists_with_renames
116 ):
117 print(f"\n{'=' * 80}")
118 print(f"Artist: {artist_name}")
119 print(f"Albums: {album_count}")
120 print(f"Files to rename: {len(rename_preview)}")
121 print("=" * 80)
122
123 # Show preview of renames (limit to first 10)
124 display_limit = 10
125 for i, item in enumerate(rename_preview[:display_limit]):
126 existing_path = item.get("existingPath", "Unknown")
127 new_path = item.get("newPath", "Unknown")
128 print(f"\n File {i + 1}:")
129 print(f" FROM: {existing_path}")
130 print(f" TO: {new_path}")
131
132 if len(rename_preview) > display_limit:
133 remaining = len(rename_preview) - display_limit
134 print(f"\n ... and {remaining} more files")
135
136 # Ask for confirmation
137 file_word = "file" if len(rename_preview) == 1 else "files"
138 prompt = (
139 f"\nRename {len(rename_preview)} {file_word} "
140 f"for '{artist_name}'?"
141 )
142 should_rename = get_confirmation_decision(args, prompt)
143
144 if should_rename:
145 print("Executing rename...")
146 # Extract track file IDs from preview
147 file_ids = [item.get("trackFileId") for item in rename_preview]
148 file_ids = [fid for fid in file_ids if fid is not None]
149
150 if file_ids:
151 result = execute_rename(client, artist_id, file_ids)
152 if result:
153 print("✓ Rename command queued successfully")
154 renamed_count += 1
155 else:
156 print("✗ Failed to queue rename command")
157 skipped_count += 1
158 else:
159 print("✗ No valid file IDs found")
160 skipped_count += 1
161 else:
162 if not args.dry_run:
163 print("Skipped")
164 skipped_count += 1
165
166 # Final summary
167 if args.dry_run:
168 print_section_header("FINAL SUMMARY")
169 print(
170 f"\n[DRY RUN] Found {len(artists_with_renames)} artists "
171 "that need renaming"
172 )
173 print("No changes were made. Remove --dry-run to apply renames.")
174 else:
175 print_final_summary(
176 len(artists_with_renames),
177 renamed_count,
178 skipped_count,
179 "Renamed",
180 )
181
182
183if __name__ == "__main__":
184 main()