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!")