diff --git a/allstarr/Services/Common/BaseDownloadService.cs b/allstarr/Services/Common/BaseDownloadService.cs index 9e971a7..9ccfad8 100644 --- a/allstarr/Services/Common/BaseDownloadService.cs +++ b/allstarr/Services/Common/BaseDownloadService.cs @@ -110,6 +110,9 @@ public abstract class BaseDownloadService : IDownloadService IOFile.SetLastAccessTime(localPath, DateTime.UtcNow); } + // Start background Odesli conversion for lyrics (if not already cached) + StartBackgroundOdesliConversion(externalProvider, externalId); + return IOFile.OpenRead(localPath); } @@ -125,6 +128,10 @@ public abstract class BaseDownloadService : IDownloadService localPath = await DownloadSongInternalAsync(externalProvider, externalId, triggerAlbumDownload: true, cancellationToken); var elapsed = (DateTime.UtcNow - startTime).TotalMilliseconds; Logger.LogInformation("Download completed, starting stream ({ElapsedMs}ms total): {Path}", elapsed, localPath); + + // Start background Odesli conversion for lyrics (after stream starts) + StartBackgroundOdesliConversion(externalProvider, externalId); + return IOFile.OpenRead(localPath); } catch (OperationCanceledException) @@ -141,6 +148,37 @@ public abstract class BaseDownloadService : IDownloadService } } + /// + /// Starts background Odesli conversion for lyrics support. + /// This is called AFTER streaming starts so it doesn't block the client. + /// + private void StartBackgroundOdesliConversion(string externalProvider, string externalId) + { + _ = Task.Run(async () => + { + try + { + // Provider-specific conversion (override in subclasses if needed) + await ConvertToSpotifyIdAsync(externalProvider, externalId); + } + catch (Exception ex) + { + Logger.LogDebug(ex, "Background Spotify ID conversion failed for {Provider}:{ExternalId}", externalProvider, externalId); + } + }); + } + + /// + /// Converts external track ID to Spotify ID for lyrics support. + /// Override in provider-specific services if needed. + /// + protected virtual Task ConvertToSpotifyIdAsync(string externalProvider, string externalId) + { + // Default implementation does nothing + // Provider-specific services can override this + return Task.CompletedTask; + } + public DownloadInfo? GetDownloadStatus(string songId) { ActiveDownloads.TryGetValue(songId, out var info); diff --git a/allstarr/Services/SquidWTF/SquidWTFDownloadService.cs b/allstarr/Services/SquidWTF/SquidWTFDownloadService.cs index 53b0494..6468a33 100644 --- a/allstarr/Services/SquidWTF/SquidWTFDownloadService.cs +++ b/allstarr/Services/SquidWTF/SquidWTFDownloadService.cs @@ -108,9 +108,6 @@ public class SquidWTFDownloadService : BaseDownloadService Logger.LogInformation("Track download URL obtained from hifi-api: {Url}", downloadInfo.DownloadUrl); Logger.LogInformation("Using format: {Format} (Quality: {Quality})", downloadInfo.MimeType, downloadInfo.AudioQuality); - // Start Spotify ID conversion in parallel with download (don't await yet) - var spotifyIdTask = _odesliService.ConvertTidalToSpotifyIdAsync(trackId, cancellationToken); - // Determine extension from MIME type var extension = downloadInfo.MimeType?.ToLower() switch { @@ -199,42 +196,26 @@ public class SquidWTFDownloadService : BaseDownloadService // Close file before writing metadata await outputFile.DisposeAsync(); - // Wait for Spotify ID conversion to complete (with 2 second timeout) - // If Odesli is slow, we'll skip it and add the Spotify ID later in background - var spotifyId = await Task.WhenAny( - spotifyIdTask, - Task.Delay(2000, cancellationToken) - ) == spotifyIdTask ? await spotifyIdTask : null; - - if (!string.IsNullOrEmpty(spotifyId)) + // Start Spotify ID conversion in background (for lyrics support) + // This doesn't block streaming - lyrics endpoint will fetch it on-demand if needed + _ = Task.Run(async () => { - song.SpotifyId = spotifyId; - Logger.LogDebug("Spotify ID obtained: {SpotifyId}", spotifyId); - } - else - { - Logger.LogDebug("Spotify ID not available yet (Odesli timeout), will update in background"); - - // Continue Odesli conversion in background (for future lyrics requests) - _ = Task.Run(async () => + try { - try + var spotifyId = await _odesliService.ConvertTidalToSpotifyIdAsync(trackId, CancellationToken.None); + if (!string.IsNullOrEmpty(spotifyId)) { - var bgSpotifyId = await spotifyIdTask; - if (!string.IsNullOrEmpty(bgSpotifyId)) - { - Logger.LogDebug("Background Spotify ID obtained: {SpotifyId}", bgSpotifyId); - // Note: We don't re-write metadata here, just cache the ID for lyrics - } + Logger.LogDebug("Background Spotify ID obtained for Tidal/{TrackId}: {SpotifyId}", trackId, spotifyId); + // Spotify ID is cached by Odesli service for future lyrics requests } - catch (Exception ex) - { - Logger.LogDebug(ex, "Background Spotify ID conversion failed"); - } - }); - } + } + catch (Exception ex) + { + Logger.LogDebug(ex, "Background Spotify ID conversion failed for Tidal/{TrackId}", trackId); + } + }); - // Write metadata and cover art + // Write metadata and cover art (without Spotify ID - it's only needed for lyrics) await WriteMetadataAsync(outputPath, song, cancellationToken); return outputPath; @@ -327,6 +308,24 @@ public class SquidWTFDownloadService : BaseDownloadService #region Utility Methods + /// + /// Converts Tidal track ID to Spotify ID for lyrics support. + /// Called in background after streaming starts. + /// + protected override async Task ConvertToSpotifyIdAsync(string externalProvider, string externalId) + { + if (externalProvider != "squidwtf") + { + return; + } + + var spotifyId = await _odesliService.ConvertTidalToSpotifyIdAsync(externalId, CancellationToken.None); + if (!string.IsNullOrEmpty(spotifyId)) + { + Logger.LogDebug("Background Spotify ID obtained for Tidal/{TrackId}: {SpotifyId}", externalId, spotifyId); + // Spotify ID is cached by Odesli service for future lyrics requests + } + } #endregion