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())) 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 ,"); 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 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("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 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(); 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]}"); } } } }