using System.Text.RegularExpressions; using GhostImageManager.Models; using HtmlAgilityPack; namespace GhostImageManager.Services; public class ImageAnalyzerService { private readonly GhostApiService _ghostApiService; private readonly ILogger _logger; public ImageAnalyzerService(GhostApiService ghostApiService, ILogger logger) { _ghostApiService = ghostApiService; _logger = logger; } public async Task> AnalyzeImagesAsync(GhostInstance instance, string searchTerm = "") { var images = new List(); if (!Directory.Exists(instance.ImageFolderPath)) { _logger.LogWarning("Image folder not found: {Path}", instance.ImageFolderPath); return images; } // 1. Alle Posts abrufen var posts = await _ghostApiService.GetPostsAsync(instance, searchTerm); // 2. Bild-Referenzen aus Posts sammeln var imageReferences = new Dictionary>(StringComparer.OrdinalIgnoreCase); foreach (var post in posts) { // Feature Image if (!string.IsNullOrEmpty(post.FeatureImage)) { if (!imageReferences.ContainsKey(post.FeatureImage)) imageReferences[post.FeatureImage] = new List(); imageReferences[post.FeatureImage].Add(new PostReference { PostId = post.Id, Title = post.Title, Slug = post.Slug, IsFeatureImage = true, IsInContent = false }); } // Bilder im HTML if (!string.IsNullOrEmpty(post.Html)) { var htmlImages = ExtractImageUrls(post.Html); foreach (var imgUrl in htmlImages) { if (!imageReferences.ContainsKey(imgUrl)) imageReferences[imgUrl] = new List(); // Prüfen ob bereits als Feature Image vorhanden var existing = imageReferences[imgUrl].FirstOrDefault(r => r.PostId == post.Id); if (existing != null) { existing.IsInContent = true; } else { imageReferences[imgUrl].Add(new PostReference { PostId = post.Id, Title = post.Title, Slug = post.Slug, IsFeatureImage = false, IsInContent = true }); } } } } // 3. Lokale Dateien scannen var localFiles = ScanLocalImageFiles(instance.ImageFolderPath); // 4. GhostImage-Objekte erstellen foreach (var file in localFiles) { var absoluteUrl = ConvertLocalPathToUrl(instance, file.FullName); var relativePath = GetRelativePath(instance.ImageFolderPath, file.FullName).Replace("\\", "/"); var image = new GhostImage { FilePath = file.FullName, FileName = file.Name, RelativePath = relativePath, Url = absoluteUrl, FileSize = file.Length, LastModified = file.LastWriteTime, UsedInPosts = imageReferences.TryGetValue(absoluteUrl, out var refs) ? refs : new List(), IsOrphaned = !imageReferences.ContainsKey(absoluteUrl) }; // Dimensionen ermitteln GetImageDimensions(image); images.Add(image); } // Auch URLs aus Posts, die lokal nicht existieren, hinzufügen (z.B. externe Bilder) foreach (var url in imageReferences.Keys) { var localPath = ConvertUrlToLocalPath(instance, url); if (!File.Exists(localPath) && !images.Any(i => i.Url.Equals(url, StringComparison.OrdinalIgnoreCase))) { images.Add(new GhostImage { Url = url, FileName = Path.GetFileName(url), RelativePath = GetRelativePath(instance.ImageFolderPath, localPath).Replace("\\", "/"), FilePath = localPath, UsedInPosts = imageReferences[url], IsOrphaned = false }); } } return images; } private List ScanLocalImageFiles(string folderPath) { var files = new List(); var extensions = new[] { ".jpg", ".jpeg", ".png", ".gif", ".webp", ".svg" }; if (!Directory.Exists(folderPath)) return files; foreach (var ext in extensions) { files.AddRange(new DirectoryInfo(folderPath) .EnumerateFiles($"*{ext}", SearchOption.AllDirectories)); } return files.OrderBy(f => f.FullName).ToList(); } private List ExtractImageUrls(string html) { var urls = new List(); try { var doc = new HtmlDocument(); doc.LoadHtml(html); var imgTags = doc.DocumentNode.SelectNodes("//img[@src]"); if (imgTags != null) { foreach (var img in imgTags) { var src = img.GetAttributeValue("src", "").Trim(); if (!string.IsNullOrEmpty(src)) urls.Add(src); } } } catch (Exception ex) { _logger.LogError(ex, "Fehler beim Parsen von HTML"); } return urls.Distinct(StringComparer.OrdinalIgnoreCase).ToList(); } private string ConvertLocalPathToUrl(GhostInstance instance, string localPath) { var relativePath = GetRelativePath(instance.ImageFolderPath, localPath).Replace("\\", "/"); return $"{instance.Url.TrimEnd('/')}/content/images/{relativePath.TrimStart('/')}"; } private string ConvertUrlToLocalPath(GhostInstance instance, string url) { var contentImagesUrl = $"{instance.Url.TrimEnd('/')}/content/images/"; if (url.StartsWith(contentImagesUrl, StringComparison.OrdinalIgnoreCase)) { var relativePath = url[contentImagesUrl.Length..]; return Path.Combine(instance.ImageFolderPath, relativePath.Replace("/", "\\")); } return Path.Combine(instance.ImageFolderPath, Path.GetFileName(url)); } private string GetRelativePath(string basePath, string targetPath) { var baseUri = new Uri(basePath.EndsWith("\\") || basePath.EndsWith("/") ? basePath : basePath + "\\"); var targetUri = new Uri(targetPath); var relativeUri = baseUri.MakeRelativeUri(targetUri); return Uri.UnescapeDataString(relativeUri.ToString()); } private void GetImageDimensions(GhostImage image) { if (image.FilePath.EndsWith(".svg", StringComparison.OrdinalIgnoreCase)) return; try { using var fs = File.OpenRead(image.FilePath); var (width, height) = ImageDimensionReader.Read(fs); image.Width = width; image.Height = height; } catch (Exception ex) { _logger.LogDebug(ex, "Konnte Bilddimensionen nicht ermitteln für {File}", image.FilePath); } } }