using System.Text.Json; using System.Text.Json.Serialization; 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); 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 "0": ImageManager.SaveCache(); 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(" 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 { "# 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(); 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}"); } 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(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(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($" Kommentare: {postFile.Comments.Count}"); Console.WriteLine($" Bilder: {postFile.ImageFiles.Count}"); // 1. ALLE Bilder hochladen + Ghost-URL-Mapping erstellen var ghostUrlMap = new Dictionary(); string html = postFile.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 var tags = new List(); if (postFile.Categories.Count > 0) { Console.WriteLine(" [Tags] Verarbeite Kategorien..."); tags = await GhostApi.EnsureTagsAsync(postFile.Categories); } // 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))}..."); } } } } } }