216 lines
7.5 KiB
C#
216 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);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|