Skip to content

Commit 527924e

Browse files
NeWbY100claude
andcommitted
feat: SRR Creator auto-include, SRS/SRR creation, Compare enhancements
SRR Creator: - Auto-include release files (.nfo, .sfv, proof images, .m3u, .cue, .log) - Auto-create SRS files for samples in Sample/ subdirectory - Create nested SRR files for subtitle archives in Subs/ directories - Detect fix/patch releases and store main RAR as proof - Generate languages.diz from VobSub .idx files - Compute OSO hashes for archived files - Add ReleaseFileScanner helper with pyrescene-compatible filtering - Stored file DataGrid with editable "Stored As" column - Remove All button for stored files - App name includes version and git hash for both SRR and SRS creators - Normalize stored file paths to forward slashes - Match pyrescene file ordering (NFO first, main SFV last) Compare view: - Add SRS file comparison support with property highlighting - Add OSO hash tree nodes with properties and hex view selection - Add stored file and RAR volume ByteRange for hex view selection - Highlight parent tree nodes when children have differences - Mark sections unique to one side as different - Normalize path separators in stored file comparison - Add Close buttons to release file handles - Move Swap button to center between panels - Align file path bar columns with content splitter Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 6582e6d commit 527924e

11 files changed

Lines changed: 933 additions & 103 deletions
Lines changed: 352 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,352 @@
1+
using System.Text.RegularExpressions;
2+
3+
namespace ReScene.NET.Helpers;
4+
5+
/// <summary>
6+
/// Scans a scene release directory for files that should be stored in the SRR.
7+
/// Implements filtering rules based on pyrescene conventions.
8+
/// </summary>
9+
internal static partial class ReleaseFileScanner
10+
{
11+
// ── Stored file extensions ──────────────────────────────
12+
13+
private static readonly HashSet<string> StoredExtensions = new(StringComparer.OrdinalIgnoreCase)
14+
{
15+
".nfo", ".sfv", ".m3u", ".cue", ".log", ".srs"
16+
};
17+
18+
private static readonly HashSet<string> ImageExtensions = new(StringComparer.OrdinalIgnoreCase)
19+
{
20+
".jpg", ".jpeg", ".png", ".bmp", ".gif"
21+
};
22+
23+
private static readonly HashSet<string> SampleExtensions = new(StringComparer.OrdinalIgnoreCase)
24+
{
25+
".avi", ".mkv", ".mp4", ".wmv", ".m4v",
26+
".flac", ".mp3",
27+
".vob", ".m2ts", ".ts", ".mpg", ".mpeg", ".evo"
28+
};
29+
30+
private static readonly HashSet<string> MusicExtensions = new(StringComparer.OrdinalIgnoreCase)
31+
{
32+
".mp3", ".flac", ".mp2"
33+
};
34+
35+
// ── Blacklisted filenames ───────────────────────────────
36+
37+
private static readonly HashSet<string> NfoBlacklist = new(StringComparer.OrdinalIgnoreCase)
38+
{
39+
"imdb.nfo", "tvmaze.nfo", "movie.nfo", "scc.nfo", "motechnetfiles.nfo", "no.nfo"
40+
};
41+
42+
private static readonly HashSet<string> LogBlacklist = new(StringComparer.OrdinalIgnoreCase)
43+
{
44+
"rushchk.log", ".upchk.log", "ufxpcrc.log"
45+
};
46+
47+
// ── Subdirectory names ──────────────────────────────────
48+
49+
private static readonly HashSet<string> ProofDirs = new(StringComparer.OrdinalIgnoreCase)
50+
{
51+
"proof", "proofs", "sample", "cover", "covers", "screenshots", "compare"
52+
};
53+
54+
private static readonly HashSet<string> SubtitleDirs = new(StringComparer.OrdinalIgnoreCase)
55+
{
56+
"subs", "vobsubs", "vobsub", "subtitles", "sub", "subpack",
57+
"vobsubs-full", "vobsubs-light", "czsubs"
58+
};
59+
60+
private static readonly HashSet<string> DiscDirs = new(StringComparer.OrdinalIgnoreCase)
61+
{
62+
"codec", "codecs"
63+
};
64+
65+
// ── Fix release detection ───────────────────────────────
66+
67+
[GeneratedRegex(@"(SFV|PPF|sync|proof?|dir|nfo|Interleaving|Trackorder).?(Fix|Patch)", RegexOptions.IgnoreCase)]
68+
private static partial Regex FixPattern1();
69+
70+
[GeneratedRegex(@"\.(FiX|FIX)(\.|-)", RegexOptions.None)]
71+
private static partial Regex FixPattern2();
72+
73+
[GeneratedRegex(@"\.DVDR\.(REPACK\.)?Fix-", RegexOptions.IgnoreCase)]
74+
private static partial Regex FixPattern3();
75+
76+
[GeneratedRegex(@"^CD\d+$", RegexOptions.IgnoreCase)]
77+
private static partial Regex CdDirPattern();
78+
79+
// ── Public methods ──────────────────────────────────────
80+
81+
/// <summary>
82+
/// Scans the release directory for files that should be stored in the SRR.
83+
/// Returns (FullPath, StoredName) tuples sorted appropriately.
84+
/// </summary>
85+
public static List<(string FullPath, string StoredName)> ScanReleaseDirectory(string releaseDir)
86+
{
87+
var files = new List<(string FullPath, string StoredName)>();
88+
bool isMusicRelease = IsMusicRelease(releaseDir);
89+
90+
// Scan the release root
91+
ScanDirectory(releaseDir, releaseDir, files);
92+
93+
// Scan known subdirectories
94+
foreach (string subDir in Directory.GetDirectories(releaseDir))
95+
{
96+
string dirName = Path.GetFileName(subDir);
97+
98+
if (ProofDirs.Contains(dirName) || SubtitleDirs.Contains(dirName) ||
99+
DiscDirs.Contains(dirName) || CdDirPattern().IsMatch(dirName))
100+
{
101+
ScanDirectory(subDir, releaseDir, files);
102+
}
103+
}
104+
105+
return SortStoredFiles(files, isMusicRelease);
106+
}
107+
108+
/// <summary>
109+
/// Finds sample media files in the release directory (Sample/ subdir or files with "sample" in name).
110+
/// </summary>
111+
public static List<string> FindSampleFiles(string releaseDir)
112+
{
113+
var samples = new List<string>();
114+
115+
// Check Sample/ subdirectory
116+
string sampleDir = Path.Combine(releaseDir, "Sample");
117+
if (Directory.Exists(sampleDir))
118+
{
119+
foreach (string file in Directory.GetFiles(sampleDir))
120+
{
121+
if (SampleExtensions.Contains(Path.GetExtension(file)))
122+
samples.Add(file);
123+
}
124+
}
125+
126+
// Check root for files with "sample" in the name
127+
foreach (string file in Directory.GetFiles(releaseDir))
128+
{
129+
string name = Path.GetFileNameWithoutExtension(file);
130+
if (name.Contains("sample", StringComparison.OrdinalIgnoreCase) &&
131+
SampleExtensions.Contains(Path.GetExtension(file)) &&
132+
!samples.Contains(file))
133+
{
134+
samples.Add(file);
135+
}
136+
}
137+
138+
return samples;
139+
}
140+
141+
/// <summary>
142+
/// Finds subtitle SFV files in subtitle subdirectories.
143+
/// </summary>
144+
public static List<string> FindSubtitleSfvFiles(string releaseDir)
145+
{
146+
var sfvFiles = new List<string>();
147+
148+
foreach (string subDir in Directory.GetDirectories(releaseDir))
149+
{
150+
string dirName = Path.GetFileName(subDir);
151+
if (SubtitleDirs.Contains(dirName))
152+
{
153+
foreach (string file in Directory.GetFiles(subDir, "*.sfv"))
154+
sfvFiles.Add(file);
155+
}
156+
}
157+
158+
return sfvFiles;
159+
}
160+
161+
/// <summary>
162+
/// Detects if the release is a fix/patch release by its name.
163+
/// </summary>
164+
public static bool IsFixRelease(string releaseName)
165+
{
166+
return FixPattern1().IsMatch(releaseName)
167+
|| FixPattern2().IsMatch(releaseName)
168+
|| FixPattern3().IsMatch(releaseName);
169+
}
170+
171+
/// <summary>
172+
/// Checks if the release directory contains music files.
173+
/// </summary>
174+
public static bool IsMusicRelease(string releaseDir)
175+
{
176+
foreach (string file in Directory.GetFiles(releaseDir))
177+
{
178+
if (MusicExtensions.Contains(Path.GetExtension(file)))
179+
return true;
180+
}
181+
182+
// Check CD subdirectories
183+
foreach (string subDir in Directory.GetDirectories(releaseDir))
184+
{
185+
if (CdDirPattern().IsMatch(Path.GetFileName(subDir)))
186+
{
187+
foreach (string file in Directory.GetFiles(subDir))
188+
{
189+
if (MusicExtensions.Contains(Path.GetExtension(file)))
190+
return true;
191+
}
192+
}
193+
}
194+
195+
return false;
196+
}
197+
198+
/// <summary>
199+
/// Finds all RAR files referenced by an SFV (lines with .rNN or .rar extensions).
200+
/// </summary>
201+
public static List<string> FindRarFilesFromSfv(string sfvPath)
202+
{
203+
string dir = Path.GetDirectoryName(sfvPath) ?? ".";
204+
var rarFiles = new List<string>();
205+
206+
foreach (string line in File.ReadLines(sfvPath))
207+
{
208+
if (string.IsNullOrWhiteSpace(line) || line.StartsWith(';'))
209+
continue;
210+
211+
int lastSpace = line.LastIndexOf(' ');
212+
if (lastSpace <= 0)
213+
continue;
214+
215+
string fileName = line[..lastSpace].Trim();
216+
string ext = Path.GetExtension(fileName);
217+
218+
if (ext.Equals(".rar", StringComparison.OrdinalIgnoreCase) ||
219+
(ext.Length == 4 && ext.StartsWith(".r", StringComparison.OrdinalIgnoreCase) &&
220+
char.IsDigit(ext[2]) && char.IsDigit(ext[3])))
221+
{
222+
string fullPath = Path.Combine(dir, fileName);
223+
if (File.Exists(fullPath))
224+
rarFiles.Add(fullPath);
225+
}
226+
}
227+
228+
return rarFiles;
229+
}
230+
231+
// ── Private helpers ─────────────────────────────────────
232+
233+
private static void ScanDirectory(string dir, string releaseDir, List<(string FullPath, string StoredName)> files)
234+
{
235+
bool isSubDir = !string.Equals(dir, releaseDir, StringComparison.OrdinalIgnoreCase);
236+
string dirName = Path.GetFileName(dir);
237+
bool isProofDir = isSubDir && ProofDirs.Contains(dirName);
238+
239+
foreach (string file in Directory.GetFiles(dir))
240+
{
241+
string ext = Path.GetExtension(file);
242+
string fileName = Path.GetFileName(file);
243+
244+
if (StoredExtensions.Contains(ext))
245+
{
246+
if (!ShouldIncludeFile(file))
247+
continue;
248+
249+
AddFile(file, releaseDir, files);
250+
}
251+
else if (ImageExtensions.Contains(ext))
252+
{
253+
if (!ShouldIncludeImage(fileName, isProofDir, isSubDir))
254+
continue;
255+
256+
AddFile(file, releaseDir, files);
257+
}
258+
}
259+
}
260+
261+
private static bool ShouldIncludeFile(string fullPath)
262+
{
263+
string fileName = Path.GetFileName(fullPath);
264+
string ext = Path.GetExtension(fileName);
265+
266+
if (ext.Equals(".nfo", StringComparison.OrdinalIgnoreCase))
267+
return !NfoBlacklist.Contains(fileName);
268+
269+
if (ext.Equals(".log", StringComparison.OrdinalIgnoreCase))
270+
{
271+
if (LogBlacklist.Contains(fileName))
272+
return false;
273+
if (fileName.StartsWith('.'))
274+
return false;
275+
}
276+
277+
return true;
278+
}
279+
280+
private static bool ShouldIncludeImage(string fileName, bool isProofDir, bool isSubDir)
281+
{
282+
// Only include images from proof-related subdirectories or with proof-like names
283+
if (!isProofDir && isSubDir)
284+
return false;
285+
286+
// In root dir, only include images with proof-like names
287+
if (!isSubDir)
288+
{
289+
string lower = fileName.ToLowerInvariant();
290+
if (!lower.Contains("proof"))
291+
return false;
292+
}
293+
294+
return !IsUnwantedImage(fileName);
295+
}
296+
297+
private static bool IsUnwantedImage(string fileName)
298+
{
299+
string lower = fileName.ToLowerInvariant();
300+
301+
// Windows Media Player album art
302+
if (lower.Contains("albumartsmall"))
303+
return true;
304+
if (lower.StartsWith("albumart_{", StringComparison.Ordinal))
305+
return true;
306+
307+
// "Folder.jpg" type
308+
string baseName = Path.GetFileNameWithoutExtension(lower);
309+
if (baseName == "folder")
310+
return true;
311+
312+
return false;
313+
}
314+
315+
private static void AddFile(string fullPath, string releaseDir, List<(string FullPath, string StoredName)> files)
316+
{
317+
// Avoid duplicates
318+
if (files.Any(f => f.FullPath.Equals(fullPath, StringComparison.OrdinalIgnoreCase)))
319+
return;
320+
321+
string storedName = Path.GetRelativePath(releaseDir, fullPath).Replace('\\', '/');
322+
files.Add((fullPath, storedName));
323+
}
324+
325+
private static List<(string FullPath, string StoredName)> SortStoredFiles(
326+
List<(string FullPath, string StoredName)> files, bool isMusicRelease)
327+
{
328+
// pyrescene order: NFO, m3u, proof images, log, cue, SRS, vobsub SRR,
329+
// subtitle SFVs (in subdirs), main SFV (root) last
330+
return files.OrderBy(f =>
331+
{
332+
string ext = Path.GetExtension(f.FullPath).ToLowerInvariant();
333+
bool isInSubDir = f.StoredName.Contains(Path.DirectorySeparatorChar)
334+
|| f.StoredName.Contains('/');
335+
336+
return ext switch
337+
{
338+
".nfo" => 0,
339+
".m3u" => 1,
340+
_ when ImageExtensions.Contains(ext) => 2,
341+
".log" when isMusicRelease => 3,
342+
".log" => 4,
343+
".cue" => 5,
344+
".srs" => 6,
345+
".srr" => 7,
346+
".sfv" when isInSubDir => 8,
347+
".sfv" => 9,
348+
_ => 4
349+
};
350+
}).ThenBy(f => f.StoredName, StringComparer.OrdinalIgnoreCase).ToList();
351+
}
352+
}

ReScene.NET/Services/ISrrCreationService.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ Task<SrrCreationResult> CreateFromRarAsync(
1616
Task<SrrCreationResult> CreateFromSfvAsync(
1717
string outputPath,
1818
string sfvFilePath,
19-
IReadOnlyList<string>? additionalFiles,
19+
IReadOnlyDictionary<string, string>? additionalFiles,
2020
SrrCreationOptions options,
2121
CancellationToken ct);
2222
}

ReScene.NET/Services/SrrCreationService.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ public Task<SrrCreationResult> CreateFromRarAsync(
2525
public Task<SrrCreationResult> CreateFromSfvAsync(
2626
string outputPath,
2727
string sfvFilePath,
28-
IReadOnlyList<string>? additionalFiles,
28+
IReadOnlyDictionary<string, string>? additionalFiles,
2929
SrrCreationOptions options,
3030
CancellationToken ct)
3131
{

0 commit comments

Comments
 (0)