Wp2ghost/GhostImageManager/Services/ImageAnalyzerService.cs
2026-09-24 19:23:54 +02:00

215 lines
7.5 KiB
C#

using System.Text.RegularExpressions;
using GhostImageManager.Models;
using HtmlAgilityPack;
namespace GhostImageManager.Services;
public class ImageAnalyzerService
{
private readonly GhostApiService _ghostApiService;
private readonly ILogger<ImageAnalyzerService> _logger;
public ImageAnalyzerService(GhostApiService ghostApiService, ILogger<ImageAnalyzerService> logger)
{
_ghostApiService = ghostApiService;
_logger = logger;
}
public async Task<List<GhostImage>> AnalyzeImagesAsync(GhostInstance instance, string searchTerm = "")
{
var images = new List<GhostImage>();
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<string, List<PostReference>>(StringComparer.OrdinalIgnoreCase);
foreach (var post in posts)
{
// Feature Image
if (!string.IsNullOrEmpty(post.FeatureImage))
{
if (!imageReferences.ContainsKey(post.FeatureImage))
imageReferences[post.FeatureImage] = new List<PostReference>();
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<PostReference>();
// 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<PostReference>(),
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<FileInfo> ScanLocalImageFiles(string folderPath)
{
var files = new List<FileInfo>();
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<string> ExtractImageUrls(string html)
{
var urls = new List<string>();
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);
}
}
}