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