From c182455a043c40fd9725c28b3883a7877649a5e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A5=9E=E4=BB=A3=E7=B6=BA=E5=87=9C?= Date: Fri, 12 Jan 2024 14:59:22 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=90=88=E5=B9=B6=E5=8A=A8=E6=80=81?= =?UTF-8?q?=E5=9B=BE=E7=89=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config.default.jsonc | 5 +- package.json | 1 + src/plugin/bilibili/dynamicNew.mjs | 23 ++------ src/plugin/bilibili/utils.mjs | 18 ++++++ src/types/config.d.ts | 1 + src/utils/CQcode.mjs | 12 +--- src/utils/image.mjs | 93 +++++++++++++++++++++++++++++- 7 files changed, 124 insertions(+), 29 deletions(-) diff --git a/config.default.jsonc b/config.default.jsonc index 3a731396..4f7f3139 100644 --- a/config.default.jsonc +++ b/config.default.jsonc @@ -275,6 +275,8 @@ "getLiveRoomInfo": false, // 是否在发送动态时预下载图片(如网络环境不佳,启用该项可能可以解决发图发一半或动图不动情况) "dynamicImgPreDl": false, + // 自动合并3/6/9宫格图(需要先启用 dynamicImgPreDl ) + "dynamicMergeImgs": false, // 图片预下载超时时间(秒),0 则无超时(不建议,小心永久卡住) "imgPreDlTimeout": 30, // 动态和直播开播推送,请查看“wiki-附加功能-哔哩哔哩推送”以了解更多 @@ -286,7 +288,8 @@ "useFeed": false, // 信息流检测间隔(秒),最小为 5 "feedCheckInterval": 10, - // B站账号动态页 cookie,启用 useFeed 时需要 + // B站账号动态页 cookie,启用 useFeed 时需要,如果解析功能出现 -352 错误也可尝试提供 cookie + // 可通过 https://bql.lolicon.app 快捷获取 "cookie": "", // 当发送者撤回原消息时是否同步撤回解析消息 "respondRecall": true, diff --git a/package.json b/package.json index 68d88b42..e9564838 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/plugin/bilibili/dynamicNew.mjs b/src/plugin/bilibili/dynamicNew.mjs index 0ee3890f..9c05f658 100644 --- a/src/plugin/bilibili/dynamicNew.mjs +++ b/src/plugin/bilibili/dynamicNew.mjs @@ -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 = { // 投票 @@ -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 } }) => [ @@ -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); }, @@ -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', { @@ -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 { diff --git a/src/plugin/bilibili/utils.mjs b/src/plugin/bilibili/utils.mjs index ad900d3f..4e85547b 100644 --- a/src/plugin/bilibili/utils.mjs +++ b/src/plugin/bilibili/utils.mjs @@ -1,3 +1,7 @@ +import { pathToFileURL } from 'url'; +import CQ from '../../utils/CQcode.mjs'; +import { dlAndMergeImgsIfCan } from '../../utils/image.mjs'; + /** * 净化链接 * @param {string} link @@ -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)); +}; diff --git a/src/types/config.d.ts b/src/types/config.d.ts index ec9e7685..067a2a5a 100644 --- a/src/types/config.d.ts +++ b/src/types/config.d.ts @@ -110,6 +110,7 @@ declare interface Bilibili { getArticleInfo: boolean; getLiveRoomInfo: boolean; dynamicImgPreDl: boolean; + dynamicMergeImgs: boolean; imgPreDlTimeout: number; push: Push; pushCheckInterval: number; diff --git a/src/utils/CQcode.mjs b/src/utils/CQcode.mjs index 56d9864f..4930ca67 100644 --- a/src/utils/CQcode.mjs +++ b/src/utils/CQcode.mjs @@ -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 { /** @@ -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'); diff --git a/src/utils/image.mjs b/src/utils/image.mjs index 4da0724a..70404244 100644 --- a/src/utils/image.mjs +++ b/src/utils/image.mjs @@ -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 { @@ -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; + } +};