Files
Libation/Source/LibationFileManager/WindowsDirectory.cs

124 lines
4.6 KiB
C#

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 });
}
}
}