karin-plugin-kkk 2.23.2 → 2.24.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +23 -0
- package/config/default_config/app.yaml +4 -1
- package/config/default_config/upload.yaml +7 -4
- package/lib/apps/admin.js +3 -3
- package/lib/apps/help.js +3 -3
- package/lib/apps/push.js +3 -3
- package/lib/apps/qrlogin.js +3 -3
- package/lib/apps/statistics.js +3 -3
- package/lib/apps/tools.js +3 -3
- package/lib/apps/update.js +3 -3
- package/lib/build-metadata.json +5 -5
- package/lib/core_chunk/{main-BQn-mQch.js → main-1eljaHiz.js} +1247 -1074
- package/lib/core_chunk/{template-2ApQpQ8R.js → template-DekmxKd7.js} +13 -2
- package/lib/core_chunk/template.js +2 -2
- package/lib/core_chunk/{vendor-9pKTNH6x.js → vendor-DxfKHvj-.js} +4 -0
- package/lib/index.js +3 -3
- package/lib/karin-plugin-kkk.css +4 -0
- package/lib/root.js +1 -1
- package/lib/web.config.js +3 -3
- 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
|
-
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-
|
|
3
|
-
import { n as init_client, r as reactServerRender } from "./template-
|
|
2
|
+
import { A as init_locale, An as init_zod, Cn as Chalk, Dn as axios_default, En as init_axios, On as Xhshow, Sn as require_protobufjs, Tn as AxiosError$1, _n as require_png, a as Window, bn as require_heic_decode, dt as init_date_fns, ft as fromUnixTime, ht as differenceInSeconds, i as init_lib, j as zhCN, jn as zod_default, kn as init_dist, mt as format, n as require_lib, pt as formatDistanceToNow, r as require_qr_code_styling, t as require_dist, vn as require_jsQR, wn as init_source, xn as require_express, yn as require_jpeg_js } from "./vendor-DxfKHvj-.js";
|
|
3
|
+
import { n as init_client, r as reactServerRender } from "./template-DekmxKd7.js";
|
|
4
4
|
import { createRequire } from "node:module";
|
|
5
5
|
import karin$1, { BOT_CONNECT, app, authMiddleware, checkPkgUpdate, checkPort, common, components, config, copyConfigSync, createBadRequestResponse, createNotFoundResponse, createServerErrorResponse, createSuccessResponse, db, defineConfig, ffmpeg, ffprobe, filesByExt, getBot, hooks, karin, karinPathHtml, karinPathTemp, logger, logs, mkdirSync, range, render, requireFileSync, restart, segment, updatePkg, watch } from "node-karin";
|
|
6
6
|
import fs from "node:fs";
|
|
@@ -6201,8 +6201,8 @@ var init_Base = __esmMin(() => {
|
|
|
6201
6201
|
await karin$1.sendMsg(selfId, contact, message2);
|
|
6202
6202
|
}
|
|
6203
6203
|
if (options) options.useGroupFile = Config.upload.usegroupfile && newFileSize > Config.upload.groupfilevalue;
|
|
6204
|
-
if (Config.upload.
|
|
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
|
});
|
|
@@ -11746,8 +11093,14 @@ var init_app_schema = __esmMin(() => {
|
|
|
11746
11093
|
{
|
|
11747
11094
|
key: "parseTip",
|
|
11748
11095
|
type: "switch",
|
|
11749
|
-
label: "解析提示",
|
|
11750
|
-
description: "发送提示信息:\"检测到xxx链接,开始解析\""
|
|
11096
|
+
label: "解析提示",
|
|
11097
|
+
description: "发送提示信息:\"检测到xxx链接,开始解析\""
|
|
11098
|
+
},
|
|
11099
|
+
{
|
|
11100
|
+
key: "fakeForward",
|
|
11101
|
+
type: "switch",
|
|
11102
|
+
label: "伪造合并转发消息",
|
|
11103
|
+
description: "开启后合并转发将使用触发者身份展示;关闭后使用机器人身份展示"
|
|
11751
11104
|
},
|
|
11752
11105
|
{
|
|
11753
11106
|
key: "errorLogSendTo",
|
|
@@ -12983,18 +12336,25 @@ var init_upload_schema = __esmMin(() => {
|
|
|
12983
12336
|
title: "发送方式配置"
|
|
12984
12337
|
},
|
|
12985
12338
|
{
|
|
12986
|
-
key: "
|
|
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
|
]
|
|
@@ -15670,93 +15044,477 @@ var deepEqual = (a, b) => {
|
|
|
15670
15044
|
isChange = true;
|
|
15671
15045
|
return true;
|
|
15672
15046
|
}
|
|
15673
|
-
if (deepEqual(a[key], b[key])) {
|
|
15674
|
-
isChange = true;
|
|
15675
|
-
return true;
|
|
15047
|
+
if (deepEqual(a[key], b[key])) {
|
|
15048
|
+
isChange = true;
|
|
15049
|
+
return true;
|
|
15050
|
+
}
|
|
15051
|
+
}
|
|
15052
|
+
}
|
|
15053
|
+
return false;
|
|
15054
|
+
};
|
|
15055
|
+
var convertToNumber = (value) => {
|
|
15056
|
+
if (/^\d+$/.test(value)) return parseInt(value, 10);
|
|
15057
|
+
else return value;
|
|
15058
|
+
};
|
|
15059
|
+
var getFirstObject = (arr) => arr.length > 0 ? arr[0] : {};
|
|
15060
|
+
var setNestedProperty = (obj, keys, value) => {
|
|
15061
|
+
let current = obj;
|
|
15062
|
+
for (let i = 0; i < keys.length - 1; i++) {
|
|
15063
|
+
const key = keys[i];
|
|
15064
|
+
if (!current[key] || typeof current[key] !== "object") current[key] = {};
|
|
15065
|
+
current = current[key];
|
|
15066
|
+
}
|
|
15067
|
+
const lastKey = keys[keys.length - 1];
|
|
15068
|
+
current[lastKey] = value;
|
|
15069
|
+
};
|
|
15070
|
+
var processFrontendData = (data$1) => {
|
|
15071
|
+
const result = {};
|
|
15072
|
+
const configKeys = Object.keys(data$1).filter((key) => !key.includes("pushlist") && key in data$1);
|
|
15073
|
+
for (const key of configKeys) {
|
|
15074
|
+
const value = data$1[key];
|
|
15075
|
+
const firstObj = Array.isArray(value) ? getFirstObject(value) : {};
|
|
15076
|
+
const objKeys = Object.keys(firstObj);
|
|
15077
|
+
if (objKeys.length === 0) continue;
|
|
15078
|
+
const configObj = {};
|
|
15079
|
+
let hasValidData = false;
|
|
15080
|
+
const nestedProps = objKeys.filter((prop) => prop.includes(":"));
|
|
15081
|
+
const flatProps = objKeys.filter((prop) => !prop.includes(":"));
|
|
15082
|
+
for (const prop of nestedProps) {
|
|
15083
|
+
let propValue = firstObj[prop];
|
|
15084
|
+
if (typeof propValue === "string") propValue = convertToNumber(propValue);
|
|
15085
|
+
if (propValue !== void 0 && propValue !== null) {
|
|
15086
|
+
setNestedProperty(configObj, prop.split(":"), propValue);
|
|
15087
|
+
hasValidData = true;
|
|
15088
|
+
}
|
|
15089
|
+
}
|
|
15090
|
+
for (const prop of flatProps) {
|
|
15091
|
+
let propValue = firstObj[prop];
|
|
15092
|
+
if (typeof propValue === "string") propValue = convertToNumber(propValue);
|
|
15093
|
+
if (propValue !== void 0 && propValue !== null) {
|
|
15094
|
+
configObj[prop] = propValue;
|
|
15095
|
+
hasValidData = true;
|
|
15096
|
+
}
|
|
15097
|
+
}
|
|
15098
|
+
if (hasValidData && Object.keys(configObj).length > 0) result[key] = configObj;
|
|
15099
|
+
}
|
|
15100
|
+
result.pushlist = {
|
|
15101
|
+
douyin: data$1["pushlist:douyin"] || [],
|
|
15102
|
+
bilibili: (data$1["pushlist:bilibili"] || []).map((item) => ({
|
|
15103
|
+
...item,
|
|
15104
|
+
host_mid: Number(item.host_mid)
|
|
15105
|
+
}))
|
|
15106
|
+
};
|
|
15107
|
+
return result;
|
|
15108
|
+
};
|
|
15109
|
+
var cleanFlattenedFields = (obj) => {
|
|
15110
|
+
if (!obj || typeof obj !== "object") return;
|
|
15111
|
+
for (const [, value] of Object.entries(obj)) if (typeof value === "object" && value !== null && !Array.isArray(value)) {
|
|
15112
|
+
cleanFlattenedFields(value);
|
|
15113
|
+
const valueObj = value;
|
|
15114
|
+
const flattenedKeys = Object.keys(valueObj).filter((k) => k.includes("."));
|
|
15115
|
+
for (const flatKey of flattenedKeys) if (hasNestedStructure(valueObj, flatKey.split("."))) delete valueObj[flatKey];
|
|
15116
|
+
}
|
|
15117
|
+
};
|
|
15118
|
+
var hasNestedStructure = (obj, path$1) => {
|
|
15119
|
+
let current = obj;
|
|
15120
|
+
for (let i = 0; i < path$1.length - 1; i++) {
|
|
15121
|
+
const key = path$1[i];
|
|
15122
|
+
if (!current[key] || typeof current[key] !== "object") return false;
|
|
15123
|
+
current = current[key];
|
|
15124
|
+
}
|
|
15125
|
+
return path$1[path$1.length - 1] in current;
|
|
15126
|
+
};
|
|
15127
|
+
await init_utils$1();
|
|
15128
|
+
var ENCODER_PRIORITY$1 = {
|
|
15129
|
+
h264: [
|
|
15130
|
+
"h264_nvenc",
|
|
15131
|
+
"h264_qsv",
|
|
15132
|
+
"h264_amf",
|
|
15133
|
+
"libx264"
|
|
15134
|
+
],
|
|
15135
|
+
h265: [
|
|
15136
|
+
"hevc_nvenc",
|
|
15137
|
+
"hevc_qsv",
|
|
15138
|
+
"hevc_amf",
|
|
15139
|
+
"libx265"
|
|
15140
|
+
],
|
|
15141
|
+
av1: [
|
|
15142
|
+
"av1_nvenc",
|
|
15143
|
+
"av1_qsv",
|
|
15144
|
+
"av1_amf",
|
|
15145
|
+
"libsvtav1",
|
|
15146
|
+
"libaom-av1"
|
|
15147
|
+
]
|
|
15148
|
+
};
|
|
15149
|
+
var SOFTWARE_FALLBACK$1 = {
|
|
15150
|
+
h264: "libx264",
|
|
15151
|
+
h265: "libx265",
|
|
15152
|
+
av1: "libsvtav1"
|
|
15153
|
+
};
|
|
15154
|
+
var cachedEncoders$1 = {};
|
|
15155
|
+
async function detectEncoder$1(codec) {
|
|
15156
|
+
if (cachedEncoders$1[codec]) return cachedEncoders$1[codec];
|
|
15157
|
+
logger.debug(`[BiliDanmaku] 开始检测 ${codec.toUpperCase()} 编码器...`);
|
|
15158
|
+
for (const encoder of ENCODER_PRIORITY$1[codec]) {
|
|
15159
|
+
logger.debug(`[BiliDanmaku] 测试编码器: ${encoder}`);
|
|
15160
|
+
try {
|
|
15161
|
+
const result = await ffmpeg(`-f lavfi -i color=c=black:s=320x240:d=0.1 -c:v ${encoder} -f null -`);
|
|
15162
|
+
logger.debug(`[BiliDanmaku] ${encoder} 测试结果: status=${result.status}`);
|
|
15163
|
+
if (result.status) {
|
|
15164
|
+
cachedEncoders$1[codec] = encoder;
|
|
15165
|
+
logger.info(`[BiliDanmaku] 使用 ${codec.toUpperCase()} 编码器: ${encoder}`);
|
|
15166
|
+
return encoder;
|
|
15167
|
+
}
|
|
15168
|
+
} catch (e) {
|
|
15169
|
+
logger.debug(`[BiliDanmaku] 编码器 ${encoder} 测试异常: ${e}`);
|
|
15170
|
+
}
|
|
15171
|
+
}
|
|
15172
|
+
const fallback = SOFTWARE_FALLBACK$1[codec];
|
|
15173
|
+
cachedEncoders$1[codec] = fallback;
|
|
15174
|
+
logger.info(`[BiliDanmaku] 回退到软件编码器: ${fallback}`);
|
|
15175
|
+
return fallback;
|
|
15176
|
+
}
|
|
15177
|
+
async function getVideoBitrate$1(path$1) {
|
|
15178
|
+
try {
|
|
15179
|
+
const fileSize = fs.statSync(path$1).size;
|
|
15180
|
+
const { stdout } = await ffprobe(`-v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 "${path$1}"`);
|
|
15181
|
+
const duration = parseFloat(stdout.trim());
|
|
15182
|
+
if (duration > 0 && fileSize > 0) {
|
|
15183
|
+
const kbps = Math.round(fileSize * 8 / duration / 1e3);
|
|
15184
|
+
logger.debug(`[BiliDanmaku] 通过文件大小计算码率: ${kbps}kbps`);
|
|
15185
|
+
return kbps;
|
|
15186
|
+
}
|
|
15187
|
+
} catch (e) {
|
|
15188
|
+
logger.debug(`[BiliDanmaku] 通过文件大小计算码率失败: ${e}`);
|
|
15189
|
+
}
|
|
15190
|
+
try {
|
|
15191
|
+
const { stdout } = await ffprobe(`-v error -select_streams v:0 -show_entries stream=bit_rate -of default=noprint_wrappers=1:nokey=1 "${path$1}"`);
|
|
15192
|
+
const bitrate = parseInt(stdout.trim());
|
|
15193
|
+
if (bitrate > 0) return Math.round(bitrate / 1e3);
|
|
15194
|
+
} catch {}
|
|
15195
|
+
try {
|
|
15196
|
+
const { stdout } = await ffprobe(`-v error -show_entries format=bit_rate -of default=noprint_wrappers=1:nokey=1 "${path$1}"`);
|
|
15197
|
+
const bitrate = parseInt(stdout.trim());
|
|
15198
|
+
if (bitrate > 0) return Math.round(bitrate / 1e3);
|
|
15199
|
+
} catch {}
|
|
15200
|
+
logger.warn("[BiliDanmaku] 无法获取视频码率,将使用 CRF 模式");
|
|
15201
|
+
return 0;
|
|
15202
|
+
}
|
|
15203
|
+
function getEncoderParams$1(encoder, targetBitrate) {
|
|
15204
|
+
const threads = Math.max(1, Math.floor(os.cpus().length / 2));
|
|
15205
|
+
if (targetBitrate && targetBitrate > 0) {
|
|
15206
|
+
const bitrateK = `${targetBitrate}k`;
|
|
15207
|
+
const maxrate = `${Math.round(targetBitrate * 2)}k`;
|
|
15208
|
+
const bufsize = `${Math.round(targetBitrate * 4)}k`;
|
|
15209
|
+
if (encoder === "h264_nvenc") return `-c:v h264_nvenc -preset p4 -rc vbr -b:v ${bitrateK} -maxrate ${maxrate} -bufsize ${bufsize}`;
|
|
15210
|
+
if (encoder === "h264_qsv") return `-c:v h264_qsv -preset medium -b:v ${bitrateK} -maxrate ${maxrate} -bufsize ${bufsize}`;
|
|
15211
|
+
if (encoder === "h264_amf") return `-c:v h264_amf -quality balanced -rc vbr_peak -b:v ${bitrateK} -maxrate ${maxrate}`;
|
|
15212
|
+
if (encoder === "libx264") return `-c:v libx264 -preset medium -b:v ${bitrateK} -maxrate ${maxrate} -bufsize ${bufsize} -threads ${threads}`;
|
|
15213
|
+
if (encoder === "hevc_nvenc") return `-c:v hevc_nvenc -preset p4 -rc vbr -b:v ${bitrateK} -maxrate ${maxrate} -bufsize ${bufsize}`;
|
|
15214
|
+
if (encoder === "hevc_qsv") return `-c:v hevc_qsv -preset medium -b:v ${bitrateK} -maxrate ${maxrate} -bufsize ${bufsize}`;
|
|
15215
|
+
if (encoder === "hevc_amf") return `-c:v hevc_amf -quality balanced -rc vbr_peak -b:v ${bitrateK} -maxrate ${maxrate}`;
|
|
15216
|
+
if (encoder === "libx265") return `-c:v libx265 -preset medium -b:v ${bitrateK} -maxrate ${maxrate} -bufsize ${bufsize} -threads ${threads}`;
|
|
15217
|
+
if (encoder === "av1_nvenc") return `-c:v av1_nvenc -preset p4 -rc vbr -b:v ${bitrateK} -maxrate ${maxrate} -bufsize ${bufsize}`;
|
|
15218
|
+
if (encoder === "av1_qsv") return `-c:v av1_qsv -preset medium -b:v ${bitrateK} -maxrate ${maxrate} -bufsize ${bufsize}`;
|
|
15219
|
+
if (encoder === "av1_amf") return `-c:v av1_amf -quality balanced -rc vbr_peak -b:v ${bitrateK} -maxrate ${maxrate}`;
|
|
15220
|
+
if (encoder === "libsvtav1") return `-c:v libsvtav1 -preset 6 -b:v ${bitrateK} -maxrate ${maxrate} -bufsize ${bufsize} -threads ${threads}`;
|
|
15221
|
+
if (encoder === "libaom-av1") return `-c:v libaom-av1 -cpu-used 4 -b:v ${bitrateK} -maxrate ${maxrate} -bufsize ${bufsize} -threads ${threads}`;
|
|
15222
|
+
return `-c:v libx265 -preset medium -b:v ${bitrateK} -maxrate ${maxrate} -bufsize ${bufsize} -threads ${threads}`;
|
|
15223
|
+
}
|
|
15224
|
+
if (encoder === "h264_nvenc") return "-c:v h264_nvenc -preset p4 -rc vbr -cq 23";
|
|
15225
|
+
if (encoder === "h264_qsv") return "-c:v h264_qsv -preset medium -global_quality 23";
|
|
15226
|
+
if (encoder === "h264_amf") return "-c:v h264_amf -quality balanced -rc cqp -qp_i 23 -qp_p 23";
|
|
15227
|
+
if (encoder === "libx264") return `-c:v libx264 -crf 23 -preset medium -threads ${threads}`;
|
|
15228
|
+
if (encoder === "hevc_nvenc") return "-c:v hevc_nvenc -preset p4 -rc vbr -cq 28";
|
|
15229
|
+
if (encoder === "hevc_qsv") return "-c:v hevc_qsv -preset medium -global_quality 28";
|
|
15230
|
+
if (encoder === "hevc_amf") return "-c:v hevc_amf -quality balanced -rc cqp -qp_i 28 -qp_p 28";
|
|
15231
|
+
if (encoder === "libx265") return `-c:v libx265 -crf 28 -preset medium -threads ${threads}`;
|
|
15232
|
+
if (encoder === "av1_nvenc") return "-c:v av1_nvenc -preset p4 -rc vbr -cq 30";
|
|
15233
|
+
if (encoder === "av1_qsv") return "-c:v av1_qsv -preset medium -global_quality 30";
|
|
15234
|
+
if (encoder === "av1_amf") return "-c:v av1_amf -quality balanced -rc cqp -qp_i 30 -qp_p 30";
|
|
15235
|
+
if (encoder === "libsvtav1") return `-c:v libsvtav1 -crf 30 -preset 6 -threads ${threads}`;
|
|
15236
|
+
if (encoder === "libaom-av1") return `-c:v libaom-av1 -crf 30 -cpu-used 4 -threads ${threads}`;
|
|
15237
|
+
return `-c:v libx265 -crf 28 -preset medium -threads ${threads}`;
|
|
15238
|
+
}
|
|
15239
|
+
var toASSColor = (color) => {
|
|
15240
|
+
const r = color >> 16 & 255;
|
|
15241
|
+
const g = color >> 8 & 255;
|
|
15242
|
+
return `${(color & 255).toString(16).padStart(2, "0")}${g.toString(16).padStart(2, "0")}${r.toString(16).padStart(2, "0")}`.toUpperCase();
|
|
15243
|
+
};
|
|
15244
|
+
var toASSTime$1 = (ms) => {
|
|
15245
|
+
const s = ms / 1e3;
|
|
15246
|
+
const h = Math.floor(s / 3600);
|
|
15247
|
+
const m = Math.floor(s % 3600 / 60);
|
|
15248
|
+
const sec = Math.floor(s % 60);
|
|
15249
|
+
const cs = Math.floor(s % 1 * 100);
|
|
15250
|
+
return `${h}:${m.toString().padStart(2, "0")}:${sec.toString().padStart(2, "0")}.${cs.toString().padStart(2, "0")}`;
|
|
15251
|
+
};
|
|
15252
|
+
var estimateWidth$1 = (text, fontSize) => {
|
|
15253
|
+
let w = 0;
|
|
15254
|
+
for (const c of text) w += c.charCodeAt(0) > 127 ? fontSize : fontSize * .5;
|
|
15255
|
+
return w;
|
|
15256
|
+
};
|
|
15257
|
+
var escapeASS$1 = (text) => text.replace(/\\/g, "\\\\").replace(/\{/g, "\\{").replace(/\}/g, "\\}").replace(/\n/g, "\\N");
|
|
15258
|
+
var escapeWinPath$1 = (path$1) => path$1.replace(/\\/g, "/").replace(/:/g, "\\:");
|
|
15259
|
+
var isLandscape$1 = (w, h) => w > h;
|
|
15260
|
+
async function getBiliResolution(path$1) {
|
|
15261
|
+
try {
|
|
15262
|
+
const { stdout } = await ffprobe(`-v error -select_streams v:0 -show_entries stream=width,height -of csv=s=x:p=0 "${path$1}"`);
|
|
15263
|
+
const [w, h] = stdout.trim().split("x").map(Number);
|
|
15264
|
+
if (w && h) return {
|
|
15265
|
+
width: w,
|
|
15266
|
+
height: h
|
|
15267
|
+
};
|
|
15268
|
+
} catch {}
|
|
15269
|
+
try {
|
|
15270
|
+
const match = ((await ffmpeg(`-i "${path$1}" -f null -`, { timeout: 5e3 })).stderr || "").match(/(\d{3,4})x(\d{3,4})/);
|
|
15271
|
+
if (match) return {
|
|
15272
|
+
width: parseInt(match[1]),
|
|
15273
|
+
height: parseInt(match[2])
|
|
15274
|
+
};
|
|
15275
|
+
} catch {}
|
|
15276
|
+
return {
|
|
15277
|
+
width: 1920,
|
|
15278
|
+
height: 1080
|
|
15279
|
+
};
|
|
15280
|
+
}
|
|
15281
|
+
async function getBiliFrameRate(path$1) {
|
|
15282
|
+
try {
|
|
15283
|
+
const { stdout } = await ffprobe(`-v error -select_streams v:0 -show_entries stream=r_frame_rate -of default=noprint_wrappers=1:nokey=1 "${path$1}"`);
|
|
15284
|
+
const [num, den] = stdout.trim().split("/").map(Number);
|
|
15285
|
+
if (den > 0) return num / den;
|
|
15286
|
+
} catch {}
|
|
15287
|
+
try {
|
|
15288
|
+
const stderr = (await ffmpeg(`-i "${path$1}" -f null -`, { timeout: 5e3 })).stderr || "";
|
|
15289
|
+
const fpsMatch = stderr.match(/(\d+(?:\.\d+)?)\s*fps/);
|
|
15290
|
+
if (fpsMatch) return parseFloat(fpsMatch[1]);
|
|
15291
|
+
const fracMatch = stderr.match(/(\d+)\/(\d+)\s*fps/);
|
|
15292
|
+
if (fracMatch) return parseInt(fracMatch[1]) / parseInt(fracMatch[2]);
|
|
15293
|
+
} catch {}
|
|
15294
|
+
return 30;
|
|
15295
|
+
}
|
|
15296
|
+
var FONT_SIZE_MAP$1 = {
|
|
15297
|
+
small: {
|
|
15298
|
+
base: 25,
|
|
15299
|
+
trackH: 30
|
|
15300
|
+
},
|
|
15301
|
+
medium: {
|
|
15302
|
+
base: 32,
|
|
15303
|
+
trackH: 38
|
|
15304
|
+
},
|
|
15305
|
+
large: {
|
|
15306
|
+
base: 40,
|
|
15307
|
+
trackH: 46
|
|
15308
|
+
}
|
|
15309
|
+
};
|
|
15310
|
+
function generateBiliASS(danmakuList, width, height, options = {}) {
|
|
15311
|
+
const { scrollTime = 8, danmakuOpacity = 70, fontName = "Microsoft YaHei", danmakuArea = .5, danmakuFontSize = "medium" } = options;
|
|
15312
|
+
const fontScale = height / 1080;
|
|
15313
|
+
const sizeConfig = FONT_SIZE_MAP$1[danmakuFontSize];
|
|
15314
|
+
const fontSize = Math.round(sizeConfig.base * fontScale);
|
|
15315
|
+
const trackH = Math.round(sizeConfig.trackH * fontScale);
|
|
15316
|
+
const topMargin = Math.round(10 * fontScale);
|
|
15317
|
+
const bottomMargin = Math.round(10 * fontScale);
|
|
15318
|
+
const areaHeight = Math.floor(height * danmakuArea) - topMargin - bottomMargin;
|
|
15319
|
+
const trackCount = Math.max(1, Math.floor((areaHeight - fontSize) / trackH));
|
|
15320
|
+
const fixedTrackCount = trackCount;
|
|
15321
|
+
const minGap = Math.round(10 * fontScale);
|
|
15322
|
+
const alpha = Math.round((100 - Math.max(0, Math.min(100, danmakuOpacity))) * 2.55).toString(16).padStart(2, "0").toUpperCase();
|
|
15323
|
+
let ass = `[Script Info]\nTitle: Bilibili Danmaku\nScriptType: v4.00+\nPlayResX: ${width}\nPlayResY: ${height}\nTimer: 100.0000\n\n[V4+ Styles]\nFormat: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding\nStyle: Scroll,${fontName},${fontSize},&H${alpha}FFFFFF,&H${alpha}FFFFFF,&H${alpha}000000,&H${alpha}000000,0,0,0,0,100,100,0,0,1,1,0,2,0,0,0,1\nStyle: Top,${fontName},${fontSize},&H${alpha}FFFFFF,&H${alpha}FFFFFF,&H${alpha}000000,&H${alpha}000000,0,0,0,0,100,100,0,0,1,1,0,8,0,0,0,1\nStyle: Bottom,${fontName},${fontSize},&H${alpha}FFFFFF,&H${alpha}FFFFFF,&H${alpha}000000,&H${alpha}000000,0,0,0,0,100,100,0,0,1,1,0,2,0,0,0,1\n\n[Events]\nFormat: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text\n`;
|
|
15324
|
+
const scrollTracks = Array(trackCount).fill(null);
|
|
15325
|
+
const topTracks = Array(fixedTrackCount).fill(0);
|
|
15326
|
+
const bottomTracks = Array(fixedTrackCount).fill(0);
|
|
15327
|
+
const calcDistance = (last, startTime, duration, textWidth) => {
|
|
15328
|
+
const lastSpeed = (width + last.textWidth) / last.duration;
|
|
15329
|
+
const newSpeed = (width + textWidth) / duration;
|
|
15330
|
+
let dist = width - (width - lastSpeed * (startTime - last.startTime) + last.textWidth) - minGap;
|
|
15331
|
+
if (newSpeed > lastSpeed) {
|
|
15332
|
+
const lastRightXAtEnd = width - lastSpeed * (startTime + duration - last.startTime) + last.textWidth;
|
|
15333
|
+
dist = Math.min(dist, -textWidth - lastRightXAtEnd - minGap);
|
|
15334
|
+
}
|
|
15335
|
+
return dist;
|
|
15336
|
+
};
|
|
15337
|
+
const sorted = [...danmakuList].sort((a, b) => a.progress - b.progress);
|
|
15338
|
+
for (const dm of sorted) {
|
|
15339
|
+
if (dm.mode > 5 || !dm.content.trim()) continue;
|
|
15340
|
+
const startTime = dm.progress;
|
|
15341
|
+
const dmSizeRatio = (dm.fontsize || 25) / 25;
|
|
15342
|
+
const dmFontSize = Math.round(fontSize * dmSizeRatio);
|
|
15343
|
+
const textWidth = estimateWidth$1(dm.content, dmFontSize);
|
|
15344
|
+
const content = escapeASS$1(dm.content);
|
|
15345
|
+
const colorTag = dm.color !== 16777215 ? `{\\c&H${toASSColor(dm.color)}&}` : "";
|
|
15346
|
+
const sizeTag = dmFontSize !== fontSize ? `{\\fs${dmFontSize}}` : "";
|
|
15347
|
+
if (dm.mode === 4) {
|
|
15348
|
+
const endTime = startTime + 4e3;
|
|
15349
|
+
let idx = bottomTracks.findIndex((t) => t <= startTime);
|
|
15350
|
+
if (idx === -1) idx = Math.floor(Math.random() * bottomTracks.length);
|
|
15351
|
+
bottomTracks[idx] = endTime;
|
|
15352
|
+
const y = height - bottomMargin - idx * trackH;
|
|
15353
|
+
ass += `Dialogue: 0,${toASSTime$1(startTime)},${toASSTime$1(endTime)},Bottom,,0,0,0,,{\\an2}${colorTag}${sizeTag}{\\pos(${width / 2},${y})}${content}\n`;
|
|
15354
|
+
} else if (dm.mode === 5) {
|
|
15355
|
+
const endTime = startTime + 4e3;
|
|
15356
|
+
let idx = topTracks.findIndex((t) => t <= startTime);
|
|
15357
|
+
if (idx === -1) idx = Math.floor(Math.random() * topTracks.length);
|
|
15358
|
+
topTracks[idx] = endTime;
|
|
15359
|
+
const y = topMargin + idx * trackH + fontSize;
|
|
15360
|
+
ass += `Dialogue: 0,${toASSTime$1(startTime)},${toASSTime$1(endTime)},Top,,0,0,0,,{\\an8}${colorTag}${sizeTag}{\\pos(${width / 2},${y})}${content}\n`;
|
|
15361
|
+
} else {
|
|
15362
|
+
const duration = scrollTime * 1e3;
|
|
15363
|
+
const endTime = startTime + duration;
|
|
15364
|
+
for (let i = 0; i < scrollTracks.length; i++) {
|
|
15365
|
+
const t = scrollTracks[i];
|
|
15366
|
+
if (t && t.startTime + t.duration <= startTime) scrollTracks[i] = null;
|
|
15367
|
+
}
|
|
15368
|
+
let bestIdx = -1;
|
|
15369
|
+
let bestDist = -Infinity;
|
|
15370
|
+
for (let i = 0; i < scrollTracks.length; i++) {
|
|
15371
|
+
const t = scrollTracks[i];
|
|
15372
|
+
if (!t) {
|
|
15373
|
+
if (bestIdx === -1) bestIdx = i;
|
|
15374
|
+
continue;
|
|
15375
|
+
}
|
|
15376
|
+
const d = calcDistance(t, startTime, duration, textWidth);
|
|
15377
|
+
if (d >= 0) {
|
|
15378
|
+
if (bestDist < 0 || d < bestDist) {
|
|
15379
|
+
bestDist = d;
|
|
15380
|
+
bestIdx = i;
|
|
15381
|
+
}
|
|
15382
|
+
}
|
|
15676
15383
|
}
|
|
15384
|
+
if (bestIdx === -1 || bestDist < 0 && scrollTracks[bestIdx] !== null) continue;
|
|
15385
|
+
scrollTracks[bestIdx] = {
|
|
15386
|
+
startTime,
|
|
15387
|
+
duration,
|
|
15388
|
+
textWidth
|
|
15389
|
+
};
|
|
15390
|
+
const y = (bestIdx + 1) * trackH;
|
|
15391
|
+
ass += `Dialogue: 0,${toASSTime$1(startTime)},${toASSTime$1(endTime)},Scroll,,0,0,0,,{\\an7}${colorTag}${sizeTag}{\\move(${width},${y},${-textWidth},${y})}${content}\n`;
|
|
15677
15392
|
}
|
|
15678
15393
|
}
|
|
15679
|
-
return
|
|
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站评论解析结果",
|
|
@@ -17780,20 +17584,70 @@ var Bilibilipush = class extends Base {
|
|
|
17780
17584
|
break;
|
|
17781
17585
|
case "DYNAMIC_TYPE_DRAW": {
|
|
17782
17586
|
const imgArray = [];
|
|
17587
|
+
const temp = [];
|
|
17783
17588
|
const title = data$1[dynamicId].Dynamic_Data.modules.module_dynamic.major?.opus?.title || "bilibili_dynamic";
|
|
17784
17589
|
const images = data$1[dynamicId].Dynamic_Data.modules.module_dynamic.major && data$1[dynamicId].Dynamic_Data.modules.module_dynamic?.major?.draw?.items || data$1[dynamicId].Dynamic_Data.modules.module_dynamic?.major?.opus.pics;
|
|
17785
17590
|
if (images.length === 0) break;
|
|
17786
17591
|
for (const [index, img2] of images.entries()) {
|
|
17787
|
-
const
|
|
17788
|
-
|
|
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,121 +17961,428 @@ const douyinComments = async (data$1, emojidata) => {
|
|
|
18108
17961
|
CommentsData: [],
|
|
18109
17962
|
image_url: []
|
|
18110
17963
|
};
|
|
18111
|
-
let id = 1;
|
|
18112
|
-
for (const comment of data$1.data.comments) {
|
|
18113
|
-
const cid = comment.cid;
|
|
18114
|
-
const aweme_id = comment.aweme_id;
|
|
18115
|
-
const nickname = comment.user.nickname;
|
|
18116
|
-
const userimageurl = comment.user.avatar_thumb.url_list[0];
|
|
18117
|
-
let text = comment.text;
|
|
18118
|
-
const ip = comment.ip_label ?? "未知";
|
|
18119
|
-
const time = comment.create_time;
|
|
18120
|
-
const label_type = comment.label_type ?? -1;
|
|
18121
|
-
const sticker = comment.sticker ? comment.sticker.animate_url.url_list[0] : null;
|
|
18122
|
-
let digg_count = comment.digg_count;
|
|
18123
|
-
const imageurl = comment.image_list && comment.image_list?.[0] && comment.image_list?.[0].origin_url && comment.image_list?.[0].origin_url.url_list ? comment.image_list?.[0].origin_url.url_list[0] : null;
|
|
18124
|
-
const status_label = comment.label_list?.[0]?.text ?? null;
|
|
18125
|
-
const userintextlongid = comment.text_extra && comment.text_extra[0] && comment.text_extra[0].sec_uid ? comment.text_extra.map((extra) => extra.sec_uid) : null;
|
|
18126
|
-
const search_text = comment.text_extra && comment.text_extra[0] && comment.text_extra[0].search_text ? comment.text_extra[0].search_text && comment.text_extra.map((extra) => ({
|
|
18127
|
-
search_text: extra.search_text,
|
|
18128
|
-
search_query_id: extra.search_query_id
|
|
18129
|
-
})) : null;
|
|
18130
|
-
const relativeTime = getRelativeTimeFromTimestamp$2(time);
|
|
18131
|
-
text = processTextFormatting(text);
|
|
18132
|
-
text = await processAtUsers$1(text, userintextlongid);
|
|
18133
|
-
text = processCommentEmojis$1(text, emojidata);
|
|
18134
|
-
const processedImageUrl = await processCommentImage(imageurl);
|
|
18135
|
-
if (processedImageUrl) imageUrls.push(processedImageUrl.startsWith("data:image/jpeg;base64,") ? `base64://${processedImageUrl.replace("data:image/jpeg;base64,", "")}` : processedImageUrl);
|
|
18136
|
-
if (sticker) imageUrls.push(sticker);
|
|
18137
|
-
if (digg_count > 1e4) digg_count = (digg_count / 1e4).toFixed(1) + "w";
|
|
18138
|
-
const replyComment = await douyinFetcher.fetchCommentReplies({
|
|
18139
|
-
aweme_id,
|
|
18140
|
-
comment_id: cid,
|
|
18141
|
-
typeMode: "strict",
|
|
18142
|
-
number: Config.douyin.subCommentLimit
|
|
18143
|
-
});
|
|
18144
|
-
const replyCommentsList = [];
|
|
18145
|
-
if (replyComment.data.comments && replyComment.data.comments.length > 0) for (const reply of replyComment.data.comments) {
|
|
18146
|
-
const replyItem = reply;
|
|
18147
|
-
const replyUserintextlongid = replyItem.text_extra && replyItem.text_extra[0] && replyItem.text_extra[0].sec_uid ? replyItem.text_extra.filter((extra) => extra.sec_uid).map((extra) => extra.sec_uid) : null;
|
|
18148
|
-
const processedReplyText = await processAtUsers$1(replyItem.text, replyUserintextlongid);
|
|
18149
|
-
const replyImageUrl = replyItem.image_list?.[0]?.origin_url?.url_list?.[0];
|
|
18150
|
-
const replyStickerUrl = replyItem.sticker?.animate_url?.url_list?.[0];
|
|
18151
|
-
let replyImageList = null;
|
|
18152
|
-
if (replyImageUrl) {
|
|
18153
|
-
const processedReplyImage = await processCommentImage(replyImageUrl);
|
|
18154
|
-
if (processedReplyImage) {
|
|
18155
|
-
replyImageList = [processedReplyImage];
|
|
18156
|
-
imageUrls.push(processedReplyImage.startsWith("data:image/jpeg;base64,") ? `base64://${processedReplyImage.replace("data:image/jpeg;base64,", "")}` : processedReplyImage);
|
|
17964
|
+
let id = 1;
|
|
17965
|
+
for (const comment of data$1.data.comments) {
|
|
17966
|
+
const cid = comment.cid;
|
|
17967
|
+
const aweme_id = comment.aweme_id;
|
|
17968
|
+
const nickname = comment.user.nickname;
|
|
17969
|
+
const userimageurl = comment.user.avatar_thumb.url_list[0];
|
|
17970
|
+
let text = comment.text;
|
|
17971
|
+
const ip = comment.ip_label ?? "未知";
|
|
17972
|
+
const time = comment.create_time;
|
|
17973
|
+
const label_type = comment.label_type ?? -1;
|
|
17974
|
+
const sticker = comment.sticker ? comment.sticker.animate_url.url_list[0] : null;
|
|
17975
|
+
let digg_count = comment.digg_count;
|
|
17976
|
+
const imageurl = comment.image_list && comment.image_list?.[0] && comment.image_list?.[0].origin_url && comment.image_list?.[0].origin_url.url_list ? comment.image_list?.[0].origin_url.url_list[0] : null;
|
|
17977
|
+
const status_label = comment.label_list?.[0]?.text ?? null;
|
|
17978
|
+
const userintextlongid = comment.text_extra && comment.text_extra[0] && comment.text_extra[0].sec_uid ? comment.text_extra.map((extra) => extra.sec_uid) : null;
|
|
17979
|
+
const search_text = comment.text_extra && comment.text_extra[0] && comment.text_extra[0].search_text ? comment.text_extra[0].search_text && comment.text_extra.map((extra) => ({
|
|
17980
|
+
search_text: extra.search_text,
|
|
17981
|
+
search_query_id: extra.search_query_id
|
|
17982
|
+
})) : null;
|
|
17983
|
+
const relativeTime = getRelativeTimeFromTimestamp$2(time);
|
|
17984
|
+
text = processTextFormatting(text);
|
|
17985
|
+
text = await processAtUsers$1(text, userintextlongid);
|
|
17986
|
+
text = processCommentEmojis$1(text, emojidata);
|
|
17987
|
+
const processedImageUrl = await processCommentImage(imageurl);
|
|
17988
|
+
if (processedImageUrl) imageUrls.push(processedImageUrl.startsWith("data:image/jpeg;base64,") ? `base64://${processedImageUrl.replace("data:image/jpeg;base64,", "")}` : processedImageUrl);
|
|
17989
|
+
if (sticker) imageUrls.push(sticker);
|
|
17990
|
+
if (digg_count > 1e4) digg_count = (digg_count / 1e4).toFixed(1) + "w";
|
|
17991
|
+
const replyComment = await douyinFetcher.fetchCommentReplies({
|
|
17992
|
+
aweme_id,
|
|
17993
|
+
comment_id: cid,
|
|
17994
|
+
typeMode: "strict",
|
|
17995
|
+
number: Config.douyin.subCommentLimit
|
|
17996
|
+
});
|
|
17997
|
+
const replyCommentsList = [];
|
|
17998
|
+
if (replyComment.data.comments && replyComment.data.comments.length > 0) for (const reply of replyComment.data.comments) {
|
|
17999
|
+
const replyItem = reply;
|
|
18000
|
+
const replyUserintextlongid = replyItem.text_extra && replyItem.text_extra[0] && replyItem.text_extra[0].sec_uid ? replyItem.text_extra.filter((extra) => extra.sec_uid).map((extra) => extra.sec_uid) : null;
|
|
18001
|
+
const processedReplyText = await processAtUsers$1(replyItem.text, replyUserintextlongid);
|
|
18002
|
+
const replyImageUrl = replyItem.image_list?.[0]?.origin_url?.url_list?.[0];
|
|
18003
|
+
const replyStickerUrl = replyItem.sticker?.animate_url?.url_list?.[0];
|
|
18004
|
+
let replyImageList = null;
|
|
18005
|
+
if (replyImageUrl) {
|
|
18006
|
+
const processedReplyImage = await processCommentImage(replyImageUrl);
|
|
18007
|
+
if (processedReplyImage) {
|
|
18008
|
+
replyImageList = [processedReplyImage];
|
|
18009
|
+
imageUrls.push(processedReplyImage.startsWith("data:image/jpeg;base64,") ? `base64://${processedReplyImage.replace("data:image/jpeg;base64,", "")}` : processedReplyImage);
|
|
18010
|
+
}
|
|
18011
|
+
} else if (replyStickerUrl) {
|
|
18012
|
+
replyImageList = [replyStickerUrl];
|
|
18013
|
+
imageUrls.push(replyStickerUrl);
|
|
18014
|
+
}
|
|
18015
|
+
replyCommentsList.push({
|
|
18016
|
+
create_time: getRelativeTimeFromTimestamp$2(replyItem.create_time),
|
|
18017
|
+
nickname: replyItem.user.nickname,
|
|
18018
|
+
userimageurl: replyItem.user.avatar_thumb.url_list[0],
|
|
18019
|
+
text: processCommentEmojis$1(processedReplyText, emojidata),
|
|
18020
|
+
digg_count: replyItem.digg_count > 1e4 ? (replyItem.digg_count / 1e4).toFixed(1) + "w" : replyItem.digg_count,
|
|
18021
|
+
ip_label: replyItem.ip_label,
|
|
18022
|
+
text_extra: replyItem.text_extra,
|
|
18023
|
+
label_text: replyItem.label_text,
|
|
18024
|
+
image_list: replyImageList,
|
|
18025
|
+
cid: replyItem.cid,
|
|
18026
|
+
reply_to_reply_id: replyItem.reply_to_reply_id,
|
|
18027
|
+
reply_to_username: replyItem.reply_to_username
|
|
18028
|
+
});
|
|
18029
|
+
}
|
|
18030
|
+
const commentObj = {
|
|
18031
|
+
id: id++,
|
|
18032
|
+
replyComment: replyCommentsList.length > 0 ? replyCommentsList : void 0,
|
|
18033
|
+
cid,
|
|
18034
|
+
aweme_id,
|
|
18035
|
+
nickname,
|
|
18036
|
+
userimageurl,
|
|
18037
|
+
text,
|
|
18038
|
+
digg_count,
|
|
18039
|
+
ip_label: ip,
|
|
18040
|
+
create_time: relativeTime,
|
|
18041
|
+
commentimage: processedImageUrl ?? void 0,
|
|
18042
|
+
label_type,
|
|
18043
|
+
sticker: sticker ?? void 0,
|
|
18044
|
+
status_label: status_label ?? void 0,
|
|
18045
|
+
is_At_user_id: userintextlongid,
|
|
18046
|
+
search_text,
|
|
18047
|
+
is_author_digged: comment.is_author_digged ?? false
|
|
18048
|
+
};
|
|
18049
|
+
jsonArray.push(commentObj);
|
|
18050
|
+
}
|
|
18051
|
+
jsonArray.sort((a, b) => {
|
|
18052
|
+
const aCount = typeof a.digg_count === "string" && a.digg_count.includes("w") ? parseFloat(a.digg_count) * 1e4 : typeof a.digg_count === "number" ? a.digg_count : 0;
|
|
18053
|
+
return (typeof b.digg_count === "string" && b.digg_count.includes("w") ? parseFloat(b.digg_count) * 1e4 : typeof b.digg_count === "number" ? b.digg_count : 0) - aCount;
|
|
18054
|
+
});
|
|
18055
|
+
const indexLabelTypeOne = jsonArray.findIndex((comment) => comment.label_type === 1);
|
|
18056
|
+
if (indexLabelTypeOne !== -1) {
|
|
18057
|
+
const commentTypeOne = jsonArray.splice(indexLabelTypeOne, 1)[0];
|
|
18058
|
+
jsonArray.unshift(commentTypeOne);
|
|
18059
|
+
}
|
|
18060
|
+
return {
|
|
18061
|
+
CommentsData: jsonArray,
|
|
18062
|
+
image_url: imageUrls
|
|
18063
|
+
};
|
|
18064
|
+
};
|
|
18065
|
+
var getRelativeTimeFromTimestamp$2 = (timestamp) => {
|
|
18066
|
+
const commentDate = fromUnixTime(timestamp);
|
|
18067
|
+
const diffSeconds = differenceInSeconds(/* @__PURE__ */ new Date(), commentDate);
|
|
18068
|
+
if (diffSeconds < 30) return "刚刚";
|
|
18069
|
+
if (diffSeconds < 7776e3) return formatDistanceToNow(commentDate, {
|
|
18070
|
+
locale: zhCN,
|
|
18071
|
+
addSuffix: true
|
|
18072
|
+
});
|
|
18073
|
+
return format(commentDate, "yyyy-MM-dd");
|
|
18074
|
+
};
|
|
18075
|
+
await init_utils$1();
|
|
18076
|
+
var ENCODER_PRIORITY = {
|
|
18077
|
+
h264: [
|
|
18078
|
+
"h264_nvenc",
|
|
18079
|
+
"h264_qsv",
|
|
18080
|
+
"h264_amf",
|
|
18081
|
+
"libx264"
|
|
18082
|
+
],
|
|
18083
|
+
h265: [
|
|
18084
|
+
"hevc_nvenc",
|
|
18085
|
+
"hevc_qsv",
|
|
18086
|
+
"hevc_amf",
|
|
18087
|
+
"libx265"
|
|
18088
|
+
],
|
|
18089
|
+
av1: [
|
|
18090
|
+
"av1_nvenc",
|
|
18091
|
+
"av1_qsv",
|
|
18092
|
+
"av1_amf",
|
|
18093
|
+
"libsvtav1",
|
|
18094
|
+
"libaom-av1"
|
|
18095
|
+
]
|
|
18096
|
+
};
|
|
18097
|
+
var SOFTWARE_FALLBACK = {
|
|
18098
|
+
h264: "libx264",
|
|
18099
|
+
h265: "libx265",
|
|
18100
|
+
av1: "libsvtav1"
|
|
18101
|
+
};
|
|
18102
|
+
var cachedEncoders = {};
|
|
18103
|
+
async function detectEncoder(codec) {
|
|
18104
|
+
if (cachedEncoders[codec]) return cachedEncoders[codec];
|
|
18105
|
+
logger.debug(`[DouyinDanmaku] 开始检测 ${codec.toUpperCase()} 编码器...`);
|
|
18106
|
+
for (const encoder of ENCODER_PRIORITY[codec]) try {
|
|
18107
|
+
if ((await ffmpeg(`-f lavfi -i color=c=black:s=320x240:d=0.1 -c:v ${encoder} -f null -`)).status) {
|
|
18108
|
+
cachedEncoders[codec] = encoder;
|
|
18109
|
+
logger.info(`[DouyinDanmaku] 使用 ${codec.toUpperCase()} 编码器: ${encoder}`);
|
|
18110
|
+
return encoder;
|
|
18111
|
+
}
|
|
18112
|
+
} catch {}
|
|
18113
|
+
const fallback = SOFTWARE_FALLBACK[codec];
|
|
18114
|
+
cachedEncoders[codec] = fallback;
|
|
18115
|
+
logger.info(`[DouyinDanmaku] 回退到软件编码器: ${fallback}`);
|
|
18116
|
+
return fallback;
|
|
18117
|
+
}
|
|
18118
|
+
async function getVideoBitrate(path$1) {
|
|
18119
|
+
try {
|
|
18120
|
+
const fileSize = fs.statSync(path$1).size;
|
|
18121
|
+
const { stdout } = await ffprobe(`-v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 "${path$1}"`);
|
|
18122
|
+
const duration = parseFloat(stdout.trim());
|
|
18123
|
+
if (duration > 0 && fileSize > 0) return Math.round(fileSize * 8 / duration / 1e3);
|
|
18124
|
+
} catch {}
|
|
18125
|
+
try {
|
|
18126
|
+
const { stdout } = await ffprobe(`-v error -select_streams v:0 -show_entries stream=bit_rate -of default=noprint_wrappers=1:nokey=1 "${path$1}"`);
|
|
18127
|
+
const bitrate = parseInt(stdout.trim());
|
|
18128
|
+
if (bitrate > 0) return Math.round(bitrate / 1e3);
|
|
18129
|
+
} catch {}
|
|
18130
|
+
return 0;
|
|
18131
|
+
}
|
|
18132
|
+
function getEncoderParams(encoder, targetBitrate) {
|
|
18133
|
+
const threads = Math.max(1, Math.floor(os.cpus().length / 2));
|
|
18134
|
+
if (targetBitrate && targetBitrate > 0) {
|
|
18135
|
+
const adjustedBitrate = Math.round(targetBitrate * 1.4);
|
|
18136
|
+
const bitrateK = `${adjustedBitrate}k`;
|
|
18137
|
+
const maxrate = `${Math.round(adjustedBitrate * 2.5)}k`;
|
|
18138
|
+
const bufsize = `${Math.round(adjustedBitrate * 4)}k`;
|
|
18139
|
+
if (encoder === "h264_nvenc") return `-c:v h264_nvenc -preset p4 -rc vbr -b:v ${bitrateK} -maxrate ${maxrate} -bufsize ${bufsize}`;
|
|
18140
|
+
if (encoder === "h264_qsv") return `-c:v h264_qsv -preset medium -b:v ${bitrateK} -maxrate ${maxrate} -bufsize ${bufsize}`;
|
|
18141
|
+
if (encoder === "h264_amf") return `-c:v h264_amf -quality balanced -rc vbr_peak -b:v ${bitrateK} -maxrate ${maxrate}`;
|
|
18142
|
+
if (encoder === "libx264") return `-c:v libx264 -preset medium -b:v ${bitrateK} -maxrate ${maxrate} -bufsize ${bufsize} -threads ${threads}`;
|
|
18143
|
+
if (encoder === "hevc_nvenc") return `-c:v hevc_nvenc -preset p4 -rc vbr -b:v ${bitrateK} -maxrate ${maxrate} -bufsize ${bufsize}`;
|
|
18144
|
+
if (encoder === "hevc_qsv") return `-c:v hevc_qsv -preset medium -b:v ${bitrateK} -maxrate ${maxrate} -bufsize ${bufsize}`;
|
|
18145
|
+
if (encoder === "hevc_amf") return `-c:v hevc_amf -quality balanced -rc vbr_peak -b:v ${bitrateK} -maxrate ${maxrate}`;
|
|
18146
|
+
if (encoder === "libx265") return `-c:v libx265 -preset medium -b:v ${bitrateK} -maxrate ${maxrate} -bufsize ${bufsize} -threads ${threads}`;
|
|
18147
|
+
if (encoder === "av1_nvenc") return `-c:v av1_nvenc -preset p4 -rc vbr -b:v ${bitrateK} -maxrate ${maxrate} -bufsize ${bufsize}`;
|
|
18148
|
+
if (encoder === "av1_qsv") return `-c:v av1_qsv -preset medium -b:v ${bitrateK} -maxrate ${maxrate} -bufsize ${bufsize}`;
|
|
18149
|
+
if (encoder === "av1_amf") return `-c:v av1_amf -quality balanced -rc vbr_peak -b:v ${bitrateK} -maxrate ${maxrate}`;
|
|
18150
|
+
if (encoder === "libsvtav1") return `-c:v libsvtav1 -preset 6 -b:v ${bitrateK} -maxrate ${maxrate} -bufsize ${bufsize} -threads ${threads}`;
|
|
18151
|
+
if (encoder === "libaom-av1") return `-c:v libaom-av1 -cpu-used 4 -b:v ${bitrateK} -maxrate ${maxrate} -bufsize ${bufsize} -threads ${threads}`;
|
|
18152
|
+
return `-c:v libx265 -preset medium -b:v ${bitrateK} -maxrate ${maxrate} -bufsize ${bufsize} -threads ${threads}`;
|
|
18153
|
+
}
|
|
18154
|
+
if (encoder === "h264_nvenc") return "-c:v h264_nvenc -preset p4 -rc vbr -cq 23";
|
|
18155
|
+
if (encoder === "h264_qsv") return "-c:v h264_qsv -preset medium -global_quality 23";
|
|
18156
|
+
if (encoder === "h264_amf") return "-c:v h264_amf -quality balanced -rc cqp -qp_i 23 -qp_p 23";
|
|
18157
|
+
if (encoder === "libx264") return `-c:v libx264 -crf 23 -preset medium -threads ${threads}`;
|
|
18158
|
+
if (encoder === "hevc_nvenc") return "-c:v hevc_nvenc -preset p4 -rc vbr -cq 28";
|
|
18159
|
+
if (encoder === "hevc_qsv") return "-c:v hevc_qsv -preset medium -global_quality 28";
|
|
18160
|
+
if (encoder === "hevc_amf") return "-c:v hevc_amf -quality balanced -rc cqp -qp_i 28 -qp_p 28";
|
|
18161
|
+
if (encoder === "libx265") return `-c:v libx265 -crf 28 -preset medium -threads ${threads}`;
|
|
18162
|
+
if (encoder === "av1_nvenc") return "-c:v av1_nvenc -preset p4 -rc vbr -cq 30";
|
|
18163
|
+
if (encoder === "av1_qsv") return "-c:v av1_qsv -preset medium -global_quality 30";
|
|
18164
|
+
if (encoder === "av1_amf") return "-c:v av1_amf -quality balanced -rc cqp -qp_i 30 -qp_p 30";
|
|
18165
|
+
if (encoder === "libsvtav1") return `-c:v libsvtav1 -crf 30 -preset 6 -threads ${threads}`;
|
|
18166
|
+
if (encoder === "libaom-av1") return `-c:v libaom-av1 -crf 30 -cpu-used 4 -threads ${threads}`;
|
|
18167
|
+
return `-c:v libx265 -crf 28 -preset medium -threads ${threads}`;
|
|
18168
|
+
}
|
|
18169
|
+
var toASSTime = (ms) => {
|
|
18170
|
+
const s = ms / 1e3;
|
|
18171
|
+
const h = Math.floor(s / 3600);
|
|
18172
|
+
const m = Math.floor(s % 3600 / 60);
|
|
18173
|
+
const sec = Math.floor(s % 60);
|
|
18174
|
+
const cs = Math.floor(s % 1 * 100);
|
|
18175
|
+
return `${h}:${m.toString().padStart(2, "0")}:${sec.toString().padStart(2, "0")}.${cs.toString().padStart(2, "0")}`;
|
|
18176
|
+
};
|
|
18177
|
+
var estimateWidth = (text, fontSize) => {
|
|
18178
|
+
let w = 0;
|
|
18179
|
+
for (const c of text) w += c.charCodeAt(0) > 127 ? fontSize : fontSize * .5;
|
|
18180
|
+
return w;
|
|
18181
|
+
};
|
|
18182
|
+
var escapeASS = (text) => text.replace(/\\/g, "\\\\").replace(/\{/g, "\\{").replace(/\}/g, "\\}").replace(/\n/g, "\\N");
|
|
18183
|
+
var escapeWinPath = (path$1) => path$1.replace(/\\/g, "/").replace(/:/g, "\\:");
|
|
18184
|
+
var isLandscape = (w, h) => w > h;
|
|
18185
|
+
async function getDouyinResolution(path$1) {
|
|
18186
|
+
try {
|
|
18187
|
+
const { stdout } = await ffprobe(`-v error -select_streams v:0 -show_entries stream=width,height -of csv=s=x:p=0 "${path$1}"`);
|
|
18188
|
+
const [w, h] = stdout.trim().split("x").map(Number);
|
|
18189
|
+
if (w && h) return {
|
|
18190
|
+
width: w,
|
|
18191
|
+
height: h
|
|
18192
|
+
};
|
|
18193
|
+
} catch {}
|
|
18194
|
+
try {
|
|
18195
|
+
const match = ((await ffmpeg(`-i "${path$1}" -f null -`, { timeout: 5e3 })).stderr || "").match(/(\d{3,4})x(\d{3,4})/);
|
|
18196
|
+
if (match) return {
|
|
18197
|
+
width: parseInt(match[1]),
|
|
18198
|
+
height: parseInt(match[2])
|
|
18199
|
+
};
|
|
18200
|
+
} catch {}
|
|
18201
|
+
return {
|
|
18202
|
+
width: 1080,
|
|
18203
|
+
height: 1920
|
|
18204
|
+
};
|
|
18205
|
+
}
|
|
18206
|
+
async function getDouyinFrameRate(path$1) {
|
|
18207
|
+
try {
|
|
18208
|
+
const { stdout } = await ffprobe(`-v error -select_streams v:0 -show_entries stream=r_frame_rate -of default=noprint_wrappers=1:nokey=1 "${path$1}"`);
|
|
18209
|
+
const [num, den] = stdout.trim().split("/").map(Number);
|
|
18210
|
+
if (den > 0) return num / den;
|
|
18211
|
+
} catch {}
|
|
18212
|
+
try {
|
|
18213
|
+
const fpsMatch = ((await ffmpeg(`-i "${path$1}" -f null -`, { timeout: 5e3 })).stderr || "").match(/(\d+(?:\.\d+)?)\s*fps/);
|
|
18214
|
+
if (fpsMatch) return parseFloat(fpsMatch[1]);
|
|
18215
|
+
} catch {}
|
|
18216
|
+
return 30;
|
|
18217
|
+
}
|
|
18218
|
+
var FONT_SIZE_MAP = {
|
|
18219
|
+
small: {
|
|
18220
|
+
base: 25,
|
|
18221
|
+
trackH: 30
|
|
18222
|
+
},
|
|
18223
|
+
medium: {
|
|
18224
|
+
base: 32,
|
|
18225
|
+
trackH: 38
|
|
18226
|
+
},
|
|
18227
|
+
large: {
|
|
18228
|
+
base: 40,
|
|
18229
|
+
trackH: 46
|
|
18230
|
+
}
|
|
18231
|
+
};
|
|
18232
|
+
function generateDouyinASS(danmakuList, width, height, options = {}) {
|
|
18233
|
+
const { scrollTime = 8, danmakuOpacity = 70, fontName = "Microsoft YaHei", danmakuArea = .5, danmakuFontSize = "medium" } = options;
|
|
18234
|
+
const fontScale = height / 1080;
|
|
18235
|
+
const sizeConfig = FONT_SIZE_MAP[danmakuFontSize];
|
|
18236
|
+
const fontSize = Math.round(sizeConfig.base * fontScale);
|
|
18237
|
+
const trackH = Math.round(sizeConfig.trackH * fontScale);
|
|
18238
|
+
const topMargin = Math.round(5 * fontScale);
|
|
18239
|
+
const areaHeight = Math.floor(height * danmakuArea) - topMargin;
|
|
18240
|
+
const trackCount = Math.max(1, Math.floor((areaHeight - fontSize) / trackH));
|
|
18241
|
+
const minGap = Math.round(15 * fontScale);
|
|
18242
|
+
const alpha = Math.round((100 - Math.max(0, Math.min(100, danmakuOpacity))) * 2.55).toString(16).padStart(2, "0").toUpperCase();
|
|
18243
|
+
let ass = `[Script Info]\nTitle: Douyin Danmaku\nScriptType: v4.00+\nPlayResX: ${width}\nPlayResY: ${height}\nTimer: 100.0000\n\n[V4+ Styles]\nFormat: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding\nStyle: Scroll,${fontName},${fontSize},&H${alpha}FFFFFF,&H${alpha}FFFFFF,&H${alpha}000000,&H${alpha}000000,0,0,0,0,100,100,0,0,1,0.8,0,2,0,0,0,1\n\n[Events]\nFormat: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text\n`;
|
|
18244
|
+
const scrollTracks = Array(trackCount).fill(null);
|
|
18245
|
+
const calcDistance = (last, startTime, duration, textWidth) => {
|
|
18246
|
+
const lastSpeed = (width + last.textWidth) / last.duration;
|
|
18247
|
+
const newSpeed = (width + textWidth) / duration;
|
|
18248
|
+
let dist = width - (width - lastSpeed * (startTime - last.startTime) + last.textWidth) - minGap;
|
|
18249
|
+
if (newSpeed > lastSpeed) {
|
|
18250
|
+
const lastRightXAtEnd = width - lastSpeed * (startTime + duration - last.startTime) + last.textWidth;
|
|
18251
|
+
dist = Math.min(dist, -textWidth - lastRightXAtEnd - minGap);
|
|
18252
|
+
}
|
|
18253
|
+
return dist;
|
|
18254
|
+
};
|
|
18255
|
+
const sorted = [...danmakuList.filter((dm) => dm.text && dm.text.trim())].sort((a, b) => a.offset_time - b.offset_time);
|
|
18256
|
+
for (const dm of sorted) {
|
|
18257
|
+
const startTime = dm.offset_time;
|
|
18258
|
+
const textWidth = estimateWidth(dm.text, fontSize);
|
|
18259
|
+
const content = escapeASS(dm.text);
|
|
18260
|
+
const duration = scrollTime * 1e3;
|
|
18261
|
+
const endTime = startTime + duration;
|
|
18262
|
+
for (let i = 0; i < scrollTracks.length; i++) {
|
|
18263
|
+
const t = scrollTracks[i];
|
|
18264
|
+
if (t && t.startTime + t.duration <= startTime) scrollTracks[i] = null;
|
|
18265
|
+
}
|
|
18266
|
+
let bestIdx = -1;
|
|
18267
|
+
let bestDist = -Infinity;
|
|
18268
|
+
for (let i = 0; i < scrollTracks.length; i++) {
|
|
18269
|
+
const t = scrollTracks[i];
|
|
18270
|
+
if (!t) {
|
|
18271
|
+
if (bestIdx === -1) bestIdx = i;
|
|
18272
|
+
continue;
|
|
18273
|
+
}
|
|
18274
|
+
const d = calcDistance(t, startTime, duration, textWidth);
|
|
18275
|
+
if (d >= 0) {
|
|
18276
|
+
if (bestDist < 0 || d < bestDist) {
|
|
18277
|
+
bestDist = d;
|
|
18278
|
+
bestIdx = i;
|
|
18157
18279
|
}
|
|
18158
|
-
} else if (replyStickerUrl) {
|
|
18159
|
-
replyImageList = [replyStickerUrl];
|
|
18160
|
-
imageUrls.push(replyStickerUrl);
|
|
18161
18280
|
}
|
|
18162
|
-
replyCommentsList.push({
|
|
18163
|
-
create_time: getRelativeTimeFromTimestamp$2(replyItem.create_time),
|
|
18164
|
-
nickname: replyItem.user.nickname,
|
|
18165
|
-
userimageurl: replyItem.user.avatar_thumb.url_list[0],
|
|
18166
|
-
text: processCommentEmojis$1(processedReplyText, emojidata),
|
|
18167
|
-
digg_count: replyItem.digg_count > 1e4 ? (replyItem.digg_count / 1e4).toFixed(1) + "w" : replyItem.digg_count,
|
|
18168
|
-
ip_label: replyItem.ip_label,
|
|
18169
|
-
text_extra: replyItem.text_extra,
|
|
18170
|
-
label_text: replyItem.label_text,
|
|
18171
|
-
image_list: replyImageList,
|
|
18172
|
-
cid: replyItem.cid,
|
|
18173
|
-
reply_to_reply_id: replyItem.reply_to_reply_id,
|
|
18174
|
-
reply_to_username: replyItem.reply_to_username
|
|
18175
|
-
});
|
|
18176
18281
|
}
|
|
18177
|
-
|
|
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 {
|
|
@@ -18313,7 +18473,6 @@ var DouYin = class extends Base {
|
|
|
18313
18473
|
headers: this.headers
|
|
18314
18474
|
});
|
|
18315
18475
|
temp.push(liveimgbgm);
|
|
18316
|
-
if (mergeMode === "continuous") bgmContext = await createLiveImageContext(liveimgbgm.filepath);
|
|
18317
18476
|
}
|
|
18318
18477
|
for (const [index, imageItem] of images.entries()) {
|
|
18319
18478
|
imagenum++;
|
|
@@ -18337,26 +18496,27 @@ var DouYin = class extends Base {
|
|
|
18337
18496
|
});
|
|
18338
18497
|
if (liveimg.filepath) {
|
|
18339
18498
|
const outputPath = Common.tempDri.video + `Douyin_Result_${Date.now()}.mp4`;
|
|
18340
|
-
let success;
|
|
18341
18499
|
const loopCount = imageItem.clip_type === 4 ? 1 : 3;
|
|
18342
|
-
|
|
18343
|
-
|
|
18344
|
-
|
|
18345
|
-
|
|
18346
|
-
|
|
18347
|
-
|
|
18348
|
-
|
|
18349
|
-
|
|
18350
|
-
|
|
18351
|
-
|
|
18352
|
-
}, bgmContext);
|
|
18353
|
-
success = result.success;
|
|
18354
|
-
bgmContext = result.context;
|
|
18355
|
-
} else success = await mergeLiveImageIndependent({
|
|
18356
|
-
videoPath: liveimg.filepath,
|
|
18500
|
+
let staticImgPath = "";
|
|
18501
|
+
if (imageItem.url_list?.[0]) staticImgPath = (await downloadFile(imageItem.url_list[0], {
|
|
18502
|
+
title: `Douyin_static_${Date.now()}_${index}.jpg`,
|
|
18503
|
+
headers: this.headers,
|
|
18504
|
+
filepath: Common.tempDri.images + `Douyin_static_${Date.now()}_${index}.jpg`
|
|
18505
|
+
})).filepath ?? "";
|
|
18506
|
+
const transitionEnabled = loopCount > 1 && Boolean(staticImgPath);
|
|
18507
|
+
const safeStaticPath = staticImgPath || liveimg.filepath;
|
|
18508
|
+
const result = await loopVideoWithTransition({
|
|
18509
|
+
inputPath: liveimg.filepath,
|
|
18357
18510
|
outputPath,
|
|
18358
|
-
loopCount
|
|
18359
|
-
|
|
18511
|
+
loopCount,
|
|
18512
|
+
staticImagePath: safeStaticPath,
|
|
18513
|
+
transitionEnabled,
|
|
18514
|
+
bgmPath: liveimgbgm?.filepath,
|
|
18515
|
+
mergeMode,
|
|
18516
|
+
context: bgmContext ?? void 0
|
|
18517
|
+
});
|
|
18518
|
+
const success = result.success;
|
|
18519
|
+
if (mergeMode === "continuous" && result.context) bgmContext = result.context;
|
|
18360
18520
|
if (success) {
|
|
18361
18521
|
const filePath = Common.tempDri.video + `tmp_${Date.now()}.mp4`;
|
|
18362
18522
|
fs.renameSync(outputPath, filePath);
|
|
@@ -18367,7 +18527,8 @@ var DouYin = class extends Base {
|
|
|
18367
18527
|
filepath: filePath,
|
|
18368
18528
|
totalBytes: 0
|
|
18369
18529
|
});
|
|
18370
|
-
|
|
18530
|
+
const videoPath = Config.upload.videoSendMode === "base64" ? `base64://${fs.readFileSync(filePath).toString("base64")}` : `file://${filePath}`;
|
|
18531
|
+
processedImages.push(segment.video(videoPath));
|
|
18371
18532
|
if (imageItem.clip_type === 5 && imageItem.url_list?.[0]) {
|
|
18372
18533
|
const imageUrl = await processImageUrl(imageItem.url_list[0], g_title, index);
|
|
18373
18534
|
processedImages.push(segment.image(imageUrl));
|
|
@@ -18375,7 +18536,7 @@ var DouYin = class extends Base {
|
|
|
18375
18536
|
} else await Common.removeFile(liveimg.filepath, true);
|
|
18376
18537
|
}
|
|
18377
18538
|
}
|
|
18378
|
-
const Element = common.makeForward(processedImages, this.e.sender.userId, this.e.sender.nick);
|
|
18539
|
+
const Element = common.makeForward(processedImages, Config.app.fakeForward ? this.e.sender.userId : this.e.bot.account.selfId, Config.app.fakeForward ? this.e.sender.nick : this.e.bot.account.name);
|
|
18379
18540
|
try {
|
|
18380
18541
|
await this.e.bot.sendForwardMsg(this.e.contact, Element, {
|
|
18381
18542
|
source: "图集内容",
|
|
@@ -18383,8 +18544,6 @@ var DouYin = class extends Base {
|
|
|
18383
18544
|
prompt: "抖音图集解析结果",
|
|
18384
18545
|
news: [{ text: "点击查看解析结果" }]
|
|
18385
18546
|
});
|
|
18386
|
-
} catch (error) {
|
|
18387
|
-
await this.e.reply(JSON.stringify(error, null, 2));
|
|
18388
18547
|
} finally {
|
|
18389
18548
|
for (const item of temp) await Common.removeFile(item.filepath, true);
|
|
18390
18549
|
}
|
|
@@ -18405,7 +18564,7 @@ var DouYin = class extends Base {
|
|
|
18405
18564
|
}).getData().then((data$2) => fs.promises.writeFile(path$1, Buffer.from(data$2)));
|
|
18406
18565
|
}
|
|
18407
18566
|
}
|
|
18408
|
-
const res = common.makeForward(imageres, this.e.sender.userId, this.e.sender.nick);
|
|
18567
|
+
const res = common.makeForward(imageres, Config.app.fakeForward ? this.e.sender.userId : this.e.bot.account.selfId, Config.app.fakeForward ? this.e.sender.nick : this.e.bot.account.name);
|
|
18409
18568
|
image_data.push(res);
|
|
18410
18569
|
image_res.push(image_data);
|
|
18411
18570
|
if (imageres.length === 1) {
|
|
@@ -18435,7 +18594,6 @@ var DouYin = class extends Base {
|
|
|
18435
18594
|
headers: this.headers
|
|
18436
18595
|
});
|
|
18437
18596
|
temp.push(liveimgbgm);
|
|
18438
|
-
if (mergeMode === "continuous") bgmContext = await createLiveImageContext(liveimgbgm.filepath);
|
|
18439
18597
|
}
|
|
18440
18598
|
const images1 = VideoData.data.aweme_detail.images ?? [];
|
|
18441
18599
|
if (!images1.length) logger.debug("未获取到合辑的图片数据");
|
|
@@ -18446,51 +18604,53 @@ var DouYin = class extends Base {
|
|
|
18446
18604
|
images.push(segment.image(imageUrl));
|
|
18447
18605
|
continue;
|
|
18448
18606
|
}
|
|
18449
|
-
const
|
|
18607
|
+
const livePhoto = await downloadFile(`https://aweme.snssdk.com/aweme/v1/play/?video_id=${item.video.play_addr_h264.uri}&ratio=1080p&line=0`, {
|
|
18450
18608
|
title: `Douyin_tmp_V_${Date.now()}.mp4`,
|
|
18451
18609
|
headers: this.headers
|
|
18452
18610
|
});
|
|
18453
|
-
if (
|
|
18611
|
+
if (livePhoto.filepath) {
|
|
18454
18612
|
const outputPath = Common.tempDri.video + `Douyin_Result_${Date.now()}.mp4`;
|
|
18455
|
-
let success;
|
|
18456
18613
|
const loopCount = item.clip_type === 4 ? 1 : 3;
|
|
18457
|
-
|
|
18458
|
-
|
|
18459
|
-
|
|
18460
|
-
|
|
18461
|
-
|
|
18462
|
-
|
|
18463
|
-
|
|
18464
|
-
|
|
18465
|
-
|
|
18466
|
-
|
|
18467
|
-
}, bgmContext);
|
|
18468
|
-
success = result.success;
|
|
18469
|
-
bgmContext = result.context;
|
|
18470
|
-
} else success = await mergeLiveImageIndependent({
|
|
18471
|
-
videoPath: liveimg.filepath,
|
|
18614
|
+
let staticImgPath = "";
|
|
18615
|
+
if (item.url_list?.[0]) staticImgPath = (await downloadFile(item.url_list[0], {
|
|
18616
|
+
title: `Douyin_static_${Date.now()}_${index}.jpg`,
|
|
18617
|
+
headers: this.headers,
|
|
18618
|
+
filepath: Common.tempDri.images + `Douyin_static_${Date.now()}_${index}.jpg`
|
|
18619
|
+
})).filepath ?? "";
|
|
18620
|
+
const transitionEnabled = loopCount > 1 && Boolean(staticImgPath);
|
|
18621
|
+
const safeStaticPath = staticImgPath || livePhoto.filepath;
|
|
18622
|
+
const result = await loopVideoWithTransition({
|
|
18623
|
+
inputPath: livePhoto.filepath,
|
|
18472
18624
|
outputPath,
|
|
18473
|
-
loopCount
|
|
18474
|
-
|
|
18625
|
+
loopCount,
|
|
18626
|
+
staticImagePath: safeStaticPath,
|
|
18627
|
+
transitionEnabled,
|
|
18628
|
+
bgmPath: liveimgbgm?.filepath,
|
|
18629
|
+
mergeMode,
|
|
18630
|
+
context: bgmContext ?? void 0
|
|
18631
|
+
});
|
|
18632
|
+
const success = result.success;
|
|
18633
|
+
if (mergeMode === "continuous" && result.context) bgmContext = result.context;
|
|
18475
18634
|
if (success) {
|
|
18476
18635
|
const filePath = Common.tempDri.video + `tmp_${Date.now()}.mp4`;
|
|
18477
18636
|
fs.renameSync(outputPath, filePath);
|
|
18478
18637
|
logger.mark(`视频文件重命名完成: ${outputPath.split("/").pop()} -> ${filePath.split("/").pop()}`);
|
|
18479
18638
|
logger.mark("正在尝试删除缓存文件");
|
|
18480
|
-
await Common.removeFile(
|
|
18639
|
+
await Common.removeFile(livePhoto.filepath, true);
|
|
18481
18640
|
temp.push({
|
|
18482
18641
|
filepath: filePath,
|
|
18483
18642
|
totalBytes: 0
|
|
18484
18643
|
});
|
|
18485
|
-
|
|
18644
|
+
const videoPath = Config.upload.videoSendMode === "base64" ? `base64://${fs.readFileSync(filePath).toString("base64")}` : `file://${filePath}`;
|
|
18645
|
+
images.push(segment.video(videoPath));
|
|
18486
18646
|
if (item.clip_type === 5 && item.url_list?.[0]) {
|
|
18487
18647
|
const imageUrl = await processImageUrl(item.url_list[0], g_title, index);
|
|
18488
18648
|
images.push(segment.image(imageUrl));
|
|
18489
18649
|
}
|
|
18490
|
-
} else await Common.removeFile(
|
|
18650
|
+
} else await Common.removeFile(livePhoto.filepath, true);
|
|
18491
18651
|
}
|
|
18492
18652
|
}
|
|
18493
|
-
const Element = common.makeForward(images, this.e.sender.userId, this.e.sender.nick);
|
|
18653
|
+
const Element = common.makeForward(images, Config.app.fakeForward ? this.e.sender.userId : this.e.bot.account.selfId, Config.app.fakeForward ? this.e.sender.nick : this.e.bot.account.name);
|
|
18494
18654
|
try {
|
|
18495
18655
|
await this.e.bot.sendForwardMsg(this.e.contact, Element, {
|
|
18496
18656
|
source: "合辑内容",
|
|
@@ -18498,8 +18658,6 @@ var DouYin = class extends Base {
|
|
|
18498
18658
|
prompt: "抖音合辑解析结果",
|
|
18499
18659
|
news: [{ text: "点击查看解析结果" }]
|
|
18500
18660
|
});
|
|
18501
|
-
} catch (error) {
|
|
18502
|
-
await this.e.reply(JSON.stringify(error, null, 2));
|
|
18503
18661
|
} finally {
|
|
18504
18662
|
for (const item of temp) await Common.removeFile(item.filepath, true);
|
|
18505
18663
|
}
|
|
@@ -18634,8 +18792,8 @@ var DouYin = class extends Base {
|
|
|
18634
18792
|
const imageUrl = await processImageUrl(v, VideoData.data.aweme_detail.desc, index);
|
|
18635
18793
|
messageElements.push(segment.image(imageUrl));
|
|
18636
18794
|
}
|
|
18637
|
-
const res = common.makeForward(messageElements, this.e.sender.userId, this.e.sender.nick);
|
|
18638
|
-
this.e.bot.sendForwardMsg(this.e.contact, res, {
|
|
18795
|
+
const res = common.makeForward(messageElements, Config.app.fakeForward ? this.e.sender.userId : this.e.bot.account.selfId, Config.app.fakeForward ? this.e.sender.nick : this.e.bot.account.name);
|
|
18796
|
+
await this.e.bot.sendForwardMsg(this.e.contact, res, {
|
|
18639
18797
|
source: "评论图片收集",
|
|
18640
18798
|
summary: `查看${messageElements.length}张图片`,
|
|
18641
18799
|
prompt: "抖音评论解析结果",
|
|
@@ -18970,11 +19128,22 @@ let DouyinImageSubType = function(DouyinImageSubType$1) {
|
|
|
18970
19128
|
return DouyinImageSubType$1;
|
|
18971
19129
|
}({});
|
|
18972
19130
|
function getWorkTypeInfo(data$1) {
|
|
19131
|
+
if (data$1.live_data) return {
|
|
19132
|
+
mainType: DouyinWorkMainType.LIVE,
|
|
19133
|
+
isVideo: false,
|
|
19134
|
+
isImage: false,
|
|
19135
|
+
isArticle: false,
|
|
19136
|
+
isLive: true,
|
|
19137
|
+
isGallery: false,
|
|
19138
|
+
isCollection: false,
|
|
19139
|
+
templatePath: "douyin/live"
|
|
19140
|
+
};
|
|
18973
19141
|
if (data$1.aweme_type === 163 || data$1.article_info) return {
|
|
18974
19142
|
mainType: DouyinWorkMainType.ARTICLE,
|
|
18975
19143
|
isVideo: false,
|
|
18976
19144
|
isImage: false,
|
|
18977
19145
|
isArticle: true,
|
|
19146
|
+
isLive: false,
|
|
18978
19147
|
isGallery: false,
|
|
18979
19148
|
isCollection: false,
|
|
18980
19149
|
templatePath: "douyin/article-work"
|
|
@@ -18987,6 +19156,7 @@ function getWorkTypeInfo(data$1) {
|
|
|
18987
19156
|
isVideo: false,
|
|
18988
19157
|
isImage: true,
|
|
18989
19158
|
isArticle: false,
|
|
19159
|
+
isLive: false,
|
|
18990
19160
|
isGallery: subType === DouyinImageSubType.GALLERY,
|
|
18991
19161
|
isCollection: subType === DouyinImageSubType.COLLECTION,
|
|
18992
19162
|
templatePath: "douyin/image-work"
|
|
@@ -18997,6 +19167,7 @@ function getWorkTypeInfo(data$1) {
|
|
|
18997
19167
|
isVideo: true,
|
|
18998
19168
|
isImage: false,
|
|
18999
19169
|
isArticle: false,
|
|
19170
|
+
isLive: false,
|
|
19000
19171
|
isGallery: false,
|
|
19001
19172
|
isCollection: false,
|
|
19002
19173
|
templatePath: "douyin/video-work"
|
|
@@ -19017,6 +19188,7 @@ function getWorkTypeDisplayName(workTypeInfo) {
|
|
|
19017
19188
|
if (workTypeInfo.isGallery) return "图集";
|
|
19018
19189
|
if (workTypeInfo.isCollection) return "合辑";
|
|
19019
19190
|
if (workTypeInfo.isArticle) return "文章";
|
|
19191
|
+
if (workTypeInfo.isLive) return "直播";
|
|
19020
19192
|
return "未知";
|
|
19021
19193
|
}
|
|
19022
19194
|
const getDouyinID = async (event, url, log = true) => {
|
|
@@ -19550,7 +19722,6 @@ var DouYinpush = class extends Base {
|
|
|
19550
19722
|
headers: douyinBaseHeaders
|
|
19551
19723
|
});
|
|
19552
19724
|
temp.push(liveimgbgm);
|
|
19553
|
-
if (mergeMode === "continuous") bgmContext = await createLiveImageContext(liveimgbgm.filepath);
|
|
19554
19725
|
}
|
|
19555
19726
|
const images1 = Detail_Data.images ?? [];
|
|
19556
19727
|
if (!images1.length) logger.debug("未获取到合辑的图片数据");
|
|
@@ -19566,26 +19737,27 @@ var DouYinpush = class extends Base {
|
|
|
19566
19737
|
});
|
|
19567
19738
|
if (liveimg.filepath) {
|
|
19568
19739
|
const outputPath = Common.tempDri.video + `Douyin_Result_${Date.now()}.mp4`;
|
|
19569
|
-
let success;
|
|
19570
19740
|
const loopCount = item.clip_type === 4 ? 1 : 3;
|
|
19571
|
-
|
|
19572
|
-
|
|
19573
|
-
|
|
19574
|
-
|
|
19575
|
-
|
|
19576
|
-
|
|
19577
|
-
|
|
19578
|
-
|
|
19579
|
-
|
|
19580
|
-
|
|
19581
|
-
}, bgmContext);
|
|
19582
|
-
success = result.success;
|
|
19583
|
-
bgmContext = result.context;
|
|
19584
|
-
} else success = await mergeLiveImageIndependent({
|
|
19585
|
-
videoPath: liveimg.filepath,
|
|
19741
|
+
let staticImgPath = "";
|
|
19742
|
+
if (item.url_list?.[0]) staticImgPath = (await downloadFile(item.url_list[0], {
|
|
19743
|
+
title: `Douyin_static_${Date.now()}_${index}.jpg`,
|
|
19744
|
+
headers: douyinBaseHeaders,
|
|
19745
|
+
filepath: Common.tempDri.images + `Douyin_static_${Date.now()}_${index}.jpg`
|
|
19746
|
+
})).filepath ?? "";
|
|
19747
|
+
const transitionEnabled = loopCount > 1 && Boolean(staticImgPath);
|
|
19748
|
+
const safeStaticPath = staticImgPath || liveimg.filepath;
|
|
19749
|
+
const result = await loopVideoWithTransition({
|
|
19750
|
+
inputPath: liveimg.filepath,
|
|
19586
19751
|
outputPath,
|
|
19587
|
-
loopCount
|
|
19588
|
-
|
|
19752
|
+
loopCount,
|
|
19753
|
+
staticImagePath: safeStaticPath,
|
|
19754
|
+
transitionEnabled,
|
|
19755
|
+
bgmPath: liveimgbgm?.filepath,
|
|
19756
|
+
mergeMode,
|
|
19757
|
+
context: bgmContext ?? void 0
|
|
19758
|
+
});
|
|
19759
|
+
const success = result.success;
|
|
19760
|
+
if (mergeMode === "continuous" && result.context) bgmContext = result.context;
|
|
19589
19761
|
if (success) {
|
|
19590
19762
|
const filePath = Common.tempDri.video + `tmp_${Date.now()}.mp4`;
|
|
19591
19763
|
fs.renameSync(outputPath, filePath);
|
|
@@ -19596,7 +19768,8 @@ var DouYinpush = class extends Base {
|
|
|
19596
19768
|
filepath: filePath,
|
|
19597
19769
|
totalBytes: 0
|
|
19598
19770
|
});
|
|
19599
|
-
|
|
19771
|
+
const videoPath = Config.upload.videoSendMode === "base64" ? `base64://${fs.readFileSync(filePath).toString("base64")}` : `file://${filePath}`;
|
|
19772
|
+
images.push(segment.video(videoPath));
|
|
19600
19773
|
if (item.clip_type === 5 && item.url_list?.[0]) {
|
|
19601
19774
|
const imageUrl = await processImageUrl(item.url_list[0], Detail_Data.desc, index);
|
|
19602
19775
|
images.push(segment.image(imageUrl));
|
|
@@ -19633,7 +19806,6 @@ var DouYinpush = class extends Base {
|
|
|
19633
19806
|
headers: douyinBaseHeaders
|
|
19634
19807
|
});
|
|
19635
19808
|
temp.push(liveimgbgm);
|
|
19636
|
-
if (mergeMode === "continuous") bgmContext = await createLiveImageContext(liveimgbgm.filepath);
|
|
19637
19809
|
}
|
|
19638
19810
|
for (const [index, item] of Detail_Data.images.entries()) {
|
|
19639
19811
|
if (item.clip_type === 2 || item.clip_type === void 0) {
|
|
@@ -19647,26 +19819,27 @@ var DouYinpush = class extends Base {
|
|
|
19647
19819
|
});
|
|
19648
19820
|
if (liveimg.filepath) {
|
|
19649
19821
|
const outputPath = Common.tempDri.video + `Douyin_Result_${Date.now()}.mp4`;
|
|
19650
|
-
let success;
|
|
19651
19822
|
const loopCount = item.clip_type === 4 ? 1 : 3;
|
|
19652
|
-
|
|
19653
|
-
|
|
19654
|
-
|
|
19655
|
-
|
|
19656
|
-
|
|
19657
|
-
|
|
19658
|
-
|
|
19659
|
-
|
|
19660
|
-
|
|
19661
|
-
|
|
19662
|
-
}, bgmContext);
|
|
19663
|
-
success = result.success;
|
|
19664
|
-
bgmContext = result.context;
|
|
19665
|
-
} else success = await mergeLiveImageIndependent({
|
|
19666
|
-
videoPath: liveimg.filepath,
|
|
19823
|
+
let staticImgPath = "";
|
|
19824
|
+
if (item.url_list?.[0]) staticImgPath = (await downloadFile(item.url_list[0], {
|
|
19825
|
+
title: `Douyin_static_${Date.now()}_${index}.jpg`,
|
|
19826
|
+
headers: douyinBaseHeaders,
|
|
19827
|
+
filepath: Common.tempDri.images + `Douyin_static_${Date.now()}_${index}.jpg`
|
|
19828
|
+
})).filepath ?? "";
|
|
19829
|
+
const transitionEnabled = loopCount > 1 && Boolean(staticImgPath);
|
|
19830
|
+
const safeStaticPath = staticImgPath || liveimg.filepath;
|
|
19831
|
+
const result = await loopVideoWithTransition({
|
|
19832
|
+
inputPath: liveimg.filepath,
|
|
19667
19833
|
outputPath,
|
|
19668
|
-
loopCount
|
|
19669
|
-
|
|
19834
|
+
loopCount,
|
|
19835
|
+
staticImagePath: safeStaticPath,
|
|
19836
|
+
transitionEnabled,
|
|
19837
|
+
bgmPath: liveimgbgm?.filepath,
|
|
19838
|
+
mergeMode,
|
|
19839
|
+
context: bgmContext ?? void 0
|
|
19840
|
+
});
|
|
19841
|
+
const success = result.success;
|
|
19842
|
+
if (mergeMode === "continuous" && result.context) bgmContext = result.context;
|
|
19670
19843
|
if (success) {
|
|
19671
19844
|
const filePath = Common.tempDri.video + `tmp_${Date.now()}.mp4`;
|
|
19672
19845
|
fs.renameSync(outputPath, filePath);
|
|
@@ -19677,7 +19850,8 @@ var DouYinpush = class extends Base {
|
|
|
19677
19850
|
filepath: filePath,
|
|
19678
19851
|
totalBytes: 0
|
|
19679
19852
|
});
|
|
19680
|
-
|
|
19853
|
+
const videoPath = Config.upload.videoSendMode === "base64" ? `base64://${fs.readFileSync(filePath).toString("base64")}` : `file://${filePath}`;
|
|
19854
|
+
processedImages.push(segment.video(videoPath));
|
|
19681
19855
|
if (item.clip_type === 5 && item.url_list?.[0]) {
|
|
19682
19856
|
const imageUrl = await processImageUrl(item.url_list[0], Detail_Data.desc, index);
|
|
19683
19857
|
processedImages.push(segment.image(imageUrl));
|
|
@@ -19985,7 +20159,6 @@ var skipDynamic = async (PushItem) => {
|
|
|
19985
20159
|
logger.debug(`检查作品是否需要过滤:${PushItem.Detail_Data.share_url}`);
|
|
19986
20160
|
return await douyinDBInstance.shouldFilter(PushItem, tags);
|
|
19987
20161
|
};
|
|
19988
|
-
init_danmaku();
|
|
19989
20162
|
init_date_fns();
|
|
19990
20163
|
init_locale();
|
|
19991
20164
|
init_Config();
|
|
@@ -20507,7 +20680,7 @@ const task = Config.app.removeCache && karin$1.task("[kkk-缓存自动删除]",
|
|
|
20507
20680
|
const twoHoursAgo = Date.now() - 7200 * 1e3;
|
|
20508
20681
|
const videoDeleted = removeOldFiles(Common.tempDri.video, twoHoursAgo);
|
|
20509
20682
|
logger.mark(`${Common.tempDri.video} 目录下已删除 ${videoDeleted} 个文件`);
|
|
20510
|
-
if (Config.
|
|
20683
|
+
if (Config.upload.imageSendMode === "file") {
|
|
20511
20684
|
const imageDeleted = removeOldFiles(Common.tempDri.images, twoHoursAgo);
|
|
20512
20685
|
logger.mark(`${Common.tempDri.images} 目录下已删除 ${imageDeleted} 个文件`);
|
|
20513
20686
|
}
|
|
@@ -21340,7 +21513,7 @@ var Xiaohongshu = class extends Base {
|
|
|
21340
21513
|
const imageUrl = await processImageUrl(item.url_default, title, index);
|
|
21341
21514
|
Imgs.push(segment.image(imageUrl));
|
|
21342
21515
|
}
|
|
21343
|
-
const res = common.makeForward(Imgs, this.e.sender.userId, this.e.sender.nick);
|
|
21516
|
+
const res = common.makeForward(Imgs, Config.app.fakeForward ? this.e.sender.userId : this.e.bot.account.selfId, Config.app.fakeForward ? this.e.sender.nick : this.e.bot.account.name);
|
|
21344
21517
|
if (NoteData.data.data.items[0].note_card.image_list.length === 1) {
|
|
21345
21518
|
const imageUrl = await processImageUrl(NoteData.data.data.items[0].note_card.image_list[0].url_default, title);
|
|
21346
21519
|
await this.e.reply(segment.image(imageUrl));
|