Skip to content

Commit

Permalink
feat: 合并动态图片
Browse files Browse the repository at this point in the history
  • Loading branch information
Tsuk1ko committed Jan 12, 2024
1 parent 2020e39 commit c182455
Show file tree
Hide file tree
Showing 7 changed files with 124 additions and 29 deletions.
5 changes: 4 additions & 1 deletion config.default.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,8 @@
"getLiveRoomInfo": false,
// 是否在发送动态时预下载图片(如网络环境不佳,启用该项可能可以解决发图发一半或动图不动情况)
"dynamicImgPreDl": false,
// 自动合并3/6/9宫格图(需要先启用 dynamicImgPreDl )
"dynamicMergeImgs": false,
// 图片预下载超时时间(秒),0 则无超时(不建议,小心永久卡住)
"imgPreDlTimeout": 30,
// 动态和直播开播推送,请查看“wiki-附加功能-哔哩哔哩推送”以了解更多
Expand All @@ -286,7 +288,8 @@
"useFeed": false,
// 信息流检测间隔(秒),最小为 5
"feedCheckInterval": 10,
// B站账号动态页 cookie,启用 useFeed 时需要
// B站账号动态页 cookie,启用 useFeed 时需要,如果解析功能出现 -352 错误也可尝试提供 cookie
// 可通过 https://bql.lolicon.app 快捷获取
"cookie": "",
// 当发送者撤回原消息时是否同步撤回解析消息
"respondRecall": true,
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
"get-stream": "^6.0.1",
"http-terminator": "^3.2.0",
"https-proxy-agent": "^5.0.1",
"image-size": "^1.1.1",
"is-stream": "^2.0.1",
"jimp": "^0.22.10",
"json-bigint": "^1.0.0",
Expand Down
23 changes: 6 additions & 17 deletions src/plugin/bilibili/dynamicNew.mjs
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { map } from 'lodash-es';
import CQ from '../../utils/CQcode.mjs';
import humanNum from '../../utils/humanNum.mjs';
import logError from '../../utils/logError.mjs';
import { retryGet } from '../../utils/retry.mjs';
import { MAGIC_USER_AGENT } from './const.mjs';
import { purgeLinkInText } from './utils.mjs';
import { handleImgsByConfig, purgeLinkInText } from './utils.mjs';

