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