fix: show both local and external artists with same name

- Artists with same name now appear separately (local + external [S])
- Fixed deduplication to keep one local AND one external per name
- Added logging to SquidWTF artist search to show actual URLs
- External artists get [S] suffix to distinguish from local
- Allows users to browse external artist albums not in local library
- TIDAL image URLs already correctly implemented with UUID splitting
This commit is contained in:
2026-02-06 19:41:06 -05:00
parent 99d701a355
commit 1d774111e7
2 changed files with 64 additions and 11 deletions

View File

@@ -294,14 +294,58 @@ public class JellyfinController : ControllerBase
.Select(x => x.Item) .Select(x => x.Item)
.ToList(); .ToList();
// Dedupe artists by name, keeping highest scored version // Dedupe artists by name, but KEEP both local and external versions
var artistScores = scoredLocalArtists.Concat(scoredExternalArtists) // Group by name, then for each name keep ONE local and ONE external (if both exist)
.GroupBy(x => x.Item.Name, StringComparer.OrdinalIgnoreCase) var artistsByName = scoredLocalArtists.Concat(scoredExternalArtists)
.Select(g => g.OrderByDescending(x => x.Score).First()) .GroupBy(x => x.Item.Name, StringComparer.OrdinalIgnoreCase);
var artistScores = new List<Artist>();
foreach (var group in artistsByName)
{
// Get best local artist (if any)
var bestLocal = group
.Where(x => x.Item.IsLocal)
.OrderByDescending(x => x.Score) .OrderByDescending(x => x.Score)
.Select(x => x.Item) .FirstOrDefault();
// Get best external artist (if any)
var bestExternal = group
.Where(x => !x.Item.IsLocal)
.OrderByDescending(x => x.Score)
.FirstOrDefault();
// Add both (if they exist)
if (bestLocal.Item != null)
{
artistScores.Add(bestLocal.Item);
}
if (bestExternal.Item != null)
{
artistScores.Add(bestExternal.Item);
}
}
// Sort by score
artistScores = artistScores
.OrderByDescending(a => {
// Find the score for this artist
var scored = scoredLocalArtists.Concat(scoredExternalArtists)
.FirstOrDefault(x => x.Item.Id == a.Id);
return scored.Score;
})
.ToList(); .ToList();
// Log deduplication details for debugging
if (_logger.IsEnabled(LogLevel.Debug))
{
var localArtistNames = scoredLocalArtists.Select(x => $"{x.Item.Name} (local, score: {x.Score:F2})").ToList();
var externalArtistNames = scoredExternalArtists.Select(x => $"{x.Item.Name} ({x.Item.ExternalProvider}, score: {x.Score:F2})").ToList();
_logger.LogDebug("🎤 Artist deduplication: Local={LocalArtists}, External={ExternalArtists}, Final={FinalCount}",
string.Join(", ", localArtistNames),
string.Join(", ", externalArtistNames),
artistScores.Count);
}
// Convert to Jellyfin format // Convert to Jellyfin format
var mergedSongs = allSongs.Select(s => _responseBuilder.ConvertSongToJellyfinItem(s)).ToList(); var mergedSongs = allSongs.Select(s => _responseBuilder.ConvertSongToJellyfinItem(s)).ToList();
var mergedAlbums = allAlbums.Select(a => _responseBuilder.ConvertAlbumToJellyfinItem(a)).ToList(); var mergedAlbums = allAlbums.Select(a => _responseBuilder.ConvertAlbumToJellyfinItem(a)).ToList();
@@ -680,27 +724,30 @@ public class JellyfinController : ControllerBase
} }
} }
// Merge and deduplicate by name // Merge and deduplicate by name, but KEEP both local and external versions
// This allows users to see both their local "Taylor Swift" and external "Taylor Swift [S]"
var artistNames = new HashSet<string>(StringComparer.OrdinalIgnoreCase); var artistNames = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
var mergedArtists = new List<Artist>(); var mergedArtists = new List<Artist>();
// Add all local artists first
foreach (var artist in localArtists) foreach (var artist in localArtists)
{ {
if (artistNames.Add(artist.Name)) if (artistNames.Add(artist.Name + ":local"))
{ {
mergedArtists.Add(artist); mergedArtists.Add(artist);
} }
} }
// Add all external artists (even if name matches local)
foreach (var artist in externalArtists) foreach (var artist in externalArtists)
{ {
if (artistNames.Add(artist.Name)) if (artistNames.Add(artist.Name + ":external"))
{ {
mergedArtists.Add(artist); mergedArtists.Add(artist);
} }
} }
_logger.LogInformation("Returning {Count} merged artists", mergedArtists.Count); _logger.LogInformation("Returning {Count} merged artists (local + external)", mergedArtists.Count);
// Convert to Jellyfin format // Convert to Jellyfin format
var artistItems = mergedArtists.Select(a => _responseBuilder.ConvertArtistToJellyfinItem(a)).ToList(); var artistItems = mergedArtists.Select(a => _responseBuilder.ConvertArtistToJellyfinItem(a)).ToList();

View File

@@ -176,10 +176,13 @@ public class SquidWTFMetadataService : IMusicMetadataService
return await TryWithFallbackAsync(async (baseUrl) => return await TryWithFallbackAsync(async (baseUrl) =>
{ {
var url = $"{baseUrl}/search/?a={Uri.EscapeDataString(query)}"; var url = $"{baseUrl}/search/?a={Uri.EscapeDataString(query)}";
_logger.LogInformation("🔍 SQUIDWTF: Searching artists with URL: {Url}", url);
var response = await _httpClient.GetAsync(url); var response = await _httpClient.GetAsync(url);
if (!response.IsSuccessStatusCode) if (!response.IsSuccessStatusCode)
{ {
_logger.LogWarning("⚠️ SQUIDWTF: Artist search failed with status {StatusCode}", response.StatusCode);
return new List<Artist>(); return new List<Artist>();
} }
@@ -196,11 +199,14 @@ public class SquidWTFMetadataService : IMusicMetadataService
{ {
if (count >= limit) break; if (count >= limit) break;
artists.Add(ParseTidalArtist(artist)); var parsedArtist = ParseTidalArtist(artist);
artists.Add(parsedArtist);
_logger.LogDebug("🎤 SQUIDWTF: Found artist: {Name} (ID: {Id})", parsedArtist.Name, parsedArtist.ExternalId);
count++; count++;
} }
} }
_logger.LogInformation("✓ SQUIDWTF: Artist search returned {Count} results", artists.Count);
return artists; return artists;
}, new List<Artist>()); }, new List<Artist>());
} }