pixivflow 2.39.0 → 2.41.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.
@@ -771,6 +771,13 @@ export interface StandaloneConfig {
771
771
  * terminalises immediately). Default: 3.
772
772
  */
773
773
  maxFallbackStages?: number;
774
+ /**
775
+ * Novel media materialization policy. 'eager' downloads inline images for
776
+ * the preview path (legacy); 'on-demand' keeps MediaReference
777
+ * (assetId/sourceUrl) and defers local files to explicit ZIP/archive.
778
+ * Default: 'eager'
779
+ */
780
+ materializationPolicy?: 'eager' | 'on-demand';
774
781
  };
775
782
  }
776
783
  //# sourceMappingURL=types.d.ts.map
@@ -40,6 +40,7 @@ const fs = __importStar(require("node:fs"));
40
40
  const path = __importStar(require("node:path"));
41
41
  const node_crypto_1 = require("node:crypto");
42
42
  const MediaAsset_1 = require("../domain/media/MediaAsset");
43
+ const novelMarkers_1 = require("../download/novelMarkers");
43
44
  const logger_1 = require("../logger");
44
45
  const redact_1 = require("../utils/redact");
45
46
  /**
@@ -59,6 +60,26 @@ function interpolateEnv(value) {
59
60
  function isDownloadedPixivAsset(asset) {
60
61
  return Boolean(asset && asset.status === 'downloaded' && asset.url && asset.localPath);
61
62
  }
63
+ function isPendingPixivAsset(asset) {
64
+ return Boolean(asset && asset.status === 'pending' && asset.url && asset.sourceId && !asset.localPath);
65
+ }
66
+ /** Build a canonical MediaAsset without requiring a local file. */
67
+ function mediaAssetFrom(asset, workId, localPath) {
68
+ const kind = asset.kind === 'uploadedimage' || asset.kind === 'pixivimage'
69
+ ? asset.kind
70
+ : undefined;
71
+ const source = String(asset.url);
72
+ if (!source || !kind)
73
+ return undefined;
74
+ return (0, MediaAsset_1.buildMediaAsset)({
75
+ workId,
76
+ kind,
77
+ sourceId: asset.sourceId ? String(asset.sourceId) : undefined,
78
+ marker: asset.marker ? String(asset.marker) : undefined,
79
+ sourceUrl: source,
80
+ artifactId: localPath,
81
+ });
82
+ }
62
83
  /**
63
84
  * Read the novel metadata sidecar into canonical `MediaAsset` values, plus the
64
85
  * legacy `{local, source}` manifest projection (local is only a render hint for
@@ -76,24 +97,14 @@ function readNovelMediaAssets(artifact) {
76
97
  const workId = meta.pixiv_id ? String(meta.pixiv_id) : artifact.pixivId;
77
98
  const mediaAssets = [];
78
99
  const manifest = [];
79
- for (const asset of meta.assets) {
80
- if (!isDownloadedPixivAsset(asset))
81
- continue;
82
- const kind = asset.kind === 'uploadedimage' || asset.kind === 'pixivimage'
83
- ? asset.kind
84
- : undefined;
100
+ const downloaded = meta.assets.filter(isDownloadedPixivAsset);
101
+ for (const asset of downloaded) {
85
102
  const source = String(asset.url);
86
103
  const localPath = String(asset.localPath);
87
- if (!source || !localPath || !kind)
104
+ const media = mediaAssetFrom(asset, workId, localPath);
105
+ if (!media)
88
106
  continue;
89
- mediaAssets.push((0, MediaAsset_1.buildMediaAsset)({
90
- workId,
91
- kind,
92
- sourceId: asset.sourceId ? String(asset.sourceId) : undefined,
93
- marker: asset.marker ? String(asset.marker) : undefined,
94
- sourceUrl: source,
95
- artifactId: localPath,
96
- }));
107
+ mediaAssets.push(media);
97
108
  manifest.push({
98
109
  local: `images/${path.basename(localPath)}`,
99
110
  source,
@@ -101,6 +112,25 @@ function readNovelMediaAssets(artifact) {
101
112
  sourceUrl: source,
102
113
  });
103
114
  }
115
+ // On-demand previews: no local downloads for this work, but the metadata
116
+ // carries resolvable Pixiv CDN sources. Pass those through as pending
117
+ // references (no artifactId) so TelePress can proxy them without files.
118
+ if (mediaAssets.length === 0) {
119
+ for (const asset of meta.assets.filter(isPendingPixivAsset)) {
120
+ const source = String(asset.url);
121
+ const refName = (0, novelMarkers_1.novelReferenceFileName)(source, String(asset.sourceId));
122
+ const media = mediaAssetFrom(asset, workId);
123
+ if (!media)
124
+ continue;
125
+ mediaAssets.push(media);
126
+ manifest.push({
127
+ local: refName,
128
+ source,
129
+ assetId: mediaAssets[mediaAssets.length - 1]?.id,
130
+ sourceUrl: source,
131
+ });
132
+ }
133
+ }
104
134
  return { mediaAssets, manifest };
105
135
  }
106
136
  catch {
@@ -145,7 +175,7 @@ async function publishRichNovelPreview(artifact, options) {
145
175
  const sources = findRichNovelSources(artifact);
146
176
  if (!sources)
147
177
  return { url: '', retryable: false, operatorHint: 'no_rich_novel_assets' };
148
- if (sources.imagePaths.length === 0) {
178
+ if (sources.imagePaths.length === 0 && sources.manifest.length === 0) {
149
179
  return { url: '', retryable: false, operatorHint: 'no_rich_novel_assets' };
150
180
  }
151
181
  const mdName = path.basename(sources.mdPath);
@@ -5,6 +5,7 @@ const logger_1 = require("../logger");
5
5
  const RankingService_1 = require("./RankingService");
6
6
  const IllustrationDownloader_1 = require("./IllustrationDownloader");
7
7
  const NovelDownloader_1 = require("./NovelDownloader");
8
+ const MaterializationPolicy_1 = require("../domain/media/MaterializationPolicy");
8
9
  const ProgressReporter_1 = require("./report/ProgressReporter");
9
10
  const DownloadPlanner_1 = require("./plan/DownloadPlanner");
10
11
  const DownloadExecutor_1 = require("./exec/DownloadExecutor");
@@ -125,7 +126,10 @@ class DownloadManager {
125
126
  const downloadConcurrency = config.download?.concurrency || 3;
126
127
  const storagePath = config.storage?.illustrationDirectory ?? config.storage?.downloadDirectory ?? './downloads';
127
128
  this.illustrationDownloader = new IllustrationDownloader_1.IllustrationDownloader(client, database, fileService, downloadConcurrency, storagePath);
128
- this.novelDownloader = new NovelDownloader_1.NovelDownloader(client, database, fileService, database);
129
+ const materialization = config.download?.materializationPolicy
130
+ ? { mode: config.download?.materializationPolicy }
131
+ : MaterializationPolicy_1.DEFAULT_MATERIALIZATION_POLICY;
132
+ this.novelDownloader = new NovelDownloader_1.NovelDownloader(client, database, fileService, database, undefined, materialization);
129
133
  this.planner = new DownloadPlanner_1.DownloadPlanner(database, {
130
134
  deliveredIds: (target, type, ids) => this.deliveryService.deliveredIds(target, type, ids),
131
135
  // CANDIDATE SELECTION dedupe: also treats a work whose review submission is
@@ -230,9 +230,16 @@ class NovelDownloader {
230
230
  // (compat format stays), inline images become relative ![](images/x.jpg)
231
231
  // refs so a later TelePress/TelePost phase can render the Telegraph page.
232
232
  let richMediaPath;
233
- if (assets.some((a) => a.status === 'downloaded')) {
233
+ const hasDownloadedImages = assets.some((a) => a.status === 'downloaded');
234
+ // On-demand mode (no local images) still emits a resolvable md sidecar from
235
+ // media references so TelePress can render via proxy without a download.
236
+ const hasResolvablePending = assets.some((a) => a.status === 'pending' && Boolean(a.url));
237
+ if (hasDownloadedImages || hasResolvablePending) {
234
238
  try {
235
- const mdPath = await this.fileService.saveText(`${header}\n${(0, novelMarkers_1.renderNovelMarkdown)(text, assets)}`, fileName.replace(/\.txt$/, '.md'), metadata);
239
+ const mdContent = hasDownloadedImages
240
+ ? (0, novelMarkers_1.renderNovelMarkdown)(text, assets)
241
+ : (0, novelMarkers_1.renderNovelMarkdownReference)(text, assets);
242
+ const mdPath = await this.fileService.saveText(`${header}\n${mdContent}`, fileName.replace(/\.txt$/, '.md'), metadata);
236
243
  richMediaPath = mdPath;
237
244
  logger_1.logger.info(`Saved novel ${detail.id} rich-media markdown sidecar`, { filePath: mdPath });
238
245
  }
@@ -75,4 +75,18 @@ export declare function demo(): void;
75
75
  * original text so nothing is silently dropped (partial success).
76
76
  */
