Wp2ghost/CSharpGhostImporter/Converter.cs
2026-09-20 08:31:23 +02:00

635 lines
24 KiB
C#

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<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();
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<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($" 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 ?? "";
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<GhostAdminTag>();
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))}...");
}
}
}
}
}
}