mirror of
https://github.com/SoPat712/allstarr.git
synced 2026-02-09 23:55:10 -05:00
fix: resolve circular dependency and fix failing tests
This commit is contained in:
@@ -22,7 +22,7 @@ public abstract class BaseDownloadService : IDownloadService
|
||||
protected readonly IMusicMetadataService MetadataService;
|
||||
protected readonly SubsonicSettings SubsonicSettings;
|
||||
protected readonly ILogger Logger;
|
||||
protected readonly IServiceProvider ServiceProvider;
|
||||
private readonly IServiceProvider _serviceProvider;
|
||||
|
||||
protected readonly string DownloadPath;
|
||||
protected readonly string CachePath;
|
||||
@@ -30,6 +30,22 @@ public abstract class BaseDownloadService : IDownloadService
|
||||
protected readonly Dictionary<string, DownloadInfo> ActiveDownloads = new();
|
||||
protected readonly SemaphoreSlim DownloadLock = new(1, 1);
|
||||
|
||||
/// <summary>
|
||||
/// Lazy-loaded PlaylistSyncService to avoid circular dependency
|
||||
/// </summary>
|
||||
private PlaylistSyncService? _playlistSyncService;
|
||||
protected PlaylistSyncService? PlaylistSyncService
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_playlistSyncService == null)
|
||||
{
|
||||
_playlistSyncService = _serviceProvider.GetService<PlaylistSyncService>();
|
||||
}
|
||||
return _playlistSyncService;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Provider name (e.g., "deezer", "qobuz")
|
||||
/// </summary>
|
||||
@@ -47,7 +63,7 @@ public abstract class BaseDownloadService : IDownloadService
|
||||
LocalLibraryService = localLibraryService;
|
||||
MetadataService = metadataService;
|
||||
SubsonicSettings = subsonicSettings;
|
||||
ServiceProvider = serviceProvider;
|
||||
_serviceProvider = serviceProvider;
|
||||
Logger = logger;
|
||||
|
||||
DownloadPath = configuration["Library:DownloadPath"] ?? "./downloads";
|
||||
@@ -269,22 +285,21 @@ public abstract class BaseDownloadService : IDownloadService
|
||||
song.LocalPath = localPath;
|
||||
|
||||
// Check if this track belongs to a playlist and update M3U
|
||||
try
|
||||
if (PlaylistSyncService != null)
|
||||
{
|
||||
var playlistSyncService = ServiceProvider.GetService(typeof(PlaylistSyncService)) as PlaylistSyncService;
|
||||
if (playlistSyncService != null)
|
||||
try
|
||||
{
|
||||
var playlistId = playlistSyncService.GetPlaylistIdForTrack(songId);
|
||||
var playlistId = PlaylistSyncService.GetPlaylistIdForTrack(songId);
|
||||
if (playlistId != null)
|
||||
{
|
||||
Logger.LogInformation("Track {SongId} belongs to playlist {PlaylistId}, adding to M3U", songId, playlistId);
|
||||
await playlistSyncService.AddTrackToM3UAsync(playlistId, song, localPath);
|
||||
await PlaylistSyncService.AddTrackToM3UAsync(playlistId, song, localPath, isFullPlaylistDownload: false);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogWarning(ex, "Failed to update playlist M3U for track {SongId}", songId);
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogWarning(ex, "Failed to update playlist M3U for track {SongId}", songId);
|
||||
}
|
||||
}
|
||||
|
||||
// Only register and scan if NOT in cache mode
|
||||
|
||||
@@ -11,6 +11,7 @@ using octo_fiesta.Models.Search;
|
||||
using octo_fiesta.Models.Subsonic;
|
||||
using octo_fiesta.Services.Local;
|
||||
using octo_fiesta.Services.Common;
|
||||
using octo_fiesta.Services.Subsonic;
|
||||
using Microsoft.Extensions.Options;
|
||||
using IOFile = System.IO.File;
|
||||
|
||||
|
||||
@@ -40,7 +40,7 @@ public class QobuzBundleService
|
||||
/// <summary>
|
||||
/// Gets the Qobuz App ID, extracting it from the bundle if not cached
|
||||
/// </summary>
|
||||
public async Task<string> GetAppIdAsync()
|
||||
public virtual async Task<string> GetAppIdAsync()
|
||||
{
|
||||
await EnsureInitializedAsync();
|
||||
return _cachedAppId!;
|
||||
@@ -49,7 +49,7 @@ public class QobuzBundleService
|
||||
/// <summary>
|
||||
/// Gets the Qobuz secrets list, extracting them from the bundle if not cached
|
||||
/// </summary>
|
||||
public async Task<List<string>> GetSecretsAsync()
|
||||
public virtual async Task<List<string>> GetSecretsAsync()
|
||||
{
|
||||
await EnsureInitializedAsync();
|
||||
return _cachedSecrets!;
|
||||
@@ -58,7 +58,7 @@ public class QobuzBundleService
|
||||
/// <summary>
|
||||
/// Gets a specific secret by index (used for signing requests)
|
||||
/// </summary>
|
||||
public async Task<string> GetSecretAsync(int index = 0)
|
||||
public virtual async Task<string> GetSecretAsync(int index = 0)
|
||||
{
|
||||
var secrets = await GetSecretsAsync();
|
||||
if (index < 0 || index >= secrets.Count)
|
||||
|
||||
@@ -8,6 +8,7 @@ using octo_fiesta.Models.Search;
|
||||
using octo_fiesta.Models.Subsonic;
|
||||
using octo_fiesta.Services.Local;
|
||||
using octo_fiesta.Services.Common;
|
||||
using octo_fiesta.Services.Subsonic;
|
||||
using Microsoft.Extensions.Options;
|
||||
using IOFile = System.IO.File;
|
||||
|
||||
|
||||
@@ -30,6 +30,10 @@ public class PlaylistSyncService
|
||||
private readonly string _musicDirectory;
|
||||
private readonly string _playlistDirectory;
|
||||
|
||||
// Cancellation token for background cleanup task
|
||||
private readonly CancellationTokenSource _cleanupCancellationTokenSource = new();
|
||||
private readonly Task _cleanupTask;
|
||||
|
||||
public PlaylistSyncService(
|
||||
IEnumerable<IMusicMetadataService> metadataServices,
|
||||
IEnumerable<IDownloadService> downloadServices,
|
||||
@@ -58,7 +62,20 @@ public class PlaylistSyncService
|
||||
}
|
||||
|
||||
// Start background cleanup task for expired cache entries
|
||||
_ = Task.Run(CleanupExpiredCacheEntriesAsync);
|
||||
_cleanupTask = Task.Run(() => CleanupExpiredCacheEntriesAsync(_cleanupCancellationTokenSource.Token));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the metadata service for the specified provider
|
||||
/// </summary>
|
||||
private IMusicMetadataService? GetMetadataServiceForProvider(string provider)
|
||||
{
|
||||
return provider.ToLower() switch
|
||||
{
|
||||
"deezer" => _deezerMetadataService,
|
||||
"qobuz" => _qobuzMetadataService,
|
||||
_ => null
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -69,7 +86,7 @@ public class PlaylistSyncService
|
||||
{
|
||||
var expiresAt = DateTime.UtcNow.Add(CacheTTL);
|
||||
_trackPlaylistCache[trackId] = (playlistId, expiresAt);
|
||||
_logger.LogDebug("Added track {TrackId} to playlist cache with playlistId {PlaylistId}", trackId, playlistId);
|
||||
_logger.LogInformation("Added track {TrackId} to playlist cache with playlistId {PlaylistId}", trackId, playlistId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -112,12 +129,11 @@ public class PlaylistSyncService
|
||||
var (provider, externalId) = PlaylistIdHelper.ParsePlaylistId(playlistId);
|
||||
|
||||
// Get playlist metadata
|
||||
var metadataService = provider.ToLower() switch
|
||||
var metadataService = GetMetadataServiceForProvider(provider);
|
||||
if (metadataService == null)
|
||||
{
|
||||
"deezer" => _deezerMetadataService,
|
||||
"qobuz" => _qobuzMetadataService,
|
||||
_ => throw new NotSupportedException($"Provider '{provider}' not supported for playlists")
|
||||
};
|
||||
throw new NotSupportedException($"Provider '{provider}' not supported for playlists");
|
||||
}
|
||||
|
||||
var playlist = await metadataService.GetPlaylistAsync(provider, externalId);
|
||||
if (playlist == null)
|
||||
@@ -145,7 +161,7 @@ public class PlaylistSyncService
|
||||
return;
|
||||
}
|
||||
|
||||
// Download all tracks
|
||||
// Download all tracks (M3U will be created once at the end)
|
||||
var downloadedTracks = new List<(Song Song, string LocalPath)>();
|
||||
|
||||
foreach (var track in tracks)
|
||||
@@ -159,6 +175,7 @@ public class PlaylistSyncService
|
||||
}
|
||||
|
||||
// Add track to playlist cache BEFORE downloading
|
||||
// This marks it as part of a full playlist download, so AddTrackToM3UAsync will skip real-time updates
|
||||
var trackId = $"ext-{provider}-{track.ExternalId}";
|
||||
AddTrackToPlaylistCache(trackId, playlistId);
|
||||
|
||||
@@ -180,7 +197,7 @@ public class PlaylistSyncService
|
||||
return;
|
||||
}
|
||||
|
||||
// Create M3U file
|
||||
// Create M3U file ONCE at the end with all downloaded tracks
|
||||
await CreateM3UPlaylistAsync(playlist.Name, downloadedTracks);
|
||||
|
||||
_logger.LogInformation("Playlist download completed: {DownloadedCount}/{TotalCount} tracks for '{PlaylistName}'",
|
||||
@@ -233,11 +250,19 @@ public class PlaylistSyncService
|
||||
|
||||
/// <summary>
|
||||
/// Adds a track to an existing M3U playlist or creates it if it doesn't exist.
|
||||
/// This is called progressively as tracks are downloaded.
|
||||
/// Called when individual tracks are played/downloaded (NOT during full playlist download).
|
||||
/// The M3U is rebuilt in the correct playlist order each time.
|
||||
/// </summary>
|
||||
public async Task AddTrackToM3UAsync(string playlistId, Song track, string localPath)
|
||||
/// <param name="isFullPlaylistDownload">If true, skips M3U update (will be done at the end by DownloadFullPlaylistAsync)</param>
|
||||
public async Task AddTrackToM3UAsync(string playlistId, Song track, string localPath, bool isFullPlaylistDownload = false)
|
||||
{
|
||||
// Skip real-time updates during full playlist download (M3U will be created once at the end)
|
||||
if (isFullPlaylistDownload)
|
||||
{
|
||||
_logger.LogDebug("Skipping M3U update for track {TrackId} (full playlist download in progress)", track.Id);
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// Get playlist metadata to get the name and track order
|
||||
@@ -249,13 +274,7 @@ public class PlaylistSyncService
|
||||
|
||||
var (provider, externalId) = PlaylistIdHelper.ParsePlaylistId(playlistId);
|
||||
|
||||
var metadataService = provider.ToLower() switch
|
||||
{
|
||||
"deezer" => _deezerMetadataService,
|
||||
"qobuz" => _qobuzMetadataService,
|
||||
_ => null
|
||||
};
|
||||
|
||||
var metadataService = GetMetadataServiceForProvider(provider);
|
||||
if (metadataService == null)
|
||||
{
|
||||
_logger.LogWarning("No metadata service found for provider '{Provider}'", provider);
|
||||
@@ -342,13 +361,13 @@ public class PlaylistSyncService
|
||||
/// <summary>
|
||||
/// Background task to clean up expired cache entries every minute
|
||||
/// </summary>
|
||||
private async Task CleanupExpiredCacheEntriesAsync()
|
||||
private async Task CleanupExpiredCacheEntriesAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
while (true)
|
||||
while (!cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
try
|
||||
{
|
||||
await Task.Delay(TimeSpan.FromMinutes(1));
|
||||
await Task.Delay(TimeSpan.FromMinutes(1), cancellationToken);
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
var expiredKeys = _trackPlaylistCache
|
||||
@@ -366,10 +385,27 @@ public class PlaylistSyncService
|
||||
_logger.LogDebug("Cleaned up {Count} expired playlist cache entries", expiredKeys.Count);
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// Expected when cancellation is requested
|
||||
break;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Error during playlist cache cleanup");
|
||||
}
|
||||
}
|
||||
|
||||
_logger.LogInformation("Playlist cache cleanup task stopped");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stops the background cleanup task
|
||||
/// </summary>
|
||||
public async Task StopCleanupAsync()
|
||||
{
|
||||
_cleanupCancellationTokenSource.Cancel();
|
||||
await _cleanupTask;
|
||||
_cleanupCancellationTokenSource.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user