diff --git a/allstarr/Controllers/ConfigController.cs b/allstarr/Controllers/ConfigController.cs index 43dc691a..d22fee89 100644 --- a/allstarr/Controllers/ConfigController.cs +++ b/allstarr/Controllers/ConfigController.cs @@ -148,6 +148,9 @@ public class ConfigController : ControllerBase providers = new { metadataOrder = GetEnvString(envVars, "MULTI_PROVIDER_METADATA_ORDER", "spotify,applemusic,deezer,qobuz,squidwtf"), + downloadOrder = GetEnvString(envVars, "MULTI_PROVIDER_DOWNLOAD_ORDER", "applemusic,deezer,qobuz,squidwtf"), + playlistOrder = GetEnvString(envVars, "MULTI_PROVIDER_PLAYLIST_ORDER", "spotify,applemusic,deezer,qobuz,squidwtf"), + lyricsOrder = GetEnvString(envVars, "MULTI_PROVIDER_LYRICS_ORDER", "spotify,lyricsplus,lrclib"), enabledSearch = GetEnvString(envVars, "MULTI_PROVIDER_ENABLED_SEARCH", "spotify,applemusic,deezer,qobuz,squidwtf"), enabledPlaylist = GetEnvString(envVars, "MULTI_PROVIDER_ENABLED_PLAYLIST", "spotify"), }, diff --git a/allstarr/Services/Common/MultiProviderDownloadService.cs b/allstarr/Services/Common/MultiProviderDownloadService.cs index 18c3b285..6b3270b2 100644 --- a/allstarr/Services/Common/MultiProviderDownloadService.cs +++ b/allstarr/Services/Common/MultiProviderDownloadService.cs @@ -169,9 +169,7 @@ public class MultiProviderDownloadService : IDownloadService private IReadOnlyList GetPrioritizedDownloadProviders() { - return _statusManager.GetEnabledSearchProviders() - .Where(p => p != "spotify") - .ToList(); + return _statusManager.GetEnabledDownloadProviders(); } private IDownloadService? GetDownloadServiceByName(string name) diff --git a/allstarr/Services/Common/ProviderStatusManager.cs b/allstarr/Services/Common/ProviderStatusManager.cs index 7df1e93c..b8a1eb0d 100644 --- a/allstarr/Services/Common/ProviderStatusManager.cs +++ b/allstarr/Services/Common/ProviderStatusManager.cs @@ -56,7 +56,7 @@ public class ProviderStatusManager public IReadOnlyList GetEnabledPlaylistProviders() { - var order = GetMetadataOrder(); + var order = GetPlaylistOrder(); var enabled = GetEnabledPlaylistRaw(); return order @@ -64,6 +64,19 @@ public class ProviderStatusManager .ToList(); } + public IReadOnlyList GetEnabledDownloadProviders() + { + var order = GetDownloadOrder(); + return order + .Where(p => IsProviderHealthy(p)) + .ToList(); + } + + public IReadOnlyList GetEnabledLyricsProviders() + { + return GetLyricsOrder(); + } + public bool IsProviderHealthy(string provider) { var prov = provider.ToLowerInvariant(); @@ -127,6 +140,30 @@ public class ProviderStatusManager .ToList(); } + private List GetDownloadOrder() + { + var val = _configuration["MULTI_PROVIDER_DOWNLOAD_ORDER"] ?? "applemusic,deezer,qobuz,squidwtf"; + return val.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) + .Select(s => s.ToLowerInvariant()) + .ToList(); + } + + private List GetPlaylistOrder() + { + var val = _configuration["MULTI_PROVIDER_PLAYLIST_ORDER"] ?? "spotify,applemusic,deezer,qobuz,squidwtf"; + return val.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) + .Select(s => s.ToLowerInvariant()) + .ToList(); + } + + private List GetLyricsOrder() + { + var val = _configuration["MULTI_PROVIDER_LYRICS_ORDER"] ?? "spotify,lyricsplus,lrclib"; + return val.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) + .Select(s => s.ToLowerInvariant()) + .ToList(); + } + private HashSet GetEnabledSearchRaw() { var val = _configuration["MULTI_PROVIDER_ENABLED_SEARCH"] ?? "spotify,applemusic,deezer,qobuz,squidwtf"; diff --git a/allstarr/Services/Lyrics/LyricsOrchestrator.cs b/allstarr/Services/Lyrics/LyricsOrchestrator.cs index fe6bd21e..b890f57e 100644 --- a/allstarr/Services/Lyrics/LyricsOrchestrator.cs +++ b/allstarr/Services/Lyrics/LyricsOrchestrator.cs @@ -1,12 +1,13 @@ using allstarr.Models.Lyrics; using allstarr.Models.Settings; using Microsoft.Extensions.Options; +using Microsoft.Extensions.Logging; +using allstarr.Services.Common; namespace allstarr.Services.Lyrics; /// /// Orchestrates lyrics fetching from multiple sources with priority-based fallback. -/// Priority order: Spotify → LyricsPlus → LRCLib /// Note: Jellyfin local lyrics are handled by the controller before calling this orchestrator. /// public class LyricsOrchestrator @@ -15,6 +16,7 @@ public class LyricsOrchestrator private readonly LyricsPlusService _lyricsPlus; private readonly LrclibService _lrclib; private readonly SpotifyApiSettings _spotifySettings; + private readonly ProviderStatusManager _statusManager; private readonly ILogger _logger; public LyricsOrchestrator( @@ -22,12 +24,14 @@ public class LyricsOrchestrator LyricsPlusService lyricsPlus, LrclibService lrclib, IOptions spotifySettings, + ProviderStatusManager statusManager, ILogger logger) { _spotifyLyrics = spotifyLyrics; _lyricsPlus = lyricsPlus; _lrclib = lrclib; _spotifySettings = spotifySettings.Value; + _statusManager = statusManager; _logger = logger; } @@ -49,31 +53,37 @@ public class LyricsOrchestrator string? spotifyTrackId = null) { var artistName = string.Join(", ", artistNames); - _logger.LogInformation("🎵 Fetching lyrics for: {Artist} - {Track}", artistName, trackName); - // 1. Try Spotify lyrics (if Spotify ID provided) - if (!string.IsNullOrEmpty(spotifyTrackId)) + var order = _statusManager.GetEnabledLyricsProviders(); + + foreach (var source in order) { - var spotifyLyrics = await TrySpotifyLyrics(spotifyTrackId, artistName, trackName); - if (spotifyLyrics != null) + try { - return spotifyLyrics; + if (source.Equals("spotify", StringComparison.OrdinalIgnoreCase)) + { + if (!string.IsNullOrEmpty(spotifyTrackId)) + { + var spotifyLyrics = await TrySpotifyLyrics(spotifyTrackId, artistName, trackName); + if (spotifyLyrics != null) return spotifyLyrics; + } + } + else if (source.Equals("lyricsplus", StringComparison.OrdinalIgnoreCase)) + { + var lyricsPlusLyrics = await TryLyricsPlusLyrics(trackName, artistNames, albumName, durationSeconds, artistName); + if (lyricsPlusLyrics != null) return lyricsPlusLyrics; + } + else if (source.Equals("lrclib", StringComparison.OrdinalIgnoreCase)) + { + var lrclibLyrics = await TryLrclibLyrics(trackName, artistNames, albumName, durationSeconds, artistName); + if (lrclibLyrics != null) return lrclibLyrics; + } + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed fetching lyrics from source: {Source}", source); } - } - - // 2. Try LyricsPlus - var lyricsPlusLyrics = await TryLyricsPlusLyrics(trackName, artistNames, albumName, durationSeconds, artistName); - if (lyricsPlusLyrics != null) - { - return lyricsPlusLyrics; - } - - // 3. Try LRCLib - var lrclibLyrics = await TryLrclibLyrics(trackName, artistNames, albumName, durationSeconds, artistName); - if (lrclibLyrics != null) - { - return lrclibLyrics; } _logger.LogInformation("❌ No lyrics found for: {Artist} - {Track}", artistName, trackName); @@ -92,36 +102,38 @@ public class LyricsOrchestrator string? spotifyTrackId = null) { var artistName = string.Join(", ", artistNames); - _logger.LogDebug("🎵 Prefetching lyrics for: {Artist} - {Track} (Spotify ID: {SpotifyId})", artistName, trackName, spotifyTrackId ?? "none"); - // 1. Try Spotify lyrics (if Spotify ID provided) - if (!string.IsNullOrEmpty(spotifyTrackId)) + var order = _statusManager.GetEnabledLyricsProviders(); + + foreach (var source in order) { - var spotifyLyrics = await TrySpotifyLyrics(spotifyTrackId, artistName, trackName); - if (spotifyLyrics != null) + try { - return true; + if (source.Equals("spotify", StringComparison.OrdinalIgnoreCase)) + { + if (!string.IsNullOrEmpty(spotifyTrackId)) + { + var spotifyLyrics = await TrySpotifyLyrics(spotifyTrackId, artistName, trackName); + if (spotifyLyrics != null) return true; + } + } + else if (source.Equals("lyricsplus", StringComparison.OrdinalIgnoreCase)) + { + var lyricsPlusLyrics = await TryLyricsPlusLyrics(trackName, artistNames, albumName, durationSeconds, artistName); + if (lyricsPlusLyrics != null) return true; + } + else if (source.Equals("lrclib", StringComparison.OrdinalIgnoreCase)) + { + var lrclibLyrics = await TryLrclibLyrics(trackName, artistNames, albumName, durationSeconds, artistName); + if (lrclibLyrics != null) return true; + } + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed prefetching lyrics from source: {Source}", source); } - } - else - { - _logger.LogDebug("No Spotify ID available for prefetch, skipping Spotify lyrics"); - } - - // 2. Try LyricsPlus - var lyricsPlusLyrics = await TryLyricsPlusLyrics(trackName, artistNames, albumName, durationSeconds, artistName); - if (lyricsPlusLyrics != null) - { - return true; - } - - // 3. Try LRCLib - var lrclibLyrics = await TryLrclibLyrics(trackName, artistNames, albumName, durationSeconds, artistName); - if (lrclibLyrics != null) - { - return true; } _logger.LogDebug("No lyrics found for prefetch: {Artist} - {Track}", artistName, trackName); diff --git a/allstarr/wwwroot/index.html b/allstarr/wwwroot/index.html index 69046b79..d4877768 100644 --- a/allstarr/wwwroot/index.html +++ b/allstarr/wwwroot/index.html @@ -949,6 +949,9 @@
+ + +

Multi-Service Providers & Priority

@@ -972,13 +975,47 @@
-

Metadata Search & Fallback Priority

-
- Drag and drop to rank search priorities (highest on top). Fallback downloads will proceed in this order. +
+
+

1. Metadata Search Priority

+
+ Priority for search results. +
+
    + +
+
+ +
+

2. Download & Playback Priority

+
+ Priority for downloads/streaming. +
+
    + +
+
+ +
+

3. Playlist Sync Priority

+
+ Priority for playlist matching. +
+
    + +
+
+ +
+

4. Lyrics Sourcing Priority

+
+ Priority for lyrics fetchers. +
+
    + +
+
-
    - -
diff --git a/allstarr/wwwroot/js/ui.js b/allstarr/wwwroot/js/ui.js index 09b71565..05970772 100644 --- a/allstarr/wwwroot/js/ui.js +++ b/allstarr/wwwroot/js/ui.js @@ -2050,15 +2050,25 @@ document.addEventListener("DOMContentLoaded", () => { function renderMultiProviderSettings(data) { const providersData = data.providers || { metadataOrder: "spotify,applemusic,deezer,qobuz,squidwtf", + downloadOrder: "applemusic,deezer,qobuz,squidwtf", + playlistOrder: "spotify,applemusic,deezer,qobuz,squidwtf", + lyricsOrder: "spotify,lyricsplus,lrclib", enabledSearch: "spotify,applemusic,deezer,qobuz,squidwtf", enabledPlaylist: "spotify" }; setInputValue("config-multi-provider-metadata-order", providersData.metadataOrder); + setInputValue("config-multi-provider-download-order", providersData.downloadOrder || "applemusic,deezer,qobuz,squidwtf"); + setInputValue("config-multi-provider-playlist-order", providersData.playlistOrder || "spotify,applemusic,deezer,qobuz,squidwtf"); + setInputValue("config-multi-provider-lyrics-order", providersData.lyricsOrder || "spotify,lyricsplus,lrclib"); setInputValue("config-multi-provider-enabled-search", providersData.enabledSearch); setInputValue("config-multi-provider-enabled-playlist", providersData.enabledPlaylist); const order = providersData.metadataOrder.split(',').map(s => s.trim().toLowerCase()); + const downloadOrder = (providersData.downloadOrder || "applemusic,deezer,qobuz,squidwtf").split(',').map(s => s.trim().toLowerCase()); + const playlistOrder = (providersData.playlistOrder || "spotify,applemusic,deezer,qobuz,squidwtf").split(',').map(s => s.trim().toLowerCase()); + const lyricsOrder = (providersData.lyricsOrder || "spotify,lyricsplus,lrclib").split(',').map(s => s.trim().toLowerCase()); + const enabledSearch = providersData.enabledSearch.split(',').map(s => s.trim().toLowerCase()); const enabledPlaylist = providersData.enabledPlaylist.split(',').map(s => s.trim().toLowerCase()); @@ -2115,17 +2125,20 @@ function renderMultiProviderSettings(data) { .catch(err => console.error("Error fetching provider status:", err)); } - const priorityList = document.getElementById("provider-priority-list"); - if (priorityList) { - priorityList.innerHTML = ""; - order.forEach(p => { + const renderListItems = (listId, orderArray) => { + const list = document.getElementById(listId); + if (!list) return; + list.innerHTML = ""; + orderArray.forEach(p => { const displayName = p === "spotify" ? "Spotify" : p === "applemusic" ? "Apple Music" : p === "deezer" ? "Deezer" : p === "qobuz" ? "Qobuz" : - p === "squidwtf" ? "SquidWTF (Tidal)" : p; + p === "squidwtf" ? "SquidWTF (Tidal)" : + p === "lyricsplus" ? "LyricsPlus" : + p === "lrclib" ? "LRCLib" : p; - priorityList.innerHTML += ` + list.innerHTML += `
  • ☰ @@ -2134,15 +2147,29 @@ function renderMultiProviderSettings(data) {
  • `; }); + }; - initDragAndDrop(); - } + renderListItems("provider-priority-list", order); + renderListItems("download-priority-list", downloadOrder); + renderListItems("playlist-priority-list", playlistOrder); + renderListItems("lyrics-priority-list", lyricsOrder); + + initDragAndDrop("provider-priority-list", updateProviderConfigInputs); + initDragAndDrop("download-priority-list", updateProviderConfigInputs); + initDragAndDrop("playlist-priority-list", updateProviderConfigInputs); + initDragAndDrop("lyrics-priority-list", updateProviderConfigInputs); } function updateProviderConfigInputs() { - const listItems = document.querySelectorAll("#provider-priority-list li"); - const order = Array.from(listItems).map(item => item.getAttribute("data-provider")); - setInputValue("config-multi-provider-metadata-order", order.join(",")); + const getListOrder = (listId) => { + const listItems = document.querySelectorAll(`#${listId} li`); + return Array.from(listItems).map(item => item.getAttribute("data-provider")); + }; + + setInputValue("config-multi-provider-metadata-order", getListOrder("provider-priority-list").join(",")); + setInputValue("config-multi-provider-download-order", getListOrder("download-priority-list").join(",")); + setInputValue("config-multi-provider-playlist-order", getListOrder("playlist-priority-list").join(",")); + setInputValue("config-multi-provider-lyrics-order", getListOrder("lyrics-priority-list").join(",")); const searchCbs = document.querySelectorAll(".provider-search-checkbox"); const enabledSearch = Array.from(searchCbs) @@ -2157,8 +2184,8 @@ function updateProviderConfigInputs() { setInputValue("config-multi-provider-enabled-playlist", enabledPlaylist.join(",")); } -function initDragAndDrop() { - const list = document.getElementById("provider-priority-list"); +function initDragAndDrop(listId, onUpdate) { + const list = document.getElementById(listId); if (!list) return; let dragEl = null; @@ -2176,7 +2203,7 @@ function initDragAndDrop() { if (dragEl) { dragEl.classList.remove("dragging"); dragEl = null; - updateProviderConfigInputs(); + onUpdate(); } });