302 lines
12 KiB
C#
302 lines
12 KiB
C#
using System.Text;
|
||
using System.Text.Json;
|
||
using GhostImageManager.Models;
|
||
|
||
namespace GhostImageManager.Services;
|
||
|
||
public class GhostApiService
|
||
{
|
||
private readonly HttpClient _httpClient;
|
||
private readonly ILogger<GhostApiService> _logger;
|
||
|
||
public GhostApiService(HttpClient httpClient, ILogger<GhostApiService> logger)
|
||
{
|
||
_httpClient = httpClient;
|
||
_logger = logger;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Lädt alle Einträge einer Ressource ("posts" oder "pages") – seitenweise, bis nichts mehr kommt.
|
||
/// </summary>
|
||
public async Task<List<GhostPost>> 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<GhostPost>();
|
||
|
||
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;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 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.
|
||
/// </summary>
|
||
public async Task<List<(string Label, string Json)>> 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<JsonDocument?> 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<GhostPost?> 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<bool> 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<List<GhostUser>> 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<GhostUser>();
|
||
}
|
||
|
||
var json = await response.Content.ReadAsStringAsync();
|
||
var doc = JsonDocument.Parse(json);
|
||
var users = new List<GhostUser>();
|
||
|
||
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<string?> 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;
|
||
}
|
||
}
|