Skip to content

Commit 4c64381

Browse files
anobakaclaude
andcommitted
feat: improve file grouping, path-mark sync, file renaming and DLsite
File grouping (FileSystemEntryGroupingService + GroupModal): - Add exact mode (closes #1074) - Add structure mode (closes #1075) - Add tier control for similarity mode (closes #1076) - Improve grouping preview UI (closes #1077) Path-mark sync: - Add resync-all-path-marks action (closes #1078) - Fix media-library association lost when syncing all marks on a path (fixes #1079) Misc: - Support batch file/folder selection in FileNameModifier (closes #1080) - Fix DLsite enhancer returning no data: add category detection, follow 30x redirects, bypass age gate (fixes #1081) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 2eab3e2 commit 4c64381

34 files changed

Lines changed: 2209 additions & 439 deletions

File tree

src/apps/Bakabase.Service/Controllers/FileController.cs

Lines changed: 19 additions & 103 deletions
Original file line numberDiff line numberDiff line change
@@ -72,13 +72,15 @@ public class FileController : Controller
7272
private readonly ISystemPlayer _systemPlayer;
7373
private readonly IFileManager _fileManager;
7474
private readonly AppService _appService;
75+
private readonly Bakabase.Service.Services.FileSystemEntryGroupingService _groupingService;
7576

7677
public FileController(ISpecialTextService specialTextService, IWebHostEnvironment env,
7778
CompressedFileService compressedFileService, IBOptionsManager<FileSystemOptions> fsOptionsManager,
7879
IwFsWatcher fileProcessorWatcher, PasswordService passwordService, ILogger<FileController> logger,
7980
BakabaseLocalizer localizer, BTaskManager taskManager, IGuiAdapter guiAdapter,
8081
FfMpegService ffMpegService, HardwareAccelerationService hardwareAccelerationService,
81-
ISystemPlayer systemPlayer, IFileManager fileManager, AppService appService)
82+
ISystemPlayer systemPlayer, IFileManager fileManager, AppService appService,
83+
Bakabase.Service.Services.FileSystemEntryGroupingService groupingService)
8284
{
8385
_specialTextService = specialTextService;
8486
_env = env;
@@ -95,6 +97,7 @@ public FileController(ISpecialTextService specialTextService, IWebHostEnvironmen
9597
_systemPlayer = systemPlayer;
9698
_fileManager = fileManager;
9799
_appService = appService;
100+
_groupingService = groupingService;
98101
}
99102

100103
private static bool IsHiddenEntry(string path)
@@ -1790,96 +1793,26 @@ public async Task<ListResponse<FileSystemEntryGroupResultViewModel>> GroupPrevie
17901793
return ListResponseBuilder<FileSystemEntryGroupResultViewModel>.NotFound;
17911794
}
17921795

1793-
var batches = new Dictionary<string, List<string>>();
1794-
1795-
if (model.GroupInternal)
1796-
{
1797-
foreach (var rootPath in model.Paths)
1798-
{
1799-
if (System.IO.File.Exists(rootPath))
1800-
{
1801-
continue;
1802-
}
1803-
1804-
if (!Directory.Exists(rootPath))
1805-
{
1806-
return ListResponseBuilder<FileSystemEntryGroupResultViewModel>.NotFound;
1807-
}
1808-
1809-
batches[rootPath] = Directory.GetFiles(rootPath).ToList();
1810-
}
1811-
}
1812-
else
1813-
{
1814-
foreach (var pg in model.Paths.GroupBy(Path.GetDirectoryName))
1815-
{
1816-
if (pg.Key.IsNotEmpty())
1817-
{
1818-
foreach (var path in pg)
1819-
{
1820-
if (System.IO.File.Exists(path) || Directory.Exists(path))
1821-
{
1822-
batches.GetOrAdd(pg.Key, _ => []).Add(path);
1823-
}
1824-
}
1825-
}
1826-
}
1827-
}
1828-
1829-
if (!batches.Any())
1796+
var results = _groupingService.Preview(model);
1797+
if (results.Count == 0)
18301798
{
18311799
return ListResponseBuilder<FileSystemEntryGroupResultViewModel>.NotFound;
18321800
}
18331801

