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