@duyquangnvx/webnovel-downloader 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +323 -261
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +20 -1
- package/dist/index.d.ts +20 -1
- package/dist/index.js +314 -259
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -448,7 +448,8 @@ __export(index_exports, {
|
|
|
448
448
|
builtinAdapters: () => builtinAdapters,
|
|
449
449
|
createDownloader: () => createDownloader,
|
|
450
450
|
createLogger: () => createLogger,
|
|
451
|
-
downloader: () => downloader
|
|
451
|
+
downloader: () => downloader,
|
|
452
|
+
parseNovelHeader: () => parseNovelHeader
|
|
452
453
|
});
|
|
453
454
|
module.exports = __toCommonJS(index_exports);
|
|
454
455
|
|
|
@@ -881,6 +882,31 @@ function clamp(n, lo, hi) {
|
|
|
881
882
|
// src/http/index.ts
|
|
882
883
|
init_cookie_jar();
|
|
883
884
|
|
|
885
|
+
// src/http/stack-builder.ts
|
|
886
|
+
var HttpStack = class _HttpStack {
|
|
887
|
+
#client;
|
|
888
|
+
constructor(client) {
|
|
889
|
+
this.#client = client;
|
|
890
|
+
}
|
|
891
|
+
static from(leaf) {
|
|
892
|
+
return new _HttpStack(leaf);
|
|
893
|
+
}
|
|
894
|
+
rotateUserAgent(uas, defaultHeaders, cookieJar) {
|
|
895
|
+
return new _HttpStack(
|
|
896
|
+
new UserAgentRotatingHttpClient(this.#client, uas, defaultHeaders, cookieJar)
|
|
897
|
+
);
|
|
898
|
+
}
|
|
899
|
+
rateLimit(limiter, onEvent) {
|
|
900
|
+
return new _HttpStack(new RateLimitedHttpClient(this.#client, limiter, onEvent));
|
|
901
|
+
}
|
|
902
|
+
retry(opts, logger, limiter, onEvent) {
|
|
903
|
+
return new _HttpStack(new RetryingHttpClient(this.#client, opts, logger, limiter, onEvent));
|
|
904
|
+
}
|
|
905
|
+
build() {
|
|
906
|
+
return this.#client;
|
|
907
|
+
}
|
|
908
|
+
};
|
|
909
|
+
|
|
884
910
|
// src/http/tiered.ts
|
|
885
911
|
init_cf_challenge();
|
|
886
912
|
var FALLBACK_UA = DEFAULT_UA_POOL[0] ?? "Mozilla/5.0";
|
|
@@ -942,21 +968,8 @@ function buildHttpClient(opts = {}) {
|
|
|
942
968
|
const logger = opts.logger ?? silentLogger;
|
|
943
969
|
const cookieJar = opts.cookieJar ?? new CookieJar();
|
|
944
970
|
const leaf = opts.undiciOverride ?? new UndiciHttpClient({ logger });
|
|
945
|
-
const ua = new UserAgentRotatingHttpClient(
|
|
946
|
-
leaf,
|
|
947
|
-
opts.userAgents ?? new UserAgents(),
|
|
948
|
-
opts.defaultHeaders,
|
|
949
|
-
cookieJar
|
|
950
|
-
);
|
|
951
971
|
const limiter = opts.rateLimiter ?? new RateLimiter(opts.rateLimit ?? DEFAULT_RATE_LIMIT);
|
|
952
|
-
const
|
|
953
|
-
const httpBranch = new RetryingHttpClient(
|
|
954
|
-
limited,
|
|
955
|
-
normalizeRetry(opts.retry),
|
|
956
|
-
logger,
|
|
957
|
-
limiter,
|
|
958
|
-
opts.onEvent
|
|
959
|
-
);
|
|
972
|
+
const httpBranch = HttpStack.from(leaf).rotateUserAgent(opts.userAgents ?? new UserAgents(), opts.defaultHeaders, cookieJar).rateLimit(limiter, opts.onEvent).retry(normalizeRetry(opts.retry), logger, limiter, opts.onEvent).build();
|
|
960
973
|
const { mode } = normalizeTransport(opts.transport);
|
|
961
974
|
if (mode === "http-only") {
|
|
962
975
|
return httpBranch;
|
|
@@ -1411,7 +1424,6 @@ function toResponse(entry) {
|
|
|
1411
1424
|
|
|
1412
1425
|
// src/core/download-run.ts
|
|
1413
1426
|
var import_p_queue = __toESM(require("p-queue"), 1);
|
|
1414
|
-
var import_node_path6 = require("path");
|
|
1415
1427
|
|
|
1416
1428
|
// src/core/debouncer.ts
|
|
1417
1429
|
var Debouncer = class {
|
|
@@ -1474,30 +1486,47 @@ init_primitives();
|
|
|
1474
1486
|
|
|
1475
1487
|
// src/pipeline/fetch-toc.ts
|
|
1476
1488
|
init_errors();
|
|
1489
|
+
|
|
1490
|
+
// src/pipeline/walk-toc.ts
|
|
1491
|
+
init_errors();
|
|
1477
1492
|
var TOC_PROGRESS_EMIT_EVERY = 50;
|
|
1478
|
-
async function
|
|
1493
|
+
async function walkToc(source, opts) {
|
|
1494
|
+
const from = opts.range?.from;
|
|
1495
|
+
const to = opts.range?.to;
|
|
1479
1496
|
let count = 0;
|
|
1480
|
-
|
|
1481
|
-
const
|
|
1482
|
-
|
|
1483
|
-
for await (const raw of adapter.fetchChapterList(url, ctx)) {
|
|
1484
|
-
if (ctx.signal.aborted) throw new CancelledError({ cause: ctx.signal.reason });
|
|
1485
|
-
const parsed = ChapterRefSchema.safeParse(raw);
|
|
1486
|
-
if (!parsed.success) {
|
|
1487
|
-
throw new ParseError(`Invalid ChapterRef from adapter`, { cause: parsed.error, url });
|
|
1488
|
-
}
|
|
1489
|
-
const ref = parsed.data;
|
|
1497
|
+
if (opts.signal.aborted) throw new CancelledError({ cause: opts.signal.reason });
|
|
1498
|
+
for await (const ref of source) {
|
|
1499
|
+
if (opts.signal.aborted) throw new CancelledError({ cause: opts.signal.reason });
|
|
1490
1500
|
if (to !== void 0 && ref.index > to) break;
|
|
1491
1501
|
if (from !== void 0 && ref.index < from) continue;
|
|
1492
|
-
await onRef(ref);
|
|
1502
|
+
await opts.onRef(ref);
|
|
1493
1503
|
count++;
|
|
1494
|
-
if (count % TOC_PROGRESS_EMIT_EVERY === 0) bus.emit({ type: "toc:progress", discovered: count });
|
|
1504
|
+
if (count % TOC_PROGRESS_EMIT_EVERY === 0) opts.bus.emit({ type: "toc:progress", discovered: count });
|
|
1495
1505
|
}
|
|
1496
|
-
bus.emit({ type: "toc:progress", discovered: count });
|
|
1497
|
-
bus.emit({ type: "toc:complete", total: count });
|
|
1506
|
+
opts.bus.emit({ type: "toc:progress", discovered: count });
|
|
1507
|
+
opts.bus.emit({ type: "toc:complete", total: count });
|
|
1498
1508
|
return count;
|
|
1499
1509
|
}
|
|
1500
1510
|
|
|
1511
|
+
// src/pipeline/fetch-toc.ts
|
|
1512
|
+
async function fetchTocStage(adapter, url, ctx, bus, onRef, range) {
|
|
1513
|
+
async function* validated() {
|
|
1514
|
+
for await (const raw of adapter.fetchChapterList(url, ctx)) {
|
|
1515
|
+
const parsed = ChapterRefSchema.safeParse(raw);
|
|
1516
|
+
if (!parsed.success) {
|
|
1517
|
+
throw new ParseError(`Invalid ChapterRef from adapter`, { cause: parsed.error, url });
|
|
1518
|
+
}
|
|
1519
|
+
yield parsed.data;
|
|
1520
|
+
}
|
|
1521
|
+
}
|
|
1522
|
+
return walkToc(validated(), {
|
|
1523
|
+
...range !== void 0 ? { range } : {},
|
|
1524
|
+
bus,
|
|
1525
|
+
signal: ctx.signal,
|
|
1526
|
+
onRef
|
|
1527
|
+
});
|
|
1528
|
+
}
|
|
1529
|
+
|
|
1501
1530
|
// src/pipeline/fetch-chapters.ts
|
|
1502
1531
|
init_errors();
|
|
1503
1532
|
async function fetchChapterTask(adapter, ref, ctx, bus) {
|
|
@@ -1525,130 +1554,15 @@ function assembleNovelData(metadata, results) {
|
|
|
1525
1554
|
// src/core/download-run.ts
|
|
1526
1555
|
init_errors();
|
|
1527
1556
|
|
|
1528
|
-
// src/storage/resume-
|
|
1529
|
-
var import_promises3 = require("fs/promises");
|
|
1557
|
+
// src/storage/resume-token.ts
|
|
1530
1558
|
var import_node_path3 = require("path");
|
|
1531
|
-
init_errors();
|
|
1532
|
-
|
|
1533
|
-
// src/storage/resume-state.ts
|
|
1534
1559
|
var import_zod4 = require("zod");
|
|
1535
|
-
init_primitives();
|
|
1536
|
-
var ResumeFailureRecordSchema = import_zod4.z.object({
|
|
1537
|
-
index: ChapterIndexSchema,
|
|
1538
|
-
lastError: import_zod4.z.object({ code: import_zod4.z.string(), message: import_zod4.z.string() }).readonly()
|
|
1539
|
-
}).readonly();
|
|
1540
|
-
var ResumeStateSchema = import_zod4.z.object({
|
|
1541
|
-
schemaVersion: import_zod4.z.literal(1),
|
|
1542
|
-
url: UrlSchema,
|
|
1543
|
-
adapterId: AdapterIdSchema,
|
|
1544
|
-
metadata: NovelMetadataSchema.nullable(),
|
|
1545
|
-
toc: import_zod4.z.array(ChapterRefSchema).readonly().nullable(),
|
|
1546
|
-
completed: import_zod4.z.array(ChapterIndexSchema).readonly(),
|
|
1547
|
-
failed: import_zod4.z.array(ResumeFailureRecordSchema).readonly(),
|
|
1548
|
-
startedAt: IsoDateSchema,
|
|
1549
|
-
updatedAt: IsoDateSchema
|
|
1550
|
-
}).readonly();
|
|
1551
|
-
|
|
1552
|
-
// src/storage/resume-store.ts
|
|
1553
|
-
var FilesystemResumeStore = class {
|
|
1554
|
-
async load(path) {
|
|
1555
|
-
let text;
|
|
1556
|
-
try {
|
|
1557
|
-
text = await (0, import_promises3.readFile)(path, "utf8");
|
|
1558
|
-
} catch (err) {
|
|
1559
|
-
if (isEnoent(err)) return null;
|
|
1560
|
-
throw err;
|
|
1561
|
-
}
|
|
1562
|
-
let raw;
|
|
1563
|
-
try {
|
|
1564
|
-
raw = JSON.parse(text);
|
|
1565
|
-
} catch (cause) {
|
|
1566
|
-
throw new ParseError("Resume state file is corrupt; delete it to start over", { cause, path });
|
|
1567
|
-
}
|
|
1568
|
-
const parsed = ResumeStateSchema.safeParse(raw);
|
|
1569
|
-
if (!parsed.success) {
|
|
1570
|
-
throw new ParseError("Resume state file is corrupt; delete it to start over", {
|
|
1571
|
-
cause: parsed.error,
|
|
1572
|
-
path
|
|
1573
|
-
});
|
|
1574
|
-
}
|
|
1575
|
-
return parsed.data;
|
|
1576
|
-
}
|
|
1577
|
-
async save(path, state) {
|
|
1578
|
-
await atomicWriteJson(path, state);
|
|
1579
|
-
}
|
|
1580
|
-
async delete(path) {
|
|
1581
|
-
try {
|
|
1582
|
-
await (0, import_promises3.unlink)(path);
|
|
1583
|
-
} catch (err) {
|
|
1584
|
-
if (!isEnoent(err)) throw err;
|
|
1585
|
-
}
|
|
1586
|
-
}
|
|
1587
|
-
};
|
|
1588
|
-
|
|
1589
|
-
// src/storage/chapter-store.ts
|
|
1590
|
-
var import_promises4 = require("fs/promises");
|
|
1591
|
-
var import_node_path4 = require("path");
|
|
1592
|
-
var FilesystemChapterStore = class {
|
|
1593
|
-
#logger;
|
|
1594
|
-
#dir;
|
|
1595
|
-
constructor(dir, opts = {}) {
|
|
1596
|
-
this.#dir = (0, import_node_path4.resolve)(dir);
|
|
1597
|
-
this.#logger = opts.logger ?? silentLogger;
|
|
1598
|
-
}
|
|
1599
|
-
async save(chapter) {
|
|
1600
|
-
await atomicWriteJson((0, import_node_path4.join)(this.#dir, `${Number(chapter.index)}.json`), chapter);
|
|
1601
|
-
}
|
|
1602
|
-
async loadAll() {
|
|
1603
|
-
let entries;
|
|
1604
|
-
try {
|
|
1605
|
-
entries = await (0, import_promises4.readdir)(this.#dir);
|
|
1606
|
-
} catch (err) {
|
|
1607
|
-
if (isEnoent(err)) return [];
|
|
1608
|
-
throw err;
|
|
1609
|
-
}
|
|
1610
|
-
const chapters = [];
|
|
1611
|
-
for (const name of entries) {
|
|
1612
|
-
if (!/^\d+\.json$/.test(name)) continue;
|
|
1613
|
-
const path = (0, import_node_path4.join)(this.#dir, name);
|
|
1614
|
-
let text;
|
|
1615
|
-
try {
|
|
1616
|
-
text = await (0, import_promises4.readFile)(path, "utf8");
|
|
1617
|
-
} catch (err) {
|
|
1618
|
-
this.#logger.warn({ path, err }, "chapter-store: failed to read file, skipping");
|
|
1619
|
-
continue;
|
|
1620
|
-
}
|
|
1621
|
-
let raw;
|
|
1622
|
-
try {
|
|
1623
|
-
raw = JSON.parse(text);
|
|
1624
|
-
} catch (err) {
|
|
1625
|
-
this.#logger.warn({ path, err }, "chapter-store: invalid JSON, skipping");
|
|
1626
|
-
continue;
|
|
1627
|
-
}
|
|
1628
|
-
const parsed = ChapterSchema.safeParse(raw);
|
|
1629
|
-
if (!parsed.success) {
|
|
1630
|
-
this.#logger.warn(
|
|
1631
|
-
{ path, issues: parsed.error.issues },
|
|
1632
|
-
"chapter-store: schema mismatch, skipping"
|
|
1633
|
-
);
|
|
1634
|
-
continue;
|
|
1635
|
-
}
|
|
1636
|
-
chapters.push(parsed.data);
|
|
1637
|
-
}
|
|
1638
|
-
chapters.sort((a, b) => Number(a.index) - Number(b.index));
|
|
1639
|
-
return chapters;
|
|
1640
|
-
}
|
|
1641
|
-
};
|
|
1642
|
-
|
|
1643
|
-
// src/storage/resume-token.ts
|
|
1644
|
-
var import_node_path5 = require("path");
|
|
1645
|
-
var import_zod5 = require("zod");
|
|
1646
1560
|
init_errors();
|
|
1647
1561
|
init_primitives();
|
|
1648
|
-
var TokenPayloadSchema =
|
|
1649
|
-
v:
|
|
1650
|
-
stateFile:
|
|
1651
|
-
urlHash:
|
|
1562
|
+
var TokenPayloadSchema = import_zod4.z.object({
|
|
1563
|
+
v: import_zod4.z.literal(1),
|
|
1564
|
+
stateFile: import_zod4.z.string().min(1),
|
|
1565
|
+
urlHash: import_zod4.z.string().min(1)
|
|
1652
1566
|
});
|
|
1653
1567
|
function encodeResumeToken(stateFile, url) {
|
|
1654
1568
|
const payload = { v: 1, stateFile, urlHash: sha1(url) };
|
|
@@ -1669,9 +1583,9 @@ function decodeResumeToken(token) {
|
|
|
1669
1583
|
return { stateFile: parsed.data.stateFile, urlHash: parsed.data.urlHash };
|
|
1670
1584
|
}
|
|
1671
1585
|
function assertWithinCacheDir(stateFile) {
|
|
1672
|
-
const root = (0,
|
|
1673
|
-
const rel = (0,
|
|
1674
|
-
if (rel === "" || rel.startsWith("..") || (0,
|
|
1586
|
+
const root = (0, import_node_path3.resolve)((0, import_node_path3.join)(userCacheDir(), "webnovel-downloader"));
|
|
1587
|
+
const rel = (0, import_node_path3.relative)(root, (0, import_node_path3.resolve)(stateFile));
|
|
1588
|
+
if (rel === "" || rel.startsWith("..") || (0, import_node_path3.isAbsolute)(rel)) {
|
|
1675
1589
|
throw new ParseError("Resume token state path escapes the cache directory");
|
|
1676
1590
|
}
|
|
1677
1591
|
}
|
|
@@ -1687,25 +1601,6 @@ function verifyResumeToken(token, url) {
|
|
|
1687
1601
|
// src/core/download-run.ts
|
|
1688
1602
|
var QUEUE_BACKPRESSURE_RATIO = 4;
|
|
1689
1603
|
var STATE_PERSIST_DEBOUNCE_MS = 500;
|
|
1690
|
-
var TOC_PROGRESS_EMIT_EVERY2 = 50;
|
|
1691
|
-
function resolveResumeConfig(opt, url, logger) {
|
|
1692
|
-
if (opt === void 0) return null;
|
|
1693
|
-
const make = (stateFile) => ({
|
|
1694
|
-
stateFile,
|
|
1695
|
-
store: new FilesystemResumeStore(),
|
|
1696
|
-
chapterStore: new FilesystemChapterStore(`${stateFile}.chapters`, { logger })
|
|
1697
|
-
});
|
|
1698
|
-
if (opt === true) {
|
|
1699
|
-
const stateFile = (0, import_node_path6.join)(userCacheDir(), "webnovel-downloader", "state", `${sha1(url)}.json`);
|
|
1700
|
-
return make(stateFile);
|
|
1701
|
-
}
|
|
1702
|
-
if ("stateFile" in opt) return make(opt.stateFile);
|
|
1703
|
-
if ("token" in opt) {
|
|
1704
|
-
const { stateFile } = verifyResumeToken(opt.token, url);
|
|
1705
|
-
return make(stateFile);
|
|
1706
|
-
}
|
|
1707
|
-
return null;
|
|
1708
|
-
}
|
|
1709
1604
|
function freshState(url, adapterId) {
|
|
1710
1605
|
const now = nowIso();
|
|
1711
1606
|
return {
|
|
@@ -1727,14 +1622,13 @@ var DownloadRun = class {
|
|
|
1727
1622
|
#adapter;
|
|
1728
1623
|
#validated;
|
|
1729
1624
|
#normalizedRange;
|
|
1730
|
-
#
|
|
1625
|
+
#resumeStorage;
|
|
1731
1626
|
#concurrency;
|
|
1732
1627
|
#signal;
|
|
1733
1628
|
#ctx;
|
|
1734
1629
|
#bus;
|
|
1735
1630
|
#logger;
|
|
1736
1631
|
#state = null;
|
|
1737
|
-
#metadata;
|
|
1738
1632
|
#successes = [];
|
|
1739
1633
|
#failures = [];
|
|
1740
1634
|
#completedSet = /* @__PURE__ */ new Set();
|
|
@@ -1748,7 +1642,7 @@ var DownloadRun = class {
|
|
|
1748
1642
|
this.#adapter = args.adapter;
|
|
1749
1643
|
this.#validated = args.validated;
|
|
1750
1644
|
this.#normalizedRange = args.normalizedRange;
|
|
1751
|
-
this.#
|
|
1645
|
+
this.#resumeStorage = args.resumeStorage;
|
|
1752
1646
|
this.#concurrency = args.concurrency;
|
|
1753
1647
|
this.#signal = args.signal;
|
|
1754
1648
|
this.#ctx = args.ctx;
|
|
@@ -1764,9 +1658,10 @@ var DownloadRun = class {
|
|
|
1764
1658
|
async execute() {
|
|
1765
1659
|
const initErr = await this.#initResume();
|
|
1766
1660
|
if (initErr) return initErr;
|
|
1767
|
-
const
|
|
1768
|
-
if (
|
|
1769
|
-
const
|
|
1661
|
+
const resolved = await this.#resolveMetadata();
|
|
1662
|
+
if ("error" in resolved) return resolved.error;
|
|
1663
|
+
const meta = resolved.meta;
|
|
1664
|
+
const ctxWithMeta = this.#startQueue(meta);
|
|
1770
1665
|
const onAbort = () => {
|
|
1771
1666
|
this.#aborted = true;
|
|
1772
1667
|
this.#queue.clear();
|
|
@@ -1776,16 +1671,16 @@ var DownloadRun = class {
|
|
|
1776
1671
|
const stage2Error = await this.#runTocStage(ctxWithMeta);
|
|
1777
1672
|
await this.#drainQueueOrAbort();
|
|
1778
1673
|
await this.#flushQuietly();
|
|
1779
|
-
return this.#finalize(stage2Error);
|
|
1674
|
+
return this.#finalize(meta, stage2Error);
|
|
1780
1675
|
} finally {
|
|
1781
1676
|
this.#signal.removeEventListener("abort", onAbort);
|
|
1782
1677
|
}
|
|
1783
1678
|
}
|
|
1784
1679
|
async #initResume() {
|
|
1785
|
-
const resumeCfg = this.#
|
|
1680
|
+
const resumeCfg = this.#resumeStorage;
|
|
1786
1681
|
if (resumeCfg) {
|
|
1787
1682
|
try {
|
|
1788
|
-
let state = await resumeCfg.
|
|
1683
|
+
let state = await resumeCfg.resume.load(resumeCfg.stateFile);
|
|
1789
1684
|
if (state) {
|
|
1790
1685
|
if (state.url !== this.#validated) {
|
|
1791
1686
|
throw new ParseError("Stale resume state: URL mismatch");
|
|
@@ -1802,7 +1697,7 @@ var DownloadRun = class {
|
|
|
1802
1697
|
}
|
|
1803
1698
|
}
|
|
1804
1699
|
if (resumeCfg && this.#state) {
|
|
1805
|
-
const prior = await resumeCfg.
|
|
1700
|
+
const prior = await resumeCfg.chapters.loadAll();
|
|
1806
1701
|
this.#successes.push(...prior);
|
|
1807
1702
|
const merged = new Set(this.#state.completed);
|
|
1808
1703
|
for (const c of prior) merged.add(c.index);
|
|
@@ -1814,32 +1709,30 @@ var DownloadRun = class {
|
|
|
1814
1709
|
return null;
|
|
1815
1710
|
}
|
|
1816
1711
|
async #persistState() {
|
|
1817
|
-
const resumeCfg = this.#
|
|
1712
|
+
const resumeCfg = this.#resumeStorage;
|
|
1818
1713
|
if (!resumeCfg || !this.#state) return;
|
|
1819
1714
|
const next = { ...this.#state, updatedAt: nowIso() };
|
|
1820
1715
|
this.#state = next;
|
|
1821
|
-
await resumeCfg.
|
|
1716
|
+
await resumeCfg.resume.save(resumeCfg.stateFile, next);
|
|
1822
1717
|
}
|
|
1823
1718
|
async #resolveMetadata() {
|
|
1824
1719
|
try {
|
|
1720
|
+
let meta;
|
|
1825
1721
|
if (this.#state?.metadata) {
|
|
1826
|
-
|
|
1722
|
+
meta = this.#state.metadata;
|
|
1827
1723
|
} else {
|
|
1828
|
-
|
|
1724
|
+
meta = await fetchMetadataStage(this.#adapter, this.#validated, this.#ctx, this.#bus);
|
|
1829
1725
|
if (this.#state) {
|
|
1830
|
-
this.#state = { ...this.#state, metadata:
|
|
1726
|
+
this.#state = { ...this.#state, metadata: meta, updatedAt: nowIso() };
|
|
1831
1727
|
await this.#persistState();
|
|
1832
1728
|
}
|
|
1833
1729
|
}
|
|
1730
|
+
return { meta };
|
|
1834
1731
|
} catch (err) {
|
|
1835
|
-
return {
|
|
1836
|
-
status: "error",
|
|
1837
|
-
error: normalizeToEnvelopeError(err)
|
|
1838
|
-
};
|
|
1732
|
+
return { error: { status: "error", error: normalizeToEnvelopeError(err) } };
|
|
1839
1733
|
}
|
|
1840
|
-
return null;
|
|
1841
1734
|
}
|
|
1842
|
-
#startQueue() {
|
|
1735
|
+
#startQueue(meta) {
|
|
1843
1736
|
const state = this.#state;
|
|
1844
1737
|
const completedSet = this.#completedSet;
|
|
1845
1738
|
const completedList = this.#completedList;
|
|
@@ -1851,8 +1744,7 @@ var DownloadRun = class {
|
|
|
1851
1744
|
if (!completedSet.has(f.index)) failedMap.set(f.index, f);
|
|
1852
1745
|
}
|
|
1853
1746
|
}
|
|
1854
|
-
|
|
1855
|
-
return { ...this.#ctx, novelMetadata: this.#metadata };
|
|
1747
|
+
return { ...this.#ctx, novelMetadata: meta };
|
|
1856
1748
|
}
|
|
1857
1749
|
async #enqueueRef(ref, ctxWithMeta) {
|
|
1858
1750
|
if (this.#aborted || this.#signal.aborted) return;
|
|
@@ -1872,7 +1764,7 @@ var DownloadRun = class {
|
|
|
1872
1764
|
try {
|
|
1873
1765
|
const chapter = await fetchChapterTask(this.#adapter, ref, ctxWithMeta, this.#bus);
|
|
1874
1766
|
if (this.#aborted) return;
|
|
1875
|
-
if (this.#
|
|
1767
|
+
if (this.#resumeStorage) await this.#resumeStorage.chapters.save(chapter);
|
|
1876
1768
|
this.#successes.push(chapter);
|
|
1877
1769
|
this.#completedList.push(ref.index);
|
|
1878
1770
|
this.#completedSet.add(ref.index);
|
|
@@ -1889,7 +1781,7 @@ var DownloadRun = class {
|
|
|
1889
1781
|
this.#bus.emit({
|
|
1890
1782
|
type: "progress",
|
|
1891
1783
|
completed: this.#successes.length,
|
|
1892
|
-
total:
|
|
1784
|
+
total: ctxWithMeta.novelMetadata?.totalChapters ?? this.#enqueuedSet.size
|
|
1893
1785
|
});
|
|
1894
1786
|
} catch (cause) {
|
|
1895
1787
|
if (cause instanceof CancelledError) return;
|
|
@@ -1910,20 +1802,16 @@ var DownloadRun = class {
|
|
|
1910
1802
|
let stage2Error;
|
|
1911
1803
|
try {
|
|
1912
1804
|
if (this.#state?.toc) {
|
|
1913
|
-
const
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
for (const ref of this.#state.toc) {
|
|
1917
|
-
if (to !== void 0 && ref.index > to) break;
|
|
1918
|
-
if (from !== void 0 && ref.index < from) continue;
|
|
1919
|
-
await this.#enqueueRef(ref, ctxWithMeta);
|
|
1920
|
-
discovered++;
|
|
1921
|
-
if (discovered % TOC_PROGRESS_EMIT_EVERY2 === 0) {
|
|
1922
|
-
this.#bus.emit({ type: "toc:progress", discovered });
|
|
1923
|
-
}
|
|
1805
|
+
const toc = this.#state.toc;
|
|
1806
|
+
async function* fromState() {
|
|
1807
|
+
for (const ref of toc) yield ref;
|
|
1924
1808
|
}
|
|
1925
|
-
|
|
1926
|
-
|
|
1809
|
+
await walkToc(fromState(), {
|
|
1810
|
+
...this.#normalizedRange !== void 0 ? { range: this.#normalizedRange } : {},
|
|
1811
|
+
bus: this.#bus,
|
|
1812
|
+
signal: this.#signal,
|
|
1813
|
+
onRef: (ref) => this.#enqueueRef(ref, ctxWithMeta)
|
|
1814
|
+
});
|
|
1927
1815
|
} else {
|
|
1928
1816
|
const tocBuffer = [];
|
|
1929
1817
|
await fetchTocStage(
|
|
@@ -1998,10 +1886,8 @@ var DownloadRun = class {
|
|
|
1998
1886
|
this.#logger.warn({ err, url: this.#validated }, "resume state persist failed (non-fatal)");
|
|
1999
1887
|
}
|
|
2000
1888
|
}
|
|
2001
|
-
#finalize(stage2Error) {
|
|
1889
|
+
#finalize(metadata, stage2Error) {
|
|
2002
1890
|
if (stage2Error) return { status: "error", error: stage2Error };
|
|
2003
|
-
if (!this.#metadata) throw new Error("invariant: metadata must be set before #finalize");
|
|
2004
|
-
const metadata = this.#metadata;
|
|
2005
1891
|
let data;
|
|
2006
1892
|
try {
|
|
2007
1893
|
data = assembleNovelData(metadata, this.#successes);
|
|
@@ -2012,8 +1898,8 @@ var DownloadRun = class {
|
|
|
2012
1898
|
};
|
|
2013
1899
|
}
|
|
2014
1900
|
if (this.#failures.length > 0) {
|
|
2015
|
-
if (this.#
|
|
2016
|
-
const token = encodeResumeToken(this.#
|
|
1901
|
+
if (this.#resumeStorage) {
|
|
1902
|
+
const token = encodeResumeToken(this.#resumeStorage.stateFile, this.#validated);
|
|
2017
1903
|
return { status: "partial", resumable: true, data, failures: this.#failures, resumeToken: token };
|
|
2018
1904
|
}
|
|
2019
1905
|
return { status: "partial", resumable: false, data, failures: this.#failures };
|
|
@@ -2029,6 +1915,185 @@ function normalizeChapterRange(range) {
|
|
|
2029
1915
|
return out;
|
|
2030
1916
|
}
|
|
2031
1917
|
|
|
1918
|
+
// src/storage/resume-storage.ts
|
|
1919
|
+
var import_node_path6 = require("path");
|
|
1920
|
+
|
|
1921
|
+
// src/storage/resume-store.ts
|
|
1922
|
+
var import_promises3 = require("fs/promises");
|
|
1923
|
+
var import_node_path4 = require("path");
|
|
1924
|
+
init_errors();
|
|
1925
|
+
|
|
1926
|
+
// src/storage/resume-state.ts
|
|
1927
|
+
var import_zod5 = require("zod");
|
|
1928
|
+
init_primitives();
|
|
1929
|
+
var ResumeFailureRecordSchema = import_zod5.z.object({
|
|
1930
|
+
index: ChapterIndexSchema,
|
|
1931
|
+
lastError: import_zod5.z.object({ code: import_zod5.z.string(), message: import_zod5.z.string() }).readonly()
|
|
1932
|
+
}).readonly();
|
|
1933
|
+
var ResumeStateSchema = import_zod5.z.object({
|
|
1934
|
+
schemaVersion: import_zod5.z.literal(1),
|
|
1935
|
+
url: UrlSchema,
|
|
1936
|
+
adapterId: AdapterIdSchema,
|
|
1937
|
+
metadata: NovelMetadataSchema.nullable(),
|
|
1938
|
+
toc: import_zod5.z.array(ChapterRefSchema).readonly().nullable(),
|
|
1939
|
+
completed: import_zod5.z.array(ChapterIndexSchema).readonly(),
|
|
1940
|
+
failed: import_zod5.z.array(ResumeFailureRecordSchema).readonly(),
|
|
1941
|
+
startedAt: IsoDateSchema,
|
|
1942
|
+
updatedAt: IsoDateSchema
|
|
1943
|
+
}).readonly();
|
|
1944
|
+
|
|
1945
|
+
// src/storage/resume-store.ts
|
|
1946
|
+
var MemoryResumeStore = class {
|
|
1947
|
+
#map = /* @__PURE__ */ new Map();
|
|
1948
|
+
async load(path) {
|
|
1949
|
+
return this.#map.get((0, import_node_path4.resolve)(path)) ?? null;
|
|
1950
|
+
}
|
|
1951
|
+
async save(path, state) {
|
|
1952
|
+
this.#map.set((0, import_node_path4.resolve)(path), state);
|
|
1953
|
+
}
|
|
1954
|
+
async delete(path) {
|
|
1955
|
+
this.#map.delete((0, import_node_path4.resolve)(path));
|
|
1956
|
+
}
|
|
1957
|
+
};
|
|
1958
|
+
var FilesystemResumeStore = class {
|
|
1959
|
+
async load(path) {
|
|
1960
|
+
let text;
|
|
1961
|
+
try {
|
|
1962
|
+
text = await (0, import_promises3.readFile)(path, "utf8");
|
|
1963
|
+
} catch (err) {
|
|
1964
|
+
if (isEnoent(err)) return null;
|
|
1965
|
+
throw err;
|
|
1966
|
+
}
|
|
1967
|
+
let raw;
|
|
1968
|
+
try {
|
|
1969
|
+
raw = JSON.parse(text);
|
|
1970
|
+
} catch (cause) {
|
|
1971
|
+
throw new ParseError("Resume state file is corrupt; delete it to start over", { cause, path });
|
|
1972
|
+
}
|
|
1973
|
+
const parsed = ResumeStateSchema.safeParse(raw);
|
|
1974
|
+
if (!parsed.success) {
|
|
1975
|
+
throw new ParseError("Resume state file is corrupt; delete it to start over", {
|
|
1976
|
+
cause: parsed.error,
|
|
1977
|
+
path
|
|
1978
|
+
});
|
|
1979
|
+
}
|
|
1980
|
+
return parsed.data;
|
|
1981
|
+
}
|
|
1982
|
+
async save(path, state) {
|
|
1983
|
+
await atomicWriteJson(path, state);
|
|
1984
|
+
}
|
|
1985
|
+
async delete(path) {
|
|
1986
|
+
try {
|
|
1987
|
+
await (0, import_promises3.unlink)(path);
|
|
1988
|
+
} catch (err) {
|
|
1989
|
+
if (!isEnoent(err)) throw err;
|
|
1990
|
+
}
|
|
1991
|
+
}
|
|
1992
|
+
};
|
|
1993
|
+
|
|
1994
|
+
// src/storage/chapter-store.ts
|
|
1995
|
+
var import_promises4 = require("fs/promises");
|
|
1996
|
+
var import_node_path5 = require("path");
|
|
1997
|
+
var MemoryChapterStore = class {
|
|
1998
|
+
#map = /* @__PURE__ */ new Map();
|
|
1999
|
+
async save(chapter) {
|
|
2000
|
+
this.#map.set(Number(chapter.index), chapter);
|
|
2001
|
+
}
|
|
2002
|
+
async loadAll() {
|
|
2003
|
+
return [...this.#map.values()].sort((a, b) => Number(a.index) - Number(b.index));
|
|
2004
|
+
}
|
|
2005
|
+
};
|
|
2006
|
+
var FilesystemChapterStore = class {
|
|
2007
|
+
#logger;
|
|
2008
|
+
#dir;
|
|
2009
|
+
constructor(dir, opts = {}) {
|
|
2010
|
+
this.#dir = (0, import_node_path5.resolve)(dir);
|
|
2011
|
+
this.#logger = opts.logger ?? silentLogger;
|
|
2012
|
+
}
|
|
2013
|
+
async save(chapter) {
|
|
2014
|
+
await atomicWriteJson((0, import_node_path5.join)(this.#dir, `${Number(chapter.index)}.json`), chapter);
|
|
2015
|
+
}
|
|
2016
|
+
async loadAll() {
|
|
2017
|
+
let entries;
|
|
2018
|
+
try {
|
|
2019
|
+
entries = await (0, import_promises4.readdir)(this.#dir);
|
|
2020
|
+
} catch (err) {
|
|
2021
|
+
if (isEnoent(err)) return [];
|
|
2022
|
+
throw err;
|
|
2023
|
+
}
|
|
2024
|
+
const chapters = [];
|
|
2025
|
+
for (const name of entries) {
|
|
2026
|
+
if (!/^\d+\.json$/.test(name)) continue;
|
|
2027
|
+
const path = (0, import_node_path5.join)(this.#dir, name);
|
|
2028
|
+
let text;
|
|
2029
|
+
try {
|
|
2030
|
+
text = await (0, import_promises4.readFile)(path, "utf8");
|
|
2031
|
+
} catch (err) {
|
|
2032
|
+
this.#logger.warn({ path, err }, "chapter-store: failed to read file, skipping");
|
|
2033
|
+
continue;
|
|
2034
|
+
}
|
|
2035
|
+
let raw;
|
|
2036
|
+
try {
|
|
2037
|
+
raw = JSON.parse(text);
|
|
2038
|
+
} catch (err) {
|
|
2039
|
+
this.#logger.warn({ path, err }, "chapter-store: invalid JSON, skipping");
|
|
2040
|
+
continue;
|
|
2041
|
+
}
|
|
2042
|
+
const parsed = ChapterSchema.safeParse(raw);
|
|
2043
|
+
if (!parsed.success) {
|
|
2044
|
+
this.#logger.warn(
|
|
2045
|
+
{ path, issues: parsed.error.issues },
|
|
2046
|
+
"chapter-store: schema mismatch, skipping"
|
|
2047
|
+
);
|
|
2048
|
+
continue;
|
|
2049
|
+
}
|
|
2050
|
+
chapters.push(parsed.data);
|
|
2051
|
+
}
|
|
2052
|
+
chapters.sort((a, b) => Number(a.index) - Number(b.index));
|
|
2053
|
+
return chapters;
|
|
2054
|
+
}
|
|
2055
|
+
};
|
|
2056
|
+
|
|
2057
|
+
// src/storage/resume-storage.ts
|
|
2058
|
+
var ResumeStorage = class _ResumeStorage {
|
|
2059
|
+
resume;
|
|
2060
|
+
chapters;
|
|
2061
|
+
stateFile;
|
|
2062
|
+
constructor(resume, chapters, stateFile) {
|
|
2063
|
+
this.resume = resume;
|
|
2064
|
+
this.chapters = chapters;
|
|
2065
|
+
this.stateFile = stateFile;
|
|
2066
|
+
}
|
|
2067
|
+
static forDefault(url, logger) {
|
|
2068
|
+
const stateFile = (0, import_node_path6.join)(userCacheDir(), "webnovel-downloader", "state", `${sha1(url)}.json`);
|
|
2069
|
+
return _ResumeStorage.#filesystem(stateFile, logger);
|
|
2070
|
+
}
|
|
2071
|
+
static forStateFile(stateFile, logger) {
|
|
2072
|
+
return _ResumeStorage.#filesystem(stateFile, logger);
|
|
2073
|
+
}
|
|
2074
|
+
static forToken(token, url, logger) {
|
|
2075
|
+
const { stateFile } = verifyResumeToken(token, url);
|
|
2076
|
+
return _ResumeStorage.#filesystem(stateFile, logger);
|
|
2077
|
+
}
|
|
2078
|
+
static memory(stateFile) {
|
|
2079
|
+
return new _ResumeStorage(new MemoryResumeStore(), new MemoryChapterStore(), stateFile);
|
|
2080
|
+
}
|
|
2081
|
+
static #filesystem(stateFile, logger) {
|
|
2082
|
+
return new _ResumeStorage(
|
|
2083
|
+
new FilesystemResumeStore(),
|
|
2084
|
+
new FilesystemChapterStore(`${stateFile}.chapters`, { logger }),
|
|
2085
|
+
stateFile
|
|
2086
|
+
);
|
|
2087
|
+
}
|
|
2088
|
+
};
|
|
2089
|
+
function resolveResumeStorage(opt, url, logger) {
|
|
2090
|
+
if (opt === void 0) return null;
|
|
2091
|
+
if (opt === true) return ResumeStorage.forDefault(url, logger);
|
|
2092
|
+
if ("stateFile" in opt) return ResumeStorage.forStateFile(opt.stateFile, logger);
|
|
2093
|
+
if ("token" in opt) return ResumeStorage.forToken(opt.token, url, logger);
|
|
2094
|
+
return null;
|
|
2095
|
+
}
|
|
2096
|
+
|
|
2032
2097
|
// src/core/downloader.ts
|
|
2033
2098
|
var DEFAULT_CACHE_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
2034
2099
|
var DEFAULT_CONCURRENCY = 4;
|
|
@@ -2200,12 +2265,12 @@ var Downloader = class {
|
|
|
2200
2265
|
};
|
|
2201
2266
|
try {
|
|
2202
2267
|
this.#transportListeners.add(transportListener);
|
|
2203
|
-
const
|
|
2268
|
+
const resumeStorage = resolveResumeStorage(options?.resume, validated, this.#logger);
|
|
2204
2269
|
const run = new DownloadRun({
|
|
2205
2270
|
adapter,
|
|
2206
2271
|
validated,
|
|
2207
2272
|
normalizedRange,
|
|
2208
|
-
|
|
2273
|
+
resumeStorage,
|
|
2209
2274
|
concurrency,
|
|
2210
2275
|
signal,
|
|
2211
2276
|
ctx,
|
|
@@ -2307,13 +2372,6 @@ function normalizeText(html) {
|
|
|
2307
2372
|
});
|
|
2308
2373
|
return text.replace(new RegExp(LINE, "g"), " ").replace(new RegExp(PARA, "g"), "\n\n").split(/\n{2,}/).map((p) => p.replace(/\s+/g, " ").trim()).filter(Boolean).join("\n\n").trim();
|
|
2309
2374
|
}
|
|
2310
|
-
function pickText($, selectors) {
|
|
2311
|
-
for (const sel of selectors) {
|
|
2312
|
-
const t = $(sel).first().text().trim();
|
|
2313
|
-
if (t) return t;
|
|
2314
|
-
}
|
|
2315
|
-
return "";
|
|
2316
|
-
}
|
|
2317
2375
|
function wordCount(text) {
|
|
2318
2376
|
return text.split(/\s+/).filter(Boolean).length;
|
|
2319
2377
|
}
|
|
@@ -2385,6 +2443,9 @@ function refsFromAnchors($, opts) {
|
|
|
2385
2443
|
return refs;
|
|
2386
2444
|
}
|
|
2387
2445
|
|
|
2446
|
+
// src/adapters/shared/header.ts
|
|
2447
|
+
init_errors();
|
|
2448
|
+
|
|
2388
2449
|
// src/adapters/shared/cover.ts
|
|
2389
2450
|
function extractCoverUrl($, selector, baseUrl) {
|
|
2390
2451
|
const el = $(selector).first();
|
|
@@ -2392,6 +2453,36 @@ function extractCoverUrl($, selector, baseUrl) {
|
|
|
2392
2453
|
return raw ? resolveUrl(baseUrl, raw) : void 0;
|
|
2393
2454
|
}
|
|
2394
2455
|
|
|
2456
|
+
// src/adapters/shared/header.ts
|
|
2457
|
+
function parseNovelHeader($, selectors, sourceUrl) {
|
|
2458
|
+
const title = $(selectors.title).first().text().trim();
|
|
2459
|
+
if (!title) throw new ParseError("Missing title on novel page", { url: sourceUrl });
|
|
2460
|
+
const author = $(selectors.author).first().text().trim();
|
|
2461
|
+
const descSelectors = typeof selectors.description === "string" ? [selectors.description] : selectors.description;
|
|
2462
|
+
let description = "";
|
|
2463
|
+
for (const sel of descSelectors) {
|
|
2464
|
+
const t = $(sel).first().text().trim();
|
|
2465
|
+
if (t) {
|
|
2466
|
+
description = t;
|
|
2467
|
+
break;
|
|
2468
|
+
}
|
|
2469
|
+
}
|
|
2470
|
+
description = description.replace(/\s+/g, " ").trim();
|
|
2471
|
+
const genres = [];
|
|
2472
|
+
$(selectors.genres).each((_, el) => {
|
|
2473
|
+
const t = $(el).text().trim();
|
|
2474
|
+
if (t && !genres.includes(t)) genres.push(t);
|
|
2475
|
+
});
|
|
2476
|
+
const coverUrl = extractCoverUrl($, selectors.cover, sourceUrl);
|
|
2477
|
+
return {
|
|
2478
|
+
title,
|
|
2479
|
+
author,
|
|
2480
|
+
description,
|
|
2481
|
+
genres,
|
|
2482
|
+
...coverUrl !== void 0 ? { coverUrl } : {}
|
|
2483
|
+
};
|
|
2484
|
+
}
|
|
2485
|
+
|
|
2395
2486
|
// src/adapters/shared/content.ts
|
|
2396
2487
|
init_errors();
|
|
2397
2488
|
function stripLeadingTitle(content, title) {
|
|
@@ -2432,31 +2523,16 @@ var TRUYENFULL_SELECTORS = {
|
|
|
2432
2523
|
|
|
2433
2524
|
// src/adapters/truyenfull/parser.ts
|
|
2434
2525
|
function parseNovelPage($, sourceUrl) {
|
|
2435
|
-
const
|
|
2436
|
-
if (!title) {
|
|
2437
|
-
throw new ParseError("Missing title on novel page", { url: sourceUrl });
|
|
2438
|
-
}
|
|
2439
|
-
const author = $(TRUYENFULL_SELECTORS.metadata.author).first().text().trim();
|
|
2440
|
-
const description = $(TRUYENFULL_SELECTORS.metadata.description).text().replace(/\s+/g, " ").trim();
|
|
2441
|
-
const genres = [];
|
|
2442
|
-
$(TRUYENFULL_SELECTORS.metadata.genres).each((_, el) => {
|
|
2443
|
-
const t = $(el).text().trim();
|
|
2444
|
-
if (t) genres.push(t);
|
|
2445
|
-
});
|
|
2446
|
-
const coverUrl = extractCoverUrl($, TRUYENFULL_SELECTORS.metadata.cover, sourceUrl);
|
|
2526
|
+
const header = parseNovelHeader($, TRUYENFULL_SELECTORS.metadata, sourceUrl);
|
|
2447
2527
|
let status = "unknown";
|
|
2448
2528
|
if ($(TRUYENFULL_SELECTORS.metadata.statusFull).length > 0) status = "completed";
|
|
2449
2529
|
else if ($(TRUYENFULL_SELECTORS.metadata.statusOngoing).length > 0) status = "ongoing";
|
|
2450
2530
|
return {
|
|
2531
|
+
...header,
|
|
2451
2532
|
sourceUrl,
|
|
2452
2533
|
sourceSite: unsafeBrandAdapterId("truyenfull"),
|
|
2453
|
-
title,
|
|
2454
|
-
author,
|
|
2455
|
-
description,
|
|
2456
|
-
genres,
|
|
2457
2534
|
status,
|
|
2458
|
-
fetchedAt: /* @__PURE__ */ new Date()
|
|
2459
|
-
...coverUrl !== void 0 ? { coverUrl } : {}
|
|
2535
|
+
fetchedAt: /* @__PURE__ */ new Date()
|
|
2460
2536
|
};
|
|
2461
2537
|
}
|
|
2462
2538
|
function parseTocPage($, startIndex, baseUrl) {
|
|
@@ -2622,32 +2698,17 @@ var METRUYENCHU_SELECTORS = {
|
|
|
2622
2698
|
// src/adapters/metruyenchu/parser.ts
|
|
2623
2699
|
var SOURCE_SITE = unsafeBrandAdapterId("metruyenchu-com-vn");
|
|
2624
2700
|
function parseNovelPage2($, sourceUrl) {
|
|
2625
|
-
const
|
|
2626
|
-
if (!title) {
|
|
2627
|
-
throw new ParseError("Missing title on novel page", { url: sourceUrl });
|
|
2628
|
-
}
|
|
2629
|
-
const author = $(METRUYENCHU_SELECTORS.metadata.author).first().text().trim();
|
|
2630
|
-
const description = pickText($, METRUYENCHU_SELECTORS.metadata.description).replace(/\s+/g, " ").trim();
|
|
2631
|
-
const genres = [];
|
|
2632
|
-
$(METRUYENCHU_SELECTORS.metadata.genres).each((_, el) => {
|
|
2633
|
-
const t = $(el).text().trim();
|
|
2634
|
-
if (t && !genres.includes(t)) genres.push(t);
|
|
2635
|
-
});
|
|
2636
|
-
const coverUrl = extractCoverUrl($, METRUYENCHU_SELECTORS.metadata.cover, sourceUrl);
|
|
2701
|
+
const header = parseNovelHeader($, METRUYENCHU_SELECTORS.metadata, sourceUrl);
|
|
2637
2702
|
let status = "unknown";
|
|
2638
2703
|
if ($(METRUYENCHU_SELECTORS.metadata.statusFull).length > 0) status = "completed";
|
|
2639
2704
|
else if (METRUYENCHU_SELECTORS.metadata.statusOngoing.some((sel) => $(sel).length > 0)) status = "ongoing";
|
|
2640
2705
|
const totalChapters = extractTotalChapters($);
|
|
2641
2706
|
return {
|
|
2707
|
+
...header,
|
|
2642
2708
|
sourceUrl,
|
|
2643
2709
|
sourceSite: SOURCE_SITE,
|
|
2644
|
-
title,
|
|
2645
|
-
author,
|
|
2646
|
-
description,
|
|
2647
|
-
genres,
|
|
2648
2710
|
status,
|
|
2649
2711
|
fetchedAt: /* @__PURE__ */ new Date(),
|
|
2650
|
-
...coverUrl ? { coverUrl } : {},
|
|
2651
2712
|
...totalChapters !== void 0 ? { totalChapters } : {}
|
|
2652
2713
|
};
|
|
2653
2714
|
}
|
|
@@ -3078,6 +3139,7 @@ var downloader = createDownloader();
|
|
|
3078
3139
|
builtinAdapters,
|
|
3079
3140
|
createDownloader,
|
|
3080
3141
|
createLogger,
|
|
3081
|
-
downloader
|
|
3142
|
+
downloader,
|
|
3143
|
+
parseNovelHeader
|
|
3082
3144
|
});
|
|
3083
3145
|
//# sourceMappingURL=index.cjs.map
|