using Microsoft.Extensions.Configuration; namespace URLNotesGrabberCORE { // Port of ThreeTxtFileHelper RunCorrectionMode (dry-run) and RunFullCorrectionMode (apply). // Scans a BAK directory of .txt files, parses posts with multi-line field support, // and either reports or applies content-column corrections to TL.db.Posts. // Apply path only writes non-empty values (mirrors original ThreeTxtFileHelper semantics) // and never touches engagement columns. public static class CorrectMode { public static int Run(IConfiguration config, string[] args, bool applyChanges) { DataAccess.EnsureTTFileHelperColumnsExist(); // Resolve BAK path: explicit arg > appSettings:PathTTBackup > derive from PathTTRoot/PathInput string? bakRootPath = args.Length > 0 ? args[0] : config["appSettings:PathTTBackup"]; if (string.IsNullOrWhiteSpace(bakRootPath)) { string? root = config["appSettings:PathTTRoot"]; if (string.IsNullOrWhiteSpace(root)) root = config["appSettings:PathInput"]; if (!string.IsNullOrWhiteSpace(root)) bakRootPath = root.TrimEnd('\\', '/') + "_BAK\\"; } if (string.IsNullOrWhiteSpace(bakRootPath) || !Directory.Exists(bakRootPath)) { Console.WriteLine($"BAK directory not found: {bakRootPath}"); return 1; } Console.WriteLine($"BAK source path: {bakRootPath}"); Console.WriteLine(applyChanges ? "Correction mode: APPLY - non-empty fields from BAK overwrite DB columns\n" : "Correction mode: Dry-run - reports multi-line field updates available\n"); string prefixesPath = config["appSettings:PathPrefixes"] ?? "prefixes.txt"; if (!Path.IsPathRooted(prefixesPath)) prefixesPath = Path.Combine(AppContext.BaseDirectory, prefixesPath); if (!File.Exists(prefixesPath)) { Console.WriteLine($"Prefixes file not found: {prefixesPath}"); return 1; } var allowedPrefixes = new HashSet(File.ReadLines(prefixesPath), StringComparer.OrdinalIgnoreCase); if (applyChanges) { Console.Write("WARNING: This will overwrite non-empty fields in matching posts from BAK files. Continue? (yes/no): "); string? response = Console.ReadLine(); if (string.IsNullOrWhiteSpace(response) || !response.Equals("yes", StringComparison.OrdinalIgnoreCase)) { Console.WriteLine("Operation cancelled."); return 0; } } var bakTxtFiles = new List(); try { foreach (var dir in Directory.GetDirectories(bakRootPath, "*", SearchOption.AllDirectories)) bakTxtFiles.AddRange(Directory.GetFiles(dir, "*.txt")); bakTxtFiles.AddRange(Directory.GetFiles(bakRootPath, "*.txt")); } catch (Exception ex) { Console.WriteLine($"Error scanning BAK directory: {ex.Message}"); return 1; } Console.WriteLine($"Found {bakTxtFiles.Count} file(s) in BAK directory\n"); int totalPostsFound = 0; int postsWithUpdates = 0; int postsUpdated = 0; int postsNotFound = 0; var correctionLog = new List(); var updateLog = new List(); foreach (string bakFile in bakTxtFiles) { Console.WriteLine($"Processing BAK file: {Path.GetFileName(bakFile)}"); try { var bakPosts = ParsePostsFromFile(bakFile, allowedPrefixes); Console.WriteLine($" Found {bakPosts.Count} post(s) in this file"); foreach (var (postId, bakData) in bakPosts) { totalPostsFound++; var dbPost = DataAccess.GetPostByIdAnyBlog(postId); if (dbPost == null) { postsNotFound++; continue; } if (applyChanges) { bool updated = DataAccess.UpdatePostContentFields(dbPost.BlogName, dbPost.PostId, bakData); if (updated) { postsUpdated++; updateLog.Add($"Post ID: {postId} - Updated from {Path.GetFileName(bakFile)}"); } } else { var updateList = BuildDryRunDiff(bakData, dbPost); if (updateList.Count > 0) { postsWithUpdates++; correctionLog.Add($"\nPost ID: {postId}"); correctionLog.Add($" File: {Path.GetFileName(bakFile)}"); correctionLog.Add($" Fields to update:"); correctionLog.AddRange(updateList); } } } } catch (Exception ex) { Console.WriteLine($" ERROR processing file: {ex.Message}"); } } if (applyChanges) { Console.WriteLine($"\n========== CORRECTION COMPLETE =========="); Console.WriteLine($"Total posts found in BAK files: {totalPostsFound}"); Console.WriteLine($"Posts updated in database: {postsUpdated}"); Console.WriteLine($"Posts not found in database: {postsNotFound}"); string logPath = config["appSettings:PathCorrectionApplied"] ?? "correction_applied.txt"; try { var logLines = new List { $"Correction Applied: {DateTime.Now:yyyy-MM-dd HH:mm:ss}", $"Total posts found in BAK files: {totalPostsFound}", $"Posts updated in database: {postsUpdated}", $"Posts not found in database: {postsNotFound}", "", "Updated Posts:" }; logLines.AddRange(updateLog); File.WriteAllLines(logPath, logLines); Console.WriteLine($"Update log saved to: {logPath}"); } catch (Exception ex) { Console.WriteLine($"Error writing log file: {ex.Message}"); } } else { Console.WriteLine($"\n========== CORRECTION REPORT (DRY RUN) =========="); Console.WriteLine($"Total posts found in BAK files: {totalPostsFound}"); Console.WriteLine($"Posts with multi-line field updates available: {postsWithUpdates}"); if (correctionLog.Count > 0) { string logPath = config["appSettings:PathCorrectionReport"] ?? "correction_report.txt"; try { File.WriteAllLines(logPath, correctionLog); Console.WriteLine($"\nDetailed report saved to: {logPath}"); } catch (Exception ex) { Console.WriteLine($"Error writing report file: {ex.Message}"); } } else { Console.WriteLine("\nNo multi-line field updates found."); } Console.WriteLine("\nDry-run complete. No database changes were made."); Console.WriteLine("If updates look correct, re-run with `-correct -apply` to apply changes."); } return 0; } private static List BuildDryRunDiff(Dictionary bakData, TTPostRecord dbPost) { var updates = new List(); foreach (var (fieldName, bakValue) in bakData) { if (string.IsNullOrWhiteSpace(bakValue)) continue; string? currentValue = fieldName.ToLowerInvariant() switch { "reblog url" => dbPost.ReblogUrl, "date" => dbPost.Date, "has image" => dbPost.HasImage, "post url" => dbPost.PostUrl, "slug" => dbPost.Slug, "reblog key" => dbPost.ReblogKey, "reblog name" => dbPost.ReblogName, "summary" => dbPost.Summary, "quote" => dbPost.Quote, "body" => dbPost.Body, "tags" => dbPost.Tags, "link" => dbPost.Link, "photo url" => dbPost.PhotoUrl, "photo caption" => dbPost.PhotoCaption, "downloaded files" => dbPost.DownloadedFiles, "audio caption" => dbPost.AudioCaption, "question" => dbPost.Question, "answer" => dbPost.Answer, "title" => dbPost.Title, _ => null }; if (bakValue != currentValue && bakValue.Contains('\n')) updates.Add($" {fieldName}: [MULTILINE]"); } return updates; } private static Dictionary> ParsePostsFromFile(string filePath, HashSet allowedPrefixes) { var posts = new Dictionary>(StringComparer.OrdinalIgnoreCase); var lines = File.ReadAllLines(filePath); int lineIndex = 0; string currentPostId = ""; var currentPostData = new Dictionary(StringComparer.OrdinalIgnoreCase); while (lineIndex < lines.Length) { string line = lines[lineIndex]; string searchText = line.Length > 25 ? line.Substring(0, 25) : line; int colonIndex = searchText.IndexOf(": "); if (colonIndex > 0) { string prefix = line.Substring(0, colonIndex).Trim(); if (!string.IsNullOrWhiteSpace(prefix) && allowedPrefixes.Contains(prefix)) { if (string.Equals(prefix, "Post ID", StringComparison.OrdinalIgnoreCase)) { if (!string.IsNullOrWhiteSpace(currentPostId) && currentPostData.Count > 0) posts[currentPostId] = new Dictionary(currentPostData, StringComparer.OrdinalIgnoreCase); currentPostId = line.Substring(colonIndex + 2).Trim(); currentPostData.Clear(); lineIndex++; continue; } var valueLines = new List { line.Substring(colonIndex + 2).Trim() }; int nextLineIndex = lineIndex + 1; while (nextLineIndex < lines.Length) { string nextLine = lines[nextLineIndex]; string nextSearch = nextLine.Length > 25 ? nextLine.Substring(0, 25) : nextLine; int nextColon = nextSearch.IndexOf(": "); if (nextColon > 0) { string nextPrefix = nextLine.Substring(0, nextColon).Trim(); if (!string.IsNullOrWhiteSpace(nextPrefix) && allowedPrefixes.Contains(nextPrefix)) break; } valueLines.Add(nextLine); nextLineIndex++; } currentPostData[prefix] = string.Join("\n", valueLines); lineIndex = nextLineIndex; continue; } } lineIndex++; } if (!string.IsNullOrWhiteSpace(currentPostId) && currentPostData.Count > 0) posts[currentPostId] = new Dictionary(currentPostData, StringComparer.OrdinalIgnoreCase); return posts; } } }