feat(fs): W4-E4 fsproxy hang isolation, partial sweep, replace-with-verify, TIMEOUT retry
- adapters/fs: FsProxy + fsworker run each file op in a killable subprocess;
progress-stall watchdog (FS_HANG_TIMEOUT_MS) for copy/hash, total timeout for
quick ops; SIGTERM -> SIGKILL -> FsError('TIMEOUT'); stat/checkRoot timeouts
never read as missing
- copyWithVerify: onExisting conflict/skip/replace (hardlink backup + atomic
rename, old file to .umt-trash or deleted), sweep stale partials before write,
document why permissions/owner are not copied
- filesMatch/filesMatchSha256 throw FsError on read failure instead of
match=false; crossDeviceMove skips redundant re-hash after sha256 copy
- FsErrorCode TIMEOUT + fsErrorClass; organize executeRecord routes fs ops
through fsproxy, retries TIMEOUT/IO up to FS_TRANSFER_MAX_ATTEMPTS then opens
a decision without deleting the record; reconcile errors no longer reported
as EEXIST
- domain: subtitle .default marker (ORGANIZE_DEFAULT_SUBTITLE_LANG) and
findOtherVersions (same season/episode/part, video only) as upgrade
disposal candidates
- scripts/bench/fs-bench.ts and benchmark record in porting doc
Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
36dc048e89
commit
2e2d56184c
21 files changed
+1970
-179
No files matched your search
@@ -1,5 +1,11 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { type OrganizeInput, planOrganize, sidecarLanguageSuffix, sidecarStem } from "../src/index.ts";
|
||||
import {
|
||||
findOtherVersions,
|
||||
type OrganizeInput,
|
||||
planOrganize,
|
||||
sidecarLanguageSuffix,
|
||||
sidecarStem,
|
||||
} from "../src/index.ts";
|
||||
|
||||
const MiB = 1024 ** 2;
|
||||
const GiB = 1024 ** 3;
|
||||
@@ -236,3 +242,55 @@ describe("附属文件主干名", () => {
|
||||
expect(sidecarLanguageSuffix([])).toBe("");
|
||||
});
|
||||
});
|
||||
|
||||
describe("W4-E4:字幕 .default 与 latest 旧版本判定口径(对照 MP)", () => {
|
||||
it("defaultLang 命中的随行字幕加 .default;源自带 .default 保留;不命中不加", () => {
|
||||
expect(sidecarLanguageSuffix(["chs"], "zh-cn")).toBe(".chi.zh-cn.default");
|
||||
expect(sidecarLanguageSuffix(["cht"], "zh-cn")).toBe(".zh-tw");
|
||||
expect(sidecarLanguageSuffix(["eng"], "en")).toBe(".eng.default");
|
||||
expect(sidecarLanguageSuffix(["eng", "default"])).toBe(".eng.default");
|
||||
expect(sidecarLanguageSuffix([], "zh-cn")).toBe("");
|
||||
});
|
||||
|
||||
it("planOrganize 透传 defaultSubtitleLang", () => {
|
||||
const plan = planOrganize({
|
||||
operation: "copy",
|
||||
mediaType: "movie",
|
||||
files: [
|
||||
{ path: "/dl/Movie.2020.1080p.BluRay.x264-GRP.mkv", size: 2e9 },
|
||||
{ path: "/dl/Movie.2020.1080p.BluRay.x264-GRP.chs.srt", size: 1e4 },
|
||||
{ path: "/dl/Movie.2020.1080p.BluRay.x264-GRP.eng.srt", size: 1e4 },
|
||||
],
|
||||
entity: { title: "Movie", year: 2020 },
|
||||
target: { movieDir: "/lib/Movie (2020)" },
|
||||
defaultSubtitleLang: "zh-cn",
|
||||
});
|
||||
const subs = plan.files.filter((f) => f.role === "sidecar").map((f) => f.dst.split("/").pop());
|
||||
expect(subs.some((n) => n?.endsWith(".chi.zh-cn.default.srt"))).toBe(true);
|
||||
expect(subs.some((n) => n?.endsWith(".eng.srt"))).toBe(true);
|
||||
});
|
||||
|
||||
it("findOtherVersions:同季同集同 Part、只看视频扩展名", () => {
|
||||
const sib = [
|
||||
"/lib/S01/Show - S01E01 - 1080p.mkv",
|
||||
"/lib/S01/Show - S01E01 - 2160p.mkv",
|
||||
"/lib/S01/Show - S01E01 - 2160p.chi.zh-cn.srt",
|
||||
"/lib/S01/Show - S01E01 - 720p.mp4",
|
||||
"/lib/S01/Show - S01E02 - 1080p.mkv",
|
||||
"/lib/S01/Show - S01E01 - 1080p.nfo",
|
||||
];
|
||||
expect(findOtherVersions("/lib/S01/Show - S01E01 - 2160p.mkv", sib).sort()).toEqual([
|
||||
"/lib/S01/Show - S01E01 - 1080p.mkv",
|
||||
"/lib/S01/Show - S01E01 - 720p.mp4",
|
||||
]);
|
||||
expect(findOtherVersions("/lib/S01/Show - S01E01 - 2160p.chi.zh-cn.srt", sib)).toEqual([]);
|
||||
const movie = [
|
||||
"/lib/M (2020)/M (2020) - 1080p.mkv",
|
||||
"/lib/M (2020)/M (2020) - 2160p.mkv",
|
||||
"/lib/M (2020)/M (2020) - 1080p.srt",
|
||||
];
|
||||
expect(findOtherVersions("/lib/M (2020)/M (2020) - 2160p.mkv", movie)).toEqual([
|
||||
"/lib/M (2020)/M (2020) - 1080p.mkv",
|
||||
]);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user