main
  1"""
  2Update artist paths in Lidarr to use a 'library' subdirectory.
  3
  4This script:
  51. Fetches all artists from Lidarr API
  62. Checks if their path is directly in the music folder or already
  7   contains 'library'
  83. Updates paths that need to be moved to
  9   <music_folder>/library/<artist>
 10"""
 11
 12import sys
 13from pathlib import Path
 14from typing import Any, Dict, List
 15
 16import requests
 17
 18
 19def get_all_artists(base_url: str, api_key: str) -> List[Dict[str, Any]]:
 20    """Fetch all artists from Lidarr API."""
 21    url = f"{base_url}/api/v1/artist"
 22    headers = {"X-Api-Key": api_key}
 23
 24    try:
 25        response = requests.get(url, headers=headers)
 26        response.raise_for_status()
 27        return response.json()
 28    except requests.exceptions.RequestException as e:
 29        print(f"Error fetching artists: {e}", file=sys.stderr)
 30        sys.exit(1)
 31
 32
 33def update_artist_path(
 34    base_url: str, api_key: str, artist_id: int, new_path: str
 35) -> bool:
 36    """Update an artist's path via Lidarr API."""
 37    url = f"{base_url}/api/v1/artist/{artist_id}"
 38    headers = {"X-Api-Key": api_key, "Content-Type": "application/json"}
 39
 40    # First, get the current artist data
 41    try:
 42        response = requests.get(url, headers=headers)
 43        response.raise_for_status()
 44        artist_data = response.json()
 45    except requests.exceptions.RequestException as e:
 46        print(f"Error fetching artist {artist_id}: {e}", file=sys.stderr)
 47        return False
 48
 49    # Update the path
 50    artist_data["path"] = new_path
 51
 52    # Send the update
 53    try:
 54        response = requests.put(url, headers=headers, json=artist_data)
 55        response.raise_for_status()
 56        return True
 57    except requests.exceptions.RequestException as e:
 58        print(f"Error updating artist {artist_id}: {e}", file=sys.stderr)
 59        return False
 60
 61
 62def run(url: str, api_key: str, music_folder: str, dry_run: bool):
 63    """Execute the lidarr update-paths command."""
 64    # Normalize URLs and paths
 65    base_url = url.rstrip("/")
 66    music_folder_path = Path(music_folder).resolve()
 67    library_folder = music_folder_path / "library"
 68
 69    print(f"Fetching artists from {base_url}...")
 70    artists = get_all_artists(base_url, api_key)
 71    print(f"Found {len(artists)} artists\n")
 72
 73    needs_update = []
 74    already_in_library = []
 75    unknown_location = []
 76
 77    # Analyze all artists
 78    for artist in artists:
 79        artist_name = artist.get("artistName", "Unknown")
 80        current_path = Path(artist.get("path", ""))
 81        artist_id = artist.get("id")
 82
 83        # Check if path is directly in music_folder
 84        if current_path.parent == music_folder_path:
 85            new_path = library_folder / current_path.name
 86            needs_update.append(
 87                (artist_id, artist_name, current_path, new_path)
 88            )
 89        # Check if already in library subfolder
 90        elif "library" in current_path.parts:
 91            already_in_library.append((artist_name, current_path))
 92        else:
 93            unknown_location.append((artist_name, current_path))
 94
 95    # Print summary
 96    print("=" * 80)
 97    print("SUMMARY")
 98    print("=" * 80)
 99
100    if already_in_library:
101        msg = f"\n✓ Already in library folder ({len(already_in_library)} "
102        msg += "artists):"
103        print(msg)
104        for name, path in already_in_library[:5]:  # Show first 5
105            print(f"  - {name}: {path}")
106        if len(already_in_library) > 5:
107            print(f"  ... and {len(already_in_library) - 5} more")
108
109    if needs_update:
110        print(f"\n→ Needs update ({len(needs_update)} artists):")
111        for artist_id, name, old_path, new_path in needs_update:
112            print(f"  - {name}:")
113            print(f"      FROM: {old_path}")
114            print(f"      TO:   {new_path}")
115
116    if unknown_location:
117        print(f"\n⚠ Unknown location ({len(unknown_location)} artists):")
118        for name, path in unknown_location[:5]:  # Show first 5
119            print(f"  - {name}: {path}")
120        if len(unknown_location) > 5:
121            print(f"  ... and {len(unknown_location) - 5} more")
122
123    # Perform updates
124    if needs_update and not dry_run:
125        print(f"\n{'=' * 80}")
126        print("UPDATING PATHS")
127        print("=" * 80)
128
129        success_count = 0
130        fail_count = 0
131
132        for artist_id, name, old_path, new_path in needs_update:
133            print(f"\nUpdating {name}...", end=" ")
134            success = update_artist_path(
135                base_url, api_key, artist_id, str(new_path)
136            )
137            if success:
138                print("✓ SUCCESS")
139                success_count += 1
140            else:
141                print("✗ FAILED")
142                fail_count += 1
143
144        print(f"\n{'=' * 80}")
145        print(f"Results: {success_count} updated, {fail_count} failed")
146        print("=" * 80)
147    elif needs_update and dry_run:
148        print(
149            "\n[DRY RUN] No changes were made. "
150            "Remove --dry-run to apply updates."
151        )
152    else:
153        print("\nNo artists need updating!")