using System.Text; using System.Text.Json; using GhostImageManager.Models; namespace GhostImageManager.Services; public class GhostApiService { private readonly HttpClient _httpClient; private readonly ILogger _logger; public GhostApiService(HttpClient httpClient, ILogger logger) { _httpClient = httpClient; _logger = logger; } /// /// Lädt alle Einträge einer Ressource ("posts" oder "pages") – seitenweise, bis nichts mehr kommt. /// public async Task> GetPostsAsync(GhostInstance instance, string filter = "", int limit = 50, string resource = "posts") { _logger.LogInformation("Hole {Resource} von {Url} (Filter: {Filter}, Limit: {Limit})", resource, instance.Url, filter, limit); var posts = new List(); for (var page = 1; ; page++) { var endpoint = $"{resource}/?limit={limit}&page={page}&filter={Uri.EscapeDataString(filter)}&formats=html,lexical" + "&fields=id,title,slug,feature_image,og_image,twitter_image,codeinjection_head,codeinjection_foot,html,lexical,created_at,updated_at,status"; var request = CreateRequest(HttpMethod.Get, instance, endpoint); var response = await _httpClient.SendAsync(request); if (!response.IsSuccessStatusCode) { var errorContent = await response.Content.ReadAsStringAsync(); _logger.LogError("Ghost API Fehler beim Laden von {Resource}: {StatusCode} - {Error}", resource, response.StatusCode, errorContent); response.EnsureSuccessStatusCode(); } var json = await response.Content.ReadAsStringAsync(); var doc = JsonDocument.Parse(json); if (!doc.RootElement.TryGetProperty(resource, out var postsElement) || postsElement.GetArrayLength() == 0) break; foreach (var post in postsElement.EnumerateArray()) { posts.Add(new GhostPost { Id = GetString(post, "id"), Title = GetString(post, "title"), Slug = GetString(post, "slug"), Html = GetString(post, "html"), Lexical = GetString(post, "lexical"), OtherImageFields = string.Join("\n", GetString(post, "og_image"), GetString(post, "twitter_image"), GetString(post, "codeinjection_head"), GetString(post, "codeinjection_foot")), FeatureImage = GetString(post, "feature_image"), CreatedAt = GetDateTime(post, "created_at"), UpdatedAt = GetDateTime(post, "updated_at"), Status = GetString(post, "status") }); } var totalPages = 1; if (doc.RootElement.TryGetProperty("meta", out var meta) && meta.TryGetProperty("pagination", out var pagination) && pagination.TryGetProperty("pages", out var pagesElement)) totalPages = pagesElement.GetInt32(); if (page >= totalPages) break; } _logger.LogInformation("{Count} {Resource} von Ghost geladen", posts.Count, resource); return posts; } /// /// Bildquellen außerhalb von Posts/Seiten: Tags, Benutzer und Website-Einstellungen (Logo, Icon, Cover ...). /// Liefert je Quelle eine Bezeichnung und das rohe JSON, in dem nach Bild-URLs gesucht wird. /// public async Task> GetOtherImageSourcesAsync(GhostInstance instance) { var sources = new List<(string Label, string Json)>(); foreach (var (resource, prefix) in new[] { ("tags", "Tag"), ("users", "Benutzer") }) { var doc = await GetJsonAsync(instance, $"{resource}/?limit=all"); if (doc != null && doc.RootElement.TryGetProperty(resource, out var items)) { foreach (var item in items.EnumerateArray()) sources.Add(($"{prefix}: {GetString(item, "name")}", item.GetRawText())); } } var settings = await GetJsonAsync(instance, "settings/"); if (settings != null) sources.Add(("Website-Einstellungen", settings.RootElement.GetRawText())); return sources; } private async Task GetJsonAsync(GhostInstance instance, string endpoint) { var request = CreateRequest(HttpMethod.Get, instance, endpoint); var response = await _httpClient.SendAsync(request); if (!response.IsSuccessStatusCode) { _logger.LogWarning("Ghost API: {Endpoint} nicht lesbar ({StatusCode}) – Bilder daraus werden nicht als verwendet erkannt", endpoint, response.StatusCode); return null; } return JsonDocument.Parse(await response.Content.ReadAsStringAsync()); } public async Task GetPostAsync(GhostInstance instance, string postId) { _logger.LogInformation("Lade Post {PostId} von {Url}", postId, instance.Url); var request = CreateRequest(HttpMethod.Get, instance, $"posts/{postId}/?fields=id,title,slug,feature_image,html,created_at,updated_at,status"); var response = await _httpClient.SendAsync(request); if (!response.IsSuccessStatusCode) { var errorContent = await response.Content.ReadAsStringAsync(); _logger.LogError("Ghost API Fehler beim Laden von Post {PostId}: {StatusCode} - {Error}", postId, response.StatusCode, errorContent); return null; } var json = await response.Content.ReadAsStringAsync(); var doc = JsonDocument.Parse(json); if (doc.RootElement.TryGetProperty("posts", out var posts) && posts.GetArrayLength() > 0) { var post = posts[0]; return new GhostPost { Id = GetString(post, "id"), Title = GetString(post, "title"), Slug = GetString(post, "slug"), Html = GetString(post, "html"), FeatureImage = GetString(post, "feature_image"), CreatedAt = GetDateTime(post, "created_at"), UpdatedAt = GetDateTime(post, "updated_at"), Status = GetString(post, "status") }; } return null; } public async Task UpdatePostAsync(GhostInstance instance, GhostPost post) { _logger.LogInformation("Aktualisiere Post {PostId} ({Title}) in Ghost", post.Id, post.Title); var request = CreateRequest(HttpMethod.Put, instance, $"posts/{post.Id}/?source=html"); var body = new { posts = new[] { new { id = post.Id, title = post.Title, html = post.Html, status = post.Status, feature_image = post.FeatureImage, updated_at = post.UpdatedAt?.ToString("o") } } }; request.Content = new StringContent(JsonSerializer.Serialize(body), Encoding.UTF8, "application/json"); var response = await _httpClient.SendAsync(request); if (!response.IsSuccessStatusCode) { var errorContent = await response.Content.ReadAsStringAsync(); _logger.LogError("Ghost API Fehler beim Aktualisieren von Post {PostId}: {StatusCode} - {Error}", post.Id, response.StatusCode, errorContent); return false; } _logger.LogInformation("Post {PostId} erfolgreich aktualisiert", post.Id); return true; } public async Task> GetUsersAsync(GhostInstance instance) { _logger.LogInformation("Hole Benutzer von {Url}", instance.Url); var request = CreateRequest(HttpMethod.Get, instance, "users/?limit=100&fields=id,name,slug,email,profile_image"); var response = await _httpClient.SendAsync(request); if (!response.IsSuccessStatusCode) { var errorContent = await response.Content.ReadAsStringAsync(); _logger.LogError("Ghost API Fehler beim Laden der Benutzer: {StatusCode} - {Error}", response.StatusCode, errorContent); return new List(); } var json = await response.Content.ReadAsStringAsync(); var doc = JsonDocument.Parse(json); var users = new List(); if (doc.RootElement.TryGetProperty("users", out var usersElement)) { foreach (var user in usersElement.EnumerateArray()) { users.Add(new GhostUser { Id = GetString(user, "id"), Name = GetString(user, "name"), Slug = GetString(user, "slug"), Email = GetString(user, "email"), ProfileImage = GetString(user, "profile_image") }); } } _logger.LogInformation("{Count} Benutzer von Ghost geladen", users.Count); return users; } public async Task UploadImageAsync(GhostInstance instance, Stream fileStream, string fileName, string? reference = null) { _logger.LogInformation("Lade Bild {FileName} zu Ghost hoch", fileName); var token = JwtTokenService.GenerateToken(instance.AdminApiKey); var request = new HttpRequestMessage(HttpMethod.Post, $"{instance.Url.TrimEnd('/')}/ghost/api/admin/images/upload/"); request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Ghost", token); request.Headers.Add("Accept-Version", "v5.0"); var content = new MultipartFormDataContent(); var streamContent = new StreamContent(fileStream); var ext = Path.GetExtension(fileName).ToLowerInvariant(); var mimeType = ext switch { ".jpg" or ".jpeg" => "image/jpeg", ".png" => "image/png", ".gif" => "image/gif", ".webp" => "image/webp", ".svg" => "image/svg+xml", _ => "application/octet-stream" }; streamContent.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue(mimeType); content.Add(streamContent, "file", fileName); if (!string.IsNullOrEmpty(reference)) content.Add(new StringContent(reference), "ref"); request.Content = content; var response = await _httpClient.SendAsync(request); if (!response.IsSuccessStatusCode) { var errorContent = await response.Content.ReadAsStringAsync(); _logger.LogError("Ghost API Fehler beim Hochladen von {FileName}: {StatusCode} - {Error}", fileName, response.StatusCode, errorContent); return null; } var json = await response.Content.ReadAsStringAsync(); var doc = JsonDocument.Parse(json); if (doc.RootElement.TryGetProperty("images", out var images) && images.GetArrayLength() > 0) { var url = GetString(images[0], "url"); _logger.LogInformation("Bild {FileName} hochgeladen: {Url}", fileName, url); return url; } _logger.LogWarning("Keine URL im Upload-Response für {FileName} gefunden", fileName); return null; } private HttpRequestMessage CreateRequest(HttpMethod method, GhostInstance instance, string endpoint) { var token = JwtTokenService.GenerateToken(instance.AdminApiKey); var request = new HttpRequestMessage(method, $"{instance.Url.TrimEnd('/')}/ghost/api/admin/{endpoint}"); request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Ghost", token); request.Headers.Add("Accept-Version", "v5.0"); return request; } private static string GetString(JsonElement element, string property) { if (element.TryGetProperty(property, out var prop) && prop.ValueKind != JsonValueKind.Null) return prop.GetString() ?? ""; return ""; } private static DateTime? GetDateTime(JsonElement element, string property) { if (element.TryGetProperty(property, out var prop) && prop.ValueKind != JsonValueKind.Null) if (DateTime.TryParse(prop.GetString(), out var dt)) return dt; return null; } }