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.
- package/CHANGELOG.md +35 -0
- package/config/default_config/app.yaml +4 -1
- package/config/default_config/upload.yaml +7 -4
- package/lib/apps/admin.js +2 -2
- package/lib/apps/help.js +2 -2
- package/lib/apps/push.js +2 -2
- package/lib/apps/qrlogin.js +2 -2
- package/lib/apps/statistics.js +2 -2
- package/lib/apps/tools.js +2 -2
- package/lib/apps/update.js +2 -2
- package/lib/build-metadata.json +5 -5
- package/lib/core_chunk/{main-BQn-mQch.js → main-DjlCg9e5.js} +1645 -1257
- package/lib/core_chunk/{template-2ApQpQ8R.js → template-CsOboAFj.js} +12 -1
- package/lib/core_chunk/template.js +1 -1
- package/lib/index.js +2 -2
- package/lib/karin-plugin-kkk.css +4 -0
- package/lib/root.js +1 -1
- package/lib/web.config.js +2 -2
- package/package.json +2 -2
|
@@ -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-
|
|
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.
|
|
6205
|
-
File = `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
|
|
7115
|
-
|
|
7116
|
-
logger.debug(`
|
|
7117
|
-
|
|
7118
|
-
|
|
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
|
|
7163
|
-
const
|
|
7164
|
-
|
|
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
|
|
7199
|
-
|
|
7200
|
-
|
|
7201
|
-
|
|
7202
|
-
|
|
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
|
|
7220
|
-
|
|
7221
|
-
|
|
7222
|
-
|
|
7223
|
-
|
|
7224
|
-
|
|
7225
|
-
|
|
7226
|
-
|
|
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
|
-
|
|
7235
|
-
|
|
7236
|
-
|
|
7237
|
-
|
|
7238
|
-
|
|
7239
|
-
|
|
7240
|
-
|
|
7241
|
-
|
|
7242
|
-
|
|
7243
|
-
|
|
7244
|
-
|
|
7245
|
-
|
|
7246
|
-
|
|
7247
|
-
|
|
7248
|
-
|
|
7249
|
-
|
|
7250
|
-
|
|
7251
|
-
|
|
7252
|
-
const
|
|
7253
|
-
const
|
|
7254
|
-
|
|
7255
|
-
|
|
7256
|
-
|
|
7257
|
-
|
|
7258
|
-
}
|
|
7259
|
-
|
|
7260
|
-
|
|
7261
|
-
|
|
7262
|
-
|
|
7263
|
-
|
|
7264
|
-
|
|
7265
|
-
|
|
7266
|
-
|
|
7267
|
-
|
|
7268
|
-
|
|
7269
|
-
|
|
7270
|
-
|
|
7271
|
-
|
|
7272
|
-
|
|
7273
|
-
|
|
7274
|
-
|
|
7275
|
-
|
|
7276
|
-
|
|
7277
|
-
|
|
7278
|
-
|
|
7279
|
-
|
|
7280
|
-
|
|
7281
|
-
|
|
7282
|
-
|
|
7283
|
-
|
|
7284
|
-
|
|
7285
|
-
|
|
7286
|
-
|
|
7287
|
-
|
|
7288
|
-
|
|
7289
|
-
|
|
7290
|
-
|
|
7291
|
-
}
|
|
7292
|
-
|
|
7293
|
-
|
|
7294
|
-
|
|
7295
|
-
|
|
7296
|
-
|
|
7297
|
-
|
|
7298
|
-
|
|
7299
|
-
|
|
7300
|
-
|
|
7301
|
-
|
|
7302
|
-
|
|
7303
|
-
|
|
7304
|
-
|
|
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
|
-
|
|
7309
|
-
|
|
7310
|
-
|
|
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
|
-
|
|
7319
|
-
|
|
7320
|
-
|
|
7321
|
-
|
|
7322
|
-
|
|
7323
|
-
|
|
7324
|
-
|
|
7325
|
-
|
|
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=\"<?xml version='1.0' encoding='UTF-8' standalone='yes' ?>\"",
|
|
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
|
-
|
|
7328
|
-
|
|
7329
|
-
|
|
7330
|
-
const
|
|
7331
|
-
if (
|
|
7332
|
-
|
|
7333
|
-
|
|
7334
|
-
|
|
7335
|
-
|
|
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
|
-
|
|
7378
|
-
|
|
7379
|
-
|
|
7380
|
-
|
|
7381
|
-
|
|
7382
|
-
|
|
7383
|
-
|
|
7384
|
-
|
|
7385
|
-
|
|
7386
|
-
|
|
7387
|
-
|
|
7388
|
-
|
|
7389
|
-
|
|
7390
|
-
|
|
7391
|
-
|
|
7392
|
-
|
|
7393
|
-
|
|
7394
|
-
|
|
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
|
-
|
|
7460
|
-
|
|
7461
|
-
|
|
7462
|
-
|
|
7463
|
-
|
|
7464
|
-
|
|
7465
|
-
|
|
7466
|
-
|
|
7467
|
-
|
|
7468
|
-
|
|
7469
|
-
|
|
7470
|
-
|
|
7471
|
-
|
|
7472
|
-
|
|
7473
|
-
|
|
7474
|
-
|
|
7475
|
-
|
|
7476
|
-
|
|
7477
|
-
|
|
7478
|
-
|
|
7479
|
-
|
|
7480
|
-
|
|
7481
|
-
|
|
7482
|
-
|
|
7483
|
-
|
|
7484
|
-
|
|
7485
|
-
|
|
7486
|
-
|
|
7487
|
-
|
|
7488
|
-
|
|
7489
|
-
|
|
7490
|
-
|
|
7491
|
-
|
|
7492
|
-
|
|
7493
|
-
|
|
7494
|
-
|
|
7495
|
-
|
|
7496
|
-
|
|
7497
|
-
|
|
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
|
-
|
|
7419
|
+
sanitizeFilename = (filename) => filename.replace(/[<>:"/\\|?*\x00-\x1f]/g, "_").replace(/^\.+/, "").replace(/\.+$/, "").replace(/\s+/g, "_").substring(0, 200);
|
|
7501
7420
|
});
|
|
7502
|
-
|
|
7503
|
-
|
|
7504
|
-
|
|
7505
|
-
|
|
7506
|
-
|
|
7507
|
-
|
|
7508
|
-
|
|
7509
|
-
|
|
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
|
-
|
|
7512
|
-
|
|
7513
|
-
|
|
7514
|
-
|
|
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
|
-
|
|
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
|
-
|
|
7636
|
-
|
|
7637
|
-
|
|
7638
|
-
|
|
7639
|
-
|
|
7640
|
-
|
|
7641
|
-
|
|
7642
|
-
|
|
7643
|
-
const
|
|
7644
|
-
|
|
7645
|
-
|
|
7646
|
-
|
|
7647
|
-
|
|
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
|
-
|
|
7652
|
-
|
|
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
|
-
|
|
7776
|
-
|
|
7777
|
-
|
|
7778
|
-
|
|
7779
|
-
|
|
7780
|
-
|
|
7781
|
-
|
|
7782
|
-
|
|
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(
|
|
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: "
|
|
12987
|
-
type: "
|
|
12988
|
-
label: "
|
|
12989
|
-
description: "
|
|
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: "
|
|
12997
|
-
disabled: $
|
|
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"), $
|
|
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
|
|
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.
|
|
15180
|
-
label: "
|
|
15181
|
-
|
|
15182
|
-
|
|
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: "
|
|
14626
|
+
description: "使用群文件上传,开启后会将视频文件上传到群文件中,需配置「群文件上传阈值」。与「本地视频发送方式 = Base64」互斥。",
|
|
15188
14627
|
defaultSelected: all.upload.usegroupfile,
|
|
15189
|
-
isDisabled: all.upload.
|
|
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.
|
|
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
|
|
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
|
-
|
|
15663
|
-
|
|
15664
|
-
|
|
15665
|
-
|
|
15666
|
-
|
|
15667
|
-
|
|
15668
|
-
|
|
15669
|
-
|
|
15670
|
-
|
|
15671
|
-
|
|
15672
|
-
|
|
15673
|
-
|
|
15674
|
-
|
|
15675
|
-
|
|
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
|
-
|
|
15680
|
-
|
|
15681
|
-
|
|
15682
|
-
|
|
15683
|
-
|
|
15684
|
-
|
|
15685
|
-
|
|
15686
|
-
|
|
15687
|
-
|
|
15688
|
-
|
|
15689
|
-
|
|
15690
|
-
|
|
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
|
-
|
|
15727
|
-
|
|
15728
|
-
|
|
15729
|
-
|
|
15730
|
-
|
|
15731
|
-
}))
|
|
15512
|
+
return {
|
|
15513
|
+
width: origW,
|
|
15514
|
+
height: origH,
|
|
15515
|
+
offsetY: 0,
|
|
15516
|
+
isVertical: false
|
|
15732
15517
|
};
|
|
15733
|
-
|
|
15734
|
-
|
|
15735
|
-
|
|
15736
|
-
if (
|
|
15737
|
-
|
|
15738
|
-
|
|
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
|
-
|
|
15745
|
-
|
|
15746
|
-
|
|
15747
|
-
|
|
15748
|
-
|
|
15749
|
-
|
|
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
|
-
|
|
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
|
-
|
|
16031
|
-
|
|
16032
|
-
|
|
16033
|
-
|
|
16034
|
-
|
|
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
|
|
17788
|
-
|
|
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
|
-
|
|
17792
|
-
|
|
17793
|
-
|
|
17794
|
-
|
|
17795
|
-
|
|
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
|
-
|
|
18178
|
-
|
|
18179
|
-
|
|
18180
|
-
|
|
18181
|
-
|
|
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
|
-
|
|
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
|
-
|
|
18199
|
-
|
|
18200
|
-
|
|
18201
|
-
|
|
18202
|
-
|
|
18203
|
-
|
|
18204
|
-
|
|
18205
|
-
|
|
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
|
-
|
|
18209
|
-
|
|
18456
|
+
width: origW,
|
|
18457
|
+
height: origH,
|
|
18458
|
+
offsetY: 0,
|
|
18459
|
+
isVertical: false
|
|
18210
18460
|
};
|
|
18211
|
-
}
|
|
18212
|
-
|
|
18213
|
-
const
|
|
18214
|
-
|
|
18215
|
-
|
|
18216
|
-
|
|
18217
|
-
|
|
18218
|
-
|
|
18219
|
-
|
|
18220
|
-
|
|
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
|
-
|
|
18343
|
-
|
|
18344
|
-
|
|
18345
|
-
|
|
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
|
-
|
|
18348
|
-
|
|
18349
|
-
|
|
18350
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
18373
|
-
|
|
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
|
|
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 (
|
|
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
|
-
|
|
18458
|
-
|
|
18459
|
-
|
|
18460
|
-
|
|
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
|
-
|
|
18463
|
-
|
|
18464
|
-
|
|
18465
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
18488
|
-
|
|
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
|
-
|
|
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
|
-
|
|
19572
|
-
|
|
19573
|
-
|
|
19574
|
-
|
|
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
|
-
|
|
19577
|
-
|
|
19578
|
-
|
|
19579
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
19602
|
-
|
|
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
|
-
|
|
19653
|
-
|
|
19654
|
-
|
|
19655
|
-
|
|
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
|
-
|
|
19658
|
-
|
|
19659
|
-
|
|
19660
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
19683
|
-
|
|
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.
|
|
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));
|