using SixLabors.ImageSharp; using SixLabors.ImageSharp.Processing; using System.Text.RegularExpressions; namespace wp2ghost { // ============================================================================= // ImageManager — Bilder herunterladen, cachen, lokal verwalten // ============================================================================= static class ImageManager { private static readonly HttpClient httpClient = new(); // Schlüssel: Bild-Schlüssel (siehe GetImageKey) — alle WordPress-Varianten eines Bildes teilen sich einen Eintrag private static Dictionary Cache = new(); private static List<(string RelativePath, string LocalPath)> FilesToZip = new(); private static string LocalImageDir = ""; private static string CachePath = ""; private static readonly Regex SizeSuffixRegex = new(@"-\d+x\d+(?=\.\w+$)", RegexOptions.Compiled); private static readonly Regex ScaledSuffixRegex = new(@"-scaled(?=\.\w+$)", RegexOptions.Compiled); public static IReadOnlyList<(string RelativePath, string LocalPath)> CachedFiles => FilesToZip; /// /// Schlüssel, unter dem alle Varianten desselben WordPress-Bildes zusammenfallen: /// Pfad ohne Host, Query (?w=1024), Größen-Suffix (-1024x683) und -scaled. /// public static string GetImageKey(string url) { string path; try { path = new Uri(url).AbsolutePath; } catch { path = url.Split('?', '#')[0]; } path = SizeSuffixRegex.Replace(path, ""); path = ScaledSuffixRegex.Replace(path, ""); return path.ToLowerInvariant(); } public static bool TryGetRelativePath(string url, out string relativePath) { if (TryGetCached(GetImageKey(url), out var cached)) { relativePath = cached.RelativePath; return true; } relativePath = ""; return false; } private static bool TryGetCached(string key, out (string RelativePath, string LocalPath) file) { if (Cache.TryGetValue(key, out var cached)) { if (File.Exists(cached.LocalPath)) { file = (cached.RelativePath, cached.LocalPath); RegisterForZip(file); return true; } Cache.Remove(key); } file = default; return false; } private static void RegisterForZip((string RelativePath, string LocalPath) file) { if (!FilesToZip.Any(f => f.RelativePath == file.RelativePath)) FilesToZip.Add(file); } public static void Init(string baseDir) { LocalImageDir = Path.Combine(baseDir, "images"); CachePath = Path.Combine(baseDir, "imagecache.txt"); if (!Directory.Exists(LocalImageDir)) Directory.CreateDirectory(LocalImageDir); LoadCache(); } public static string ImageContainerPath => LocalImageDir; public static void SaveCache() { var lines = new List(); foreach (var kvp in Cache) lines.Add($"{kvp.Value.SourceUrl}|{kvp.Value.RelativePath}|{kvp.Value.LocalPath}|{kvp.Key}"); File.WriteAllLines(CachePath, lines); } public static void ClearAll() { if (Directory.Exists(LocalImageDir)) { Directory.Delete(LocalImageDir, true); Directory.CreateDirectory(LocalImageDir); } Cache.Clear(); FilesToZip.Clear(); SaveCache(); Console.WriteLine("Bilder wurden lokal gelöscht."); } public static void ShowInfo() { if (Cache.Count > 0) { Console.WriteLine("\nLokal gespeicherte Bilder:"); foreach (var kvp in Cache.Take(20)) Console.WriteLine($" {kvp.Value.SourceUrl} -> images/{kvp.Value.RelativePath}"); if (Cache.Count > 20) Console.WriteLine($" ... und {Cache.Count - 20} weitere"); } } public static bool IsMainImage(string url) { if (string.IsNullOrEmpty(url)) return false; try { var uri = new Uri(url); string fileName = Path.GetFileName(uri.LocalPath); // WordPress-Sizes: -300x200, -scaled, -1024x768 etc. if (SizeSuffixRegex.IsMatch(fileName)) return false; if (fileName.Contains("-scaled")) return false; return true; } catch { return false; } } /// /// Lädt EIN Bild für eine Gruppe von URL-Varianten desselben WordPress-Bildes /// (Original, -scaled, -1024x683, ?w=...). Es wird die beste verfügbare Variante geladen. /// public static async Task<(string RelativePath, string LocalPath)?> DownloadBestAsync(IEnumerable variants, DateTime? postDate) { // Query/Fragment entfernen: ?w=1024 & Co. liefern höchstens eine verkleinerte Fassung var urls = variants .Where(u => !string.IsNullOrEmpty(u)) .Select(u => u.Split('?', '#')[0]) .Where(u => Uri.IsWellFormedUriString(u, UriKind.Absolute)) .Distinct() .ToList(); if (urls.Count == 0) return null; string key = GetImageKey(urls[0]); if (TryGetCached(key, out var cachedFile)) return cachedFile; // Reihenfolge: Original -> -scaled -> erratenes Original -> Größenvarianten (größte zuerst) var candidates = new List(); candidates.AddRange(urls.Where(IsMainImage)); candidates.AddRange(urls.Where(u => ScaledSuffixRegex.IsMatch(u) && !SizeSuffixRegex.IsMatch(u))); candidates.Add(ScaledSuffixRegex.Replace(SizeSuffixRegex.Replace(urls[0], ""), "")); candidates.AddRange(urls.Where(u => SizeSuffixRegex.IsMatch(u)).OrderByDescending(GetVariantArea)); string lastError = ""; foreach (string candidate in candidates.Distinct()) { try { byte[] imageBytes = await httpClient.GetByteArrayAsync(candidate); var now = DateTime.Now; string datePath = $"{now:yyyy}/{now:MM}"; string localDir = Path.Combine(LocalImageDir, datePath); if (!Directory.Exists(localDir)) Directory.CreateDirectory(localDir); // Eindeutigen Dateinamen bestimmen (nie eine vorhandene Datei wiederverwenden) var pDate = postDate ?? now; string ext = Path.GetExtension(new Uri(candidate).AbsolutePath); int counter = 1 + new DirectoryInfo(localDir).GetFiles().Length; string relativePath, localPath; do { relativePath = $"{datePath}/{pDate:yyyyMMdd}_{counter++}{ext}"; localPath = Path.Combine(LocalImageDir, relativePath); } while (File.Exists(localPath)); await File.WriteAllBytesAsync(localPath, imageBytes); Console.WriteLine($" [Bild] Heruntergeladen: {Path.GetFileName(new Uri(candidate).AbsolutePath)} → {relativePath}"); ResizeImage(localPath); var fileResult = (relativePath, localPath); Cache[key] = (candidate, relativePath, localPath); RegisterForZip(fileResult); return fileResult; } catch (Exception ex) { lastError = ex.Message; } } Console.WriteLine($" [Bild-Fehler] Konnte {urls[0]} nicht laden: {lastError}"); return null; } private static long GetVariantArea(string url) { var m = Regex.Match(url, @"-(\d+)x(\d+)(?=\.\w+$)"); return m.Success ? long.Parse(m.Groups[1].Value) * long.Parse(m.Groups[2].Value) : 0; } private static void ResizeImage(string localPath) { const int maxSize = 2048; try { using var image = Image.Load(localPath); int width = image.Width; int height = image.Height; int longEdge = Math.Max(width, height); if (longEdge <= maxSize) return; int newWidth, newHeight; if (width >= height) { newWidth = maxSize; newHeight = (int)Math.Round(height * ((double)maxSize / width)); } else { newHeight = maxSize; newWidth = (int)Math.Round(width * ((double)maxSize / height)); } image.Mutate(x => x.Resize(newWidth, newHeight)); image.Save(localPath); Console.WriteLine($" [Bild] Angepasst: {width}x{height} → {newWidth}x{newHeight}"); } catch (Exception ex) { Console.WriteLine($" [Bild-Warnung] Resize fehlgeschlagen für {Path.GetFileName(localPath)}: {ex.Message}"); } } private static void LoadCache() { if (!File.Exists(CachePath)) return; try { var lines = File.ReadAllLines(CachePath); foreach (var line in lines) { var parts = line.Split('|'); if (parts.Length >= 3) { // Schlüssel immer aus der URL ableiten (ältere Cache-Dateien enthalten nur den Dateinamen) Cache[GetImageKey(parts[0])] = (parts[0], parts[1], parts[2]); } } } catch (Exception ex) { Console.WriteLine($"Fehler beim Laden des Bild-Caches: {ex.Message}"); } } } }