const additionalFormatters = {
// 投票
Expand All @@ -25,12 +26,7 @@ const additionalFormatters = {

const majorFormatters = {
// 图片
MAJOR_TYPE_DRAW: async ({ draw: { items } }) => {
const { dynamicImgPreDl, imgPreDlTimeout } = global.config.bot.bilibili;
return dynamicImgPreDl
? await Promise.all(items.map(({ src }) => CQ.imgPreDl(src, undefined, { timeout: imgPreDlTimeout * 1000 })))
: items.map(({ src }) => CQ.img(src));
},
MAJOR_TYPE_DRAW: ({ draw: { items } }) => handleImgsByConfig(map(items, 'src')),

// 视频
MAJOR_TYPE_ARCHIVE: ({ archive: { cover, aid, bvid, title, stat } }) => [
Expand Down Expand Up @@ -101,13 +97,7 @@ const majorFormatters = {
if (title) lines.push('', `《${CQ.escape(title.trim())}》`);
if (text) lines.push('', CQ.escape(purgeLinkInText(text.trim())));
if (pics.length) {
const { dynamicImgPreDl, imgPreDlTimeout } = global.config.bot.bilibili;
lines.push(
'',
...(dynamicImgPreDl
? await Promise.all(pics.map(({ url }) => CQ.imgPreDl(url, undefined, { timeout: imgPreDlTimeout * 1000 })))
: pics.map(({ url }) => CQ.img(url)))
);
lines.push('', ...(await handleImgsByConfig(map(pics, 'url'))));
}
return lines.slice(1);
},
Expand Down Expand Up @@ -151,6 +141,7 @@ export const getDynamicInfoFromItem = async item => {

export const getDynamicInfo = async id => {
try {
const { cookie } = global.config.bot.bilibili;
const {
data: { data, code, message },
} = await retryGet('https://api.bilibili.com/x/polymer/web-dynamic/v1/detail', {
Expand All @@ -160,9 +151,7 @@ export const getDynamicInfo = async id => {
id,
features: 'itemOpusStyle',
},
headers: {
'User-Agent': MAGIC_USER_AGENT,
},
headers: cookie ? { Cookie: cookie } : { 'User-Agent': MAGIC_USER_AGENT },
});
if (code === 4101131 || code === 4101105) {
return {
Expand Down
18 changes: 18 additions & 0 deletions src/plugin/bilibili/utils.mjs
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
import { pathToFileURL } from 'url';
import CQ from '../../utils/CQcode.mjs';
import { dlAndMergeImgsIfCan } from '../../utils/image.mjs';

/**
* 净化链接
* @param {string} link
Expand All @@ -21,3 +25,17 @@ export const purgeLink = link => {
* @param {string} text
*/
export const purgeLinkInText = text => text.replace(/https?:\/\/[-\w~!@#$%&*()+=;':,.?/]+/g, url => purgeLink(url));

/**
* @param {string[]} urls
*/
export const handleImgsByConfig = async urls => {
const { dynamicImgPreDl, imgPreDlTimeout, dynamicMergeImgs } = global.config.bot.bilibili;
if (dynamicImgPreDl) {
const config = { timeout: imgPreDlTimeout * 1000 };
return dynamicMergeImgs
? (await dlAndMergeImgsIfCan(urls, config)).map(url => CQ.img(pathToFileURL(url)))
: await Promise.all(urls.map(url => CQ.imgPreDl(url, undefined, config)));
}
return urls.map(url => CQ.img(url));
};
1 change: 1 addition & 0 deletions src/types/config.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ declare interface Bilibili {
getArticleInfo: boolean;
getLiveRoomInfo: boolean;
dynamicImgPreDl: boolean;
dynamicMergeImgs: boolean;
imgPreDlTimeout: number;
push: Push;
pushCheckInterval: number;
Expand Down
12 changes: 2 additions & 10 deletions src/utils/CQcode.mjs
Original file line number Diff line number Diff line change
@@ -1,11 +1,7 @@
import { pathToFileURL } from 'url';
import _ from 'lodash-es';
import promiseLimit from 'promise-limit';
import { createCache, getCache } from './cache.mjs';
import { dlImgToCache } from './image.mjs';
import logError from './logError.mjs';
import { retryGet } from './retry.mjs';

const dlImgLimit = promiseLimit(4);

class CQCode {
/**
Expand Down Expand Up @@ -139,11 +135,7 @@ class CQCode {
*/
static async imgPreDl(url, type, config = {}) {
try {
let path = getCache(url);
if (!path) {
const { data } = await dlImgLimit(() => retryGet(url, { responseType: 'arraybuffer', ...config }));
path = createCache(url, data);
}
const path = await dlImgToCache(url, config);
return new CQCode('image', { file: pathToFileURL(path), type }).toString();
} catch (e) {
logError('[error] cq img pre-download');
Expand Down
93 changes: 92 additions & 1 deletion src/utils/image.mjs
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
import { promisify } from 'util';
import imageSize from 'image-size';
import Jimp from 'jimp';
import promiseLimit from 'promise-limit';
import Axios from './axiosProxy.mjs';
import { createCache, getCache } from './cache.mjs';
import CQ from './CQcode.mjs';
import { imgAntiShielding } from './imgAntiShielding.mjs';
import logError from './logError.mjs';
import { retryAsync } from './retry.mjs';
import { retryAsync, retryGet } from './retry.mjs';

const imageSizeAsync = promisify(imageSize);

export const getCqImg64FromUrl = async (url, type = undefined) => {
try {
Expand Down Expand Up @@ -36,3 +42,88 @@ export const getAntiShieldedCqImg64FromUrl = async (url, mode, type = undefined)
}
return '';
};

const dlImgLimit = promiseLimit(4);

/**
* @param {string} url
* @param {import('axios').AxiosRequestConfig} [config] Axios 配置
* @returns
*/
export const dlImgToCache = async (url, config = {}) => {
const cachedPath = getCache(url);
if (cachedPath) return cachedPath;
const { data } = await dlImgLimit(() => retryGet(url, { responseType: 'arraybuffer', ...config }));
return createCache(url, data);
};

const minusMod3 = num => num - (num % 3);

/**
* @param {string[]} paths
*/
const check9ImgCanMerge = async paths => {
if (paths.length < 3) return 0;
if (paths.length === 9 && getCache(paths.join(','))) return 9;
if (paths.length >= 6 && getCache(paths.slice(0, 6).join(','))) return 6;
if (paths.length >= 3 && getCache(paths.slice(0, 3).join(','))) return 3;
try {
let fw = 0;
for (const [i, path] of paths.entries()) {
const size = await imageSizeAsync(path);
if (size.width !== size.height || size.type === 'gif') return minusMod3(i + 1);
if (i === 0) fw = size.width;
else if (size.width !== fw) return minusMod3(i + 1);
}
} catch (error) {
console.error('[utils/image] get image size error');
console.error(error);
return 0;
}
return minusMod3(paths.length);
};

/**
* @param {string[]} paths
* @param {number} count
*/
const mergeImgs = async (paths, count) => {
const mergePaths = paths.slice(0, count);
const cacheKey = mergePaths.join(',');
const cachedImg = getCache(cacheKey);
if (cachedImg) return [cachedImg, ...paths.slice(count)];
const imgs = await Promise.all(mergePaths.map(path => Jimp.read(path)));
const width = imgs[0].getWidth();
const mergedImg = new Jimp(width * 3, width * Math.round(count / 3));
for (const [i, img] of imgs.entries()) {
const col = i % 3;
const row = Math.floor(i / 3);
mergedImg.blit(img, col * width, row * width);
}
const buffer = await mergedImg.getBufferAsync(Jimp.MIME_PNG);
return [createCache(cacheKey, buffer), ...paths.slice(count)];
};

/**
* @param {string[]} urls
* @param {import('axios').AxiosRequestConfig} [config] Axios 配置
*/
export const dlAndMergeImgsIfCan = async (urls, config = {}) => {
let paths;
try {
paths = await Promise.all(urls.map(url => dlImgToCache(url, config)));
} catch (error) {
console.error('[utils/image] image download error');
console.error(error);
return urls;
}
const mergeCount = await check9ImgCanMerge(paths);
if (!mergeCount) return paths;
try {
return await mergeImgs(paths, mergeCount);
} catch (error) {
console.error('[utils/image] merge9Imgs error');
console.error(error);
return paths;
}
};

0 comments on commit c182455

Please sign in to comment.