Skip to content

Commit 8c1e4a4

Browse files
NeWbY100claude
andcommitted
feat(ui): SRS scan-progress modals, Main File option, Compare-tab Cluster matching, reconstructor warnings
SRS Reconstructor: - Modal stays open through "Rebuilding" and "Verifying CRC" instead of closing the moment the find-tracks phase ends. Heading transitions to "Rebuilding Sample" while CollectMediaFrameData walks the media file, then "Verifying CRC" while the output is hashed; closes on "Complete". - Live scan progress (percent, MB scanned, throughput, ETA) is rendered during the frame-data collection and the attachment-extraction passes via the library's new ScanProgress event on SRSRebuilder. SRS Creator: - New "Main file" input row (Browse + Clear). When set, the SRS creation pipeline runs the library's main-file verification step and the resulting SRS carries each track's MatchOffset — equivalent to pyrescene's -c flag, used by scene-tool when packaging samples. - Progress modal now appears during the long Profiling Sample step (previously a silent 6+ seconds on large samples). Transitions to "Verifying Against Main File" while the verifier scans, then "Writing SRS" before closing. - ISrsCreationService / SRSCreationService forward the writer's ScanProgress event into the viewmodel. Compare tab: - FindMatchingNodeRecursive special-cases SRSContainerChunks: the "Container Structure" parent and its many chunk children all share NodeType=SRSContainerChunks with FileName=null, so the previous generic match returned whichever SRSContainerChunks node it saw first (usually the parent, whose Data is a List<SRSContainerChunk> rather than a single chunk) — collapsing to an empty properties panel. Match parent-to-parent and chunk-to-chunk by Label instead. RAR Reconstructor: - After loading an SRR, warn via MessageBox when it contains no RAR reconstruction info (zero RAR file entries, zero archived files, and no detected compression method). The import still completes — empty imported state clears any stale data from a previous session — but the user is told to configure options manually instead of being left confused. - Subscribe to BruteForceService.TimestampPreservationFailed and accumulate failures during a run. When the operation completes, pop a single summary MessageBox listing up to 10 affected paths and the error message per failure, explaining that the produced RAR's File Time (DOS) may not match the original. Per-file warnings still go through the system log in real time. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 1ed1c0d commit 8c1e4a4

9 files changed

Lines changed: 266 additions & 5 deletions

ReScene.NET/Services/BruteForceService.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ public class BruteForceService : IBruteForceService
1212
public event EventHandler<LogEventArgs>? LogMessage;
1313
public event EventHandler<FileCopyProgressEventArgs>? FileCopyProgress;
1414
public event EventHandler<CRCValidationProgressEventArgs>? CRCValidationProgress;
15+
public event EventHandler<TimestampPreservationFailedEventArgs>? TimestampPreservationFailed;
1516

1617
private Manager? _manager;
1718

@@ -25,6 +26,7 @@ public async Task<bool> RunAsync(BruteForceOptions options)
2526
_manager.BruteForceStatusChanged += (s, e) => StatusChanged?.Invoke(s, e);
2627
_manager.FileCopyProgress += (s, e) => FileCopyProgress?.Invoke(s, e);
2728
_manager.CRCValidationProgress += (s, e) => CRCValidationProgress?.Invoke(s, e);
29+
_manager.TimestampPreservationFailed += (s, e) => TimestampPreservationFailed?.Invoke(s, e);
2830

2931
return await _manager.BruteForceRARVersionAsync(options);
3032
}

ReScene.NET/Services/IBruteForceService.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,5 @@ public interface IBruteForceService
1111
public event EventHandler<LogEventArgs>? LogMessage;
1212
public event EventHandler<FileCopyProgressEventArgs>? FileCopyProgress;
1313
public event EventHandler<CRCValidationProgressEventArgs>? CRCValidationProgress;
14+
public event EventHandler<TimestampPreservationFailedEventArgs>? TimestampPreservationFailed;
1415
}

ReScene.NET/Services/ISrsCreationService.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,11 @@ public interface ISrsCreationService
1212
/// </summary>
1313
public event EventHandler<SRSCreationProgressEventArgs>? Progress;
1414

15+
/// <summary>
16+
/// Raised to report byte-level scan progress during sample profiling.
17+
/// </summary>
18+
public event EventHandler<SRSScanProgressEventArgs>? ScanProgress;
19+
1520
/// <summary>
1621
/// Creates an SRS file from a sample media file.
1722
/// </summary>

ReScene.NET/Services/SRSCreationService.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,13 @@ public event EventHandler<SRSCreationProgressEventArgs>? Progress
1616
remove => _writer.Progress -= value;
1717
}
1818