1834-
var vms = batches.Select(g =>
1835-
{
1836-
var rootPath = g.Key.StandardizePath()!;
1837-
var vm = new FileSystemEntryGroupResultViewModel
1838-
{
1839-
RootPath = rootPath
1840-
};
1841-
if (model.SimilarityThreshold == 1.0m)
1842-
{
1843-
vm.Groups = g.Value.GroupBy(Path.GetFileNameWithoutExtension)
1844-
.Where(x => x.Key.IsNotEmpty()).Select(x =>
1845-
new FileSystemEntryGroupResultViewModel.GroupViewModel
1846-
{
1847-
DirectoryName = x.Key.StandardizePath()!,
1848-
Filenames = x.Select(y => Path.GetFileName(y)!).ToArray()
1849-
}).ToArray();
1850-
}
1851-
else
1852-
{
1853-
var groups = new Dictionary<string, List<string>>();
1854-
foreach (var path in g.Value)
1855-
{
1856-
var key = Path.GetFileNameWithoutExtension(path);
1857-
if (key.IsNotEmpty())
1858-
{
1859-
var similarKey =
1860-
groups.Keys.FirstOrDefault(x => x.IsSimilarTo(key, model.SimilarityThreshold));
1861-
if (similarKey.IsNotEmpty())
1862-
{
1863-
groups[similarKey].Add(path);
1864-
}
1865-
else
1866-
{
1867-
groups[key] = [path];
1868-
}
1869-
}
1870-
}
1871-
1872-
vm.Groups = groups.Select(x => new FileSystemEntryGroupResultViewModel.GroupViewModel
1873-
{
1874-
DirectoryName = x.Value.Select(x => Path.GetFileNameWithoutExtension(x)!).OrderByDescending(y => y.Length).First().StandardizePath()!,
1875-
Filenames = x.Value.Select(y => Path.GetFileName(y)!).ToArray()
1876-
}).ToArray();
1877-
}
1802+
return new ListResponse<FileSystemEntryGroupResultViewModel>(results);
1803+
}
18781804

1879-
return vm;
1880-
});
1805+
[HttpPut("group-similarity-breakpoints")]
1806+
[SwaggerOperation(OperationId = "GetFileSystemEntriesGroupSimilarityBreakpoints")]
1807+
public async Task<ListResponse<decimal>> GroupSimilarityBreakpoints(
1808+
[FromBody] FileSystemEntryGroupInputModel model)
1809+
{
1810+
if (model.Paths.Length == 0)
1811+
{
1812+
return new ListResponse<decimal>(new[] { 0m, 1m });
1813+
}
18811814

1882-
return new ListResponse<FileSystemEntryGroupResultViewModel>(vms);
1815+
return new ListResponse<decimal>(_groupingService.ComputeSimilarityBreakpoints(model));
18831816
}
18841817

18851818
[HttpPut("group")]
@@ -1892,24 +1825,7 @@ public async Task<BaseResponse> Group([FromBody] FileSystemEntryGroupInputModel
18921825
return r;
18931826
}
18941827

1895-
var batches = r.Data!;
1896-
1897-
foreach (var batch in batches)
1898-
{
1899-
foreach (var group in batch.Groups)
1900-
{
1901-
var dirFullname = Path.Combine(batch.RootPath, group.DirectoryName);
1902-
Directory.CreateDirectory(dirFullname);
1903-
foreach (var f in group.Filenames)
1904-
{
1905-
var sourceFile = Path.Combine(batch.RootPath, f);
1906-
var fileFullname = Path.Combine(dirFullname, f);
1907-
// Use quickly way in same drive
1908-
System.IO.File.Move(sourceFile, fileFullname, false);
1909-
}
1910-
}
1911-
}
1912-
1828+
_groupingService.Execute(model, r.Data!.ToList());
19131829
return BaseResponseBuilder.Ok;
19141830
}
19151831

src/apps/Bakabase.Service/Controllers/PathMarkController.cs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,30 @@ public async Task<BaseResponse> StartSyncByPath([FromQuery] string path)
250250
return BaseResponseBuilder.Ok;
251251
}
252252

