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