77
77
  export declare function renderNovelMarkdown(source: string, assets: NovelAsset[]): string;
78
+ /**
79
+ * Stable relative filename for an inline novel media reference without a local
80
+ * download. Uses the sourceId plus the URL extension so the manifest can point
81
+ * at a proxy URL while the rendered Markdown still resolves to
82
+ * `images/<sourceId>.jpg`.
83
+ */
84
+ export declare function novelReferenceFileName(url: string | undefined, sourceId: string): string;
85
+ /**
86
+ * Markdown sidecar for on-demand (non-downloading) previews. Renders
87
+ * `![](images/<ref>)` for every resolvable asset so the manifest can be
88
+ * rehydrated by TelePress via proxy; markers without a resolvable URL stay as
89
+ * their original text.
90
+ */
91
+ export declare function renderNovelMarkdownReference(source: string, assets: NovelAsset[]): string;
78
92
  //# sourceMappingURL=novelMarkers.d.ts.map
@@ -4,6 +4,8 @@ exports.scanNovelMarkers = scanNovelMarkers;
4
4
  exports.extractNovelAssets = extractNovelAssets;
5
5
  exports.demo = demo;
6
6
  exports.renderNovelMarkdown = renderNovelMarkdown;
7
+ exports.novelReferenceFileName = novelReferenceFileName;
8
+ exports.renderNovelMarkdownReference = renderNovelMarkdownReference;
7
9
  const node_path_1 = require("node:path");
