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()