using System.Text.Json;
using wp2ghost;
namespace Wp2GhostUI;
public enum GhostState { Unknown, Missing, Present, WrongDate }
/// Eine Zeile der Post-Liste: das Wichtigste aus einer JSON-Datei in ghostPosts/.
public class PostRow
{
public int WpId { get; init; }
public string Title { get; init; } = "";
public string GhostSlug { get; init; } = "";
public DateTime? Date { get; init; }
public string? DateUtc { get; init; }
public List Categories { get; init; } = new();
public int Images { get; init; }
public int Videos { get; init; }
public int Comments { get; init; }
public bool Archived { get; init; }
public string Path { get; init; } = "";
public GhostState Ghost { get; set; } = GhostState.Unknown;
}
///
/// Liest die abgerufenen Posts aus dem Datenordner (ghostPosts/ und ghostPosts/archiv/).
/// Ob ein Post in Ghost liegt, gilt für die aktive Ghost-Instanz.
///
public class PostCatalog(ImporterSession session)
{
// Gelesene Dateien merken, solange sie sich nicht ändern (die JSONs enthalten den ganzen Beitrag)
private readonly Dictionary _cache = new();
// Ergebnis des letzten Abgleichs mit der aktiven Ghost-Instanz: Slug → Veröffentlichungsdatum
private Dictionary? _ghostPosts;
public DateTime? GhostCheckedAt { get; private set; }
private string PostsDir => System.IO.Path.Combine(session.DataDir, "ghostPosts");
private string ArchiveDir => System.IO.Path.Combine(PostsDir, "archiv");
public List Load()
{
var rows = new List();
foreach (var (dir, archived) in new[] { (PostsDir, false), (ArchiveDir, true) })
{
if (!Directory.Exists(dir)) continue;
foreach (string file in Directory.GetFiles(dir, "*.json"))
{
var stamp = File.GetLastWriteTimeUtc(file);
if (!_cache.TryGetValue(file, out var cached) || cached.Stamp != stamp)
{
var row = Read(file, archived);
if (row == null) continue;
_cache[file] = cached = (stamp, row);
}
rows.Add(cached.Row);
}
}
// Ein Post, der nach dem Archivieren neu abgerufen wurde, liegt in beiden Ordnern: die aktive Datei gilt
rows = rows.GroupBy(r => r.WpId).Select(g => g.OrderBy(r => r.Archived).First()).OrderBy(r => r.WpId).ToList();
foreach (var row in rows) row.Ghost = StateOf(row);
return rows;
}
/// Nach dem Wechsel der Ghost-Instanz gilt der letzte Abgleich nicht mehr.
public void ResetGhostState()
{
_ghostPosts = null;
GhostCheckedAt = null;
}
private static PostRow? Read(string file, bool archived)
{
try
{
var post = JsonSerializer.Deserialize(File.ReadAllText(file));
if (post == null || post.WpId <= 0) return null;
string? utc = DateHelper.ToGhostUtc(post.PublishedAt ?? post.CreatedAt);
return new PostRow
{
WpId = post.WpId,
Title = System.Net.WebUtility.HtmlDecode(post.Title ?? "(kein Titel)"),
GhostSlug = SlugHelper.Clean(post.Slug, $"post-{post.WpId}"),
DateUtc = utc,
Date = utc == null ? null : DateTimeOffset.Parse(utc).LocalDateTime,
Categories = post.Categories.Where(c => !CategoryRules.IsIgnored(c))
.Select(c => System.Net.WebUtility.HtmlDecode(c.Name ?? "")).Where(n => n.Length > 0).ToList(),
Images = post.ImageFiles.Count,
Videos = post.MediaFiles.Count,
Comments = CommentBlockBuilder.CountRealComments(post.Comments),
Archived = archived,
Path = file
};
}
catch
{
return null;
}
}
/// Holt die Liste der Posts aus Ghost, um in der Liste anzuzeigen, was schon dort ist.
public async Task CompareWithGhostAsync()
{
Console.Write(" Lade Posts aus Ghost...");
var ghostPosts = await GhostApi.BrowseAllPostsAsync();
Console.WriteLine($" {ghostPosts.Count}");
_ghostPosts = ghostPosts.Where(p => p.Slug != null)
.GroupBy(p => p.Slug!, StringComparer.OrdinalIgnoreCase)
.ToDictionary(g => g.Key, g => g.First().PublishedAt, StringComparer.OrdinalIgnoreCase);
GhostCheckedAt = DateTime.Now;
var rows = Load();
Console.WriteLine($" In Ghost vorhanden: {rows.Count(r => r.Ghost is GhostState.Present or GhostState.WrongDate)} von {rows.Count} abgerufenen Posts" +
$" (davon {rows.Count(r => r.Ghost == GhostState.WrongDate)} mit abweichendem Datum).");
}
private GhostState StateOf(PostRow row)
{
if (_ghostPosts == null) return GhostState.Unknown;
// Eine Zuordnung aus der Prüffunktion hat Vorrang vor dem bereinigten Slug
string slug = SlugAssignments.GetGhostSlug(row.WpId) ?? row.GhostSlug;
if (!_ghostPosts.TryGetValue(slug, out string? publishedAt)) return GhostState.Missing;
if (row.DateUtc == null || publishedAt == null) return GhostState.Present;
bool sameDate = DateTimeOffset.TryParse(publishedAt, out var actual)
&& Math.Abs((actual - DateTimeOffset.Parse(row.DateUtc)).TotalSeconds) < 60;
return sameDate ? GhostState.Present : GhostState.WrongDate;
}
public int Archive(IEnumerable rows)
{
Directory.CreateDirectory(ArchiveDir);
int moved = 0;
foreach (var row in rows.Where(r => !r.Archived && File.Exists(r.Path)))
{
File.Move(row.Path, System.IO.Path.Combine(ArchiveDir, System.IO.Path.GetFileName(row.Path)), overwrite: true);
moved++;
}
return moved;
}
public int Restore(IEnumerable rows)
{
int moved = 0;
foreach (var row in rows.Where(r => r.Archived && File.Exists(r.Path)))
{
string target = System.IO.Path.Combine(PostsDir, System.IO.Path.GetFileName(row.Path));
if (File.Exists(target)) continue; // es gibt bereits eine neuere aktive Datei
File.Move(row.Path, target);
moved++;
}
return moved;
}
}