8
10
  const linkRegex = /https?:\/\/\S+/;
9
11
  function parseMarker(span) {
@@ -141,4 +143,50 @@ function renderNovelMarkdown(source, assets) {
141
143
  })
142
144
  .join('');
143
145
  }
146
+ /**
147
+ * Stable relative filename for an inline novel media reference without a local
148
+ * download. Uses the sourceId plus the URL extension so the manifest can point
149
+ * at a proxy URL while the rendered Markdown still resolves to
150
+ * `images/<sourceId>.jpg`.
151
+ */
152
+ function novelReferenceFileName(url, sourceId) {
153
+ let ext = '';
154
+ if (url) {
155
+ try {
156
+ const path = new URL(url).pathname;
157
+ const match = /\.(jpg|jpeg|png|gif|webp)$/i.exec(path);
158
+ if (match)
159
+ ext = match[1].toLowerCase();
160
+ }
161
+ catch {
162
+ // non-URL fall through to default ext
163
+ }
164
+ }
165
+ const safeId = sourceId.replace(/[^\w.-]+/g, '_');
166
+ return `images/${safeId}${ext ? `.${ext}` : '.jpg'}`;
167
+ }
168
+ /**
169
+ * Markdown sidecar for on-demand (non-downloading) previews. Renders
170
+ * `![](images/<ref>)` for every resolvable asset so the manifest can be
171
+ * rehydrated by TelePress via proxy; markers without a resolvable URL stay as
172
+ * their original text.
173
+ */
174
+ function renderNovelMarkdownReference(source, assets) {
175
+ return scanNovelMarkers(source)
176
+ .map((marker) => {
177
+ if (marker.type === 'text')
178
+ return marker.value;
179
+ const asset = assets.find((a) => a.marker === marker.raw);
180
+ if (!asset)
181
+ return marker.raw;
182
+ if (asset.status === 'downloaded' && asset.localPath) {
183
+ return `![](images/${(0, node_path_1.basename)(asset.localPath)})`;
184
+ }
185
+ if (asset.status === 'pending' && asset.url) {
186
+ return `![](${novelReferenceFileName(asset.url, asset.sourceId)})`;
187
+ }
188
+ return marker.raw;
189
+ })
190
+ .join('');
191
+ }
144
192
  //# 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.39.0",
4
+ "version": "2.41.0",
5
5
  "private": true
6
6
  }
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.39.0', commit: '6cee00e9c813' };
5
+ exports.BUILD = { version: '2.41.0', commit: '471ff53f25c9' };
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.39.0",
4
+ "version": "2.41.0",
5
5
  "description": "PixivFlow WebUI Backend - CommonJS module"
6
6
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pixivflow",
3
- "version": "2.39.0",
3
+ "version": "2.41.0",
4
4
  "description": "🎨 Pixiv 下载、筛选与自动收集工具 - 批量下载插画和小说、按标签/热度/日期筛选、定时任务与可靠 HTTP 交付 | Pixiv downloader and automation toolkit with filtering, scheduling and reliable HTTP delivery",
5
5
  "repository": {
6
6
  "type": "git",