using System; using System.Threading; namespace LibationFileManager; public static class WindowsDirectory { const int FolderIconMaxAttempts = 5; public static void SetCoverAsFolderIcon(string? pictureId, string directory, CancellationToken cancellationToken) { //Currently only works for Windows and macOS if (!Configuration.Instance.UseCoverAsFolderIcon) return; if (string.IsNullOrEmpty(pictureId)) { Serilog.Log.Logger.Warning("No picture ID provided to set cover art as folder icon. {@DebugInfo}", new { directory }); return; } // Load JPEG bytes from Images cache (or download). Prefer bytes → ICO so we never depend on a // path that might not exist when Amazon omits Content-Length or another downloader left a stale cache entry. for (var attempt = 1; attempt <= FolderIconMaxAttempts; attempt++) { cancellationToken.ThrowIfCancellationRequested(); try { var jpegBytes = PictureStorage.GetPictureSynchronously(new(pictureId, PictureSize._300x300), cancellationToken); if (jpegBytes.Length == 0) { if (attempt < FolderIconMaxAttempts) { Serilog.Log.Logger.Debug("Folder icon: empty 300x300 image on attempt {Attempt}/{Max}; retrying after delay. {@DebugInfo}", attempt, FolderIconMaxAttempts, new { directory, pictureId }); DelayBetweenFolderIconRetries(cancellationToken, attempt); continue; } break; } InteropFactory.Create().SetFolderIcon(imageJpegBytes: jpegBytes, directory: directory); return; } catch (Exception ex) { if (attempt < FolderIconMaxAttempts) { Serilog.Log.Logger.Debug(ex, "Folder icon: attempt {Attempt}/{Max} failed; retrying after delay. {@DebugInfo}", attempt, FolderIconMaxAttempts, new { directory, pictureId }); DelayBetweenFolderIconRetries(cancellationToken, attempt); continue; } if (TrySetFolderIconUsingPictureSize(pictureId, directory, PictureSize.Native, cancellationToken)) { Serilog.Log.Logger.Information( "Set Explorer folder icon using full-size cover after 300x300 failed (decode, ICO conversion, or writing desktop.ini/Icon.ico). {@DebugInfo}", new { directory, pictureId }); return; } Serilog.Log.Logger.Error(ex, "Could not set Explorer folder icon after {MaxAttempts} attempts (decode, ICO conversion, or writing desktop.ini/Icon.ico failed). The audiobook download itself should still be fine; try liberating again, or check folder permissions if the library is on removable media. {@DebugInfo}", FolderIconMaxAttempts, new { directory, pictureId }); TryDeleteFolderIcon(directory); return; } } // 300x300 never returned usable bytes — Native is a separate CDN URL and is often already cached by DownloadCoverArt in the same session. if (TrySetFolderIconUsingPictureSize(pictureId, directory, PictureSize.Native, cancellationToken)) { Serilog.Log.Logger.Information( "Set Explorer folder icon using full-size cover after 300x300 was empty or missing. {@DebugInfo}", new { directory, pictureId }); return; } Serilog.Log.Logger.Warning( "Could not set Explorer folder icon: neither 300x300 nor full-size cover became available. The audiobook download itself is unaffected. Check your network to Amazon images, disk space under Libation's Images folder, or try liberating again. {@DebugInfo}", new { directory, pictureId }); TryDeleteFolderIcon(directory); } static bool TrySetFolderIconUsingPictureSize(string pictureId, string directory, PictureSize size, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); try { var jpegBytes = PictureStorage.GetPictureSynchronously(new(pictureId, size), cancellationToken); if (jpegBytes.Length == 0) return false; InteropFactory.Create().SetFolderIcon(imageJpegBytes: jpegBytes, directory: directory); return true; } catch (Exception ex) { Serilog.Log.Logger.Debug(ex, "Folder icon: could not set using {PictureSize}. {@DebugInfo}", size, new { directory, pictureId }); return false; } } static void DelayBetweenFolderIconRetries(CancellationToken cancellationToken, int attemptAfterFailure) { // 100, 200, 400, 800 ms; bounded backoff without Task.Delay allocation on hot path var ms = 100 * (1 << (attemptAfterFailure - 1)); if (ms > 0) cancellationToken.WaitHandle.WaitOne(ms); } static void TryDeleteFolderIcon(string directory) { try { InteropFactory.Create().DeleteFolderIcon(directory); } catch (Exception ex) { Serilog.Log.Logger.Error(ex, "Error rolling back folder icon files. {@DebugInfo}", new { directory }); } } }