main
1"""
2Fix duplicate path segments in Lidarr artist paths.
3
4This script:
51. Fetches all artists from Lidarr API
62. Detects duplicate path segments (e.g., /music/library/library/Artist)
73. Fixes them to remove the duplicate (e.g., /music/library/Artist)
84. Updates the artist paths via PUT /api/v1/artist/{id}
9"""
10
11
12from lib import ArrClient, CommandContext, print_section_header
13
14
15def strip_duplicate_path_segments(path: str) -> tuple[str, bool, str]:
16 """
17 Detect and strip duplicate path segments.
18
19 For example: /neo/music/library/library -> /neo/music/library
20
21 Returns:
22 Tuple of (cleaned_path, was_modified, duplicate_segment)
23 """
24 parts = [p for p in path.split('/') if p] # Split and remove empty parts
25
26 # Check for any duplicate consecutive segments
27 for i in range(len(parts) - 1):
28 if parts[i] == parts[i + 1]:
29 # Found duplicate - remove it
30 cleaned_parts = parts[:i + 1] + parts[i + 2:]
31 cleaned = '/' + '/'.join(cleaned_parts)
32 return (cleaned, True, parts[i])
33
34 return (path, False, "")
35
36
37def run(url: str, api_key: str, dry_run: bool, no_confirm: bool):
38 """Execute the lidarr fix-duplicate-paths command."""
39 # Create client and context
40 lidarr = ArrClient(url, api_key)
41 ctx = CommandContext(dry_run, no_confirm)
42
43 print_section_header("FETCHING ARTISTS FROM LIDARR")
44 print("Fetching all artists...")
45 artists = lidarr.get("/api/v1/artist")
46 print(f"Found {len(artists)} artists\n")
47
48 # Analyze all artists for duplicate path segments
49 needs_fixing = []
50 already_correct = []
51
52 for artist in artists:
53 artist_name = artist.get("artistName", "Unknown")
54 artist_id = artist.get("id")
55 current_path = artist.get("path", "")
56
57 cleaned_path, has_duplicate, duplicate_segment = strip_duplicate_path_segments(current_path)
58
59 if has_duplicate:
60 needs_fixing.append({
61 "id": artist_id,
62 "name": artist_name,
63 "old_path": current_path,
64 "new_path": cleaned_path,
65 "duplicate": duplicate_segment,
66 })
67 else:
68 already_correct.append(artist_name)
69
70 # Print summary
71 print_section_header("SUMMARY")
72
73 if already_correct:
74 print(f"\n✓ Already correct ({len(already_correct)} artists)")
75 if len(already_correct) <= 10:
76 for name in already_correct:
77 print(f" - {name}")
78 else:
79 for name in already_correct[:5]:
80 print(f" - {name}")
81 print(f" ... and {len(already_correct) - 5} more")
82
83 if needs_fixing:
84 print(f"\n→ Needs fixing ({len(needs_fixing)} artists):\n")
85 for artist_info in needs_fixing:
86 print(f" • {artist_info['name']}")
87 print(f" Duplicate segment: '{artist_info['duplicate']}'")
88 print(f" FROM: {artist_info['old_path']}")
89 print(f" TO: {artist_info['new_path']}")
90 print()
91 else:
92 print("\n✓ No artists with duplicate path segments found!")
93 return
94
95 # Perform fixes
96 if not ctx.dry_run:
97 # Ask for confirmation
98 if not ctx.no_confirm:
99 response = input(f"\nFix {len(needs_fixing)} artist path(s)? (y/n): ").lower().strip()
100 if response not in ["y", "yes"]:
101 print("Operation cancelled")
102 return
103
104 print_section_header("FIXING PATHS")
105
106 success_count = 0
107 fail_count = 0
108
109 for artist_info in needs_fixing:
110 artist_id = artist_info["id"]
111 artist_name = artist_info["name"]
112 new_path = artist_info["new_path"]
113
114 print(f"\nFixing {artist_name}...", end=" ")
115
116 # Fetch current artist data
117 artist_data = lidarr.get(f"/api/v1/artist/{artist_id}")
118 if not artist_data:
119 print("✗ FAILED (could not fetch artist data)")
120 fail_count += 1
121 continue
122
123 # Update the path
124 artist_data["path"] = new_path
125
126 # Send the update
127 result = lidarr.put(f"/api/v1/artist/{artist_id}", artist_data)
128 if result:
129 print("✓ SUCCESS")
130 success_count += 1
131 else:
132 print("✗ FAILED")
133 fail_count += 1
134
135 print_section_header("FINAL SUMMARY")
136 print(f"\nTotal artists processed: {len(needs_fixing)}")
137 print(f" - Successfully fixed: {success_count}")
138 print(f" - Failed: {fail_count}")
139
140 if success_count > 0:
141 print("\n✓ Path fixes have been applied!")
142 print("Note: You may need to trigger a 'Rescan Artist Folder' in Lidarr")
143 print(" for the changes to take full effect.")
144 else:
145 print("\n[DRY RUN] No changes were made. Remove --dry-run to apply fixes.")