karin-plugin-kkk 2.23.2 → 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
- 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-9pKTNH6x.js";
3
- import { n as init_client, r as reactServerRender } from "./template-2ApQpQ8R.js";
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-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
  });
@@ -11746,8 +11093,14 @@ var init_app_schema = __esmMin(() => {
11746
11093
  {
11747
11094
  key: "parseTip",
11748
11095
  type: "switch",
11749
- label: "解析提示",
11750
- description: "发送提示信息:\"检测到xxx链接,开始解析\""
11096
+ label: "解析提示",
11097
+ description: "发送提示信息:\"检测到xxx链接,开始解析\""
11098
+ },
11099
+ {
11100
+ key: "fakeForward",
11101
+ type: "switch",
11102
+ label: "伪造合并转发消息",
11103
+ description: "开启后合并转发将使用触发者身份展示;关闭后使用机器人身份展示"
11751
11104
  },
11752
11105
  {
11753
11106
  key: "errorLogSendTo",
@@ -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
  ]
@@ -15670,93 +15044,477 @@ var deepEqual = (a, b) => {
15670
15044
  isChange = true;
15671
15045
  return true;
15672
15046
  }
15673
- if (deepEqual(a[key], b[key])) {
15674
- isChange = true;
15675
- return true;
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;
15099
+ }
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;
15108
+ };
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];
15116
+ }
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];
15124
+ }
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
+ }
15171
+ }
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}`);
15189
+ }
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;
15367
+ }
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站评论解析结果",
@@ -17780,20 +17584,70 @@ var Bilibilipush = class extends Base {
17780
17584
  break;
17781
17585
  case "DYNAMIC_TYPE_DRAW": {
17782
17586
  const imgArray = [];
17587
+ const temp = [];
17783
17588
  const title = data$1[dynamicId].Dynamic_Data.modules.module_dynamic.major?.opus?.title || "bilibili_dynamic";
17784
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;
17785
17590
  if (images.length === 0) break;
17786
17591
  for (const [index, img2] of images.entries()) {
17787
- const imageUrl = await processImageUrl(img2.src ?? img2.url, title, index);
17788
- imgArray.push(segment.image(imageUrl));
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,121 +17961,428 @@ 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 {
@@ -18313,7 +18473,6 @@ var DouYin = class extends Base {
18313
18473
  headers: this.headers
18314
18474
  });
18315
18475
  temp.push(liveimgbgm);
18316
- if (mergeMode === "continuous") bgmContext = await createLiveImageContext(liveimgbgm.filepath);
18317
18476
  }
18318
18477
  for (const [index, imageItem] of images.entries()) {
18319
18478
  imagenum++;
@@ -18337,26 +18496,27 @@ var DouYin = class extends Base {
18337
18496
  });
18338
18497
  if (liveimg.filepath) {
18339
18498
  const outputPath = Common.tempDri.video + `Douyin_Result_${Date.now()}.mp4`;
18340
- let success;
18341
18499
  const loopCount = imageItem.clip_type === 4 ? 1 : 3;
18342
- if (!liveimgbgm) if (loopCount > 1) success = await loopVideo(liveimg.filepath, outputPath, loopCount);
18343
- else {
18344
- fs.renameSync(liveimg.filepath, outputPath);
18345
- success = true;
18346
- }
18347
- else if (mergeMode === "continuous" && bgmContext) {
18348
- const result = await mergeLiveImageContinuous({
18349
- videoPath: liveimg.filepath,
18350
- outputPath,
18351
- loopCount
18352
- }, bgmContext);
18353
- success = result.success;
18354
- bgmContext = result.context;
18355
- } else success = await mergeLiveImageIndependent({
18356
- 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,
18357
18510
  outputPath,
18358
- loopCount
18359
- }, 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;
18360
18520
  if (success) {
18361
18521
  const filePath = Common.tempDri.video + `tmp_${Date.now()}.mp4`;
18362
18522
  fs.renameSync(outputPath, filePath);
@@ -18367,7 +18527,8 @@ var DouYin = class extends Base {
18367
18527
  filepath: filePath,
18368
18528
  totalBytes: 0
18369
18529
  });
18370
- 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));
18371
18532
  if (imageItem.clip_type === 5 && imageItem.url_list?.[0]) {
18372
18533
  const imageUrl = await processImageUrl(imageItem.url_list[0], g_title, index);
18373
18534
  processedImages.push(segment.image(imageUrl));
@@ -18375,7 +18536,7 @@ var DouYin = class extends Base {
18375
18536
  } else await Common.removeFile(liveimg.filepath, true);
18376
18537
  }
18377
18538
  }
18378
- 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);
18379
18540
  try {
18380
18541
  await this.e.bot.sendForwardMsg(this.e.contact, Element, {
18381
18542
  source: "图集内容",
@@ -18383,8 +18544,6 @@ var DouYin = class extends Base {
18383
18544
  prompt: "抖音图集解析结果",
18384
18545
  news: [{ text: "点击查看解析结果" }]
18385
18546
  });
18386
- } catch (error) {
18387
- await this.e.reply(JSON.stringify(error, null, 2));
18388
18547
  } finally {
18389
18548
  for (const item of temp) await Common.removeFile(item.filepath, true);
18390
18549
  }
@@ -18405,7 +18564,7 @@ var DouYin = class extends Base {
18405
18564
  }).getData().then((data$2) => fs.promises.writeFile(path$1, Buffer.from(data$2)));
18406
18565
  }
18407
18566
  }
18408
- 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);
18409
18568
  image_data.push(res);
18410
18569
  image_res.push(image_data);
18411
18570
  if (imageres.length === 1) {
@@ -18435,7 +18594,6 @@ var DouYin = class extends Base {
18435
18594
  headers: this.headers
18436
18595
  });
18437
18596
  temp.push(liveimgbgm);
18438
- if (mergeMode === "continuous") bgmContext = await createLiveImageContext(liveimgbgm.filepath);
18439
18597
  }
18440
18598
  const images1 = VideoData.data.aweme_detail.images ?? [];
18441
18599
  if (!images1.length) logger.debug("未获取到合辑的图片数据");
@@ -18446,51 +18604,53 @@ var DouYin = class extends Base {
18446
18604
  images.push(segment.image(imageUrl));
18447
18605
  continue;
18448
18606
  }
18449
- 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`, {
18450
18608
  title: `Douyin_tmp_V_${Date.now()}.mp4`,
18451
18609
  headers: this.headers
18452
18610
  });