253+
/// <summary>
254+
/// Force a full re-sync of every existing path mark — re-marks them as Pending
255+
/// (except those already PendingDelete or actively Syncing) and triggers a sync.
256+
/// Use this to recover from any state where the index of effects has drifted.
257+
/// </summary>
258+
[HttpPost("sync/force-all")]
259+
[SwaggerOperation(OperationId = "ForceResyncAllPathMarks")]
260+
public async Task<BaseResponse> ForceResyncAll()
261+
{
262+
var allMarks = await service.GetAll(m => !m.IsDeleted);
263+
var markIds = allMarks
264+
.Where(m => m.SyncStatus != PathMarkSyncStatus.Syncing)
265+
.Select(m => m.Id)
266+
.ToArray();
267+
268+
if (markIds.Length == 0)
269+
{
270+
return BaseResponseBuilder.Ok;
271+
}
272+
273+
await syncService.EnqueueSync(markIds);
274+
return BaseResponseBuilder.Ok;
275+
}
276+
253277
/// <summary>
254278
/// Start resource synchronization for a specific source (e.g., Steam, DLsite, ExHentai).
255279
/// This triggers resource discovery from the source, and if new resources are found,

src/apps/Bakabase.Service/Extensions/BakabaseBusinessExtensions.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ public static IServiceCollection AddInsideWorldBusinesses(this IServiceCollectio
5555
services.AddScoped<PasswordService>();
5656

5757
services.TryAddSingleton<IwFsWatcher>();
58+
services.AddSingleton<Bakabase.Service.Services.FileSystemEntryGroupingService>();
5859

5960
#region Optimized after V190
6061

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,36 @@
1-
using System.ComponentModel.DataAnnotations;
1+
using System.ComponentModel.DataAnnotations;
22

33
namespace Bakabase.Service.Models.Input;
44

5+
public enum FileSystemEntryGroupStrategyType
6+
{
7+
Similarity = 0,
8+
KeyExtraction = 1,
9+
Affix = 2,
10+
}
11+
12+
public enum FileSystemEntryGroupAffixDirection
13+
{
14+
Prefix = 0,
15+
Suffix = 1,
16+
Both = 2,
17+
}
18+
519
public record FileSystemEntryGroupInputModel
620
{
721
public string[] Paths { get; set; } = [];
822
public bool GroupInternal { get; set; }
23+
public FileSystemEntryGroupStrategyType StrategyType { get; set; } =
24+
FileSystemEntryGroupStrategyType.Similarity;
25+
926
[Range(0, 1)]
1027
public decimal SimilarityThreshold { get; set; } = 1.0m;
11-
}
28+
29+
public string? KeyExtractionRegex { get; set; }
30+
31+
public FileSystemEntryGroupAffixDirection AffixDirection { get; set; } =
32+
FileSystemEntryGroupAffixDirection.Prefix;
33+
34+
[Range(1, 1000)]
35+
public int AffixMinLength { get; set; } = 3;
36+
}
Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,29 @@
1-
using System.Collections.Generic;
2-
31
namespace Bakabase.Service.Models.View;
42

53
public record FileSystemEntryGroupResultViewModel
64
{
75
public string RootPath { get; set; } = null!;
86
public GroupViewModel[] Groups { get; set; } = [];
7+
public EntryViewModel[] UntouchedEntries { get; set; } = [];
98

109
public record GroupViewModel
1110
{
1211
public string DirectoryName { get; set; } = null!;
13-
public string[] Filenames { get; set; } = [];
12+
public EntryViewModel[] Entries { get; set; } = [];
13+
public string? ExistingFolderTarget { get; set; }
14+
public string? RenamedSourceName { get; set; }
15+
}
16+
17+
public record EntryViewModel
18+
{
19+
public string Name { get; set; } = null!;
20+
public bool IsDirectory { get; set; }
21+
public MatchSpan[] MatchSpans { get; set; } = [];
22+
}
23+
24+
public record MatchSpan
25+
{
26+
public int Start { get; set; }
27+
public int Length { get; set; }
1428
}
15-
}
29+
}

0 commit comments

Comments
 (0)