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