karin-plugin-kkk 2.23.1 → 2.24.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.
@@ -1,6 +1,6 @@
1
1
  import { n as __esmMin, o as __toESM, r as __export } from "./rolldown-runtime-BMXAG3ag.js";
2
2
  import { A as init_locale, An as init_zod, Cn as Chalk, Dn as axios_default, En as init_axios, On as Xhshow, Sn as require_protobufjs, Tn as AxiosError$1, _n as require_png, a as Window, bn as require_heic_decode, dt as init_date_fns, ft as fromUnixTime, ht as differenceInSeconds, i as init_lib, j as zhCN, jn as zod_default, kn as init_dist, mt as format, n as require_lib, pt as formatDistanceToNow, r as require_qr_code_styling, t as require_dist, vn as require_jsQR, wn as init_source, xn as require_express, yn as require_jpeg_js } from "./vendor-DxfKHvj-.js";
3
- import { n as init_client, r as reactServerRender } from "./template-CnW8M_F0.js";
3
+ import { n as init_client, r as reactServerRender } from "./template-DekmxKd7.js";
4
4
  import { createRequire } from "node:module";
5
5
  import karin$1, { BOT_CONNECT, app, authMiddleware, checkPkgUpdate, checkPort, common, components, config, copyConfigSync, createBadRequestResponse, createNotFoundResponse, createServerErrorResponse, createSuccessResponse, db, defineConfig, ffmpeg, ffprobe, filesByExt, getBot, hooks, karin, karinPathHtml, karinPathTemp, logger, logs, mkdirSync, range, render, requireFileSync, restart, segment, updatePkg, watch } from "node-karin";
6
6
  import fs from "node:fs";
