Wp2ghost/CSharpGhostImporter/Converter.cs

882 lines
33 KiB
C#

using System.Text.Json;
using System.Text.Json.Serialization;
using System.Net;
using System.Net.Http;
using HtmlAgilityPack;
using wp2ghost;
class Program
{
private static string WpBaseUrl = "https://deine-domain.com";
internal static string OutputZipPath = "ghost_import.zip";
private static int MaxPosts = -1;
private static string ConfigPath = "";
private static string BaseDir = "";
private static string GhostUrl = "";
private static string GhostAdminApiKey = "";
private static string GhostContentApiKey = "";
static async Task Main(string[] args)
{
BaseDir = Directory.GetCurrentDirectory();
ConfigPath = Path.Combine(BaseDir, "config.txt");
LoadConfig();
ImageManager.Init(BaseDir);
PostFetcher.Init(BaseDir);
GhostExporter.Init(BaseDir);
LinkDecisionManager.Init(BaseDir);
GhostApi.Init(GhostUrl, GhostAdminApiKey, GhostContentApiKey, ImageManager.ImageContainerPath);
Console.OutputEncoding = System.Text.Encoding.UTF8;
Console.Title = "WP2Ghost Converter";
bool running = true;
while (running)
{
ShowMenu();
string? choice = Console.ReadLine()?.Trim();
switch (choice)
{
case "1":
await PostFetcher.FetchFromWordPressAsync(WpBaseUrl, MaxPosts);
break;
case "2":
await GhostExporter.CreateZipAsync();
break;
case "3":
ConfigureSettings();
break;
case "4":
ShowCacheInfo();
break;
case "5":
PostFetcher.ClearProcessedPosts();
break;
case "6":
ImageManager.ClearAll();
break;
case "7":
await GhostMenuAsync();
break;
case "8":
LinkDecisionManager.ShowInfo();
break;
case "0":
ImageManager.SaveCache();
LinkDecisionManager.Save();
running = false;
Console.WriteLine("Programm beendet. Auf Wiedersehen!");
break;
default:
Console.WriteLine("Ungültige Auswahl. Bitte erneut eingeben.");
break;
}
}
}
static void ShowMenu()
{
Console.WriteLine();
Console.WriteLine("=========================================");
Console.WriteLine(" WP2Ghost - WordPress to Ghost");
Console.WriteLine("=========================================");
Console.WriteLine($"WordPress-URL : {WpBaseUrl}");
Console.WriteLine($"Ausgabe-Datei : {OutputZipPath}");
Console.WriteLine($"Max. Beiträge/Seiten: {(MaxPosts == -1 ? "Alle" : MaxPosts.ToString())}");
Console.WriteLine($"Abgefragte Objekte : {PostFetcher.ProcessedCount}");
Console.WriteLine($"Ghost-URL : {(string.IsNullOrEmpty(GhostUrl) ? "(nicht konfiguriert)" : GhostUrl)}");
Console.WriteLine($"Ghost-Status : {(GhostApi.IsConfigured() ? "verbunden" : "nicht konfiguriert")}");
Console.WriteLine("-----------------------------------------");
Console.WriteLine(" 1 - Von WordPress abrufen (Posts & Pages)");
Console.WriteLine(" 2 - Ghost ZIP erstellen");
Console.WriteLine(" 3 - Einstellungen konfigurieren");
Console.WriteLine(" 4 - Cache-Informationen anzeigen");
Console.WriteLine(" 5 - Abgefragte Objekte zurücksetzen");
Console.WriteLine(" 6 - Bilder lokal löschen");
Console.WriteLine(" 7 - Beiträge direkt an Ghost senden");
Console.WriteLine(" 8 - Gespeicherte Link-Entscheidungen anzeigen");
Console.WriteLine(" 0 - Beenden");
Console.WriteLine("-----------------------------------------");
Console.Write("Auswahl: ");
}
static void LoadConfig()
{
if (!File.Exists(ConfigPath))
{
SaveConfig();
Console.WriteLine($"Neue Konfigurationsdatei erstellt: {ConfigPath}");
return;
}
try
{
var lines = File.ReadAllLines(ConfigPath);
foreach (var line in lines)
{
if (string.IsNullOrWhiteSpace(line) || line.StartsWith("#"))
continue;
var parts = line.Split('=', 2);
if (parts.Length == 2)
{
string key = parts[0].Trim();
string value = parts[1].Trim();
switch (key.ToLower())
{
case "wpbaseurl":
WpBaseUrl = value;
break;
case "outputzippath":
OutputZipPath = value;
break;
case "maxposts":
if (int.TryParse(value, out int max))
MaxPosts = max;
break;
case "ghosturl":
GhostUrl = value;
break;
case "ghostadminapikey":
GhostAdminApiKey = value;
break;
case "ghostcontentapikey":
GhostContentApiKey = value;
break;
}
}
}
}
catch (Exception ex)
{
Console.WriteLine($"Fehler beim Laden der Konfiguration: {ex.Message}");
}
}
static void SaveConfig()
{
var lines = new List<string>
{
"# WP2Ghost Konfiguration",
"# Ändere die Werte und starte das Programm neu",
"",
$"wpBaseUrl={WpBaseUrl}",
$"outputZipPath={OutputZipPath}",
$"maxPosts={MaxPosts}",
"",
"# Ghost API (optional - für direkten Push)",
$"ghostUrl={GhostUrl}",
$"ghostAdminApiKey={GhostAdminApiKey}",
};
File.WriteAllLines(ConfigPath, lines);
}
static void ConfigureSettings()
{
Console.WriteLine();
Console.WriteLine("--- Einstellungen konfigurieren ---");
Console.Write($"WordPress-URL [{WpBaseUrl}]: ");
string? input = Console.ReadLine()?.Trim();
if (!string.IsNullOrEmpty(input))
WpBaseUrl = input;
Console.Write($"Ausgabe-Datei [{OutputZipPath}]: ");
input = Console.ReadLine()?.Trim();
if (!string.IsNullOrEmpty(input))
OutputZipPath = input;
Console.Write($"Maximale Anzahl Posts (leer = alle) [{(MaxPosts == -1 ? "alle" : MaxPosts.ToString())}]: ");
input = Console.ReadLine()?.Trim();
if (!string.IsNullOrEmpty(input) && int.TryParse(input, out int max))
MaxPosts = max;
Console.WriteLine();
Console.WriteLine("--- Ghost API (optional) ---");
Console.Write($"Ghost-URL [{GhostUrl}]: ");
input = Console.ReadLine()?.Trim();
if (!string.IsNullOrEmpty(input))
GhostUrl = input;
Console.Write($"Ghost Admin API Key [{(string.IsNullOrEmpty(GhostAdminApiKey) ? "(nicht gesetzt)" : "****")}] : ");
input = Console.ReadLine()?.Trim();
if (!string.IsNullOrEmpty(input))
GhostAdminApiKey = input;
Console.Write($"Ghost Content API Key [{(string.IsNullOrEmpty(GhostContentApiKey) ? "(nicht gesetzt)" : "****")}] : ");
input = Console.ReadLine()?.Trim();
if (!string.IsNullOrEmpty(input))
GhostContentApiKey = input;
GhostApi.Init(GhostUrl, GhostAdminApiKey, GhostContentApiKey, ImageManager.ImageContainerPath);
SaveConfig();
Console.WriteLine("Einstellungen gespeichert.");
}
static void ShowCacheInfo()
{
Console.WriteLine();
Console.WriteLine("--- Cache-Informationen ---");
Console.WriteLine($"Abgefragte Objekte: {PostFetcher.ProcessedCount}");
Console.WriteLine($"Ghost-Beiträge geladen: {GhostExporter.PostCount}");
ImageManager.ShowInfo();
PostFetcher.ShowInfo();
}
static async Task GhostMenuAsync()
{
if (!GhostApi.IsConfigured())
{
Console.WriteLine();
Console.WriteLine("Ghost API nicht konfiguriert. Bitte zuerst Option 3 (Einstellungen) nutzen.");
return;
}
bool inGhostMenu = true;
while (inGhostMenu)
{
Console.WriteLine();
Console.WriteLine("=========================================");
Console.WriteLine(" Ghost Admin API");
Console.WriteLine("=========================================");
Console.WriteLine($" Ghost-URL : {GhostUrl}");
Console.WriteLine("-----------------------------------------");
Console.WriteLine(" 1 - Posts anzeigen (Ghost)");
Console.WriteLine(" 2 - Post lesen (nach ID)");
Console.WriteLine(" 3 - Post erstellen");
Console.WriteLine(" 4 - Post bearbeiten");
Console.WriteLine(" 5 - Post löschen");
Console.WriteLine(" 6 - Alle lokalen Posts an Ghost senden");
Console.WriteLine(" 7 - Bilder hochladen");
Console.WriteLine(" 8 - Post aus JSON erstellen/aktualisieren");
Console.WriteLine(" 0 - Zurück zum Hauptmenü");
Console.WriteLine("-----------------------------------------");
Console.Write("Auswahl: ");
string? choice = Console.ReadLine()?.Trim();
switch (choice)
{
case "1":
await GhostBrowsePostsAsync();
break;
case "2":
await GhostReadPostAsync();
break;
case "3":
await GhostCreatePostAsync();
break;
case "4":
await GhostEditPostAsync();
break;
case "5":
await GhostDeletePostAsync();
break;
case "6":
await GhostApi.PushAllPostsAsync(html => HtmlGhostConverter.Convert(html));
break;
case "7":
await GhostUploadImagesAsync();
break;
case "8":
await GhostCreateOrUpdateFromJsonAsync();
break;
case "0":
inGhostMenu = false;
break;
default:
Console.WriteLine("Ungültige Auswahl.");
break;
}
}
}
static async Task GhostBrowsePostsAsync()
{
Console.Write("Filter (leer = alle, z.B. status:published): ");
string? filter = Console.ReadLine()?.Trim();
Console.Write("Anzahl (leer = 50): ");
string? limitStr = Console.ReadLine()?.Trim();
int limit = int.TryParse(limitStr, out int l) ? l : 50;
Console.WriteLine();
Console.WriteLine("Lade Posts von Ghost...");
var posts = await GhostApi.BrowsePostsAsync(filter ?? "", limit);
if (posts.Count == 0)
{
Console.WriteLine("Keine Posts gefunden.");
return;
}
Console.WriteLine();
Console.WriteLine($" {"ID",-24} {"Status",-12} {"Titel"}");
Console.WriteLine($" {new string('-', 23)} {new string('-', 11)} {new string('-', 30)}");
foreach (var post in posts)
{
Console.WriteLine($" {post.Id,-24} {post.Status,-12} {post.Title}");
}
Console.WriteLine();
Console.WriteLine($" Gesamt: {posts.Count} Posts");
}
static async Task GhostReadPostAsync()
{
Console.Write("Post-ID: ");
string? id = Console.ReadLine()?.Trim();
if (string.IsNullOrEmpty(id)) return;
var post = await GhostApi.GetPostAsync(id);
if (post == null)
{
Console.WriteLine("Post nicht gefunden.");
return;
}
Console.WriteLine();
Console.WriteLine($" ID : {post.Id}");
Console.WriteLine($" Titel : {post.Title}");
Console.WriteLine($" Slug : {post.Slug}");
Console.WriteLine($" Status : {post.Status}");
Console.WriteLine($" Erstellt : {post.CreatedAt}");
Console.WriteLine($" Aktualisiert: {post.UpdatedAt}");
Console.WriteLine($" Feature Img : {post.FeatureImage ?? "(keine)"}");
Console.WriteLine($" HTML ({(post.Html?.Length ?? 0)} Zeichen):");
Console.WriteLine($" {new string('-', 50)}");
string preview = post.Html ?? "";
if (preview.Length > 500) preview = preview[..500] + "...";
Console.WriteLine($" {preview}");
}
static async Task GhostCreatePostAsync()
{
Console.Write("Titel: ");
string? title = Console.ReadLine()?.Trim();
if (string.IsNullOrEmpty(title)) return;
Console.Write("HTML-Inhalt: ");
string? html = Console.ReadLine()?.Trim();
// Links validieren und ggf. korrigieren
html = await ValidateAndFixLinksAsync(html);
Console.Write("Status (published/draft) [published]: ");
string? status = Console.ReadLine()?.Trim();
if (string.IsNullOrEmpty(status)) status = "published";
Console.Write("Slug (leer = automatisch): ");
string? slug = Console.ReadLine()?.Trim();
if (string.IsNullOrEmpty(slug)) slug = null;
Console.WriteLine();
Console.Write("Erstelle Post...");
var post = await GhostApi.CreatePostAsync(title, html ?? "", status, slug);
if (post != null)
Console.WriteLine($" OK (ID: {post.Id})");
else
Console.WriteLine(" FEHLER");
}
static async Task GhostEditPostAsync()
{
Console.Write("Post-ID: ");
string? id = Console.ReadLine()?.Trim();
if (string.IsNullOrEmpty(id)) return;
var existing = await GhostApi.GetPostAsync(id);
if (existing == null)
{
Console.WriteLine("Post nicht gefunden.");
return;
}
Console.WriteLine($" Aktueller Titel: {existing.Title}");
Console.Write("Neuer Titel (Enter = unverändert): ");
string? title = Console.ReadLine()?.Trim();
if (string.IsNullOrEmpty(title)) title = existing.Title;
Console.Write("Neuer HTML-Inhalt (Enter = unverändert): ");
string? html = Console.ReadLine()?.Trim();
if (string.IsNullOrEmpty(html)) html = existing.Html;
Console.Write($"Neuer Status ({existing.Status}): ");
string? status = Console.ReadLine()?.Trim();
if (string.IsNullOrEmpty(status)) status = existing.Status;
Console.WriteLine();
Console.Write("Aktualisiere Post...");
var updated = await GhostApi.UpdatePostAsync(id, title ?? "", html ?? "", existing.UpdatedAt ?? "", status ?? "published");
if (updated != null)
Console.WriteLine(" OK");
else
Console.WriteLine(" FEHLER");
}
static async Task GhostDeletePostAsync()
{
Console.Write("Post-ID: ");
string? id = Console.ReadLine()?.Trim();
if (string.IsNullOrEmpty(id)) return;
var post = await GhostApi.GetPostAsync(id);
if (post == null)
{
Console.WriteLine("Post nicht gefunden.");
return;
}
Console.WriteLine($" Post: {post.Title}");
Console.Write("Wirklich löschen? (j/n): ");
string? confirm = Console.ReadLine()?.Trim();
if (confirm?.ToLower() != "j") return;
Console.Write("Lösche Post...");
bool success = await GhostApi.DeletePostAsync(id);
Console.WriteLine(success ? " OK" : " FEHLER");
}
static async Task GhostUploadImagesAsync()
{
var cachedFiles = ImageManager.CachedFiles;
if (cachedFiles.Count == 0)
{
Console.WriteLine("Keine Bilder im Cache. Bitte zuerst Option 1 (Abruf) nutzen.");
return;
}
Console.WriteLine($" {cachedFiles.Count} Bilder zum Hochladen vorhanden.");
Console.Write("Alle Bilder hochladen? (j/n): ");
string? confirm = Console.ReadLine()?.Trim();
if (confirm?.ToLower() != "j") return;
int uploaded = 0;
foreach (var file in cachedFiles)
{
Console.Write($" [Upload] {Path.GetFileName(file.LocalPath)}...");
string? url = await GhostApi.UploadImageAsync(file.LocalPath);
if (url != null)
{
Console.WriteLine($" -> {url}");
uploaded++;
}
else
Console.WriteLine(" FEHLER");
}
Console.WriteLine();
Console.WriteLine($" Hochgeladen: {uploaded}/{cachedFiles.Count}");
}
// =========================================================================
// HTML-Links validieren und bei Bedarf korrigieren
// =========================================================================
private static async Task<string> ValidateAndFixLinksAsync(string? html)
{
if (string.IsNullOrWhiteSpace(html)) return html ?? "";
var doc = new HtmlDocument();
doc.LoadHtml(html);
var invalidLinks = new List<(string text, string href, HtmlNode node)>();
// Alle <a>-Tags suchen
var anchors = doc.DocumentNode.SelectNodes("//a[@href]");
if (anchors == null) return html;
foreach (var anchor in anchors)
{
string href = anchor.GetAttributeValue("href", "").Trim();
string text = anchor.InnerText.Trim();
if (string.IsNullOrEmpty(href)) continue;
// Prüfe, ob URL valide ist (inkl. Reichbarkeitsprüfung)
if (!await IsValidUrlAsync(href, checkReachability: true))
{
invalidLinks.Add((text, href, anchor));
}
}
if (invalidLinks.Count == 0) return html;
// User über invaliden Links informieren und zur Korrektur auffordern
Console.WriteLine($"\n {invalidLinks.Count} ungültiger Link(s) gefunden:");
for (int i = 0; i < invalidLinks.Count; i++)
{
Console.WriteLine($" [{i + 1}] Text: \"{invalidLinks[i].text}\" | Link: {invalidLinks[i].href}");
}
Console.WriteLine("\nOptionen pro Link:");
Console.WriteLine(" d = Link löschen (Text bleibt, <a>-Tag entfernt)");
Console.WriteLine(" e = Link bearbeiten (neue URL eingeben)");
Console.WriteLine(" Enter = Link so übernehmen (beibehalten)");
// Für jeden invaliden Link die User-Entscheidung einholen
foreach (var (linkText, linkHref, anchorNode) in invalidLinks)
{
// 1. Memory: Frühere Entscheidung suchen und vorschlagen
var previous = LinkDecisionManager.GetPreviousDecision(linkText, linkHref);
Console.WriteLine($"\n ---------------------------------------------");
Console.WriteLine($" Link-Text : \"{linkText}\"");
Console.WriteLine($" Aktueller Link : {linkHref}");
if (previous != null)
{
string prevInfo = previous.Decision switch
{
"delete" => "LÖSCHEN",
"edit" => $"ERSETZEN durch '{previous.NewUrl}'",
_ => "BEIBEHALTEN"
};
Console.WriteLine($" [Memory] Frühere Entscheidung: {prevInfo} (vom {previous.Timestamp:dd.MM.yyyy HH:mm})");
}
// 2. Browser-Öffnen anbieten (da Link nicht erreichbar ist)
string searchQuery = Uri.EscapeDataString(linkText);
string searchUrl = $"https://www.google.com/search?q={searchQuery}";
Console.WriteLine($" [Suche] Fallstrick: Link nicht erreichbar. Browser-Suche verfügbar.");
Console.Write($" Optionen: [Enter=übernehmen" + (previous != null ? $" / v=Vorschlag '{previous.Decision}' anwenden" : "") + " / d=löschen / e=bearbeiten / b=Browser öffnen]: ");
string? choice = Console.ReadLine()?.Trim()?.ToLowerInvariant();
// Browser öffnen
if (choice == "b")
{
Console.WriteLine($" -> Öffne Browser mit Suche nach: \"{linkText}\"");
OpenBrowser(searchUrl);
// Nach dem Browser-Öffnen erneut nach der Entscheidung fragen
Console.Write($" Jetzt entscheiden? [Enter=übernehmen / d=löschen / e=bearbeiten]: ");
choice = Console.ReadLine()?.Trim()?.ToLowerInvariant();
}
// Memory-Vorschlag anwenden
if (choice == "v" && previous != null)
{
ApplyDecision(anchorNode, previous, linkText, linkHref, ref html, doc);
Console.WriteLine($" -> Frühere Entscheidung angewendet: {previous.Decision}");
continue;
}
if (choice == "d")
{
// Link löschen: <a>-Tag entfernen, Text erhalten
var parent = anchorNode.ParentNode;
if (parent != null)
{
parent.ReplaceChild(HtmlNode.CreateNode(anchorNode.InnerText), anchorNode);
}
Console.WriteLine($" -> Link gelöscht");
LinkDecisionManager.SaveDecision(linkText, linkHref, "delete");
}
else if (choice == "e")
{
// Link bearbeiten: neue URL eingeben
Console.Write(" Neue URL eingeben: ");
string? newUrl = Console.ReadLine()?.Trim();
if (!string.IsNullOrEmpty(newUrl))
{
anchorNode.SetAttributeValue("href", newUrl);
Console.WriteLine($" -> Auf '{newUrl}' aktualisiert");
LinkDecisionManager.SaveDecision(linkText, linkHref, "edit", newUrl);
}
else
{
Console.WriteLine($" -> Beibehalten");
LinkDecisionManager.SaveDecision(linkText, linkHref, "keep");
}
}
else
{
Console.WriteLine($" -> Beibehalten");
LinkDecisionManager.SaveDecision(linkText, linkHref, "keep");
}
}
return doc.DocumentNode.OuterHtml;
}
// =========================================================================
// Hilfsmethoden für Link-Entscheidungen
// =========================================================================
/// <summary>
/// Wendet eine gespeicherte (frühere) Entscheidung auf den Link an.
/// </summary>
private static void ApplyDecision(HtmlNode anchorNode, LinkDecision decision, string linkText, string linkHref, ref string? html, HtmlDocument doc)
{
if (decision.Decision == "delete")
{
var parent = anchorNode.ParentNode;
if (parent != null)
{
parent.ReplaceChild(HtmlNode.CreateNode(anchorNode.InnerText), anchorNode);
}
}
else if (decision.Decision == "edit" && !string.IsNullOrEmpty(decision.NewUrl))
{
anchorNode.SetAttributeValue("href", decision.NewUrl);
}
// "keep" = nichts tun
}
/// <summary>
/// Öffnet die Standard-Browser-Anwendung mit der angegebenen URL.
/// </summary>
private static void OpenBrowser(string url)
{
try
{
var psi = new System.Diagnostics.ProcessStartInfo
{
FileName = url,
UseShellExecute = true
};
System.Diagnostics.Process.Start(psi);
}
catch (Exception ex)
{
Console.WriteLine($" [Browser-Fehler] Konnte Browser nicht öffnen: {ex.Message}");
}
}
private static async Task<bool> IsValidUrlAsync(string url, bool checkReachability = false)
{
if (string.IsNullOrWhiteSpace(url)) return false;
// Relative URLs (beginnen mit / oder sind Pfade) sind für interne Links gültig
if (url.StartsWith("/") || !url.Contains(":"))
{
if (checkReachability) { /* relative links are considered valid regardless */ }
return true;
}
// Prüfe auf gültiges URI-Format mit http/https Schemes
try
{
var uri = new Uri(url, UriKind.Absolute);
if (uri.Scheme != Uri.UriSchemeHttp && uri.Scheme != Uri.UriSchemeHttps)
return false;
// Optionale Reichbarkeitsprüfung
if (checkReachability)
{
using var httpClient = new HttpClient { Timeout = TimeSpan.FromSeconds(3) };
try
{
var request = new HttpRequestMessage(HttpMethod.Head, uri);
var response = await httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead);
return (int)response.StatusCode < 400; // 2xx oder 3xx = erreichbar
}
catch (HttpRequestException)
{
// Verbindungsfehler, DNS-Failure, Timeout etc.
return false;
}
catch (TaskCanceledException)
{
// Timeout
return false;
}
}
return true;
}
catch
{
return false;
}
}
// Synchronen Wrapper für die bestehende Validierung
private static bool IsValidUrl(string url)
{
return IsValidUrlAsync(url, false).Result;
}
static async Task GhostCreateOrUpdateFromJsonAsync()
{
string ghostPostsDir = Path.Combine(BaseDir, "ghostPosts");
if (!Directory.Exists(ghostPostsDir))
{
Console.WriteLine("Verzeichnis 'ghostPosts' nicht gefunden.");
return;
}
var jsonFiles = Directory.GetFiles(ghostPostsDir, "*.json");
if (jsonFiles.Length == 0)
{
Console.WriteLine("Keine JSON-Dateien in ghostPosts/ vorhanden.");
return;
}
Console.WriteLine($" {jsonFiles.Length} verfügbare Posts:");
foreach (var f in jsonFiles)
{
string id = Path.GetFileNameWithoutExtension(f);
Console.Write($" {id} - ");
try
{
var preview = JsonSerializer.Deserialize<GhostPostFile>(File.ReadAllText(f));
Console.WriteLine(preview?.Title ?? "(kein Titel)");
}
catch { Console.WriteLine("(Fehler beim Lesen)"); }
}
Console.Write("Ghost-ID des Posts: ");
string? ghostId = Console.ReadLine()?.Trim();
if (string.IsNullOrEmpty(ghostId)) return;
string jsonPath = Path.Combine(ghostPostsDir, $"{ghostId}.json");
if (!File.Exists(jsonPath))
{
Console.WriteLine($"Datei {jsonPath} nicht gefunden.");
return;
}
var postFile = JsonSerializer.Deserialize<GhostPostFile>(File.ReadAllText(jsonPath));
if (postFile == null)
{
Console.WriteLine("JSON konnte nicht gelesen werden.");
return;
}
Console.WriteLine($" Post: {postFile.Title}");
Console.WriteLine($" Slug: {postFile.Slug}");
Console.WriteLine($" Kategorien: {postFile.Categories.Count}");
Console.WriteLine($" Tags: {postFile.Tags.Count}");
Console.WriteLine($" Kommentare: {postFile.Comments.Count}");
Console.WriteLine($" Bilder: {postFile.ImageFiles.Count}");
// 1. ALLE Bilder hochladen + Ghost-URL-Mapping erstellen
var ghostUrlMap = new Dictionary<string, string>();
string html = postFile.Html ?? "";
html = await ValidateAndFixLinksAsync(html);
if (postFile.ImageFiles.Count > 0)
{
Console.WriteLine($" [Bilder] Lade {postFile.ImageFiles.Count} Bilder hoch...");
foreach (string relPath in postFile.ImageFiles)
{
string localPath = Path.Combine(BaseDir, "images", relPath);
if (!File.Exists(localPath))
{
Console.WriteLine($" [Übersprungen] {relPath} (nicht lokal)");
continue;
}
Console.Write($" [Upload] {Path.GetFileName(localPath)}...");
string? url = await GhostApi.UploadImageIfNewAsync(localPath);
if (url != null)
{
Console.WriteLine($" -> {url}");
// Backslash-normalisiert für alte JSONs
string normalized = relPath.Replace("\\", "/");
string placeholder1 = $"__GHOST_URL__/content/images/{normalized}";
string placeholder2 = $"__GHOST_URL__/content/images/{relPath}";
ghostUrlMap[placeholder1] = url;
if (placeholder1 != placeholder2)
ghostUrlMap[placeholder2] = url;
}
else
Console.WriteLine(" FEHLER");
}
}
// 2. Featured Image hochladen (separat, da nicht in ImageFiles)
string? featureImageUrl = null;
if (!string.IsNullOrEmpty(postFile.FeatureImage))
{
string localFeatPath = postFile.FeatureImage.Replace("__GHOST_URL__/content/images/", "");
localFeatPath = Path.Combine(BaseDir, "images", localFeatPath);
if (File.Exists(localFeatPath))
{
Console.Write(" [Featured Image]...");
featureImageUrl = await GhostApi.UploadImageIfNewAsync(localFeatPath);
Console.WriteLine(featureImageUrl != null ? $" OK" : " FEHLER");
if (featureImageUrl != null)
ghostUrlMap[postFile.FeatureImage] = featureImageUrl;
}
}
// 3. __GHOST_URL__ im HTML durch echte URLs ersetzen
foreach (var kvp in ghostUrlMap)
html = html.Replace(kvp.Key, kvp.Value);
// Falls noch Rest Platzhalter → Warnung
if (html.Contains("__GHOST_URL__"))
{
int remaining = System.Text.RegularExpressions.Regex.Matches(html, "__GHOST_URL__").Count;
Console.WriteLine($" [Warnung] {remaining} unrestrierte __GHOST_URL__ Platzhalter im HTML");
}
// 4. Tags erstellen/zuordnen (Kategorien + Tags)
var tags = new List<GhostAdminTag>();
var allTagSources = new List<WpCategory>();
if (postFile.Categories.Count > 0)
allTagSources.AddRange(postFile.Categories);
if (postFile.Tags.Count > 0)
allTagSources.AddRange(postFile.Tags);
if (allTagSources.Count > 0)
{
Console.WriteLine($" [Tags] Verarbeite {postFile.Categories.Count} Kategorien + {postFile.Tags.Count} Tags...");
tags = await GhostApi.EnsureTagsAsync(allTagSources);
}
// 5. Prüfe ob Post mit gleichem Slug bereits in Ghost existiert
Console.Write(" Prüfe Ghost...");
var existingPosts = await GhostApi.BrowsePostsAsync($"slug:{postFile.Slug}", 1);
var existing = existingPosts.FirstOrDefault();
GhostAdminPost? result = null;
if (existing != null)
{
Console.WriteLine($" vorhanden (ID: {existing.Id})");
Console.Write(" Aktualisiere...");
result = await GhostApi.UpdatePostExtendedAsync(
existing.Id!, postFile.Title ?? "", html,
existing.UpdatedAt ?? "", postFile.Status ?? "published",
featureImageUrl, tags, postFile.Keywords);
Console.WriteLine(result != null ? " OK" : " FEHLER");
}
else
{
Console.WriteLine(" nicht vorhanden");
Console.Write(" Erstelle neuen Post...");
result = await GhostApi.CreatePostExtendedAsync(
postFile.Title ?? "", html,
postFile.Status ?? "published", postFile.Slug,
featureImageUrl, tags, postFile.Keywords);
Console.WriteLine(result != null ? $" OK (ID: {result.Id})" : " FEHLER");
}
// 4. Kommentare als Members verarbeiten
if (postFile.Comments.Count > 0)
{
Console.WriteLine();
Console.Write($" {postFile.Comments.Count} Kommentare verarbeiten? (j/n): ");
string? commentConfirm = Console.ReadLine()?.Trim();
if (commentConfirm?.ToLower() == "j")
{
foreach (var comment in postFile.Comments)
{
if (string.IsNullOrEmpty(comment.AuthorEmail)) continue;
var member = await GhostApi.EnsureMemberAsync(comment.AuthorName ?? "Anonymous", comment.AuthorEmail);
if (member != null)
{
Console.WriteLine($" [Kommentar] {comment.AuthorName}: {comment.Content?.Substring(0, Math.Min(50, comment.Content?.Length ?? 0))}...");
}
}
}
}
}
}