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.
@@ -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 ![](images/x.jpg)
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 `![](images/xxx.jpg)` 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 `![](images/xxx.jpg)` 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 ? `![](images/${(0, node_path_1.basename)(asset.localPath)})` : marker.raw;
141
+ })
142
+ .join('');
143
+ }
122
144
  //# sourceMappingURL=novelMarkers.js.map
package/dist/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "type": "commonjs",
3
3
  "name": "pixivflow",
4
- "version": "2.24.0",
4
+ "version": "2.26.0",
5
5
  "private": true
6
6
  }
@@ -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.24.0', commit: '8c7fa0a4b74b' };
5
+ exports.BUILD = { version: '2.26.0', commit: '299b0e29cecb' };
6
6
  //# sourceMappingURL=version.js.map
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "type": "commonjs",
3
3
  "name": "pixivflow-webui-backend",
4
- "version": "2.24.0",
4
+ "version": "2.26.0",
5
5
  "description": "PixivFlow WebUI Backend - CommonJS module"
6
6
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "pixivflow",
3
- "version": "2.24.0",
4
- "description": "\ud83c\udfa8 Pixiv \u4e0b\u8f7d\u3001\u7b5b\u9009\u4e0e\u81ea\u52a8\u6536\u96c6\u5de5\u5177 - \u6279\u91cf\u4e0b\u8f7d\u63d2\u753b\u548c\u5c0f\u8bf4\u3001\u6309\u6807\u7b7e/\u70ed\u5ea6/\u65e5\u671f\u7b5b\u9009\u3001\u5b9a\u65f6\u4efb\u52a1\u4e0e\u53ef\u9760 HTTP \u4ea4\u4ed8 | Pixiv downloader and automation toolkit with filtering, scheduling and reliable HTTP delivery",
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",