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        )