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