Merge pull request #1746 from rmcrackan/rmcrackan/1744-images

#1744 - attempt to harded code for cover image/folder image
This commit is contained in:
rmcrackan
2026-04-17 16:11:21 -04:00
committed by GitHub
7 changed files with 89 additions and 21 deletions

View File

@@ -5,6 +5,7 @@ namespace LibationFileManager;
public interface IInteropFunctions
{
void SetFolderIcon(string image, string directory);
void SetFolderIcon(byte[] imageJpegBytes, string directory);
void DeleteFolderIcon(string directory);
Process? RunAsRoot(string exe, string args);
void InstallUpgrade(string upgradeBundle);

View File

@@ -10,6 +10,7 @@ public class NullInteropFunctions : IInteropFunctions
public NullInteropFunctions(params object[] values) { }
public void SetFolderIcon(string image, string directory) => throw new PlatformNotSupportedException();
public void SetFolderIcon(byte[] imageJpegBytes, string directory) => throw new PlatformNotSupportedException();
public void DeleteFolderIcon(string directory) => throw new PlatformNotSupportedException();
public bool CanUpgrade => throw new PlatformNotSupportedException();
public string ReleaseIdString => throw new PlatformNotSupportedException();

View File

@@ -88,25 +88,30 @@ public static class PictureStorage
{
lock (cacheLocker)
{
if (!cache.ContainsKey(def) || cache[def] is null)
var path = getPath(def);
// Disk is authoritative. Ignore in-memory cache when the file is missing so a later
// successful download (or a CDN that omits Content-Length) is not blocked by a stale placeholder.
if (File.Exists(path))
{
var path = getPath(def);
var bytes
= File.Exists(path)
? File.ReadAllBytes(path)
: downloadBytes(def, cancellationToken);
cache[def] = bytes;
cache[def] = File.ReadAllBytes(path);
return cache[def];
}
return cache[def];
if (cache.ContainsKey(def))
cache.Remove(def);
var bytes = downloadBytes(def, cancellationToken);
if (File.Exists(path))
cache[def] = bytes;
return bytes;
}
}
public static void SetDefaultImage(PictureSize pictureSize, byte[] bytes)
=> defaultImages[pictureSize] = bytes;
public static byte[] GetDefaultImage(PictureSize size)
=> defaultImages.ContainsKey(size)
? defaultImages[size]
: new byte[0];
=> defaultImages.TryGetValue(size, out byte[]? value) ? value : [];
static void BackgroundDownloader()
{
@@ -124,6 +129,9 @@ public static class PictureStorage
}
private static HttpClient imageDownloadClient { get; } = new HttpClient();
private const long MaxPictureDownloadBytes = 25 * 1024 * 1024;
private static byte[] downloadBytes(PictureDefinition def, CancellationToken cancellationToken = default)
{
if (def.PictureId is null)
@@ -136,14 +144,36 @@ public static class PictureStorage
using var requestMessage = new HttpRequestMessage(HttpMethod.Get, "ht" + $"tps://images-na.ssl-images-amazon.com/images/I/{def.PictureId}{sizeStr}.jpg");
using var response = imageDownloadClient.Send(requestMessage, HttpCompletionOption.ResponseHeadersRead, cancellationToken).EnsureSuccessStatusCode();
if (response.Content.Headers.ContentLength is not long size)
return GetDefaultImage(def.Size);
byte[] bytes;
if (response.Content.Headers.ContentLength is long knownSize && knownSize >= 0)
{
if (knownSize == 0 || knownSize > MaxPictureDownloadBytes)
return GetDefaultImage(def.Size);
var bytes = new byte[size];
using var respStream = response.Content.ReadAsStream(cancellationToken);
respStream.ReadExactly(bytes);
bytes = new byte[knownSize];
using (var respStream = response.Content.ReadAsStream(cancellationToken))
respStream.ReadExactly(bytes);
}
else
{
// Chunked responses often omit Content-Length; the previous implementation treated that as failure,
// left no file on disk, and callers that opened the expected path then broke (e.g. folder icons).
using var respStream = response.Content.ReadAsStream(cancellationToken);
using var ms = new MemoryStream();
var buffer = new byte[81920];
int read;
while ((read = respStream.Read(buffer, 0, buffer.Length)) > 0)
{
if (ms.Length + read > MaxPictureDownloadBytes)
return GetDefaultImage(def.Size);
ms.Write(buffer, 0, read);
}
bytes = ms.ToArray();
if (bytes.Length == 0)
return GetDefaultImage(def.Size);
}
// save image file. make sure to not save default image
var path = getPath(def);
File.WriteAllBytes(path, bytes);

View File

@@ -18,9 +18,16 @@ public static class WindowsDirectory
return;
}
// get path of cover art in Images dir. Download first if not exists
var coverArtPath = PictureStorage.GetPicturePathSynchronously(new(pictureId, PictureSize._300x300), cancellationToken);
InteropFactory.Create().SetFolderIcon(image: coverArtPath, directory: directory);
// 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.
var jpegBytes = PictureStorage.GetPictureSynchronously(new(pictureId, PictureSize._300x300), cancellationToken);
if (jpegBytes.Length == 0)
{
Serilog.Log.Logger.Warning("Cover art unavailable for folder icon (empty image). {@DebugInfo}", new { directory, pictureId });
return;
}
InteropFactory.Create().SetFolderIcon(imageJpegBytes: jpegBytes, directory: directory);
}
catch (Exception ex)
{

View File

@@ -22,6 +22,7 @@ internal class LinuxInterop : IInteropFunctions
public LinuxInterop(params object[] values) { }
public void SetFolderIcon(string image, string directory) => throw new PlatformNotSupportedException();
public void SetFolderIcon(byte[] imageJpegBytes, string directory) => throw new PlatformNotSupportedException();
public void DeleteFolderIcon(string directory) => throw new PlatformNotSupportedException();
public string ReleaseIdString => LibationScaffolding.ReleaseIdentifier.ToString() + (File.Exists("/bin/apt") ? "_DEB" : "_RPM");

View File

@@ -1,6 +1,7 @@
using Dinah.Core;
using LibationFileManager;
using System.Diagnostics;
using System.IO;
namespace MacOSConfigApp;
@@ -14,6 +15,25 @@ internal class MacOSInterop : IInteropFunctions
{
Process.Start("fileicon", $"set {directory.SurroundWithQuotes()} {image.SurroundWithQuotes()}").WaitForExit();
}
public void SetFolderIcon(byte[] imageJpegBytes, string directory)
{
var tempPath = Path.Combine(Path.GetTempPath(), $"LibationFolderIcon-{Guid.NewGuid():N}.jpg");
try
{
File.WriteAllBytes(tempPath, imageJpegBytes);
SetFolderIcon(tempPath, directory);
}
finally
{
try
{
if (File.Exists(tempPath))
File.Delete(tempPath);
}
catch { /* best effort */ }
}
}
public void DeleteFolderIcon(string directory)
{
Process.Start("fileicon", $"rm {directory.SurroundWithQuotes()}").WaitForExit();

View File

@@ -13,7 +13,15 @@ internal class WinInterop : IInteropFunctions
public WinInterop(params object[] values) { }
public void SetFolderIcon(string image, string directory)
{
var icon = Image.Load(image).ToIcon();
using var img = Image.Load(image);
var icon = img.ToIcon();
new DirectoryInfo(directory)?.SetIcon(icon, "Music");
}
public void SetFolderIcon(byte[] imageJpegBytes, string directory)
{
using var img = Image.Load(new MemoryStream(imageJpegBytes, writable: false));
var icon = img.ToIcon();
new DirectoryInfo(directory)?.SetIcon(icon, "Music");
}