TL.db syncs between machines whose absolute paths differ, so a single TTFolderPath column cannot be correct on both at once -- the stored paths are only trustworthy on the machine that wrote them. That made --updatepaths a mandatory prelude to every --output rather than the one-time setup step it looks like. --output now refreshes the column from the TumblThree Index metadata before exporting. The root comes from the first non-flag argument, else appSettings:PathTTRoot. With no root available it says so and exports whatever TL.db already holds; a root whose Index folder is missing is a hard stop, since silently exporting stale paths is the failure this change exists to prevent. --norefresh skips the refresh for a pure export. The scan is extracted from UpdateBlogPathsRunner.Run into a reusable Scan() that returns counts instead of only printing them, so --updatepaths keeps its per-file detail while --output prints a single summary line rather than a few hundred lines ahead of the export. Verified against a throwaway database: a stale cross-machine path is repaired and the export lands in the correct local folder; no configured root warns and continues (exit 0); a missing Index folder stops (exit 1); --norefresh skips the refresh and exports (exit 0). Co-Authored-By: Claude Opus 5 <[email protected]>
207 lines
10 KiB
C#
207 lines
10 KiB
C#
using Microsoft.Extensions.Configuration;
|
|
|
|
namespace URLNotesGrabberCORE
|
|
{
|
|
// Port of ThreeTxtFileHelper RunOutputMode + WritePostToFile + RenameExistingTxtFilesToBak.
|
|
// For each Blog with a TTFolderPath, renames any existing .txt files in that folder to .bak,
|
|
// then writes one .txt per PostType containing all posts of that type (date-sorted, fixed
|
|
// field order). Reads from TL.db via DataAccess.GetAllPostsForBlog.
|
|
public static class OutputMode
|
|
{
|
|
public static int Run(IConfiguration config, string[]? args = null)
|
|
{
|
|
DataAccess.EnsureTTFileHelperColumnsExist();
|
|
|
|
string dbPath = DataAccess.GetActiveDbPath();
|
|
Console.WriteLine($"Database: {Path.GetFullPath(dbPath)}");
|
|
|
|
if (!RefreshPaths(config, args ?? Array.Empty<string>()))
|
|
return 1;
|
|
|
|
var blogs = DataAccess.GetAllBlogsWithTTFolderPath();
|
|
int activeBlogs = DataAccess.CountActiveBlogs();
|
|
Console.WriteLine($"{blogs.Count} of {activeBlogs} active blog(s) have a TTFolderPath.");
|
|
|
|
if (blogs.Count == 0)
|
|
{
|
|
Console.WriteLine($"\nNothing to export: no blog in {Path.GetFullPath(dbPath)} has a TTFolderPath.");
|
|
Console.WriteLine("Point --output at a TumblThree root so it can populate them: --output <root>,");
|
|
Console.WriteLine("or set appSettings:PathTTRoot so the refresh runs automatically.");
|
|
return 1;
|
|
}
|
|
|
|
int missingFolderCount = 0;
|
|
int writtenCount = 0;
|
|
|
|
foreach (var (blogName, folder) in blogs)
|
|
{
|
|
Console.WriteLine($"\nProcessing blog: {blogName}");
|
|
|
|
// A stored path that this machine cannot see means the value was written on
|
|
// another machine -- re-running --updatepaths locally is the fix, so say so
|
|
// rather than lumping it in with "not set".
|
|
if (!Directory.Exists(folder))
|
|
{
|
|
Console.WriteLine($" TTFolderPath folder not found: {folder}. Skipping.");
|
|
missingFolderCount++;
|
|
continue;
|
|
}
|
|
|
|
Console.WriteLine($" TTFolderPath: {folder}");
|
|
writtenCount++;
|
|
|
|
try
|
|
{
|
|
foreach (var bakFile in Directory.GetFiles(folder, "*.bak"))
|
|
File.Delete(bakFile);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Console.WriteLine($" Error deleting .bak files: {ex.Message}");
|
|
}
|
|
|
|
RenameExistingTxtFilesToBak(folder);
|
|
|
|
var posts = DataAccess.GetAllPostsForBlog(blogName);
|
|
Console.WriteLine($" Found {posts.Count} post(s) for this blog.");
|
|
|
|
var grouped = posts.GroupBy(p => p.PostType ?? "Unknown");
|
|
foreach (var typeGroup in grouped)
|
|
{
|
|
string postType = typeGroup.Key ?? "Unknown";
|
|
string outputFilePath = Path.Combine(folder, $"{postType}.txt");
|
|
var ordered = typeGroup.OrderBy(p => p.Date).ToList();
|
|
Console.WriteLine($" Writing {ordered.Count} post(s) to {postType}.txt");
|
|
|
|
using var writer = new StreamWriter(outputFilePath, false, System.Text.Encoding.UTF8);
|
|
bool isFirst = true;
|
|
foreach (var post in ordered)
|
|
{
|
|
if (!isFirst)
|
|
{
|
|
writer.WriteLine();
|
|
writer.WriteLine();
|
|
}
|
|
WritePostToFile(writer, post);
|
|
isFirst = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
Console.WriteLine($"\nOutput mode complete. {writtenCount} blog(s) exported, {missingFolderCount} skipped for a missing folder.");
|
|
|
|
if (writtenCount == 0)
|
|
Console.WriteLine("Every TTFolderPath points at a folder this machine cannot see. The paths were most likely written on another machine -- re-run --updatepaths <root> here so they match local drive letters.");
|
|
|
|
return 0;
|
|
}
|
|
|
|
// Re-reads the TumblThree Index metadata into Blogs.TTFolderPath before exporting.
|
|
// A TL.db synced between machines cannot hold one absolute path that is valid on
|
|
// both, so the stored paths are only trustworthy on the machine that wrote them --
|
|
// which makes this refresh part of a normal export rather than a separate chore.
|
|
// Returns false only when the run should stop.
|
|
private static bool RefreshPaths(IConfiguration config, string[] args)
|
|
{
|
|
var settings = config.GetSection("appSettings");
|
|
|
|
if (args.Any(a => string.Equals(a, "--norefresh", StringComparison.OrdinalIgnoreCase)))
|
|
{
|
|
Console.WriteLine("Path refresh skipped (--norefresh); exporting to whatever paths TL.db already holds.");
|
|
return true;
|
|
}
|
|
|
|
string? root = args.FirstOrDefault(a => !a.StartsWith("--", StringComparison.Ordinal))
|
|
?? settings.GetValue<string>("PathTTRoot");
|
|
|
|
var result = UpdateBlogPathsRunner.Scan(root, verbose: false);
|
|
|
|
switch (result.Outcome)
|
|
{
|
|
case UpdateBlogPathsRunner.ScanOutcome.NoRootConfigured:
|
|
Console.WriteLine("No TumblThree root configured (appSettings:PathTTRoot is empty and none was passed),");
|
|
Console.WriteLine("so TTFolderPath was not refreshed. Pass one as --output <root> to refresh it.");
|
|
return true;
|
|
|
|
case UpdateBlogPathsRunner.ScanOutcome.IndexFolderMissing:
|
|
// Silently exporting stale paths here would defeat the point of folding
|
|
// the refresh in, so a bad root is a hard stop.
|
|
Console.WriteLine($"Index folder not found at: {result.IndexPath}");
|
|
Console.WriteLine("Fix the root (or pass --norefresh to export the paths already in TL.db).");
|
|
return false;
|
|
|
|
default:
|
|
Console.WriteLine($"Refreshed paths from {result.IndexPath}: " +
|
|
$"{result.MetadataFiles} metadata file(s), {result.Written} written, " +
|
|
$"{result.Unchanged} already correct, {result.NoLocation} without a location, " +
|
|
$"{result.NoMatchingRow} without a blog row, {result.Errors} error(s).");
|
|
return true;
|
|
}
|
|
}
|
|
|
|
private static void RenameExistingTxtFilesToBak(string folderPath)
|
|
{
|
|
try
|
|
{
|
|
foreach (var txtFile in Directory.GetFiles(folderPath, "*.txt"))
|
|
{
|
|
string bakPath = Path.ChangeExtension(txtFile, ".bak");
|
|
if (File.Exists(bakPath)) File.Delete(bakPath);
|
|
File.Move(txtFile, bakPath, overwrite: true);
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Console.WriteLine($" Error renaming txt files to .bak: {ex.Message}");
|
|
}
|
|
}
|
|
|
|
private static void WritePostToFile(StreamWriter writer, TTPostRecord post)
|
|
{
|
|
var startColumns = new[] { "Post ID", "Date", "Post URL", "Slug", "Reblog Key", "Reblog URL", "Reblog Name", "Title", "Body" };
|
|
var endColumns = new[] { "Tags", "Downloaded Files" };
|
|
|
|
var columns = new Dictionary<string, string>();
|
|
if (!string.IsNullOrWhiteSpace(post.PostId)) columns["Post ID"] = post.PostId;
|
|
if (!string.IsNullOrWhiteSpace(post.Date)) columns["Date"] = post.Date!;
|
|
if (!string.IsNullOrWhiteSpace(post.PostUrl)) columns["Post URL"] = post.PostUrl!;
|
|
if (!string.IsNullOrWhiteSpace(post.Slug)) columns["Slug"] = post.Slug!;
|
|
if (!string.IsNullOrWhiteSpace(post.ReblogKey)) columns["Reblog Key"] = post.ReblogKey!;
|
|
if (!string.IsNullOrWhiteSpace(post.ReblogUrl)) columns["Reblog URL"] = post.ReblogUrl!;
|
|
if (!string.IsNullOrWhiteSpace(post.ReblogName)) columns["Reblog Name"] = post.ReblogName!;
|
|
if (!string.IsNullOrWhiteSpace(post.Title)) columns["Title"] = post.Title!;
|
|
if (!string.IsNullOrWhiteSpace(post.Body)) columns["Body"] = post.Body!;
|
|
if (!string.IsNullOrWhiteSpace(post.HasImage)) columns["Has Image"] = post.HasImage!;
|
|
if (!string.IsNullOrWhiteSpace(post.Summary)) columns["Summary"] = post.Summary!;
|
|
if (!string.IsNullOrWhiteSpace(post.Quote)) columns["Quote"] = post.Quote!;
|
|
if (!string.IsNullOrWhiteSpace(post.Link)) columns["Link"] = post.Link!;
|
|
if (!string.IsNullOrWhiteSpace(post.PhotoUrl)) columns["Photo URL"] = post.PhotoUrl!;
|
|
if (!string.IsNullOrWhiteSpace(post.PhotoCaption)) columns["Photo Caption"] = post.PhotoCaption!;
|
|
if (!string.IsNullOrWhiteSpace(post.AudioCaption)) columns["Audio Caption"] = post.AudioCaption!;
|
|
if (!string.IsNullOrWhiteSpace(post.Question)) columns["Question"] = post.Question!;
|
|
if (!string.IsNullOrWhiteSpace(post.Answer)) columns["Answer"] = post.Answer!;
|
|
if (!string.IsNullOrWhiteSpace(post.Tags)) columns["Tags"] = post.Tags!;
|
|
if (!string.IsNullOrWhiteSpace(post.DownloadedFiles)) columns["Downloaded Files"] = post.DownloadedFiles!;
|
|
|
|
foreach (var col in startColumns)
|
|
{
|
|
if (columns.ContainsKey(col))
|
|
{
|
|
writer.WriteLine($"{col}: {columns[col]}");
|
|
columns.Remove(col);
|
|
}
|
|
}
|
|
|
|
var remaining = columns.Keys.Where(k => !endColumns.Contains(k)).OrderBy(k => k).ToList();
|
|
foreach (var col in remaining)
|
|
writer.WriteLine($"{col}: {columns[col]}");
|
|
|
|
foreach (var col in endColumns)
|
|
{
|
|
if (columns.ContainsKey(col))
|
|
writer.WriteLine($"{col}: {columns[col]}");
|
|
}
|
|
}
|
|
}
|
|
}
|