154 lines
6.4 KiB
C#
154 lines
6.4 KiB
C#
using System.Text.Json;
|
|
using wp2ghost;
|
|
|
|
namespace Wp2GhostUI;
|
|
|
|
public enum GhostState { Unknown, Missing, Present, WrongDate }
|
|
|
|
/// <summary>Eine Zeile der Post-Liste: das Wichtigste aus einer JSON-Datei in ghostPosts/.</summary>
|
|
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<string> 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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Liest die abgerufenen Posts aus dem Datenordner (ghostPosts/ und ghostPosts/archiv/).
|
|
/// Ob ein Post in Ghost liegt, gilt für die aktive Ghost-Instanz.
|
|
/// </summary>
|
|
public class PostCatalog(ImporterSession session)
|
|
{
|
|
// Gelesene Dateien merken, solange sie sich nicht ändern (die JSONs enthalten den ganzen Beitrag)
|
|
private readonly Dictionary<string, (DateTime Stamp, PostRow Row)> _cache = new();
|
|
// Ergebnis des letzten Abgleichs mit der aktiven Ghost-Instanz: Slug → Veröffentlichungsdatum
|
|
private Dictionary<string, string?>? _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<PostRow> Load()
|
|
{
|
|
var rows = new List<PostRow>();
|
|
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;
|
|
}
|
|
|
|
/// <summary>Nach dem Wechsel der Ghost-Instanz gilt der letzte Abgleich nicht mehr.</summary>
|
|
public void ResetGhostState()
|
|
{
|
|
_ghostPosts = null;
|
|
GhostCheckedAt = null;
|
|
}
|
|
|
|
private static PostRow? Read(string file, bool archived)
|
|
{
|
|
try
|
|
{
|
|
var post = JsonSerializer.Deserialize<GhostPostFile>(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;
|
|
}
|
|
}
|
|
|
|
/// <summary>Holt die Liste der Posts aus Ghost, um in der Liste anzuzeigen, was schon dort ist.</summary>
|
|
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<PostRow> 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<PostRow> 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;
|
|
}
|
|
}
|