mirror of
https://github.com/SoPat712/allstarr.git
synced 2026-02-10 16:08:39 -05:00
Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
713ecd4ec8
|
|||
|
0ff1e3a428
|
|||
|
cef18b9482
|
@@ -279,53 +279,50 @@ public class JellyfinController : ControllerBase
|
|||||||
// Parse Jellyfin results into domain models
|
// Parse Jellyfin results into domain models
|
||||||
var (localSongs, localAlbums, localArtists) = _modelMapper.ParseItemsResponse(jellyfinResult);
|
var (localSongs, localAlbums, localArtists) = _modelMapper.ParseItemsResponse(jellyfinResult);
|
||||||
|
|
||||||
// Respect source ordering (SquidWTF/Tidal has better search ranking than our fuzzy matching)
|
// Sort all results by match score (local tracks get +10 boost)
|
||||||
// Just interleave local and external results based on which source has better overall match
|
// This ensures best matches appear first regardless of source
|
||||||
|
var allSongs = localSongs.Concat(externalResult.Songs)
|
||||||
// Calculate average match score for each source to determine which should come first
|
.Select(s => new { Song = s, Score = FuzzyMatcher.CalculateSimilarity(cleanQuery, s.Title) + (s.IsLocal ? 10.0 : 0.0) })
|
||||||
var localSongsAvgScore = localSongs.Any()
|
.OrderByDescending(x => x.Score)
|
||||||
? localSongs.Average(s => FuzzyMatcher.CalculateSimilarity(cleanQuery, s.Title))
|
.Select(x => x.Song)
|
||||||
: 0.0;
|
.ToList();
|
||||||
var externalSongsAvgScore = externalResult.Songs.Any()
|
|
||||||
? externalResult.Songs.Average(s => FuzzyMatcher.CalculateSimilarity(cleanQuery, s.Title))
|
|
||||||
: 0.0;
|
|
||||||
|
|
||||||
var localAlbumsAvgScore = localAlbums.Any()
|
var allAlbums = localAlbums.Concat(externalResult.Albums)
|
||||||
? localAlbums.Average(a => FuzzyMatcher.CalculateSimilarity(cleanQuery, a.Title))
|
.Select(a => new { Album = a, Score = FuzzyMatcher.CalculateSimilarity(cleanQuery, a.Title) + (a.IsLocal ? 10.0 : 0.0) })
|
||||||
: 0.0;
|
.OrderByDescending(x => x.Score)
|
||||||
var externalAlbumsAvgScore = externalResult.Albums.Any()
|
.Select(x => x.Album)
|
||||||
? externalResult.Albums.Average(a => FuzzyMatcher.CalculateSimilarity(cleanQuery, a.Title))
|
.ToList();
|
||||||
: 0.0;
|
|
||||||
|
|
||||||
var localArtistsAvgScore = localArtists.Any()
|
var allArtists = localArtists.Concat(externalResult.Artists)
|
||||||
? localArtists.Average(a => FuzzyMatcher.CalculateSimilarity(cleanQuery, a.Name))
|
.Select(a => new { Artist = a, Score = FuzzyMatcher.CalculateSimilarity(cleanQuery, a.Name) + (a.IsLocal ? 10.0 : 0.0) })
|
||||||
: 0.0;
|
.OrderByDescending(x => x.Score)
|
||||||
var externalArtistsAvgScore = externalResult.Artists.Any()
|
.Select(x => x.Artist)
|
||||||
? externalResult.Artists.Average(a => FuzzyMatcher.CalculateSimilarity(cleanQuery, a.Name))
|
.ToList();
|
||||||
: 0.0;
|
|
||||||
|
|
||||||
// Interleave results: put better-matching source first, preserve original ordering within each source
|
// Log top results for debugging
|
||||||
var allSongs = localSongsAvgScore >= externalSongsAvgScore
|
|
||||||
? localSongs.Concat(externalResult.Songs).ToList()
|
|
||||||
: externalResult.Songs.Concat(localSongs).ToList();
|
|
||||||
|
|
||||||
var allAlbums = localAlbumsAvgScore >= externalAlbumsAvgScore
|
|
||||||
? localAlbums.Concat(externalResult.Albums).ToList()
|
|
||||||
: externalResult.Albums.Concat(localAlbums).ToList();
|
|
||||||
|
|
||||||
var allArtists = localArtistsAvgScore >= externalArtistsAvgScore
|
|
||||||
? localArtists.Concat(externalResult.Artists).ToList()
|
|
||||||
: externalResult.Artists.Concat(localArtists).ToList();
|
|
||||||
|
|
||||||
// Log results for debugging
|
|
||||||
if (_logger.IsEnabled(LogLevel.Debug))
|
if (_logger.IsEnabled(LogLevel.Debug))
|
||||||
{
|
{
|
||||||
_logger.LogDebug("🎵 Songs: Local avg score={LocalScore:F2}, External avg score={ExtScore:F2}, Local first={LocalFirst}",
|
if (allSongs.Any())
|
||||||
localSongsAvgScore, externalSongsAvgScore, localSongsAvgScore >= externalSongsAvgScore);
|
{
|
||||||
_logger.LogDebug("💿 Albums: Local avg score={LocalScore:F2}, External avg score={ExtScore:F2}, Local first={LocalFirst}",
|
var topSong = allSongs.First();
|
||||||
localAlbumsAvgScore, externalAlbumsAvgScore, localAlbumsAvgScore >= externalAlbumsAvgScore);
|
var topScore = FuzzyMatcher.CalculateSimilarity(cleanQuery, topSong.Title) + (topSong.IsLocal ? 10.0 : 0.0);
|
||||||
_logger.LogDebug("🎤 Artists: Local avg score={LocalScore:F2}, External avg score={ExtScore:F2}, Local first={LocalFirst}",
|
_logger.LogDebug("🎵 Top song: '{Title}' (local={IsLocal}, score={Score:F2})",
|
||||||
localArtistsAvgScore, externalArtistsAvgScore, localArtistsAvgScore >= externalArtistsAvgScore);
|
topSong.Title, topSong.IsLocal, topScore);
|
||||||
|
}
|
||||||
|
if (allAlbums.Any())
|
||||||
|
{
|
||||||
|
var topAlbum = allAlbums.First();
|
||||||
|
var topScore = FuzzyMatcher.CalculateSimilarity(cleanQuery, topAlbum.Title) + (topAlbum.IsLocal ? 10.0 : 0.0);
|
||||||
|
_logger.LogDebug("💿 Top album: '{Title}' (local={IsLocal}, score={Score:F2})",
|
||||||
|
topAlbum.Title, topAlbum.IsLocal, topScore);
|
||||||
|
}
|
||||||
|
if (allArtists.Any())
|
||||||
|
{
|
||||||
|
var topArtist = allArtists.First();
|
||||||
|
var topScore = FuzzyMatcher.CalculateSimilarity(cleanQuery, topArtist.Name) + (topArtist.IsLocal ? 10.0 : 0.0);
|
||||||
|
_logger.LogDebug("🎤 Top artist: '{Name}' (local={IsLocal}, score={Score:F2})",
|
||||||
|
topArtist.Name, topArtist.IsLocal, topScore);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert to Jellyfin format
|
// Convert to Jellyfin format
|
||||||
@@ -343,7 +340,7 @@ public class JellyfinController : ControllerBase
|
|||||||
mergedAlbums.AddRange(playlistItems);
|
mergedAlbums.AddRange(playlistItems);
|
||||||
}
|
}
|
||||||
|
|
||||||
_logger.LogInformation("Merged results (preserving source order): Songs={Songs}, Albums={Albums}, Artists={Artists}",
|
_logger.LogInformation("Merged and sorted results by score: Songs={Songs}, Albums={Albums}, Artists={Artists}",
|
||||||
mergedSongs.Count, mergedAlbums.Count, mergedArtists.Count);
|
mergedSongs.Count, mergedAlbums.Count, mergedArtists.Count);
|
||||||
|
|
||||||
// Pre-fetch lyrics for top 3 songs in background (don't await)
|
// Pre-fetch lyrics for top 3 songs in background (don't await)
|
||||||
|
|||||||
@@ -212,7 +212,18 @@ public class DeezerMetadataService : IMusicMetadataService
|
|||||||
// Enrich with MusicBrainz genres if missing
|
// Enrich with MusicBrainz genres if missing
|
||||||
if (_genreEnrichment != null && string.IsNullOrEmpty(song.Genre))
|
if (_genreEnrichment != null && string.IsNullOrEmpty(song.Genre))
|
||||||
{
|
{
|
||||||
await _genreEnrichment.EnrichSongGenreAsync(song);
|
// Fire-and-forget: don't block the response waiting for genre enrichment
|
||||||
|
_ = Task.Run(async () =>
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await _genreEnrichment.EnrichSongGenreAsync(song);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// Silently ignore genre enrichment failures
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return song;
|
return song;
|
||||||
|
|||||||
@@ -186,7 +186,18 @@ public class QobuzMetadataService : IMusicMetadataService
|
|||||||
// Enrich with MusicBrainz genres if missing
|
// Enrich with MusicBrainz genres if missing
|
||||||
if (_genreEnrichment != null && song != null && string.IsNullOrEmpty(song.Genre))
|
if (_genreEnrichment != null && song != null && string.IsNullOrEmpty(song.Genre))
|
||||||
{
|
{
|
||||||
await _genreEnrichment.EnrichSongGenreAsync(song);
|
// Fire-and-forget: don't block the response waiting for genre enrichment
|
||||||
|
_ = Task.Run(async () =>
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await _genreEnrichment.EnrichSongGenreAsync(song);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogDebug(ex, "Failed to enrich genre for {Title}", song.Title);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return song;
|
return song;
|
||||||
|
|||||||
@@ -1302,6 +1302,61 @@ public class SpotifyTrackMatchingService : BackgroundService
|
|||||||
|
|
||||||
if (finalItems.Count > 0)
|
if (finalItems.Count > 0)
|
||||||
{
|
{
|
||||||
|
// Enrich external tracks with genres from MusicBrainz
|
||||||
|
if (externalUsedCount > 0)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var genreEnrichment = _serviceProvider.GetService<GenreEnrichmentService>();
|
||||||
|
if (genreEnrichment != null)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("🎨 Enriching {Count} external tracks with genres from MusicBrainz...", externalUsedCount);
|
||||||
|
|
||||||
|
// Extract external songs from matched tracks
|
||||||
|
var externalSongs = matchedTracks
|
||||||
|
.Where(t => t.MatchedSong != null && !t.MatchedSong.IsLocal)
|
||||||
|
.Select(t => t.MatchedSong!)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
// Enrich genres in parallel
|
||||||
|
await genreEnrichment.EnrichSongsGenresAsync(externalSongs);
|
||||||
|
|
||||||
|
// Update the genres in finalItems
|
||||||
|
foreach (var item in finalItems)
|
||||||
|
{
|
||||||
|
if (item.TryGetValue("Id", out var idObj) && idObj is string id && id.StartsWith("ext-"))
|
||||||
|
{
|
||||||
|
// Find the corresponding song
|
||||||
|
var song = externalSongs.FirstOrDefault(s => s.Id == id);
|
||||||
|
if (song != null && !string.IsNullOrEmpty(song.Genre))
|
||||||
|
{
|
||||||
|
// Update Genres array
|
||||||
|
item["Genres"] = new[] { song.Genre };
|
||||||
|
|
||||||
|
// Update GenreItems array
|
||||||
|
item["GenreItems"] = new[]
|
||||||
|
{
|
||||||
|
new Dictionary<string, object?>
|
||||||
|
{
|
||||||
|
["Name"] = song.Genre,
|
||||||
|
["Id"] = $"genre-{song.Genre.ToLowerInvariant()}"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
_logger.LogDebug("✓ Enriched {Title} with genre: {Genre}", song.Title, song.Genre);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.LogInformation("✅ Genre enrichment complete for {Playlist}", playlistName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(ex, "Failed to enrich genres for {Playlist}, continuing without genres", playlistName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Save to Redis cache with same expiration as matched tracks (until next cron run)
|
// Save to Redis cache with same expiration as matched tracks (until next cron run)
|
||||||
var cacheKey = $"spotify:playlist:items:{playlistName}";
|
var cacheKey = $"spotify:playlist:items:{playlistName}";
|
||||||
await _cache.SetAsync(cacheKey, finalItems, cacheExpiration);
|
await _cache.SetAsync(cacheKey, finalItems, cacheExpiration);
|
||||||
|
|||||||
@@ -292,7 +292,18 @@ public class SquidWTFMetadataService : IMusicMetadataService
|
|||||||
// Enrich with MusicBrainz genres if missing (SquidWTF/Tidal doesn't provide genres)
|
// Enrich with MusicBrainz genres if missing (SquidWTF/Tidal doesn't provide genres)
|
||||||
if (_genreEnrichment != null && string.IsNullOrEmpty(song.Genre))
|
if (_genreEnrichment != null && string.IsNullOrEmpty(song.Genre))
|
||||||
{
|
{
|
||||||
await _genreEnrichment.EnrichSongGenreAsync(song);
|
// Fire-and-forget: don't block the response waiting for genre enrichment
|
||||||
|
_ = Task.Run(async () =>
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await _genreEnrichment.EnrichSongGenreAsync(song);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogDebug(ex, "Failed to enrich genre for {Title}", song.Title);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// NOTE: Spotify ID conversion happens during download (in SquidWTFDownloadService)
|
// NOTE: Spotify ID conversion happens during download (in SquidWTFDownloadService)
|
||||||
|
|||||||
Reference in New Issue
Block a user