karin-plugin-kkk 2.23.2 → 2.25.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  import { n as __esmMin, o as __toESM, r as __export } from "./rolldown-runtime-BMXAG3ag.js";
2
2
  import { A as init_locale, An as init_zod, Cn as Chalk, Dn as axios_default, En as init_axios, On as Xhshow, Sn as require_protobufjs, Tn as AxiosError$1, _n as require_png, a as Window, bn as require_heic_decode, dt as init_date_fns, ft as fromUnixTime, ht as differenceInSeconds, i as init_lib, j as zhCN, jn as zod_default, kn as init_dist, mt as format, n as require_lib, pt as formatDistanceToNow, r as require_qr_code_styling, t as require_dist, vn as require_jsQR, wn as init_source, xn as require_express, yn as require_jpeg_js } from "./vendor-9pKTNH6x.js";
3
- import { n as init_client, r as reactServerRender } from "./template-2ApQpQ8R.js";
3
+ import { n as init_client, r as reactServerRender } from "./template-CsOboAFj.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,952 +7111,364 @@ 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;
7114
+ async function fixM4sFile(inputPath, outputPath) {
7115
+ const result = await ffmpeg(`-y -i "${inputPath}" -c copy -movflags +faststart "${outputPath}"`);
7116
+ if (result.status) logger.debug(`m4s 文件修复成功: ${outputPath}`);
7117
+ else logger.error("m4s 文件修复失败", result);
7118
+ return result.status;
7161
7119
  }
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}`;
7120
+ async function getMediaDuration(path$1) {
7121
+ const { stdout } = await ffprobe(`-v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 "${path$1}"`);
7122
+ return Number.parseFloat(stdout.trim());
7197
7123
  }
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
- };
7124
+ async function mergeVideoAudio(videoPath, audioPath, resultPath) {
7125
+ const result = await ffmpeg(`-y -i "${videoPath}" -i "${audioPath}" -c copy "${resultPath}"`);
7126
+ if (result.status) logger.debug(`视频合成成功: ${resultPath}`);
7127
+ else logger.error("视频合成失败", result);
7128
+ return result.status;
7218
7129
  }
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;
7130
+ async function compressVideo(options) {
7131
+ const { inputPath, outputPath, targetBitrate, maxRate = targetBitrate * 1.5, bufSize = targetBitrate * 2, crf = 35, removeSource = true } = options;
7132
+ const result = await ffmpeg(`-y -i "${inputPath}" -b:v ${targetBitrate}k -maxrate ${maxRate}k -bufsize ${bufSize}k -crf ${crf} -preset medium -c:v libx264 -vf "scale='if(gte(iw/ih,16/9),1280,-1)':'if(gte(iw/ih,16/9),-1,720)',scale=ceil(iw/2)*2:ceil(ih/2)*2" "${outputPath}"`);
7133
+ if (result.status) {
7134
+ logger.debug(`视频压缩成功: ${outputPath}`);
7135
+ if (removeSource) Common.removeFile(inputPath);
7136
+ } else logger.error(`视频压缩失败: ${inputPath}`, result);
7137
+ return result.status;
7233
7138
  }
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
- }
7139
+ var getMediaFrameRate, loopVideoWithTransition, xmpHeaderBuffer, isJpegBuffer, buildMotionPhotoXmp, injectXmpToJpeg, readOrConvertToJpeg, buildGoogleMotionPhoto;
7140
+ var init_FFmpeg = __esmMin(async () => {
7141
+ await init_utils$1();
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.debug(`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
+ };
7307
7226
  }