19+
/// <inheritdoc />
20+
public event EventHandler<SRSScanProgressEventArgs>? ScanProgress
21+
{
22+
add => _writer.ScanProgress += value;
23+
remove => _writer.ScanProgress -= value;
24+
}
25+
1926
/// <inheritdoc />
2027
public Task<SRSCreationResult> CreateAsync(
2128
string outputPath,

ReScene.NET/ViewModels/FileCompareViewModel.cs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1347,6 +1347,27 @@ private void SyncTreeSelection(ObservableCollection<TreeNodeViewModel> targetRoo
13471347
}
13481348
}
13491349
}
1350+
else if (nodeData.NodeType == CompareNodeType.SRSContainerChunks
1351+
&& sourceData.NodeType == CompareNodeType.SRSContainerChunks)
1352+
{
1353+
// SRSContainerChunks NodeType is shared by both the "Container
1354+
// Structure" parent (Data = List<SRSContainerChunk>) and every
1355+
// chunk child (Data = SRSContainerChunk). Match parent-to-parent
1356+
// and chunk-to-chunk by Label so a clicked Cluster lights up the
1357+
// corresponding Cluster on the other side rather than the first
1358+
// SRSContainerChunks node encountered.
1359+
if (nodeData.Data is List<SRSContainerChunk> && sourceData.Data is List<SRSContainerChunk>)
1360+
{
1361+
return node;
1362+
}
1363+
1364+
if (nodeData.Data is SRSContainerChunk nodeChunk
1365+
&& sourceData.Data is SRSContainerChunk sourceChunk
1366+
&& nodeChunk.Label == sourceChunk.Label)
1367+
{
1368+
return node;
1369+
}
1370+
}
13501371
else if (nodeData.NodeType == sourceData.NodeType && nodeData.FileName == sourceData.FileName)
13511372
{
13521373
return node;

ReScene.NET/ViewModels/ReconstructorViewModel.cs

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,12 @@ public partial class ReconstructorViewModel : ViewModelBase
5858
private CustomPackerType _importedCustomPackerType = CustomPackerType.None;
5959
private string? _importedSRRFilePath;
6060

61+
// Timestamp-preservation failures accumulated during the current run.
62+
// Surfaced as a single MessageBox when the operation completes so the
63+
// user is aware that the resulting RAR's File Time (DOS) may not match
64+
// the original for those files.
65+
private readonly List<TimestampPreservationFailedEventArgs> _timestampFailures = [];
66+
6167
public ReconstructorViewModel(IBruteForceService bruteForceService, IFileDialogService fileDialog)
6268
{
6369
_bruteForceService = bruteForceService;
@@ -68,6 +74,7 @@ public ReconstructorViewModel(IBruteForceService bruteForceService, IFileDialogS
6874
_bruteForceService.LogMessage += OnLogMessage;
6975
_bruteForceService.FileCopyProgress += OnFileCopyProgress;
7076
_bruteForceService.CRCValidationProgress += OnCRCValidationProgress;
77+
_bruteForceService.TimestampPreservationFailed += OnTimestampPreservationFailed;
7178

7279
_elapsedTimer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(1) };
7380
_elapsedTimer.Tick += (_, _) => OnElapsedTimerTick();
@@ -369,6 +376,27 @@ private async Task ImportSRRAsync()
369376
var srr = SRRFile.Load(path);
370377
Log(LogTarget.System, "SRR loaded successfully");
371378

379+
// Detect SRRs that carry no RAR reconstruction information
380+
// (no RAR volume entries, no archived-file metadata, no detected
381+
// compression method). These can't drive automatic option setup,
382+
// so warn the user that they'll need to configure things manually.
383+
bool hasRarReconstructionInfo = srr.RARFiles.Count > 0
384+
|| srr.ArchivedFiles.Count > 0
385+
|| srr.CompressionMethod.HasValue;
386+
387+
if (!hasRarReconstructionInfo)
388+
{
389+
Log(LogTarget.System,
390+
"WARNING: SRR contains no RAR reconstruction information.");
391+
MessageBox.Show(
392+
"This SRR file does not contain RAR reconstruction information " +
393+
"(no RAR volume entries, archived files, or compression metadata).\n\n" +
394+
"You will need to configure the RAR options manually before reconstructing.",
395+
"No RAR Reconstruction Info",
396+
MessageBoxButton.OK,
397+
MessageBoxImage.Information);
398+
}
399+
372400
// Custom packer detection
373401
if (srr.HasCustomPackerHeaders)
374402
{
@@ -1124,6 +1152,7 @@ private async Task StartAsync()
11241152
SystemLog = string.Empty;
11251153
Phase1Log = string.Empty;
11261154
Phase2Log = string.Empty;
1155+
_timestampFailures.Clear();
11271156

11281157
// Reset progress window state
11291158
TestCountText = string.Empty;
@@ -1894,10 +1923,57 @@ private void OnStatusChanged(object? _, BruteForceStatusChangedEventArgs e)
18941923
OperationCompletionStatus.Cancelled => "Cancelled.",
18951924
_ => "Completed."
18961925
};
1926+
1927+
ShowTimestampFailureWarningIfAny();
18971928
}
18981929
});
18991930
}
19001931

