2026-09-24 19:23:54 +02:00
using System.Text ;
using System.Text.Json ;
using GhostImageManager.Models ;
namespace GhostImageManager.Services ;
public class GhostApiService
{
private readonly HttpClient _httpClient ;
2026-09-28 19:09:09 +02:00
private readonly ILogger < GhostApiService > _logger ;
2026-09-24 19:23:54 +02:00
2026-09-28 19:09:09 +02:00
public GhostApiService ( HttpClient httpClient , ILogger < GhostApiService > logger )
2026-09-24 19:23:54 +02:00
{
_httpClient = httpClient ;
2026-09-28 19:09:09 +02:00
_logger = logger ;
2026-09-24 19:23:54 +02:00
}
2026-10-14 19:12:29 +02:00
/// <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" )
2026-09-24 19:23:54 +02:00
{
2026-10-14 19:12:29 +02:00
_logger . LogInformation ( "Hole {Resource} von {Url} (Filter: {Filter}, Limit: {Limit})" , resource , instance . Url , filter , limit ) ;
var posts = new List < GhostPost > ( ) ;
2026-09-28 19:09:09 +02:00
2026-10-14 19:12:29 +02:00
for ( var page = 1 ; ; page + + )
2026-09-28 19:09:09 +02:00
{
2026-10-14 19:12:29 +02:00
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" ;
2026-09-24 19:23:54 +02:00
2026-10-14 19:12:29 +02:00
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 ;
2026-09-24 19:23:54 +02:00
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" ) ,
2026-10-14 19:12:29 +02:00
Lexical = GetString ( post , "lexical" ) ,
OtherImageFields = string . Join ( "\n" ,
GetString ( post , "og_image" ) ,
GetString ( post , "twitter_image" ) ,
GetString ( post , "codeinjection_head" ) ,
GetString ( post , "codeinjection_foot" ) ) ,
2026-09-24 19:23:54 +02:00
FeatureImage = GetString ( post , "feature_image" ) ,
CreatedAt = GetDateTime ( post , "created_at" ) ,
UpdatedAt = GetDateTime ( post , "updated_at" ) ,
Status = GetString ( post , "status" )
} ) ;
}
2026-10-14 19:12:29 +02:00
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 ;
2026-09-24 19:23:54 +02:00
}
2026-10-14 19:12:29 +02:00
_logger . LogInformation ( "{Count} {Resource} von Ghost geladen" , posts . Count , resource ) ;
2026-09-24 19:23:54 +02:00
return posts ;
}
2026-10-14 19:12:29 +02:00
/// <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 ( ) ) ;
}
2026-09-24 19:23:54 +02:00
public async Task < GhostPost ? > GetPostAsync ( GhostInstance instance , string postId )
{
2026-09-28 19:09:09 +02:00
_logger . LogInformation ( "Lade Post {PostId} von {Url}" , postId , instance . Url ) ;
2026-09-24 19:23:54 +02:00
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 ) ;
2026-09-28 19:09:09 +02:00
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 ;
}
2026-09-24 19:23:54 +02:00
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 )
{
2026-09-28 19:09:09 +02:00
_logger . LogInformation ( "Aktualisiere Post {PostId} ({Title}) in Ghost" , post . Id , post . Title ) ;
2026-09-24 19:23:54 +02:00
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 ) ;
2026-09-28 19:09:09 +02:00
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 ;
2026-09-24 19:23:54 +02:00
}
public async Task < string? > UploadImageAsync ( GhostInstance instance , Stream fileStream , string fileName , string? reference = null )
{
2026-09-28 19:09:09 +02:00
_logger . LogInformation ( "Lade Bild {FileName} zu Ghost hoch" , fileName ) ;
2026-09-24 19:23:54 +02:00
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 ) ;
2026-09-28 19:09:09 +02:00
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 ;
}
2026-09-24 19:23:54 +02:00
var json = await response . Content . ReadAsStringAsync ( ) ;
var doc = JsonDocument . Parse ( json ) ;
if ( doc . RootElement . TryGetProperty ( "images" , out var images ) & & images . GetArrayLength ( ) > 0 )
{
2026-09-28 19:09:09 +02:00
var url = GetString ( images [ 0 ] , "url" ) ;
_logger . LogInformation ( "Bild {FileName} hochgeladen: {Url}" , fileName , url ) ;
return url ;
2026-09-24 19:23:54 +02:00
}
2026-09-28 19:09:09 +02:00
_logger . LogWarning ( "Keine URL im Upload-Response für {FileName} gefunden" , fileName ) ;
2026-09-24 19:23:54 +02:00
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 ;
}
}