pixivflow 2.25.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;
@@ -188,9 +189,11 @@ class NovelDownloader {
188
189
  // Rich-media markdown sidecar (RFC 1 Phase 2): same path as the .txt
189
190
  // (compat format stays), inline images become relative ![](images/x.jpg)
190
191
  // refs so a later TelePress/TelePost phase can render the Telegraph page.
192
+ let richMediaPath;
191
193
  if (assets.some((a) => a.status === 'downloaded')) {
192
194
  try {
193
195
  const mdPath = await this.fileService.saveText(`${header}\n${(0, novelMarkers_1.renderNovelMarkdown)(text, assets)}`, fileName.replace(/\.txt$/, '.md'), metadata);
196
+ richMediaPath = mdPath;
194
197
  logger_1.logger.info(`Saved novel ${detail.id} rich-media markdown sidecar`, { filePath: mdPath });
195
198
  }
196
199
  catch (error) {
@@ -243,6 +246,33 @@ class NovelDownloader {
243
246
  catch (error) {
244
247
  logger_1.logger.warn(`Failed to save metadata for novel ${detail.id}: ${error instanceof Error ? error.message : String(error)}`);
245
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
+ }
246
276
  this.database.insertDownload({
247
277
  pixivId: String(detail.id),
248
278
  type: 'novel',
@@ -264,7 +294,7 @@ class NovelDownloader {
264
294
  type: 'novel',
265
295
  title: detail.title,
266
296
  tags: tags.map((item) => item.name).filter(Boolean),
267
- files: [filePath],
297
+ files: archivePath ? [filePath, archivePath] : [filePath],
268
298
  cleanupFiles: metadataPath ? [metadataPath] : [],
269
299
  spoiler: (detail.x_restrict ?? 0) > 0,
270
300
  xRestrict: detail.x_restrict,
package/dist/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "type": "commonjs",
3
3
  "name": "pixivflow",
4
- "version": "2.25.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.25.0', commit: '532ba3534889' };
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.25.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.25.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",