7308
- if (bestIdx === -1 || bestDist < 0 && scrollTracks[bestIdx] !== null) continue;
7309
- scrollTracks[bestIdx] = {
7310
- startTime,
7311
- duration,
7312
- textWidth
7227
+ return {
7228
+ success: result$1.status,
7229
+ context: mergeContext
7313
7230
  };
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
7231
  }
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
7232
+ const result = await ffmpeg(`-y ${inputArgs} -filter_complex "${filterComplex}" -map "[outv]" -c:v libx264 -pix_fmt yuv420p "${outputPath}"`);
7233
+ if (result.status) logger.debug(`Live Photo 效果视频重放成功: ${outputPath}`);
7234
+ else logger.error("Live Photo 效果视频重放失败", result);
7235
+ return { success: result.status };
7236
+ };
7237
+ xmpHeaderBuffer = Buffer.from("http://ns.adobe.com/xap/1.0/\0", "utf8");
7238
+ isJpegBuffer = (fileBuffer) => fileBuffer.length > 2 && fileBuffer[0] === 255 && fileBuffer[1] === 216;
7239
+ buildMotionPhotoXmp = (videoLength, presentationTimestampUs) => `<x:xmpmeta xmlns:x="adobe:ns:meta/" x:xmptk="Adobe XMP Core 5.1.0-jc003"><rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"><rdf:Description rdf:about="" ${[
7240
+ "xmlns:GCamera=\"http://ns.google.com/photos/1.0/camera/\"",
7241
+ "xmlns:MiCamera=\"http://ns.xiaomi.com/photos/1.0/camera/\"",
7242
+ "xmlns:Container=\"http://ns.google.com/photos/1.0/container/\"",
7243
+ "xmlns:Item=\"http://ns.google.com/photos/1.0/container/item/\"",
7244
+ `GCamera:MotionPhoto="1" GCamera:MotionPhotoVersion="1" GCamera:MotionPhotoPresentationTimestampUs="${presentationTimestampUs}"`,
7245
+ `GCamera:MicroVideo="1" GCamera:MicroVideoVersion="1" GCamera:MicroVideoOffset="${videoLength}"`,
7246
+ `GCamera:MicroVideoPresentationTimestampUs="${presentationTimestampUs}"`,
7247
+ "MiCamera:XMPMeta=\"&lt;?xml version=&apos;1.0&apos; encoding=&apos;UTF-8&apos; standalone=&apos;yes&apos; ?&gt;\"",
7248
+ "xmlns:OpCamera=\"http://ns.oplus.com/photos/1.0/camera/\"",
7249
+ `OpCamera:MotionPhotoPrimaryPresentationTimestampUs="${presentationTimestampUs}"`,
7250
+ "OpCamera:MotionPhotoOwner=\"oplus\"",
7251
+ "OpCamera:OLivePhotoVersion=\"2\"",
7252
+ `OpCamera:VideoLength="${videoLength}"`
7253
+ ].join(" ")}><Container:Directory><rdf:Seq><rdf:li rdf:parseType="Resource"><Container:Item Item:Mime="image/jpeg" Item:Semantic="Primary" Item:Length="0" Item:Padding="0" /></rdf:li><rdf:li rdf:parseType="Resource"><Container:Item Item:Mime="video/mp4" Item:Semantic="MotionPhoto" Item:Length="${videoLength}" Item:Padding="0" /></rdf:li></rdf:Seq></Container:Directory></rdf:Description></rdf:RDF></x:xmpmeta>`;
7254
+ injectXmpToJpeg = (jpegBuffer, xmpPacket) => {
7255
+ if (!isJpegBuffer(jpegBuffer)) throw new Error("输入图片不是 JPEG 格式");
7256
+ const xmpPayload = Buffer.concat([xmpHeaderBuffer, Buffer.from(xmpPacket, "utf8")]);
7257
+ const app1Length = xmpPayload.length + 2;
7258
+ if (app1Length > 65535) throw new Error("XMP 数据过大,无法写入 JPEG APP1");
7259
+ const app1Segment = Buffer.alloc(4);
7260
+ app1Segment[0] = 255;
7261
+ app1Segment[1] = 225;
7262
+ app1Segment.writeUInt16BE(app1Length, 2);
7263
+ return Buffer.concat([
7264
+ jpegBuffer.subarray(0, 2),
7265
+ app1Segment,
7266
+ xmpPayload,
7267
+ jpegBuffer.subarray(2)
7268
+ ]);
7326
7269
  };
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
- };
7270
+ readOrConvertToJpeg = async (imagePath) => {
7271
+ const sourceBuffer = fs.readFileSync(imagePath);
7272
+ if (isJpegBuffer(sourceBuffer)) return sourceBuffer;
7273
+ const tempJpegPath = path.join(Common.tempDri.images, `MotionPhoto_${Date.now()}_${Math.random().toString(36).slice(2)}.jpg`);
7274
+ if (!(await ffmpeg(`-y -i "${imagePath}" -frames:v 1 -q:v 2 "${tempJpegPath}"`)).status) throw new Error(`图片转换 JPEG 失败: ${imagePath}`);
7275
+ try {
7276
+ return fs.readFileSync(tempJpegPath);
7277
+ } finally {
7278
+ fs.rmSync(tempJpegPath, { force: true });
7355
7279
  }
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
7280
  };
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);
7281
+ buildGoogleMotionPhoto = async (options) => {
7282
+ const { imagePath, videoPath, outputPath, presentationTimestampUs } = options;
7283
+ try {
7284
+ const imageBuffer = await readOrConvertToJpeg(imagePath);
7285
+ const videoBuffer = fs.readFileSync(videoPath);
7286
+ let resolvedPresentationTimestampUs = presentationTimestampUs;
7287
+ if (resolvedPresentationTimestampUs === void 0 || resolvedPresentationTimestampUs < 0) {
7288
+ const videoDurationSeconds = await getMediaDuration(videoPath);
7289
+ if (Number.isFinite(videoDurationSeconds) && videoDurationSeconds > 0) resolvedPresentationTimestampUs = Math.round(videoDurationSeconds * 5e5);
7290
+ else resolvedPresentationTimestampUs = 15e5;
7291
+ }
7292
+ const jpegWithXmp = injectXmpToJpeg(imageBuffer, buildMotionPhotoXmp(videoBuffer.length, resolvedPresentationTimestampUs));
7293
+ fs.mkdirSync(path.dirname(outputPath), { recursive: true });
7294
+ fs.writeFileSync(outputPath, Buffer.concat([jpegWithXmp, videoBuffer]));
7295
+ logger.debug(`Google Motion Photo 封面生成成功: ${outputPath}`);
7296
+ return true;
7297
+ } catch (error) {
7298
+ logger.error("Google Motion Photo 封面生成失败", error);
7299
+ return false;
7431
7300
  }
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
7301
  };
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
7302
+ });
7303
+ var ERROR_CODE_MAP, RECOVERABLE_ERROR_CODES, RECOVERABLE_KEYWORDS, BASE_HEADERS;
7304
+ var init_constants = __esmMin(() => {
7305
+ ERROR_CODE_MAP = {
7306
+ ECONNRESET: "连接被重置",
7307
+ ECONNREFUSED: "连接被拒绝",
7308
+ ECONNABORTED: "连接中止",
7309
+ ETIMEDOUT: "连接超时",
7310
+ ENETUNREACH: "网络不可达",
7311
+ EHOSTUNREACH: "主机不可达",
7312
+ ENOTFOUND: "DNS解析失败",
7313
+ EPIPE: "管道破裂",
7314
+ EAI_AGAIN: "DNS临时失败",
7315
+ ERR_BAD_OPTION_VALUE: "无效的配置选项值",
7316
+ ERR_BAD_OPTION: "无效的配置选项",
7317
+ ERR_NETWORK: "网络错误",
7318
+ ERR_DEPRECATED: "使用了已弃用的功能",
7319
+ ERR_BAD_RESPONSE: "无效的响应",
7320
+ ERR_BAD_REQUEST: "无效的请求",
7321
+ ERR_CANCELED: "请求被取消",
7322
+ ERR_NOT_SUPPORT: "不支持的功能",
7323
+ ERR_INVALID_URL: "无效的URL",
7324
+ EHTTP: "HTTP错误",
7325
+ EACCES: "权限不足",
7326
+ ENOENT: "文件或目录不存在",
7327
+ EMFILE: "打开的文件过多",
7328
+ ENOSPC: "磁盘空间不足"
7329
+ };
7330
+ RECOVERABLE_ERROR_CODES = [
7331
+ "ECONNRESET",
7332
+ "ETIMEDOUT",
7333
+ "ECONNREFUSED",
7334
+ "ENOTFOUND",
7335
+ "ENETUNREACH",
7336
+ "EHOSTUNREACH",
7337
+ "EPIPE",
7338
+ "EAI_AGAIN",
7339
+ "ECONNABORTED"
7340
+ ];
7341
+ RECOVERABLE_KEYWORDS = [
7342
+ "aborted",
7343
+ "timeout",
7344
+ "network",
7345
+ "ECONNRESET",
7346
+ "socket hang up",
7347
+ "connection reset"
7348
+ ];
7349
+ BASE_HEADERS = {
7350
+ Accept: "*/*",
7351
+ "accept-language": "zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6",
7352
+ "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36 Edg/137.0.0.0"
7353
+ };
7354
+ });
7355
+ var getErrorDescription, sanitizeHeaders, isRecoverableNetworkError, isThrottlingError, calculateBackoffDelay, formatBytes, sanitizeFilename;
7356
+ var init_helpers = __esmMin(() => {
7357
+ init_constants();
7358
+ getErrorDescription = (error) => {
7359
+ const code = error?.code || (error instanceof AxiosError ? error.code : null);
7360
+ const message = error?.message || String(error);
7361
+ if (code && ERROR_CODE_MAP[code]) return `${ERROR_CODE_MAP[code]} (${code}): ${message}`;
7362
+ else if (code) return `错误代码 ${code}: ${message}`;
7363
+ else return message;
7364
+ };
7365
+ sanitizeHeaders = (headers) => {
7366
+ if (!headers) return {};
7367
+ const sanitized = {};
7368
+ const sensitiveKeys = [
7369
+ "cookie",
7370
+ "cookies",
7371
+ "authorization",
7372
+ "x-api-key",
7373
+ "api-key",
7374
+ "token"
7375
+ ];
7376
+ const ipSensitiveKeys = [
7377
+ "x-forwarded-for",
7378
+ "x-real-ip",
7379
+ "x-client-ip",
7380
+ "x-originating-ip"
7381
+ ];
7382
+ for (const [key, value] of Object.entries(headers)) {
7383
+ const lowerKey = key.toLowerCase();
7384
+ if (sensitiveKeys.some((sk) => lowerKey.includes(sk))) continue;
7385
+ if (ipSensitiveKeys.some((sk) => lowerKey.includes(sk))) continue;
7386
+ sanitized[key] = String(value);
7498
7387
  }
7388
+ return sanitized;
7389
+ };
7390
+ isRecoverableNetworkError = (error) => {
7391
+ if (error?.code && RECOVERABLE_ERROR_CODES.includes(error.code)) return true;
7392
+ if (error instanceof AxiosError) {
7393
+ if (error.code && RECOVERABLE_ERROR_CODES.includes(error.code)) return true;
7394
+ const errorMessage = error.message?.toLowerCase() || "";
7395
+ if (RECOVERABLE_KEYWORDS.some((keyword) => errorMessage.includes(keyword.toLowerCase()))) return true;
7396
+ }
7397
+ if (error instanceof Error) {
7398
+ const errorMessage = error.message?.toLowerCase() || "";
7399
+ if (RECOVERABLE_KEYWORDS.some((keyword) => errorMessage.includes(keyword.toLowerCase()))) return true;
7400
+ }
7401
+ return false;
7402
+ };
7403
+ isThrottlingError = (error) => {
7404
+ if ((error?.code || (error instanceof AxiosError ? error.code : null)) === "ECONNRESET") return true;
7405
+ const message = error?.message?.toLowerCase() || "";
7406
+ return [
7407
+ "connection reset",
7408
+ "socket hang up",
7409
+ "econnreset"
7410
+ ].some((keyword) => message.includes(keyword));
7411
+ };
7412
+ calculateBackoffDelay = (retryCount, baseDelay = 1e3, maxDelay = 8e3) => Math.min(2 ** retryCount * baseDelay, maxDelay);
7413
+ formatBytes = (bytes) => {
7414
+ if (bytes < 1024) return `${bytes} B`;
7415
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
7416
+ if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(2)} MB`;
7417
+ return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)} GB`;
7499
7418
  };
7500
- MAX_OUTPUT_WIDTH$1 = 2160;
7419
+ sanitizeFilename = (filename) => filename.replace(/[<>:"/\\|?*\x00-\x1f]/g, "_").replace(/^\.+/, "").replace(/\.+$/, "").replace(/\s+/g, "_").substring(0, 200);
7501
7420
  });
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;
7421
+ var ThrottleStream;
7422
+ var init_ThrottleStream = __esmMin(() => {
7423
+ ThrottleStream = class extends Transform {
7424
+ bytesPerSecond;
7425
+ startTime;
7426
+ totalBytes;
7427
+ constructor(bytesPerSecond) {
7428
+ super();
7429
+ this.bytesPerSecond = bytesPerSecond;
7430
+ this.startTime = Date.now();
7431
+ this.totalBytes = 0;
7510
7432
  }
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);
7433
+ setSpeed(newSpeed) {
7434
+ this.bytesPerSecond = newSpeed;
7435
+ this.startTime = Date.now();
7436
+ this.totalBytes = 0;
7621
7437
  }
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;
7438
+ getSpeed() {
7439
+ return this.bytesPerSecond;
7634
7440
  }
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
- }
7441
+ getCurrentSpeed() {
7442
+ const elapsed = (Date.now() - this.startTime) / 1e3;
7443
+ if (elapsed <= 0) return 0;
7444
+ return this.totalBytes / elapsed;
7445
+ }
7446
+ _transform(chunk, _encoding, callback) {
7447
+ this.totalBytes += chunk.length;
7448
+ const elapsed = (Date.now() - this.startTime) / 1e3;
7449
+ const expectedTime = this.totalBytes / this.bytesPerSecond;
7450
+ const delay$1 = Math.max(0, (expectedTime - elapsed) * 1e3);
7451
+ if (delay$1 > 0) setTimeout(() => {
7452
+ this.push(chunk);
7453
+ callback();
7454
+ }, delay$1);
7455
+ else {
7456
+ this.push(chunk);
7457
+ callback();
7649
7458
  }
7650
7459
  }
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
- };
7460
+ _flush(callback) {
7461
+ callback();
7697
7462
  }
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
7463
  };
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
- async function fixM4sFile(inputPath, outputPath) {
7814
- const result = await ffmpeg(`-y -i "${inputPath}" -c copy -movflags +faststart "${outputPath}"`);
7815
- if (result.status) logger.debug(`m4s 文件修复成功: ${outputPath}`);
7816
- else logger.error("m4s 文件修复失败", result);
7817
- return result.status;
7818
- }
7819
- async function getMediaDuration(path$1) {
7820
- 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;
7832
- }
7833
- async function mergeVideoAudio(videoPath, audioPath, resultPath) {
7834
- const result = await ffmpeg(`-y -i "${videoPath}" -i "${audioPath}" -c copy "${resultPath}"`);
7835
- if (result.status) logger.mark(`视频合成成功: ${resultPath}`);
7836
- else logger.error("视频合成失败", result);
7837
- return result.status;
7838
- }
7839
- async function compressVideo(options) {
7840
- const { inputPath, outputPath, targetBitrate, maxRate = targetBitrate * 1.5, bufSize = targetBitrate * 2, crf = 35, removeSource = true } = options;
7841
- const result = await ffmpeg(`-y -i "${inputPath}" -b:v ${targetBitrate}k -maxrate ${maxRate}k -bufsize ${bufSize}k -crf ${crf} -preset medium -c:v libx264 -vf "scale='if(gte(iw/ih,16/9),1280,-1)':'if(gte(iw/ih,16/9),-1,720)',scale=ceil(iw/2)*2:ceil(ih/2)*2" "${outputPath}"`);
7842
- if (result.status) {
7843
- logger.mark(`视频压缩成功: ${outputPath}`);
7844
- if (removeSource) Common.removeFile(inputPath);
7845
- } else logger.error(`视频压缩失败: ${inputPath}`, result);
7846
- return result.status;
7847
- }
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
- }
7886
- var init_FFmpeg = __esmMin(async () => {
7887
- await init_utils$1();
7888
- await init_danmaku$1();
7889
- await init_danmaku();
7890
- });
7891
- var ERROR_CODE_MAP, RECOVERABLE_ERROR_CODES, RECOVERABLE_KEYWORDS, BASE_HEADERS;
7892
- var init_constants = __esmMin(() => {
7893
- ERROR_CODE_MAP = {
7894
- ECONNRESET: "连接被重置",
7895
- ECONNREFUSED: "连接被拒绝",
7896
- ECONNABORTED: "连接中止",
7897
- ETIMEDOUT: "连接超时",
7898
- ENETUNREACH: "网络不可达",
7899
- EHOSTUNREACH: "主机不可达",
7900
- ENOTFOUND: "DNS解析失败",
7901
- EPIPE: "管道破裂",
7902
- EAI_AGAIN: "DNS临时失败",
7903
- ERR_BAD_OPTION_VALUE: "无效的配置选项值",
7904
- ERR_BAD_OPTION: "无效的配置选项",
7905
- ERR_NETWORK: "网络错误",
7906
- ERR_DEPRECATED: "使用了已弃用的功能",
7907
- ERR_BAD_RESPONSE: "无效的响应",
7908
- ERR_BAD_REQUEST: "无效的请求",
7909
- ERR_CANCELED: "请求被取消",
7910
- ERR_NOT_SUPPORT: "不支持的功能",
7911
- ERR_INVALID_URL: "无效的URL",
7912
- EHTTP: "HTTP错误",
7913
- EACCES: "权限不足",
7914
- ENOENT: "文件或目录不存在",
7915
- EMFILE: "打开的文件过多",
7916
- ENOSPC: "磁盘空间不足"
7917
- };
7918
- RECOVERABLE_ERROR_CODES = [
7919
- "ECONNRESET",
7920
- "ETIMEDOUT",
7921
- "ECONNREFUSED",
7922
- "ENOTFOUND",
7923
- "ENETUNREACH",
7924
- "EHOSTUNREACH",
7925
- "EPIPE",
7926
- "EAI_AGAIN",
7927
- "ECONNABORTED"
7928
- ];
7929
- RECOVERABLE_KEYWORDS = [
7930
- "aborted",
7931
- "timeout",
7932
- "network",
7933
- "ECONNRESET",
7934
- "socket hang up",
7935
- "connection reset"
7936
- ];
7937
- BASE_HEADERS = {
7938
- Accept: "*/*",
7939
- "accept-language": "zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6",
7940
- "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36 Edg/137.0.0.0"
7941
- };
7942
- });
7943
- var getErrorDescription, sanitizeHeaders, isRecoverableNetworkError, isThrottlingError, calculateBackoffDelay, formatBytes, sanitizeFilename;
7944
- var init_helpers = __esmMin(() => {
7945
- init_constants();
7946
- getErrorDescription = (error) => {
7947
- const code = error?.code || (error instanceof AxiosError ? error.code : null);
7948
- const message = error?.message || String(error);
7949
- if (code && ERROR_CODE_MAP[code]) return `${ERROR_CODE_MAP[code]} (${code}): ${message}`;
7950
- else if (code) return `错误代码 ${code}: ${message}`;
7951
- else return message;
7952
- };
7953
- sanitizeHeaders = (headers) => {
7954
- if (!headers) return {};
7955
- const sanitized = {};
7956
- const sensitiveKeys = [
7957
- "cookie",
7958
- "cookies",
7959
- "authorization",
7960
- "x-api-key",
7961
- "api-key",
7962
- "token"
7963
- ];
7964
- const ipSensitiveKeys = [
7965
- "x-forwarded-for",
7966
- "x-real-ip",
7967
- "x-client-ip",
7968
- "x-originating-ip"
7969
- ];
7970
- for (const [key, value] of Object.entries(headers)) {
7971
- const lowerKey = key.toLowerCase();
7972
- if (sensitiveKeys.some((sk) => lowerKey.includes(sk))) continue;
7973
- if (ipSensitiveKeys.some((sk) => lowerKey.includes(sk))) continue;
7974
- sanitized[key] = String(value);
7975
- }
7976
- return sanitized;
7977
- };
7978
- isRecoverableNetworkError = (error) => {
7979
- if (error?.code && RECOVERABLE_ERROR_CODES.includes(error.code)) return true;
7980
- if (error instanceof AxiosError) {
7981
- if (error.code && RECOVERABLE_ERROR_CODES.includes(error.code)) return true;
7982
- const errorMessage = error.message?.toLowerCase() || "";
7983
- if (RECOVERABLE_KEYWORDS.some((keyword) => errorMessage.includes(keyword.toLowerCase()))) return true;
7984
- }
7985
- if (error instanceof Error) {
7986
- const errorMessage = error.message?.toLowerCase() || "";
7987
- if (RECOVERABLE_KEYWORDS.some((keyword) => errorMessage.includes(keyword.toLowerCase()))) return true;
7988
- }
7989
- return false;
7990
- };
7991
- isThrottlingError = (error) => {
7992
- if ((error?.code || (error instanceof AxiosError ? error.code : null)) === "ECONNRESET") return true;
7993
- const message = error?.message?.toLowerCase() || "";
7994
- return [
7995
- "connection reset",
7996
- "socket hang up",
7997
- "econnreset"
7998
- ].some((keyword) => message.includes(keyword));
7999
- };
8000
- calculateBackoffDelay = (retryCount, baseDelay = 1e3, maxDelay = 8e3) => Math.min(2 ** retryCount * baseDelay, maxDelay);
8001
- formatBytes = (bytes) => {
8002
- if (bytes < 1024) return `${bytes} B`;
8003
- if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
8004
- if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(2)} MB`;
8005
- return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)} GB`;
8006
- };
8007
- sanitizeFilename = (filename) => filename.replace(/[<>:"/\\|?*\x00-\x1f]/g, "_").replace(/^\.+/, "").replace(/\.+$/, "").replace(/\s+/g, "_").substring(0, 200);
8008
- });
8009
- var ThrottleStream;
8010
- var init_ThrottleStream = __esmMin(() => {
8011
- ThrottleStream = class extends Transform {
8012
- bytesPerSecond;
8013
- startTime;
8014
- totalBytes;
8015
- constructor(bytesPerSecond) {
8016
- super();
8017
- this.bytesPerSecond = bytesPerSecond;
8018
- this.startTime = Date.now();
8019
- this.totalBytes = 0;
8020
- }
8021
- setSpeed(newSpeed) {
8022
- this.bytesPerSecond = newSpeed;
8023
- this.startTime = Date.now();
8024
- this.totalBytes = 0;
8025
- }
8026
- getSpeed() {
8027
- return this.bytesPerSecond;
8028
- }
8029
- getCurrentSpeed() {
8030
- const elapsed = (Date.now() - this.startTime) / 1e3;
8031
- if (elapsed <= 0) return 0;
8032
- return this.totalBytes / elapsed;
8033
- }
8034
- _transform(chunk, _encoding, callback) {
8035
- this.totalBytes += chunk.length;
8036
- const elapsed = (Date.now() - this.startTime) / 1e3;
8037
- const expectedTime = this.totalBytes / this.bytesPerSecond;
8038
- const delay$1 = Math.max(0, (expectedTime - elapsed) * 1e3);
8039
- if (delay$1 > 0) setTimeout(() => {
8040
- this.push(chunk);
8041
- callback();
8042
- }, delay$1);
8043
- else {
8044
- this.push(chunk);
8045
- callback();
8046
- }
8047
- }
8048
- _flush(callback) {
8049
- callback();
8050
- }
8051
- };
8052
- });
8053
- var DEFAULT_THROTTLE_CONFIG;
8054
- var init_types = __esmMin(() => {
8055
- DEFAULT_THROTTLE_CONFIG = {
8056
- enabled: true,
8057
- maxSpeed: 10 * 1024 * 1024,
8058
- autoReduceRatio: .7,
8059
- minSpeed: 1 * 1024 * 1024
7464
+ });
7465
+ var DEFAULT_THROTTLE_CONFIG;
7466
+ var init_types = __esmMin(() => {
7467
+ DEFAULT_THROTTLE_CONFIG = {
7468
+ enabled: true,
7469
+ maxSpeed: 10 * 1024 * 1024,
7470
+ autoReduceRatio: .7,
7471
+ minSpeed: 1 * 1024 * 1024
8060
7472
  };
8061
7473
  });
8062
7474
  var Downloader;
@@ -8435,7 +7847,7 @@ var init_Network$1 = __esmMin(() => {
8435
7847
  if (!isRecoverableNetworkError(error)) return Promise.reject(error);
8436
7848
  config$1.__retryCount += 1;
8437
7849
  const nextDelay = Math.max(1e3, Math.min(2 ** (config$1.__retryCount - 1) * 1e3, 8e3));
8438
- logger.warn(`请求失败,正在重试... (${config$1.__retryCount}/${this.maxRetries}),将在 ${nextDelay / 1e3} 秒后重试`);
7850
+ logger.warn(`[karin-plugin-kkk] axios 实例请求失败,正在重试... (${config$1.__retryCount}/${this.maxRetries}),将在 ${nextDelay / 1e3} 秒后重试`);
8439
7851
  await new Promise((resolve$1) => setTimeout(resolve$1, nextDelay));
8440
7852
  return this.axiosInstance(config$1);
8441
7853
  });
@@ -11749,6 +11161,12 @@ var init_app_schema = __esmMin(() => {
11749
11161
  label: "解析提示",
11750
11162
  description: "发送提示信息:\"检测到xxx链接,开始解析\""
11751
11163
  },
11164
+ {
11165
+ key: "fakeForward",
11166
+ type: "switch",
11167
+ label: "伪造合并转发消息",
11168
+ description: "开启后合并转发将使用触发者身份展示;关闭后使用机器人身份展示"
11169
+ },
11752
11170
  {
11753
11171
  key: "errorLogSendTo",
11754
11172
  type: "checkbox",
@@ -12983,18 +12401,25 @@ var init_upload_schema = __esmMin(() => {
12983
12401
  title: "发送方式配置"
12984
12402
  },
12985
12403
  {
12986
- key: "sendbase64",
12987
- type: "switch",
12988
- label: "转换Base64",
12989
- description: "发送视频经本插件转换为base64格式后再发送,适合Karin与机器人不在同一网络环境下开启。与「群文件上传」互斥。",
12990
- disabled: $var("usegroupfile")
12404
+ key: "videoSendMode",
12405
+ type: "radio",
12406
+ label: "本地视频发送方式",
12407
+ description: "选择发送本地视频的方式:\n• File - 使用 file 协议发送(需 Karin 与协议端同系统)\n• Base64 - 转 base64 发送(传输数据量增大 1/3,不在同一网络环境可能导致额外带宽成本,适合 karin 和协议端不在同一网络环境)",
12408
+ disabled: $var("usegroupfile"),
12409
+ options: [{
12410
+ label: "File 协议(本地文件)",
12411
+ value: "file"
12412
+ }, {
12413
+ label: "Base64(编码传输)",
12414
+ value: "base64"
12415
+ }]
12991
12416
  },
12992
12417
  {
12993
12418
  key: "usegroupfile",
12994
12419
  type: "switch",
12995
12420
  label: "群文件上传",
12996
- description: "使用群文件上传,开启后会将视频文件上传到群文件中,需配置「群文件上传阈值」。与「转换Base64」互斥",
12997
- disabled: $var("sendbase64")
12421
+ description: "使用群文件上传,开启后会将视频文件上传到群文件中,需配置「群文件上传阈值」。与「本地视频发送方式 = Base64」互斥。",
12422
+ disabled: $eq("videoSendMode", "base64")
12998
12423
  },
12999
12424
  {
13000
12425
  key: "groupfilevalue",
@@ -13002,14 +12427,14 @@ var init_upload_schema = __esmMin(() => {
13002
12427
  inputType: "number",
13003
12428
  label: "群文件上传阈值",
13004
12429
  description: "当文件大小超过该值时将使用群文件上传,单位:MB,「使用群文件上传」开启后才会生效",
13005
- disabled: $or($not("usegroupfile"), $var("sendbase64")),
12430
+ disabled: $or($not("usegroupfile"), $eq("videoSendMode", "base64")),
13006
12431
  rules: [{ min: 1 }]
13007
12432
  },
13008
12433
  {
13009
12434
  key: "imageSendMode",
13010
12435
  type: "radio",
13011
12436
  label: "网络图片发送方式",
13012
- description: "选择发送网络图片的方式:\n• URL - 直接传递链接给上游(可能因上游网络问题超时)\n• File - 下载后用 file 协议发送(需 Karin 与协议端同系统)\n• Base64 - 转 base64 发送(传输数据增大 1/3,不在同一网络环境可能导致额外带宽成本)",
12437
+ description: "选择发送网络图片的方式:\n• URL - 直接传递链接给上游(可能因上游网络问题超时)\n• File - 下载后用 file 协议发送(需 Karin 与协议端同系统)\n• Base64 - 转 base64 发送(传输数据量增大 1/3,不在同一网络环境可能导致额外带宽成本)",
13013
12438
  options: [
13014
12439
  {
13015
12440
  label: "URL 链接(直接传递)",
@@ -15110,6 +14535,11 @@ const webConfig = defineConfig({
15110
14535
  description: "发送提示信息:\"检测到xxx链接,开始解析\"",
15111
14536
  defaultSelected: all.app.parseTip
15112
14537
  }),
14538
+ components.switch.create("fakeForward", {
14539
+ label: "伪造合并转发消息",
14540
+ description: "开启后合并转发将使用触发者身份展示;关闭后使用机器人身份展示",
14541
+ defaultSelected: all.app.fakeForward
14542
+ }),
15113
14543
  components.checkbox.group("errorLogSendTo", {
15114
14544
  label: "错误日志",
15115
14545
  description: "遇到错误时谁会收到错误日志。注:推送任务只可发送给主人。「第一个主人」与「所有主人」互斥。",
@@ -15176,24 +14606,33 @@ const webConfig = defineConfig({
15176
14606
  description: "发送方式配置",
15177
14607
  descPosition: 20
15178
14608
  }),
15179
- components.switch.create("sendbase64", {
15180
- label: "转换Base64",
15181
- description: "发送视频经本插件转换为base64格式后再发送,适合Karin与机器人不在同一网络环境下开启。与「群文件上传」互斥。",
15182
- defaultSelected: all.upload.sendbase64,
15183
- isDisabled: all.upload.usegroupfile
14609
+ components.radio.group("videoSendMode", {
14610
+ label: "本地视频发送方式",
14611
+ orientation: "vertical",
14612
+ defaultValue: all.upload.videoSendMode,
14613
+ isDisabled: all.upload.usegroupfile,
14614
+ radio: [components.radio.create("videoSendMode:radio-1", {
14615
+ label: "File 协议(本地文件)",
14616
+ description: "使用 file 协议发送本地视频,需 Karin 与协议端在同一系统",
14617
+ value: "file"
14618
+ }), components.radio.create("videoSendMode:radio-2", {
14619
+ label: "Base64(编码传输)",
14620
+ description: "将本地视频转换为 base64 发送,传输数据量增大约 30%,不在同一网络环境可能导致额外带宽成本,适合 karin 和协议端不在同一网络环境",
14621
+ value: "base64"
14622
+ })]
15184
14623
  }),
15185
14624
  components.switch.create("usegroupfile", {
15186
14625
  label: "群文件上传",
15187
- description: "使用群文件上传,开启后会将视频文件上传到群文件中,需配置「群文件上传阈值」。与「转换Base64」互斥",
14626
+ description: "使用群文件上传,开启后会将视频文件上传到群文件中,需配置「群文件上传阈值」。与「本地视频发送方式 = Base64」互斥。",
15188
14627
  defaultSelected: all.upload.usegroupfile,
15189
- isDisabled: all.upload.sendbase64
14628
+ isDisabled: all.upload.videoSendMode === "base64"
15190
14629
  }),
15191
14630
  components.input.number("groupfilevalue", {
15192
14631
  label: "群文件上传阈值",
15193
14632
  description: "当文件大小超过该值时将使用群文件上传,单位:MB,「使用群文件上传」开启后才会生效",
15194
14633
  defaultValue: all.upload.groupfilevalue.toString(),
15195
14634
  rules: [{ min: 1 }],
15196
- isDisabled: !all.upload.usegroupfile || all.upload.sendbase64
14635
+ isDisabled: !all.upload.usegroupfile || all.upload.videoSendMode === "base64"
15197
14636
  }),
15198
14637
  components.radio.group("imageSendMode", {
15199
14638
  label: "网络图片发送方式",
@@ -15212,7 +14651,7 @@ const webConfig = defineConfig({
15212
14651
  }),
15213
14652
  components.radio.create("imageSendMode:radio-3", {
15214
14653
  label: "Base64(编码传输)",
15215
- description: "下载后转换为 base64 发送,传输数据增大约 1/3,不在同一网络环境可能导致额外带宽成本",
14654
+ description: "下载后转换为 base64 发送,传输数据量增大约 30%,不在同一网络环境可能导致额外带宽成本",
15216
14655
  value: "base64"
15217
14656
  })
15218
14657
  ]
@@ -15624,139 +15063,523 @@ const webConfig = defineConfig({
15624
15063
  }
15625
15064
  }
15626
15065
  }
15627
- await Config.syncConfigToDatabase();
15628
- if (needReloadAmagi) try {
15629
- const { reloadAmagiConfig: reloadAmagiConfig$1 } = await Promise.resolve().then(() => (init_amagiClient(), amagiClient_exports));
15630
- reloadAmagiConfig$1();
15631
- } catch (error) {
15632
- logger.error(`[WebConfig] 重载 Amagi Client 失败: ${error}`);
15633
- }
15634
- return {
15635
- mergeCfg,
15636
- formatCfg,
15637
- success,
15638
- message: success ? "保存成功 Ciallo~(∠・ω< )⌒☆" : "配置无变化 Ciallo~(∠・ω< )⌒☆"
15639
- };
15640
- }
15641
- });
15642
- var web_config_default = webConfig;
15643
- var customizer = (value, srcValue) => {
15644
- if (Array.isArray(srcValue)) return srcValue;
15645
- };
15646
- var deepEqual = (a, b) => {
15647
- if (a === b) return false;
15648
- if (typeof a === "string" && typeof b === "string") {
15649
- if (a !== b) return true;
15650
- }
15651
- if (typeof a === "number" && typeof b === "number") {
15652
- if (a !== b) return true;
15653
- }
15654
- if (typeof a === "boolean" && typeof b === "boolean") {
15655
- if (a !== b) return true;
15656
- }
15657
- if (a === null || b === null || typeof a !== typeof b) return true;
15658
- if (Array.isArray(a) && Array.isArray(b)) {
15659
- if (a.length !== b.length) return true;
15660
- for (let i = 0; i < a.length; i++) if (deepEqual(a[i], b[i])) return true;
15066
+ await Config.syncConfigToDatabase();
15067
+ if (needReloadAmagi) try {
15068
+ const { reloadAmagiConfig: reloadAmagiConfig$1 } = await Promise.resolve().then(() => (init_amagiClient(), amagiClient_exports));
15069
+ reloadAmagiConfig$1();
15070
+ } catch (error) {
15071
+ logger.error(`[WebConfig] 重载 Amagi Client 失败: ${error}`);
15072
+ }
15073
+ return {
15074
+ mergeCfg,
15075
+ formatCfg,
15076
+ success,
15077
+ message: success ? "保存成功 Ciallo~(∠・ω< )⌒☆" : "配置无变化 Ciallo~(∠・ω< )⌒☆"
15078
+ };
15079
+ }
15080
+ });
15081
+ var web_config_default = webConfig;
15082
+ var customizer = (value, srcValue) => {
15083
+ if (Array.isArray(srcValue)) return srcValue;
15084
+ };
15085
+ var deepEqual = (a, b) => {
15086
+ if (a === b) return false;
15087
+ if (typeof a === "string" && typeof b === "string") {
15088
+ if (a !== b) return true;
15089
+ }
15090
+ if (typeof a === "number" && typeof b === "number") {
15091
+ if (a !== b) return true;
15092
+ }
15093
+ if (typeof a === "boolean" && typeof b === "boolean") {
15094
+ if (a !== b) return true;
15095
+ }
15096
+ if (a === null || b === null || typeof a !== typeof b) return true;
15097
+ if (Array.isArray(a) && Array.isArray(b)) {
15098
+ if (a.length !== b.length) return true;
15099
+ for (let i = 0; i < a.length; i++) if (deepEqual(a[i], b[i])) return true;
15100
+ }
15101
+ let isChange = false;
15102
+ if (typeof a === "object" && typeof b === "object") {
15103
+ if (isChange) return true;
15104
+ const keysA = Object.keys(a);
15105
+ const keysB = Object.keys(b);
15106
+ if (keysA.length !== keysB.length) return true;
15107
+ for (const key of keysA) {
15108
+ if (!keysB.includes(key)) {
15109
+ isChange = true;
15110
+ return true;
15111
+ }
15112
+ if (deepEqual(a[key], b[key])) {
15113
+ isChange = true;
15114
+ return true;
15115
+ }
15116
+ }
15117
+ }
15118
+ return false;
15119
+ };
15120
+ var convertToNumber = (value) => {
15121
+ if (/^\d+$/.test(value)) return parseInt(value, 10);
15122
+ else return value;
15123
+ };
15124
+ var getFirstObject = (arr) => arr.length > 0 ? arr[0] : {};
15125
+ var setNestedProperty = (obj, keys, value) => {
15126
+ let current = obj;
15127
+ for (let i = 0; i < keys.length - 1; i++) {
15128
+ const key = keys[i];
15129
+ if (!current[key] || typeof current[key] !== "object") current[key] = {};
15130
+ current = current[key];
15131
+ }
15132
+ const lastKey = keys[keys.length - 1];
15133
+ current[lastKey] = value;
15134
+ };
15135
+ var processFrontendData = (data$1) => {
15136
+ const result = {};
15137
+ const configKeys = Object.keys(data$1).filter((key) => !key.includes("pushlist") && key in data$1);
15138
+ for (const key of configKeys) {
15139
+ const value = data$1[key];
15140
+ const firstObj = Array.isArray(value) ? getFirstObject(value) : {};
15141
+ const objKeys = Object.keys(firstObj);
15142
+ if (objKeys.length === 0) continue;
15143
+ const configObj = {};
15144
+ let hasValidData = false;
15145
+ const nestedProps = objKeys.filter((prop) => prop.includes(":"));
15146
+ const flatProps = objKeys.filter((prop) => !prop.includes(":"));
15147
+ for (const prop of nestedProps) {
15148
+ let propValue = firstObj[prop];
15149
+ if (typeof propValue === "string") propValue = convertToNumber(propValue);
15150
+ if (propValue !== void 0 && propValue !== null) {
15151
+ setNestedProperty(configObj, prop.split(":"), propValue);
15152
+ hasValidData = true;
15153
+ }
15154
+ }
15155
+ for (const prop of flatProps) {
15156
+ let propValue = firstObj[prop];
15157
+ if (typeof propValue === "string") propValue = convertToNumber(propValue);
15158
+ if (propValue !== void 0 && propValue !== null) {
15159
+ configObj[prop] = propValue;
15160
+ hasValidData = true;
15161
+ }
15162
+ }
15163
+ if (hasValidData && Object.keys(configObj).length > 0) result[key] = configObj;
15164
+ }
15165
+ result.pushlist = {
15166
+ douyin: data$1["pushlist:douyin"] || [],
15167
+ bilibili: (data$1["pushlist:bilibili"] || []).map((item) => ({
15168
+ ...item,
15169
+ host_mid: Number(item.host_mid)
15170
+ }))
15171
+ };
15172
+ return result;
15173
+ };
15174
+ var cleanFlattenedFields = (obj) => {
15175
+ if (!obj || typeof obj !== "object") return;
15176
+ for (const [, value] of Object.entries(obj)) if (typeof value === "object" && value !== null && !Array.isArray(value)) {
15177
+ cleanFlattenedFields(value);
15178
+ const valueObj = value;
15179
+ const flattenedKeys = Object.keys(valueObj).filter((k) => k.includes("."));
15180
+ for (const flatKey of flattenedKeys) if (hasNestedStructure(valueObj, flatKey.split("."))) delete valueObj[flatKey];
15181
+ }
15182
+ };
15183
+ var hasNestedStructure = (obj, path$1) => {
15184
+ let current = obj;
15185
+ for (let i = 0; i < path$1.length - 1; i++) {
15186
+ const key = path$1[i];
15187
+ if (!current[key] || typeof current[key] !== "object") return false;
15188
+ current = current[key];
15189
+ }
15190
+ return path$1[path$1.length - 1] in current;
15191
+ };
15192
+ await init_utils$1();
15193
+ var ENCODER_PRIORITY$1 = {
15194
+ h264: [
15195
+ "h264_nvenc",
15196
+ "h264_qsv",
15197
+ "h264_amf",
15198
+ "libx264"
15199
+ ],
15200
+ h265: [
15201
+ "hevc_nvenc",
15202
+ "hevc_qsv",
15203
+ "hevc_amf",
15204
+ "libx265"
15205
+ ],
15206
+ av1: [
15207
+ "av1_nvenc",
15208
+ "av1_qsv",
15209
+ "av1_amf",
15210
+ "libsvtav1",
15211
+ "libaom-av1"
15212
+ ]
15213
+ };
15214
+ var SOFTWARE_FALLBACK$1 = {
15215
+ h264: "libx264",
15216
+ h265: "libx265",
15217
+ av1: "libsvtav1"
15218
+ };
15219
+ var cachedEncoders$1 = {};
15220
+ async function detectEncoder$1(codec) {
15221
+ if (cachedEncoders$1[codec]) return cachedEncoders$1[codec];
15222
+ logger.debug(`[BiliDanmaku] 开始检测 ${codec.toUpperCase()} 编码器...`);
15223
+ for (const encoder of ENCODER_PRIORITY$1[codec]) {
15224
+ logger.debug(`[BiliDanmaku] 测试编码器: ${encoder}`);
15225
+ try {
15226
+ const result = await ffmpeg(`-f lavfi -i color=c=black:s=320x240:d=0.1 -c:v ${encoder} -f null -`);
15227
+ logger.debug(`[BiliDanmaku] ${encoder} 测试结果: status=${result.status}`);
15228
+ if (result.status) {
15229
+ cachedEncoders$1[codec] = encoder;
15230
+ logger.info(`[BiliDanmaku] 使用 ${codec.toUpperCase()} 编码器: ${encoder}`);
15231
+ return encoder;
15232
+ }
15233
+ } catch (e) {
15234
+ logger.debug(`[BiliDanmaku] 编码器 ${encoder} 测试异常: ${e}`);
15235
+ }
15236
+ }
15237
+ const fallback = SOFTWARE_FALLBACK$1[codec];
15238
+ cachedEncoders$1[codec] = fallback;
15239
+ logger.info(`[BiliDanmaku] 回退到软件编码器: ${fallback}`);
15240
+ return fallback;
15241
+ }
15242
+ async function getVideoBitrate$1(path$1) {
15243
+ try {
15244
+ const fileSize = fs.statSync(path$1).size;
15245
+ const { stdout } = await ffprobe(`-v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 "${path$1}"`);
15246
+ const duration = parseFloat(stdout.trim());
15247
+ if (duration > 0 && fileSize > 0) {
15248
+ const kbps = Math.round(fileSize * 8 / duration / 1e3);
15249
+ logger.debug(`[BiliDanmaku] 通过文件大小计算码率: ${kbps}kbps`);
15250
+ return kbps;
15251
+ }
15252
+ } catch (e) {
15253
+ logger.debug(`[BiliDanmaku] 通过文件大小计算码率失败: ${e}`);
15254
+ }
15255
+ try {
15256
+ const { stdout } = await ffprobe(`-v error -select_streams v:0 -show_entries stream=bit_rate -of default=noprint_wrappers=1:nokey=1 "${path$1}"`);
15257
+ const bitrate = parseInt(stdout.trim());
15258
+ if (bitrate > 0) return Math.round(bitrate / 1e3);
15259
+ } catch {}
15260
+ try {
15261
+ const { stdout } = await ffprobe(`-v error -show_entries format=bit_rate -of default=noprint_wrappers=1:nokey=1 "${path$1}"`);
15262
+ const bitrate = parseInt(stdout.trim());
15263
+ if (bitrate > 0) return Math.round(bitrate / 1e3);
15264
+ } catch {}
15265
+ logger.warn("[BiliDanmaku] 无法获取视频码率,将使用 CRF 模式");
15266
+ return 0;
15267
+ }
15268
+ function getEncoderParams$1(encoder, targetBitrate) {
15269
+ const threads = Math.max(1, Math.floor(os.cpus().length / 2));
15270
+ if (targetBitrate && targetBitrate > 0) {
15271
+ const bitrateK = `${targetBitrate}k`;
15272
+ const maxrate = `${Math.round(targetBitrate * 2)}k`;
15273
+ const bufsize = `${Math.round(targetBitrate * 4)}k`;
15274
+ if (encoder === "h264_nvenc") return `-c:v h264_nvenc -preset p4 -rc vbr -b:v ${bitrateK} -maxrate ${maxrate} -bufsize ${bufsize}`;
15275
+ if (encoder === "h264_qsv") return `-c:v h264_qsv -preset medium -b:v ${bitrateK} -maxrate ${maxrate} -bufsize ${bufsize}`;
15276
+ if (encoder === "h264_amf") return `-c:v h264_amf -quality balanced -rc vbr_peak -b:v ${bitrateK} -maxrate ${maxrate}`;
15277
+ if (encoder === "libx264") return `-c:v libx264 -preset medium -b:v ${bitrateK} -maxrate ${maxrate} -bufsize ${bufsize} -threads ${threads}`;
15278
+ if (encoder === "hevc_nvenc") return `-c:v hevc_nvenc -preset p4 -rc vbr -b:v ${bitrateK} -maxrate ${maxrate} -bufsize ${bufsize}`;
15279
+ if (encoder === "hevc_qsv") return `-c:v hevc_qsv -preset medium -b:v ${bitrateK} -maxrate ${maxrate} -bufsize ${bufsize}`;
15280
+ if (encoder === "hevc_amf") return `-c:v hevc_amf -quality balanced -rc vbr_peak -b:v ${bitrateK} -maxrate ${maxrate}`;
15281
+ if (encoder === "libx265") return `-c:v libx265 -preset medium -b:v ${bitrateK} -maxrate ${maxrate} -bufsize ${bufsize} -threads ${threads}`;
15282
+ if (encoder === "av1_nvenc") return `-c:v av1_nvenc -preset p4 -rc vbr -b:v ${bitrateK} -maxrate ${maxrate} -bufsize ${bufsize}`;
15283
+ if (encoder === "av1_qsv") return `-c:v av1_qsv -preset medium -b:v ${bitrateK} -maxrate ${maxrate} -bufsize ${bufsize}`;
15284
+ if (encoder === "av1_amf") return `-c:v av1_amf -quality balanced -rc vbr_peak -b:v ${bitrateK} -maxrate ${maxrate}`;
15285
+ if (encoder === "libsvtav1") return `-c:v libsvtav1 -preset 6 -b:v ${bitrateK} -maxrate ${maxrate} -bufsize ${bufsize} -threads ${threads}`;
15286
+ if (encoder === "libaom-av1") return `-c:v libaom-av1 -cpu-used 4 -b:v ${bitrateK} -maxrate ${maxrate} -bufsize ${bufsize} -threads ${threads}`;
15287
+ return `-c:v libx265 -preset medium -b:v ${bitrateK} -maxrate ${maxrate} -bufsize ${bufsize} -threads ${threads}`;
15288
+ }
15289
+ if (encoder === "h264_nvenc") return "-c:v h264_nvenc -preset p4 -rc vbr -cq 23";
15290
+ if (encoder === "h264_qsv") return "-c:v h264_qsv -preset medium -global_quality 23";
15291
+ if (encoder === "h264_amf") return "-c:v h264_amf -quality balanced -rc cqp -qp_i 23 -qp_p 23";
15292
+ if (encoder === "libx264") return `-c:v libx264 -crf 23 -preset medium -threads ${threads}`;
15293
+ if (encoder === "hevc_nvenc") return "-c:v hevc_nvenc -preset p4 -rc vbr -cq 28";
15294
+ if (encoder === "hevc_qsv") return "-c:v hevc_qsv -preset medium -global_quality 28";
15295
+ if (encoder === "hevc_amf") return "-c:v hevc_amf -quality balanced -rc cqp -qp_i 28 -qp_p 28";
15296
+ if (encoder === "libx265") return `-c:v libx265 -crf 28 -preset medium -threads ${threads}`;
15297
+ if (encoder === "av1_nvenc") return "-c:v av1_nvenc -preset p4 -rc vbr -cq 30";
15298
+ if (encoder === "av1_qsv") return "-c:v av1_qsv -preset medium -global_quality 30";
15299
+ if (encoder === "av1_amf") return "-c:v av1_amf -quality balanced -rc cqp -qp_i 30 -qp_p 30";
15300
+ if (encoder === "libsvtav1") return `-c:v libsvtav1 -crf 30 -preset 6 -threads ${threads}`;
15301
+ if (encoder === "libaom-av1") return `-c:v libaom-av1 -crf 30 -cpu-used 4 -threads ${threads}`;
15302
+ return `-c:v libx265 -crf 28 -preset medium -threads ${threads}`;
15303
+ }
15304
+ var toASSColor = (color) => {
15305
+ const r = color >> 16 & 255;
15306
+ const g = color >> 8 & 255;
15307
+ return `${(color & 255).toString(16).padStart(2, "0")}${g.toString(16).padStart(2, "0")}${r.toString(16).padStart(2, "0")}`.toUpperCase();
15308
+ };
15309
+ var toASSTime$1 = (ms) => {
15310
+ const s = ms / 1e3;
15311
+ const h = Math.floor(s / 3600);
15312
+ const m = Math.floor(s % 3600 / 60);
15313
+ const sec = Math.floor(s % 60);
15314
+ const cs = Math.floor(s % 1 * 100);
15315
+ return `${h}:${m.toString().padStart(2, "0")}:${sec.toString().padStart(2, "0")}.${cs.toString().padStart(2, "0")}`;
15316
+ };
15317
+ var estimateWidth$1 = (text, fontSize) => {
15318
+ let w = 0;
15319
+ for (const c of text) w += c.charCodeAt(0) > 127 ? fontSize : fontSize * .5;
15320
+ return w;
15321
+ };
15322
+ var escapeASS$1 = (text) => text.replace(/\\/g, "\\\\").replace(/\{/g, "\\{").replace(/\}/g, "\\}").replace(/\n/g, "\\N");
15323
+ var escapeWinPath$1 = (path$1) => path$1.replace(/\\/g, "/").replace(/:/g, "\\:");
15324
+ var isLandscape$1 = (w, h) => w > h;
15325
+ async function getBiliResolution(path$1) {
15326
+ try {
15327
+ const { stdout } = await ffprobe(`-v error -select_streams v:0 -show_entries stream=width,height -of csv=s=x:p=0 "${path$1}"`);
15328
+ const [w, h] = stdout.trim().split("x").map(Number);
15329
+ if (w && h) return {
15330
+ width: w,
15331
+ height: h
15332
+ };
15333
+ } catch {}
15334
+ try {
15335
+ const match = ((await ffmpeg(`-i "${path$1}" -f null -`, { timeout: 5e3 })).stderr || "").match(/(\d{3,4})x(\d{3,4})/);
15336
+ if (match) return {
15337
+ width: parseInt(match[1]),
15338
+ height: parseInt(match[2])
15339
+ };
15340
+ } catch {}
15341
+ return {
15342
+ width: 1920,
15343
+ height: 1080
15344
+ };
15345
+ }
15346
+ async function getBiliFrameRate(path$1) {
15347
+ try {
15348
+ 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}"`);
15349
+ const [num, den] = stdout.trim().split("/").map(Number);
15350
+ if (den > 0) return num / den;
15351
+ } catch {}
15352
+ try {
15353
+ const stderr = (await ffmpeg(`-i "${path$1}" -f null -`, { timeout: 5e3 })).stderr || "";
15354
+ const fpsMatch = stderr.match(/(\d+(?:\.\d+)?)\s*fps/);
15355
+ if (fpsMatch) return parseFloat(fpsMatch[1]);
15356
+ const fracMatch = stderr.match(/(\d+)\/(\d+)\s*fps/);
15357
+ if (fracMatch) return parseInt(fracMatch[1]) / parseInt(fracMatch[2]);
15358
+ } catch {}
15359
+ return 30;
15360
+ }
15361
+ var FONT_SIZE_MAP$1 = {
15362
+ small: {
15363
+ base: 25,
15364
+ trackH: 30
15365
+ },
15366
+ medium: {
15367
+ base: 32,
15368
+ trackH: 38
15369
+ },
15370
+ large: {
15371
+ base: 40,
15372
+ trackH: 46
15373
+ }
15374
+ };
15375
+ function generateBiliASS(danmakuList, width, height, options = {}) {
15376
+ const { scrollTime = 8, danmakuOpacity = 70, fontName = "Microsoft YaHei", danmakuArea = .5, danmakuFontSize = "medium" } = options;
15377
+ const fontScale = height / 1080;
15378
+ const sizeConfig = FONT_SIZE_MAP$1[danmakuFontSize];
15379
+ const fontSize = Math.round(sizeConfig.base * fontScale);
15380
+ const trackH = Math.round(sizeConfig.trackH * fontScale);
15381
+ const topMargin = Math.round(10 * fontScale);
15382
+ const bottomMargin = Math.round(10 * fontScale);
15383
+ const areaHeight = Math.floor(height * danmakuArea) - topMargin - bottomMargin;
15384
+ const trackCount = Math.max(1, Math.floor((areaHeight - fontSize) / trackH));
15385
+ const fixedTrackCount = trackCount;
15386
+ const minGap = Math.round(10 * fontScale);
15387
+ const alpha = Math.round((100 - Math.max(0, Math.min(100, danmakuOpacity))) * 2.55).toString(16).padStart(2, "0").toUpperCase();
15388
+ 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`;
15389
+ const scrollTracks = Array(trackCount).fill(null);
15390
+ const topTracks = Array(fixedTrackCount).fill(0);
15391
+ const bottomTracks = Array(fixedTrackCount).fill(0);
15392
+ const calcDistance = (last, startTime, duration, textWidth) => {
15393
+ const lastSpeed = (width + last.textWidth) / last.duration;
15394
+ const newSpeed = (width + textWidth) / duration;
15395
+ let dist = width - (width - lastSpeed * (startTime - last.startTime) + last.textWidth) - minGap;
15396
+ if (newSpeed > lastSpeed) {
15397
+ const lastRightXAtEnd = width - lastSpeed * (startTime + duration - last.startTime) + last.textWidth;
15398
+ dist = Math.min(dist, -textWidth - lastRightXAtEnd - minGap);
15399
+ }
15400
+ return dist;
15401
+ };
15402
+ const sorted = [...danmakuList].sort((a, b) => a.progress - b.progress);
15403
+ for (const dm of sorted) {
15404
+ if (dm.mode > 5 || !dm.content.trim()) continue;
15405
+ const startTime = dm.progress;
15406
+ const dmSizeRatio = (dm.fontsize || 25) / 25;
15407
+ const dmFontSize = Math.round(fontSize * dmSizeRatio);
15408
+ const textWidth = estimateWidth$1(dm.content, dmFontSize);
15409
+ const content = escapeASS$1(dm.content);
15410
+ const colorTag = dm.color !== 16777215 ? `{\\c&H${toASSColor(dm.color)}&}` : "";
15411
+ const sizeTag = dmFontSize !== fontSize ? `{\\fs${dmFontSize}}` : "";
15412
+ if (dm.mode === 4) {
15413
+ const endTime = startTime + 4e3;
15414
+ let idx = bottomTracks.findIndex((t) => t <= startTime);
15415
+ if (idx === -1) idx = Math.floor(Math.random() * bottomTracks.length);
15416
+ bottomTracks[idx] = endTime;
15417
+ const y = height - bottomMargin - idx * trackH;
15418
+ ass += `Dialogue: 0,${toASSTime$1(startTime)},${toASSTime$1(endTime)},Bottom,,0,0,0,,{\\an2}${colorTag}${sizeTag}{\\pos(${width / 2},${y})}${content}\n`;
15419
+ } else if (dm.mode === 5) {
15420
+ const endTime = startTime + 4e3;
15421
+ let idx = topTracks.findIndex((t) => t <= startTime);
15422
+ if (idx === -1) idx = Math.floor(Math.random() * topTracks.length);
15423
+ topTracks[idx] = endTime;
15424
+ const y = topMargin + idx * trackH + fontSize;
15425
+ ass += `Dialogue: 0,${toASSTime$1(startTime)},${toASSTime$1(endTime)},Top,,0,0,0,,{\\an8}${colorTag}${sizeTag}{\\pos(${width / 2},${y})}${content}\n`;
15426
+ } else {
15427
+ const duration = scrollTime * 1e3;
15428
+ const endTime = startTime + duration;
15429
+ for (let i = 0; i < scrollTracks.length; i++) {
15430
+ const t = scrollTracks[i];
15431
+ if (t && t.startTime + t.duration <= startTime) scrollTracks[i] = null;
15432
+ }
15433
+ let bestIdx = -1;
15434
+ let bestDist = -Infinity;
15435
+ for (let i = 0; i < scrollTracks.length; i++) {
15436
+ const t = scrollTracks[i];
15437
+ if (!t) {
15438
+ if (bestIdx === -1) bestIdx = i;
15439
+ continue;
15440
+ }
15441
+ const d = calcDistance(t, startTime, duration, textWidth);
15442
+ if (d >= 0) {
15443
+ if (bestDist < 0 || d < bestDist) {
15444
+ bestDist = d;
15445
+ bestIdx = i;
15446
+ }
15447
+ }
15448
+ }
15449
+ if (bestIdx === -1 || bestDist < 0 && scrollTracks[bestIdx] !== null) continue;
15450
+ scrollTracks[bestIdx] = {
15451
+ startTime,
15452
+ duration,
15453
+ textWidth
15454
+ };
15455
+ const y = (bestIdx + 1) * trackH;
15456
+ ass += `Dialogue: 0,${toASSTime$1(startTime)},${toASSTime$1(endTime)},Scroll,,0,0,0,,{\\an7}${colorTag}${sizeTag}{\\move(${width},${y},${-textWidth},${y})}${content}\n`;
15457
+ }
15661
15458
  }
15662
- let isChange = false;
15663
- if (typeof a === "object" && typeof b === "object") {
15664
- if (isChange) return true;
15665
- const keysA = Object.keys(a);
15666
- const keysB = Object.keys(b);
15667
- if (keysA.length !== keysB.length) return true;
15668
- for (const key of keysA) {
15669
- if (!keysB.includes(key)) {
15670
- isChange = true;
15671
- return true;
15672
- }
15673
- if (deepEqual(a[key], b[key])) {
15674
- isChange = true;
15675
- return true;
15676
- }
15459
+ return ass;
15460
+ }
15461
+ var MAX_OUTPUT_WIDTH$1 = 2160;
15462
+ function calcCanvas$1(origW, origH, verticalMode) {
15463
+ if (verticalMode === "off") return {
15464
+ width: origW,
15465
+ height: origH,
15466
+ offsetY: 0,
15467
+ isVertical: false
15468
+ };
15469
+ const ratio = origW / origH;
15470
+ const isWide = isLandscape$1(origW, origH);
15471
+ if (verticalMode === "force") {
15472
+ const targetRatio = 16 / 9;
15473
+ if (isWide) {
15474
+ const newW = Math.min(origH, MAX_OUTPUT_WIDTH$1);
15475
+ const newH = Math.round(newW * targetRatio);
15476
+ const scaledH = Math.round(newW / ratio);
15477
+ return {
15478
+ width: newW,
15479
+ height: newH,
15480
+ offsetY: Math.round((newH - scaledH) / 2),
15481
+ isVertical: true,
15482
+ scale: newW / origW
15483
+ };
15484
+ } else {
15485
+ const newW = Math.min(origW, MAX_OUTPUT_WIDTH$1);
15486
+ const scaleRatio = newW / origW;
15487
+ const scaledOrigH = Math.round(origH * scaleRatio);
15488
+ const newH = Math.round(newW * targetRatio);
15489
+ const offsetY = Math.round((newH - scaledOrigH) / 2);
15490
+ return {
15491
+ width: newW,
15492
+ height: newH,
15493
+ offsetY: Math.max(0, offsetY),
15494
+ isVertical: true,
15495
+ scale: scaleRatio
15496
+ };
15677
15497
  }
15678
15498
  }
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
- }
15723
- }
15724
- if (hasValidData && Object.keys(configObj).length > 0) result[key] = configObj;
15499
+ if (isWide && ratio >= 1.7) {
15500
+ const newW = Math.min(origH, MAX_OUTPUT_WIDTH$1);
15501
+ const scaleRatio = newW / origH;
15502
+ const newH = Math.round(origW * scaleRatio);
15503
+ const scaledH = Math.round(newW / ratio);
15504
+ return {
15505
+ width: newW,
15506
+ height: newH,
15507
+ offsetY: Math.round((newH - scaledH) / 2),
15508
+ isVertical: true,
15509
+ scale: newW / origW
15510
+ };
15725
15511
  }
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
- }))
15512
+ return {
15513
+ width: origW,
15514
+ height: origH,
15515
+ offsetY: 0,
15516
+ isVertical: false
15732
15517
  };
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];
15518
+ }
15519
+ function buildFilter$1(canvas, assPath) {
15520
+ const escaped = escapeWinPath$1(assPath);
15521
+ if (canvas.isVertical) {
15522
+ 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}'`;
15523
+ return `pad=${canvas.width}:${canvas.height}:0:${canvas.offsetY}:black,subtitles='${escaped}'`;
15742
15524
  }
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];
15525
+ return `subtitles='${escaped}'`;
15526
+ }
15527
+ async function burnBiliDanmaku(videoPath, danmakuList, outputPath, options = {}) {
15528
+ const { removeSource = false, verticalMode = "off", videoCodec = "h265" } = options;
15529
+ const resolution = await getBiliResolution(videoPath);
15530
+ const frameRate = await getBiliFrameRate(videoPath);
15531
+ const sourceBitrate = await getVideoBitrate$1(videoPath);
15532
+ const canvas = calcCanvas$1(resolution.width, resolution.height, verticalMode);
15533
+ if (canvas.isVertical) logger.debug(`[BiliDanmaku] 竖屏模式: ${resolution.width}x${resolution.height} -> ${canvas.width}x${canvas.height}`);
15534
+ const assContent = generateBiliASS(danmakuList, canvas.width, canvas.height, options);
15535
+ const assPath = videoPath.replace(/\.[^.]+$/, "_danmaku.ass");
15536
+ fs.writeFileSync(assPath, assContent, "utf-8");
15537
+ logger.debug(`[BiliDanmaku] 弹幕字幕已生成: ${assPath}`);
15538
+ 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}"`);
15539
+ Common.removeFile(assPath, true);
15540
+ if (result.status) {
15541
+ logger.mark(`[BiliDanmaku] 弹幕烧录成功: ${outputPath}`);
15542
+ if (removeSource) Common.removeFile(videoPath);
15543
+ } else logger.error("[BiliDanmaku] 弹幕烧录失败", result);
15544
+ return result.status;
15545
+ }
15546
+ async function mergeAndBurnBili(videoPath, audioPath, danmakuList, outputPath, options = {}) {
15547
+ const { removeSource = false, verticalMode = "off", videoCodec = "h265" } = options;
15548
+ if (!fs.existsSync(videoPath)) {
15549
+ logger.error(`[BiliDanmaku] 视频文件不存在: ${videoPath}`);
15550
+ return false;
15750
15551
  }
15751
- return path$1[path$1.length - 1] in current;
15752
- };
15552
+ if (!fs.existsSync(audioPath)) {
15553
+ logger.error(`[BiliDanmaku] 音频文件不存在: ${audioPath}`);
15554
+ return false;
15555
+ }
15556
+ const resolution = await getBiliResolution(videoPath);
15557
+ const frameRate = await getBiliFrameRate(videoPath);
15558
+ const sourceBitrate = await getVideoBitrate$1(videoPath);
15559
+ const canvas = calcCanvas$1(resolution.width, resolution.height, verticalMode);
15560
+ if (canvas.isVertical) logger.debug(`[BiliDanmaku] 竖屏模式: ${resolution.width}x${resolution.height} -> ${canvas.width}x${canvas.height}`);
15561
+ logger.debug(`[BiliDanmaku] 分辨率: ${canvas.width}x${canvas.height}, 帧率: ${frameRate}fps, 码率: ${sourceBitrate}kbps`);
15562
+ const assContent = generateBiliASS(danmakuList, canvas.width, canvas.height, options);
15563
+ const assPath = videoPath.replace(/\.[^.]+$/, "_danmaku.ass");
15564
+ fs.writeFileSync(assPath, assContent, "utf-8");
15565
+ logger.debug(`[BiliDanmaku] 弹幕字幕已生成: ${assPath},共 ${danmakuList.length} 条`);
15566
+ 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}"`);
15567
+ Common.removeFile(assPath, true);
15568
+ if (result.status) {
15569
+ logger.mark(`[BiliDanmaku] 视频合成+弹幕烧录成功: ${outputPath}`);
15570
+ if (removeSource) {
15571
+ Common.removeFile(videoPath);
15572
+ Common.removeFile(audioPath);
15573
+ }
15574
+ } else logger.error("[BiliDanmaku] 视频合成+弹幕烧录失败", result);
15575
+ return result.status;
15576
+ }
15753
15577
  await init_src();
15754
15578
  await init_date_fns();
15755
15579
  await init_locale();
15756
15580
  await init_utils$1();
15757
15581
  await init_amagiClient();
15758
15582
  await init_Config();
15759
- await init_danmaku$1();
15760
15583
  var img$1;
15761
15584
  var Bilibili = class extends Base {
15762
15585
  e;
@@ -15866,8 +15689,8 @@ var Bilibili = class extends Base {
15866
15689
  const imageUrl = await processImageUrl(v, infoData.data.data.title, index);
15867
15690
  messageElements.push(segment.image(imageUrl));
15868
15691
  }
15869
- const res = common.makeForward(messageElements, this.e.sender.userId, this.e.sender.nick);
15870
- this.e.bot.sendForwardMsg(this.e.contact, res, {
15692
+ 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);
15693
+ await this.e.bot.sendForwardMsg(this.e.contact, res, {
15871
15694
  source: "评论图片收集",
15872
15695
  summary: `查看${messageElements.length}张图片`,
15873
15696
  prompt: "B站评论解析结果",
@@ -16019,20 +15842,89 @@ var Bilibili = class extends Base {
16019
15842
  switch (dynamicInfo.data.data.item.type) {
16020
15843
  case DynamicType.DRAW: {
16021
15844
  const imgArray = [];
15845
+ const temp = [];
16022
15846
  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) {
15847
+ 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) {
15848
+ const livePhoto = await downloadFile(img$2.live_url, {
15849
+ title: `Bilibili_tmp_V_${Date.now()}_${index}.mp4`,
15850
+ headers: BASE_HEADERS
15851
+ });
15852
+ if (livePhoto.filepath) {
15853
+ const outputPath = Common.tempDri.video + `Bilibili_Live_${Date.now()}_${index}.mp4`;
15854
+ let success;
15855
+ const staticImg = await downloadFile(img$2.url, {
15856
+ title: `Bilibili_static_${Date.now()}_${index}.jpg`,
15857
+ headers: BASE_HEADERS,
15858
+ filepath: Common.tempDri.images + `Bilibili_static_${Date.now()}_${index}.jpg`
15859
+ });
15860
+ if (staticImg.filepath) temp.push({
15861
+ filepath: staticImg.filepath,
15862
+ totalBytes: 0
15863
+ });
15864
+ const loopCount = 3;
15865
+ if (!staticImg.filepath) {
15866
+ await Common.removeFile(livePhoto.filepath, true);
15867
+ continue;
15868
+ }
15869
+ success = (await loopVideoWithTransition({
15870
+ inputPath: livePhoto.filepath,
15871
+ outputPath,
15872
+ loopCount,
15873
+ staticImagePath: staticImg.filepath,
15874
+ transitionEnabled: loopCount > 1
15875
+ })).success;
15876
+ if (success) {
15877
+ const filePath = Common.tempDri.video + `tmp_${Date.now()}.mp4`;
15878
+ fs.renameSync(outputPath, filePath);
15879
+ logger.mark(`视频文件重命名完成: ${outputPath.split("/").pop()} -> ${filePath.split("/").pop()}`);
15880
+ temp.push({
15881
+ filepath: filePath,
15882
+ totalBytes: 0
15883
+ });
15884
+ const videoPath = Config.upload.videoSendMode === "base64" ? `base64://${fs.readFileSync(filePath).toString("base64")}` : `file://${filePath}`;
15885
+ imgArray.push(segment.video(videoPath));
15886
+ let hasPushedMotionPhotoCover = false;
15887
+ if (staticImg.filepath) {
15888
+ const motionPhotoCoverPath = Common.tempDri.images + `MVIMG_${format(/* @__PURE__ */ new Date(), "yyyyMMdd_HHmmss_SSS")}_${index}.jpg`;
15889
+ if (await buildGoogleMotionPhoto({
15890
+ imagePath: staticImg.filepath,
15891
+ videoPath: livePhoto.filepath,
15892
+ outputPath: motionPhotoCoverPath
15893
+ })) {
15894
+ temp.push({
15895
+ filepath: motionPhotoCoverPath,
15896
+ totalBytes: 0
15897
+ });
15898
+ const motionPhotoCover = Config.upload.imageSendMode === "base64" ? `base64://${fs.readFileSync(motionPhotoCoverPath).toString("base64")}` : `file://${motionPhotoCoverPath}`;
15899
+ imgArray.push(segment.image(motionPhotoCover));
15900
+ hasPushedMotionPhotoCover = true;
15901
+ }
15902
+ }
15903
+ if (!hasPushedMotionPhotoCover) {
15904
+ const imageUrl = await processImageUrl(img$2.url, title, index);
15905
+ imgArray.push(segment.image(imageUrl));
15906
+ }
15907
+ logger.mark("正在尝试删除缓存文件");
15908
+ await Common.removeFile(livePhoto.filepath, true);
15909
+ } else await Common.removeFile(livePhoto.filepath, true);
15910
+ }
15911
+ } else {
16024
15912
  const imageUrl = await processImageUrl(img$2.url, title, index);
16025
15913
  imgArray.push(segment.image(imageUrl));
16026
15914
  }
16027
15915
  if (imgArray.length === 1) this.e.reply(imgArray[0]);
16028
15916
  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
- });
15917
+ 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);
15918
+ try {
15919
+ await this.e.bot.sendForwardMsg(this.e.contact, forwardMsg, {
15920
+ source: "图片合集",
15921
+ summary: `查看${imgArray.length}张图片消息`,
15922
+ prompt: "B站图文动态解析结果",
15923
+ news: [{ text: "点击查看解析结果" }]
15924
+ });
15925
+ } finally {
15926
+ for (const item of temp) await Common.removeFile(item.filepath, true);
15927
+ }
16036
15928
  }
16037
15929
  const dynamicCARD$1 = JSON.parse(dynamicInfoCard.data.data.card.card);
16038
15930
  if ("topic" in dynamicInfo.data.data.item.modules.module_dynamic && dynamicInfo.data.data.item.modules.module_dynamic.topic !== null) {
@@ -16287,8 +16179,8 @@ var Bilibili = class extends Base {
16287
16179
  }
16288
16180
  if (messageElements.length === 1) this.e.reply(messageElements[0]);
16289
16181
  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, {
16182
+ 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);
16183
+ await this.e.bot.sendForwardMsg(this.e.contact, forwardMsg, {
16292
16184
  source: "图片合集",
16293
16185
  summary: `查看${messageElements.length}张图片消息`,
16294
16186
  prompt: "B站专栏动态解析结果",
@@ -16337,8 +16229,8 @@ var Bilibili = class extends Base {
16337
16229
  const imageUrl = await processImageUrl(v, title, index);
16338
16230
  messageElements.push(segment.image(imageUrl));
16339
16231
  }
16340
- const res = common.makeForward(messageElements, this.e.sender.userId, this.e.sender.nick);
16341
- this.e.bot.sendForwardMsg(this.e.contact, res, {
16232
+ 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);
16233
+ await this.e.bot.sendForwardMsg(this.e.contact, res, {
16342
16234
  source: "评论图片收集",
16343
16235
  summary: `查看${messageElements.length}张图片`,
16344
16236
  prompt: "B站评论解析结果",
@@ -17780,20 +17672,93 @@ var Bilibilipush = class extends Base {
17780
17672
  break;
17781
17673
  case "DYNAMIC_TYPE_DRAW": {
17782
17674
  const imgArray = [];
17675
+ const temp = [];
17783
17676
  const title = data$1[dynamicId].Dynamic_Data.modules.module_dynamic.major?.opus?.title || "bilibili_dynamic";
17784
17677
  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
17678
  if (images.length === 0) break;
17786
17679
  for (const [index, img2] of images.entries()) {
17787
- const imageUrl = await processImageUrl(img2.src ?? img2.url, title, index);
17788
- imgArray.push(segment.image(imageUrl));
17680
+ const imageSrc = img2.src ?? img2.url;
17681
+ if (img2.live_url && imageSrc) {
17682
+ const livePhoto = await downloadFile(img2.live_url, {
17683
+ title: `Bilibili_tmp_V_${Date.now()}_${index}.mp4`,
17684
+ headers: bilibiliBaseHeaders
17685
+ });
17686
+ if (livePhoto.filepath) {
17687
+ const outputPath = Common.tempDri.video + `Bilibili_Live_${Date.now()}_${index}.mp4`;
17688
+ const staticImg = await downloadFile(imageSrc, {
17689
+ title: `Bilibili_static_${Date.now()}_${index}.jpg`,
17690
+ headers: bilibiliBaseHeaders,
17691
+ filepath: Common.tempDri.images + `Bilibili_static_${Date.now()}_${index}.jpg`
17692
+ });
17693
+ if (staticImg.filepath) temp.push({
17694
+ filepath: staticImg.filepath,
17695
+ totalBytes: 0
17696
+ });
17697
+ const loopCount = 3;
17698
+ if (!staticImg.filepath) {
17699
+ await Common.removeFile(livePhoto.filepath, true);
17700
+ continue;
17701
+ }
17702
+ if ((await loopVideoWithTransition({
17703
+ inputPath: livePhoto.filepath,
17704
+ outputPath,
17705
+ loopCount,
17706
+ staticImagePath: staticImg.filepath,
17707
+ transitionEnabled: loopCount > 1
17708
+ })).success) {
17709
+ const filePath = Common.tempDri.video + `tmp_${Date.now()}.mp4`;
17710
+ fs.renameSync(outputPath, filePath);
17711
+ logger.mark(`视频文件重命名完成: ${outputPath.split("/").pop()} -> ${filePath.split("/").pop()}`);
17712
+ temp.push({
17713
+ filepath: filePath,
17714
+ totalBytes: 0
17715
+ });
17716
+ const videoPath = Config.upload.videoSendMode === "base64" ? `base64://${fs.readFileSync(filePath).toString("base64")}` : `file://${filePath}`;
17717
+ imgArray.push(segment.video(videoPath));
17718
+ let hasPushedMotionPhotoCover = false;
17719
+ if (staticImg.filepath) {
17720
+ const motionPhotoCoverPath = Common.tempDri.images + `MVIMG_${Date.now()}_${index}.jpg`;
17721
+ if (await buildGoogleMotionPhoto({
17722
+ imagePath: staticImg.filepath,
17723
+ videoPath: livePhoto.filepath,
17724
+ outputPath: motionPhotoCoverPath
17725
+ })) {
17726
+ temp.push({
17727
+ filepath: motionPhotoCoverPath,
17728
+ totalBytes: 0
17729
+ });
17730
+ const motionPhotoCover = Config.upload.imageSendMode === "base64" ? `base64://${fs.readFileSync(motionPhotoCoverPath).toString("base64")}` : `file://${motionPhotoCoverPath}`;
17731
+ imgArray.push(segment.image(motionPhotoCover));
17732
+ hasPushedMotionPhotoCover = true;
17733
+ }
17734
+ }
17735
+ if (!hasPushedMotionPhotoCover) {
17736
+ const imageUrl = await processImageUrl(imageSrc, title, index);
17737
+ imgArray.push(segment.image(imageUrl));
17738
+ }
17739
+ logger.mark("正在尝试删除缓存文件");
17740
+ await Common.removeFile(livePhoto.filepath, true);
17741
+ continue;
17742
+ }
17743
+ await Common.removeFile(livePhoto.filepath, true);
17744
+ }
17745
+ }
17746
+ if (imageSrc) {
17747
+ const imageUrl = await processImageUrl(imageSrc, title, index);
17748
+ imgArray.push(segment.image(imageUrl));
17749
+ }
17789
17750
  }
17790
17751
  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
- });
17752
+ try {
17753
+ await bot.sendForwardMsg(Contact, forwardMsg, {
17754
+ source: "图片合集",
17755
+ summary: `查看${imgArray.length}张图片消息`,
17756
+ prompt: "B站图文动态解析结果",
17757
+ news: [{ text: "点击查看解析结果" }]
17758
+ });
17759
+ } finally {
17760
+ for (const item of temp) await Common.removeFile(item.filepath, true);
17761
+ }
17797
17762
  break;
17798
17763
  }
17799
17764
  case "DYNAMIC_TYPE_ARTICLE": {
@@ -17811,7 +17776,7 @@ var Bilibilipush = class extends Base {
17811
17776
  if (messageElements.length === 1) bot.sendMsg(Contact, messageElements);
17812
17777
  if (messageElements.length > 1) {
17813
17778
  const forwardMsg = common.makeForward(messageElements, botId, bot.account.name);
17814
- bot.sendForwardMsg(Contact, forwardMsg, {
17779
+ await bot.sendForwardMsg(Contact, forwardMsg, {
17815
17780
  source: "图片合集",
17816
17781
  summary: `查看${messageElements.length}张图片消息`,
17817
17782
  prompt: "B站专栏动态解析结果",
@@ -18036,7 +18001,6 @@ var skipDynamic$1 = async (PushItem) => {
18036
18001
  logger.debug(`检查动态是否需要过滤:https://t.bilibili.com/${PushItem.Dynamic_Data.id_str}`);
18037
18002
  return await bilibiliDBInstance.shouldFilter(PushItem, tags);
18038
18003
  };
18039
- init_danmaku$1();
18040
18004
  init_riskControl();
18041
18005
  await init_date_fns();
18042
18006
  await init_locale();
@@ -18155,74 +18119,381 @@ const douyinComments = async (data$1, emojidata) => {
18155
18119
  replyImageList = [processedReplyImage];
18156
18120
  imageUrls.push(processedReplyImage.startsWith("data:image/jpeg;base64,") ? `base64://${processedReplyImage.replace("data:image/jpeg;base64,", "")}` : processedReplyImage);
18157
18121
  }
18158
- } else if (replyStickerUrl) {
18159
- replyImageList = [replyStickerUrl];
18160
- imageUrls.push(replyStickerUrl);
18122
+ } else if (replyStickerUrl) {
18123
+ replyImageList = [replyStickerUrl];
18124
+ imageUrls.push(replyStickerUrl);
18125
+ }
18126
+ replyCommentsList.push({
18127
+ create_time: getRelativeTimeFromTimestamp$2(replyItem.create_time),
18128
+ nickname: replyItem.user.nickname,
18129
+ userimageurl: replyItem.user.avatar_thumb.url_list[0],
18130
+ text: processCommentEmojis$1(processedReplyText, emojidata),
18131
+ digg_count: replyItem.digg_count > 1e4 ? (replyItem.digg_count / 1e4).toFixed(1) + "w" : replyItem.digg_count,
18132
+ ip_label: replyItem.ip_label,
18133
+ text_extra: replyItem.text_extra,
18134
+ label_text: replyItem.label_text,
18135
+ image_list: replyImageList,
18136
+ cid: replyItem.cid,
18137
+ reply_to_reply_id: replyItem.reply_to_reply_id,
18138
+ reply_to_username: replyItem.reply_to_username
18139
+ });
18140
+ }
18141
+ const commentObj = {
18142
+ id: id++,
18143
+ replyComment: replyCommentsList.length > 0 ? replyCommentsList : void 0,
18144
+ cid,
18145
+ aweme_id,
18146
+ nickname,
18147
+ userimageurl,
18148
+ text,
18149
+ digg_count,
18150
+ ip_label: ip,
18151
+ create_time: relativeTime,
18152
+ commentimage: processedImageUrl ?? void 0,
18153
+ label_type,
18154
+ sticker: sticker ?? void 0,
18155
+ status_label: status_label ?? void 0,
18156
+ is_At_user_id: userintextlongid,
18157
+ search_text,
18158
+ is_author_digged: comment.is_author_digged ?? false
18159
+ };
18160
+ jsonArray.push(commentObj);
18161
+ }
18162
+ jsonArray.sort((a, b) => {
18163
+ 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;
18164
+ 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;
18165
+ });
18166
+ const indexLabelTypeOne = jsonArray.findIndex((comment) => comment.label_type === 1);
18167
+ if (indexLabelTypeOne !== -1) {
18168
+ const commentTypeOne = jsonArray.splice(indexLabelTypeOne, 1)[0];
18169
+ jsonArray.unshift(commentTypeOne);
18170
+ }
18171
+ return {
18172
+ CommentsData: jsonArray,
18173
+ image_url: imageUrls
18174
+ };
18175
+ };
18176
+ var getRelativeTimeFromTimestamp$2 = (timestamp) => {
18177
+ const commentDate = fromUnixTime(timestamp);
18178
+ const diffSeconds = differenceInSeconds(/* @__PURE__ */ new Date(), commentDate);
18179
+ if (diffSeconds < 30) return "刚刚";
18180
+ if (diffSeconds < 7776e3) return formatDistanceToNow(commentDate, {
18181
+ locale: zhCN,
18182
+ addSuffix: true
18183
+ });
18184
+ return format(commentDate, "yyyy-MM-dd");
18185
+ };
18186
+ await init_utils$1();
18187
+ var ENCODER_PRIORITY = {
18188
+ h264: [
18189
+ "h264_nvenc",
18190
+ "h264_qsv",
18191
+ "h264_amf",
18192
+ "libx264"
18193
+ ],
18194
+ h265: [
18195
+ "hevc_nvenc",
18196
+ "hevc_qsv",
18197
+ "hevc_amf",
18198
+ "libx265"
18199
+ ],
18200
+ av1: [
18201
+ "av1_nvenc",
18202
+ "av1_qsv",
18203
+ "av1_amf",
18204
+ "libsvtav1",
18205
+ "libaom-av1"
18206
+ ]
18207
+ };
18208
+ var SOFTWARE_FALLBACK = {
18209
+ h264: "libx264",
18210
+ h265: "libx265",
18211
+ av1: "libsvtav1"
18212
+ };
18213
+ var cachedEncoders = {};
18214
+ async function detectEncoder(codec) {
18215
+ if (cachedEncoders[codec]) return cachedEncoders[codec];
18216
+ logger.debug(`[DouyinDanmaku] 开始检测 ${codec.toUpperCase()} 编码器...`);
18217
+ for (const encoder of ENCODER_PRIORITY[codec]) try {
18218
+ if ((await ffmpeg(`-f lavfi -i color=c=black:s=320x240:d=0.1 -c:v ${encoder} -f null -`)).status) {
18219
+ cachedEncoders[codec] = encoder;
18220
+ logger.info(`[DouyinDanmaku] 使用 ${codec.toUpperCase()} 编码器: ${encoder}`);
18221
+ return encoder;
18222
+ }
18223
+ } catch {}
18224
+ const fallback = SOFTWARE_FALLBACK[codec];
18225
+ cachedEncoders[codec] = fallback;
18226
+ logger.info(`[DouyinDanmaku] 回退到软件编码器: ${fallback}`);
18227
+ return fallback;
18228
+ }
18229
+ async function getVideoBitrate(path$1) {
18230
+ try {
18231
+ const fileSize = fs.statSync(path$1).size;
18232
+ const { stdout } = await ffprobe(`-v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 "${path$1}"`);
18233
+ const duration = parseFloat(stdout.trim());
18234
+ if (duration > 0 && fileSize > 0) return Math.round(fileSize * 8 / duration / 1e3);
18235
+ } catch {}
18236
+ try {
18237
+ const { stdout } = await ffprobe(`-v error -select_streams v:0 -show_entries stream=bit_rate -of default=noprint_wrappers=1:nokey=1 "${path$1}"`);
18238
+ const bitrate = parseInt(stdout.trim());
18239
+ if (bitrate > 0) return Math.round(bitrate / 1e3);
18240
+ } catch {}
18241
+ return 0;
18242
+ }
18243
+ function getEncoderParams(encoder, targetBitrate) {
18244
+ const threads = Math.max(1, Math.floor(os.cpus().length / 2));
18245
+ if (targetBitrate && targetBitrate > 0) {
18246
+ const adjustedBitrate = Math.round(targetBitrate * 1.4);
18247
+ const bitrateK = `${adjustedBitrate}k`;
18248
+ const maxrate = `${Math.round(adjustedBitrate * 2.5)}k`;
18249
+ const bufsize = `${Math.round(adjustedBitrate * 4)}k`;
18250
+ if (encoder === "h264_nvenc") return `-c:v h264_nvenc -preset p4 -rc vbr -b:v ${bitrateK} -maxrate ${maxrate} -bufsize ${bufsize}`;
18251
+ if (encoder === "h264_qsv") return `-c:v h264_qsv -preset medium -b:v ${bitrateK} -maxrate ${maxrate} -bufsize ${bufsize}`;
18252
+ if (encoder === "h264_amf") return `-c:v h264_amf -quality balanced -rc vbr_peak -b:v ${bitrateK} -maxrate ${maxrate}`;
18253
+ if (encoder === "libx264") return `-c:v libx264 -preset medium -b:v ${bitrateK} -maxrate ${maxrate} -bufsize ${bufsize} -threads ${threads}`;
18254
+ if (encoder === "hevc_nvenc") return `-c:v hevc_nvenc -preset p4 -rc vbr -b:v ${bitrateK} -maxrate ${maxrate} -bufsize ${bufsize}`;
18255
+ if (encoder === "hevc_qsv") return `-c:v hevc_qsv -preset medium -b:v ${bitrateK} -maxrate ${maxrate} -bufsize ${bufsize}`;
18256
+ if (encoder === "hevc_amf") return `-c:v hevc_amf -quality balanced -rc vbr_peak -b:v ${bitrateK} -maxrate ${maxrate}`;
18257
+ if (encoder === "libx265") return `-c:v libx265 -preset medium -b:v ${bitrateK} -maxrate ${maxrate} -bufsize ${bufsize} -threads ${threads}`;
18258
+ if (encoder === "av1_nvenc") return `-c:v av1_nvenc -preset p4 -rc vbr -b:v ${bitrateK} -maxrate ${maxrate} -bufsize ${bufsize}`;
18259
+ if (encoder === "av1_qsv") return `-c:v av1_qsv -preset medium -b:v ${bitrateK} -maxrate ${maxrate} -bufsize ${bufsize}`;
18260
+ if (encoder === "av1_amf") return `-c:v av1_amf -quality balanced -rc vbr_peak -b:v ${bitrateK} -maxrate ${maxrate}`;
18261
+ if (encoder === "libsvtav1") return `-c:v libsvtav1 -preset 6 -b:v ${bitrateK} -maxrate ${maxrate} -bufsize ${bufsize} -threads ${threads}`;
18262
+ if (encoder === "libaom-av1") return `-c:v libaom-av1 -cpu-used 4 -b:v ${bitrateK} -maxrate ${maxrate} -bufsize ${bufsize} -threads ${threads}`;
18263
+ return `-c:v libx265 -preset medium -b:v ${bitrateK} -maxrate ${maxrate} -bufsize ${bufsize} -threads ${threads}`;
18264
+ }
18265
+ if (encoder === "h264_nvenc") return "-c:v h264_nvenc -preset p4 -rc vbr -cq 23";
18266
+ if (encoder === "h264_qsv") return "-c:v h264_qsv -preset medium -global_quality 23";
18267
+ if (encoder === "h264_amf") return "-c:v h264_amf -quality balanced -rc cqp -qp_i 23 -qp_p 23";
18268
+ if (encoder === "libx264") return `-c:v libx264 -crf 23 -preset medium -threads ${threads}`;
18269
+ if (encoder === "hevc_nvenc") return "-c:v hevc_nvenc -preset p4 -rc vbr -cq 28";
18270
+ if (encoder === "hevc_qsv") return "-c:v hevc_qsv -preset medium -global_quality 28";
18271
+ if (encoder === "hevc_amf") return "-c:v hevc_amf -quality balanced -rc cqp -qp_i 28 -qp_p 28";
18272
+ if (encoder === "libx265") return `-c:v libx265 -crf 28 -preset medium -threads ${threads}`;
18273
+ if (encoder === "av1_nvenc") return "-c:v av1_nvenc -preset p4 -rc vbr -cq 30";
18274
+ if (encoder === "av1_qsv") return "-c:v av1_qsv -preset medium -global_quality 30";
18275
+ if (encoder === "av1_amf") return "-c:v av1_amf -quality balanced -rc cqp -qp_i 30 -qp_p 30";
18276
+ if (encoder === "libsvtav1") return `-c:v libsvtav1 -crf 30 -preset 6 -threads ${threads}`;
18277
+ if (encoder === "libaom-av1") return `-c:v libaom-av1 -crf 30 -cpu-used 4 -threads ${threads}`;
18278
+ return `-c:v libx265 -crf 28 -preset medium -threads ${threads}`;
18279
+ }
18280
+ var toASSTime = (ms) => {
18281
+ const s = ms / 1e3;
18282
+ const h = Math.floor(s / 3600);
18283
+ const m = Math.floor(s % 3600 / 60);
18284
+ const sec = Math.floor(s % 60);
18285
+ const cs = Math.floor(s % 1 * 100);
18286
+ return `${h}:${m.toString().padStart(2, "0")}:${sec.toString().padStart(2, "0")}.${cs.toString().padStart(2, "0")}`;
18287
+ };
18288
+ var estimateWidth = (text, fontSize) => {
18289
+ let w = 0;
18290
+ for (const c of text) w += c.charCodeAt(0) > 127 ? fontSize : fontSize * .5;
18291
+ return w;
18292
+ };
18293
+ var escapeASS = (text) => text.replace(/\\/g, "\\\\").replace(/\{/g, "\\{").replace(/\}/g, "\\}").replace(/\n/g, "\\N");
18294
+ var escapeWinPath = (path$1) => path$1.replace(/\\/g, "/").replace(/:/g, "\\:");
18295
+ var isLandscape = (w, h) => w > h;
18296
+ async function getDouyinResolution(path$1) {
18297
+ try {
18298
+ const { stdout } = await ffprobe(`-v error -select_streams v:0 -show_entries stream=width,height -of csv=s=x:p=0 "${path$1}"`);
18299
+ const [w, h] = stdout.trim().split("x").map(Number);
18300
+ if (w && h) return {
18301
+ width: w,
18302
+ height: h
18303
+ };
18304
+ } catch {}
18305
+ try {
18306
+ const match = ((await ffmpeg(`-i "${path$1}" -f null -`, { timeout: 5e3 })).stderr || "").match(/(\d{3,4})x(\d{3,4})/);
18307
+ if (match) return {
18308
+ width: parseInt(match[1]),
18309
+ height: parseInt(match[2])
18310
+ };
18311
+ } catch {}
18312
+ return {
18313
+ width: 1080,
18314
+ height: 1920
18315
+ };
18316
+ }
18317
+ async function getDouyinFrameRate(path$1) {
18318
+ try {
18319
+ 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}"`);
18320
+ const [num, den] = stdout.trim().split("/").map(Number);
18321
+ if (den > 0) return num / den;
18322
+ } catch {}
18323
+ try {
18324
+ const fpsMatch = ((await ffmpeg(`-i "${path$1}" -f null -`, { timeout: 5e3 })).stderr || "").match(/(\d+(?:\.\d+)?)\s*fps/);
18325
+ if (fpsMatch) return parseFloat(fpsMatch[1]);
18326
+ } catch {}
18327
+ return 30;
18328
+ }
18329
+ var FONT_SIZE_MAP = {
18330
+ small: {
18331
+ base: 25,
18332
+ trackH: 30
18333
+ },
18334
+ medium: {
18335
+ base: 32,
18336
+ trackH: 38
18337
+ },
18338
+ large: {
18339
+ base: 40,
18340
+ trackH: 46
18341
+ }
18342
+ };
18343
+ function generateDouyinASS(danmakuList, width, height, options = {}) {
18344
+ const { scrollTime = 8, danmakuOpacity = 70, fontName = "Microsoft YaHei", danmakuArea = .5, danmakuFontSize = "medium" } = options;
18345
+ const fontScale = height / 1080;
18346
+ const sizeConfig = FONT_SIZE_MAP[danmakuFontSize];
18347
+ const fontSize = Math.round(sizeConfig.base * fontScale);
18348
+ const trackH = Math.round(sizeConfig.trackH * fontScale);
18349
+ const topMargin = Math.round(5 * fontScale);
18350
+ const areaHeight = Math.floor(height * danmakuArea) - topMargin;
18351
+ const trackCount = Math.max(1, Math.floor((areaHeight - fontSize) / trackH));
18352
+ const minGap = Math.round(15 * fontScale);
18353
+ const alpha = Math.round((100 - Math.max(0, Math.min(100, danmakuOpacity))) * 2.55).toString(16).padStart(2, "0").toUpperCase();
18354
+ 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`;
18355
+ const scrollTracks = Array(trackCount).fill(null);
18356
+ const calcDistance = (last, startTime, duration, textWidth) => {
18357
+ const lastSpeed = (width + last.textWidth) / last.duration;
18358
+ const newSpeed = (width + textWidth) / duration;
18359
+ let dist = width - (width - lastSpeed * (startTime - last.startTime) + last.textWidth) - minGap;
18360
+ if (newSpeed > lastSpeed) {
18361
+ const lastRightXAtEnd = width - lastSpeed * (startTime + duration - last.startTime) + last.textWidth;
18362
+ dist = Math.min(dist, -textWidth - lastRightXAtEnd - minGap);
18363
+ }
18364
+ return dist;
18365
+ };
18366
+ const sorted = [...danmakuList.filter((dm) => dm.text && dm.text.trim())].sort((a, b) => a.offset_time - b.offset_time);
18367
+ for (const dm of sorted) {
18368
+ const startTime = dm.offset_time;
18369
+ const textWidth = estimateWidth(dm.text, fontSize);
18370
+ const content = escapeASS(dm.text);
18371
+ const duration = scrollTime * 1e3;
18372
+ const endTime = startTime + duration;
18373
+ for (let i = 0; i < scrollTracks.length; i++) {
18374
+ const t = scrollTracks[i];
18375
+ if (t && t.startTime + t.duration <= startTime) scrollTracks[i] = null;
18376
+ }
18377
+ let bestIdx = -1;
18378
+ let bestDist = -Infinity;
18379
+ for (let i = 0; i < scrollTracks.length; i++) {
18380
+ const t = scrollTracks[i];
18381
+ if (!t) {
18382
+ if (bestIdx === -1) bestIdx = i;
18383
+ continue;
18384
+ }
18385
+ const d = calcDistance(t, startTime, duration, textWidth);
18386
+ if (d >= 0) {
18387
+ if (bestDist < 0 || d < bestDist) {
18388
+ bestDist = d;
18389
+ bestIdx = i;
18390
+ }
18161
18391
  }
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
18392
  }
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
18393
+ if (bestIdx === -1 || bestDist < 0 && scrollTracks[bestIdx] !== null) continue;
18394
+ scrollTracks[bestIdx] = {
18395
+ startTime,
18396
+ duration,
18397
+ textWidth
18195
18398
  };
18196
- jsonArray.push(commentObj);
18399
+ const y = topMargin + bestIdx * trackH + fontSize;
18400
+ ass += `Dialogue: 0,${toASSTime(startTime)},${toASSTime(endTime)},Scroll,,0,0,0,,{\\an7}{\\move(${width},${y},${-textWidth},${y})}${content}\n`;
18197
18401
  }
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);
18402
+ return ass;
18403
+ }
18404
+ var MAX_OUTPUT_WIDTH = 2160;
18405
+ function calcCanvas(origW, origH, verticalMode) {
18406
+ if (verticalMode === "off") return {
18407
+ width: origW,
18408
+ height: origH,
18409
+ offsetY: 0,
18410
+ isVertical: false
18411
+ };
18412
+ const ratio = origW / origH;
18413
+ const isWide = isLandscape(origW, origH);
18414
+ if (verticalMode === "force") {
18415
+ const targetRatio = 16 / 9;
18416
+ if (isWide) {
18417
+ const newW = Math.min(origH, MAX_OUTPUT_WIDTH);
18418
+ const newH = Math.round(newW * targetRatio);
18419
+ const scaledH = Math.round(newW / ratio);
18420
+ return {
18421
+ width: newW,
18422
+ height: newH,
18423
+ offsetY: Math.round((newH - scaledH) / 2),
18424
+ isVertical: true,
18425
+ scale: newW / origW
18426
+ };
18427
+ } else {
18428
+ const newW = Math.min(origW, MAX_OUTPUT_WIDTH);
18429
+ const scaleRatio = newW / origW;
18430
+ const scaledOrigH = Math.round(origH * scaleRatio);
18431
+ const newH = Math.round(newW * targetRatio);
18432
+ const offsetY = Math.round((newH - scaledOrigH) / 2);
18433
+ return {
18434
+ width: newW,
18435
+ height: newH,
18436
+ offsetY: Math.max(0, offsetY),
18437
+ isVertical: true,
18438
+ scale: scaleRatio
18439
+ };
18440
+ }
18441
+ }
18442
+ if (isWide && ratio >= 1.7) {
18443
+ const newW = Math.min(origH, MAX_OUTPUT_WIDTH);
18444
+ const scaleRatio = newW / origH;
18445
+ const newH = Math.round(origW * scaleRatio);
18446
+ const scaledH = Math.round(newW / ratio);
18447
+ return {
18448
+ width: newW,
18449
+ height: newH,
18450
+ offsetY: Math.round((newH - scaledH) / 2),
18451
+ isVertical: true,
18452
+ scale: newW / origW
18453
+ };
18206
18454
  }
18207
18455
  return {
18208
- CommentsData: jsonArray,
18209
- image_url: imageUrls
18456
+ width: origW,
18457
+ height: origH,
18458
+ offsetY: 0,
18459
+ isVertical: false
18210
18460
  };
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
- };
18461
+ }
18462
+ function buildFilter(canvas, assPath) {
18463
+ const escaped = escapeWinPath(assPath);
18464
+ if (canvas.isVertical) {
18465
+ 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}'`;
18466
+ return `pad=${canvas.width}:${canvas.height}:0:${canvas.offsetY}:black,subtitles='${escaped}'`;
18467
+ }
18468
+ return `subtitles='${escaped}'`;
18469
+ }
18470
+ async function burnDouyinDanmaku(videoPath, danmakuList, outputPath, options = {}) {
18471
+ const { removeSource = false, verticalMode = "off", videoCodec = "h265" } = options;
18472
+ if (!fs.existsSync(videoPath)) {
18473
+ logger.error(`[DouyinDanmaku] 视频文件不存在: ${videoPath}`);
18474
+ return false;
18475
+ }
18476
+ const resolution = await getDouyinResolution(videoPath);
18477
+ const frameRate = await getDouyinFrameRate(videoPath);
18478
+ const sourceBitrate = await getVideoBitrate(videoPath);
18479
+ const canvas = calcCanvas(resolution.width, resolution.height, verticalMode);
18480
+ if (canvas.isVertical) logger.debug(`[DouyinDanmaku] 竖屏模式: ${resolution.width}x${resolution.height} -> ${canvas.width}x${canvas.height}`);
18481
+ logger.debug(`[DouyinDanmaku] 分辨率: ${canvas.width}x${canvas.height}, 帧率: ${frameRate}fps, 码率: ${sourceBitrate}kbps`);
18482
+ const assContent = generateDouyinASS(danmakuList, canvas.width, canvas.height, options);
18483
+ const assPath = videoPath.replace(/\.[^.]+$/, "_danmaku.ass");
18484
+ fs.writeFileSync(assPath, assContent, "utf-8");
18485
+ logger.debug(`[DouyinDanmaku] 弹幕字幕已生成: ${assPath},共 ${danmakuList.length} 条`);
18486
+ const result = await ffmpeg(`-y -i "${videoPath}" -vf "${buildFilter(canvas, assPath)}" -r ${frameRate} ${getEncoderParams(await detectEncoder(videoCodec), sourceBitrate)} -c:a copy "${outputPath}"`);
18487
+ Common.removeFile(assPath, true);
18488
+ if (result.status) {
18489
+ logger.mark(`[DouyinDanmaku] 弹幕烧录成功: ${outputPath}`);
18490
+ if (removeSource) Common.removeFile(videoPath);
18491
+ } else logger.error("[DouyinDanmaku] 弹幕烧录失败", result);
18492
+ return result.status;
18493
+ }
18222
18494
  await init_date_fns();
18223
18495
  await init_utils$1();
18224
18496
  await init_Config();
18225
- await init_danmaku();
18226
18497
  var mp4size = "";
18227
18498
  var img;
18228
18499
  var DouYin = class extends Base {
@@ -18313,7 +18584,6 @@ var DouYin = class extends Base {
18313
18584
  headers: this.headers
18314
18585
  });
18315
18586
  temp.push(liveimgbgm);
18316
- if (mergeMode === "continuous") bgmContext = await createLiveImageContext(liveimgbgm.filepath);
18317
18587
  }
18318
18588
  for (const [index, imageItem] of images.entries()) {
18319
18589
  imagenum++;
@@ -18337,45 +18607,73 @@ var DouYin = class extends Base {
18337
18607
  });
18338
18608
  if (liveimg.filepath) {
18339
18609
  const outputPath = Common.tempDri.video + `Douyin_Result_${Date.now()}.mp4`;
18340
- let success;
18341
18610
  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;
18611
+ let staticImgPath = "";
18612
+ if (imageItem.url_list?.[0]) {
18613
+ const staticImg = await downloadFile(imageItem.url_list[0], {
18614
+ title: `Douyin_static_${Date.now()}_${index}.jpg`,
18615
+ headers: this.headers,
18616
+ filepath: Common.tempDri.images + `Douyin_static_${Date.now()}_${index}.jpg`
18617
+ });
18618
+ temp.push({
18619
+ filepath: staticImg.filepath,
18620
+ totalBytes: 0
18621
+ });
18622
+ staticImgPath = staticImg.filepath ?? "";
18346
18623
  }
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,
18624
+ const transitionEnabled = loopCount > 1 && Boolean(staticImgPath);
18625
+ const safeStaticPath = staticImgPath || liveimg.filepath;
18626
+ const result = await loopVideoWithTransition({
18627
+ inputPath: liveimg.filepath,
18357
18628
  outputPath,
18358
- loopCount
18359
- }, liveimgbgm.filepath);
18629
+ loopCount,
18630
+ staticImagePath: safeStaticPath,
18631
+ transitionEnabled,
18632
+ bgmPath: liveimgbgm?.filepath,
18633
+ mergeMode,
18634
+ context: bgmContext ?? void 0
18635
+ });
18636
+ const success = result.success;
18637
+ if (mergeMode === "continuous" && result.context) bgmContext = result.context;
18360
18638
  if (success) {
18361
18639
  const filePath = Common.tempDri.video + `tmp_${Date.now()}.mp4`;
18362
18640
  fs.renameSync(outputPath, filePath);
18363
18641
  logger.mark(`视频文件重命名完成: ${outputPath.split("/").pop()} -> ${filePath.split("/").pop()}`);
18364
- logger.mark("正在尝试删除缓存文件");
18365
- await Common.removeFile(liveimg.filepath, true);
18366
18642
  temp.push({
18367
18643
  filepath: filePath,
18368
18644
  totalBytes: 0
18369
18645
  });
18370
- processedImages.push(segment.video("file://" + filePath));
18646
+ const videoPath = Config.upload.videoSendMode === "base64" ? `base64://${fs.readFileSync(filePath).toString("base64")}` : `file://${filePath}`;
18647
+ processedImages.push(segment.video(videoPath));
18371
18648
  if (imageItem.clip_type === 5 && imageItem.url_list?.[0]) {
18372
- const imageUrl = await processImageUrl(imageItem.url_list[0], g_title, index);
18373
- processedImages.push(segment.image(imageUrl));
18649
+ let hasPushedMotionPhotoCover = false;
18650
+ if (staticImgPath) {
18651
+ const motionPhotoCoverPath = Common.tempDri.images + `MVIMG_${format(/* @__PURE__ */ new Date(), "yyyyMMdd_HHmmss_SSS")}_${index}.jpg`;
18652
+ if (await buildGoogleMotionPhoto({
18653
+ imagePath: staticImgPath,
18654
+ videoPath: liveimg.filepath,
18655
+ outputPath: motionPhotoCoverPath
18656
+ })) {
18657
+ temp.push({
18658
+ filepath: motionPhotoCoverPath,
18659
+ totalBytes: 0
18660
+ });
18661
+ const motionPhotoCover = Config.upload.imageSendMode === "base64" ? `base64://${fs.readFileSync(motionPhotoCoverPath).toString("base64")}` : `file://${motionPhotoCoverPath}`;
18662
+ processedImages.push(segment.image(motionPhotoCover));
18663
+ hasPushedMotionPhotoCover = true;
18664
+ }
18665
+ }
18666
+ if (!hasPushedMotionPhotoCover) {
18667
+ const imageUrl = await processImageUrl(imageItem.url_list[0], g_title, index);
18668
+ processedImages.push(segment.image(imageUrl));
18669
+ }
18374
18670
  }
18671
+ logger.mark("正在尝试删除缓存文件");
18672
+ await Common.removeFile(liveimg.filepath, true);
18375
18673
  } else await Common.removeFile(liveimg.filepath, true);
18376
18674
  }
18377
18675
  }
18378
- const Element = common.makeForward(processedImages, this.e.sender.userId, this.e.sender.nick);
18676
+ 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
18677
  try {
18380
18678
  await this.e.bot.sendForwardMsg(this.e.contact, Element, {
18381
18679
  source: "图集内容",
@@ -18383,8 +18681,6 @@ var DouYin = class extends Base {
18383
18681
  prompt: "抖音图集解析结果",
18384
18682
  news: [{ text: "点击查看解析结果" }]
18385
18683
  });
18386
- } catch (error) {
18387
- await this.e.reply(JSON.stringify(error, null, 2));
18388
18684
  } finally {
18389
18685
  for (const item of temp) await Common.removeFile(item.filepath, true);
18390
18686
  }
@@ -18405,7 +18701,7 @@ var DouYin = class extends Base {
18405
18701
  }).getData().then((data$2) => fs.promises.writeFile(path$1, Buffer.from(data$2)));
18406
18702
  }
18407
18703
  }
18408
- const res = common.makeForward(imageres, this.e.sender.userId, this.e.sender.nick);
18704
+ 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
18705
  image_data.push(res);
18410
18706
  image_res.push(image_data);
18411
18707
  if (imageres.length === 1) {
@@ -18435,7 +18731,6 @@ var DouYin = class extends Base {
18435
18731
  headers: this.headers
18436
18732
  });
18437
18733
  temp.push(liveimgbgm);
18438
- if (mergeMode === "continuous") bgmContext = await createLiveImageContext(liveimgbgm.filepath);
18439
18734
  }
18440
18735
  const images1 = VideoData.data.aweme_detail.images ?? [];
18441
18736
  if (!images1.length) logger.debug("未获取到合辑的图片数据");
@@ -18446,51 +18741,79 @@ var DouYin = class extends Base {
18446
18741
  images.push(segment.image(imageUrl));
18447
18742
  continue;
18448
18743
  }
18449
- const liveimg = await downloadFile(`https://aweme.snssdk.com/aweme/v1/play/?video_id=${item.video.play_addr_h264.uri}&ratio=1080p&line=0`, {
18744
+ const livePhoto = await downloadFile(`https://aweme.snssdk.com/aweme/v1/play/?video_id=${item.video.play_addr_h264.uri}&ratio=1080p&line=0`, {
18450
18745
  title: `Douyin_tmp_V_${Date.now()}.mp4`,
18451
18746
  headers: this.headers
18452
18747
  });
18453
- if (liveimg.filepath) {
18748
+ if (livePhoto.filepath) {
18454
18749
  const outputPath = Common.tempDri.video + `Douyin_Result_${Date.now()}.mp4`;
18455
- let success;
18456
18750
  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;
18751
+ let staticImgPath = "";
18752
+ if (item.url_list?.[0]) {
18753
+ const staticImg = await downloadFile(item.url_list[0], {
18754
+ title: `Douyin_static_${Date.now()}_${index}.jpg`,
18755
+ headers: this.headers,
18756
+ filepath: Common.tempDri.images + `Douyin_static_${Date.now()}_${index}.jpg`
18757
+ });
18758
+ temp.push({
18759
+ filepath: staticImg.filepath,
18760
+ totalBytes: 0
18761
+ });
18762
+ staticImgPath = staticImg.filepath ?? "";
18461
18763
  }
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,
18764
+ const transitionEnabled = loopCount > 1 && Boolean(staticImgPath);
18765
+ const safeStaticPath = staticImgPath || livePhoto.filepath;
18766
+ const result = await loopVideoWithTransition({
18767
+ inputPath: livePhoto.filepath,
18472
18768
  outputPath,
18473
- loopCount
18474
- }, liveimgbgm.filepath);
18769
+ loopCount,
18770
+ staticImagePath: safeStaticPath,
18771
+ transitionEnabled,
18772
+ bgmPath: liveimgbgm?.filepath,
18773
+ mergeMode,
18774
+ context: bgmContext ?? void 0
18775
+ });
18776
+ const success = result.success;
18777
+ if (mergeMode === "continuous" && result.context) bgmContext = result.context;
18475
18778
  if (success) {
18476
18779
  const filePath = Common.tempDri.video + `tmp_${Date.now()}.mp4`;
18477
18780
  fs.renameSync(outputPath, filePath);
18478
18781
  logger.mark(`视频文件重命名完成: ${outputPath.split("/").pop()} -> ${filePath.split("/").pop()}`);
18479
- logger.mark("正在尝试删除缓存文件");
18480
- await Common.removeFile(liveimg.filepath, true);
18481
18782
  temp.push({
18482
18783
  filepath: filePath,
18483
18784
  totalBytes: 0
18484
18785
  });
18485
- images.push(segment.video("file://" + filePath));
18786
+ const videoPath = Config.upload.videoSendMode === "base64" ? `base64://${fs.readFileSync(filePath).toString("base64")}` : `file://${filePath}`;
18787
+ images.push(segment.video(videoPath));
18486
18788
  if (item.clip_type === 5 && item.url_list?.[0]) {
18487
- const imageUrl = await processImageUrl(item.url_list[0], g_title, index);
18488
- images.push(segment.image(imageUrl));
18789
+ let hasPushedMotionPhotoCover = false;
18790
+ if (staticImgPath) {
18791
+ const motionPhotoCoverPath = Common.tempDri.images + `MVIMG_${format(/* @__PURE__ */ new Date(), "yyyyMMdd_HHmmss_SSS")}_${index}.jpg`;
18792
+ if (await buildGoogleMotionPhoto({
18793
+ imagePath: staticImgPath,
18794
+ videoPath: livePhoto.filepath,
18795
+ outputPath: motionPhotoCoverPath
18796
+ })) {
18797
+ temp.push({
18798
+ filepath: motionPhotoCoverPath,
18799
+ totalBytes: 0
18800
+ });
18801
+ const motionPhotoCover = Config.upload.imageSendMode === "base64" ? `base64://${fs.readFileSync(motionPhotoCoverPath).toString("base64")}` : `file://${motionPhotoCoverPath}`;
18802
+ images.push(segment.image(motionPhotoCover));
18803
+ hasPushedMotionPhotoCover = true;
18804
+ }
18805
+ }
18806
+ if (!hasPushedMotionPhotoCover) {
18807
+ const imageUrl = await processImageUrl(item.url_list[0], g_title, index);
18808
+ images.push(segment.image(imageUrl));
18809
+ }
18489
18810
  }
18490
- } else await Common.removeFile(liveimg.filepath, true);
18811
+ logger.mark("正在尝试删除缓存文件");
18812
+ await Common.removeFile(livePhoto.filepath, true);
18813
+ } else await Common.removeFile(livePhoto.filepath, true);
18491
18814
  }
18492
18815
  }
18493
- const Element = common.makeForward(images, this.e.sender.userId, this.e.sender.nick);
18816
+ 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
18817
  try {
18495
18818
  await this.e.bot.sendForwardMsg(this.e.contact, Element, {
18496
18819
  source: "合辑内容",
@@ -18498,8 +18821,6 @@ var DouYin = class extends Base {
18498
18821
  prompt: "抖音合辑解析结果",
18499
18822
  news: [{ text: "点击查看解析结果" }]
18500
18823
  });
18501
- } catch (error) {
18502
- await this.e.reply(JSON.stringify(error, null, 2));
18503
18824
  } finally {
18504
18825
  for (const item of temp) await Common.removeFile(item.filepath, true);
18505
18826
  }
@@ -18634,8 +18955,8 @@ var DouYin = class extends Base {
18634
18955
  const imageUrl = await processImageUrl(v, VideoData.data.aweme_detail.desc, index);
18635
18956
  messageElements.push(segment.image(imageUrl));
18636
18957
  }
18637
- const res = common.makeForward(messageElements, this.e.sender.userId, this.e.sender.nick);
18638
- this.e.bot.sendForwardMsg(this.e.contact, res, {
18958
+ 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);
18959
+ await this.e.bot.sendForwardMsg(this.e.contact, res, {
18639
18960
  source: "评论图片收集",
18640
18961
  summary: `查看${messageElements.length}张图片`,
18641
18962
  prompt: "抖音评论解析结果",
@@ -18970,11 +19291,22 @@ let DouyinImageSubType = function(DouyinImageSubType$1) {
18970
19291
  return DouyinImageSubType$1;
18971
19292
  }({});
18972
19293
  function getWorkTypeInfo(data$1) {
19294
+ if (data$1.live_data) return {
19295
+ mainType: DouyinWorkMainType.LIVE,
19296
+ isVideo: false,
19297
+ isImage: false,
19298
+ isArticle: false,
19299
+ isLive: true,
19300
+ isGallery: false,
19301
+ isCollection: false,
19302
+ templatePath: "douyin/live"
19303
+ };
18973
19304
  if (data$1.aweme_type === 163 || data$1.article_info) return {
18974
19305
  mainType: DouyinWorkMainType.ARTICLE,
18975
19306
  isVideo: false,
18976
19307
  isImage: false,
18977
19308
  isArticle: true,
19309
+ isLive: false,
18978
19310
  isGallery: false,
18979
19311
  isCollection: false,
18980
19312
  templatePath: "douyin/article-work"
@@ -18987,6 +19319,7 @@ function getWorkTypeInfo(data$1) {
18987
19319
  isVideo: false,
18988
19320
  isImage: true,
18989
19321
  isArticle: false,
19322
+ isLive: false,
18990
19323
  isGallery: subType === DouyinImageSubType.GALLERY,
18991
19324
  isCollection: subType === DouyinImageSubType.COLLECTION,
18992
19325
  templatePath: "douyin/image-work"
@@ -18997,6 +19330,7 @@ function getWorkTypeInfo(data$1) {
18997
19330
  isVideo: true,
18998
19331
  isImage: false,
18999
19332
  isArticle: false,
19333
+ isLive: false,
19000
19334
  isGallery: false,
19001
19335
  isCollection: false,
19002
19336
  templatePath: "douyin/video-work"
@@ -19017,6 +19351,7 @@ function getWorkTypeDisplayName(workTypeInfo) {
19017
19351
  if (workTypeInfo.isGallery) return "图集";
19018
19352
  if (workTypeInfo.isCollection) return "合辑";
19019
19353
  if (workTypeInfo.isArticle) return "文章";
19354
+ if (workTypeInfo.isLive) return "直播";
19020
19355
  return "未知";
19021
19356
  }
19022
19357
  const getDouyinID = async (event, url, log = true) => {
@@ -19550,7 +19885,6 @@ var DouYinpush = class extends Base {
19550
19885
  headers: douyinBaseHeaders
19551
19886
  });
19552
19887
  temp.push(liveimgbgm);
19553
- if (mergeMode === "continuous") bgmContext = await createLiveImageContext(liveimgbgm.filepath);
19554
19888
  }
19555
19889
  const images1 = Detail_Data.images ?? [];
19556
19890
  if (!images1.length) logger.debug("未获取到合辑的图片数据");
@@ -19566,41 +19900,69 @@ var DouYinpush = class extends Base {
19566
19900
  });
19567
19901
  if (liveimg.filepath) {
19568
19902
  const outputPath = Common.tempDri.video + `Douyin_Result_${Date.now()}.mp4`;
19569
- let success;
19570
19903
  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;
19904
+ let staticImgPath = "";
19905
+ if (item.url_list?.[0]) {
19906
+ const staticImg = await downloadFile(item.url_list[0], {
19907
+ title: `Douyin_static_${Date.now()}_${index}.jpg`,
19908
+ headers: douyinBaseHeaders,
19909
+ filepath: Common.tempDri.images + `Douyin_static_${Date.now()}_${index}.jpg`
19910
+ });
19911
+ if (staticImg.filepath) temp.push({
19912
+ filepath: staticImg.filepath,
19913
+ totalBytes: 0
19914
+ });
19915
+ staticImgPath = staticImg.filepath ?? "";
19575
19916
  }
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,
19917
+ const transitionEnabled = loopCount > 1 && Boolean(staticImgPath);
19918
+ const safeStaticPath = staticImgPath || liveimg.filepath;
19919
+ const result = await loopVideoWithTransition({
19920
+ inputPath: liveimg.filepath,
19586
19921
  outputPath,
19587
- loopCount
19588
- }, liveimgbgm.filepath);
19922
+ loopCount,
19923
+ staticImagePath: safeStaticPath,
19924
+ transitionEnabled,
19925
+ bgmPath: liveimgbgm?.filepath,
19926
+ mergeMode,
19927
+ context: bgmContext ?? void 0
19928
+ });
19929
+ const success = result.success;
19930
+ if (mergeMode === "continuous" && result.context) bgmContext = result.context;
19589
19931
  if (success) {
19590
19932
  const filePath = Common.tempDri.video + `tmp_${Date.now()}.mp4`;
19591
19933
  fs.renameSync(outputPath, filePath);
19592
19934
  logger.mark(`视频文件重命名完成: ${outputPath.split("/").pop()} -> ${filePath.split("/").pop()}`);
19593
- logger.mark("正在尝试删除缓存文件");
19594
- await Common.removeFile(liveimg.filepath, true);
19595
19935
  temp.push({
19596
19936
  filepath: filePath,
19597
19937
  totalBytes: 0
19598
19938
  });
19599
- images.push(segment.video("file://" + filePath));
19939
+ const videoPath = Config.upload.videoSendMode === "base64" ? `base64://${fs.readFileSync(filePath).toString("base64")}` : `file://${filePath}`;
19940
+ images.push(segment.video(videoPath));
19600
19941
  if (item.clip_type === 5 && item.url_list?.[0]) {
19601
- const imageUrl = await processImageUrl(item.url_list[0], Detail_Data.desc, index);
19602
- images.push(segment.image(imageUrl));
19942
+ let hasPushedMotionPhotoCover = false;
19943
+ if (staticImgPath) {
19944
+ const motionPhotoCoverPath = Common.tempDri.images + `MVIMG_${format(/* @__PURE__ */ new Date(), "yyyyMMdd_HHmmss_SSS")}_${index}.jpg`;
19945
+ if (await buildGoogleMotionPhoto({
19946
+ imagePath: staticImgPath,
19947
+ videoPath: liveimg.filepath,
19948
+ outputPath: motionPhotoCoverPath
19949
+ })) {
19950
+ temp.push({
19951
+ filepath: motionPhotoCoverPath,
19952
+ totalBytes: 0
19953
+ });
19954
+ const motionPhotoCover = Config.upload.imageSendMode === "base64" ? `base64://${fs.readFileSync(motionPhotoCoverPath).toString("base64")}` : `file://${motionPhotoCoverPath}`;
19955
+ images.push(segment.image(motionPhotoCover));
19956
+ hasPushedMotionPhotoCover = true;
19957
+ }
19958
+ }
19959
+ if (!hasPushedMotionPhotoCover) {
19960
+ const imageUrl = await processImageUrl(item.url_list[0], Detail_Data.desc, index);
19961
+ images.push(segment.image(imageUrl));
19962
+ }
19603
19963
  }
19964
+ logger.mark("正在尝试删除缓存文件");
19965
+ await Common.removeFile(liveimg.filepath, true);
19604
19966
  } else await Common.removeFile(liveimg.filepath, true);
19605
19967
  }
19606
19968
  }
@@ -19633,7 +19995,6 @@ var DouYinpush = class extends Base {
19633
19995
  headers: douyinBaseHeaders
19634
19996
  });
19635
19997
  temp.push(liveimgbgm);
19636
- if (mergeMode === "continuous") bgmContext = await createLiveImageContext(liveimgbgm.filepath);
19637
19998
  }
19638
19999
  for (const [index, item] of Detail_Data.images.entries()) {
19639
20000
  if (item.clip_type === 2 || item.clip_type === void 0) {
@@ -19647,41 +20008,69 @@ var DouYinpush = class extends Base {
19647
20008
  });
19648
20009
  if (liveimg.filepath) {
19649
20010
  const outputPath = Common.tempDri.video + `Douyin_Result_${Date.now()}.mp4`;
19650
- let success;
19651
20011
  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;
20012
+ let staticImgPath = "";
20013
+ if (item.url_list?.[0]) {
20014
+ const staticImg = await downloadFile(item.url_list[0], {
20015
+ title: `Douyin_static_${Date.now()}_${index}.jpg`,
20016
+ headers: douyinBaseHeaders,
20017
+ filepath: Common.tempDri.images + `Douyin_static_${Date.now()}_${index}.jpg`
20018
+ });
20019
+ if (staticImg.filepath) temp.push({
20020
+ filepath: staticImg.filepath,
20021
+ totalBytes: 0
20022
+ });
20023
+ staticImgPath = staticImg.filepath ?? "";
19656
20024
  }
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,
20025
+ const transitionEnabled = loopCount > 1 && Boolean(staticImgPath);
20026
+ const safeStaticPath = staticImgPath || liveimg.filepath;
20027
+ const result = await loopVideoWithTransition({
20028
+ inputPath: liveimg.filepath,
19667
20029
  outputPath,
19668
- loopCount
19669
- }, liveimgbgm.filepath);
20030
+ loopCount,
20031
+ staticImagePath: safeStaticPath,
20032
+ transitionEnabled,
20033
+ bgmPath: liveimgbgm?.filepath,
20034
+ mergeMode,
20035
+ context: bgmContext ?? void 0
20036
+ });
20037
+ const success = result.success;
20038
+ if (mergeMode === "continuous" && result.context) bgmContext = result.context;
19670
20039
  if (success) {
19671
20040
  const filePath = Common.tempDri.video + `tmp_${Date.now()}.mp4`;
19672
20041
  fs.renameSync(outputPath, filePath);
19673
20042
  logger.mark(`视频文件重命名完成: ${outputPath.split("/").pop()} -> ${filePath.split("/").pop()}`);
19674
- logger.mark("正在尝试删除缓存文件");
19675
- await Common.removeFile(liveimg.filepath, true);
19676
20043
  temp.push({
19677
20044
  filepath: filePath,
19678
20045
  totalBytes: 0
19679
20046
  });
19680
- processedImages.push(segment.video("file://" + filePath));
20047
+ const videoPath = Config.upload.videoSendMode === "base64" ? `base64://${fs.readFileSync(filePath).toString("base64")}` : `file://${filePath}`;
20048
+ processedImages.push(segment.video(videoPath));
19681
20049
  if (item.clip_type === 5 && item.url_list?.[0]) {
19682
- const imageUrl = await processImageUrl(item.url_list[0], Detail_Data.desc, index);
19683
- processedImages.push(segment.image(imageUrl));
20050
+ let hasPushedMotionPhotoCover = false;
20051
+ if (staticImgPath) {
20052
+ const motionPhotoCoverPath = Common.tempDri.images + `MVIMG_${format(/* @__PURE__ */ new Date(), "yyyyMMdd_HHmmss_SSS")}_${index}.jpg`;
20053
+ if (await buildGoogleMotionPhoto({
20054
+ imagePath: staticImgPath,
20055
+ videoPath: liveimg.filepath,
20056
+ outputPath: motionPhotoCoverPath
20057
+ })) {
20058
+ temp.push({
20059
+ filepath: motionPhotoCoverPath,
20060
+ totalBytes: 0
20061
+ });
20062
+ const motionPhotoCover = Config.upload.imageSendMode === "base64" ? `base64://${fs.readFileSync(motionPhotoCoverPath).toString("base64")}` : `file://${motionPhotoCoverPath}`;
20063
+ processedImages.push(segment.image(motionPhotoCover));
20064
+ hasPushedMotionPhotoCover = true;
20065
+ }
20066
+ }
20067
+ if (!hasPushedMotionPhotoCover) {
20068
+ const imageUrl = await processImageUrl(item.url_list[0], Detail_Data.desc, index);
20069
+ processedImages.push(segment.image(imageUrl));
20070
+ }
19684
20071
  }
20072
+ logger.mark("正在尝试删除缓存文件");
20073
+ await Common.removeFile(liveimg.filepath, true);
19685
20074
  } else await Common.removeFile(liveimg.filepath, true);
19686
20075
  }
19687
20076
  }
@@ -19985,7 +20374,6 @@ var skipDynamic = async (PushItem) => {
19985
20374
  logger.debug(`检查作品是否需要过滤:${PushItem.Detail_Data.share_url}`);
19986
20375
  return await douyinDBInstance.shouldFilter(PushItem, tags);
19987
20376
  };
19988
- init_danmaku();
19989
20377
  init_date_fns();
19990
20378
  init_locale();
19991
20379
  init_Config();
@@ -20507,7 +20895,7 @@ const task = Config.app.removeCache && karin$1.task("[kkk-缓存自动删除]",
20507
20895
  const twoHoursAgo = Date.now() - 7200 * 1e3;
20508
20896
  const videoDeleted = removeOldFiles(Common.tempDri.video, twoHoursAgo);
20509
20897
  logger.mark(`${Common.tempDri.video} 目录下已删除 ${videoDeleted} 个文件`);
20510
- if (Config.app.downloadImageLocally) {
20898
+ if (Config.upload.imageSendMode === "file") {
20511
20899
  const imageDeleted = removeOldFiles(Common.tempDri.images, twoHoursAgo);
20512
20900
  logger.mark(`${Common.tempDri.images} 目录下已删除 ${imageDeleted} 个文件`);
20513
20901
  }
@@ -21340,7 +21728,7 @@ var Xiaohongshu = class extends Base {
21340
21728
  const imageUrl = await processImageUrl(item.url_default, title, index);
21341
21729
  Imgs.push(segment.image(imageUrl));
21342
21730
  }
21343
- const res = common.makeForward(Imgs, this.e.sender.userId, this.e.sender.nick);
21731
+ 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
21732
  if (NoteData.data.data.items[0].note_card.image_list.length === 1) {
21345
21733
  const imageUrl = await processImageUrl(NoteData.data.data.items[0].note_card.image_list[0].url_default, title);
21346
21734
  await this.e.reply(segment.image(imageUrl));