1932+
private void OnTimestampPreservationFailed(object? _, TimestampPreservationFailedEventArgs e)
1933+
{
1934+
// The library already logs a Warning via its logger (routed through
1935+
// OnLogMessage). Track the failure here so we can show a single
1936+
// summary MessageBox when the run finishes.
1937+
_timestampFailures.Add(e);
1938+
}
1939+
1940+
private void ShowTimestampFailureWarningIfAny()
1941+
{
1942+
if (_timestampFailures.Count == 0)
1943+
{
1944+
return;
1945+
}
1946+
1947+
const int MaxFilesToList = 10;
1948+
var sb = new System.Text.StringBuilder();
1949+
sb.AppendLine("Could not copy the source file's modification time onto the working copy " +
1950+
"for the following file(s):");
1951+
sb.AppendLine();
1952+
1953+
int shown = Math.Min(_timestampFailures.Count, MaxFilesToList);
1954+
for (int i = 0; i < shown; i++)
1955+
{
1956+
TimestampPreservationFailedEventArgs f = _timestampFailures[i];
1957+
sb.AppendLine($" • {f.DestinationPath}");
1958+
sb.AppendLine($" ({f.ErrorMessage})");
1959+
}
1960+
1961+
if (_timestampFailures.Count > MaxFilesToList)
1962+
{
1963+
sb.AppendLine($" … and {_timestampFailures.Count - MaxFilesToList} more.");
1964+
}
1965+
1966+
sb.AppendLine();
1967+
sb.AppendLine("WinRAR will pack these files with the copy time instead of the original " +
1968+
"modification time, so the resulting RAR's File Time (DOS) may differ " +
1969+
"from the original release.");
1970+
1971+
MessageBox.Show(sb.ToString(),
1972+
"Timestamp Preservation Failed",
1973+
MessageBoxButton.OK,
1974+
MessageBoxImage.Warning);
1975+
}
1976+
19011977
private void OnLogMessage(object? _, LogEventArgs e) => Application.Current.Dispatcher.Invoke(() => AppendLog(e.Target, e.Message));
19021978

19031979
private void Log(LogTarget target, string message) => AppendLog(target, message);

ReScene.NET/ViewModels/SRSCreatorViewModel.cs

Lines changed: 111 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System.Collections.ObjectModel;
2+
using System.Diagnostics;
23
using System.Windows;
34
using CommunityToolkit.Mvvm.ComponentModel;
45
using CommunityToolkit.Mvvm.Input;
@@ -26,6 +27,7 @@ public SRSCreatorViewModel(ISrsCreationService srsService, IFileDialogService fi
2627
_settingsService = settingsService;
2728

2829
_sRSService.Progress += OnProgress;
30+
_sRSService.ScanProgress += OnScanProgress;
2931

3032
AppSettings settings = _settingsService.Load();
3133

@@ -108,11 +110,18 @@ public SRSCreatorViewModel(ISrsCreationService srsService, IFileDialogService fi
108110
[ObservableProperty]
109111
private bool _iSOProcessing;
110112

113+
private Stopwatch? _scanStopwatch;
114+
private bool _scanModalActive;
115+
111116
// Output
112117
[ObservableProperty]
113118
[NotifyCanExecuteChangedFor(nameof(CreateSRSCommand))]
114119
private string _outputPath = string.Empty;
115120

121+
// Optional main file for match-offset verification (mirrors pyrescene -c)
122+
[ObservableProperty]
123+
private string _mainFilePath = string.Empty;
124+
116125
// Options
117126
[ObservableProperty]
118127
private string _appName = string.Empty;
@@ -178,6 +187,21 @@ private async Task BrowseInputAsync()
178187
}
179188
}
180189

