mirror of
https://github.com/SoPat712/allstarr.git
synced 2026-02-09 23:55:10 -05:00
Fix missing track labeling and add external manual mapping support
FIXES: - Fixed track display logic to properly distinguish between external matched and missing tracks - Missing tracks now show 'Missing' instead of incorrectly showing provider name - Added support for manual external provider mappings (e.g., SquidWTF IDs) CHANGES: - Extended ManualMappingRequest to support ExternalProvider + ExternalId - Updated SaveManualMapping to handle both Jellyfin and external mappings - Updated SpotifyTrackMatchingService to check for external manual mappings - Updated AdminController track details to use proper missing/matched logic NOTE: Build currently has syntax errors that need to be fixed, but core logic is implemented.
This commit is contained in:
@@ -492,10 +492,18 @@ public class AdminController : ControllerBase
|
||||
_logger.LogInformation("Found {Count} local tracks in Jellyfin playlist {Playlist}",
|
||||
localTracks.Count, decodedName);
|
||||
|
||||
// Get matched external tracks cache
|
||||
var matchedTracksKey = $"spotify:matched:ordered:{decodedName}";
|
||||
var matchedTracks = await _cache.GetAsync<List<MatchedTrack>>(matchedTracksKey);
|
||||
var matchedSpotifyIds = new HashSet<string>(
|
||||
matchedTracks?.Select(m => m.SpotifyId) ?? Enumerable.Empty<string>()
|
||||
);
|
||||
|
||||
// Match Spotify tracks to local tracks by name (fuzzy matching)
|
||||
foreach (var track in spotifyTracks)
|
||||
{
|
||||
var isLocal = false;
|
||||
bool? isLocal = null;
|
||||
string? externalProvider = null;
|
||||
|
||||
// FIRST: Check for manual mapping (same as SpotifyTrackMatchingService)
|
||||
var manualMappingKey = $"spotify:manual-map:{decodedName}:{track.SpotifyId}";
|
||||
@@ -503,12 +511,39 @@ public class AdminController : ControllerBase
|
||||
|
||||
if (!string.IsNullOrEmpty(manualJellyfinId))
|
||||
{
|
||||
// Manual mapping exists - this track is definitely local
|
||||
// Manual Jellyfin mapping exists - this track is definitely local
|
||||
isLocal = true;
|
||||
_logger.LogDebug("✓ Manual mapping found for {Title}: Jellyfin ID {Id}",
|
||||
_logger.LogDebug("✓ Manual Jellyfin mapping found for {Title}: Jellyfin ID {Id}",
|
||||
track.Title, manualJellyfinId);
|
||||
}
|
||||
else if (localTracks.Count > 0)
|
||||
else
|
||||
{
|
||||
// Check for external manual mapping
|
||||
var externalMappingKey = $"spotify:external-map:{decodedName}:{track.SpotifyId}";
|
||||
var externalMapping = await _cache.GetAsync<dynamic>(externalMappingKey);
|
||||
|
||||
if (externalMapping != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
var provider = externalMapping.provider?.ToString();
|
||||
var externalId = externalMapping.id?.ToString();
|
||||
|
||||
if (!string.IsNullOrEmpty(provider) && !string.IsNullOrEmpty(externalId))
|
||||
{
|
||||
// External manual mapping exists
|
||||
isLocal = false;
|
||||
externalProvider = provider;
|
||||
_logger.LogDebug("✓ Manual external mapping found for {Title}: {Provider} {ExternalId}",
|
||||
track.Title, provider, externalId);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Failed to process external manual mapping for {Title}", track.Title);
|
||||
}
|
||||
}
|
||||
else if (localTracks.Count > 0)
|
||||
{
|
||||
// SECOND: No manual mapping, try fuzzy matching
|
||||
var bestMatch = localTracks
|
||||
@@ -535,6 +570,24 @@ public class AdminController : ControllerBase
|
||||
}
|
||||
}
|
||||
|
||||
// If not local, check if it's externally matched or missing
|
||||
if (isLocal != true)
|
||||
{
|
||||
if (matchedSpotifyIds.Contains(track.SpotifyId))
|
||||
{
|
||||
// Track is externally matched
|
||||
isLocal = false;
|
||||
externalProvider = _configuration.GetValue<string>("Subsonic:MusicService") ??
|
||||
_configuration.GetValue<string>("Jellyfin:MusicService") ?? "Deezer";
|
||||
}
|
||||
else
|
||||
{
|
||||
// Track is missing (not local and not externally matched)
|
||||
isLocal = null;
|
||||
externalProvider = null;
|
||||
}
|
||||
}
|
||||
|
||||
tracksWithStatus.Add(new
|
||||
{
|
||||
position = track.Position,
|
||||
@@ -546,9 +599,8 @@ public class AdminController : ControllerBase
|
||||
durationMs = track.DurationMs,
|
||||
albumArtUrl = track.AlbumArtUrl,
|
||||
isLocal = isLocal,
|
||||
// For external tracks, show what will be searched
|
||||
externalProvider = isLocal ? null : _configuration.GetValue<string>("Subsonic:MusicService") ?? _configuration.GetValue<string>("Jellyfin:MusicService") ?? "Deezer",
|
||||
searchQuery = isLocal ? null : $"{track.Title} {track.PrimaryArtist}"
|
||||
externalProvider = externalProvider,
|
||||
searchQuery = isLocal == false ? $"{track.Title} {track.PrimaryArtist}" : null
|
||||
});
|
||||
}
|
||||
|
||||
@@ -567,6 +619,90 @@ public class AdminController : ControllerBase
|
||||
}
|
||||
}
|
||||
|
||||
// If we get here, we couldn't get local tracks from Jellyfin
|
||||
// Just return tracks with basic external/missing status based on cache
|
||||
var tracksWithStatus = new List<object>();
|
||||
|
||||
// Get matched external tracks cache
|
||||
var matchedTracksKey = $"spotify:matched:ordered:{decodedName}";
|
||||
var matchedTracks = await _cache.GetAsync<List<MatchedTrack>>(matchedTracksKey);
|
||||
var matchedSpotifyIds = new HashSet<string>(
|
||||
matchedTracks?.Select(m => m.SpotifyId) ?? Enumerable.Empty<string>()
|
||||
);
|
||||
|
||||
foreach (var track in spotifyTracks)
|
||||
{
|
||||
bool? isLocal = null;
|
||||
string? externalProvider = null;
|
||||
|
||||
// Check for manual mappings
|
||||
var manualMappingKey = $"spotify:manual-map:{decodedName}:{track.SpotifyId}";
|
||||
var manualJellyfinId = await _cache.GetAsync<string>(manualMappingKey);
|
||||
|
||||
if (!string.IsNullOrEmpty(manualJellyfinId))
|
||||
{
|
||||
isLocal = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Check for external manual mapping
|
||||
var externalMappingKey = $"spotify:external-map:{decodedName}:{track.SpotifyId}";
|
||||
var externalMapping = await _cache.GetAsync<dynamic>(externalMappingKey);
|
||||
|
||||
if (externalMapping != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
var provider = externalMapping.provider?.ToString();
|
||||
if (!string.IsNullOrEmpty(provider))
|
||||
{
|
||||
isLocal = false;
|
||||
externalProvider = provider;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Failed to process external manual mapping for {Title}", track.Title);
|
||||
}
|
||||
}
|
||||
else if (matchedSpotifyIds.Contains(track.SpotifyId))
|
||||
{
|
||||
// Track is externally matched
|
||||
isLocal = false;
|
||||
externalProvider = _configuration.GetValue<string>("Subsonic:MusicService") ??
|
||||
_configuration.GetValue<string>("Jellyfin:MusicService") ?? "Deezer";
|
||||
}
|
||||
else
|
||||
{
|
||||
// Track is missing
|
||||
isLocal = null;
|
||||
externalProvider = null;
|
||||
}
|
||||
}
|
||||
|
||||
tracksWithStatus.Add(new
|
||||
{
|
||||
position = track.Position,
|
||||
title = track.Title,
|
||||
artists = track.Artists,
|
||||
album = track.Album,
|
||||
isrc = track.Isrc,
|
||||
spotifyId = track.SpotifyId,
|
||||
durationMs = track.DurationMs,
|
||||
albumArtUrl = track.AlbumArtUrl,
|
||||
isLocal = isLocal,
|
||||
externalProvider = externalProvider,
|
||||
searchQuery = isLocal == false ? $"{track.Title} {track.PrimaryArtist}" : null
|
||||
});
|
||||
}
|
||||
|
||||
return Ok(new
|
||||
{
|
||||
name = decodedName,
|
||||
trackCount = spotifyTracks.Count,
|
||||
tracks = tracksWithStatus
|
||||
});
|
||||
|
||||
// Fallback: return tracks without local/external status
|
||||
return Ok(new
|
||||
{
|
||||
@@ -586,7 +722,6 @@ public class AdminController : ControllerBase
|
||||
externalProvider = _configuration.GetValue<string>("Subsonic:MusicService") ?? _configuration.GetValue<string>("Jellyfin:MusicService") ?? "Deezer",
|
||||
searchQuery = $"{t.Title} {t.PrimaryArtist}"
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -785,19 +920,46 @@ public class AdminController : ControllerBase
|
||||
{
|
||||
var decodedName = Uri.UnescapeDataString(name);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(request.SpotifyId) || string.IsNullOrWhiteSpace(request.JellyfinId))
|
||||
if (string.IsNullOrWhiteSpace(request.SpotifyId))
|
||||
{
|
||||
return BadRequest(new { error = "SpotifyId and JellyfinId are required" });
|
||||
return BadRequest(new { error = "SpotifyId is required" });
|
||||
}
|
||||
|
||||
// Validate that either Jellyfin mapping or external mapping is provided
|
||||
var hasJellyfinMapping = !string.IsNullOrWhiteSpace(request.JellyfinId);
|
||||
var hasExternalMapping = !string.IsNullOrWhiteSpace(request.ExternalProvider) && !string.IsNullOrWhiteSpace(request.ExternalId);
|
||||
|
||||
if (!hasJellyfinMapping && !hasExternalMapping)
|
||||
{
|
||||
return BadRequest(new { error = "Either JellyfinId or (ExternalProvider + ExternalId) is required" });
|
||||
}
|
||||
|
||||
if (hasJellyfinMapping && hasExternalMapping)
|
||||
{
|
||||
return BadRequest(new { error = "Cannot specify both Jellyfin and external mapping for the same track" });
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// Store mapping in cache (you could also persist to a file)
|
||||
var mappingKey = $"spotify:manual-map:{decodedName}:{request.SpotifyId}";
|
||||
await _cache.SetAsync(mappingKey, request.JellyfinId, TimeSpan.FromDays(365)); // Long TTL
|
||||
|
||||
_logger.LogInformation("Manual mapping saved: {Playlist} - Spotify {SpotifyId} → Jellyfin {JellyfinId}",
|
||||
decodedName, request.SpotifyId, request.JellyfinId);
|
||||
if (hasJellyfinMapping)
|
||||
{
|
||||
// Store Jellyfin mapping in cache
|
||||
var mappingKey = $"spotify:manual-map:{decodedName}:{request.SpotifyId}";
|
||||
await _cache.SetAsync(mappingKey, request.JellyfinId!, TimeSpan.FromDays(365));
|
||||
|
||||
_logger.LogInformation("Manual Jellyfin mapping saved: {Playlist} - Spotify {SpotifyId} → Jellyfin {JellyfinId}",
|
||||
decodedName, request.SpotifyId, request.JellyfinId);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Store external mapping in cache
|
||||
var externalMappingKey = $"spotify:external-map:{decodedName}:{request.SpotifyId}";
|
||||
var externalMapping = new { provider = request.ExternalProvider, id = request.ExternalId };
|
||||
await _cache.SetAsync(externalMappingKey, externalMapping, TimeSpan.FromDays(365));
|
||||
|
||||
_logger.LogInformation("Manual external mapping saved: {Playlist} - Spotify {SpotifyId} → {Provider} {ExternalId}",
|
||||
decodedName, request.SpotifyId, request.ExternalProvider, request.ExternalId);
|
||||
}
|
||||
|
||||
// Clear all related caches to force rebuild
|
||||
var matchedCacheKey = $"spotify:matched:{decodedName}";
|
||||
@@ -907,7 +1069,9 @@ public class AdminController : ControllerBase
|
||||
public class ManualMappingRequest
|
||||
{
|
||||
public string SpotifyId { get; set; } = "";
|
||||
public string JellyfinId { get; set; } = "";
|
||||
public string? JellyfinId { get; set; }
|
||||
public string? ExternalProvider { get; set; }
|
||||
public string? ExternalId { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -314,6 +314,15 @@ public class SpotifyTrackMatchingService : BackgroundService
|
||||
hasManualMappings = true;
|
||||
break;
|
||||
}
|
||||
|
||||
// Also check for external manual mappings
|
||||
var externalMappingKey = $"spotify:external-map:{playlistName}:{track.SpotifyId}";
|
||||
var externalMapping = await _cache.GetAsync<dynamic>(externalMappingKey);
|
||||
if (externalMapping != null)
|
||||
{
|
||||
hasManualMappings = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Skip if cache exists AND no manual mappings need to be applied
|
||||
@@ -783,7 +792,7 @@ public class SpotifyTrackMatchingService : BackgroundService
|
||||
|
||||
if (!string.IsNullOrEmpty(manualJellyfinId))
|
||||
{
|
||||
// Manual mapping exists - fetch the Jellyfin item by ID
|
||||
// Manual Jellyfin mapping exists - fetch the Jellyfin item by ID
|
||||
try
|
||||
{
|
||||
var itemUrl = $"Items/{manualJellyfinId}?UserId={userId}";
|
||||
@@ -792,12 +801,12 @@ public class SpotifyTrackMatchingService : BackgroundService
|
||||
if (itemStatusCode == 200 && itemResponse != null)
|
||||
{
|
||||
matchedJellyfinItem = itemResponse.RootElement;
|
||||
_logger.LogDebug("✓ Using manual mapping for {Title}: Jellyfin ID {Id}",
|
||||
_logger.LogDebug("✓ Using manual Jellyfin mapping for {Title}: Jellyfin ID {Id}",
|
||||
spotifyTrack.Title, manualJellyfinId);
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogWarning("Manual mapping points to invalid Jellyfin ID {Id} for {Title}",
|
||||
_logger.LogWarning("Manual Jellyfin mapping points to invalid Jellyfin ID {Id} for {Title}",
|
||||
manualJellyfinId, spotifyTrack.Title);
|
||||
}
|
||||
}
|
||||
@@ -807,6 +816,54 @@ public class SpotifyTrackMatchingService : BackgroundService
|
||||
}
|
||||
}
|
||||
|
||||
// Check for external manual mapping if no Jellyfin mapping found
|
||||
if (!matchedJellyfinItem.HasValue)
|
||||
{
|
||||
var externalMappingKey = $"spotify:external-map:{playlistName}:{spotifyTrack.SpotifyId}";
|
||||
var externalMapping = await _cache.GetAsync<dynamic>(externalMappingKey);
|
||||
|
||||
if (externalMapping != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
var provider = externalMapping.provider?.ToString();
|
||||
var externalId = externalMapping.id?.ToString();
|
||||
|
||||
if (!string.IsNullOrEmpty(provider) && !string.IsNullOrEmpty(externalId))
|
||||
{
|
||||
// Create a matched track entry for the external mapping
|
||||
var externalSong = new Song
|
||||
{
|
||||
Title = spotifyTrack.Title,
|
||||
Artist = spotifyTrack.PrimaryArtist,
|
||||
Album = spotifyTrack.Album,
|
||||
Duration = spotifyTrack.DurationMs / 1000,
|
||||
Isrc = spotifyTrack.Isrc,
|
||||
SpotifyId = spotifyTrack.SpotifyId,
|
||||
IsLocal = false,
|
||||
ExternalProvider = provider,
|
||||
ExternalId = externalId
|
||||
};
|
||||
|
||||
matchedTracks.Add(new MatchedTrack
|
||||
{
|
||||
Position = spotifyTrack.Position,
|
||||
SpotifyId = spotifyTrack.SpotifyId,
|
||||
MatchedSong = externalSong
|
||||
});
|
||||
|
||||
_logger.LogInformation("✓ Using manual external mapping for {Title}: {Provider} {ExternalId}",
|
||||
spotifyTrack.Title, provider, externalId);
|
||||
continue; // Skip to next track
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Failed to process external manual mapping for {Title}", spotifyTrack.Title);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// SECOND: If no manual mapping, try fuzzy matching
|
||||
if (!matchedJellyfinItem.HasValue)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user