@anyul/koishi-plugin-rss 5.2.47 → 5.3.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/README.md +33 -2
- package/lib/commands/index.d.ts +32 -0
- package/lib/commands/index.js +361 -218
- package/lib/commands/subscription-create.d.ts +18 -5
- package/lib/commands/subscription-create.js +40 -28
- package/lib/commands/subscription-edit.js +5 -5
- package/lib/commands/subscription-management.js +9 -9
- package/lib/commands/web-monitor.d.ts +20 -1
- package/lib/commands/web-monitor.js +21 -19
- package/lib/config.js +1 -1
- package/lib/constants.d.ts +1 -1
- package/lib/constants.js +2 -6
- package/lib/core/telegram-video-restore.d.ts +6 -0
- package/lib/core/telegram-video-restore.js +86 -24
- package/lib/tsconfig.tsbuildinfo +1 -1
- package/lib/utils/message-cache.d.ts +9 -0
- package/lib/utils/message-cache.js +36 -21
- package/lib/utils/tdl.d.ts +19 -5
- package/lib/utils/tdl.js +49 -31
- package/package.json +1 -1
|
@@ -70,6 +70,10 @@ export declare class MessageCacheManager {
|
|
|
70
70
|
}): Promise<CacheStats>;
|
|
71
71
|
/**
|
|
72
72
|
* 清理缓存
|
|
73
|
+
*
|
|
74
|
+
* 注意:Koishi 的 database.remove 接受查询条件对象,会批量删除所有匹配行。
|
|
75
|
+
* 这里先查出保留部分的边界 id,再用 `< 边界` 的条件一次批量删除,
|
|
76
|
+
* 避免逐条 remove 造成的 N 次往返与中途失败留下半清理状态。
|
|
73
77
|
*/
|
|
74
78
|
cleanup(options?: {
|
|
75
79
|
guildId?: string;
|
|
@@ -84,6 +88,11 @@ export declare class MessageCacheManager {
|
|
|
84
88
|
guildId?: string;
|
|
85
89
|
platform?: string;
|
|
86
90
|
}): Promise<number>;
|
|
91
|
+
/**
|
|
92
|
+
* 批量删除辅助:先按 where 统计条数,再一次 remove 删除全部匹配行。
|
|
93
|
+
* 用于 clearAll / cleanup,避免逐条往返与整表入内存。
|
|
94
|
+
*/
|
|
95
|
+
private batchRemove;
|
|
87
96
|
/**
|
|
88
97
|
* 检查并自动清理
|
|
89
98
|
*/
|
|
@@ -122,6 +122,10 @@ class MessageCacheManager {
|
|
|
122
122
|
}
|
|
123
123
|
/**
|
|
124
124
|
* 清理缓存
|
|
125
|
+
*
|
|
126
|
+
* 注意:Koishi 的 database.remove 接受查询条件对象,会批量删除所有匹配行。
|
|
127
|
+
* 这里先查出保留部分的边界 id,再用 `< 边界` 的条件一次批量删除,
|
|
128
|
+
* 避免逐条 remove 造成的 N 次往返与中途失败留下半清理状态。
|
|
125
129
|
*/
|
|
126
130
|
async cleanup(options = {}) {
|
|
127
131
|
const { guildId, rssId, platform, keepLatest = this.maxCacheSize } = options;
|
|
@@ -133,21 +137,22 @@ class MessageCacheManager {
|
|
|
133
137
|
where.rssId = rssId;
|
|
134
138
|
if (platform)
|
|
135
139
|
where.platform = platform;
|
|
136
|
-
//
|
|
137
|
-
const
|
|
138
|
-
sort: { createdAt: 'desc' }
|
|
140
|
+
// 只查到"要保留的最新 N 条"对应的边界,无需把整表拉进内存
|
|
141
|
+
const keepBoundary = await this.ctx.database.get('rss_message_cache', where, {
|
|
142
|
+
sort: { createdAt: 'desc' },
|
|
143
|
+
limit: 1,
|
|
144
|
+
offset: Math.max(0, keepLatest - 1),
|
|
139
145
|
});
|
|
140
|
-
//
|
|
141
|
-
if (
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
return 0;
|
|
146
|
+
// 没有边界行,说明总量 ≤ keepLatest,无需清理
|
|
147
|
+
if (keepBoundary.length === 0)
|
|
148
|
+
return 0;
|
|
149
|
+
const boundaryCreatedAt = keepBoundary[0].createdAt;
|
|
150
|
+
// 删除比边界更旧(含同时间戳但靠后)的消息:createdAt < 边界
|
|
151
|
+
const deleteWhere = { ...where, createdAt: { $lt: boundaryCreatedAt } };
|
|
152
|
+
const removed = await this.batchRemove(deleteWhere);
|
|
153
|
+
if (removed > 0)
|
|
154
|
+
logger.info(`清理了 ${removed} 条缓存消息`);
|
|
155
|
+
return removed;
|
|
151
156
|
}
|
|
152
157
|
/**
|
|
153
158
|
* 清空所有缓存
|
|
@@ -160,14 +165,24 @@ class MessageCacheManager {
|
|
|
160
165
|
where.guildId = guildId;
|
|
161
166
|
if (platform)
|
|
162
167
|
where.platform = platform;
|
|
163
|
-
|
|
168
|
+
const removed = await this.batchRemove(where);
|
|
169
|
+
if (removed > 0)
|
|
170
|
+
logger.info(`清空了 ${removed} 条缓存消息`);
|
|
171
|
+
return removed;
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* 批量删除辅助:先按 where 统计条数,再一次 remove 删除全部匹配行。
|
|
175
|
+
* 用于 clearAll / cleanup,避免逐条往返与整表入内存。
|
|
176
|
+
*/
|
|
177
|
+
async batchRemove(where) {
|
|
178
|
+
// Koishi database.get 不直接支持 count,先取一次总数
|
|
164
179
|
const messages = await this.ctx.database.get('rss_message_cache', where);
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
return
|
|
180
|
+
const count = messages.length;
|
|
181
|
+
if (count === 0)
|
|
182
|
+
return 0;
|
|
183
|
+
// remove 接受查询条件,会删除所有匹配行(批量语义)
|
|
184
|
+
await this.ctx.database.remove('rss_message_cache', where);
|
|
185
|
+
return count;
|
|
171
186
|
}
|
|
172
187
|
/**
|
|
173
188
|
* 检查并自动清理
|
package/lib/utils/tdl.d.ts
CHANGED
|
@@ -80,10 +80,23 @@ export declare function detectVideoTooBig(html: any): boolean;
|
|
|
80
80
|
/**
|
|
81
81
|
* 从 cheerio 文档里提取 "Video is too big" 占位 blockquote 内的海报图地址。
|
|
82
82
|
*
|
|
83
|
+
* 多视频 album 时,返回**第一个**匹配 blockquote 的 poster(向后兼容)。
|
|
84
|
+
* 如需全部,用 extractTooBigPosters。
|
|
85
|
+
*
|
|
83
86
|
* @param html cheerio 实例
|
|
84
87
|
* @returns 找到则返回图片 URL,否则空串
|
|
85
88
|
*/
|
|
86
89
|
export declare function extractTooBigPoster(html: any): string;
|
|
90
|
+
/**
|
|
91
|
+
* 提取所有 "Video is too big" 占位 blockquote 的海报图地址,按文档出现顺序返回。
|
|
92
|
+
*
|
|
93
|
+
* 多视频 album 场景:3 个 blockquote 各带一张 poster,
|
|
94
|
+
* 返回顺序与 blockquote 顺序一致,用于按位置配对注入视频。
|
|
95
|
+
*
|
|
96
|
+
* @param html cheerio 实例
|
|
97
|
+
* @returns poster URL 数组(无图位置返回空串占位,长度 = too-big blockquote 数)
|
|
98
|
+
*/
|
|
99
|
+
export declare function extractTooBigPosters(html: any): string[];
|
|
87
100
|
export interface DownloadWithTdlOptions {
|
|
88
101
|
config: Config;
|
|
89
102
|
/** Telegram 消息链接,如 https://t.me/anyul996/28 */
|
|
@@ -96,17 +109,18 @@ export interface DownloadWithTdlOptions {
|
|
|
96
109
|
workDir?: string;
|
|
97
110
|
}
|
|
98
111
|
/**
|
|
99
|
-
* 调用 tdl
|
|
112
|
+
* 调用 tdl 下载 Telegram 消息的视频(支持多视频 album)。
|
|
100
113
|
*
|
|
101
|
-
* 命令形态:`tdl [--storage ...] [--proxy ...] dl -u <link> -d <tmpDir
|
|
102
|
-
* (全局 flag 必须在子命令 dl
|
|
114
|
+
* 命令形态:`tdl [--storage ...] [--proxy ...] dl -u <link> -d <tmpDir> --group`
|
|
115
|
+
* (全局 flag 必须在子命令 dl 之前;--group 自动检测相册/分组消息,下载全部媒体;
|
|
116
|
+
* 对单视频无副作用——自动检测到 1 项)
|
|
103
117
|
*
|
|
104
118
|
* 代理与会话解析优先级:
|
|
105
119
|
* - `--proxy`:config.tdl.proxy(专用)→ 订阅级代理(proxyByEnv != false)
|
|
106
120
|
* - `--storage`:config.tdl.storage(登录与下载必须一致,否则找不到会话)
|
|
107
121
|
*
|
|
108
|
-
* @returns
|
|
122
|
+
* @returns 成功返回视频绝对路径数组(按文件名排序 = 专辑顺序);二进制缺失/未登录/超时/无产物时返回 null
|
|
109
123
|
*/
|
|
110
|
-
export declare function downloadWithTdl(opts: DownloadWithTdlOptions): Promise<string | null>;
|
|
124
|
+
export declare function downloadWithTdl(opts: DownloadWithTdlOptions): Promise<string[] | null>;
|
|
111
125
|
/** 静默删除目录,失败不抛错 */
|
|
112
126
|
export declare function safeRemoveDir(dir: string): Promise<void>;
|
package/lib/utils/tdl.js
CHANGED
|
@@ -52,6 +52,7 @@ exports._resetBinaryCacheForTest = _resetBinaryCacheForTest;
|
|
|
52
52
|
exports.parseTelegramLink = parseTelegramLink;
|
|
53
53
|
exports.detectVideoTooBig = detectVideoTooBig;
|
|
54
54
|
exports.extractTooBigPoster = extractTooBigPoster;
|
|
55
|
+
exports.extractTooBigPosters = extractTooBigPosters;
|
|
55
56
|
exports.downloadWithTdl = downloadWithTdl;
|
|
56
57
|
exports.safeRemoveDir = safeRemoveDir;
|
|
57
58
|
const child_process_1 = require("child_process");
|
|
@@ -262,42 +263,55 @@ function detectVideoTooBig(html) {
|
|
|
262
263
|
/**
|
|
263
264
|
* 从 cheerio 文档里提取 "Video is too big" 占位 blockquote 内的海报图地址。
|
|
264
265
|
*
|
|
266
|
+
* 多视频 album 时,返回**第一个**匹配 blockquote 的 poster(向后兼容)。
|
|
267
|
+
* 如需全部,用 extractTooBigPosters。
|
|
268
|
+
*
|
|
265
269
|
* @param html cheerio 实例
|
|
266
270
|
* @returns 找到则返回图片 URL,否则空串
|
|
267
271
|
*/
|
|
268
272
|
function extractTooBigPoster(html) {
|
|
273
|
+
return extractTooBigPosters(html)[0] || '';
|
|
274
|
+
}
|
|
275
|
+
/**
|
|
276
|
+
* 提取所有 "Video is too big" 占位 blockquote 的海报图地址,按文档出现顺序返回。
|
|
277
|
+
*
|
|
278
|
+
* 多视频 album 场景:3 个 blockquote 各带一张 poster,
|
|
279
|
+
* 返回顺序与 blockquote 顺序一致,用于按位置配对注入视频。
|
|
280
|
+
*
|
|
281
|
+
* @param html cheerio 实例
|
|
282
|
+
* @returns poster URL 数组(无图位置返回空串占位,长度 = too-big blockquote 数)
|
|
283
|
+
*/
|
|
284
|
+
function extractTooBigPosters(html) {
|
|
269
285
|
if (!html)
|
|
270
|
-
return
|
|
286
|
+
return [];
|
|
287
|
+
const posters = [];
|
|
271
288
|
try {
|
|
272
|
-
let poster = '';
|
|
273
289
|
html('blockquote').each((_, el) => {
|
|
274
|
-
if (poster)
|
|
275
|
-
return;
|
|
276
290
|
const $el = html(el);
|
|
277
291
|
const text = $el.text() || '';
|
|
278
292
|
if (VIDEO_TOO_BIG_PATTERN.test(text)) {
|
|
279
293
|
const img = $el.find('img').first().attr('src') || '';
|
|
280
|
-
|
|
281
|
-
poster = img;
|
|
294
|
+
posters.push(img);
|
|
282
295
|
}
|
|
283
296
|
});
|
|
284
|
-
return poster;
|
|
285
297
|
}
|
|
286
298
|
catch {
|
|
287
|
-
return
|
|
299
|
+
return [];
|
|
288
300
|
}
|
|
301
|
+
return posters;
|
|
289
302
|
}
|
|
290
303
|
/**
|
|
291
|
-
* 调用 tdl
|
|
304
|
+
* 调用 tdl 下载 Telegram 消息的视频(支持多视频 album)。
|
|
292
305
|
*
|
|
293
|
-
* 命令形态:`tdl [--storage ...] [--proxy ...] dl -u <link> -d <tmpDir
|
|
294
|
-
* (全局 flag 必须在子命令 dl
|
|
306
|
+
* 命令形态:`tdl [--storage ...] [--proxy ...] dl -u <link> -d <tmpDir> --group`
|
|
307
|
+
* (全局 flag 必须在子命令 dl 之前;--group 自动检测相册/分组消息,下载全部媒体;
|
|
308
|
+
* 对单视频无副作用——自动检测到 1 项)
|
|
295
309
|
*
|
|
296
310
|
* 代理与会话解析优先级:
|
|
297
311
|
* - `--proxy`:config.tdl.proxy(专用)→ 订阅级代理(proxyByEnv != false)
|
|
298
312
|
* - `--storage`:config.tdl.storage(登录与下载必须一致,否则找不到会话)
|
|
299
313
|
*
|
|
300
|
-
* @returns
|
|
314
|
+
* @returns 成功返回视频绝对路径数组(按文件名排序 = 专辑顺序);二进制缺失/未登录/超时/无产物时返回 null
|
|
301
315
|
*/
|
|
302
316
|
async function downloadWithTdl(opts) {
|
|
303
317
|
const { config, link, proxyAgent } = opts;
|
|
@@ -332,7 +346,7 @@ async function downloadWithTdl(opts) {
|
|
|
332
346
|
args.push('--proxy', proxyUrl);
|
|
333
347
|
(0, logger_1.debug)(config, `tdl 使用代理: ${maskProxyUrl(proxyUrl)}`, 'tdl', 'details');
|
|
334
348
|
}
|
|
335
|
-
args.push('dl', '-u', link, '-d', workDir);
|
|
349
|
+
args.push('dl', '-u', link, '-d', workDir, '--group');
|
|
336
350
|
const label = tdlBin === 'tdl' ? 'tdl' : tdlBin;
|
|
337
351
|
(0, logger_1.debug)(config, `调用 tdl 下载: ${label} ${args.join(' ')}(超时 ${timeoutSeconds}s)`, 'tdl', 'info');
|
|
338
352
|
try {
|
|
@@ -352,23 +366,24 @@ async function downloadWithTdl(opts) {
|
|
|
352
366
|
await safeRemoveDir(workDir);
|
|
353
367
|
return null;
|
|
354
368
|
}
|
|
355
|
-
// 5.
|
|
356
|
-
const
|
|
357
|
-
if (
|
|
369
|
+
// 5. 扫描产物,收集全部视频文件(按文件名排序,对齐专辑顺序)
|
|
370
|
+
const videoPaths = pickVideoFiles(workDir);
|
|
371
|
+
if (videoPaths.length === 0) {
|
|
358
372
|
(0, logger_1.debug)(config, `tdl 下载完成但目录内未发现视频文件: ${workDir}`, 'tdl', 'info');
|
|
359
373
|
await safeRemoveDir(workDir);
|
|
360
374
|
return null;
|
|
361
375
|
}
|
|
362
|
-
(0, logger_1.debug)(config, `tdl
|
|
363
|
-
return
|
|
376
|
+
(0, logger_1.debug)(config, `tdl 下载成功(${videoPaths.length} 个视频): ${videoPaths.join(', ')}`, 'tdl', 'info');
|
|
377
|
+
return videoPaths;
|
|
364
378
|
}
|
|
365
|
-
/**
|
|
366
|
-
*
|
|
367
|
-
*
|
|
368
|
-
|
|
369
|
-
|
|
379
|
+
/** 在目录中挑选全部视频文件,按文件名升序排列。
|
|
380
|
+
*
|
|
381
|
+
* tdl --group 下文件名形如 `{dialogID}_{msgID}_{name}.mp4`,msgID 递增 = 专辑顺序,
|
|
382
|
+
* 按文件名排序即可对齐 RSS 描述里 blockquote 的出现顺序。
|
|
383
|
+
* 过滤掉 tdl 同时下载的配图(.jpg)、描述(.json);跳过 0 字节半成品。 */
|
|
384
|
+
function pickVideoFiles(dir) {
|
|
370
385
|
const videoExts = new Set(['.mp4', '.mov', '.webm', '.mkv', '.m4v']);
|
|
371
|
-
|
|
386
|
+
const found = [];
|
|
372
387
|
try {
|
|
373
388
|
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
374
389
|
for (const entry of entries) {
|
|
@@ -377,21 +392,24 @@ function pickLargestVideo(dir) {
|
|
|
377
392
|
const ext = path.extname(entry.name).toLowerCase();
|
|
378
393
|
if (!videoExts.has(ext))
|
|
379
394
|
continue;
|
|
380
|
-
|
|
395
|
+
// 校验非空文件(跳过 0 字节半成品)
|
|
381
396
|
try {
|
|
382
|
-
const stat = fs.statSync(
|
|
383
|
-
if (
|
|
384
|
-
|
|
397
|
+
const stat = fs.statSync(path.join(dir, entry.name));
|
|
398
|
+
if (stat.size === 0)
|
|
399
|
+
continue;
|
|
385
400
|
}
|
|
386
401
|
catch {
|
|
387
|
-
|
|
402
|
+
continue;
|
|
388
403
|
}
|
|
404
|
+
found.push({ path: path.join(dir, entry.name), name: entry.name });
|
|
389
405
|
}
|
|
390
406
|
}
|
|
391
407
|
catch {
|
|
392
|
-
return
|
|
408
|
+
return [];
|
|
393
409
|
}
|
|
394
|
-
|
|
410
|
+
// 按文件名升序,保证多视频与 blockquote 顺序一致
|
|
411
|
+
found.sort((a, b) => a.name.localeCompare(b.name, 'en'));
|
|
412
|
+
return found.map(f => f.path);
|
|
395
413
|
}
|
|
396
414
|
/** 静默删除目录,失败不抛错 */
|
|
397
415
|
async function safeRemoveDir(dir) {
|
package/package.json
CHANGED