190+
[RelayCommand]
191+
private async Task BrowseMainFileAsync()
192+
{
193+
string? path = await _fileDialog.OpenFileAsync("Select Main File (Full Movie)",
194+
FileDialogFilters.MediaFiles);
195+
196+
if (path is not null)
197+
{
198+
MainFilePath = path;
199+
}
200+
}
201+
202+
[RelayCommand]
203+
private void ClearMainFile() => MainFilePath = string.Empty;
204+
181205
[RelayCommand]
182206
private async Task BrowseOutputAsync()
183207
{
@@ -219,7 +243,8 @@ private async Task CreateSRSAsync()
219243
{
220244
var options = new SRSCreationOptions
221245
{
222-
AppName = string.IsNullOrWhiteSpace(AppName) ? FormatUtilities.GetDefaultAppName() : AppName
246+
AppName = string.IsNullOrWhiteSpace(AppName) ? FormatUtilities.GetDefaultAppName() : AppName,
247+
MainFilePath = string.IsNullOrWhiteSpace(MainFilePath) ? null : MainFilePath
223248
};
224249

225250
Log("Starting SRS creation...");
@@ -274,9 +299,30 @@ await ISOMediaExtractor.ExtractFileAsync(
274299
Log($"Input: {samplePath}");
275300
Log($"Output: {OutputPath}");
276301

302+
// Show progress modal during profiling (can take many seconds on large samples)
303+
ISOProgressHeading = "Profiling Sample";
304+
ISOCurrentFileText = Path.GetFileName(samplePath);
305+
ISOFileCountText = "Reading sample structure and computing CRC...";
306+
ISOOverallPercent = 0;
307+
ISOCurrentPercent = 0;
308+
ISOCurrentSizeText = string.Empty;
309+
ISOProcessedText = string.Empty;
310+
ISORemainingText = string.Empty;
311+
ISOSpeedText = string.Empty;
312+
ISOEtaText = string.Empty;
313+
_scanStopwatch = Stopwatch.StartNew();
314+
_scanModalActive = true;
315+
ISOProcessing = true;
316+
317+
// Yield to let the dispatcher open the modal before heavy work starts
318+
await Task.Yield();
319+
277320
SRSCreationResult result = await _sRSService.CreateAsync(
278321
OutputPath, samplePath, options, _cts.Token);
279322

323+
_scanModalActive = false;
324+
ISOProcessing = false;
325+
280326
if (result.Success)
281327
{
282328
ProgressPercent = 100;
@@ -311,6 +357,7 @@ await ISOMediaExtractor.ExtractFileAsync(
311357
}
312358
finally
313359
{
360+
_scanModalActive = false;
314361
ISOProcessing = false;
315362
IsCreating = false;
316363
_cts?.Dispose();
@@ -359,9 +406,72 @@ private void OnProgress(object? _, SRSCreationProgressEventArgs e)
359406
{
360407
ProgressMessage = e.Message;
361408
Log(e.Message);
409+
410+
if (!_scanModalActive)
411+
{
412+
return;
413+
}
414+
415+
// Transition the modal as we move from profiling -> verifying -> writing -> complete
416+
if (e.Message.StartsWith("Verifying sample against main file", StringComparison.OrdinalIgnoreCase))
417+
{
418+
ISOProgressHeading = "Verifying Against Main File";
419+
ISOCurrentFileText = "Searching for track signatures in main file...";
420+
ISOOverallPercent = 0;
421+
ISOCurrentPercent = 0;
422+
_scanStopwatch?.Restart();
423+
}
424+
else if (e.Message.StartsWith("Writing SRS", StringComparison.OrdinalIgnoreCase))
425+
{
426+
ISOProgressHeading = "Writing SRS";
427+
ISOCurrentFileText = "Writing SRS file...";
428+
ISOOverallPercent = 100;
429+
ISOCurrentPercent = 100;
430+
}
431+
});
432+
}
433+
434+
private void OnScanProgress(object? _, SRSScanProgressEventArgs e)
435+
{
436+
Application.Current.Dispatcher.BeginInvoke(() =>
437+
{
438+
if (!_scanModalActive)
439+
{
440+
return;
441+
}
442+
443+
ISOOverallPercent = e.Percent;
444+
ISOCurrentPercent = e.Percent;
445+
ISOCurrentFileText = e.Phase;
446+
UpdateScanStats(e.BytesScanned, e.BytesTotal);
362447
});
363448
}
364449

450+
private void UpdateScanStats(long processed, long total)
451+
{
452+
if (total <= 0 || _scanStopwatch is null)
453+
{
454+
return;
455+
}
456+
457+
double elapsed = _scanStopwatch.Elapsed.TotalSeconds;
458+
ISOProcessedText = $"{FormatUtilities.FormatSize(processed)} / {FormatUtilities.FormatSize(total)}";
459+
460+
long remaining = total - processed;
461+
ISORemainingText = FormatUtilities.FormatSize(remaining);
462+
463+
if (elapsed > 0.5 && processed > 0)
464+
{
465+
double bytesPerSec = processed / elapsed;
466+
ISOSpeedText = $"{FormatUtilities.FormatSize((long)bytesPerSec)}/s";
467+
468+
double secondsRemaining = remaining / bytesPerSec;
469+
ISOEtaText = secondsRemaining < 60
470+
? $"{secondsRemaining:F0}s"
471+
: $"{(int)(secondsRemaining / 60)}m {(int)(secondsRemaining % 60)}s";
472+
}
473+
}
474+
365475
private void Log(string message) => AppendLogEntry(LogEntries, message);
366476

367477
#region ISO Support

0 commit comments

Comments
 (0)