main
1"""
2Rename albums in Lidarr with interactive confirmation.
3
4This script:
51. Fetches all artists from Lidarr API
62. Checks which artists have albums with files that need renaming
73. Previews the rename changes for each album
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_artist_albums(
24 client: ArrClient, artist_id: int
25) -> List[Dict[str, Any]]:
26 """Fetch all albums for a specific artist."""
27 return client.get("/api/v1/album", params={"artistId": artist_id})
28
29
30def get_rename_preview(
31 client: ArrClient, artist_id: int
32) -> List[Dict[str, Any]]:
33 """Get preview of files that will be renamed for an artist."""
34 return client.get("/api/v1/rename", params={"artistId": artist_id})
35
36
37def execute_rename(
38 client: ArrClient, artist_id: int, file_ids: List[int]
39) -> Dict[str, Any]:
40 """Execute rename operation for an artist."""
41 payload = {
42 "name": "RenameFiles",
43 "artistId": artist_id,
44 "files": file_ids,
45 }
46 return client.post("/api/v1/command", payload)
47
48
49def run(url: str, api_key: str, dry_run: bool, no_confirm: bool):
50 """Execute the lidarr rename-albums command."""
51 # Create client and context
52 client = ArrClient(url, api_key)
53 ctx = CommandContext(dry_run, no_confirm)
54
55 print(f"Fetching artists from {client.base_url}...")
56 all_artists = client.get("/api/v1/artist")
57 print(f"Found {len(all_artists)} artists\n")
58
59 artists_with_renames = []
60 artists_without_renames = []
61
62 # Check each artist for rename candidates
63 print("Checking which artists have albums needing renaming...")
64 for artist in all_artists:
65 artist_id = artist.get("id")
66 artist_name = artist.get("artistName", "Unknown")
67
68 rename_preview = get_rename_preview(client, artist_id)
69
70 if rename_preview:
71 # Get album count for this artist
72 albums = get_artist_albums(client, artist_id)
73 album_count = len(albums) if albums else 0
74
75 artists_with_renames.append(
76 (artist_id, artist_name, album_count, rename_preview)
77 )
78 else:
79 artists_without_renames.append(artist_name)
80
81 # Print summary
82 print_section_header("SUMMARY")
83 print_item_list(artists_without_renames, "✓ No renames needed")
84
85 if artists_with_renames:
86 count = len(artists_with_renames)
87 print(f"\n→ Artists with renames needed: {count}")
88
89 if not artists_with_renames:
90 print("\nNo artists need renaming!")
91 return
92
93 # Process each artist that needs renaming
94 print_section_header("RENAME PREVIEW")
95
96 renamed_count = 0
97 skipped_count = 0
98
99 for artist_id, artist_name, album_count, rename_preview in (
100 artists_with_renames
101 ):
102 print(f"\n{'=' * 80}")
103 print(f"Artist: {artist_name}")
104 print(f"Albums: {album_count}")
105 print(f"Files to rename: {len(rename_preview)}")
106 print("=" * 80)
107
108 # Show preview of renames (limit to first 10)
109 display_limit = 10
110 for i, item in enumerate(rename_preview[:display_limit]):
111 existing_path = item.get("existingPath", "Unknown")
112 new_path = item.get("newPath", "Unknown")
113 print(f"\n File {i + 1}:")
114 print(f" FROM: {existing_path}")
115 print(f" TO: {new_path}")
116
117 if len(rename_preview) > display_limit:
118 remaining = len(rename_preview) - display_limit
119 print(f"\n ... and {remaining} more files")
120
121 # Ask for confirmation
122 file_word = "file" if len(rename_preview) == 1 else "files"
123 prompt = (
124 f"\nRename {len(rename_preview)} {file_word} "
125 f"for '{artist_name}'?"
126 )
127 should_rename = get_confirmation_decision(ctx, prompt)
128
129 if should_rename:
130 print("Executing rename...")
131 # Extract track file IDs from preview
132 file_ids = [item.get("trackFileId") 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, artist_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 ctx.dry_run:
148 print("Skipped")
149 skipped_count += 1
150
151 # Final summary
152 if ctx.dry_run:
153 print_section_header("FINAL SUMMARY")
154 print(
155 f"\n[DRY RUN] Found {len(artists_with_renames)} artists "
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(artists_with_renames),
162 renamed_count,
163 skipped_count,
164 "Renamed",
165 )