18453
- if (liveimg.filepath) {
18611
+ if (livePhoto.filepath) {
18454
18612
  const outputPath = Common.tempDri.video + `Douyin_Result_${Date.now()}.mp4`;
18455
- let success;
18456
18613
  const loopCount = item.clip_type === 4 ? 1 : 3;
18457
- if (!liveimgbgm) if (loopCount > 1) success = await loopVideo(liveimg.filepath, outputPath, loopCount);
18458
- else {
18459
- fs.renameSync(liveimg.filepath, outputPath);
18460
- success = true;
18461
- }
18462
- else if (mergeMode === "continuous" && bgmContext) {
18463
- const result = await mergeLiveImageContinuous({
18464
- videoPath: liveimg.filepath,
18465
- outputPath,
18466
- loopCount
18467
- }, bgmContext);
18468
- success = result.success;
18469
- bgmContext = result.context;
18470
- } else success = await mergeLiveImageIndependent({
18471
- 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,
18472
18624
  outputPath,
18473
- loopCount
18474
- }, 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;
18475
18634
  if (success) {
18476
18635
  const filePath = Common.tempDri.video + `tmp_${Date.now()}.mp4`;
18477
18636
  fs.renameSync(outputPath, filePath);
18478
18637
  logger.mark(`视频文件重命名完成: ${outputPath.split("/").pop()} -> ${filePath.split("/").pop()}`);
18479
18638
  logger.mark("正在尝试删除缓存文件");
18480
- await Common.removeFile(liveimg.filepath, true);
18639
+ await Common.removeFile(livePhoto.filepath, true);
18481
18640
  temp.push({
18482
18641
  filepath: filePath,
18483
18642
  totalBytes: 0
18484
18643
  });
18485
- 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));
18486
18646
  if (item.clip_type === 5 && item.url_list?.[0]) {
18487
18647
  const imageUrl = await processImageUrl(item.url_list[0], g_title, index);
18488
18648
  images.push(segment.image(imageUrl));
18489
18649
  }
18490
- } else await Common.removeFile(liveimg.filepath, true);
18650
+ } else await Common.removeFile(livePhoto.filepath, true);
18491
18651
  }
18492
18652
  }
