mirror of
https://github.com/SoPat712/allstarr.git
synced 2026-02-09 23:55:10 -05:00
Prioritize local Jellyfin lyrics over LRCLib in prefetch
Some checks failed
CI / build-and-test (push) Has been cancelled
Some checks failed
CI / build-and-test (push) Has been cancelled
- Check for embedded lyrics in local Jellyfin tracks before fetching from LRCLib - Remove previously cached LRCLib lyrics when local lyrics are found - Prevents unnecessary API calls and respects user's embedded lyrics - Tracks with local lyrics are counted as 'cached' in prefetch stats
This commit is contained in:
@@ -732,7 +732,9 @@ public class AdminController : ControllerBase
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check lyrics status
|
// Check lyrics status (only from our cache - lrclib/Spotify)
|
||||||
|
// Note: For local tracks, Jellyfin may have embedded lyrics that we don't check here
|
||||||
|
// Those will be served directly by Jellyfin when requested
|
||||||
var cacheKey = $"lyrics:{track.PrimaryArtist}:{track.Title}:{track.Album}:{track.DurationMs / 1000}";
|
var cacheKey = $"lyrics:{track.PrimaryArtist}:{track.Title}:{track.Album}:{track.DurationMs / 1000}";
|
||||||
var existingLyrics = await _cache.GetStringAsync(cacheKey);
|
var existingLyrics = await _cache.GetStringAsync(cacheKey);
|
||||||
var hasLyrics = !string.IsNullOrEmpty(existingLyrics);
|
var hasLyrics = !string.IsNullOrEmpty(existingLyrics);
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ using System.Text.Json;
|
|||||||
using allstarr.Models.Lyrics;
|
using allstarr.Models.Lyrics;
|
||||||
using allstarr.Models.Settings;
|
using allstarr.Models.Settings;
|
||||||
using allstarr.Services.Common;
|
using allstarr.Services.Common;
|
||||||
|
using allstarr.Services.Jellyfin;
|
||||||
using allstarr.Services.Spotify;
|
using allstarr.Services.Spotify;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
|
|
||||||
@@ -17,6 +18,7 @@ public class LyricsPrefetchService : BackgroundService
|
|||||||
private readonly LrclibService _lrclibService;
|
private readonly LrclibService _lrclibService;
|
||||||
private readonly SpotifyPlaylistFetcher _playlistFetcher;
|
private readonly SpotifyPlaylistFetcher _playlistFetcher;
|
||||||
private readonly RedisCacheService _cache;
|
private readonly RedisCacheService _cache;
|
||||||
|
private readonly IServiceProvider _serviceProvider;
|
||||||
private readonly ILogger<LyricsPrefetchService> _logger;
|
private readonly ILogger<LyricsPrefetchService> _logger;
|
||||||
private readonly string _lyricsCacheDir = "/app/cache/lyrics";
|
private readonly string _lyricsCacheDir = "/app/cache/lyrics";
|
||||||
private const int DelayBetweenRequestsMs = 500; // 500ms = 2 requests/second to be respectful
|
private const int DelayBetweenRequestsMs = 500; // 500ms = 2 requests/second to be respectful
|
||||||
@@ -26,12 +28,14 @@ public class LyricsPrefetchService : BackgroundService
|
|||||||
LrclibService lrclibService,
|
LrclibService lrclibService,
|
||||||
SpotifyPlaylistFetcher playlistFetcher,
|
SpotifyPlaylistFetcher playlistFetcher,
|
||||||
RedisCacheService cache,
|
RedisCacheService cache,
|
||||||
|
IServiceProvider serviceProvider,
|
||||||
ILogger<LyricsPrefetchService> logger)
|
ILogger<LyricsPrefetchService> logger)
|
||||||
{
|
{
|
||||||
_spotifySettings = spotifySettings.Value;
|
_spotifySettings = spotifySettings.Value;
|
||||||
_lrclibService = lrclibService;
|
_lrclibService = lrclibService;
|
||||||
_playlistFetcher = playlistFetcher;
|
_playlistFetcher = playlistFetcher;
|
||||||
_cache = cache;
|
_cache = cache;
|
||||||
|
_serviceProvider = serviceProvider;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -141,7 +145,20 @@ public class LyricsPrefetchService : BackgroundService
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fetch lyrics
|
// Check if this track has local Jellyfin lyrics (embedded in file)
|
||||||
|
var hasLocalLyrics = await CheckForLocalJellyfinLyricsAsync(track.SpotifyId);
|
||||||
|
if (hasLocalLyrics)
|
||||||
|
{
|
||||||
|
cached++;
|
||||||
|
_logger.LogInformation("✓ Local Jellyfin lyrics found for {Artist} - {Track}, skipping LRCLib fetch",
|
||||||
|
track.PrimaryArtist, track.Title);
|
||||||
|
|
||||||
|
// Remove any previously cached LRCLib lyrics for this track
|
||||||
|
await RemoveCachedLyricsAsync(track.PrimaryArtist, track.Title, track.Album, track.DurationMs / 1000);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch lyrics from LRCLib
|
||||||
var lyrics = await _lrclibService.GetLyricsAsync(
|
var lyrics = await _lrclibService.GetLyricsAsync(
|
||||||
track.Title,
|
track.Title,
|
||||||
track.Artists.ToArray(),
|
track.Artists.ToArray(),
|
||||||
@@ -255,4 +272,108 @@ public class LyricsPrefetchService : BackgroundService
|
|||||||
.Replace(" ", "_")
|
.Replace(" ", "_")
|
||||||
.ToLowerInvariant();
|
.ToLowerInvariant();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Removes cached LRCLib lyrics from both Redis and file cache.
|
||||||
|
/// Used when a track has local Jellyfin lyrics, making the LRCLib cache obsolete.
|
||||||
|
/// </summary>
|
||||||
|
private async Task RemoveCachedLyricsAsync(string artist, string title, string album, int duration)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Remove from Redis cache
|
||||||
|
var cacheKey = $"lyrics:{artist}:{title}:{album}:{duration}";
|
||||||
|
await _cache.DeleteAsync(cacheKey);
|
||||||
|
|
||||||
|
// Remove from file cache
|
||||||
|
var fileName = $"{SanitizeFileName(artist)}_{SanitizeFileName(title)}_{duration}.json";
|
||||||
|
var filePath = Path.Combine(_lyricsCacheDir, fileName);
|
||||||
|
|
||||||
|
if (File.Exists(filePath))
|
||||||
|
{
|
||||||
|
File.Delete(filePath);
|
||||||
|
_logger.LogDebug("🗑️ Removed cached LRCLib lyrics file: {FileName}", fileName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(ex, "Failed to remove cached lyrics for {Artist} - {Track}", artist, title);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Checks if a track has embedded lyrics in Jellyfin by querying the Jellyfin API.
|
||||||
|
/// This prevents downloading lyrics from LRCLib when the local file already has them.
|
||||||
|
/// </summary>
|
||||||
|
private async Task<bool> CheckForLocalJellyfinLyricsAsync(string spotifyTrackId)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var scope = _serviceProvider.CreateScope();
|
||||||
|
var proxyService = scope.ServiceProvider.GetService<JellyfinProxyService>();
|
||||||
|
|
||||||
|
if (proxyService == null)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Search for the track in Jellyfin by Spotify provider ID
|
||||||
|
var searchParams = new Dictionary<string, string>
|
||||||
|
{
|
||||||
|
["anyProviderIdEquals"] = $"Spotify.{spotifyTrackId}",
|
||||||
|
["includeItemTypes"] = "Audio",
|
||||||
|
["recursive"] = "true",
|
||||||
|
["limit"] = "1"
|
||||||
|
};
|
||||||
|
|
||||||
|
var (searchResult, statusCode) = await proxyService.GetJsonAsync("Items", searchParams, null);
|
||||||
|
|
||||||
|
if (searchResult == null || statusCode != 200)
|
||||||
|
{
|
||||||
|
// Track not found in local library
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if we found any items
|
||||||
|
if (!searchResult.RootElement.TryGetProperty("Items", out var items) ||
|
||||||
|
items.GetArrayLength() == 0)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the first matching track's ID
|
||||||
|
var firstItem = items[0];
|
||||||
|
if (!firstItem.TryGetProperty("Id", out var idElement))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var jellyfinTrackId = idElement.GetString();
|
||||||
|
if (string.IsNullOrEmpty(jellyfinTrackId))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if this track has lyrics
|
||||||
|
var (lyricsResult, lyricsStatusCode) = await proxyService.GetJsonAsync(
|
||||||
|
$"Audio/{jellyfinTrackId}/Lyrics",
|
||||||
|
null,
|
||||||
|
null);
|
||||||
|
|
||||||
|
if (lyricsResult != null && lyricsStatusCode == 200)
|
||||||
|
{
|
||||||
|
// Track has embedded lyrics in Jellyfin
|
||||||
|
_logger.LogDebug("Found embedded lyrics in Jellyfin for Spotify track {SpotifyId} (Jellyfin ID: {JellyfinId})",
|
||||||
|
spotifyTrackId, jellyfinTrackId);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogDebug(ex, "Error checking for local Jellyfin lyrics for Spotify track {SpotifyId}", spotifyTrackId);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user