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; } }