18493
- 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);
18494
18654
  try {
18495
18655
  await this.e.bot.sendForwardMsg(this.e.contact, Element, {
18496
18656
  source: "合辑内容",
@@ -18498,8 +18658,6 @@ var DouYin = class extends Base {
18498
18658
  prompt: "抖音合辑解析结果",
18499
18659
  news: [{ text: "点击查看解析结果" }]
18500
18660
  });
18501
- } catch (error) {
18502
- await this.e.reply(JSON.stringify(error, null, 2));
18503
18661
  } finally {
18504
18662
  for (const item of temp) await Common.removeFile(item.filepath, true);
18505
18663
  }
@@ -18634,8 +18792,8 @@ var DouYin = class extends Base {
18634
18792
  const imageUrl = await processImageUrl(v, VideoData.data.aweme_detail.desc, index);
18635
18793
  messageElements.push(segment.image(imageUrl));
18636
18794
  }
18637
- const res = common.makeForward(messageElements, this.e.sender.userId, this.e.sender.nick);
18638
- 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, {
18639
18797
  source: "评论图片收集",
18640
18798
  summary: `查看${messageElements.length}张图片`,
18641
18799
  prompt: "抖音评论解析结果",
@@ -18970,11 +19128,22 @@ let DouyinImageSubType = function(DouyinImageSubType$1) {
18970
19128
  return DouyinImageSubType$1;
18971
19129
  }({});
18972
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
+ };
18973
19141
  if (data$1.aweme_type === 163 || data$1.article_info) return {
18974
19142
  mainType: DouyinWorkMainType.ARTICLE,
18975
19143
  isVideo: false,
18976
19144
  isImage: false,
18977
19145
  isArticle: true,
19146
+ isLive: false,
18978
19147
  isGallery: false,
18979
19148
  isCollection: false,
18980
19149
  templatePath: "douyin/article-work"
@@ -18987,6 +19156,7 @@ function getWorkTypeInfo(data$1) {
18987
19156
  isVideo: false,
18988
19157
  isImage: true,
18989
19158
  isArticle: false,
19159
+ isLive: false,
18990
19160
  isGallery: subType === DouyinImageSubType.GALLERY,
18991
19161
  isCollection: subType === DouyinImageSubType.COLLECTION,
18992
19162
  templatePath: "douyin/image-work"
@@ -18997,6 +19167,7 @@ function getWorkTypeInfo(data$1) {
18997
19167
  isVideo: true,
18998
19168
  isImage: false,
18999
19169
  isArticle: false,
19170
+ isLive: false,
19000
19171
  isGallery: false,
19001
19172
  isCollection: false,
19002
19173
  templatePath: "douyin/video-work"
@@ -19017,6 +19188,7 @@ function getWorkTypeDisplayName(workTypeInfo) {
19017
19188
  if (workTypeInfo.isGallery) return "图集";
19018
19189
  if (workTypeInfo.isCollection) return "合辑";
19019
19190
  if (workTypeInfo.isArticle) return "文章";
19191
+ if (workTypeInfo.isLive) return "直播";
19020
19192
  return "未知";
19021
19193
  }
19022
19194
  const getDouyinID = async (event, url, log = true) => {
@@ -19550,7 +19722,6 @@ var DouYinpush = class extends Base {
19550
19722
  headers: douyinBaseHeaders
19551
19723
  });
19552
19724
  temp.push(liveimgbgm);
19553
- if (mergeMode === "continuous") bgmContext = await createLiveImageContext(liveimgbgm.filepath);
19554
19725
  }
19555
19726
  const images1 = Detail_Data.images ?? [];
19556
19727
  if (!images1.length) logger.debug("未获取到合辑的图片数据");
@@ -19566,26 +19737,27 @@ var DouYinpush = class extends Base {
19566
19737
  });
19567
19738
  if (liveimg.filepath) {
19568
19739
  const outputPath = Common.tempDri.video + `Douyin_Result_${Date.now()}.mp4`;
19569
- let success;
19570
19740
  const loopCount = item.clip_type === 4 ? 1 : 3;
19571
- if (!liveimgbgm) if (loopCount > 1) success = await loopVideo(liveimg.filepath, outputPath, loopCount);
19572
- else {
19573
- fs.renameSync(liveimg.filepath, outputPath);
19574
- success = true;
19575
- }
19576
- else if (mergeMode === "continuous" && bgmContext) {
19577
- const result = await mergeLiveImageContinuous({
19578
- videoPath: liveimg.filepath,
19579
- outputPath,
19580
- loopCount
19581
- }, bgmContext);
19582
- success = result.success;
19583
- bgmContext = result.context;
19584
- } else success = await mergeLiveImageIndependent({
19585
- 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,
19586
19751
  outputPath,
19587
- loopCount
19588
- }, 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;
19589
19761
  if (success) {
19590
19762
  const filePath = Common.tempDri.video + `tmp_${Date.now()}.mp4`;
19591
19763
  fs.renameSync(outputPath, filePath);
@@ -19596,7 +19768,8 @@ var DouYinpush = class extends Base {
19596
19768
  filepath: filePath,
19597
19769
  totalBytes: 0
19598
19770
  });
19599
- 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));
19600
19773
  if (item.clip_type === 5 && item.url_list?.[0]) {
19601
19774
  const imageUrl = await processImageUrl(item.url_list[0], Detail_Data.desc, index);
19602
19775
  images.push(segment.image(imageUrl));
@@ -19633,7 +19806,6 @@ var DouYinpush = class extends Base {
19633
19806
  headers: douyinBaseHeaders
19634
19807
  });
19635
19808
  temp.push(liveimgbgm);
19636
- if (mergeMode === "continuous") bgmContext = await createLiveImageContext(liveimgbgm.filepath);
19637
19809
  }
19638
19810
  for (const [index, item] of Detail_Data.images.entries()) {
19639
19811
  if (item.clip_type === 2 || item.clip_type === void 0) {
@@ -19647,26 +19819,27 @@ var DouYinpush = class extends Base {
19647
19819
  });
19648
19820
  if (liveimg.filepath) {
19649
19821
  const outputPath = Common.tempDri.video + `Douyin_Result_${Date.now()}.mp4`;
19650
- let success;
19651
19822
  const loopCount = item.clip_type === 4 ? 1 : 3;
19652
- if (!liveimgbgm) if (loopCount > 1) success = await loopVideo(liveimg.filepath, outputPath, loopCount);
19653
- else {
19654
- fs.renameSync(liveimg.filepath, outputPath);
19655
- success = true;
19656
- }
19657
- else if (mergeMode === "continuous" && bgmContext) {
19658
- const result = await mergeLiveImageContinuous({
19659
- videoPath: liveimg.filepath,
19660
- outputPath,
19661
- loopCount
19662
- }, bgmContext);
19663
- success = result.success;
19664
- bgmContext = result.context;
19665
- } else success = await mergeLiveImageIndependent({
19666
- 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,
19667
19833
  outputPath,
19668
- loopCount
19669
- }, 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;
19670
19843
  if (success) {
19671
19844
  const filePath = Common.tempDri.video + `tmp_${Date.now()}.mp4`;
19672
19845
  fs.renameSync(outputPath, filePath);
@@ -19677,7 +19850,8 @@ var DouYinpush = class extends Base {
19677
19850
  filepath: filePath,
19678
19851
  totalBytes: 0
19679
19852
  });
19680
- 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));
19681
19855
  if (item.clip_type === 5 && item.url_list?.[0]) {
19682
19856
  const imageUrl = await processImageUrl(item.url_list[0], Detail_Data.desc, index);
19683
19857
  processedImages.push(segment.image(imageUrl));
@@ -19985,7 +20159,6 @@ var skipDynamic = async (PushItem) => {
19985
20159
  logger.debug(`检查作品是否需要过滤:${PushItem.Detail_Data.share_url}`);
19986
20160
  return await douyinDBInstance.shouldFilter(PushItem, tags);
19987
20161
  };
19988
- init_danmaku();
19989
20162
  init_date_fns();
19990
20163
  init_locale();
19991
20164
  init_Config();
@@ -20507,7 +20680,7 @@ const task = Config.app.removeCache && karin$1.task("[kkk-缓存自动删除]",
20507
20680
  const twoHoursAgo = Date.now() - 7200 * 1e3;
20508
20681
  const videoDeleted = removeOldFiles(Common.tempDri.video, twoHoursAgo);
20509
20682
  logger.mark(`${Common.tempDri.video} 目录下已删除 ${videoDeleted} 个文件`);
20510
- if (Config.app.downloadImageLocally) {
20683
+ if (Config.upload.imageSendMode === "file") {
20511
20684
  const imageDeleted = removeOldFiles(Common.tempDri.images, twoHoursAgo);
20512
20685
  logger.mark(`${Common.tempDri.images} 目录下已删除 ${imageDeleted} 个文件`);
20513
20686
  }
@@ -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));