@@ -6201,8 +6201,8 @@ var init_Base = __esmMin(() => {
6201
6201
  await karin$1.sendMsg(selfId, contact, message2);
6202
6202
  }
6203
6203
  if (options) options.useGroupFile = Config.upload.usegroupfile && newFileSize > Config.upload.groupfilevalue;
6204
- if (Config.upload.sendbase64 && !options?.useGroupFile) {
6205
- File = `base64://${(await fs.promises.readFile(file.filepath)).toString("base64")}`;
6204
+ if (Config.upload.videoSendMode === "base64" && !options?.useGroupFile) {
6205
+ File = `base64://${fs.readFileSync(file.filepath).toString("base64")}`;
6206
6206
  logger.mark(`已开启视频文件 base64转换 正在进行${logger.yellow("base64转换中")}...`);
6207
6207
  } else File = options?.useGroupFile ? file.filepath : `file://${file.filepath}`;
6208
6208
  try {
@@ -6273,7 +6273,7 @@ var init_Base = __esmMin(() => {
6273
6273
  const { filepath, totalBytes } = await new Network({
6274
6274
  url: videoUrl,
6275
6275
  headers: opt.headers ?? BASE_HEADERS,
6276
- filepath: Common.tempDri.video + opt.title,
6276
+ filepath: opt.filepath ?? Common.tempDri.video + opt.title,
6277
6277
  timeout: 6e4,
6278
6278
  maxRetries: 3,
6279
6279
  throttle: throttleConfig
@@ -6294,7 +6294,7 @@ var init_Base = __esmMin(() => {
6294
6294
  const formattedRemainingTime = remainingTime > 60 ? `${Math.floor(remainingTime / 60)}min ${Math.floor(remainingTime % 60)}s` : `${remainingTime.toFixed(0)}s`;
6295
6295
  const downloadedSizeMB = (downloadedBytes / 1048576).toFixed(1);
6296
6296
  const totalSizeMB = (totalBytes$1 / 1048576).toFixed(1);
6297
- console.log(`⬇️ ${opt.title} ${generateProgressBar(progressPercentage)} ${coloredPercentage} ${downloadedSizeMB}/${totalSizeMB} MB | ${formattedSpeed} 剩余: ${formattedRemainingTime}\r`);
6297
+ console.log(`⬇️ ${opt.title ?? (opt.filepath && opt.filepath.split("/").pop()) ?? "未知文件"} ${generateProgressBar(progressPercentage)} ${coloredPercentage} ${downloadedSizeMB}/${totalSizeMB} MB | ${formattedSpeed} 剩余: ${formattedRemainingTime}\r`);
6298
6298
  });
6299
6299
  return {
6300
6300
  filepath,
@@ -7111,705 +7111,6 @@ var init_EmojiReaction = __esmMin(() => {
7111
7111
  }
7112
7112
  };
7113
7113
  });
7114
- async function detectEncoder$1(codec) {
7115
- if (cachedEncoders$1[codec]) return cachedEncoders$1[codec];
7116
- logger.debug(`[BiliDanmaku] 开始检测 ${codec.toUpperCase()} 编码器...`);
7117
- for (const encoder of ENCODER_PRIORITY$1[codec]) {
7118
- logger.debug(`[BiliDanmaku] 测试编码器: ${encoder}`);
7119
- try {
7120
- const result = await ffmpeg(`-f lavfi -i color=c=black:s=320x240:d=0.1 -c:v ${encoder} -f null -`);
7121
- logger.debug(`[BiliDanmaku] ${encoder} 测试结果: status=${result.status}`);
7122
- if (result.status) {
7123
- cachedEncoders$1[codec] = encoder;
7124
- logger.info(`[BiliDanmaku] 使用 ${codec.toUpperCase()} 编码器: ${encoder}`);
7125
- return encoder;
7126
- }
7127
- } catch (e) {
7128
- logger.debug(`[BiliDanmaku] 编码器 ${encoder} 测试异常: ${e}`);
7129
- }
7130
- }
7131
- const fallback = SOFTWARE_FALLBACK$1[codec];
7132
- cachedEncoders$1[codec] = fallback;
7133
- logger.info(`[BiliDanmaku] 回退到软件编码器: ${fallback}`);
7134
- return fallback;
7135
- }
7136
- async function getVideoBitrate$1(path$1) {
7137
- try {
7138
- const fileSize = fs.statSync(path$1).size;
7139
- const { stdout } = await ffprobe(`-v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 "${path$1}"`);
7140
- const duration = parseFloat(stdout.trim());
7141
- if (duration > 0 && fileSize > 0) {
7142
- const kbps = Math.round(fileSize * 8 / duration / 1e3);
7143
- logger.debug(`[BiliDanmaku] 通过文件大小计算码率: ${kbps}kbps`);
7144
- return kbps;
7145
- }
7146
- } catch (e) {
7147
- logger.debug(`[BiliDanmaku] 通过文件大小计算码率失败: ${e}`);
7148
- }
7149
- try {
7150
- const { stdout } = await ffprobe(`-v error -select_streams v:0 -show_entries stream=bit_rate -of default=noprint_wrappers=1:nokey=1 "${path$1}"`);
7151
- const bitrate = parseInt(stdout.trim());
7152
- if (bitrate > 0) return Math.round(bitrate / 1e3);
7153
- } catch {}
7154
- try {
7155
- const { stdout } = await ffprobe(`-v error -show_entries format=bit_rate -of default=noprint_wrappers=1:nokey=1 "${path$1}"`);
7156
- const bitrate = parseInt(stdout.trim());
7157
- if (bitrate > 0) return Math.round(bitrate / 1e3);
7158
- } catch {}
7159
- logger.warn("[BiliDanmaku] 无法获取视频码率,将使用 CRF 模式");
7160
- return 0;
7161
- }
7162
- function getEncoderParams$1(encoder, targetBitrate) {
7163
- const threads = Math.max(1, Math.floor(os.cpus().length / 2));
7164
- if (targetBitrate && targetBitrate > 0) {
7165
- const bitrateK = `${targetBitrate}k`;
7166
- const maxrate = `${Math.round(targetBitrate * 2)}k`;
7167
- const bufsize = `${Math.round(targetBitrate * 4)}k`;
7168
- if (encoder === "h264_nvenc") return `-c:v h264_nvenc -preset p4 -rc vbr -b:v ${bitrateK} -maxrate ${maxrate} -bufsize ${bufsize}`;
7169
- if (encoder === "h264_qsv") return `-c:v h264_qsv -preset medium -b:v ${bitrateK} -maxrate ${maxrate} -bufsize ${bufsize}`;
7170
- if (encoder === "h264_amf") return `-c:v h264_amf -quality balanced -rc vbr_peak -b:v ${bitrateK} -maxrate ${maxrate}`;
7171
- if (encoder === "libx264") return `-c:v libx264 -preset medium -b:v ${bitrateK} -maxrate ${maxrate} -bufsize ${bufsize} -threads ${threads}`;
7172
- if (encoder === "hevc_nvenc") return `-c:v hevc_nvenc -preset p4 -rc vbr -b:v ${bitrateK} -maxrate ${maxrate} -bufsize ${bufsize}`;
7173
- if (encoder === "hevc_qsv") return `-c:v hevc_qsv -preset medium -b:v ${bitrateK} -maxrate ${maxrate} -bufsize ${bufsize}`;
7174
- if (encoder === "hevc_amf") return `-c:v hevc_amf -quality balanced -rc vbr_peak -b:v ${bitrateK} -maxrate ${maxrate}`;
7175
- if (encoder === "libx265") return `-c:v libx265 -preset medium -b:v ${bitrateK} -maxrate ${maxrate} -bufsize ${bufsize} -threads ${threads}`;
7176
- if (encoder === "av1_nvenc") return `-c:v av1_nvenc -preset p4 -rc vbr -b:v ${bitrateK} -maxrate ${maxrate} -bufsize ${bufsize}`;
7177
- if (encoder === "av1_qsv") return `-c:v av1_qsv -preset medium -b:v ${bitrateK} -maxrate ${maxrate} -bufsize ${bufsize}`;
7178
- if (encoder === "av1_amf") return `-c:v av1_amf -quality balanced -rc vbr_peak -b:v ${bitrateK} -maxrate ${maxrate}`;
7179
- if (encoder === "libsvtav1") return `-c:v libsvtav1 -preset 6 -b:v ${bitrateK} -maxrate ${maxrate} -bufsize ${bufsize} -threads ${threads}`;
7180
- if (encoder === "libaom-av1") return `-c:v libaom-av1 -cpu-used 4 -b:v ${bitrateK} -maxrate ${maxrate} -bufsize ${bufsize} -threads ${threads}`;
7181
- return `-c:v libx265 -preset medium -b:v ${bitrateK} -maxrate ${maxrate} -bufsize ${bufsize} -threads ${threads}`;
7182
- }
7183
- if (encoder === "h264_nvenc") return "-c:v h264_nvenc -preset p4 -rc vbr -cq 23";
7184
- if (encoder === "h264_qsv") return "-c:v h264_qsv -preset medium -global_quality 23";
7185
- if (encoder === "h264_amf") return "-c:v h264_amf -quality balanced -rc cqp -qp_i 23 -qp_p 23";
7186
- if (encoder === "libx264") return `-c:v libx264 -crf 23 -preset medium -threads ${threads}`;
7187
- if (encoder === "hevc_nvenc") return "-c:v hevc_nvenc -preset p4 -rc vbr -cq 28";
7188
- if (encoder === "hevc_qsv") return "-c:v hevc_qsv -preset medium -global_quality 28";
7189
- if (encoder === "hevc_amf") return "-c:v hevc_amf -quality balanced -rc cqp -qp_i 28 -qp_p 28";
7190
- if (encoder === "libx265") return `-c:v libx265 -crf 28 -preset medium -threads ${threads}`;
7191
- if (encoder === "av1_nvenc") return "-c:v av1_nvenc -preset p4 -rc vbr -cq 30";
7192
- if (encoder === "av1_qsv") return "-c:v av1_qsv -preset medium -global_quality 30";
7193
- if (encoder === "av1_amf") return "-c:v av1_amf -quality balanced -rc cqp -qp_i 30 -qp_p 30";
7194
- if (encoder === "libsvtav1") return `-c:v libsvtav1 -crf 30 -preset 6 -threads ${threads}`;
7195
- if (encoder === "libaom-av1") return `-c:v libaom-av1 -crf 30 -cpu-used 4 -threads ${threads}`;
7196
- return `-c:v libx265 -crf 28 -preset medium -threads ${threads}`;
7197
- }
7198
- async function getBiliResolution(path$1) {
7199
- try {
7200
- const { stdout } = await ffprobe(`-v error -select_streams v:0 -show_entries stream=width,height -of csv=s=x:p=0 "${path$1}"`);
7201
- const [w, h] = stdout.trim().split("x").map(Number);
7202
- if (w && h) return {
7203
- width: w,
7204
- height: h
7205
- };
7206
- } catch {}
7207
- try {
7208
- const match = ((await ffmpeg(`-i "${path$1}" -f null -`, { timeout: 5e3 })).stderr || "").match(/(\d{3,4})x(\d{3,4})/);
7209
- if (match) return {
7210
- width: parseInt(match[1]),
7211
- height: parseInt(match[2])
7212
- };
7213
- } catch {}
7214
- return {
7215
- width: 1920,
7216
- height: 1080
7217
- };
7218
- }
7219
- async function getBiliFrameRate(path$1) {
7220
- try {
7221
- const { stdout } = await ffprobe(`-v error -select_streams v:0 -show_entries stream=r_frame_rate -of default=noprint_wrappers=1:nokey=1 "${path$1}"`);
7222
- const [num, den] = stdout.trim().split("/").map(Number);
7223
- if (den > 0) return num / den;
7224
- } catch {}
7225
- try {
7226
- const stderr = (await ffmpeg(`-i "${path$1}" -f null -`, { timeout: 5e3 })).stderr || "";
7227
- const fpsMatch = stderr.match(/(\d+(?:\.\d+)?)\s*fps/);
7228
- if (fpsMatch) return parseFloat(fpsMatch[1]);
7229
- const fracMatch = stderr.match(/(\d+)\/(\d+)\s*fps/);
7230
- if (fracMatch) return parseInt(fracMatch[1]) / parseInt(fracMatch[2]);
7231
- } catch {}
7232
- return 30;
7233
- }
7234
- function generateBiliASS(danmakuList, width, height, options = {}) {
7235
- const { scrollTime = 8, danmakuOpacity = 70, fontName = "Microsoft YaHei", danmakuArea = .5, danmakuFontSize = "medium" } = options;
7236
- const fontScale = height / 1080;
7237
- const sizeConfig = FONT_SIZE_MAP$1[danmakuFontSize];
7238
- const fontSize = Math.round(sizeConfig.base * fontScale);
7239
- const trackH = Math.round(sizeConfig.trackH * fontScale);
7240
- const topMargin = Math.round(10 * fontScale);
7241
- const bottomMargin = Math.round(10 * fontScale);
7242
- const areaHeight = Math.floor(height * danmakuArea) - topMargin - bottomMargin;
7243
- const trackCount = Math.max(1, Math.floor((areaHeight - fontSize) / trackH));
7244
- const fixedTrackCount = trackCount;
7245
- const minGap = Math.round(10 * fontScale);
7246
- const alpha = Math.round((100 - Math.max(0, Math.min(100, danmakuOpacity))) * 2.55).toString(16).padStart(2, "0").toUpperCase();
7247
- let ass = `[Script Info]\nTitle: Bilibili Danmaku\nScriptType: v4.00+\nPlayResX: ${width}\nPlayResY: ${height}\nTimer: 100.0000\n\n[V4+ Styles]\nFormat: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding\nStyle: Scroll,${fontName},${fontSize},&H${alpha}FFFFFF,&H${alpha}FFFFFF,&H${alpha}000000,&H${alpha}000000,0,0,0,0,100,100,0,0,1,1,0,2,0,0,0,1\nStyle: Top,${fontName},${fontSize},&H${alpha}FFFFFF,&H${alpha}FFFFFF,&H${alpha}000000,&H${alpha}000000,0,0,0,0,100,100,0,0,1,1,0,8,0,0,0,1\nStyle: Bottom,${fontName},${fontSize},&H${alpha}FFFFFF,&H${alpha}FFFFFF,&H${alpha}000000,&H${alpha}000000,0,0,0,0,100,100,0,0,1,1,0,2,0,0,0,1\n\n[Events]\nFormat: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text\n`;
7248
- const scrollTracks = Array(trackCount).fill(null);
7249
- const topTracks = Array(fixedTrackCount).fill(0);
7250
- const bottomTracks = Array(fixedTrackCount).fill(0);
7251
- const calcDistance = (last, startTime, duration, textWidth) => {
7252
- const lastSpeed = (width + last.textWidth) / last.duration;
7253
- const newSpeed = (width + textWidth) / duration;
7254
- let dist = width - (width - lastSpeed * (startTime - last.startTime) + last.textWidth) - minGap;
7255
- if (newSpeed > lastSpeed) {
7256
- const lastRightXAtEnd = width - lastSpeed * (startTime + duration - last.startTime) + last.textWidth;
7257
- dist = Math.min(dist, -textWidth - lastRightXAtEnd - minGap);
7258
- }
7259
- return dist;
7260
- };
7261
- const sorted = [...danmakuList].sort((a, b) => a.progress - b.progress);
7262
- for (const dm of sorted) {
7263
- if (dm.mode > 5 || !dm.content.trim()) continue;
7264
- const startTime = dm.progress;
7265
- const dmSizeRatio = (dm.fontsize || 25) / 25;
7266
- const dmFontSize = Math.round(fontSize * dmSizeRatio);
7267
- const textWidth = estimateWidth$1(dm.content, dmFontSize);
7268
- const content = escapeASS$1(dm.content);
7269
- const colorTag = dm.color !== 16777215 ? `{\\c&H${toASSColor(dm.color)}&}` : "";
7270
- const sizeTag = dmFontSize !== fontSize ? `{\\fs${dmFontSize}}` : "";
7271
- if (dm.mode === 4) {
7272
- const endTime = startTime + 4e3;
7273
- let idx = bottomTracks.findIndex((t) => t <= startTime);
7274
- if (idx === -1) idx = Math.floor(Math.random() * bottomTracks.length);
7275
- bottomTracks[idx] = endTime;
7276
- const y = height - bottomMargin - idx * trackH;
7277
- ass += `Dialogue: 0,${toASSTime$1(startTime)},${toASSTime$1(endTime)},Bottom,,0,0,0,,{\\an2}${colorTag}${sizeTag}{\\pos(${width / 2},${y})}${content}\n`;
7278
- } else if (dm.mode === 5) {
7279
- const endTime = startTime + 4e3;
7280
- let idx = topTracks.findIndex((t) => t <= startTime);
7281
- if (idx === -1) idx = Math.floor(Math.random() * topTracks.length);
7282
- topTracks[idx] = endTime;
7283
- const y = topMargin + idx * trackH + fontSize;
7284
- ass += `Dialogue: 0,${toASSTime$1(startTime)},${toASSTime$1(endTime)},Top,,0,0,0,,{\\an8}${colorTag}${sizeTag}{\\pos(${width / 2},${y})}${content}\n`;
7285
- } else {
7286
- const duration = scrollTime * 1e3;
7287
- const endTime = startTime + duration;
7288
- for (let i = 0; i < scrollTracks.length; i++) {
7289
- const t = scrollTracks[i];
7290
- if (t && t.startTime + t.duration <= startTime) scrollTracks[i] = null;
7291
- }
7292
- let bestIdx = -1;
7293
- let bestDist = -Infinity;
7294
- for (let i = 0; i < scrollTracks.length; i++) {
7295
- const t = scrollTracks[i];
7296
- if (!t) {
7297
- if (bestIdx === -1) bestIdx = i;
7298
- continue;
7299
- }
7300
- const d = calcDistance(t, startTime, duration, textWidth);
7301
- if (d >= 0) {
7302
- if (bestDist < 0 || d < bestDist) {
7303
- bestDist = d;
7304
- bestIdx = i;
7305
- }
7306
- }
7307
- }
7308
- if (bestIdx === -1 || bestDist < 0 && scrollTracks[bestIdx] !== null) continue;
7309
- scrollTracks[bestIdx] = {
7310
- startTime,
7311
- duration,
7312
- textWidth
7313
- };
7314
- const y = (bestIdx + 1) * trackH;
7315
- ass += `Dialogue: 0,${toASSTime$1(startTime)},${toASSTime$1(endTime)},Scroll,,0,0,0,,{\\an7}${colorTag}${sizeTag}{\\move(${width},${y},${-textWidth},${y})}${content}\n`;
7316
- }
7317
- }
7318
- return ass;
7319
- }
7320
- function calcCanvas$1(origW, origH, verticalMode) {
7321
- if (verticalMode === "off") return {
7322
- width: origW,
7323
- height: origH,
7324
- offsetY: 0,
7325
- isVertical: false
7326
- };
7327
- const ratio = origW / origH;
7328
- const isWide = isLandscape$1(origW, origH);
7329
- if (verticalMode === "force") {
7330
- const targetRatio = 16 / 9;
7331
- if (isWide) {
7332
- const newW = Math.min(origH, MAX_OUTPUT_WIDTH$1);
7333
- const newH = Math.round(newW * targetRatio);
7334
- const scaledH = Math.round(newW / ratio);
7335
- return {
7336
- width: newW,
7337
- height: newH,
7338
- offsetY: Math.round((newH - scaledH) / 2),
7339
- isVertical: true,
7340
- scale: newW / origW
7341
- };
7342
- } else {
7343
- const newW = Math.min(origW, MAX_OUTPUT_WIDTH$1);
7344
- const scaleRatio = newW / origW;
7345
- const scaledOrigH = Math.round(origH * scaleRatio);
7346
- const newH = Math.round(newW * targetRatio);
7347
- const offsetY = Math.round((newH - scaledOrigH) / 2);
7348
- return {
7349
- width: newW,
7350
- height: newH,
7351
- offsetY: Math.max(0, offsetY),
7352
- isVertical: true,
7353
- scale: scaleRatio
7354
- };
7355
- }
7356
- }
7357
- if (isWide && ratio >= 1.7) {
7358
- const newW = Math.min(origH, MAX_OUTPUT_WIDTH$1);
7359
- const scaleRatio = newW / origH;
7360
- const newH = Math.round(origW * scaleRatio);
7361
- const scaledH = Math.round(newW / ratio);
7362
- return {
7363
- width: newW,
7364
- height: newH,
7365
- offsetY: Math.round((newH - scaledH) / 2),
7366
- isVertical: true,
7367
- scale: newW / origW
7368
- };
7369
- }
7370
- return {
7371
- width: origW,
7372
- height: origH,
7373
- offsetY: 0,
7374
- isVertical: false
7375
- };
7376
- }
7377
- function buildFilter$1(canvas, assPath) {
7378
- const escaped = escapeWinPath$1(assPath);
7379
- if (canvas.isVertical) {
7380
- if (canvas.scale && canvas.scale !== 1 && canvas.scale < 1) return `scale=${canvas.width}:-1,pad=${canvas.width}:${canvas.height}:0:${canvas.offsetY}:black,subtitles='${escaped}'`;
7381
- return `pad=${canvas.width}:${canvas.height}:0:${canvas.offsetY}:black,subtitles='${escaped}'`;
7382
- }
7383
- return `subtitles='${escaped}'`;
7384
- }
7385
- async function burnBiliDanmaku(videoPath, danmakuList, outputPath, options = {}) {
7386
- const { removeSource = false, verticalMode = "off", videoCodec = "h265" } = options;
7387
- const resolution = await getBiliResolution(videoPath);
7388
- const frameRate = await getBiliFrameRate(videoPath);
7389
- const sourceBitrate = await getVideoBitrate$1(videoPath);
7390
- const canvas = calcCanvas$1(resolution.width, resolution.height, verticalMode);
7391
- if (canvas.isVertical) logger.debug(`[BiliDanmaku] 竖屏模式: ${resolution.width}x${resolution.height} -> ${canvas.width}x${canvas.height}`);
7392
- const assContent = generateBiliASS(danmakuList, canvas.width, canvas.height, options);
7393
- const assPath = videoPath.replace(/\.[^.]+$/, "_danmaku.ass");
7394
- fs.writeFileSync(assPath, assContent, "utf-8");
7395
- logger.debug(`[BiliDanmaku] 弹幕字幕已生成: ${assPath}`);
7396
- const result = await ffmpeg(`-y -i "${videoPath}" -vf "${buildFilter$1(canvas, assPath)}" -r ${frameRate} ${getEncoderParams$1(await detectEncoder$1(videoCodec), sourceBitrate)} -c:a copy "${outputPath}"`);
7397
- Common.removeFile(assPath, true);
7398
- if (result.status) {
7399
- logger.mark(`[BiliDanmaku] 弹幕烧录成功: ${outputPath}`);
7400
- if (removeSource) Common.removeFile(videoPath);
7401
- } else logger.error("[BiliDanmaku] 弹幕烧录失败", result);
7402
- return result.status;
7403
- }
7404
- async function mergeAndBurnBili(videoPath, audioPath, danmakuList, outputPath, options = {}) {
7405
- const { removeSource = false, verticalMode = "off", videoCodec = "h265" } = options;
7406
- if (!fs.existsSync(videoPath)) {
7407
- logger.error(`[BiliDanmaku] 视频文件不存在: ${videoPath}`);
7408
- return false;
7409
- }
7410
- if (!fs.existsSync(audioPath)) {
7411
- logger.error(`[BiliDanmaku] 音频文件不存在: ${audioPath}`);
7412
- return false;
7413
- }
7414
- const resolution = await getBiliResolution(videoPath);
7415
- const frameRate = await getBiliFrameRate(videoPath);
7416
- const sourceBitrate = await getVideoBitrate$1(videoPath);
7417
- const canvas = calcCanvas$1(resolution.width, resolution.height, verticalMode);
7418
- if (canvas.isVertical) logger.debug(`[BiliDanmaku] 竖屏模式: ${resolution.width}x${resolution.height} -> ${canvas.width}x${canvas.height}`);
7419
- logger.debug(`[BiliDanmaku] 分辨率: ${canvas.width}x${canvas.height}, 帧率: ${frameRate}fps, 码率: ${sourceBitrate}kbps`);
7420
- const assContent = generateBiliASS(danmakuList, canvas.width, canvas.height, options);
7421
- const assPath = videoPath.replace(/\.[^.]+$/, "_danmaku.ass");
7422
- fs.writeFileSync(assPath, assContent, "utf-8");
7423
- logger.debug(`[BiliDanmaku] 弹幕字幕已生成: ${assPath},共 ${danmakuList.length} 条`);
7424
- const result = await ffmpeg(`-y -i "${videoPath}" -i "${audioPath}" -f mp4 -vf "${buildFilter$1(canvas, assPath)}" -r ${frameRate} ${getEncoderParams$1(await detectEncoder$1(videoCodec), sourceBitrate)} -c:a aac -b:a 192k "${outputPath}"`);
7425
- Common.removeFile(assPath, true);
7426
- if (result.status) {
7427
- logger.mark(`[BiliDanmaku] 视频合成+弹幕烧录成功: ${outputPath}`);
7428
- if (removeSource) {
7429
- Common.removeFile(videoPath);
7430
- Common.removeFile(audioPath);
7431
- }
7432
- } else logger.error("[BiliDanmaku] 视频合成+弹幕烧录失败", result);
7433
- return result.status;
7434
- }
7435
- var ENCODER_PRIORITY$1, SOFTWARE_FALLBACK$1, cachedEncoders$1, toASSColor, toASSTime$1, estimateWidth$1, escapeASS$1, escapeWinPath$1, isLandscape$1, FONT_SIZE_MAP$1, MAX_OUTPUT_WIDTH$1;
7436
- var init_danmaku$1 = __esmMin(async () => {
7437
- await init_utils$1();
7438
- ENCODER_PRIORITY$1 = {
7439
- h264: [
7440
- "h264_nvenc",
7441
- "h264_qsv",
7442
- "h264_amf",
7443
- "libx264"
7444
- ],
7445
- h265: [
7446
- "hevc_nvenc",
7447
- "hevc_qsv",
7448
- "hevc_amf",
7449
- "libx265"
7450
- ],
7451
- av1: [
7452
- "av1_nvenc",
7453
- "av1_qsv",
7454
- "av1_amf",
7455
- "libsvtav1",
7456
- "libaom-av1"
7457
- ]
7458
- };
7459
- SOFTWARE_FALLBACK$1 = {
7460
- h264: "libx264",
7461
- h265: "libx265",
7462
- av1: "libsvtav1"
7463
- };
7464
- cachedEncoders$1 = {};
7465
- toASSColor = (color) => {
7466
- const r = color >> 16 & 255;
7467
- const g = color >> 8 & 255;
7468
- return `${(color & 255).toString(16).padStart(2, "0")}${g.toString(16).padStart(2, "0")}${r.toString(16).padStart(2, "0")}`.toUpperCase();
7469
- };
7470
- toASSTime$1 = (ms) => {
7471
- const s = ms / 1e3;
7472
- const h = Math.floor(s / 3600);
7473
- const m = Math.floor(s % 3600 / 60);
7474
- const sec = Math.floor(s % 60);
7475
- const cs = Math.floor(s % 1 * 100);
7476
- return `${h}:${m.toString().padStart(2, "0")}:${sec.toString().padStart(2, "0")}.${cs.toString().padStart(2, "0")}`;
7477
- };
7478
- estimateWidth$1 = (text, fontSize) => {
7479
- let w = 0;
7480
- for (const c of text) w += c.charCodeAt(0) > 127 ? fontSize : fontSize * .5;
7481
- return w;
7482
- };
7483
- escapeASS$1 = (text) => text.replace(/\\/g, "\\\\").replace(/\{/g, "\\{").replace(/\}/g, "\\}").replace(/\n/g, "\\N");
7484
- escapeWinPath$1 = (path$1) => path$1.replace(/\\/g, "/").replace(/:/g, "\\:");
7485
- isLandscape$1 = (w, h) => w > h;
7486
- FONT_SIZE_MAP$1 = {
7487
- small: {
7488
- base: 25,
7489
- trackH: 30
7490
- },
7491
- medium: {
7492
- base: 32,
7493
- trackH: 38
7494
- },
7495
- large: {
7496
- base: 40,
7497
- trackH: 46
7498
- }
7499
- };
7500
- MAX_OUTPUT_WIDTH$1 = 2160;
7501
- });
7502
- async function detectEncoder(codec) {
7503
- if (cachedEncoders[codec]) return cachedEncoders[codec];
7504
- logger.debug(`[DouyinDanmaku] 开始检测 ${codec.toUpperCase()} 编码器...`);
7505
- for (const encoder of ENCODER_PRIORITY[codec]) try {
7506
- if ((await ffmpeg(`-f lavfi -i color=c=black:s=320x240:d=0.1 -c:v ${encoder} -f null -`)).status) {
7507
- cachedEncoders[codec] = encoder;
7508
- logger.info(`[DouyinDanmaku] 使用 ${codec.toUpperCase()} 编码器: ${encoder}`);
7509
- return encoder;
7510
- }
7511
- } catch {}
7512
- const fallback = SOFTWARE_FALLBACK[codec];
7513
- cachedEncoders[codec] = fallback;
7514
- logger.info(`[DouyinDanmaku] 回退到软件编码器: ${fallback}`);
7515
- return fallback;
7516
- }
7517
- async function getVideoBitrate(path$1) {
7518
- try {
7519
- const fileSize = fs.statSync(path$1).size;
7520
- const { stdout } = await ffprobe(`-v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 "${path$1}"`);
7521
- const duration = parseFloat(stdout.trim());
7522
- if (duration > 0 && fileSize > 0) return Math.round(fileSize * 8 / duration / 1e3);
7523
- } catch {}
7524
- try {
7525
- const { stdout } = await ffprobe(`-v error -select_streams v:0 -show_entries stream=bit_rate -of default=noprint_wrappers=1:nokey=1 "${path$1}"`);
7526
- const bitrate = parseInt(stdout.trim());
7527
- if (bitrate > 0) return Math.round(bitrate / 1e3);
7528
- } catch {}
7529
- return 0;
7530
- }
7531
- function getEncoderParams(encoder, targetBitrate) {
7532
- const threads = Math.max(1, Math.floor(os.cpus().length / 2));
7533
- if (targetBitrate && targetBitrate > 0) {
7534
- const adjustedBitrate = Math.round(targetBitrate * 1.4);
7535
- const bitrateK = `${adjustedBitrate}k`;
7536
- const maxrate = `${Math.round(adjustedBitrate * 2.5)}k`;
7537
- const bufsize = `${Math.round(adjustedBitrate * 4)}k`;
7538
- if (encoder === "h264_nvenc") return `-c:v h264_nvenc -preset p4 -rc vbr -b:v ${bitrateK} -maxrate ${maxrate} -bufsize ${bufsize}`;
7539
- if (encoder === "h264_qsv") return `-c:v h264_qsv -preset medium -b:v ${bitrateK} -maxrate ${maxrate} -bufsize ${bufsize}`;
7540
- if (encoder === "h264_amf") return `-c:v h264_amf -quality balanced -rc vbr_peak -b:v ${bitrateK} -maxrate ${maxrate}`;
7541
- if (encoder === "libx264") return `-c:v libx264 -preset medium -b:v ${bitrateK} -maxrate ${maxrate} -bufsize ${bufsize} -threads ${threads}`;
7542
- if (encoder === "hevc_nvenc") return `-c:v hevc_nvenc -preset p4 -rc vbr -b:v ${bitrateK} -maxrate ${maxrate} -bufsize ${bufsize}`;
7543
- if (encoder === "hevc_qsv") return `-c:v hevc_qsv -preset medium -b:v ${bitrateK} -maxrate ${maxrate} -bufsize ${bufsize}`;
7544
- if (encoder === "hevc_amf") return `-c:v hevc_amf -quality balanced -rc vbr_peak -b:v ${bitrateK} -maxrate ${maxrate}`;
7545
- if (encoder === "libx265") return `-c:v libx265 -preset medium -b:v ${bitrateK} -maxrate ${maxrate} -bufsize ${bufsize} -threads ${threads}`;
7546
- if (encoder === "av1_nvenc") return `-c:v av1_nvenc -preset p4 -rc vbr -b:v ${bitrateK} -maxrate ${maxrate} -bufsize ${bufsize}`;
7547
- if (encoder === "av1_qsv") return `-c:v av1_qsv -preset medium -b:v ${bitrateK} -maxrate ${maxrate} -bufsize ${bufsize}`;
7548
- if (encoder === "av1_amf") return `-c:v av1_amf -quality balanced -rc vbr_peak -b:v ${bitrateK} -maxrate ${maxrate}`;
7549
- if (encoder === "libsvtav1") return `-c:v libsvtav1 -preset 6 -b:v ${bitrateK} -maxrate ${maxrate} -bufsize ${bufsize} -threads ${threads}`;
7550
- if (encoder === "libaom-av1") return `-c:v libaom-av1 -cpu-used 4 -b:v ${bitrateK} -maxrate ${maxrate} -bufsize ${bufsize} -threads ${threads}`;
7551
- return `-c:v libx265 -preset medium -b:v ${bitrateK} -maxrate ${maxrate} -bufsize ${bufsize} -threads ${threads}`;
7552
- }
7553
- if (encoder === "h264_nvenc") return "-c:v h264_nvenc -preset p4 -rc vbr -cq 23";
7554
- if (encoder === "h264_qsv") return "-c:v h264_qsv -preset medium -global_quality 23";
7555
- if (encoder === "h264_amf") return "-c:v h264_amf -quality balanced -rc cqp -qp_i 23 -qp_p 23";
7556
- if (encoder === "libx264") return `-c:v libx264 -crf 23 -preset medium -threads ${threads}`;
7557
- if (encoder === "hevc_nvenc") return "-c:v hevc_nvenc -preset p4 -rc vbr -cq 28";
7558
- if (encoder === "hevc_qsv") return "-c:v hevc_qsv -preset medium -global_quality 28";
7559
- if (encoder === "hevc_amf") return "-c:v hevc_amf -quality balanced -rc cqp -qp_i 28 -qp_p 28";
7560
- if (encoder === "libx265") return `-c:v libx265 -crf 28 -preset medium -threads ${threads}`;
7561
- if (encoder === "av1_nvenc") return "-c:v av1_nvenc -preset p4 -rc vbr -cq 30";
7562
- if (encoder === "av1_qsv") return "-c:v av1_qsv -preset medium -global_quality 30";
7563
- if (encoder === "av1_amf") return "-c:v av1_amf -quality balanced -rc cqp -qp_i 30 -qp_p 30";
7564
- if (encoder === "libsvtav1") return `-c:v libsvtav1 -crf 30 -preset 6 -threads ${threads}`;
7565
- if (encoder === "libaom-av1") return `-c:v libaom-av1 -crf 30 -cpu-used 4 -threads ${threads}`;
7566
- return `-c:v libx265 -crf 28 -preset medium -threads ${threads}`;
7567
- }
7568
- async function getDouyinResolution(path$1) {
7569
- try {
7570
- const { stdout } = await ffprobe(`-v error -select_streams v:0 -show_entries stream=width,height -of csv=s=x:p=0 "${path$1}"`);
7571
- const [w, h] = stdout.trim().split("x").map(Number);
7572
- if (w && h) return {
7573
- width: w,
7574
- height: h
7575
- };
7576
- } catch {}
7577
- try {
7578
- const match = ((await ffmpeg(`-i "${path$1}" -f null -`, { timeout: 5e3 })).stderr || "").match(/(\d{3,4})x(\d{3,4})/);
7579
- if (match) return {
7580
- width: parseInt(match[1]),
7581
- height: parseInt(match[2])
7582
- };
7583
- } catch {}
7584
- return {
7585
- width: 1080,
7586
- height: 1920
7587
- };
7588
- }
7589
- async function getDouyinFrameRate(path$1) {
7590
- try {
7591
- const { stdout } = await ffprobe(`-v error -select_streams v:0 -show_entries stream=r_frame_rate -of default=noprint_wrappers=1:nokey=1 "${path$1}"`);
7592
- const [num, den] = stdout.trim().split("/").map(Number);
7593
- if (den > 0) return num / den;
7594
- } catch {}
7595
- try {
7596
- const fpsMatch = ((await ffmpeg(`-i "${path$1}" -f null -`, { timeout: 5e3 })).stderr || "").match(/(\d+(?:\.\d+)?)\s*fps/);
7597
- if (fpsMatch) return parseFloat(fpsMatch[1]);
7598
- } catch {}
7599
- return 30;
7600
- }
7601
- function generateDouyinASS(danmakuList, width, height, options = {}) {
7602
- const { scrollTime = 8, danmakuOpacity = 70, fontName = "Microsoft YaHei", danmakuArea = .5, danmakuFontSize = "medium" } = options;
7603
- const fontScale = height / 1080;
7604
- const sizeConfig = FONT_SIZE_MAP[danmakuFontSize];
7605
- const fontSize = Math.round(sizeConfig.base * fontScale);
7606
- const trackH = Math.round(sizeConfig.trackH * fontScale);
7607
- const topMargin = Math.round(5 * fontScale);
7608
- const areaHeight = Math.floor(height * danmakuArea) - topMargin;
7609
- const trackCount = Math.max(1, Math.floor((areaHeight - fontSize) / trackH));
7610
- const minGap = Math.round(15 * fontScale);
7611
- const alpha = Math.round((100 - Math.max(0, Math.min(100, danmakuOpacity))) * 2.55).toString(16).padStart(2, "0").toUpperCase();
7612
- let ass = `[Script Info]\nTitle: Douyin Danmaku\nScriptType: v4.00+\nPlayResX: ${width}\nPlayResY: ${height}\nTimer: 100.0000\n\n[V4+ Styles]\nFormat: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding\nStyle: Scroll,${fontName},${fontSize},&H${alpha}FFFFFF,&H${alpha}FFFFFF,&H${alpha}000000,&H${alpha}000000,0,0,0,0,100,100,0,0,1,0.8,0,2,0,0,0,1\n\n[Events]\nFormat: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text\n`;
7613
- const scrollTracks = Array(trackCount).fill(null);
7614
- const calcDistance = (last, startTime, duration, textWidth) => {
7615
- const lastSpeed = (width + last.textWidth) / last.duration;
7616
- const newSpeed = (width + textWidth) / duration;
7617
- let dist = width - (width - lastSpeed * (startTime - last.startTime) + last.textWidth) - minGap;
7618
- if (newSpeed > lastSpeed) {
7619
- const lastRightXAtEnd = width - lastSpeed * (startTime + duration - last.startTime) + last.textWidth;
7620
- dist = Math.min(dist, -textWidth - lastRightXAtEnd - minGap);
7621
- }
7622
- return dist;
7623
- };
7624
- const sorted = [...danmakuList.filter((dm) => dm.text && dm.text.trim())].sort((a, b) => a.offset_time - b.offset_time);
7625
- for (const dm of sorted) {
7626
- const startTime = dm.offset_time;
7627
- const textWidth = estimateWidth(dm.text, fontSize);
7628
- const content = escapeASS(dm.text);
7629
- const duration = scrollTime * 1e3;
7630
- const endTime = startTime + duration;
7631
- for (let i = 0; i < scrollTracks.length; i++) {
7632
- const t = scrollTracks[i];
7633
- if (t && t.startTime + t.duration <= startTime) scrollTracks[i] = null;
7634
- }
7635
- let bestIdx = -1;
7636
- let bestDist = -Infinity;
7637
- for (let i = 0; i < scrollTracks.length; i++) {
7638
- const t = scrollTracks[i];
7639
- if (!t) {
7640
- if (bestIdx === -1) bestIdx = i;
7641
- continue;
7642
- }
7643
- const d = calcDistance(t, startTime, duration, textWidth);
7644
- if (d >= 0) {
7645
- if (bestDist < 0 || d < bestDist) {
7646
- bestDist = d;
7647
- bestIdx = i;
7648
- }
7649
- }
7650
- }
7651
- if (bestIdx === -1 || bestDist < 0 && scrollTracks[bestIdx] !== null) continue;
7652
- scrollTracks[bestIdx] = {
7653
- startTime,
7654
- duration,
7655
- textWidth
7656
- };
7657
- const y = topMargin + bestIdx * trackH + fontSize;
7658
- ass += `Dialogue: 0,${toASSTime(startTime)},${toASSTime(endTime)},Scroll,,0,0,0,,{\\an7}{\\move(${width},${y},${-textWidth},${y})}${content}\n`;
7659
- }
7660
- return ass;
7661
- }
7662
- function calcCanvas(origW, origH, verticalMode) {
7663
- if (verticalMode === "off") return {
7664
- width: origW,
7665
- height: origH,
7666
- offsetY: 0,
7667
- isVertical: false
7668
- };
7669
- const ratio = origW / origH;
7670
- const isWide = isLandscape(origW, origH);
7671
- if (verticalMode === "force") {
7672
- const targetRatio = 16 / 9;
7673
- if (isWide) {
7674
- const newW = Math.min(origH, MAX_OUTPUT_WIDTH);
7675
- const newH = Math.round(newW * targetRatio);
7676
- const scaledH = Math.round(newW / ratio);
7677
- return {
7678
- width: newW,
7679
- height: newH,
7680
- offsetY: Math.round((newH - scaledH) / 2),
7681
- isVertical: true,
7682
- scale: newW / origW
7683
- };
7684
- } else {
7685
- const newW = Math.min(origW, MAX_OUTPUT_WIDTH);
7686
- const scaleRatio = newW / origW;
7687
- const scaledOrigH = Math.round(origH * scaleRatio);
7688
- const newH = Math.round(newW * targetRatio);
7689
- const offsetY = Math.round((newH - scaledOrigH) / 2);
7690
- return {
7691
- width: newW,
7692
- height: newH,
7693
- offsetY: Math.max(0, offsetY),
7694
- isVertical: true,
7695
- scale: scaleRatio
7696
- };
7697
- }
7698
- }
7699
- if (isWide && ratio >= 1.7) {
7700
- const newW = Math.min(origH, MAX_OUTPUT_WIDTH);
7701
- const scaleRatio = newW / origH;
7702
- const newH = Math.round(origW * scaleRatio);
7703
- const scaledH = Math.round(newW / ratio);
7704
- return {
7705
- width: newW,
7706
- height: newH,
7707
- offsetY: Math.round((newH - scaledH) / 2),
7708
- isVertical: true,
7709
- scale: newW / origW
7710
- };
7711
- }
7712
- return {
7713
- width: origW,
7714
- height: origH,
7715
- offsetY: 0,
7716
- isVertical: false
7717
- };
7718
- }
7719
- function buildFilter(canvas, assPath) {
7720
- const escaped = escapeWinPath(assPath);
7721
- if (canvas.isVertical) {
7722
- if (canvas.scale && canvas.scale !== 1 && canvas.scale < 1) return `scale=${canvas.width}:-1,pad=${canvas.width}:${canvas.height}:0:${canvas.offsetY}:black,subtitles='${escaped}'`;
7723
- return `pad=${canvas.width}:${canvas.height}:0:${canvas.offsetY}:black,subtitles='${escaped}'`;
7724
- }
7725
- return `subtitles='${escaped}'`;
7726
- }
7727
- async function burnDouyinDanmaku(videoPath, danmakuList, outputPath, options = {}) {
7728
- const { removeSource = false, verticalMode = "off", videoCodec = "h265" } = options;
7729
- if (!fs.existsSync(videoPath)) {
7730
- logger.error(`[DouyinDanmaku] 视频文件不存在: ${videoPath}`);
7731
- return false;
7732
- }
7733
- const resolution = await getDouyinResolution(videoPath);
7734
- const frameRate = await getDouyinFrameRate(videoPath);
7735
- const sourceBitrate = await getVideoBitrate(videoPath);
7736
- const canvas = calcCanvas(resolution.width, resolution.height, verticalMode);
7737
- if (canvas.isVertical) logger.debug(`[DouyinDanmaku] 竖屏模式: ${resolution.width}x${resolution.height} -> ${canvas.width}x${canvas.height}`);
7738
- logger.debug(`[DouyinDanmaku] 分辨率: ${canvas.width}x${canvas.height}, 帧率: ${frameRate}fps, 码率: ${sourceBitrate}kbps`);
7739
- const assContent = generateDouyinASS(danmakuList, canvas.width, canvas.height, options);
7740
- const assPath = videoPath.replace(/\.[^.]+$/, "_danmaku.ass");
7741
- fs.writeFileSync(assPath, assContent, "utf-8");
7742
- logger.debug(`[DouyinDanmaku] 弹幕字幕已生成: ${assPath},共 ${danmakuList.length} 条`);
7743
- const result = await ffmpeg(`-y -i "${videoPath}" -vf "${buildFilter(canvas, assPath)}" -r ${frameRate} ${getEncoderParams(await detectEncoder(videoCodec), sourceBitrate)} -c:a copy "${outputPath}"`);
7744
- Common.removeFile(assPath, true);
7745
- if (result.status) {
7746
- logger.mark(`[DouyinDanmaku] 弹幕烧录成功: ${outputPath}`);
7747
- if (removeSource) Common.removeFile(videoPath);
7748
- } else logger.error("[DouyinDanmaku] 弹幕烧录失败", result);
7749
- return result.status;
7750
- }
7751
- var ENCODER_PRIORITY, SOFTWARE_FALLBACK, cachedEncoders, toASSTime, estimateWidth, escapeASS, escapeWinPath, isLandscape, FONT_SIZE_MAP, MAX_OUTPUT_WIDTH;
7752
- var init_danmaku = __esmMin(async () => {
7753
- await init_utils$1();
7754
- ENCODER_PRIORITY = {
7755
- h264: [
7756
- "h264_nvenc",
7757
- "h264_qsv",
7758
- "h264_amf",
7759
- "libx264"
7760
- ],
7761
- h265: [
7762
- "hevc_nvenc",
7763
- "hevc_qsv",
7764
- "hevc_amf",
7765
- "libx265"
7766
- ],
7767
- av1: [
7768
- "av1_nvenc",
7769
- "av1_qsv",
7770
- "av1_amf",
7771
- "libsvtav1",
7772
- "libaom-av1"
7773
- ]
7774
- };
7775
- SOFTWARE_FALLBACK = {
7776
- h264: "libx264",
7777
- h265: "libx265",
7778
- av1: "libsvtav1"
7779
- };
7780
- cachedEncoders = {};
7781
- toASSTime = (ms) => {
7782
- const s = ms / 1e3;
7783
- const h = Math.floor(s / 3600);
7784
- const m = Math.floor(s % 3600 / 60);
7785
- const sec = Math.floor(s % 60);
7786
- const cs = Math.floor(s % 1 * 100);
7787
- return `${h}:${m.toString().padStart(2, "0")}:${sec.toString().padStart(2, "0")}.${cs.toString().padStart(2, "0")}`;
7788
- };
7789
- estimateWidth = (text, fontSize) => {
7790
- let w = 0;
7791
- for (const c of text) w += c.charCodeAt(0) > 127 ? fontSize : fontSize * .5;
7792
- return w;
7793
- };
7794
- escapeASS = (text) => text.replace(/\\/g, "\\\\").replace(/\{/g, "\\{").replace(/\}/g, "\\}").replace(/\n/g, "\\N");
7795
- escapeWinPath = (path$1) => path$1.replace(/\\/g, "/").replace(/:/g, "\\:");
7796
- isLandscape = (w, h) => w > h;
7797
- FONT_SIZE_MAP = {
7798
- small: {
7799
- base: 25,
7800
- trackH: 30
7801
- },
7802
- medium: {
7803
- base: 32,
7804
- trackH: 38
7805
- },
7806
- large: {
7807
- base: 40,
7808
- trackH: 46
7809
- }
7810
- };
7811
- MAX_OUTPUT_WIDTH = 2160;
7812
- });
7813
7114
  async function fixM4sFile(inputPath, outputPath) {
7814
7115
  const result = await ffmpeg(`-y -i "${inputPath}" -c copy -movflags +faststart "${outputPath}"`);
7815
7116
  if (result.status) logger.debug(`m4s 文件修复成功: ${outputPath}`);
@@ -7818,17 +7119,7 @@ async function fixM4sFile(inputPath, outputPath) {
7818
7119
  }
7819
7120
  async function getMediaDuration(path$1) {
7820
7121
  const { stdout } = await ffprobe(`-v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 "${path$1}"`);
7821
- return parseFloat(parseFloat(stdout.trim()).toFixed(2));
7822
- }
7823
- async function loopVideo(inputPath, outputPath, loopCount) {
7824
- if (loopCount <= 1) {
7825
- fs.copyFileSync(inputPath, outputPath);
7826
- return true;
7827
- }
7828
- const result = await ffmpeg(`-y -stream_loop ${loopCount - 1} -i "${inputPath}" -c copy "${outputPath}"`);
7829
- if (result.status) logger.mark(`视频重放成功: ${outputPath}`);
7830
- else logger.error("视频重放失败", result);
7831
- return result.status;
7122
+ return Number.parseFloat(stdout.trim());
7832
7123
  }
7833
7124
  async function mergeVideoAudio(videoPath, audioPath, resultPath) {
7834
7125
  const result = await ffmpeg(`-y -i "${videoPath}" -i "${audioPath}" -c copy "${resultPath}"`);
@@ -7845,48 +7136,104 @@ async function compressVideo(options) {
7845
7136
  } else logger.error(`视频压缩失败: ${inputPath}`, result);
7846
7137
  return result.status;
7847
7138
  }
7848
- async function createLiveImageContext(bgmPath) {
7849
- return {
7850
- bgmPath,
7851
- bgmDuration: await getMediaDuration(bgmPath),
7852
- usedDuration: 0
7853
- };
7854
- }
7855
- async function mergeLiveImageIndependent(options, bgmPath) {
7856
- const { videoPath, outputPath, loopCount = 3 } = options;
7857
- const result = await ffmpeg(`-y -stream_loop ${loopCount - 1} -i "${videoPath}" -i "${bgmPath}" -filter_complex "[0:v]setpts=N/FRAME_RATE/TB[v];[0:a][1:a]amix=inputs=2:duration=shortest:dropout_transition=3[aout]" -map "[v]" -map "[aout]" -c:v libx264 -c:a aac -b:a 192k -shortest "${outputPath}"`);
7858
- if (result.status) logger.mark(`Live 图合成成功: ${outputPath}`);
7859
- else logger.error("Live 图合成失败", result);
7860
- return result.status;
7861
- }
7862
- async function mergeLiveImageContinuous(options, context) {
7863
- const { videoPath, outputPath, loopCount = 3 } = options;
7864
- const { bgmPath, bgmDuration, usedDuration } = context;
7865
- const totalDuration = await getMediaDuration(videoPath) * loopCount;
7866
- const bgmStartTime = usedDuration % bgmDuration;
7867
- const remainingBgm = bgmDuration - bgmStartTime;
7868
- let inputArgs;
7869
- if (totalDuration <= remainingBgm) inputArgs = `-y -stream_loop ${loopCount - 1} -i "${videoPath}" -ss ${bgmStartTime} -i "${bgmPath}"`;
7870
- else {
7871
- const bgmLoopCount = Math.ceil(totalDuration / bgmDuration) + 1;
7872
- inputArgs = `-y -stream_loop ${loopCount - 1} -i "${videoPath}" -stream_loop ${bgmLoopCount} -ss ${bgmStartTime} -i "${bgmPath}"`;
7873
- }
7874
- const result = await ffmpeg(`${inputArgs} -filter_complex "[0:v]setpts=N/FRAME_RATE/TB[v];[0:a][1:a]amix=inputs=2:duration=first:dropout_transition=3[aout]" -map "[v]" -map "[aout]" -c:v libx264 -c:a aac -b:a 192k -shortest "${outputPath}"`);
7875
- const newUsedDuration = (usedDuration + totalDuration) % bgmDuration;
7876
- if (result.status) logger.mark(`Live 图连续合成成功: ${outputPath}`);
7877
- else logger.error("Live 图连续合成失败", result);
7878
- return {
7879
- success: result.status,
7880
- context: {
7881
- ...context,
7882
- usedDuration: newUsedDuration
7883
- }
7884
- };
7885
- }
7139
+ var getMediaFrameRate, loopVideoWithTransition;
7886
7140
  var init_FFmpeg = __esmMin(async () => {
7887
7141
  await init_utils$1();
7888
- await init_danmaku$1();
7889
- await init_danmaku();
7142
+ getMediaFrameRate = async (path$1) => {
7143
+ const { stdout } = await ffprobe(`-v error -select_streams v:0 -show_entries stream=avg_frame_rate -of default=noprint_wrappers=1:nokey=1 "${path$1}"`);
7144
+ const rate = stdout.trim();
7145
+ if (!rate) return 30;
7146
+ if (rate.includes("/")) {
7147
+ const [num, den] = rate.split("/", 2).map((value) => Number(value));
7148
+ if (!num || !den) return 30;
7149
+ return Math.round(num / den * 100) / 100;
7150
+ }
7151
+ const parsed = Number(rate);
7152
+ if (!parsed || Number.isNaN(parsed)) return 30;
7153
+ return Math.round(parsed * 100) / 100;
7154
+ };
7155
+ loopVideoWithTransition = async (options) => {
7156
+ const { inputPath, outputPath, loopCount, staticImagePath, transitionEnabled = true, bgmPath, mergeMode = "independent", context } = options;
7157
+ const { stdout: durationStdout } = await ffprobe(`-v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 "${inputPath}"`);
7158
+ const duration = Number(durationStdout.trim()) || 0;
7159
+ const videoFps = await getMediaFrameRate(inputPath);
7160
+ const fadeDuration = transitionEnabled ? Math.min(.5, Math.max(.12, duration * .18)) : 0;
7161
+ const staticDuration = transitionEnabled ? 2.5 : 0;
7162
+ const videoFadeOffset = transitionEnabled ? Math.max(0, duration - fadeDuration) : 0;
7163
+ let inputArgs = `-stream_loop ${Math.max(0, loopCount - 1)} -i "${inputPath}"`;
7164
+ let filterComplex = "[0:v]setpts=PTS-STARTPTS,format=yuv420p,setsar=1[outv]";
7165
+ let composedDuration = duration * Math.max(1, loopCount);
7166
+ if (transitionEnabled) {
7167
+ inputArgs = `-stream_loop ${Math.max(0, loopCount)} -i "${inputPath}" -loop 1 -i "${staticImagePath}"`;
7168
+ const splitLabels = Array.from({ length: loopCount }, (_$1, index) => `[vsplit${index}]`).join("");
7169
+ const stillSplitLabels = Array.from({ length: loopCount }, (_$1, index) => `[still${index}]`).join("");
7170
+ const filterParts = [
7171
+ `[0:v]setpts=PTS-STARTPTS,settb=1/1000,format=yuv420p,setsar=1,fps=${videoFps}[vbase]`,
7172
+ `[vbase]split=${loopCount}${splitLabels}`,
7173
+ `[1:v]setpts=PTS-STARTPTS,settb=1/1000,format=yuv420p,setsar=1,fps=${videoFps}[still_base]`,
7174
+ `[still_base]split=${loopCount}${stillSplitLabels}`
7175
+ ];
7176
+ for (let i = 0; i < loopCount; i += 1) {
7177
+ const start = Math.max(0, duration * i);
7178
+ filterParts.push(`[vsplit${i}]trim=start=${start}:duration=${duration},setpts=PTS-STARTPTS,settb=1/1000[v${i}]`);
7179
+ filterParts.push(`[still${i}][v${i}]scale2ref=iw:ih:flags=lanczos[s${i}raw][v${i}r]`);
7180
+ filterParts.push(`[s${i}raw]trim=duration=${staticDuration},setpts=PTS-STARTPTS,settb=1/1000[s${i}]`);
7181
+ }
7182
+ let lastLabel = "x_s0";
7183
+ composedDuration = duration;
7184
+ filterParts.push(`[v0r][s0]xfade=transition=fade:duration=${fadeDuration}:offset=${videoFadeOffset}[${lastLabel}]`);
7185
+ composedDuration = composedDuration + staticDuration - fadeDuration;
7186
+ for (let i = 1; i < loopCount; i += 1) {
7187
+ const toVideoLabel = `x_v${i}`;
7188
+ const toStillLabel = `x_s${i}`;
7189
+ const offsetToVideo = Math.max(0, composedDuration - fadeDuration);
7190
+ filterParts.push(`[${lastLabel}][v${i}r]xfade=transition=fade:duration=${fadeDuration}:offset=${offsetToVideo}[${toVideoLabel}]`);
7191
+ composedDuration = composedDuration + duration - fadeDuration;
7192
+ const offsetToStill = Math.max(0, composedDuration - fadeDuration);
7193
+ filterParts.push(`[${toVideoLabel}][s${i}]xfade=transition=fade:duration=${fadeDuration}:offset=${offsetToStill}[${toStillLabel}]`);
7194
+ composedDuration = composedDuration + staticDuration - fadeDuration;
7195
+ lastLabel = toStillLabel;
7196
+ }
7197
+ filterParts.push(`[${lastLabel}]null[outv]`);
7198
+ filterComplex = filterParts.join(";");
7199
+ }
7200
+ let mergeContext;
7201
+ if (bgmPath) {
7202
+ const baseContext = context ?? {
7203
+ bgmPath,
7204
+ bgmDuration: await getMediaDuration(bgmPath),
7205
+ usedDuration: 0
7206
+ };
7207
+ const totalDuration = transitionEnabled ? composedDuration : duration * Math.max(1, loopCount);
7208
+ let bgmInputArgs = `-i "${bgmPath}"`;
7209
+ const bgmInputIndex = transitionEnabled ? 2 : 1;
7210
+ const bgmNeedLoop = totalDuration > baseContext.bgmDuration;
7211
+ if (mergeMode === "continuous") {
7212
+ const bgmStartTime = baseContext.usedDuration % baseContext.bgmDuration;
7213
+ if (totalDuration <= baseContext.bgmDuration - bgmStartTime) bgmInputArgs = `-ss ${bgmStartTime} -i "${bgmPath}"`;
7214
+ else bgmInputArgs = `-stream_loop ${Math.ceil(totalDuration / baseContext.bgmDuration) + 1} -ss ${bgmStartTime} -i "${bgmPath}"`;
7215
+ } else if (bgmNeedLoop) bgmInputArgs = `-stream_loop ${Math.max(0, Math.ceil(totalDuration / baseContext.bgmDuration) - 1)} -i "${bgmPath}"`;
7216
+ const result$1 = await ffmpeg(`-y ${inputArgs} ${bgmInputArgs} -filter_complex "${filterComplex};[0:a][${bgmInputIndex}:a]amix=inputs=2:duration=longest:dropout_transition=3[aout]" -map "[outv]" -map "[aout]" -c:v libx264 -c:a aac -b:a 192k -pix_fmt yuv420p -shortest "${outputPath}"`);
7217
+ if (result$1.status) logger.mark(`Live Photo 效果视频重放成功: ${outputPath}`);
7218
+ else logger.error("Live Photo 效果视频重放失败", result$1);
7219
+ if (mergeMode === "continuous") {
7220
+ const outputDuration = result$1.status ? await getMediaDuration(outputPath) : totalDuration;
7221
+ const validDuration = Number.isFinite(outputDuration) && outputDuration > 0 ? outputDuration : totalDuration;
7222
+ mergeContext = {
7223
+ ...baseContext,
7224
+ usedDuration: (baseContext.usedDuration + validDuration) % baseContext.bgmDuration
7225
+ };
7226
+ }
7227
+ return {
7228
+ success: result$1.status,
7229
+ context: mergeContext
7230
+ };
7231
+ }
7232
+ const result = await ffmpeg(`-y ${inputArgs} -filter_complex "${filterComplex}" -map "[outv]" -c:v libx264 -pix_fmt yuv420p "${outputPath}"`);
7233
+ if (result.status) logger.mark(`Live Photo 效果视频重放成功: ${outputPath}`);
7234
+ else logger.error("Live Photo 效果视频重放失败", result);
7235
+ return { success: result.status };
7236
+ };
7890
7237
  });
7891
7238
  var ERROR_CODE_MAP, RECOVERABLE_ERROR_CODES, RECOVERABLE_KEYWORDS, BASE_HEADERS;
7892
7239
  var init_constants = __esmMin(() => {
@@ -8435,7 +7782,7 @@ var init_Network$1 = __esmMin(() => {
8435
7782
  if (!isRecoverableNetworkError(error)) return Promise.reject(error);
8436
7783
  config$1.__retryCount += 1;
8437
7784
  const nextDelay = Math.max(1e3, Math.min(2 ** (config$1.__retryCount - 1) * 1e3, 8e3));
8438
- logger.warn(`请求失败,正在重试... (${config$1.__retryCount}/${this.maxRetries}),将在 ${nextDelay / 1e3} 秒后重试`);
7785
+ logger.warn(`[karin-plugin-kkk] axios 实例请求失败,正在重试... (${config$1.__retryCount}/${this.maxRetries}),将在 ${nextDelay / 1e3} 秒后重试`);
8439
7786
  await new Promise((resolve$1) => setTimeout(resolve$1, nextDelay));
8440
7787
  return this.axiosInstance(config$1);
8441
7788
  });
@@ -11749,6 +11096,12 @@ var init_app_schema = __esmMin(() => {
11749
11096
  label: "解析提示",
11750
11097
  description: "发送提示信息:\"检测到xxx链接,开始解析\""
11751
11098
  },
11099
+ {
11100
+ key: "fakeForward",
11101
+ type: "switch",
11102
+ label: "伪造合并转发消息",
11103
+ description: "开启后合并转发将使用触发者身份展示;关闭后使用机器人身份展示"
11104
+ },
11752
11105
  {
11753
11106
  key: "errorLogSendTo",
11754
11107
  type: "checkbox",
@@ -12983,18 +12336,25 @@ var init_upload_schema = __esmMin(() => {
12983
12336
  title: "发送方式配置"
12984
12337
  },
12985
12338
  {
12986
- key: "sendbase64",
12987
- type: "switch",
12988
- label: "转换Base64",
12989
- description: "发送视频经本插件转换为base64格式后再发送,适合Karin与机器人不在同一网络环境下开启。与「群文件上传」互斥。",
12990
- disabled: $var("usegroupfile")
12339
+ key: "videoSendMode",
12340
+ type: "radio",
12341
+ label: "本地视频发送方式",
12342
+ description: "选择发送本地视频的方式:\n• File - 使用 file 协议发送(需 Karin 与协议端同系统)\n• Base64 - 转 base64 发送(传输数据量增大 1/3,不在同一网络环境可能导致额外带宽成本,适合 karin 和协议端不在同一网络环境)",
12343
+ disabled: $var("usegroupfile"),
12344
+ options: [{
12345
+ label: "File 协议(本地文件)",
12346
+ value: "file"
12347
+ }, {
12348
+ label: "Base64(编码传输)",
12349
+ value: "base64"
12350
+ }]
12991
12351
  },
12992
12352
  {
12993
12353
  key: "usegroupfile",
12994
12354
  type: "switch",
12995
12355
  label: "群文件上传",
12996
- description: "使用群文件上传,开启后会将视频文件上传到群文件中,需配置「群文件上传阈值」。与「转换Base64」互斥",
12997
- disabled: $var("sendbase64")
12356
+ description: "使用群文件上传,开启后会将视频文件上传到群文件中,需配置「群文件上传阈值」。与「本地视频发送方式 = Base64」互斥。",
12357
+ disabled: $eq("videoSendMode", "base64")
12998
12358
  },
12999
12359
  {
13000
12360
  key: "groupfilevalue",
@@ -13002,14 +12362,14 @@ var init_upload_schema = __esmMin(() => {
13002
12362
  inputType: "number",
13003
12363
  label: "群文件上传阈值",
13004
12364
  description: "当文件大小超过该值时将使用群文件上传,单位:MB,「使用群文件上传」开启后才会生效",
13005
- disabled: $or($not("usegroupfile"), $var("sendbase64")),
12365
+ disabled: $or($not("usegroupfile"), $eq("videoSendMode", "base64")),
13006
12366
  rules: [{ min: 1 }]
13007
12367
  },
13008
12368
  {
13009
12369
  key: "imageSendMode",
13010
12370
  type: "radio",
13011
12371
  label: "网络图片发送方式",
13012
- description: "选择发送网络图片的方式:\n• URL - 直接传递链接给上游(可能因上游网络问题超时)\n• File - 下载后用 file 协议发送(需 Karin 与协议端同系统)\n• Base64 - 转 base64 发送(传输数据增大 1/3,不在同一网络环境可能导致额外带宽成本)",
12372
+ description: "选择发送网络图片的方式:\n• URL - 直接传递链接给上游(可能因上游网络问题超时)\n• File - 下载后用 file 协议发送(需 Karin 与协议端同系统)\n• Base64 - 转 base64 发送(传输数据量增大 1/3,不在同一网络环境可能导致额外带宽成本)",
13013
12373
  options: [
13014
12374
  {
13015
12375
  label: "URL 链接(直接传递)",
@@ -15110,6 +14470,11 @@ const webConfig = defineConfig({
15110
14470
  description: "发送提示信息:\"检测到xxx链接,开始解析\"",
15111
14471
  defaultSelected: all.app.parseTip
15112
14472
  }),
14473
+ components.switch.create("fakeForward", {
14474
+ label: "伪造合并转发消息",
14475
+ description: "开启后合并转发将使用触发者身份展示;关闭后使用机器人身份展示",
14476
+ defaultSelected: all.app.fakeForward
14477
+ }),
15113
14478
  components.checkbox.group("errorLogSendTo", {
15114
14479
  label: "错误日志",
15115
14480
  description: "遇到错误时谁会收到错误日志。注:推送任务只可发送给主人。「第一个主人」与「所有主人」互斥。",
@@ -15176,24 +14541,33 @@ const webConfig = defineConfig({
15176
14541
  description: "发送方式配置",
15177
14542
  descPosition: 20
15178
14543
  }),
15179
- components.switch.create("sendbase64", {
15180
- label: "转换Base64",
15181
- description: "发送视频经本插件转换为base64格式后再发送,适合Karin与机器人不在同一网络环境下开启。与「群文件上传」互斥。",
15182
- defaultSelected: all.upload.sendbase64,
15183
- isDisabled: all.upload.usegroupfile
14544
+ components.radio.group("videoSendMode", {
14545
+ label: "本地视频发送方式",
14546
+ orientation: "vertical",
14547
+ defaultValue: all.upload.videoSendMode,
14548
+ isDisabled: all.upload.usegroupfile,
14549
+ radio: [components.radio.create("videoSendMode:radio-1", {
14550
+ label: "File 协议(本地文件)",
14551
+ description: "使用 file 协议发送本地视频,需 Karin 与协议端在同一系统",
14552
+ value: "file"
14553
+ }), components.radio.create("videoSendMode:radio-2", {
14554
+ label: "Base64(编码传输)",
14555
+ description: "将本地视频转换为 base64 发送,传输数据量增大约 30%,不在同一网络环境可能导致额外带宽成本,适合 karin 和协议端不在同一网络环境",
14556
+ value: "base64"
14557
+ })]
15184
14558
  }),
15185
14559
  components.switch.create("usegroupfile", {
15186
14560
  label: "群文件上传",
15187
- description: "使用群文件上传,开启后会将视频文件上传到群文件中,需配置「群文件上传阈值」。与「转换Base64」互斥",
14561
+ description: "使用群文件上传,开启后会将视频文件上传到群文件中,需配置「群文件上传阈值」。与「本地视频发送方式 = Base64」互斥。",
15188
14562
  defaultSelected: all.upload.usegroupfile,
15189
- isDisabled: all.upload.sendbase64
14563
+ isDisabled: all.upload.videoSendMode === "base64"
15190
14564
  }),
15191
14565
  components.input.number("groupfilevalue", {
15192
14566
  label: "群文件上传阈值",
15193
14567
  description: "当文件大小超过该值时将使用群文件上传,单位:MB,「使用群文件上传」开启后才会生效",
15194
14568
  defaultValue: all.upload.groupfilevalue.toString(),
15195
14569
  rules: [{ min: 1 }],
15196
- isDisabled: !all.upload.usegroupfile || all.upload.sendbase64
14570
+ isDisabled: !all.upload.usegroupfile || all.upload.videoSendMode === "base64"
15197
14571
  }),
15198
14572
  components.radio.group("imageSendMode", {
15199
14573
  label: "网络图片发送方式",
@@ -15212,7 +14586,7 @@ const webConfig = defineConfig({
15212
14586
  }),
15213
14587
  components.radio.create("imageSendMode:radio-3", {
15214
14588
  label: "Base64(编码传输)",
15215
- description: "下载后转换为 base64 发送,传输数据增大约 1/3,不在同一网络环境可能导致额外带宽成本",
14589
+ description: "下载后转换为 base64 发送,传输数据量增大约 30%,不在同一网络环境可能导致额外带宽成本",
15216
14590
  value: "base64"
15217
14591
  })
15218
14592
  ]
@@ -15624,139 +14998,523 @@ const webConfig = defineConfig({
15624
14998
  }
15625
14999
  }
15626
15000
  }
15627
- await Config.syncConfigToDatabase();
15628
- if (needReloadAmagi) try {
15629
- const { reloadAmagiConfig: reloadAmagiConfig$1 } = await Promise.resolve().then(() => (init_amagiClient(), amagiClient_exports));
15630
- reloadAmagiConfig$1();
15631
- } catch (error) {
15632
- logger.error(`[WebConfig] 重载 Amagi Client 失败: ${error}`);
15633
- }
15634
- return {
15635
- mergeCfg,
15636
- formatCfg,
15637
- success,
15638
- message: success ? "保存成功 Ciallo~(∠・ω< )⌒☆" : "配置无变化 Ciallo~(∠・ω< )⌒☆"
15639
- };
15001
+ await Config.syncConfigToDatabase();
15002
+ if (needReloadAmagi) try {
15003
+ const { reloadAmagiConfig: reloadAmagiConfig$1 } = await Promise.resolve().then(() => (init_amagiClient(), amagiClient_exports));
15004
+ reloadAmagiConfig$1();
15005
+ } catch (error) {
15006
+ logger.error(`[WebConfig] 重载 Amagi Client 失败: ${error}`);
15007
+ }
15008
+ return {
15009
+ mergeCfg,
15010
+ formatCfg,
15011
+ success,
15012
+ message: success ? "保存成功 Ciallo~(∠・ω< )⌒☆" : "配置无变化 Ciallo~(∠・ω< )⌒☆"
15013
+ };
15014
+ }
15015
+ });
15016
+ var web_config_default = webConfig;
15017
+ var customizer = (value, srcValue) => {
15018
+ if (Array.isArray(srcValue)) return srcValue;
15019
+ };
15020
+ var deepEqual = (a, b) => {
15021
+ if (a === b) return false;
15022
+ if (typeof a === "string" && typeof b === "string") {
15023
+ if (a !== b) return true;
15024
+ }
15025
+ if (typeof a === "number" && typeof b === "number") {
15026
+ if (a !== b) return true;
15027
+ }
15028
+ if (typeof a === "boolean" && typeof b === "boolean") {
15029
+ if (a !== b) return true;
15030
+ }
15031
+ if (a === null || b === null || typeof a !== typeof b) return true;
15032
+ if (Array.isArray(a) && Array.isArray(b)) {
15033
+ if (a.length !== b.length) return true;
15034
+ for (let i = 0; i < a.length; i++) if (deepEqual(a[i], b[i])) return true;
15035
+ }
15036
+ let isChange = false;
15037
+ if (typeof a === "object" && typeof b === "object") {
15038
+ if (isChange) return true;
15039
+ const keysA = Object.keys(a);
15040
+ const keysB = Object.keys(b);
15041
+ if (keysA.length !== keysB.length) return true;
15042
+ for (const key of keysA) {
15043
+ if (!keysB.includes(key)) {
15044
+ isChange = true;
15045
+ return true;
15046
+ }
15047
+ if (deepEqual(a[key], b[key])) {
15048
+ isChange = true;
15049
+ return true;
15050
+ }
15051
+ }
15052
+ }
15053
+ return false;
15054
+ };
15055
+ var convertToNumber = (value) => {
15056
+ if (/^\d+$/.test(value)) return parseInt(value, 10);
15057
+ else return value;
15058
+ };
15059
+ var getFirstObject = (arr) => arr.length > 0 ? arr[0] : {};
15060
+ var setNestedProperty = (obj, keys, value) => {
15061
+ let current = obj;
15062
+ for (let i = 0; i < keys.length - 1; i++) {
15063
+ const key = keys[i];
15064
+ if (!current[key] || typeof current[key] !== "object") current[key] = {};
15065
+ current = current[key];
15066
+ }
15067
+ const lastKey = keys[keys.length - 1];
15068
+ current[lastKey] = value;
15069
+ };
15070
+ var processFrontendData = (data$1) => {
15071
+ const result = {};
15072
+ const configKeys = Object.keys(data$1).filter((key) => !key.includes("pushlist") && key in data$1);
15073
+ for (const key of configKeys) {
15074
+ const value = data$1[key];
15075
+ const firstObj = Array.isArray(value) ? getFirstObject(value) : {};
15076
+ const objKeys = Object.keys(firstObj);
15077
+ if (objKeys.length === 0) continue;
15078
+ const configObj = {};
15079
+ let hasValidData = false;
15080
+ const nestedProps = objKeys.filter((prop) => prop.includes(":"));
15081
+ const flatProps = objKeys.filter((prop) => !prop.includes(":"));
15082
+ for (const prop of nestedProps) {
15083
+ let propValue = firstObj[prop];
15084
+ if (typeof propValue === "string") propValue = convertToNumber(propValue);
15085
+ if (propValue !== void 0 && propValue !== null) {
15086
+ setNestedProperty(configObj, prop.split(":"), propValue);
15087
+ hasValidData = true;
15088
+ }
15089
+ }
15090
+ for (const prop of flatProps) {
15091
+ let propValue = firstObj[prop];
15092
+ if (typeof propValue === "string") propValue = convertToNumber(propValue);
15093
+ if (propValue !== void 0 && propValue !== null) {
15094
+ configObj[prop] = propValue;
15095
+ hasValidData = true;
15096
+ }
15097
+ }
15098
+ if (hasValidData && Object.keys(configObj).length > 0) result[key] = configObj;
15640
15099
  }
15641
- });
15642
- var web_config_default = webConfig;
15643
- var customizer = (value, srcValue) => {
15644
- if (Array.isArray(srcValue)) return srcValue;
15100
+ result.pushlist = {
15101
+ douyin: data$1["pushlist:douyin"] || [],
15102
+ bilibili: (data$1["pushlist:bilibili"] || []).map((item) => ({
15103
+ ...item,
15104
+ host_mid: Number(item.host_mid)
15105
+ }))
15106
+ };
15107
+ return result;
15645
15108
  };
15646
- var deepEqual = (a, b) => {
15647
- if (a === b) return false;
15648
- if (typeof a === "string" && typeof b === "string") {
15649
- if (a !== b) return true;
15109
+ var cleanFlattenedFields = (obj) => {
15110
+ if (!obj || typeof obj !== "object") return;
15111
+ for (const [, value] of Object.entries(obj)) if (typeof value === "object" && value !== null && !Array.isArray(value)) {
15112
+ cleanFlattenedFields(value);
15113
+ const valueObj = value;
15114
+ const flattenedKeys = Object.keys(valueObj).filter((k) => k.includes("."));
15115
+ for (const flatKey of flattenedKeys) if (hasNestedStructure(valueObj, flatKey.split("."))) delete valueObj[flatKey];
15650
15116
  }
15651
- if (typeof a === "number" && typeof b === "number") {
15652
- if (a !== b) return true;
15117
+ };
15118
+ var hasNestedStructure = (obj, path$1) => {
15119
+ let current = obj;
15120
+ for (let i = 0; i < path$1.length - 1; i++) {
15121
+ const key = path$1[i];
15122
+ if (!current[key] || typeof current[key] !== "object") return false;
15123
+ current = current[key];
15653
15124
  }
15654
- if (typeof a === "boolean" && typeof b === "boolean") {
15655
- if (a !== b) return true;
15125
+ return path$1[path$1.length - 1] in current;
15126
+ };
15127
+ await init_utils$1();
15128
+ var ENCODER_PRIORITY$1 = {
15129
+ h264: [
15130
+ "h264_nvenc",
15131
+ "h264_qsv",
15132
+ "h264_amf",
15133
+ "libx264"
15134
+ ],
15135
+ h265: [
15136
+ "hevc_nvenc",
15137
+ "hevc_qsv",
15138
+ "hevc_amf",
15139
+ "libx265"
15140
+ ],
15141
+ av1: [
15142
+ "av1_nvenc",
15143
+ "av1_qsv",
15144
+ "av1_amf",
15145
+ "libsvtav1",
15146
+ "libaom-av1"
15147
+ ]
15148
+ };
15149
+ var SOFTWARE_FALLBACK$1 = {
15150
+ h264: "libx264",
15151
+ h265: "libx265",
15152
+ av1: "libsvtav1"
15153
+ };
15154
+ var cachedEncoders$1 = {};
15155
+ async function detectEncoder$1(codec) {
15156
+ if (cachedEncoders$1[codec]) return cachedEncoders$1[codec];
15157
+ logger.debug(`[BiliDanmaku] 开始检测 ${codec.toUpperCase()} 编码器...`);
15158
+ for (const encoder of ENCODER_PRIORITY$1[codec]) {
15159
+ logger.debug(`[BiliDanmaku] 测试编码器: ${encoder}`);
15160
+ try {
15161
+ const result = await ffmpeg(`-f lavfi -i color=c=black:s=320x240:d=0.1 -c:v ${encoder} -f null -`);
15162
+ logger.debug(`[BiliDanmaku] ${encoder} 测试结果: status=${result.status}`);
15163
+ if (result.status) {
15164
+ cachedEncoders$1[codec] = encoder;
15165
+ logger.info(`[BiliDanmaku] 使用 ${codec.toUpperCase()} 编码器: ${encoder}`);
15166
+ return encoder;
15167
+ }
15168
+ } catch (e) {
15169
+ logger.debug(`[BiliDanmaku] 编码器 ${encoder} 测试异常: ${e}`);
15170
+ }
15656
15171
  }
15657
- if (a === null || b === null || typeof a !== typeof b) return true;
15658
- if (Array.isArray(a) && Array.isArray(b)) {
15659
- if (a.length !== b.length) return true;
15660
- for (let i = 0; i < a.length; i++) if (deepEqual(a[i], b[i])) return true;
15172
+ const fallback = SOFTWARE_FALLBACK$1[codec];
15173
+ cachedEncoders$1[codec] = fallback;
15174
+ logger.info(`[BiliDanmaku] 回退到软件编码器: ${fallback}`);
15175
+ return fallback;
15176
+ }
15177
+ async function getVideoBitrate$1(path$1) {
15178
+ try {
15179
+ const fileSize = fs.statSync(path$1).size;
15180
+ const { stdout } = await ffprobe(`-v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 "${path$1}"`);
15181
+ const duration = parseFloat(stdout.trim());
15182
+ if (duration > 0 && fileSize > 0) {
15183
+ const kbps = Math.round(fileSize * 8 / duration / 1e3);
15184
+ logger.debug(`[BiliDanmaku] 通过文件大小计算码率: ${kbps}kbps`);
15185
+ return kbps;
15186
+ }
15187
+ } catch (e) {
15188
+ logger.debug(`[BiliDanmaku] 通过文件大小计算码率失败: ${e}`);
15661
15189
  }
15662
- let isChange = false;
15663
- if (typeof a === "object" && typeof b === "object") {
15664
- if (isChange) return true;
15665
- const keysA = Object.keys(a);
15666
- const keysB = Object.keys(b);
15667
- if (keysA.length !== keysB.length) return true;
15668
- for (const key of keysA) {
15669
- if (!keysB.includes(key)) {
15670
- isChange = true;
15671
- return true;
15190
+ try {
15191
+ const { stdout } = await ffprobe(`-v error -select_streams v:0 -show_entries stream=bit_rate -of default=noprint_wrappers=1:nokey=1 "${path$1}"`);
15192
+ const bitrate = parseInt(stdout.trim());
15193
+ if (bitrate > 0) return Math.round(bitrate / 1e3);
15194
+ } catch {}
15195
+ try {
15196
+ const { stdout } = await ffprobe(`-v error -show_entries format=bit_rate -of default=noprint_wrappers=1:nokey=1 "${path$1}"`);
15197
+ const bitrate = parseInt(stdout.trim());
15198
+ if (bitrate > 0) return Math.round(bitrate / 1e3);
15199
+ } catch {}
15200
+ logger.warn("[BiliDanmaku] 无法获取视频码率,将使用 CRF 模式");
15201
+ return 0;
15202
+ }
15203
+ function getEncoderParams$1(encoder, targetBitrate) {
15204
+ const threads = Math.max(1, Math.floor(os.cpus().length / 2));
15205
+ if (targetBitrate && targetBitrate > 0) {
15206
+ const bitrateK = `${targetBitrate}k`;
15207
+ const maxrate = `${Math.round(targetBitrate * 2)}k`;
15208
+ const bufsize = `${Math.round(targetBitrate * 4)}k`;
15209
+ if (encoder === "h264_nvenc") return `-c:v h264_nvenc -preset p4 -rc vbr -b:v ${bitrateK} -maxrate ${maxrate} -bufsize ${bufsize}`;
15210
+ if (encoder === "h264_qsv") return `-c:v h264_qsv -preset medium -b:v ${bitrateK} -maxrate ${maxrate} -bufsize ${bufsize}`;
15211
+ if (encoder === "h264_amf") return `-c:v h264_amf -quality balanced -rc vbr_peak -b:v ${bitrateK} -maxrate ${maxrate}`;
15212
+ if (encoder === "libx264") return `-c:v libx264 -preset medium -b:v ${bitrateK} -maxrate ${maxrate} -bufsize ${bufsize} -threads ${threads}`;
15213
+ if (encoder === "hevc_nvenc") return `-c:v hevc_nvenc -preset p4 -rc vbr -b:v ${bitrateK} -maxrate ${maxrate} -bufsize ${bufsize}`;
15214
+ if (encoder === "hevc_qsv") return `-c:v hevc_qsv -preset medium -b:v ${bitrateK} -maxrate ${maxrate} -bufsize ${bufsize}`;
15215
+ if (encoder === "hevc_amf") return `-c:v hevc_amf -quality balanced -rc vbr_peak -b:v ${bitrateK} -maxrate ${maxrate}`;
15216
+ if (encoder === "libx265") return `-c:v libx265 -preset medium -b:v ${bitrateK} -maxrate ${maxrate} -bufsize ${bufsize} -threads ${threads}`;
15217
+ if (encoder === "av1_nvenc") return `-c:v av1_nvenc -preset p4 -rc vbr -b:v ${bitrateK} -maxrate ${maxrate} -bufsize ${bufsize}`;
15218
+ if (encoder === "av1_qsv") return `-c:v av1_qsv -preset medium -b:v ${bitrateK} -maxrate ${maxrate} -bufsize ${bufsize}`;
15219
+ if (encoder === "av1_amf") return `-c:v av1_amf -quality balanced -rc vbr_peak -b:v ${bitrateK} -maxrate ${maxrate}`;
15220
+ if (encoder === "libsvtav1") return `-c:v libsvtav1 -preset 6 -b:v ${bitrateK} -maxrate ${maxrate} -bufsize ${bufsize} -threads ${threads}`;
15221
+ if (encoder === "libaom-av1") return `-c:v libaom-av1 -cpu-used 4 -b:v ${bitrateK} -maxrate ${maxrate} -bufsize ${bufsize} -threads ${threads}`;
15222
+ return `-c:v libx265 -preset medium -b:v ${bitrateK} -maxrate ${maxrate} -bufsize ${bufsize} -threads ${threads}`;
15223
+ }
15224
+ if (encoder === "h264_nvenc") return "-c:v h264_nvenc -preset p4 -rc vbr -cq 23";
15225
+ if (encoder === "h264_qsv") return "-c:v h264_qsv -preset medium -global_quality 23";
15226
+ if (encoder === "h264_amf") return "-c:v h264_amf -quality balanced -rc cqp -qp_i 23 -qp_p 23";
15227
+ if (encoder === "libx264") return `-c:v libx264 -crf 23 -preset medium -threads ${threads}`;
15228
+ if (encoder === "hevc_nvenc") return "-c:v hevc_nvenc -preset p4 -rc vbr -cq 28";
15229
+ if (encoder === "hevc_qsv") return "-c:v hevc_qsv -preset medium -global_quality 28";
15230
+ if (encoder === "hevc_amf") return "-c:v hevc_amf -quality balanced -rc cqp -qp_i 28 -qp_p 28";
15231
+ if (encoder === "libx265") return `-c:v libx265 -crf 28 -preset medium -threads ${threads}`;
15232
+ if (encoder === "av1_nvenc") return "-c:v av1_nvenc -preset p4 -rc vbr -cq 30";
15233
+ if (encoder === "av1_qsv") return "-c:v av1_qsv -preset medium -global_quality 30";
15234
+ if (encoder === "av1_amf") return "-c:v av1_amf -quality balanced -rc cqp -qp_i 30 -qp_p 30";
15235
+ if (encoder === "libsvtav1") return `-c:v libsvtav1 -crf 30 -preset 6 -threads ${threads}`;
15236
+ if (encoder === "libaom-av1") return `-c:v libaom-av1 -crf 30 -cpu-used 4 -threads ${threads}`;
15237
+ return `-c:v libx265 -crf 28 -preset medium -threads ${threads}`;
15238
+ }
15239
+ var toASSColor = (color) => {
15240
+ const r = color >> 16 & 255;
15241
+ const g = color >> 8 & 255;
15242
+ return `${(color & 255).toString(16).padStart(2, "0")}${g.toString(16).padStart(2, "0")}${r.toString(16).padStart(2, "0")}`.toUpperCase();
15243
+ };
15244
+ var toASSTime$1 = (ms) => {
15245
+ const s = ms / 1e3;
15246
+ const h = Math.floor(s / 3600);
15247
+ const m = Math.floor(s % 3600 / 60);
15248
+ const sec = Math.floor(s % 60);
15249
+ const cs = Math.floor(s % 1 * 100);
15250
+ return `${h}:${m.toString().padStart(2, "0")}:${sec.toString().padStart(2, "0")}.${cs.toString().padStart(2, "0")}`;
15251
+ };
15252
+ var estimateWidth$1 = (text, fontSize) => {
15253
+ let w = 0;
15254
+ for (const c of text) w += c.charCodeAt(0) > 127 ? fontSize : fontSize * .5;
15255
+ return w;
15256
+ };
15257
+ var escapeASS$1 = (text) => text.replace(/\\/g, "\\\\").replace(/\{/g, "\\{").replace(/\}/g, "\\}").replace(/\n/g, "\\N");
15258
+ var escapeWinPath$1 = (path$1) => path$1.replace(/\\/g, "/").replace(/:/g, "\\:");
15259
+ var isLandscape$1 = (w, h) => w > h;
15260
+ async function getBiliResolution(path$1) {
15261
+ try {
15262
+ const { stdout } = await ffprobe(`-v error -select_streams v:0 -show_entries stream=width,height -of csv=s=x:p=0 "${path$1}"`);
15263
+ const [w, h] = stdout.trim().split("x").map(Number);
15264
+ if (w && h) return {
15265
+ width: w,
15266
+ height: h
15267
+ };
15268
+ } catch {}
15269
+ try {
15270
+ const match = ((await ffmpeg(`-i "${path$1}" -f null -`, { timeout: 5e3 })).stderr || "").match(/(\d{3,4})x(\d{3,4})/);
15271
+ if (match) return {
15272
+ width: parseInt(match[1]),
15273
+ height: parseInt(match[2])
15274
+ };
15275
+ } catch {}
15276
+ return {
15277
+ width: 1920,
15278
+ height: 1080
15279
+ };
15280
+ }
15281
+ async function getBiliFrameRate(path$1) {
15282
+ try {
15283
+ const { stdout } = await ffprobe(`-v error -select_streams v:0 -show_entries stream=r_frame_rate -of default=noprint_wrappers=1:nokey=1 "${path$1}"`);
15284
+ const [num, den] = stdout.trim().split("/").map(Number);
15285
+ if (den > 0) return num / den;
15286
+ } catch {}
15287
+ try {
15288
+ const stderr = (await ffmpeg(`-i "${path$1}" -f null -`, { timeout: 5e3 })).stderr || "";
15289
+ const fpsMatch = stderr.match(/(\d+(?:\.\d+)?)\s*fps/);
15290
+ if (fpsMatch) return parseFloat(fpsMatch[1]);
15291
+ const fracMatch = stderr.match(/(\d+)\/(\d+)\s*fps/);
15292
+ if (fracMatch) return parseInt(fracMatch[1]) / parseInt(fracMatch[2]);
15293
+ } catch {}
15294
+ return 30;
15295
+ }
15296
+ var FONT_SIZE_MAP$1 = {
15297
+ small: {
15298
+ base: 25,
15299
+ trackH: 30
15300
+ },
15301
+ medium: {
15302
+ base: 32,
15303
+ trackH: 38
15304
+ },
15305
+ large: {
15306
+ base: 40,
15307
+ trackH: 46
15308
+ }
15309
+ };
15310
+ function generateBiliASS(danmakuList, width, height, options = {}) {
15311
+ const { scrollTime = 8, danmakuOpacity = 70, fontName = "Microsoft YaHei", danmakuArea = .5, danmakuFontSize = "medium" } = options;
15312
+ const fontScale = height / 1080;
15313
+ const sizeConfig = FONT_SIZE_MAP$1[danmakuFontSize];
15314
+ const fontSize = Math.round(sizeConfig.base * fontScale);
15315
+ const trackH = Math.round(sizeConfig.trackH * fontScale);
15316
+ const topMargin = Math.round(10 * fontScale);
15317
+ const bottomMargin = Math.round(10 * fontScale);
15318
+ const areaHeight = Math.floor(height * danmakuArea) - topMargin - bottomMargin;
15319
+ const trackCount = Math.max(1, Math.floor((areaHeight - fontSize) / trackH));
15320
+ const fixedTrackCount = trackCount;
15321
+ const minGap = Math.round(10 * fontScale);
15322
+ const alpha = Math.round((100 - Math.max(0, Math.min(100, danmakuOpacity))) * 2.55).toString(16).padStart(2, "0").toUpperCase();
15323
+ let ass = `[Script Info]\nTitle: Bilibili Danmaku\nScriptType: v4.00+\nPlayResX: ${width}\nPlayResY: ${height}\nTimer: 100.0000\n\n[V4+ Styles]\nFormat: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding\nStyle: Scroll,${fontName},${fontSize},&H${alpha}FFFFFF,&H${alpha}FFFFFF,&H${alpha}000000,&H${alpha}000000,0,0,0,0,100,100,0,0,1,1,0,2,0,0,0,1\nStyle: Top,${fontName},${fontSize},&H${alpha}FFFFFF,&H${alpha}FFFFFF,&H${alpha}000000,&H${alpha}000000,0,0,0,0,100,100,0,0,1,1,0,8,0,0,0,1\nStyle: Bottom,${fontName},${fontSize},&H${alpha}FFFFFF,&H${alpha}FFFFFF,&H${alpha}000000,&H${alpha}000000,0,0,0,0,100,100,0,0,1,1,0,2,0,0,0,1\n\n[Events]\nFormat: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text\n`;
15324
+ const scrollTracks = Array(trackCount).fill(null);
15325
+ const topTracks = Array(fixedTrackCount).fill(0);
15326
+ const bottomTracks = Array(fixedTrackCount).fill(0);
15327
+ const calcDistance = (last, startTime, duration, textWidth) => {
15328
+ const lastSpeed = (width + last.textWidth) / last.duration;
15329
+ const newSpeed = (width + textWidth) / duration;
15330
+ let dist = width - (width - lastSpeed * (startTime - last.startTime) + last.textWidth) - minGap;
15331
+ if (newSpeed > lastSpeed) {
15332
+ const lastRightXAtEnd = width - lastSpeed * (startTime + duration - last.startTime) + last.textWidth;
15333
+ dist = Math.min(dist, -textWidth - lastRightXAtEnd - minGap);
15334
+ }
15335
+ return dist;
15336
+ };
15337
+ const sorted = [...danmakuList].sort((a, b) => a.progress - b.progress);
15338
+ for (const dm of sorted) {
15339
+ if (dm.mode > 5 || !dm.content.trim()) continue;
15340
+ const startTime = dm.progress;
15341
+ const dmSizeRatio = (dm.fontsize || 25) / 25;
15342
+ const dmFontSize = Math.round(fontSize * dmSizeRatio);
15343
+ const textWidth = estimateWidth$1(dm.content, dmFontSize);
15344
+ const content = escapeASS$1(dm.content);
15345
+ const colorTag = dm.color !== 16777215 ? `{\\c&H${toASSColor(dm.color)}&}` : "";
15346
+ const sizeTag = dmFontSize !== fontSize ? `{\\fs${dmFontSize}}` : "";
15347
+ if (dm.mode === 4) {
15348
+ const endTime = startTime + 4e3;
15349
+ let idx = bottomTracks.findIndex((t) => t <= startTime);
15350
+ if (idx === -1) idx = Math.floor(Math.random() * bottomTracks.length);
15351
+ bottomTracks[idx] = endTime;
15352
+ const y = height - bottomMargin - idx * trackH;
15353
+ ass += `Dialogue: 0,${toASSTime$1(startTime)},${toASSTime$1(endTime)},Bottom,,0,0,0,,{\\an2}${colorTag}${sizeTag}{\\pos(${width / 2},${y})}${content}\n`;
15354
+ } else if (dm.mode === 5) {
15355
+ const endTime = startTime + 4e3;
15356
+ let idx = topTracks.findIndex((t) => t <= startTime);
15357
+ if (idx === -1) idx = Math.floor(Math.random() * topTracks.length);
15358
+ topTracks[idx] = endTime;
15359
+ const y = topMargin + idx * trackH + fontSize;
15360
+ ass += `Dialogue: 0,${toASSTime$1(startTime)},${toASSTime$1(endTime)},Top,,0,0,0,,{\\an8}${colorTag}${sizeTag}{\\pos(${width / 2},${y})}${content}\n`;
15361
+ } else {
15362
+ const duration = scrollTime * 1e3;
15363
+ const endTime = startTime + duration;
15364
+ for (let i = 0; i < scrollTracks.length; i++) {
15365
+ const t = scrollTracks[i];
15366
+ if (t && t.startTime + t.duration <= startTime) scrollTracks[i] = null;
15672
15367
  }
15673
- if (deepEqual(a[key], b[key])) {
15674
- isChange = true;
15675
- return true;
15368
+ let bestIdx = -1;
15369
+ let bestDist = -Infinity;
15370
+ for (let i = 0; i < scrollTracks.length; i++) {
15371
+ const t = scrollTracks[i];
15372
+ if (!t) {
15373
+ if (bestIdx === -1) bestIdx = i;
15374
+ continue;
15375
+ }
15376
+ const d = calcDistance(t, startTime, duration, textWidth);
15377
+ if (d >= 0) {
15378
+ if (bestDist < 0 || d < bestDist) {
15379
+ bestDist = d;
15380
+ bestIdx = i;
15381
+ }
15382
+ }
15676
15383
  }
15384
+ if (bestIdx === -1 || bestDist < 0 && scrollTracks[bestIdx] !== null) continue;
15385
+ scrollTracks[bestIdx] = {
15386
+ startTime,
15387
+ duration,
15388
+ textWidth
15389
+ };
15390
+ const y = (bestIdx + 1) * trackH;
15391
+ ass += `Dialogue: 0,${toASSTime$1(startTime)},${toASSTime$1(endTime)},Scroll,,0,0,0,,{\\an7}${colorTag}${sizeTag}{\\move(${width},${y},${-textWidth},${y})}${content}\n`;
15677
15392
  }
15678
15393
  }
15679
- return false;
15680
- };
15681
- var convertToNumber = (value) => {
15682
- if (/^\d+$/.test(value)) return parseInt(value, 10);
15683
- else return value;
15684
- };
15685
- var getFirstObject = (arr) => arr.length > 0 ? arr[0] : {};
15686
- var setNestedProperty = (obj, keys, value) => {
15687
- let current = obj;
15688
- for (let i = 0; i < keys.length - 1; i++) {
15689
- const key = keys[i];
15690
- if (!current[key] || typeof current[key] !== "object") current[key] = {};
15691
- current = current[key];
15692
- }
15693
- const lastKey = keys[keys.length - 1];
15694
- current[lastKey] = value;
15695
- };
15696
- var processFrontendData = (data$1) => {
15697
- const result = {};
15698
- const configKeys = Object.keys(data$1).filter((key) => !key.includes("pushlist") && key in data$1);
15699
- for (const key of configKeys) {
15700
- const value = data$1[key];
15701
- const firstObj = Array.isArray(value) ? getFirstObject(value) : {};
15702
- const objKeys = Object.keys(firstObj);
15703
- if (objKeys.length === 0) continue;
15704
- const configObj = {};
15705
- let hasValidData = false;
15706
- const nestedProps = objKeys.filter((prop) => prop.includes(":"));
15707
- const flatProps = objKeys.filter((prop) => !prop.includes(":"));
15708
- for (const prop of nestedProps) {
15709
- let propValue = firstObj[prop];
15710
- if (typeof propValue === "string") propValue = convertToNumber(propValue);
15711
- if (propValue !== void 0 && propValue !== null) {
15712
- setNestedProperty(configObj, prop.split(":"), propValue);
15713
- hasValidData = true;
15714
- }
15715
- }
15716
- for (const prop of flatProps) {
15717
- let propValue = firstObj[prop];
15718
- if (typeof propValue === "string") propValue = convertToNumber(propValue);
15719
- if (propValue !== void 0 && propValue !== null) {
15720
- configObj[prop] = propValue;
15721
- hasValidData = true;
15722
- }
15394
+ return ass;
15395
+ }
15396
+ var MAX_OUTPUT_WIDTH$1 = 2160;
15397
+ function calcCanvas$1(origW, origH, verticalMode) {
15398
+ if (verticalMode === "off") return {
15399
+ width: origW,
15400
+ height: origH,
15401
+ offsetY: 0,
15402
+ isVertical: false
15403
+ };
15404
+ const ratio = origW / origH;
15405
+ const isWide = isLandscape$1(origW, origH);
15406
+ if (verticalMode === "force") {
15407
+ const targetRatio = 16 / 9;
15408
+ if (isWide) {
15409
+ const newW = Math.min(origH, MAX_OUTPUT_WIDTH$1);
15410
+ const newH = Math.round(newW * targetRatio);
15411
+ const scaledH = Math.round(newW / ratio);
15412
+ return {
15413
+ width: newW,
15414
+ height: newH,
15415
+ offsetY: Math.round((newH - scaledH) / 2),
15416
+ isVertical: true,
15417
+ scale: newW / origW
15418
+ };
15419
+ } else {
15420
+ const newW = Math.min(origW, MAX_OUTPUT_WIDTH$1);
15421
+ const scaleRatio = newW / origW;
15422
+ const scaledOrigH = Math.round(origH * scaleRatio);
15423
+ const newH = Math.round(newW * targetRatio);
15424
+ const offsetY = Math.round((newH - scaledOrigH) / 2);
15425
+ return {
15426
+ width: newW,
15427
+ height: newH,
15428
+ offsetY: Math.max(0, offsetY),
15429
+ isVertical: true,
15430
+ scale: scaleRatio
15431
+ };
15723
15432
  }
15724
- if (hasValidData && Object.keys(configObj).length > 0) result[key] = configObj;
15725
15433
  }
15726
- result.pushlist = {
15727
- douyin: data$1["pushlist:douyin"] || [],
15728
- bilibili: (data$1["pushlist:bilibili"] || []).map((item) => ({
15729
- ...item,
15730
- host_mid: Number(item.host_mid)
15731
- }))
15434
+ if (isWide && ratio >= 1.7) {
15435
+ const newW = Math.min(origH, MAX_OUTPUT_WIDTH$1);
15436
+ const scaleRatio = newW / origH;
15437
+ const newH = Math.round(origW * scaleRatio);
15438
+ const scaledH = Math.round(newW / ratio);
15439
+ return {
15440
+ width: newW,
15441
+ height: newH,
15442
+ offsetY: Math.round((newH - scaledH) / 2),
15443
+ isVertical: true,
15444
+ scale: newW / origW
15445
+ };
15446
+ }
15447
+ return {
15448
+ width: origW,
15449
+ height: origH,
15450
+ offsetY: 0,
15451
+ isVertical: false
15732
15452
  };
15733
- return result;
15734
- };
15735
- var cleanFlattenedFields = (obj) => {
15736
- if (!obj || typeof obj !== "object") return;
15737
- for (const [, value] of Object.entries(obj)) if (typeof value === "object" && value !== null && !Array.isArray(value)) {
15738
- cleanFlattenedFields(value);
15739
- const valueObj = value;
15740
- const flattenedKeys = Object.keys(valueObj).filter((k) => k.includes("."));
15741
- for (const flatKey of flattenedKeys) if (hasNestedStructure(valueObj, flatKey.split("."))) delete valueObj[flatKey];
15453
+ }
15454
+ function buildFilter$1(canvas, assPath) {
15455
+ const escaped = escapeWinPath$1(assPath);
15456
+ if (canvas.isVertical) {
15457
+ if (canvas.scale && canvas.scale !== 1 && canvas.scale < 1) return `scale=${canvas.width}:-1,pad=${canvas.width}:${canvas.height}:0:${canvas.offsetY}:black,subtitles='${escaped}'`;
15458
+ return `pad=${canvas.width}:${canvas.height}:0:${canvas.offsetY}:black,subtitles='${escaped}'`;
15742
15459
  }
15743
- };
15744
- var hasNestedStructure = (obj, path$1) => {
15745
- let current = obj;
15746
- for (let i = 0; i < path$1.length - 1; i++) {
15747
- const key = path$1[i];
15748
- if (!current[key] || typeof current[key] !== "object") return false;
15749
- current = current[key];
15460
+ return `subtitles='${escaped}'`;
15461
+ }
15462
+ async function burnBiliDanmaku(videoPath, danmakuList, outputPath, options = {}) {
15463
+ const { removeSource = false, verticalMode = "off", videoCodec = "h265" } = options;
15464
+ const resolution = await getBiliResolution(videoPath);
15465
+ const frameRate = await getBiliFrameRate(videoPath);
15466
+ const sourceBitrate = await getVideoBitrate$1(videoPath);
15467
+ const canvas = calcCanvas$1(resolution.width, resolution.height, verticalMode);
15468
+ if (canvas.isVertical) logger.debug(`[BiliDanmaku] 竖屏模式: ${resolution.width}x${resolution.height} -> ${canvas.width}x${canvas.height}`);
15469
+ const assContent = generateBiliASS(danmakuList, canvas.width, canvas.height, options);
15470
+ const assPath = videoPath.replace(/\.[^.]+$/, "_danmaku.ass");
15471
+ fs.writeFileSync(assPath, assContent, "utf-8");
15472
+ logger.debug(`[BiliDanmaku] 弹幕字幕已生成: ${assPath}`);
15473
+ const result = await ffmpeg(`-y -i "${videoPath}" -vf "${buildFilter$1(canvas, assPath)}" -r ${frameRate} ${getEncoderParams$1(await detectEncoder$1(videoCodec), sourceBitrate)} -c:a copy "${outputPath}"`);
15474
+ Common.removeFile(assPath, true);
15475
+ if (result.status) {
15476
+ logger.mark(`[BiliDanmaku] 弹幕烧录成功: ${outputPath}`);
15477
+ if (removeSource) Common.removeFile(videoPath);
15478
+ } else logger.error("[BiliDanmaku] 弹幕烧录失败", result);
15479
+ return result.status;
15480
+ }
15481
+ async function mergeAndBurnBili(videoPath, audioPath, danmakuList, outputPath, options = {}) {
15482
+ const { removeSource = false, verticalMode = "off", videoCodec = "h265" } = options;
15483
+ if (!fs.existsSync(videoPath)) {
15484
+ logger.error(`[BiliDanmaku] 视频文件不存在: ${videoPath}`);
15485
+ return false;
15750
15486
  }
15751
- return path$1[path$1.length - 1] in current;
15752
- };
15487
+ if (!fs.existsSync(audioPath)) {
15488
+ logger.error(`[BiliDanmaku] 音频文件不存在: ${audioPath}`);
15489
+ return false;
15490
+ }
15491
+ const resolution = await getBiliResolution(videoPath);
15492
+ const frameRate = await getBiliFrameRate(videoPath);
15493
+ const sourceBitrate = await getVideoBitrate$1(videoPath);
15494
+ const canvas = calcCanvas$1(resolution.width, resolution.height, verticalMode);
15495
+ if (canvas.isVertical) logger.debug(`[BiliDanmaku] 竖屏模式: ${resolution.width}x${resolution.height} -> ${canvas.width}x${canvas.height}`);
15496
+ logger.debug(`[BiliDanmaku] 分辨率: ${canvas.width}x${canvas.height}, 帧率: ${frameRate}fps, 码率: ${sourceBitrate}kbps`);
15497
+ const assContent = generateBiliASS(danmakuList, canvas.width, canvas.height, options);
15498
+ const assPath = videoPath.replace(/\.[^.]+$/, "_danmaku.ass");
15499
+ fs.writeFileSync(assPath, assContent, "utf-8");
15500
+ logger.debug(`[BiliDanmaku] 弹幕字幕已生成: ${assPath},共 ${danmakuList.length} 条`);
15501
+ const result = await ffmpeg(`-y -i "${videoPath}" -i "${audioPath}" -f mp4 -vf "${buildFilter$1(canvas, assPath)}" -r ${frameRate} ${getEncoderParams$1(await detectEncoder$1(videoCodec), sourceBitrate)} -c:a aac -b:a 192k "${outputPath}"`);
15502
+ Common.removeFile(assPath, true);
15503
+ if (result.status) {
15504
+ logger.mark(`[BiliDanmaku] 视频合成+弹幕烧录成功: ${outputPath}`);
15505
+ if (removeSource) {
15506
+ Common.removeFile(videoPath);
15507
+ Common.removeFile(audioPath);
15508
+ }
15509
+ } else logger.error("[BiliDanmaku] 视频合成+弹幕烧录失败", result);
15510
+ return result.status;
15511
+ }
15753
15512
  await init_src();
15754
15513
  await init_date_fns();
15755
15514
  await init_locale();
15756
15515
  await init_utils$1();
15757
15516
  await init_amagiClient();
15758
15517
  await init_Config();
15759
- await init_danmaku$1();
15760
15518
  var img$1;
15761
15519
  var Bilibili = class extends Base {
15762
15520
  e;
@@ -15866,8 +15624,8 @@ var Bilibili = class extends Base {
15866
15624
  const imageUrl = await processImageUrl(v, infoData.data.data.title, index);
15867
15625
  messageElements.push(segment.image(imageUrl));
15868
15626
  }
15869
- const res = common.makeForward(messageElements, this.e.sender.userId, this.e.sender.nick);
15870
- this.e.bot.sendForwardMsg(this.e.contact, res, {
15627
+ const res = common.makeForward(messageElements, Config.app.fakeForward ? this.e.sender.userId : this.e.bot.account.selfId, Config.app.fakeForward ? this.e.sender.nick : this.e.bot.account.name);
15628
+ await this.e.bot.sendForwardMsg(this.e.contact, res, {
15871
15629
  source: "评论图片收集",
15872
15630
  summary: `查看${messageElements.length}张图片`,
15873
15631
  prompt: "B站评论解析结果",
@@ -16019,20 +15777,66 @@ var Bilibili = class extends Base {
16019
15777
  switch (dynamicInfo.data.data.item.type) {
16020
15778
  case DynamicType.DRAW: {
16021
15779
  const imgArray = [];
15780
+ const temp = [];
16022
15781
  const title = dynamicInfo.data.data.item.modules.module_dynamic.major.opus.title || "bilibili_dynamic";
16023
- for (const [index, img$2] of dynamicInfo.data.data.item.modules.module_dynamic.major.opus.pics.entries()) if (img$2.url) {
15782
+ for (const [index, img$2] of dynamicInfo.data.data.item.modules.module_dynamic.major.opus.pics.entries()) if (img$2.url) if (img$2.live_url) {
15783
+ const livePhoto = await downloadFile(img$2.live_url, {
15784
+ title: `Bilibili_tmp_V_${Date.now()}_${index}.mp4`,
15785
+ headers: BASE_HEADERS
15786
+ });
15787
+ if (livePhoto.filepath) {
15788
+ const outputPath = Common.tempDri.video + `Bilibili_Live_${Date.now()}_${index}.mp4`;
15789
+ let success;
15790
+ const staticImg = await downloadFile(img$2.url, {
15791
+ title: `Bilibili_static_${Date.now()}_${index}.jpg`,
15792
+ headers: BASE_HEADERS,
15793
+ filepath: Common.tempDri.images + `Bilibili_static_${Date.now()}_${index}.jpg`
15794
+ });
15795
+ const loopCount = 3;
15796
+ if (!staticImg.filepath) {
15797
+ await Common.removeFile(livePhoto.filepath, true);
15798
+ continue;
15799
+ }
15800
+ success = (await loopVideoWithTransition({
15801
+ inputPath: livePhoto.filepath,
15802
+ outputPath,
15803
+ loopCount,
15804
+ staticImagePath: staticImg.filepath,
15805
+ transitionEnabled: loopCount > 1
15806
+ })).success;
15807
+ if (success) {
15808
+ const filePath = Common.tempDri.video + `tmp_${Date.now()}.mp4`;
15809
+ fs.renameSync(outputPath, filePath);
15810
+ logger.mark(`视频文件重命名完成: ${outputPath.split("/").pop()} -> ${filePath.split("/").pop()}`);
15811
+ logger.mark("正在尝试删除缓存文件");
15812
+ await Common.removeFile(livePhoto.filepath, true);
15813
+ temp.push({
15814
+ filepath: filePath,
15815
+ totalBytes: 0
15816
+ });
15817
+ const videoPath = Config.upload.videoSendMode === "base64" ? `base64://${fs.readFileSync(filePath).toString("base64")}` : `file://${filePath}`;
15818
+ imgArray.push(segment.video(videoPath));
15819
+ const imageUrl = await processImageUrl(img$2.url, title, index);
15820
+ imgArray.push(segment.image(imageUrl));
15821
+ } else await Common.removeFile(livePhoto.filepath, true);
15822
+ }
15823
+ } else {
16024
15824
  const imageUrl = await processImageUrl(img$2.url, title, index);
16025
15825
  imgArray.push(segment.image(imageUrl));
16026
15826
  }
16027
15827
  if (imgArray.length === 1) this.e.reply(imgArray[0]);
16028
15828
  if (imgArray.length > 1) {
16029
- const forwardMsg = common.makeForward(imgArray, this.e.userId, this.e.sender.nick);
16030
- await this.e.bot.sendForwardMsg(this.e.contact, forwardMsg, {
16031
- source: "图片合集",
16032
- summary: `查看${imgArray.length}张图片消息`,
16033
- prompt: "B站图文动态解析结果",
16034
- news: [{ text: "点击查看解析结果" }]
16035
- });
15829
+ const forwardMsg = common.makeForward(imgArray, Config.app.fakeForward ? this.e.sender.userId : this.e.bot.account.selfId, Config.app.fakeForward ? this.e.sender.nick : this.e.bot.account.name);
15830
+ try {
15831
+ await this.e.bot.sendForwardMsg(this.e.contact, forwardMsg, {
15832
+ source: "图片合集",
15833
+ summary: `查看${imgArray.length}张图片消息`,
15834
+ prompt: "B站图文动态解析结果",
15835
+ news: [{ text: "点击查看解析结果" }]
15836
+ });
15837
+ } finally {
15838
+ for (const item of temp) await Common.removeFile(item.filepath, true);
15839
+ }
16036
15840
  }
16037
15841
  const dynamicCARD$1 = JSON.parse(dynamicInfoCard.data.data.card.card);
16038
15842
  if ("topic" in dynamicInfo.data.data.item.modules.module_dynamic && dynamicInfo.data.data.item.modules.module_dynamic.topic !== null) {
@@ -16287,8 +16091,8 @@ var Bilibili = class extends Base {
16287
16091
  }
16288
16092
  if (messageElements.length === 1) this.e.reply(messageElements[0]);
16289
16093
  if (messageElements.length > 1) {
16290
- const forwardMsg = common.makeForward(messageElements, this.e.userId, this.e.sender.nick);
16291
- this.e.bot.sendForwardMsg(this.e.contact, forwardMsg, {
16094
+ const forwardMsg = common.makeForward(messageElements, Config.app.fakeForward ? this.e.sender.userId : this.e.bot.account.selfId, Config.app.fakeForward ? this.e.sender.nick : this.e.bot.account.name);
16095
+ await this.e.bot.sendForwardMsg(this.e.contact, forwardMsg, {
16292
16096
  source: "图片合集",
16293
16097
  summary: `查看${messageElements.length}张图片消息`,
16294
16098
  prompt: "B站专栏动态解析结果",
@@ -16337,8 +16141,8 @@ var Bilibili = class extends Base {
16337
16141
  const imageUrl = await processImageUrl(v, title, index);
16338
16142
  messageElements.push(segment.image(imageUrl));
16339
16143
  }
16340
- const res = common.makeForward(messageElements, this.e.sender.userId, this.e.sender.nick);
16341
- this.e.bot.sendForwardMsg(this.e.contact, res, {
16144
+ const res = common.makeForward(messageElements, Config.app.fakeForward ? this.e.sender.userId : this.e.bot.account.selfId, Config.app.fakeForward ? this.e.sender.nick : this.e.bot.account.name);
16145
+ await this.e.bot.sendForwardMsg(this.e.contact, res, {
16342
16146
  source: "评论图片收集",
16343
16147
  summary: `查看${messageElements.length}张图片`,
16344
16148
  prompt: "B站评论解析结果",
@@ -17776,24 +17580,74 @@ var Bilibilipush = class extends Base {
17776
17580
  await Common.removeFile(mp3File.filepath, true);
17777
17581
  }
17778
17582
  }
17779
- }
17780
- break;
17781
- case "DYNAMIC_TYPE_DRAW": {
17782
- const imgArray = [];
17783
- const title = data$1[dynamicId].Dynamic_Data.modules.module_dynamic.major?.opus?.title || "bilibili_dynamic";
17784
- const images = data$1[dynamicId].Dynamic_Data.modules.module_dynamic.major && data$1[dynamicId].Dynamic_Data.modules.module_dynamic?.major?.draw?.items || data$1[dynamicId].Dynamic_Data.modules.module_dynamic?.major?.opus.pics;
17785
- if (images.length === 0) break;
17786
- for (const [index, img2] of images.entries()) {
17787
- const imageUrl = await processImageUrl(img2.src ?? img2.url, title, index);
17788
- imgArray.push(segment.image(imageUrl));
17583
+ }
17584
+ break;
17585
+ case "DYNAMIC_TYPE_DRAW": {
17586
+ const imgArray = [];
17587
+ const temp = [];
17588
+ const title = data$1[dynamicId].Dynamic_Data.modules.module_dynamic.major?.opus?.title || "bilibili_dynamic";
17589
+ const images = data$1[dynamicId].Dynamic_Data.modules.module_dynamic.major && data$1[dynamicId].Dynamic_Data.modules.module_dynamic?.major?.draw?.items || data$1[dynamicId].Dynamic_Data.modules.module_dynamic?.major?.opus.pics;
17590
+ if (images.length === 0) break;
17591
+ for (const [index, img2] of images.entries()) {
17592
+ const imageSrc = img2.src ?? img2.url;
17593
+ if (img2.live_url && imageSrc) {
17594
+ const livePhoto = await downloadFile(img2.live_url, {
17595
+ title: `Bilibili_tmp_V_${Date.now()}_${index}.mp4`,
17596
+ headers: bilibiliBaseHeaders
17597
+ });
17598
+ if (livePhoto.filepath) {
17599
+ const outputPath = Common.tempDri.video + `Bilibili_Live_${Date.now()}_${index}.mp4`;
17600
+ const staticImg = await downloadFile(imageSrc, {
17601
+ title: `Bilibili_static_${Date.now()}_${index}.jpg`,
17602
+ headers: bilibiliBaseHeaders,
17603
+ filepath: Common.tempDri.images + `Bilibili_static_${Date.now()}_${index}.jpg`
17604
+ });
17605
+ const loopCount = 3;
17606
+ if (!staticImg.filepath) {
17607
+ await Common.removeFile(livePhoto.filepath, true);
17608
+ continue;
17609
+ }
17610
+ if ((await loopVideoWithTransition({
17611
+ inputPath: livePhoto.filepath,
17612
+ outputPath,
17613
+ loopCount,
17614
+ staticImagePath: staticImg.filepath,
17615
+ transitionEnabled: loopCount > 1
17616
+ })).success) {
17617
+ const filePath = Common.tempDri.video + `tmp_${Date.now()}.mp4`;
17618
+ fs.renameSync(outputPath, filePath);
17619
+ logger.mark(`视频文件重命名完成: ${outputPath.split("/").pop()} -> ${filePath.split("/").pop()}`);
17620
+ logger.mark("正在尝试删除缓存文件");
17621
+ await Common.removeFile(staticImg.filepath, true);
17622
+ temp.push({
17623
+ filepath: filePath,
17624
+ totalBytes: 0
17625
+ });
17626
+ const videoPath = Config.upload.videoSendMode === "base64" ? `base64://${fs.readFileSync(filePath).toString("base64")}` : `file://${filePath}`;
17627
+ imgArray.push(segment.video(videoPath));
17628
+ const imageUrl = await processImageUrl(imageSrc, title, index);
17629
+ imgArray.push(segment.image(imageUrl));
17630
+ continue;
17631
+ }
17632
+ await Common.removeFile(livePhoto.filepath, true);
17633
+ }
17634
+ }
17635
+ if (imageSrc) {
17636
+ const imageUrl = await processImageUrl(imageSrc, title, index);
17637
+ imgArray.push(segment.image(imageUrl));
17638
+ }
17789
17639
  }
17790
17640
  const forwardMsg = common.makeForward(imgArray, botId, bot.account.name);
17791
- bot.sendForwardMsg(Contact, forwardMsg, {
17792
- source: "图片合集",
17793
- summary: `查看${imgArray.length}张图片消息`,
17794
- prompt: "B站图文动态解析结果",
17795
- news: [{ text: "点击查看解析结果" }]
17796
- });
17641
+ try {
17642
+ await bot.sendForwardMsg(Contact, forwardMsg, {
17643
+ source: "图片合集",
17644
+ summary: `查看${imgArray.length}张图片消息`,
17645
+ prompt: "B站图文动态解析结果",
17646
+ news: [{ text: "点击查看解析结果" }]
17647
+ });
17648
+ } finally {
17649
+ for (const item of temp) await Common.removeFile(item.filepath, true);
17650
+ }
17797
17651
  break;
17798
17652
  }
17799
17653
  case "DYNAMIC_TYPE_ARTICLE": {
@@ -17811,7 +17665,7 @@ var Bilibilipush = class extends Base {
17811
17665
  if (messageElements.length === 1) bot.sendMsg(Contact, messageElements);
17812
17666
  if (messageElements.length > 1) {
17813
17667
  const forwardMsg = common.makeForward(messageElements, botId, bot.account.name);
17814
- bot.sendForwardMsg(Contact, forwardMsg, {
17668
+ await bot.sendForwardMsg(Contact, forwardMsg, {
17815
17669
  source: "图片合集",
17816
17670
  summary: `查看${messageElements.length}张图片消息`,
17817
17671
  prompt: "B站专栏动态解析结果",
@@ -18036,7 +17890,6 @@ var skipDynamic$1 = async (PushItem) => {
18036
17890
  logger.debug(`检查动态是否需要过滤:https://t.bilibili.com/${PushItem.Dynamic_Data.id_str}`);
18037
17891
  return await bilibiliDBInstance.shouldFilter(PushItem, tags);
18038
17892
  };
18039
- init_danmaku$1();
18040
17893
  init_riskControl();
18041
17894
  await init_date_fns();
18042
17895
  await init_locale();
@@ -18108,127 +17961,433 @@ const douyinComments = async (data$1, emojidata) => {
18108
17961
  CommentsData: [],
18109
17962
  image_url: []
18110
17963
  };
18111
- let id = 1;
18112
- for (const comment of data$1.data.comments) {
18113
- const cid = comment.cid;
18114
- const aweme_id = comment.aweme_id;
18115
- const nickname = comment.user.nickname;
18116
- const userimageurl = comment.user.avatar_thumb.url_list[0];
18117
- let text = comment.text;
18118
- const ip = comment.ip_label ?? "未知";
18119
- const time = comment.create_time;
18120
- const label_type = comment.label_type ?? -1;
18121
- const sticker = comment.sticker ? comment.sticker.animate_url.url_list[0] : null;
18122
- let digg_count = comment.digg_count;
18123
- const imageurl = comment.image_list && comment.image_list?.[0] && comment.image_list?.[0].origin_url && comment.image_list?.[0].origin_url.url_list ? comment.image_list?.[0].origin_url.url_list[0] : null;
18124
- const status_label = comment.label_list?.[0]?.text ?? null;
18125
- const userintextlongid = comment.text_extra && comment.text_extra[0] && comment.text_extra[0].sec_uid ? comment.text_extra.map((extra) => extra.sec_uid) : null;
18126
- const search_text = comment.text_extra && comment.text_extra[0] && comment.text_extra[0].search_text ? comment.text_extra[0].search_text && comment.text_extra.map((extra) => ({
18127
- search_text: extra.search_text,
18128
- search_query_id: extra.search_query_id
18129
- })) : null;
18130
- const relativeTime = getRelativeTimeFromTimestamp$2(time);
18131
- text = processTextFormatting(text);
18132
- text = await processAtUsers$1(text, userintextlongid);
18133
- text = processCommentEmojis$1(text, emojidata);
18134
- const processedImageUrl = await processCommentImage(imageurl);
18135
- if (processedImageUrl) imageUrls.push(processedImageUrl.startsWith("data:image/jpeg;base64,") ? `base64://${processedImageUrl.replace("data:image/jpeg;base64,", "")}` : processedImageUrl);
18136
- if (sticker) imageUrls.push(sticker);
18137
- if (digg_count > 1e4) digg_count = (digg_count / 1e4).toFixed(1) + "w";
18138
- const replyComment = await douyinFetcher.fetchCommentReplies({
18139
- aweme_id,
18140
- comment_id: cid,
18141
- typeMode: "strict",
18142
- number: Config.douyin.subCommentLimit
18143
- });
18144
- const replyCommentsList = [];
18145
- if (replyComment.data.comments && replyComment.data.comments.length > 0) for (const reply of replyComment.data.comments) {
18146
- const replyItem = reply;
18147
- const replyUserintextlongid = replyItem.text_extra && replyItem.text_extra[0] && replyItem.text_extra[0].sec_uid ? replyItem.text_extra.filter((extra) => extra.sec_uid).map((extra) => extra.sec_uid) : null;
18148
- const processedReplyText = await processAtUsers$1(replyItem.text, replyUserintextlongid);
18149
- const replyImageUrl = replyItem.image_list?.[0]?.origin_url?.url_list?.[0];
18150
- const replyStickerUrl = replyItem.sticker?.animate_url?.url_list?.[0];
18151
- let replyImageList = null;
18152
- if (replyImageUrl) {
18153
- const processedReplyImage = await processCommentImage(replyImageUrl);
18154
- if (processedReplyImage) {
18155
- replyImageList = [processedReplyImage];
18156
- imageUrls.push(processedReplyImage.startsWith("data:image/jpeg;base64,") ? `base64://${processedReplyImage.replace("data:image/jpeg;base64,", "")}` : processedReplyImage);
17964
+ let id = 1;
17965
+ for (const comment of data$1.data.comments) {
17966
+ const cid = comment.cid;
17967
+ const aweme_id = comment.aweme_id;
17968
+ const nickname = comment.user.nickname;
17969
+ const userimageurl = comment.user.avatar_thumb.url_list[0];
17970
+ let text = comment.text;
17971
+ const ip = comment.ip_label ?? "未知";
17972
+ const time = comment.create_time;
17973
+ const label_type = comment.label_type ?? -1;
17974
+ const sticker = comment.sticker ? comment.sticker.animate_url.url_list[0] : null;
17975
+ let digg_count = comment.digg_count;
17976
+ const imageurl = comment.image_list && comment.image_list?.[0] && comment.image_list?.[0].origin_url && comment.image_list?.[0].origin_url.url_list ? comment.image_list?.[0].origin_url.url_list[0] : null;
17977
+ const status_label = comment.label_list?.[0]?.text ?? null;
17978
+ const userintextlongid = comment.text_extra && comment.text_extra[0] && comment.text_extra[0].sec_uid ? comment.text_extra.map((extra) => extra.sec_uid) : null;
17979
+ const search_text = comment.text_extra && comment.text_extra[0] && comment.text_extra[0].search_text ? comment.text_extra[0].search_text && comment.text_extra.map((extra) => ({
17980
+ search_text: extra.search_text,
17981
+ search_query_id: extra.search_query_id
17982
+ })) : null;
17983
+ const relativeTime = getRelativeTimeFromTimestamp$2(time);
17984
+ text = processTextFormatting(text);
17985
+ text = await processAtUsers$1(text, userintextlongid);
17986
+ text = processCommentEmojis$1(text, emojidata);
17987
+ const processedImageUrl = await processCommentImage(imageurl);
17988
+ if (processedImageUrl) imageUrls.push(processedImageUrl.startsWith("data:image/jpeg;base64,") ? `base64://${processedImageUrl.replace("data:image/jpeg;base64,", "")}` : processedImageUrl);
17989
+ if (sticker) imageUrls.push(sticker);
17990
+ if (digg_count > 1e4) digg_count = (digg_count / 1e4).toFixed(1) + "w";
17991
+ const replyComment = await douyinFetcher.fetchCommentReplies({
17992
+ aweme_id,
17993
+ comment_id: cid,
17994
+ typeMode: "strict",
17995
+ number: Config.douyin.subCommentLimit
17996
+ });
17997
+ const replyCommentsList = [];
17998
+ if (replyComment.data.comments && replyComment.data.comments.length > 0) for (const reply of replyComment.data.comments) {
17999
+ const replyItem = reply;
18000
+ const replyUserintextlongid = replyItem.text_extra && replyItem.text_extra[0] && replyItem.text_extra[0].sec_uid ? replyItem.text_extra.filter((extra) => extra.sec_uid).map((extra) => extra.sec_uid) : null;
18001
+ const processedReplyText = await processAtUsers$1(replyItem.text, replyUserintextlongid);
18002
+ const replyImageUrl = replyItem.image_list?.[0]?.origin_url?.url_list?.[0];
18003
+ const replyStickerUrl = replyItem.sticker?.animate_url?.url_list?.[0];
18004
+ let replyImageList = null;
18005
+ if (replyImageUrl) {
18006
+ const processedReplyImage = await processCommentImage(replyImageUrl);
18007
+ if (processedReplyImage) {
18008
+ replyImageList = [processedReplyImage];
18009
+ imageUrls.push(processedReplyImage.startsWith("data:image/jpeg;base64,") ? `base64://${processedReplyImage.replace("data:image/jpeg;base64,", "")}` : processedReplyImage);
18010
+ }
18011
+ } else if (replyStickerUrl) {
18012
+ replyImageList = [replyStickerUrl];
18013
+ imageUrls.push(replyStickerUrl);
18014
+ }
18015
+ replyCommentsList.push({
18016
+ create_time: getRelativeTimeFromTimestamp$2(replyItem.create_time),
18017
+ nickname: replyItem.user.nickname,
18018
+ userimageurl: replyItem.user.avatar_thumb.url_list[0],
18019
+ text: processCommentEmojis$1(processedReplyText, emojidata),
18020
+ digg_count: replyItem.digg_count > 1e4 ? (replyItem.digg_count / 1e4).toFixed(1) + "w" : replyItem.digg_count,
18021
+ ip_label: replyItem.ip_label,
18022
+ text_extra: replyItem.text_extra,
18023
+ label_text: replyItem.label_text,
18024
+ image_list: replyImageList,
18025
+ cid: replyItem.cid,
18026
+ reply_to_reply_id: replyItem.reply_to_reply_id,
18027
+ reply_to_username: replyItem.reply_to_username
18028
+ });
18029
+ }
18030
+ const commentObj = {
18031
+ id: id++,
18032
+ replyComment: replyCommentsList.length > 0 ? replyCommentsList : void 0,
18033
+ cid,
18034
+ aweme_id,
18035
+ nickname,
18036
+ userimageurl,
18037
+ text,
18038
+ digg_count,
18039
+ ip_label: ip,
18040
+ create_time: relativeTime,
18041
+ commentimage: processedImageUrl ?? void 0,
18042
+ label_type,
18043
+ sticker: sticker ?? void 0,
18044
+ status_label: status_label ?? void 0,
18045
+ is_At_user_id: userintextlongid,
18046
+ search_text,
18047
+ is_author_digged: comment.is_author_digged ?? false
18048
+ };
18049
+ jsonArray.push(commentObj);
18050
+ }
18051
+ jsonArray.sort((a, b) => {
18052
+ const aCount = typeof a.digg_count === "string" && a.digg_count.includes("w") ? parseFloat(a.digg_count) * 1e4 : typeof a.digg_count === "number" ? a.digg_count : 0;
18053
+ return (typeof b.digg_count === "string" && b.digg_count.includes("w") ? parseFloat(b.digg_count) * 1e4 : typeof b.digg_count === "number" ? b.digg_count : 0) - aCount;
18054
+ });
18055
+ const indexLabelTypeOne = jsonArray.findIndex((comment) => comment.label_type === 1);
18056
+ if (indexLabelTypeOne !== -1) {
18057
+ const commentTypeOne = jsonArray.splice(indexLabelTypeOne, 1)[0];
18058
+ jsonArray.unshift(commentTypeOne);
18059
+ }
18060
+ return {
18061
+ CommentsData: jsonArray,
18062
+ image_url: imageUrls
18063
+ };
18064
+ };
18065
+ var getRelativeTimeFromTimestamp$2 = (timestamp) => {
18066
+ const commentDate = fromUnixTime(timestamp);
18067
+ const diffSeconds = differenceInSeconds(/* @__PURE__ */ new Date(), commentDate);
18068
+ if (diffSeconds < 30) return "刚刚";
18069
+ if (diffSeconds < 7776e3) return formatDistanceToNow(commentDate, {
18070
+ locale: zhCN,
18071
+ addSuffix: true
18072
+ });
18073
+ return format(commentDate, "yyyy-MM-dd");
18074
+ };
18075
+ await init_utils$1();
18076
+ var ENCODER_PRIORITY = {
18077
+ h264: [
18078
+ "h264_nvenc",
18079
+ "h264_qsv",
18080
+ "h264_amf",
18081
+ "libx264"
18082
+ ],
18083
+ h265: [
18084
+ "hevc_nvenc",
18085
+ "hevc_qsv",
18086
+ "hevc_amf",
18087
+ "libx265"
18088
+ ],
18089
+ av1: [
18090
+ "av1_nvenc",
18091
+ "av1_qsv",
18092
+ "av1_amf",
18093
+ "libsvtav1",
18094
+ "libaom-av1"
18095
+ ]
18096
+ };
18097
+ var SOFTWARE_FALLBACK = {
18098
+ h264: "libx264",
18099
+ h265: "libx265",
18100
+ av1: "libsvtav1"
18101
+ };
18102
+ var cachedEncoders = {};
18103
+ async function detectEncoder(codec) {
18104
+ if (cachedEncoders[codec]) return cachedEncoders[codec];
18105
+ logger.debug(`[DouyinDanmaku] 开始检测 ${codec.toUpperCase()} 编码器...`);
18106
+ for (const encoder of ENCODER_PRIORITY[codec]) try {
18107
+ if ((await ffmpeg(`-f lavfi -i color=c=black:s=320x240:d=0.1 -c:v ${encoder} -f null -`)).status) {
18108
+ cachedEncoders[codec] = encoder;
18109
+ logger.info(`[DouyinDanmaku] 使用 ${codec.toUpperCase()} 编码器: ${encoder}`);
18110
+ return encoder;
18111
+ }
18112
+ } catch {}
18113
+ const fallback = SOFTWARE_FALLBACK[codec];
18114
+ cachedEncoders[codec] = fallback;
18115
+ logger.info(`[DouyinDanmaku] 回退到软件编码器: ${fallback}`);
18116
+ return fallback;
18117
+ }
18118
+ async function getVideoBitrate(path$1) {
18119
+ try {
18120
+ const fileSize = fs.statSync(path$1).size;
18121
+ const { stdout } = await ffprobe(`-v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 "${path$1}"`);
18122
+ const duration = parseFloat(stdout.trim());
18123
+ if (duration > 0 && fileSize > 0) return Math.round(fileSize * 8 / duration / 1e3);
18124
+ } catch {}
18125
+ try {
18126
+ const { stdout } = await ffprobe(`-v error -select_streams v:0 -show_entries stream=bit_rate -of default=noprint_wrappers=1:nokey=1 "${path$1}"`);
18127
+ const bitrate = parseInt(stdout.trim());
18128
+ if (bitrate > 0) return Math.round(bitrate / 1e3);
18129
+ } catch {}
18130
+ return 0;
18131
+ }
18132
+ function getEncoderParams(encoder, targetBitrate) {
18133
+ const threads = Math.max(1, Math.floor(os.cpus().length / 2));
18134
+ if (targetBitrate && targetBitrate > 0) {
18135
+ const adjustedBitrate = Math.round(targetBitrate * 1.4);
18136
+ const bitrateK = `${adjustedBitrate}k`;
18137
+ const maxrate = `${Math.round(adjustedBitrate * 2.5)}k`;
18138
+ const bufsize = `${Math.round(adjustedBitrate * 4)}k`;
18139
+ if (encoder === "h264_nvenc") return `-c:v h264_nvenc -preset p4 -rc vbr -b:v ${bitrateK} -maxrate ${maxrate} -bufsize ${bufsize}`;
18140
+ if (encoder === "h264_qsv") return `-c:v h264_qsv -preset medium -b:v ${bitrateK} -maxrate ${maxrate} -bufsize ${bufsize}`;
18141
+ if (encoder === "h264_amf") return `-c:v h264_amf -quality balanced -rc vbr_peak -b:v ${bitrateK} -maxrate ${maxrate}`;
18142
+ if (encoder === "libx264") return `-c:v libx264 -preset medium -b:v ${bitrateK} -maxrate ${maxrate} -bufsize ${bufsize} -threads ${threads}`;
18143
+ if (encoder === "hevc_nvenc") return `-c:v hevc_nvenc -preset p4 -rc vbr -b:v ${bitrateK} -maxrate ${maxrate} -bufsize ${bufsize}`;
18144
+ if (encoder === "hevc_qsv") return `-c:v hevc_qsv -preset medium -b:v ${bitrateK} -maxrate ${maxrate} -bufsize ${bufsize}`;
18145
+ if (encoder === "hevc_amf") return `-c:v hevc_amf -quality balanced -rc vbr_peak -b:v ${bitrateK} -maxrate ${maxrate}`;
18146
+ if (encoder === "libx265") return `-c:v libx265 -preset medium -b:v ${bitrateK} -maxrate ${maxrate} -bufsize ${bufsize} -threads ${threads}`;
18147
+ if (encoder === "av1_nvenc") return `-c:v av1_nvenc -preset p4 -rc vbr -b:v ${bitrateK} -maxrate ${maxrate} -bufsize ${bufsize}`;
18148
+ if (encoder === "av1_qsv") return `-c:v av1_qsv -preset medium -b:v ${bitrateK} -maxrate ${maxrate} -bufsize ${bufsize}`;
18149
+ if (encoder === "av1_amf") return `-c:v av1_amf -quality balanced -rc vbr_peak -b:v ${bitrateK} -maxrate ${maxrate}`;
18150
+ if (encoder === "libsvtav1") return `-c:v libsvtav1 -preset 6 -b:v ${bitrateK} -maxrate ${maxrate} -bufsize ${bufsize} -threads ${threads}`;
18151
+ if (encoder === "libaom-av1") return `-c:v libaom-av1 -cpu-used 4 -b:v ${bitrateK} -maxrate ${maxrate} -bufsize ${bufsize} -threads ${threads}`;
18152
+ return `-c:v libx265 -preset medium -b:v ${bitrateK} -maxrate ${maxrate} -bufsize ${bufsize} -threads ${threads}`;
18153
+ }
18154
+ if (encoder === "h264_nvenc") return "-c:v h264_nvenc -preset p4 -rc vbr -cq 23";
18155
+ if (encoder === "h264_qsv") return "-c:v h264_qsv -preset medium -global_quality 23";
18156
+ if (encoder === "h264_amf") return "-c:v h264_amf -quality balanced -rc cqp -qp_i 23 -qp_p 23";
18157
+ if (encoder === "libx264") return `-c:v libx264 -crf 23 -preset medium -threads ${threads}`;
18158
+ if (encoder === "hevc_nvenc") return "-c:v hevc_nvenc -preset p4 -rc vbr -cq 28";
18159
+ if (encoder === "hevc_qsv") return "-c:v hevc_qsv -preset medium -global_quality 28";
18160
+ if (encoder === "hevc_amf") return "-c:v hevc_amf -quality balanced -rc cqp -qp_i 28 -qp_p 28";
18161
+ if (encoder === "libx265") return `-c:v libx265 -crf 28 -preset medium -threads ${threads}`;
18162
+ if (encoder === "av1_nvenc") return "-c:v av1_nvenc -preset p4 -rc vbr -cq 30";
18163
+ if (encoder === "av1_qsv") return "-c:v av1_qsv -preset medium -global_quality 30";
18164
+ if (encoder === "av1_amf") return "-c:v av1_amf -quality balanced -rc cqp -qp_i 30 -qp_p 30";
18165
+ if (encoder === "libsvtav1") return `-c:v libsvtav1 -crf 30 -preset 6 -threads ${threads}`;
18166
+ if (encoder === "libaom-av1") return `-c:v libaom-av1 -crf 30 -cpu-used 4 -threads ${threads}`;
18167
+ return `-c:v libx265 -crf 28 -preset medium -threads ${threads}`;
18168
+ }
18169
+ var toASSTime = (ms) => {
18170
+ const s = ms / 1e3;
18171
+ const h = Math.floor(s / 3600);
18172
+ const m = Math.floor(s % 3600 / 60);
18173
+ const sec = Math.floor(s % 60);
18174
+ const cs = Math.floor(s % 1 * 100);
18175
+ return `${h}:${m.toString().padStart(2, "0")}:${sec.toString().padStart(2, "0")}.${cs.toString().padStart(2, "0")}`;
18176
+ };
18177
+ var estimateWidth = (text, fontSize) => {
18178
+ let w = 0;
18179
+ for (const c of text) w += c.charCodeAt(0) > 127 ? fontSize : fontSize * .5;
18180
+ return w;
18181
+ };
18182
+ var escapeASS = (text) => text.replace(/\\/g, "\\\\").replace(/\{/g, "\\{").replace(/\}/g, "\\}").replace(/\n/g, "\\N");
18183
+ var escapeWinPath = (path$1) => path$1.replace(/\\/g, "/").replace(/:/g, "\\:");
18184
+ var isLandscape = (w, h) => w > h;
18185
+ async function getDouyinResolution(path$1) {
18186
+ try {
18187
+ const { stdout } = await ffprobe(`-v error -select_streams v:0 -show_entries stream=width,height -of csv=s=x:p=0 "${path$1}"`);
18188
+ const [w, h] = stdout.trim().split("x").map(Number);
18189
+ if (w && h) return {
18190
+ width: w,
18191
+ height: h
18192
+ };
18193
+ } catch {}
18194
+ try {
18195
+ const match = ((await ffmpeg(`-i "${path$1}" -f null -`, { timeout: 5e3 })).stderr || "").match(/(\d{3,4})x(\d{3,4})/);
18196
+ if (match) return {
18197
+ width: parseInt(match[1]),
18198
+ height: parseInt(match[2])
18199
+ };
18200
+ } catch {}
18201
+ return {
18202
+ width: 1080,
18203
+ height: 1920
18204
+ };
18205
+ }
18206
+ async function getDouyinFrameRate(path$1) {
18207
+ try {
18208
+ const { stdout } = await ffprobe(`-v error -select_streams v:0 -show_entries stream=r_frame_rate -of default=noprint_wrappers=1:nokey=1 "${path$1}"`);
18209
+ const [num, den] = stdout.trim().split("/").map(Number);
18210
+ if (den > 0) return num / den;
18211
+ } catch {}
18212
+ try {
18213
+ const fpsMatch = ((await ffmpeg(`-i "${path$1}" -f null -`, { timeout: 5e3 })).stderr || "").match(/(\d+(?:\.\d+)?)\s*fps/);
18214
+ if (fpsMatch) return parseFloat(fpsMatch[1]);
18215
+ } catch {}
18216
+ return 30;
18217
+ }
18218
+ var FONT_SIZE_MAP = {
18219
+ small: {
18220
+ base: 25,
18221
+ trackH: 30
18222
+ },
18223
+ medium: {
18224
+ base: 32,
18225
+ trackH: 38
18226
+ },
18227
+ large: {
18228
+ base: 40,
18229
+ trackH: 46
18230
+ }
18231
+ };
18232
+ function generateDouyinASS(danmakuList, width, height, options = {}) {
18233
+ const { scrollTime = 8, danmakuOpacity = 70, fontName = "Microsoft YaHei", danmakuArea = .5, danmakuFontSize = "medium" } = options;
18234
+ const fontScale = height / 1080;
18235
+ const sizeConfig = FONT_SIZE_MAP[danmakuFontSize];
18236
+ const fontSize = Math.round(sizeConfig.base * fontScale);
18237
+ const trackH = Math.round(sizeConfig.trackH * fontScale);
18238
+ const topMargin = Math.round(5 * fontScale);
18239
+ const areaHeight = Math.floor(height * danmakuArea) - topMargin;
18240
+ const trackCount = Math.max(1, Math.floor((areaHeight - fontSize) / trackH));
18241
+ const minGap = Math.round(15 * fontScale);
18242
+ const alpha = Math.round((100 - Math.max(0, Math.min(100, danmakuOpacity))) * 2.55).toString(16).padStart(2, "0").toUpperCase();
18243
+ let ass = `[Script Info]\nTitle: Douyin Danmaku\nScriptType: v4.00+\nPlayResX: ${width}\nPlayResY: ${height}\nTimer: 100.0000\n\n[V4+ Styles]\nFormat: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding\nStyle: Scroll,${fontName},${fontSize},&H${alpha}FFFFFF,&H${alpha}FFFFFF,&H${alpha}000000,&H${alpha}000000,0,0,0,0,100,100,0,0,1,0.8,0,2,0,0,0,1\n\n[Events]\nFormat: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text\n`;
18244
+ const scrollTracks = Array(trackCount).fill(null);
18245
+ const calcDistance = (last, startTime, duration, textWidth) => {
18246
+ const lastSpeed = (width + last.textWidth) / last.duration;
18247
+ const newSpeed = (width + textWidth) / duration;
18248
+ let dist = width - (width - lastSpeed * (startTime - last.startTime) + last.textWidth) - minGap;
18249
+ if (newSpeed > lastSpeed) {
18250
+ const lastRightXAtEnd = width - lastSpeed * (startTime + duration - last.startTime) + last.textWidth;
18251
+ dist = Math.min(dist, -textWidth - lastRightXAtEnd - minGap);
18252
+ }
18253
+ return dist;
18254
+ };
18255
+ const sorted = [...danmakuList.filter((dm) => dm.text && dm.text.trim())].sort((a, b) => a.offset_time - b.offset_time);
18256
+ for (const dm of sorted) {
18257
+ const startTime = dm.offset_time;
18258
+ const textWidth = estimateWidth(dm.text, fontSize);
18259
+ const content = escapeASS(dm.text);
18260
+ const duration = scrollTime * 1e3;
18261
+ const endTime = startTime + duration;
18262
+ for (let i = 0; i < scrollTracks.length; i++) {
18263
+ const t = scrollTracks[i];
18264
+ if (t && t.startTime + t.duration <= startTime) scrollTracks[i] = null;
18265
+ }
18266
+ let bestIdx = -1;
18267
+ let bestDist = -Infinity;
18268
+ for (let i = 0; i < scrollTracks.length; i++) {
18269
+ const t = scrollTracks[i];
18270
+ if (!t) {
18271
+ if (bestIdx === -1) bestIdx = i;
18272
+ continue;
18273
+ }
18274
+ const d = calcDistance(t, startTime, duration, textWidth);
18275
+ if (d >= 0) {
18276
+ if (bestDist < 0 || d < bestDist) {
18277
+ bestDist = d;
18278
+ bestIdx = i;
18157
18279
  }
18158
- } else if (replyStickerUrl) {
18159
- replyImageList = [replyStickerUrl];
18160
- imageUrls.push(replyStickerUrl);
18161
18280
  }
18162
- replyCommentsList.push({
18163
- create_time: getRelativeTimeFromTimestamp$2(replyItem.create_time),
18164
- nickname: replyItem.user.nickname,
18165
- userimageurl: replyItem.user.avatar_thumb.url_list[0],
18166
- text: processCommentEmojis$1(processedReplyText, emojidata),
18167
- digg_count: replyItem.digg_count > 1e4 ? (replyItem.digg_count / 1e4).toFixed(1) + "w" : replyItem.digg_count,
18168
- ip_label: replyItem.ip_label,
18169
- text_extra: replyItem.text_extra,
18170
- label_text: replyItem.label_text,
18171
- image_list: replyImageList,
18172
- cid: replyItem.cid,
18173
- reply_to_reply_id: replyItem.reply_to_reply_id,
18174
- reply_to_username: replyItem.reply_to_username
18175
- });
18176
18281
  }
18177
- const commentObj = {
18178
- id: id++,
18179
- replyComment: replyCommentsList.length > 0 ? replyCommentsList : void 0,
18180
- cid,
18181
- aweme_id,
18182
- nickname,
18183
- userimageurl,
18184
- text,
18185
- digg_count,
18186
- ip_label: ip,
18187
- create_time: relativeTime,
18188
- commentimage: processedImageUrl ?? void 0,
18189
- label_type,
18190
- sticker: sticker ?? void 0,
18191
- status_label: status_label ?? void 0,
18192
- is_At_user_id: userintextlongid,
18193
- search_text,
18194
- is_author_digged: comment.is_author_digged ?? false
18282
+ if (bestIdx === -1 || bestDist < 0 && scrollTracks[bestIdx] !== null) continue;
18283
+ scrollTracks[bestIdx] = {
18284
+ startTime,
18285
+ duration,
18286
+ textWidth
18195
18287
  };
18196
- jsonArray.push(commentObj);
18288
+ const y = topMargin + bestIdx * trackH + fontSize;
18289
+ ass += `Dialogue: 0,${toASSTime(startTime)},${toASSTime(endTime)},Scroll,,0,0,0,,{\\an7}{\\move(${width},${y},${-textWidth},${y})}${content}\n`;
18197
18290
  }
18198
- jsonArray.sort((a, b) => {
18199
- const aCount = typeof a.digg_count === "string" && a.digg_count.includes("w") ? parseFloat(a.digg_count) * 1e4 : typeof a.digg_count === "number" ? a.digg_count : 0;
18200
- return (typeof b.digg_count === "string" && b.digg_count.includes("w") ? parseFloat(b.digg_count) * 1e4 : typeof b.digg_count === "number" ? b.digg_count : 0) - aCount;
18201
- });
18202
- const indexLabelTypeOne = jsonArray.findIndex((comment) => comment.label_type === 1);
18203
- if (indexLabelTypeOne !== -1) {
18204
- const commentTypeOne = jsonArray.splice(indexLabelTypeOne, 1)[0];
18205
- jsonArray.unshift(commentTypeOne);
18291
+ return ass;
18292
+ }
18293
+ var MAX_OUTPUT_WIDTH = 2160;
18294
+ function calcCanvas(origW, origH, verticalMode) {
18295
+ if (verticalMode === "off") return {
18296
+ width: origW,
18297
+ height: origH,
18298
+ offsetY: 0,
18299
+ isVertical: false
18300
+ };
18301
+ const ratio = origW / origH;
18302
+ const isWide = isLandscape(origW, origH);
18303
+ if (verticalMode === "force") {
18304
+ const targetRatio = 16 / 9;
18305
+ if (isWide) {
18306
+ const newW = Math.min(origH, MAX_OUTPUT_WIDTH);
18307
+ const newH = Math.round(newW * targetRatio);
18308
+ const scaledH = Math.round(newW / ratio);
18309
+ return {
18310
+ width: newW,
18311
+ height: newH,
18312
+ offsetY: Math.round((newH - scaledH) / 2),
18313
+ isVertical: true,
18314
+ scale: newW / origW
18315
+ };
18316
+ } else {
18317
+ const newW = Math.min(origW, MAX_OUTPUT_WIDTH);
18318
+ const scaleRatio = newW / origW;
18319
+ const scaledOrigH = Math.round(origH * scaleRatio);
18320
+ const newH = Math.round(newW * targetRatio);
18321
+ const offsetY = Math.round((newH - scaledOrigH) / 2);
18322
+ return {
18323
+ width: newW,
18324
+ height: newH,
18325
+ offsetY: Math.max(0, offsetY),
18326
+ isVertical: true,
18327
+ scale: scaleRatio
18328
+ };
18329
+ }
18330
+ }
18331
+ if (isWide && ratio >= 1.7) {
18332
+ const newW = Math.min(origH, MAX_OUTPUT_WIDTH);
18333
+ const scaleRatio = newW / origH;
18334
+ const newH = Math.round(origW * scaleRatio);
18335
+ const scaledH = Math.round(newW / ratio);
18336
+ return {
18337
+ width: newW,
18338
+ height: newH,
18339
+ offsetY: Math.round((newH - scaledH) / 2),
18340
+ isVertical: true,
18341
+ scale: newW / origW
18342
+ };
18206
18343
  }
18207
18344
  return {
18208
- CommentsData: jsonArray,
18209
- image_url: imageUrls
18345
+ width: origW,
18346
+ height: origH,
18347
+ offsetY: 0,
18348
+ isVertical: false
18210
18349
  };
18211
- };
18212
- var getRelativeTimeFromTimestamp$2 = (timestamp) => {
18213
- const commentDate = fromUnixTime(timestamp);
18214
- const diffSeconds = differenceInSeconds(/* @__PURE__ */ new Date(), commentDate);
18215
- if (diffSeconds < 30) return "刚刚";
18216
- if (diffSeconds < 7776e3) return formatDistanceToNow(commentDate, {
18217
- locale: zhCN,
18218
- addSuffix: true
18219
- });
18220
- return format(commentDate, "yyyy-MM-dd");
18221
- };
18350
+ }
18351
+ function buildFilter(canvas, assPath) {
18352
+ const escaped = escapeWinPath(assPath);
18353
+ if (canvas.isVertical) {
18354
+ if (canvas.scale && canvas.scale !== 1 && canvas.scale < 1) return `scale=${canvas.width}:-1,pad=${canvas.width}:${canvas.height}:0:${canvas.offsetY}:black,subtitles='${escaped}'`;
18355
+ return `pad=${canvas.width}:${canvas.height}:0:${canvas.offsetY}:black,subtitles='${escaped}'`;
18356
+ }
18357
+ return `subtitles='${escaped}'`;
18358
+ }
18359
+ async function burnDouyinDanmaku(videoPath, danmakuList, outputPath, options = {}) {
18360
+ const { removeSource = false, verticalMode = "off", videoCodec = "h265" } = options;
18361
+ if (!fs.existsSync(videoPath)) {
18362
+ logger.error(`[DouyinDanmaku] 视频文件不存在: ${videoPath}`);
18363
+ return false;
18364
+ }
18365
+ const resolution = await getDouyinResolution(videoPath);
18366
+ const frameRate = await getDouyinFrameRate(videoPath);
18367
+ const sourceBitrate = await getVideoBitrate(videoPath);
18368
+ const canvas = calcCanvas(resolution.width, resolution.height, verticalMode);
18369
+ if (canvas.isVertical) logger.debug(`[DouyinDanmaku] 竖屏模式: ${resolution.width}x${resolution.height} -> ${canvas.width}x${canvas.height}`);
18370
+ logger.debug(`[DouyinDanmaku] 分辨率: ${canvas.width}x${canvas.height}, 帧率: ${frameRate}fps, 码率: ${sourceBitrate}kbps`);
18371
+ const assContent = generateDouyinASS(danmakuList, canvas.width, canvas.height, options);
18372
+ const assPath = videoPath.replace(/\.[^.]+$/, "_danmaku.ass");
18373
+ fs.writeFileSync(assPath, assContent, "utf-8");
18374
+ logger.debug(`[DouyinDanmaku] 弹幕字幕已生成: ${assPath},共 ${danmakuList.length} 条`);
18375
+ const result = await ffmpeg(`-y -i "${videoPath}" -vf "${buildFilter(canvas, assPath)}" -r ${frameRate} ${getEncoderParams(await detectEncoder(videoCodec), sourceBitrate)} -c:a copy "${outputPath}"`);
18376
+ Common.removeFile(assPath, true);
18377
+ if (result.status) {
18378
+ logger.mark(`[DouyinDanmaku] 弹幕烧录成功: ${outputPath}`);
18379
+ if (removeSource) Common.removeFile(videoPath);
18380
+ } else logger.error("[DouyinDanmaku] 弹幕烧录失败", result);
18381
+ return result.status;
18382
+ }
18222
18383
  await init_date_fns();
18223
18384
  await init_utils$1();
18224
18385
  await init_Config();
18225
- await init_danmaku();
18226
18386
  var mp4size = "";
18227
18387
  var img;
18228
18388
  var DouYin = class extends Base {
18229
18389
  e;
18230
18390
  type;
18231
- is_mp4;
18232
18391
  is_slides;
18233
18392
  forceBurnDanmaku;
18234
18393
  hasProcessedLiveImage;
@@ -18239,7 +18398,6 @@ var DouYin = class extends Base {
18239
18398
  super(e);
18240
18399
  this.e = e;
18241
18400
  this.type = iddata?.type;
18242
- this.is_mp4 = iddata?.is_mp4;
18243
18401
  this.is_slides = false;
18244
18402
  this.forceBurnDanmaku = options?.forceBurnDanmaku ?? false;
18245
18403
  this.hasProcessedLiveImage = false;
@@ -18283,7 +18441,6 @@ var DouYin = class extends Base {
18283
18441
  const aweme_type = VideoData.data.aweme_detail.aweme_type;
18284
18442
  const isArticle = aweme_type === 163;
18285
18443
  const isVideo = aweme_type === 0;
18286
- if (this.is_mp4 === void 0) this.is_mp4 = isVideo;
18287
18444
  const CommentsData = await this.amagi.douyin.fetcher.fetchWorkComments({
18288
18445
  aweme_id: data$1.aweme_id,
18289
18446
  number: Config.douyin.numcomment,
@@ -18294,7 +18451,7 @@ var DouYin = class extends Base {
18294
18451
  let g_title;
18295
18452
  let imagenum = 0;
18296
18453
  const image_res = [];
18297
- if (this.is_mp4 === false && !isArticle) switch (true) {
18454
+ if (!isVideo && !isArticle) switch (true) {
18298
18455
  case this.is_slides === false && VideoData.data.aweme_detail.images !== null: {
18299
18456
  const image_data = [];
18300
18457
  const imageres = [];
@@ -18316,7 +18473,6 @@ var DouYin = class extends Base {
18316
18473
  headers: this.headers
18317
18474
  });
18318
18475
  temp.push(liveimgbgm);
18319
- if (mergeMode === "continuous") bgmContext = await createLiveImageContext(liveimgbgm.filepath);
18320
18476
  }
18321
18477
  for (const [index, imageItem] of images.entries()) {
18322
18478
  imagenum++;
@@ -18340,26 +18496,27 @@ var DouYin = class extends Base {
18340
18496
  });
18341
18497
  if (liveimg.filepath) {
18342
18498
  const outputPath = Common.tempDri.video + `Douyin_Result_${Date.now()}.mp4`;
18343
- let success;
18344
18499
  const loopCount = imageItem.clip_type === 4 ? 1 : 3;
18345
- if (!liveimgbgm) if (loopCount > 1) success = await loopVideo(liveimg.filepath, outputPath, loopCount);
18346
- else {
18347
- fs.renameSync(liveimg.filepath, outputPath);
18348
- success = true;
18349
- }
18350
- else if (mergeMode === "continuous" && bgmContext) {
18351
- const result = await mergeLiveImageContinuous({
18352
- videoPath: liveimg.filepath,
18353
- outputPath,
18354
- loopCount
18355
- }, bgmContext);
18356
- success = result.success;
18357
- bgmContext = result.context;
18358
- } else success = await mergeLiveImageIndependent({
18359
- videoPath: liveimg.filepath,
18500
+ let staticImgPath = "";
18501
+ if (imageItem.url_list?.[0]) staticImgPath = (await downloadFile(imageItem.url_list[0], {
18502
+ title: `Douyin_static_${Date.now()}_${index}.jpg`,
18503
+ headers: this.headers,
18504
+ filepath: Common.tempDri.images + `Douyin_static_${Date.now()}_${index}.jpg`
18505
+ })).filepath ?? "";
18506
+ const transitionEnabled = loopCount > 1 && Boolean(staticImgPath);
18507
+ const safeStaticPath = staticImgPath || liveimg.filepath;
18508
+ const result = await loopVideoWithTransition({
18509
+ inputPath: liveimg.filepath,
18360
18510
  outputPath,
18361
- loopCount
18362
- }, liveimgbgm.filepath);
18511
+ loopCount,
18512
+ staticImagePath: safeStaticPath,
18513
+ transitionEnabled,
18514
+ bgmPath: liveimgbgm?.filepath,
18515
+ mergeMode,
18516
+ context: bgmContext ?? void 0
18517
+ });
18518
+ const success = result.success;
18519
+ if (mergeMode === "continuous" && result.context) bgmContext = result.context;
18363
18520
  if (success) {
18364
18521
  const filePath = Common.tempDri.video + `tmp_${Date.now()}.mp4`;
18365
18522
  fs.renameSync(outputPath, filePath);
@@ -18370,7 +18527,8 @@ var DouYin = class extends Base {
18370
18527
  filepath: filePath,
18371
18528
  totalBytes: 0
18372
18529
  });
18373
- processedImages.push(segment.video("file://" + filePath));
18530
+ const videoPath = Config.upload.videoSendMode === "base64" ? `base64://${fs.readFileSync(filePath).toString("base64")}` : `file://${filePath}`;
18531
+ processedImages.push(segment.video(videoPath));
18374
18532
  if (imageItem.clip_type === 5 && imageItem.url_list?.[0]) {
18375
18533
  const imageUrl = await processImageUrl(imageItem.url_list[0], g_title, index);
18376
18534
  processedImages.push(segment.image(imageUrl));
@@ -18378,7 +18536,7 @@ var DouYin = class extends Base {
18378
18536
  } else await Common.removeFile(liveimg.filepath, true);
18379
18537
  }
18380
18538
  }
18381
- const Element = common.makeForward(processedImages, this.e.sender.userId, this.e.sender.nick);
18539
+ const Element = common.makeForward(processedImages, Config.app.fakeForward ? this.e.sender.userId : this.e.bot.account.selfId, Config.app.fakeForward ? this.e.sender.nick : this.e.bot.account.name);
18382
18540
  try {
18383
18541
  await this.e.bot.sendForwardMsg(this.e.contact, Element, {
18384
18542
  source: "图集内容",
@@ -18386,8 +18544,6 @@ var DouYin = class extends Base {
18386
18544
  prompt: "抖音图集解析结果",
18387
18545
  news: [{ text: "点击查看解析结果" }]
18388
18546
  });
18389
- } catch (error) {
18390
- await this.e.reply(JSON.stringify(error, null, 2));
18391
18547
  } finally {
18392
18548
  for (const item of temp) await Common.removeFile(item.filepath, true);
18393
18549
  }
@@ -18408,7 +18564,7 @@ var DouYin = class extends Base {
18408
18564
  }).getData().then((data$2) => fs.promises.writeFile(path$1, Buffer.from(data$2)));
18409
18565
  }
18410
18566
  }
18411
- const res = common.makeForward(imageres, this.e.sender.userId, this.e.sender.nick);
18567
+ const res = common.makeForward(imageres, Config.app.fakeForward ? this.e.sender.userId : this.e.bot.account.selfId, Config.app.fakeForward ? this.e.sender.nick : this.e.bot.account.name);
18412
18568
  image_data.push(res);
18413
18569
  image_res.push(image_data);
18414
18570
  if (imageres.length === 1) {
@@ -18438,7 +18594,6 @@ var DouYin = class extends Base {
18438
18594
  headers: this.headers
18439
18595
  });
18440
18596
  temp.push(liveimgbgm);
18441
- if (mergeMode === "continuous") bgmContext = await createLiveImageContext(liveimgbgm.filepath);
18442
18597
  }
18443
18598
  const images1 = VideoData.data.aweme_detail.images ?? [];
18444
18599
  if (!images1.length) logger.debug("未获取到合辑的图片数据");
@@ -18449,51 +18604,53 @@ var DouYin = class extends Base {
18449
18604
  images.push(segment.image(imageUrl));
18450
18605
  continue;
18451
18606
  }
18452
- const liveimg = await downloadFile(`https://aweme.snssdk.com/aweme/v1/play/?video_id=${item.video.play_addr_h264.uri}&ratio=1080p&line=0`, {
18607
+ const livePhoto = await downloadFile(`https://aweme.snssdk.com/aweme/v1/play/?video_id=${item.video.play_addr_h264.uri}&ratio=1080p&line=0`, {
18453
18608
  title: `Douyin_tmp_V_${Date.now()}.mp4`,
18454
18609
  headers: this.headers
18455
18610
  });
18456
- if (liveimg.filepath) {
18611
+ if (livePhoto.filepath) {
18457
18612
  const outputPath = Common.tempDri.video + `Douyin_Result_${Date.now()}.mp4`;
18458
- let success;
18459
18613
  const loopCount = item.clip_type === 4 ? 1 : 3;
18460
- if (!liveimgbgm) if (loopCount > 1) success = await loopVideo(liveimg.filepath, outputPath, loopCount);
18461
- else {
18462
- fs.renameSync(liveimg.filepath, outputPath);
18463
- success = true;
18464
- }
18465
- else if (mergeMode === "continuous" && bgmContext) {
18466
- const result = await mergeLiveImageContinuous({
18467
- videoPath: liveimg.filepath,
18468
- outputPath,
18469
- loopCount
18470
- }, bgmContext);
18471
- success = result.success;
18472
- bgmContext = result.context;
18473
- } else success = await mergeLiveImageIndependent({
18474
- videoPath: liveimg.filepath,
18614
+ let staticImgPath = "";
18615
+ if (item.url_list?.[0]) staticImgPath = (await downloadFile(item.url_list[0], {
18616
+ title: `Douyin_static_${Date.now()}_${index}.jpg`,
18617
+ headers: this.headers,
18618
+ filepath: Common.tempDri.images + `Douyin_static_${Date.now()}_${index}.jpg`
18619
+ })).filepath ?? "";
18620
+ const transitionEnabled = loopCount > 1 && Boolean(staticImgPath);
18621
+ const safeStaticPath = staticImgPath || livePhoto.filepath;
18622
+ const result = await loopVideoWithTransition({
18623
+ inputPath: livePhoto.filepath,
18475
18624
  outputPath,
18476
- loopCount
18477
- }, liveimgbgm.filepath);
18625
+ loopCount,
18626
+ staticImagePath: safeStaticPath,
18627
+ transitionEnabled,
18628
+ bgmPath: liveimgbgm?.filepath,
18629
+ mergeMode,
18630
+ context: bgmContext ?? void 0
18631
+ });
18632
+ const success = result.success;
18633
+ if (mergeMode === "continuous" && result.context) bgmContext = result.context;
18478
18634
  if (success) {
18479
18635
  const filePath = Common.tempDri.video + `tmp_${Date.now()}.mp4`;
18480
18636
  fs.renameSync(outputPath, filePath);
18481
18637
  logger.mark(`视频文件重命名完成: ${outputPath.split("/").pop()} -> ${filePath.split("/").pop()}`);
18482
18638
  logger.mark("正在尝试删除缓存文件");
18483
- await Common.removeFile(liveimg.filepath, true);
18639
+ await Common.removeFile(livePhoto.filepath, true);
18484
18640
  temp.push({
18485
18641
  filepath: filePath,
18486
18642
  totalBytes: 0
18487
18643
  });
18488
- images.push(segment.video("file://" + filePath));
18644
+ const videoPath = Config.upload.videoSendMode === "base64" ? `base64://${fs.readFileSync(filePath).toString("base64")}` : `file://${filePath}`;
18645
+ images.push(segment.video(videoPath));
18489
18646
  if (item.clip_type === 5 && item.url_list?.[0]) {
18490
18647
  const imageUrl = await processImageUrl(item.url_list[0], g_title, index);
18491
18648
  images.push(segment.image(imageUrl));
18492
18649
  }
18493
- } else await Common.removeFile(liveimg.filepath, true);
18650
+ } else await Common.removeFile(livePhoto.filepath, true);
18494
18651
  }
18495
18652
  }
18496
- const Element = common.makeForward(images, this.e.sender.userId, this.e.sender.nick);
18653
+ const Element = common.makeForward(images, Config.app.fakeForward ? this.e.sender.userId : this.e.bot.account.selfId, Config.app.fakeForward ? this.e.sender.nick : this.e.bot.account.name);
18497
18654
  try {
18498
18655
  await this.e.bot.sendForwardMsg(this.e.contact, Element, {
18499
18656
  source: "合辑内容",
@@ -18501,8 +18658,6 @@ var DouYin = class extends Base {
18501
18658
  prompt: "抖音合辑解析结果",
18502
18659
  news: [{ text: "点击查看解析结果" }]
18503
18660
  });
18504
- } catch (error) {
18505
- await this.e.reply(JSON.stringify(error, null, 2));
18506
18661
  } finally {
18507
18662
  for (const item of temp) await Common.removeFile(item.filepath, true);
18508
18663
  }
@@ -18514,7 +18669,7 @@ var DouYin = class extends Base {
18514
18669
  let music_url = "";
18515
18670
  if (music.play_url.uri === "") music_url = JSON.parse(music.extra).original_song_url;
18516
18671
  else music_url = music.play_url.uri;
18517
- if (this.is_mp4 === false && Config.app.removeCache === false && music_url !== void 0) try {
18672
+ if (!isVideo && Config.app.removeCache === false && music_url !== void 0) try {
18518
18673
  const path$1 = Common.tempDri.images + `${g_title}/BGM.mp3`;
18519
18674
  await new Network({
18520
18675
  url: music_url,
@@ -18523,11 +18678,11 @@ var DouYin = class extends Base {
18523
18678
  } catch (error) {
18524
18679
  console.log(error);
18525
18680
  }
18526
- music_url && (this.is_mp4 === false || isArticle) && music_url !== void 0 && !this.hasProcessedLiveImage && await this.e.reply(segment.record(music_url, false));
18681
+ music_url && !isVideo && music_url !== void 0 && !this.hasProcessedLiveImage && await this.e.reply(segment.record(music_url, false));
18527
18682
  }
18528
18683
  let FPS;
18529
18684
  let video = null;
18530
- if (this.is_mp4) {
18685
+ if (isVideo) {
18531
18686
  video = VideoData.data.aweme_detail.video;
18532
18687
  FPS = video.bit_rate[0]?.FPS ?? "获取失败";
18533
18688
  logger.debug(`开始排除不符合条件的视频分辨率;\n\n 共拥有${logger.yellow(video.bit_rate.length)}个视频源\n\n 视频ID:${logger.green(VideoData.data.aweme_detail.aweme_id)}\n\n 分享链接:${logger.green(VideoData.data.aweme_detail.share_url)}\n `);
@@ -18545,7 +18700,7 @@ var DouYin = class extends Base {
18545
18700
  if (Config.douyin.sendContent.includes("info")) if (Config.douyin.videoInfoMode === "text") {
18546
18701
  const replyContent = [];
18547
18702
  const { digg_count, share_count, collect_count, comment_count, recommend_count } = VideoData.data.aweme_detail.statistics;
18548
- const coverUrl = await processImageUrl(isArticle ? VideoData.data.aweme_detail.video.origin_cover.url_list[0] : this.is_mp4 ? VideoData.data.aweme_detail.video.animated_cover?.url_list[0] ?? VideoData.data.aweme_detail.video.cover.url_list[0] : VideoData.data.aweme_detail.images[0].url_list[0], VideoData.data.aweme_detail.desc);
18703
+ const coverUrl = await processImageUrl(isArticle ? VideoData.data.aweme_detail.video.origin_cover.url_list[0] : isVideo ? VideoData.data.aweme_detail.video.animated_cover?.url_list[0] ?? VideoData.data.aweme_detail.video.cover.url_list[0] : VideoData.data.aweme_detail.images[0].url_list[0], VideoData.data.aweme_detail.desc);
18549
18704
  const contentMap = {
18550
18705
  cover: segment.image(coverUrl),
18551
18706
  title: segment.text(`\n📺 标题: ${VideoData.data.aweme_detail.desc}\n`),
@@ -18583,11 +18738,11 @@ var DouYin = class extends Base {
18583
18738
  gender: userProfile.data.user.gender ?? 0,
18584
18739
  user_age: userProfile.data.user.user_age ?? 0
18585
18740
  } : void 0,
18586
- image_url: isArticle ? VideoData.data.aweme_detail.video.origin_cover.url_list[0] : this.is_mp4 ? VideoData.data.aweme_detail.video.animated_cover?.url_list[0] ?? VideoData.data.aweme_detail.video.dynamic_cover?.url_list[0] ?? VideoData.data.aweme_detail.video.cover_original_scale?.url_list[0] ?? VideoData.data.aweme_detail.video.cover.url_list[0] : VideoData.data.aweme_detail.images[0].url_list[0],
18741
+ image_url: isArticle ? VideoData.data.aweme_detail.video.origin_cover.url_list[0] : isVideo ? VideoData.data.aweme_detail.video.animated_cover?.url_list[0] ?? VideoData.data.aweme_detail.video.dynamic_cover?.url_list[0] ?? VideoData.data.aweme_detail.video.cover_original_scale?.url_list[0] ?? VideoData.data.aweme_detail.video.cover.url_list[0] : VideoData.data.aweme_detail.images[0].url_list[0],
18587
18742
  cover_size: isArticle ? VideoData.data.aweme_detail.video.origin_cover ? {
18588
18743
  width: VideoData.data.aweme_detail.video.origin_cover.width,
18589
18744
  height: VideoData.data.aweme_detail.video.origin_cover.height
18590
- } : void 0 : this.is_mp4 ? VideoData.data.aweme_detail.video.cover ? {
18745
+ } : void 0 : isVideo ? VideoData.data.aweme_detail.video.cover ? {
18591
18746
  width: VideoData.data.aweme_detail.video.cover_original_scale.width,
18592
18747
  height: VideoData.data.aweme_detail.video.cover_original_scale.height
18593
18748
  } : void 0 : VideoData.data.aweme_detail.images?.[0] ? {
@@ -18600,7 +18755,7 @@ var DouYin = class extends Base {
18600
18755
  title: VideoData.data.aweme_detail.music.title,
18601
18756
  cover: VideoData.data.aweme_detail.music.cover_hd?.url_list[0] ?? VideoData.data.aweme_detail.music.cover_large?.url_list[0]
18602
18757
  } : void 0,
18603
- video: this.is_mp4 ? {
18758
+ video: isVideo ? {
18604
18759
  duration: VideoData.data.aweme_detail.video.duration,
18605
18760
  width: VideoData.data.aweme_detail.video.width,
18606
18761
  height: VideoData.data.aweme_detail.video.height,
@@ -18619,16 +18774,16 @@ var DouYin = class extends Base {
18619
18774
  for (const item of VideoData.data.aweme_detail.suggest_words.suggest_words) if (item.words && item.scene === "comment_top_rec") for (const v of item.words) v.word && suggest.push(v.word);
18620
18775
  }
18621
18776
  const img$2 = await Render("douyin/comment", {
18622
- Type: isArticle ? "文章" : this.is_mp4 ? "视频" : this.is_slides ? "合辑" : "图集",
18777
+ Type: isArticle ? "文章" : isVideo ? "视频" : this.is_slides ? "合辑" : "图集",
18623
18778
  CommentsData: douyinCommentsRes.CommentsData,
18624
18779
  CommentLength: Config.douyin.realCommentCount ? VideoData.data.aweme_detail.statistics.comment_count : douyinCommentsRes.CommentsData.length ?? 0,
18625
- share_url: this.is_mp4 ? `https://aweme.snssdk.com/aweme/v1/play/?video_id=${VideoData.data.aweme_detail.video.play_addr.uri}&ratio=1080p&line=0` : VideoData.data.aweme_detail.share_url,
18780
+ share_url: isVideo ? `https://aweme.snssdk.com/aweme/v1/play/?video_id=${VideoData.data.aweme_detail.video.play_addr.uri}&ratio=1080p&line=0` : VideoData.data.aweme_detail.share_url,
18626
18781
  VideoSize: mp4size,
18627
18782
  VideoFPS: FPS,
18628
18783
  ImageLength: imagenum,
18629
18784
  Region: VideoData.data.aweme_detail.region,
18630
18785
  suggestWrod: suggest,
18631
- Resolution: this.is_mp4 && video ? `${video.bit_rate[0].play_addr.width} x ${video.bit_rate[0].play_addr.height}` : null,
18786
+ Resolution: isVideo && video ? `${video.bit_rate[0].play_addr.width} x ${video.bit_rate[0].play_addr.height}` : null,
18632
18787
  maxDepth: Config.douyin.subCommentDepth
18633
18788
  });
18634
18789
  const messageElements = [];
@@ -18637,8 +18792,8 @@ var DouYin = class extends Base {
18637
18792
  const imageUrl = await processImageUrl(v, VideoData.data.aweme_detail.desc, index);
18638
18793
  messageElements.push(segment.image(imageUrl));
18639
18794
  }
18640
- const res = common.makeForward(messageElements, this.e.sender.userId, this.e.sender.nick);
18641
- this.e.bot.sendForwardMsg(this.e.contact, res, {
18795
+ const res = common.makeForward(messageElements, Config.app.fakeForward ? this.e.sender.userId : this.e.bot.account.selfId, Config.app.fakeForward ? this.e.sender.nick : this.e.bot.account.name);
18796
+ await this.e.bot.sendForwardMsg(this.e.contact, res, {
18642
18797
  source: "评论图片收集",
18643
18798
  summary: `查看${messageElements.length}张图片`,
18644
18799
  prompt: "抖音评论解析结果",
@@ -18648,7 +18803,7 @@ var DouYin = class extends Base {
18648
18803
  this.e.reply(img$2);
18649
18804
  }
18650
18805
  }
18651
- if (this.is_mp4 && !isArticle && Config.douyin.sendContent.includes("video")) {
18806
+ if (isVideo && !isArticle && Config.douyin.sendContent.includes("video")) {
18652
18807
  let danmakuList = [];
18653
18808
  if ((this.forceBurnDanmaku || Config.douyin.burnDanmaku) && video) try {
18654
18809
  const duration = video.duration;
@@ -18973,11 +19128,22 @@ let DouyinImageSubType = function(DouyinImageSubType$1) {
18973
19128
  return DouyinImageSubType$1;
18974
19129
  }({});
18975
19130
  function getWorkTypeInfo(data$1) {
19131
+ if (data$1.live_data) return {
19132
+ mainType: DouyinWorkMainType.LIVE,
19133
+ isVideo: false,
19134
+ isImage: false,
19135
+ isArticle: false,
19136
+ isLive: true,
19137
+ isGallery: false,
19138
+ isCollection: false,
19139
+ templatePath: "douyin/live"
19140
+ };
18976
19141
  if (data$1.aweme_type === 163 || data$1.article_info) return {
18977
19142
  mainType: DouyinWorkMainType.ARTICLE,
18978
19143
  isVideo: false,
18979
19144
  isImage: false,
18980
19145
  isArticle: true,
19146
+ isLive: false,
18981
19147
  isGallery: false,
18982
19148
  isCollection: false,
18983
19149
  templatePath: "douyin/article-work"
@@ -18990,6 +19156,7 @@ function getWorkTypeInfo(data$1) {
18990
19156
  isVideo: false,
18991
19157
  isImage: true,
18992
19158
  isArticle: false,
19159
+ isLive: false,
18993
19160
  isGallery: subType === DouyinImageSubType.GALLERY,
18994
19161
  isCollection: subType === DouyinImageSubType.COLLECTION,
18995
19162
  templatePath: "douyin/image-work"
@@ -19000,6 +19167,7 @@ function getWorkTypeInfo(data$1) {
19000
19167
  isVideo: true,
19001
19168
  isImage: false,
19002
19169
  isArticle: false,
19170
+ isLive: false,
19003
19171
  isGallery: false,
19004
19172
  isCollection: false,
19005
19173
  templatePath: "douyin/video-work"
@@ -19015,6 +19183,14 @@ function getWorkCoverUrl(workTypeInfo, data$1) {
19015
19183
  }
19016
19184
  return "";
19017
19185
  }
19186
+ function getWorkTypeDisplayName(workTypeInfo) {
19187
+ if (workTypeInfo.isVideo) return "视频";
19188
+ if (workTypeInfo.isGallery) return "图集";
19189
+ if (workTypeInfo.isCollection) return "合辑";
19190
+ if (workTypeInfo.isArticle) return "文章";
19191
+ if (workTypeInfo.isLive) return "直播";
19192
+ return "未知";
19193
+ }
19018
19194
  const getDouyinID = async (event, url, log = true) => {
19019
19195
  const resp = await axios.get(url, {
19020
19196
  headers: { "User-Agent": "Apifox/1.0.0 (https://apifox.com)" },
@@ -19365,10 +19541,7 @@ var DouYinpush = class extends Base {
19365
19541
  const skip = await skipDynamic(pushItem);
19366
19542
  skip && logger.warn(`作品 https://www.douyin.com/video/${actualAwemeId} 已被处理,跳过`);
19367
19543
  let img$2 = [];
19368
- let iddata = {
19369
- is_mp4: true,
19370
- type: "one_work"
19371
- };
19544
+ let iddata = { type: "one_work" };
19372
19545
  if (!skip) iddata = await getDouyinID(this.e, Detail_Data.share_url ?? "https://live.douyin.com/" + Detail_Data.room_data?.owner.web_rid, false);
19373
19546
  if (!skip) if (pushItem.pushType === "live" && "room_data" in pushItem.Detail_Data && Detail_Data.live_data) img$2 = await Render("douyin/live", {
19374
19547
  image_url: Detail_Data.live_data.data.data.data[0]?.cover?.url_list[0] ?? Detail_Data.live_data.data.data.qrcode_url,
@@ -19466,7 +19639,7 @@ var DouYinpush = class extends Base {
19466
19639
  create_time: format(fromUnixTime(pushItem.create_time), "yyyy-MM-dd HH:mm"),
19467
19640
  avater_url: "https://p3-pc.douyinpic.com/aweme/1080x1080/" + Detail_Data.user_info.data.user.avatar_larger.uri,
19468
19641
  share_url: Config.douyin.push.shareType === "web" ? realUrl : `https://aweme.snssdk.com/aweme/v1/play/?video_id=${Detail_Data.video.play_addr.uri}&ratio=1080p&line=0`,
19469
- username: Detail_Data.author.nickname,
19642
+ username: Detail_Data.user_info.data.user.nickname,
19470
19643
  "抖音号": Detail_Data.user_info.data.user.unique_id === "" ? Detail_Data.user_info.data.user.short_id : Detail_Data.user_info.data.user.unique_id,
19471
19644
  "粉丝": this.count(Detail_Data.user_info.data.user.follower_count),
19472
19645
  "获赞": this.count(Detail_Data.user_info.data.user.total_favorited),
@@ -19476,22 +19649,23 @@ var DouYinpush = class extends Base {
19476
19649
  const raw = Detail_Data.cooperation_info;
19477
19650
  if (!raw) return void 0;
19478
19651
  const rawCreators = Array.isArray(raw.co_creators) ? raw.co_creators : [];
19479
- const author = Detail_Data.author;
19480
- const authorUid = author?.uid;
19481
- const authorSecUid = author?.sec_uid;
19482
- const authorNickname = author?.nickname;
19483
- const authorInCreators = rawCreators.some((c) => authorUid && c.uid && c.uid === authorUid || authorSecUid && c.sec_uid && c.sec_uid === authorSecUid || authorNickname && c.nickname && c.nickname === authorNickname);
19484
- const co_creators = rawCreators.map((c) => {
19485
- const firstUrl = c.avatar_thumb?.url_list?.[0] ?? (c.avatar_thumb?.uri ? `https://p3.douyinpic.com/${c.avatar_thumb.uri}` : void 0);
19486
- return {
19487
- avatar_thumb: firstUrl ? { url_list: [firstUrl] } : void 0,
19488
- nickname: c.nickname,
19489
- role_title: c.role_title
19490
- };
19652
+ const subscriberUid = Detail_Data.user_info.data.user.uid;
19653
+ const subscriberSecUid = Detail_Data.user_info.data.user.sec_uid;
19654
+ const subscriberInCreators = rawCreators.find((c) => subscriberUid && c.uid && c.uid === subscriberUid || subscriberSecUid && c.sec_uid && c.sec_uid === subscriberSecUid);
19655
+ const co_creators = rawCreators.map((c) => ({
19656
+ avatar_url: c.avatar_thumb?.url_list?.[0] ?? (c.avatar_thumb?.uri ? `https://p3.douyinpic.com/${c.avatar_thumb.uri}` : void 0),
19657
+ nickname: c.nickname,
19658
+ role_title: c.role_title
19659
+ }));
19660
+ if (Detail_Data.author && !rawCreators.some((c) => Detail_Data.author?.uid && c.uid && c.uid === Detail_Data.author.uid || Detail_Data.author?.sec_uid && c.sec_uid && c.sec_uid === Detail_Data.author.sec_uid || Detail_Data.author?.nickname && c.nickname && c.nickname === Detail_Data.author.nickname)) co_creators.unshift({
19661
+ avatar_url: Detail_Data.author.avatar_thumb?.url_list?.[0] ?? (Detail_Data.author.avatar_thumb?.uri ? `https://p3.douyinpic.com/${Detail_Data.author.avatar_thumb.uri}` : void 0),
19662
+ nickname: Detail_Data.author.nickname,
19663
+ role_title: "作者"
19491
19664
  });
19492
19665
  return {
19493
- co_creator_nums: Math.max(Number(raw.co_creator_nums || 0), co_creators.length) + (authorInCreators ? 0 : 1),
19494
- co_creators
19666
+ co_creator_nums: Math.max(Number(raw.co_creator_nums || 0), co_creators.length),
19667
+ co_creators,
19668
+ subscriber_role: subscriberInCreators?.role_title ?? (subscriberUid && Detail_Data.author?.uid && subscriberUid === Detail_Data.author.uid || subscriberSecUid && Detail_Data.author?.sec_uid && subscriberSecUid === Detail_Data.author.sec_uid || Detail_Data.user_info.data.user.nickname && Detail_Data.author?.nickname && Detail_Data.user_info.data.user.nickname === Detail_Data.author.nickname ? "作者" : void 0)
19495
19669
  };
19496
19670
  })()
19497
19671
  });
@@ -19505,8 +19679,9 @@ var DouYinpush = class extends Base {
19505
19679
  status = await karin$1.sendMsg(botId, Contact, img$2 ? [...img$2] : []);
19506
19680
  if (pushItem.pushType === "live" && "room_data" in pushItem.Detail_Data && status.message_id) await douyinDBInstance.updateLiveStatus(pushItem.sec_uid, true);
19507
19681
  if (Config.douyin.push.parsedynamic && status.message_id) {
19508
- logger.debug(`开始解析作品,类型为:${iddata.is_mp4 ? "视频" : "图集"}`);
19509
- if (iddata.is_mp4) try {
19682
+ const workTypeInfo = getWorkTypeInfo(Detail_Data);
19683
+ logger.debug(`开始解析作品,类型为:${getWorkTypeDisplayName(workTypeInfo)}`);
19684
+ if (workTypeInfo.isVideo) try {
19510
19685
  let downloadUrl = `https://aweme.snssdk.com/aweme/v1/play/?video_id=${Detail_Data.video.play_addr.uri}&ratio=1080p&line=0`;
19511
19686
  logger.debug(`开始排除不符合条件的视频分辨率;\n\n 共拥有${logger.yellow(Detail_Data.video.bit_rate.length)}个视频源\n\n 视频ID:${logger.green(Detail_Data.aweme_id)}\n\n 分享链接:${logger.green(Detail_Data.share_url)}\n `);
19512
19687
  const videoObj = douyinProcessVideos(Detail_Data.video.bit_rate, Config.douyin.videoQuality);
@@ -19531,7 +19706,7 @@ var DouYinpush = class extends Base {
19531
19706
  } catch (error) {
19532
19707
  throw new Error(`下载视频失败: ${error}`);
19533
19708
  }
19534
- else if (!iddata.is_mp4 && iddata.type === "one_work") {
19709
+ else if (workTypeInfo.isImage && iddata.type === "one_work") {
19535
19710
  if (Detail_Data.is_slides === true && Detail_Data.images) {
19536
19711
  const images = [];
19537
19712
  const temp = [];
@@ -19547,7 +19722,6 @@ var DouYinpush = class extends Base {
19547
19722
  headers: douyinBaseHeaders
19548
19723
  });
19549
19724
  temp.push(liveimgbgm);
19550
- if (mergeMode === "continuous") bgmContext = await createLiveImageContext(liveimgbgm.filepath);
19551
19725
  }
19552
19726
  const images1 = Detail_Data.images ?? [];
19553
19727
  if (!images1.length) logger.debug("未获取到合辑的图片数据");
@@ -19563,26 +19737,27 @@ var DouYinpush = class extends Base {
19563
19737
  });
19564
19738
  if (liveimg.filepath) {
19565
19739
  const outputPath = Common.tempDri.video + `Douyin_Result_${Date.now()}.mp4`;
19566
- let success;
19567
19740
  const loopCount = item.clip_type === 4 ? 1 : 3;
19568
- if (!liveimgbgm) if (loopCount > 1) success = await loopVideo(liveimg.filepath, outputPath, loopCount);
19569
- else {
19570
- fs.renameSync(liveimg.filepath, outputPath);
19571
- success = true;
19572
- }
19573
- else if (mergeMode === "continuous" && bgmContext) {
19574
- const result = await mergeLiveImageContinuous({
19575
- videoPath: liveimg.filepath,
19576
- outputPath,
19577
- loopCount
19578
- }, bgmContext);
19579
- success = result.success;
19580
- bgmContext = result.context;
19581
- } else success = await mergeLiveImageIndependent({
19582
- videoPath: liveimg.filepath,
19741
+ let staticImgPath = "";
19742
+ if (item.url_list?.[0]) staticImgPath = (await downloadFile(item.url_list[0], {
19743
+ title: `Douyin_static_${Date.now()}_${index}.jpg`,
19744
+ headers: douyinBaseHeaders,
19745
+ filepath: Common.tempDri.images + `Douyin_static_${Date.now()}_${index}.jpg`
19746
+ })).filepath ?? "";
19747
+ const transitionEnabled = loopCount > 1 && Boolean(staticImgPath);
19748
+ const safeStaticPath = staticImgPath || liveimg.filepath;
19749
+ const result = await loopVideoWithTransition({
19750
+ inputPath: liveimg.filepath,
19583
19751
  outputPath,
19584
- loopCount
19585
- }, liveimgbgm.filepath);
19752
+ loopCount,
19753
+ staticImagePath: safeStaticPath,
19754
+ transitionEnabled,
19755
+ bgmPath: liveimgbgm?.filepath,
19756
+ mergeMode,
19757
+ context: bgmContext ?? void 0
19758
+ });
19759
+ const success = result.success;
19760
+ if (mergeMode === "continuous" && result.context) bgmContext = result.context;
19586
19761
  if (success) {
19587
19762
  const filePath = Common.tempDri.video + `tmp_${Date.now()}.mp4`;
19588
19763
  fs.renameSync(outputPath, filePath);
@@ -19593,7 +19768,8 @@ var DouYinpush = class extends Base {
19593
19768
  filepath: filePath,
19594
19769
  totalBytes: 0
19595
19770
  });
19596
- images.push(segment.video("file://" + filePath));
19771
+ const videoPath = Config.upload.videoSendMode === "base64" ? `base64://${fs.readFileSync(filePath).toString("base64")}` : `file://${filePath}`;
19772
+ images.push(segment.video(videoPath));
19597
19773
  if (item.clip_type === 5 && item.url_list?.[0]) {
19598
19774
  const imageUrl = await processImageUrl(item.url_list[0], Detail_Data.desc, index);
19599
19775
  images.push(segment.image(imageUrl));
@@ -19630,7 +19806,6 @@ var DouYinpush = class extends Base {
19630
19806
  headers: douyinBaseHeaders
19631
19807
  });
19632
19808
  temp.push(liveimgbgm);
19633
- if (mergeMode === "continuous") bgmContext = await createLiveImageContext(liveimgbgm.filepath);
19634
19809
  }
19635
19810
  for (const [index, item] of Detail_Data.images.entries()) {
19636
19811
  if (item.clip_type === 2 || item.clip_type === void 0) {
@@ -19644,26 +19819,27 @@ var DouYinpush = class extends Base {
19644
19819
  });
19645
19820
  if (liveimg.filepath) {
19646
19821
  const outputPath = Common.tempDri.video + `Douyin_Result_${Date.now()}.mp4`;
19647
- let success;
19648
19822
  const loopCount = item.clip_type === 4 ? 1 : 3;
19649
- if (!liveimgbgm) if (loopCount > 1) success = await loopVideo(liveimg.filepath, outputPath, loopCount);
19650
- else {
19651
- fs.renameSync(liveimg.filepath, outputPath);
19652
- success = true;
19653
- }
19654
- else if (mergeMode === "continuous" && bgmContext) {
19655
- const result = await mergeLiveImageContinuous({
19656
- videoPath: liveimg.filepath,
19657
- outputPath,
19658
- loopCount
19659
- }, bgmContext);
19660
- success = result.success;
19661
- bgmContext = result.context;
19662
- } else success = await mergeLiveImageIndependent({
19663
- videoPath: liveimg.filepath,
19823
+ let staticImgPath = "";
19824
+ if (item.url_list?.[0]) staticImgPath = (await downloadFile(item.url_list[0], {
19825
+ title: `Douyin_static_${Date.now()}_${index}.jpg`,
19826
+ headers: douyinBaseHeaders,
19827
+ filepath: Common.tempDri.images + `Douyin_static_${Date.now()}_${index}.jpg`
19828
+ })).filepath ?? "";
19829
+ const transitionEnabled = loopCount > 1 && Boolean(staticImgPath);
19830
+ const safeStaticPath = staticImgPath || liveimg.filepath;
19831
+ const result = await loopVideoWithTransition({
19832
+ inputPath: liveimg.filepath,
19664
19833
  outputPath,
19665
- loopCount
19666
- }, liveimgbgm.filepath);
19834
+ loopCount,
19835
+ staticImagePath: safeStaticPath,
19836
+ transitionEnabled,
19837
+ bgmPath: liveimgbgm?.filepath,
19838
+ mergeMode,
19839
+ context: bgmContext ?? void 0
19840
+ });
19841
+ const success = result.success;
19842
+ if (mergeMode === "continuous" && result.context) bgmContext = result.context;
19667
19843
  if (success) {
19668
19844
  const filePath = Common.tempDri.video + `tmp_${Date.now()}.mp4`;
19669
19845
  fs.renameSync(outputPath, filePath);
@@ -19674,7 +19850,8 @@ var DouYinpush = class extends Base {
19674
19850
  filepath: filePath,
19675
19851
  totalBytes: 0
19676
19852
  });
19677
- processedImages.push(segment.video("file://" + filePath));
19853
+ const videoPath = Config.upload.videoSendMode === "base64" ? `base64://${fs.readFileSync(filePath).toString("base64")}` : `file://${filePath}`;
19854
+ processedImages.push(segment.video(videoPath));
19678
19855
  if (item.clip_type === 5 && item.url_list?.[0]) {
19679
19856
  const imageUrl = await processImageUrl(item.url_list[0], Detail_Data.desc, index);
19680
19857
  processedImages.push(segment.image(imageUrl));
@@ -19982,7 +20159,6 @@ var skipDynamic = async (PushItem) => {
19982
20159
  logger.debug(`检查作品是否需要过滤:${PushItem.Detail_Data.share_url}`);
19983
20160
  return await douyinDBInstance.shouldFilter(PushItem, tags);
19984
20161
  };
19985
- init_danmaku();
19986
20162
  init_date_fns();
19987
20163
  init_locale();
19988
20164
  init_Config();
@@ -20504,7 +20680,7 @@ const task = Config.app.removeCache && karin$1.task("[kkk-缓存自动删除]",
20504
20680
  const twoHoursAgo = Date.now() - 7200 * 1e3;
20505
20681
  const videoDeleted = removeOldFiles(Common.tempDri.video, twoHoursAgo);
20506
20682
  logger.mark(`${Common.tempDri.video} 目录下已删除 ${videoDeleted} 个文件`);
20507
- if (Config.app.downloadImageLocally) {
20683
+ if (Config.upload.imageSendMode === "file") {
20508
20684
  const imageDeleted = removeOldFiles(Common.tempDri.images, twoHoursAgo);
20509
20685
  logger.mark(`${Common.tempDri.images} 目录下已删除 ${imageDeleted} 个文件`);
20510
20686
  }
@@ -20867,14 +21043,11 @@ var handleTestDouyinPush = wrapWithErrorHandler(async (e) => {
20867
21043
  const authorSecUid = author?.sec_uid;
20868
21044
  const authorNickname = author?.nickname;
20869
21045
  const authorInCreators = rawCreators.some((c) => authorUid && c.uid && c.uid === authorUid || authorSecUid && c.sec_uid && c.sec_uid === authorSecUid || authorNickname && c.nickname && c.nickname === authorNickname);
20870
- const co_creators = rawCreators.map((c) => {
20871
- const firstUrl = c.avatar_thumb?.url_list?.[0] ?? (c.avatar_thumb?.uri ? `https://p3.douyinpic.com/${c.avatar_thumb.uri}` : void 0);
20872
- return {
20873
- avatar_thumb: firstUrl ? { url_list: [firstUrl] } : void 0,
20874
- nickname: c.nickname,
20875
- role_title: c.role_title
20876
- };
20877
- });
21046
+ const co_creators = rawCreators.map((c) => ({
21047
+ avatar_url: c.avatar_thumb?.url_list?.[0] ?? (c.avatar_thumb?.uri ? `https://p3.douyinpic.com/${c.avatar_thumb.uri}` : void 0),
21048
+ nickname: c.nickname,
21049
+ role_title: c.role_title
21050
+ }));
20878
21051
  return {
20879
21052
  co_creator_nums: Math.max(Number(raw.co_creator_nums || 0), co_creators.length) + (authorInCreators ? 0 : 1),
20880
21053
  co_creators
@@ -21340,7 +21513,7 @@ var Xiaohongshu = class extends Base {
21340
21513
  const imageUrl = await processImageUrl(item.url_default, title, index);
21341
21514
  Imgs.push(segment.image(imageUrl));
21342
21515
  }
21343
- const res = common.makeForward(Imgs, this.e.sender.userId, this.e.sender.nick);
21516
+ const res = common.makeForward(Imgs, Config.app.fakeForward ? this.e.sender.userId : this.e.bot.account.selfId, Config.app.fakeForward ? this.e.sender.nick : this.e.bot.account.name);
21344
21517
  if (NoteData.data.data.items[0].note_card.image_list.length === 1) {
21345
21518
  const imageUrl = await processImageUrl(NoteData.data.data.items[0].note_card.image_list[0].url_default, title);
21346
21519
  await this.e.reply(segment.image(imageUrl));