Improve plugin and Fetcher
This commit is contained in:
parent
d9df4947f4
commit
b598b0d5af
9 changed files with 522 additions and 25 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -1,3 +1,4 @@
|
|||
/CSharpGhostImporter/.vs/*
|
||||
/CSharpGhostImporter/bin/*
|
||||
/CSharpGhostImporter/obj/*
|
||||
/0
|
||||
|
|
|
|||
|
|
@ -1,5 +1,8 @@
|
|||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using HtmlAgilityPack;
|
||||
using wp2ghost;
|
||||
|
||||
class Program
|
||||
|
|
@ -22,6 +25,7 @@ class Program
|
|||
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;
|
||||
|
|
@ -57,8 +61,12 @@ class Program
|
|||
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;
|
||||
|
|
@ -89,6 +97,7 @@ class Program
|
|||
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: ");
|
||||
|
|
@ -352,6 +361,9 @@ class Program
|
|||
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";
|
||||
|
|
@ -459,6 +471,233 @@ class Program
|
|||
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");
|
||||
|
|
@ -509,12 +748,14 @@ class Program
|
|||
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)
|
||||
{
|
||||
|
|
@ -572,12 +813,18 @@ class Program
|
|||
Console.WriteLine($" [Warnung] {remaining} unrestrierte __GHOST_URL__ Platzhalter im HTML");
|
||||
}
|
||||
|
||||
// 4. Tags erstellen/zuordnen
|
||||
// 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 Kategorien...");
|
||||
tags = await GhostApi.EnsureTagsAsync(postFile.Categories);
|
||||
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
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ namespace wp2ghost
|
|||
[JsonPropertyName("published_at")] public string? PublishedAt { get; set; }
|
||||
[JsonPropertyName("feature_image")] public string? FeatureImage { get; set; }
|
||||
[JsonPropertyName("categories")] public List<WpCategory> Categories { get; set; } = new();
|
||||
[JsonPropertyName("tags")] public List<WpCategory> Tags { get; set; } = new();
|
||||
[JsonPropertyName("keywords")] public string? Keywords { get; set; }
|
||||
[JsonPropertyName("comments")] public List<WpComment> Comments { get; set; } = new();
|
||||
[JsonPropertyName("image_files")] public List<string> ImageFiles { get; set; } = new();
|
||||
|
|
|
|||
|
|
@ -99,6 +99,7 @@ namespace wp2ghost
|
|||
ConvertSingleImages(htmlDoc);
|
||||
ConvertFigures(htmlDoc);
|
||||
ConvertWpClasses(htmlDoc);
|
||||
AddTargetBlankToExternalLinks(htmlDoc);
|
||||
|
||||
return htmlDoc.DocumentNode.OuterHtml;
|
||||
}
|
||||
|
|
@ -134,11 +135,27 @@ namespace wp2ghost
|
|||
container.AddClass("kg-gallery-container");
|
||||
|
||||
var wrappers = new List<HtmlNode>();
|
||||
var imageCaptions = new List<string>();
|
||||
|
||||
foreach (var item in items)
|
||||
{
|
||||
var img = item.SelectSingleNode(".//img");
|
||||
if (img == null) continue;
|
||||
|
||||
// Bild-Titel aus data-image-caption Attribut extrahieren
|
||||
string dataCaption = img.GetAttributeValue("data-image-caption", "").Trim();
|
||||
if (!string.IsNullOrEmpty(dataCaption))
|
||||
imageCaptions.Add(dataCaption);
|
||||
|
||||
// Auch figcaption innerhalb des gallery-items prüfen
|
||||
var figCap = item.SelectSingleNode(".//figcaption");
|
||||
if (figCap != null && !string.IsNullOrWhiteSpace(figCap.InnerText))
|
||||
{
|
||||
string capText = figCap.InnerText.Trim();
|
||||
if (!string.IsNullOrEmpty(capText) && !imageCaptions.Contains(capText))
|
||||
imageCaptions.Add(capText);
|
||||
}
|
||||
|
||||
string src = GetBestImageUrl(img);
|
||||
if (string.IsNullOrEmpty(src)) continue;
|
||||
|
||||
|
|
@ -150,11 +167,14 @@ namespace wp2ghost
|
|||
AppendGalleryRows(htmlDoc, container, wrappers, GetGalleryColumns(gallery));
|
||||
ghostGallery.AppendChild(container);
|
||||
|
||||
var captionNode = gallery.SelectSingleNode(".//*[contains(@class, 'gallery-caption')] | .//figcaption");
|
||||
if (captionNode != null && !string.IsNullOrWhiteSpace(captionNode.InnerText))
|
||||
// Gallery-Bild-Titel übertragen, wenn vorhanden
|
||||
if (imageCaptions.Count > 0)
|
||||
{
|
||||
// Ersten nicht-leeren Titel nehmen als Gallery-Kapitel
|
||||
string firstCaption = imageCaptions[0];
|
||||
|
||||
var newCaption = htmlDoc.CreateElement("figcaption");
|
||||
newCaption.InnerHtml = captionNode.InnerHtml;
|
||||
newCaption.InnerHtml = firstCaption;
|
||||
ghostGallery.AppendChild(newCaption);
|
||||
ghostGallery.AddClass("kg-card-hascaption");
|
||||
}
|
||||
|
|
@ -207,14 +227,23 @@ namespace wp2ghost
|
|||
AppendGalleryRows(htmlDoc, container, wrappers, GetGalleryColumns(gallery));
|
||||
ghostGallery.AppendChild(container);
|
||||
|
||||
// Caption aus erstem figcaption im Gallery-Container extrahieren
|
||||
var captionNode = gallery.SelectSingleNode(".//figcaption");
|
||||
if (captionNode != null && !string.IsNullOrWhiteSpace(captionNode.InnerText))
|
||||
// Alle individuellen Bildunterschriften aus der Gutenberg-Gallery sammeln
|
||||
var captionNodes = gallery.SelectNodes(".//figcaption");
|
||||
if (captionNodes != null && captionNodes.Count > 0)
|
||||
{
|
||||
var newCaption = htmlDoc.CreateElement("figcaption");
|
||||
newCaption.InnerHtml = captionNode.InnerHtml;
|
||||
ghostGallery.AppendChild(newCaption);
|
||||
ghostGallery.AddClass("kg-card-hascaption");
|
||||
var distinctCaptions = captionNodes
|
||||
.Select(n => n.InnerText.Trim())
|
||||
.Where(t => !string.IsNullOrWhiteSpace(t))
|
||||
.Distinct()
|
||||
.ToList();
|
||||
|
||||
if (distinctCaptions.Count > 0)
|
||||
{
|
||||
var newCaption = htmlDoc.CreateElement("figcaption");
|
||||
newCaption.InnerHtml = string.Join(" · ", distinctCaptions);
|
||||
ghostGallery.AppendChild(newCaption);
|
||||
ghostGallery.AddClass("kg-card-hascaption");
|
||||
}
|
||||
}
|
||||
|
||||
// data-carousel-extra als Ghost-Attribut übertragen (falls vorhanden)
|
||||
|
|
@ -567,6 +596,28 @@ namespace wp2ghost
|
|||
}
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Externe Links mit target="_blank" versehen
|
||||
// =========================================================================
|
||||
private static void AddTargetBlankToExternalLinks(HtmlDocument htmlDoc)
|
||||
{
|
||||
var anchors = htmlDoc.DocumentNode.SelectNodes("//a[@href]");
|
||||
if (anchors == null) return;
|
||||
|
||||
foreach (var anchor in anchors.ToList())
|
||||
{
|
||||
string href = anchor.GetAttributeValue("href", "").Trim();
|
||||
if (string.IsNullOrEmpty(href)) continue;
|
||||
|
||||
// Nur absolute externe URLs (http/https)
|
||||
if (href.StartsWith("http://") || href.StartsWith("https://"))
|
||||
{
|
||||
anchor.SetAttributeValue("target", "_blank");
|
||||
anchor.SetAttributeValue("rel", "noopener noreferrer");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static bool HasClass(HtmlNode node, string className)
|
||||
{
|
||||
string classes = node.GetAttributeValue("class", "");
|
||||
|
|
|
|||
|
|
@ -20,11 +20,28 @@ namespace wp2ghost
|
|||
public static IReadOnlyList<(string RelativePath, string LocalPath)> CachedFiles => FilesToZip;
|
||||
public static bool TryGetRelativePath(string url, out string relativePath)
|
||||
{
|
||||
// 1. Direkte URL-Prüfung im Cache
|
||||
if (Cache.TryGetValue(url, out var cached))
|
||||
{
|
||||
relativePath = cached.RelativePath;
|
||||
return true;
|
||||
}
|
||||
|
||||
// 2. Prüfung über WordPress-Dateinamen (fürFeatured Images mit anderer URL)
|
||||
string wpName = Path.GetFileNameWithoutExtension(new Uri(url).AbsolutePath);
|
||||
if (WordpressToGhostImageName.TryGetValue(wpName, out string? relPath) && !string.IsNullOrEmpty(relPath))
|
||||
{
|
||||
string localPath = Path.Combine(LocalImageDir, relPath);
|
||||
if (File.Exists(localPath))
|
||||
{
|
||||
relativePath = relPath;
|
||||
// Auch im Cache eintragen für zukünftige Suche
|
||||
Cache[url] = (relPath, localPath);
|
||||
return true;
|
||||
}
|
||||
WordpressToGhostImageName.Remove(wpName);
|
||||
}
|
||||
|
||||
relativePath = "";
|
||||
return false;
|
||||
}
|
||||
|
|
@ -46,7 +63,10 @@ namespace wp2ghost
|
|||
{
|
||||
var lines = new List<string>();
|
||||
foreach (var kvp in Cache)
|
||||
lines.Add($"{kvp.Key}|{kvp.Value.RelativePath}|{kvp.Value.LocalPath}");
|
||||
{
|
||||
string wpName = Path.GetFileNameWithoutExtension(new Uri(kvp.Key).AbsolutePath);
|
||||
lines.Add($"{kvp.Key}|{kvp.Value.RelativePath}|{kvp.Value.LocalPath}|{wpName}");
|
||||
}
|
||||
File.WriteAllLines(CachePath, lines);
|
||||
}
|
||||
|
||||
|
|
@ -223,8 +243,13 @@ namespace wp2ghost
|
|||
foreach (var line in lines)
|
||||
{
|
||||
var parts = line.Split('|');
|
||||
if (parts.Length == 3)
|
||||
if (parts.Length >= 3)
|
||||
{
|
||||
Cache[parts[0]] = (parts[1], parts[2]);
|
||||
// WordPress-Dateinamen auch ins Dictionary laden
|
||||
if (parts.Length >= 4 && !string.IsNullOrEmpty(parts[3]))
|
||||
WordpressToGhostImageName[parts[3]] = parts[1];
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
|
|
|
|||
138
CSharpGhostImporter/LinkDecisionManager.cs
Normal file
138
CSharpGhostImporter/LinkDecisionManager.cs
Normal file
|
|
@ -0,0 +1,138 @@
|
|||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace wp2ghost
|
||||
{
|
||||
// =============================================================================
|
||||
// LinkDecision — gespeicherte Entscheidung für einen Link
|
||||
// =============================================================================
|
||||
public class LinkDecision
|
||||
{
|
||||
[JsonPropertyName("link_text")]
|
||||
public string LinkText { get; set; } = "";
|
||||
|
||||
[JsonPropertyName("link_href")]
|
||||
public string LinkHref { get; set; } = "";
|
||||
|
||||
// Entscheidung: "keep" (beibehalten), "delete" (löschen), "edit" (ersetzt)
|
||||
[JsonPropertyName("decision")]
|
||||
public string Decision { get; set; } = "";
|
||||
|
||||
// Nur bei "edit": die neue URL
|
||||
[JsonPropertyName("new_url")]
|
||||
public string? NewUrl { get; set; }
|
||||
|
||||
[JsonPropertyName("timestamp")]
|
||||
public DateTime Timestamp { get; set; } = DateTime.Now;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// LinkDecisionManager — merkt sich User-Entscheidungen für Links (Memory)
|
||||
// =============================================================================
|
||||
public static class LinkDecisionManager
|
||||
{
|
||||
private static Dictionary<string, LinkDecision> _decisions = new(StringComparer.OrdinalIgnoreCase);
|
||||
private static string _decisionsPath = "";
|
||||
|
||||
// Key = Normalisierter Link (Text|Href)
|
||||
private static string NormalizeKey(string linkText, string linkHref)
|
||||
{
|
||||
return $"{linkText.Trim().ToLowerInvariant()}|{linkHref.Trim().ToLowerInvariant()}";
|
||||
}
|
||||
|
||||
public static void Init(string baseDir)
|
||||
{
|
||||
_decisionsPath = Path.Combine(baseDir, "link_decisions.json");
|
||||
Load();
|
||||
}
|
||||
|
||||
public static void Save()
|
||||
{
|
||||
if (string.IsNullOrEmpty(_decisionsPath)) return;
|
||||
try
|
||||
{
|
||||
var options = new JsonSerializerOptions { WriteIndented = true };
|
||||
File.WriteAllText(_decisionsPath, JsonSerializer.Serialize(_decisions.Values.ToList(), options));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($" [Link-Memory] Fehler beim Speichern der Entscheidungen: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private static void Load()
|
||||
{
|
||||
if (!File.Exists(_decisionsPath)) return;
|
||||
try
|
||||
{
|
||||
var json = File.ReadAllText(_decisionsPath);
|
||||
var list = JsonSerializer.Deserialize<List<LinkDecision>>(json);
|
||||
if (list != null)
|
||||
{
|
||||
foreach (var d in list)
|
||||
{
|
||||
string key = NormalizeKey(d.LinkText, d.LinkHref);
|
||||
_decisions[key] = d;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($" [Link-Memory] Fehler beim Laden der Entscheidungen: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Speichert eine User-Entscheidung für einen Link.
|
||||
/// </summary>
|
||||
public static void SaveDecision(string linkText, string linkHref, string decision, string? newUrl = null)
|
||||
{
|
||||
string key = NormalizeKey(linkText, linkHref);
|
||||
_decisions[key] = new LinkDecision
|
||||
{
|
||||
LinkText = linkText,
|
||||
LinkHref = linkHref,
|
||||
Decision = decision,
|
||||
NewUrl = newUrl
|
||||
};
|
||||
Save();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Versucht eine frühere Entscheidung für diesen Link zu finden.
|
||||
/// </summary>
|
||||
public static LinkDecision? GetPreviousDecision(string linkText, string linkHref)
|
||||
{
|
||||
string key = NormalizeKey(linkText, linkHref);
|
||||
if (_decisions.TryGetValue(key, out var d))
|
||||
return d;
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gibt die Anzahl gespeicherter Entscheidungen zurück (für Info-Anzeige).
|
||||
/// </summary>
|
||||
public static int Count => _decisions.Count;
|
||||
|
||||
public static void ShowInfo()
|
||||
{
|
||||
if (_decisions.Count > 0)
|
||||
{
|
||||
Console.WriteLine($"\nGespeicherte Link-Entscheidungen: {_decisions.Count}");
|
||||
foreach (var kvp in _decisions.Take(20))
|
||||
{
|
||||
var d = kvp.Value;
|
||||
string info = d.Decision switch
|
||||
{
|
||||
"delete" => "gelöscht",
|
||||
"edit" => $"ersetzt durch '{d.NewUrl}'",
|
||||
_ => "beibehalten"
|
||||
};
|
||||
Console.WriteLine($" \"{d.LinkText}\" -> {info}");
|
||||
}
|
||||
if (_decisions.Count > 20)
|
||||
Console.WriteLine($" ... und {_decisions.Count - 20} weitere");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -78,6 +78,9 @@ namespace wp2ghost
|
|||
return;
|
||||
}
|
||||
|
||||
ProcessedPosts.Clear();
|
||||
LoadProcessedPosts();
|
||||
|
||||
var pendingIds = allPostIds.Where(id => !ProcessedPosts.Contains(id)).ToList();
|
||||
Console.WriteLine($"Insgesamt gefunden: {allPostIds.Count}, Bereits abgefragt: {allPostIds.Count - pendingIds.Count}, Noch zu verarbeiten: {pendingIds.Count}");
|
||||
|
||||
|
|
@ -105,11 +108,15 @@ namespace wp2ghost
|
|||
Console.WriteLine($"[{++processed}/{pendingIds.Count}] -> Beitrag/Seite ID: {wpId}...");
|
||||
string detailUrl = $"{wpBaseUrl.TrimEnd('/')}/wp-json/custom/v1/post-details/{wpId}";
|
||||
|
||||
//string postUrl = $"{wpBaseUrl.TrimEnd('/')}/wp-json/wp/v2/posts/{wpId}?_embed";
|
||||
|
||||
try
|
||||
{
|
||||
var detailJson = await httpClient.GetStringAsync(detailUrl);
|
||||
var wpPost = JsonSerializer.Deserialize<WpPostResponse>(detailJson);
|
||||
|
||||
//var postJson = await httpClient.GetStringAsync(postUrl);
|
||||
|
||||
if (wpPost == null)
|
||||
{
|
||||
Console.WriteLine($" Übersprungen (null).");
|
||||
|
|
@ -217,7 +224,7 @@ namespace wp2ghost
|
|||
GhostPosts.Add(ghostPost);
|
||||
|
||||
// JSON-Datei in ghostPosts/ speichern
|
||||
var postFile = new GhostPostFile
|
||||
var ghostPostFile = new GhostPostFile
|
||||
{
|
||||
WpId = wpId,
|
||||
WpSlug = wpPost.Slug,
|
||||
|
|
@ -230,13 +237,17 @@ namespace wp2ghost
|
|||
PublishedAt = postDate.ToString("o"),
|
||||
FeatureImage = featureImageGhostPath,
|
||||
Categories = wpPost.Categories ?? new(),
|
||||
Tags = wpPost.Tags ?? new(),
|
||||
Keywords = wpPost.Keywords ?? "",
|
||||
Comments = wpPost.Comments ?? new(),
|
||||
ImageFiles = imageFiles,
|
||||
Html = htmlContent
|
||||
};
|
||||
string jsonPath = Path.Combine(GhostPostsDir, $"{postFile.WpId}.json");
|
||||
File.WriteAllText(jsonPath, JsonSerializer.Serialize(postFile, new JsonSerializerOptions { WriteIndented = true }));
|
||||
string jsonPath = Path.Combine(GhostPostsDir, $"{ghostPostFile.WpId}.json");
|
||||
File.WriteAllText(jsonPath, JsonSerializer.Serialize(ghostPostFile, new JsonSerializerOptions { WriteIndented = true }));
|
||||
|
||||
// Cache sofort speichern (nicht erst beim Beenden)
|
||||
ImageManager.SaveCache();
|
||||
|
||||
MarkProcessed(wpId, wpPost.Slug);
|
||||
internalId++;
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ namespace wp2ghost
|
|||
[JsonPropertyName("excerpt")] public string? Excerpt { get; set; }
|
||||
[JsonPropertyName("featured_image")] public string? FeaturedImage { get; set; }
|
||||
[JsonPropertyName("categories")] public List<WpCategory>? Categories { get; set; } = new();
|
||||
[JsonPropertyName("tags")] public List<WpCategory>? Tags { get; set; } = new();
|
||||
[JsonPropertyName("keywords")] public string? Keywords { get; set; }
|
||||
[JsonPropertyName("comments")] public List<WpComment>? Comments { get; set; } = new();
|
||||
[JsonPropertyName("media_files")] public List<string>? MediaFiles { get; set; } = new();
|
||||
|
|
|
|||
|
|
@ -175,7 +175,7 @@ function rbr_get_post_details_with_media($data) {
|
|||
// Duplikate entfernen
|
||||
$unique_media = array_values(array_unique($media_urls));
|
||||
|
||||
// 4. Kategorien ( Categories Only, keine Tags)
|
||||
// 4. Kategorien
|
||||
$categories = array();
|
||||
$cats = get_the_category($post_id);
|
||||
if (!empty($cats) && !is_wp_error($cats)) {
|
||||
|
|
@ -188,23 +188,44 @@ function rbr_get_post_details_with_media($data) {
|
|||
}
|
||||
}
|
||||
|
||||
// 5. Keywords (aus Yoast SEO oder Rank Math oder Custom Field)
|
||||
// 5. Tags (post_tag Taxonomy)
|
||||
$tags = array();
|
||||
$post_tags = get_the_tags($post_id);
|
||||
if (!empty($post_tags) && !is_wp_error($post_tags)) {
|
||||
foreach ($post_tags as $tag) {
|
||||
$tags[] = array(
|
||||
'id' => intval($tag->term_id),
|
||||
'name' => $tag->name,
|
||||
'slug' => $tag->slug,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 6. Keywords (aus Yoast SEO oder Rank Math oder Custom Field)
|
||||
$keywords = '';
|
||||
// Yoast SEO
|
||||
if (function_exists('yoast_get_primary_term')) {
|
||||
$keywords = get_post_meta($post_id, '_yoast_wpseo_focuskw', true);
|
||||
// Yoast SEO: Prüfe auf Meta-Key direkt (funktioniert in allen Versionen)
|
||||
$yoast_kw = get_post_meta($post_id, '_yoast_wpseo_focuskw', true);
|
||||
if (!empty($yoast_kw)) {
|
||||
$keywords = $yoast_kw;
|
||||
}
|
||||
// Rank Math
|
||||
if (empty($keywords) && function_exists('rank_math_get_single_post_data')) {
|
||||
$rank_data = rank_math_get_single_post_data($post_id);
|
||||
if (!empty($rank_data->get_keywords())) {
|
||||
$keywords = $rank_data->get_keywords();
|
||||
if (method_exists($rank_data, 'get_keywords')) {
|
||||
$kw = $rank_data->get_keywords();
|
||||
if (!empty($kw)) {
|
||||
$keywords = $kw;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Fallback: Custom Field 'post_keywords'
|
||||
if (empty($keywords)) {
|
||||
$keywords = get_post_meta($post_id, 'post_keywords', true);
|
||||
}
|
||||
// Fallback: Tags als Keywords
|
||||
if (empty($keywords) && !empty($tags)) {
|
||||
$keywords = implode(', ', array_column($tags, 'name'));
|
||||
}
|
||||
// Fallback: Kategorien als Keywords
|
||||
if (empty($keywords) && !empty($categories)) {
|
||||
$keywords = implode(', ', array_column($categories, 'name'));
|
||||
|
|
@ -241,6 +262,7 @@ function rbr_get_post_details_with_media($data) {
|
|||
),
|
||||
'featured_image' => $featured_image_url,
|
||||
'categories' => $categories,
|
||||
'tags' => $tags,
|
||||
'keywords' => $keywords,
|
||||
'comments' => $comments,
|
||||
'media_files' => $unique_media,
|
||||
|
|
|
|||
Loading…
Reference in a new issue