pixivflow 2.24.0 → 2.26.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/download/NovelDownloader.js +43 -1
- package/dist/download/novelMarkers.d.ts +8 -0
- package/dist/download/novelMarkers.js +22 -0
- package/dist/package.json +1 -1
- package/dist/utils/zip.d.ts +14 -0
- package/dist/utils/zip.js +44 -0
- package/dist/version.js +1 -1
- package/dist/webui/package.json +1 -1
- package/package.json +3 -2
|
@@ -38,6 +38,7 @@ const logger_1 = require("../logger");
|
|
|
38
38
|
const node_path_1 = require("node:path");
|
|
39
39
|
const language_detection_1 = require("../utils/language-detection");
|
|
40
40
|
const novelMarkers_1 = require("./novelMarkers");
|
|
41
|
+
const zip_1 = require("../utils/zip");
|
|
41
42
|
const LANGUAGE_CACHE_TTL_MS = 7 * 24 * 60 * 60 * 1000;
|
|
42
43
|
class NovelDownloader {
|
|
43
44
|
client;
|
|
@@ -185,6 +186,20 @@ class NovelDownloader {
|
|
|
185
186
|
logger_1.logger.info(`Novel ${detail.id} inline images: ${assets.filter((a) => a.status === 'downloaded').length}/${assets.length} downloaded`, { novelId: detail.id });
|
|
186
187
|
}
|
|
187
188
|
}
|
|
189
|
+
// Rich-media markdown sidecar (RFC 1 Phase 2): same path as the .txt
|
|
190
|
+
// (compat format stays), inline images become relative 
|
|
191
|
+
// refs so a later TelePress/TelePost phase can render the Telegraph page.
|
|
192
|
+
let richMediaPath;
|
|
193
|
+
if (assets.some((a) => a.status === 'downloaded')) {
|
|
194
|
+
try {
|
|
195
|
+
const mdPath = await this.fileService.saveText(`${header}\n${(0, novelMarkers_1.renderNovelMarkdown)(text, assets)}`, fileName.replace(/\.txt$/, '.md'), metadata);
|
|
196
|
+
richMediaPath = mdPath;
|
|
197
|
+
logger_1.logger.info(`Saved novel ${detail.id} rich-media markdown sidecar`, { filePath: mdPath });
|
|
198
|
+
}
|
|
199
|
+
catch (error) {
|
|
200
|
+
logger_1.logger.warn(`Failed to save rich-media markdown sidecar for novel ${detail.id}: ${error instanceof Error ? error.message : String(error)}`);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
188
203
|
const pixivMetadata = {
|
|
189
204
|
pixiv_id: detail.id,
|
|
190
205
|
title: detail.title,
|
|
@@ -231,6 +246,33 @@ class NovelDownloader {
|
|
|
231
246
|
catch (error) {
|
|
232
247
|
logger_1.logger.warn(`Failed to save metadata for novel ${detail.id}: ${error instanceof Error ? error.message : String(error)}`);
|
|
233
248
|
}
|
|
249
|
+
// Rich-media ZIP archive (Phase 3): a save-only download package carrying
|
|
250
|
+
// txt + md + metadata + images. The md stays an internal render input, never
|
|
251
|
+
// a user-facing attachment; txt remains the authoritative/compat file.
|
|
252
|
+
let archivePath;
|
|
253
|
+
if (assets.some((a) => a.status === 'downloaded')) {
|
|
254
|
+
const zipName = this.fileService.sanitizeFileName(`${detail.id}_${detail.title}.zip`);
|
|
255
|
+
const dest = (0, node_path_1.join)((0, node_path_1.dirname)(filePath), zipName);
|
|
256
|
+
const entries = [
|
|
257
|
+
{ name: fileName, sourcePath: filePath },
|
|
258
|
+
];
|
|
259
|
+
if (richMediaPath)
|
|
260
|
+
entries.push({ name: `${fileName.replace(/\.txt$/, '.md')}`, sourcePath: richMediaPath });
|
|
261
|
+
if (metadataPath)
|
|
262
|
+
entries.push({ name: `${fileName}.json`, sourcePath: metadataPath });
|
|
263
|
+
for (const a of assets) {
|
|
264
|
+
if (a.status === 'downloaded' && a.localPath) {
|
|
265
|
+
entries.push({ name: `images/${(0, node_path_1.basename)(a.localPath)}`, sourcePath: a.localPath });
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
try {
|
|
269
|
+
archivePath = await (0, zip_1.createZipArchive)(dest, entries);
|
|
270
|
+
logger_1.logger.info(`Saved novel ${detail.id} zip archive`, { filePath: archivePath });
|
|
271
|
+
}
|
|
272
|
+
catch (error) {
|
|
273
|
+
logger_1.logger.warn(`Failed to save zip archive for novel ${detail.id}: ${error instanceof Error ? error.message : String(error)}`);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
234
276
|
this.database.insertDownload({
|
|
235
277
|
pixivId: String(detail.id),
|
|
236
278
|
type: 'novel',
|
|
@@ -252,7 +294,7 @@ class NovelDownloader {
|
|
|
252
294
|
type: 'novel',
|
|
253
295
|
title: detail.title,
|
|
254
296
|
tags: tags.map((item) => item.name).filter(Boolean),
|
|
255
|
-
files: [filePath],
|
|
297
|
+
files: archivePath ? [filePath, archivePath] : [filePath],
|
|
256
298
|
cleanupFiles: metadataPath ? [metadataPath] : [],
|
|
257
299
|
spoiler: (detail.x_restrict ?? 0) > 0,
|
|
258
300
|
xRestrict: detail.x_restrict,
|
|
@@ -51,4 +51,12 @@ export declare function scanNovelMarkers(source: string): NovelMarker[];
|
|
|
51
51
|
export declare function extractNovelAssets(text: string, response?: Pick<PixivNovelTextResponse, 'images' | 'illusts'>): NovelAsset[];
|
|
52
52
|
/** Minimal self-check for the scanner's double-bracket handling. */
|
|
53
53
|
export declare function demo(): void;
|
|
54
|
+
/**
|
|
55
|
+
* Rich-media Markdown sidecar (RFC 1 Phase 2). Rebuilds the novel body with
|
|
56
|
+
* each downloaded inline resource as a relative `` reference
|
|
57
|
+
* (the `.txt` file remains the authoritative/compat format and its path is
|
|
58
|
+
* unchanged). Markers whose asset is `unavailable`/`failed` are left in the
|
|
59
|
+
* original text so nothing is silently dropped (partial success).
|
|
60
|
+
*/
|
|
61
|
+
export declare function renderNovelMarkdown(source: string, assets: NovelAsset[]): string;
|
|
54
62
|
//# sourceMappingURL=novelMarkers.d.ts.map
|
|
@@ -3,6 +3,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.scanNovelMarkers = scanNovelMarkers;
|
|
4
4
|
exports.extractNovelAssets = extractNovelAssets;
|
|
5
5
|
exports.demo = demo;
|
|
6
|
+
exports.renderNovelMarkdown = renderNovelMarkdown;
|
|
7
|
+
const node_path_1 = require("node:path");
|
|
6
8
|
const linkRegex = /https?:\/\/\S+/;
|
|
7
9
|
function parseMarker(span) {
|
|
8
10
|
if (span.startsWith('[newpage]'))
|
|
@@ -119,4 +121,24 @@ function demo() {
|
|
|
119
121
|
assert(Array.isArray(scanNovelMarkers(s)) && scanNovelMarkers(s).length > 0, 'markers parsed');
|
|
120
122
|
}
|
|
121
123
|
}
|
|
124
|
+
/**
|
|
125
|
+
* Rich-media Markdown sidecar (RFC 1 Phase 2). Rebuilds the novel body with
|
|
126
|
+
* each downloaded inline resource as a relative `` reference
|
|
127
|
+
* (the `.txt` file remains the authoritative/compat format and its path is
|
|
128
|
+
* unchanged). Markers whose asset is `unavailable`/`failed` are left in the
|
|
129
|
+
* original text so nothing is silently dropped (partial success).
|
|
130
|
+
*/
|
|
131
|
+
function renderNovelMarkdown(source, assets) {
|
|
132
|
+
const downloaded = new Map(assets
|
|
133
|
+
.filter((a) => a.status === 'downloaded' && Boolean(a.localPath))
|
|
134
|
+
.map((a) => [a.marker, a]));
|
|
135
|
+
return scanNovelMarkers(source)
|
|
136
|
+
.map((marker) => {
|
|
137
|
+
if (marker.type === 'text')
|
|
138
|
+
return marker.value;
|
|
139
|
+
const asset = downloaded.get(marker.raw);
|
|
140
|
+
return asset ? `(asset.localPath)})` : marker.raw;
|
|
141
|
+
})
|
|
142
|
+
.join('');
|
|
143
|
+
}
|
|
122
144
|
//# sourceMappingURL=novelMarkers.js.map
|
package/dist/package.json
CHANGED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export interface ArchiveEntry {
|
|
2
|
+
/** Path the file is stored under inside the zip (e.g. "images/001.jpg"). */
|
|
3
|
+
name: string;
|
|
4
|
+
/** Absolute path of the file on disk. */
|
|
5
|
+
sourcePath: string;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Create a zip archive at `destPath` containing `entries` (order preserved).
|
|
9
|
+
* Returns destPath on success; on failure removes a partial destination so a
|
|
10
|
+
* caller never hands a half-written archive to a delivery. Pure FS helper — no
|
|
11
|
+
* domain logic.
|
|
12
|
+
*/
|
|
13
|
+
export declare function createZipArchive(destPath: string, entries: ArchiveEntry[]): Promise<string>;
|
|
14
|
+
//# sourceMappingURL=zip.d.ts.map
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createZipArchive = createZipArchive;
|
|
4
|
+
const node_fs_1 = require("node:fs");
|
|
5
|
+
const node_fs_2 = require("node:fs");
|
|
6
|
+
const node_module_1 = require("node:module");
|
|
7
|
+
// archiver ships no matching TS factory types; bind it at runtime via
|
|
8
|
+
// createRequire and keep a minimal local contract (nothing Pixiv-specific).
|
|
9
|
+
const archiver = (0, node_module_1.createRequire)(__filename)('archiver');
|
|
10
|
+
/**
|
|
11
|
+
* Create a zip archive at `destPath` containing `entries` (order preserved).
|
|
12
|
+
* Returns destPath on success; on failure removes a partial destination so a
|
|
13
|
+
* caller never hands a half-written archive to a delivery. Pure FS helper — no
|
|
14
|
+
* domain logic.
|
|
15
|
+
*/
|
|
16
|
+
async function createZipArchive(destPath, entries) {
|
|
17
|
+
const output = (0, node_fs_1.createWriteStream)(destPath);
|
|
18
|
+
const archive = archiver('zip', { zlib: { level: 9 } });
|
|
19
|
+
const settled = new Promise((resolve, reject) => {
|
|
20
|
+
output.on('close', () => resolve(destPath));
|
|
21
|
+
output.on('error', reject);
|
|
22
|
+
archive.on('error', reject);
|
|
23
|
+
});
|
|
24
|
+
try {
|
|
25
|
+
archive.pipe(output);
|
|
26
|
+
for (const e of entries) {
|
|
27
|
+
archive.file(e.sourcePath, { name: e.name });
|
|
28
|
+
}
|
|
29
|
+
await archive.finalize();
|
|
30
|
+
await settled;
|
|
31
|
+
await node_fs_2.promises.access(destPath);
|
|
32
|
+
return destPath;
|
|
33
|
+
}
|
|
34
|
+
catch (error) {
|
|
35
|
+
try {
|
|
36
|
+
await node_fs_2.promises.rm(destPath, { force: true });
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
/* best-effort cleanup */
|
|
40
|
+
}
|
|
41
|
+
throw new Error(`Failed to create zip archive: ${error instanceof Error ? error.message : String(error)}`);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
//# sourceMappingURL=zip.js.map
|
package/dist/version.js
CHANGED
|
@@ -2,5 +2,5 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.BUILD = void 0;
|
|
4
4
|
// GENERATED by scripts/write-version.js — do not edit manually.
|
|
5
|
-
exports.BUILD = { version: '2.
|
|
5
|
+
exports.BUILD = { version: '2.26.0', commit: '299b0e29cecb' };
|
|
6
6
|
//# sourceMappingURL=version.js.map
|
package/dist/webui/package.json
CHANGED
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pixivflow",
|
|
3
|
-
"version": "2.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "2.26.0",
|
|
4
|
+
"description": "🎨 Pixiv 下载、筛选与自动收集工具 - 批量下载插画和小说、按标签/热度/日期筛选、定时任务与可靠 HTTP 交付 | Pixiv downloader and automation toolkit with filtering, scheduling and reliable HTTP delivery",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
7
7
|
"url": "git+https://github.com/redtidev1918/PixivFlow.git"
|
|
@@ -116,6 +116,7 @@
|
|
|
116
116
|
},
|
|
117
117
|
"dependencies": {
|
|
118
118
|
"@redtidev/pixiv-client": "0.1.0",
|
|
119
|
+
"archiver": "^7.0.1",
|
|
119
120
|
"axios": "^1.20.0",
|
|
120
121
|
"cors": "^2.8.6",
|
|
121
122
|
"cron-parser": "^4.9.0",
|