koishi-plugin-kkk 1.0.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 +57 -0
- package/LICENSE +674 -0
- package/README.md +98 -0
- package/assets/web/assets/CronEditor-CYgFpX-g.js +1 -0
- package/assets/web/assets/DesktopLayout-iI43ij11.js +1 -0
- package/assets/web/assets/MobileLayout-CziU8MCR.js +1 -0
- package/assets/web/assets/ThemeSwitch-DFohhwd9.js +1 -0
- package/assets/web/assets/index-BRROK0vh.js +47 -0
- package/assets/web/assets/index-Dj-StEiS.css +2 -0
- package/assets/web/assets/separator-CxcFlCzz.js +1 -0
- package/assets/web/favicon.svg +1 -0
- package/assets/web/icons.svg +24 -0
- package/assets/web/index.html +15 -0
- package/assets/webui.html +115 -0
- package/client/index.ts +63 -0
- package/config/default_config/app.yaml +64 -0
- package/config/default_config/bilibili.yaml +116 -0
- package/config/default_config/config.json +157 -0
- package/config/default_config/cookies.yaml +11 -0
- package/config/default_config/douyin.yaml +99 -0
- package/config/default_config/kuaishou.yaml +8 -0
- package/config/default_config/pushlist.yaml +37 -0
- package/config/default_config/request.yaml +22 -0
- package/config/default_config/upload.yaml +50 -0
- package/config/default_config/xiaohongshu.yaml +23 -0
- package/dist/index.js +1 -0
- package/lib/compat/axios.js +23 -0
- package/lib/compat/cron.js +77 -0
- package/lib/compat/esm.js +15 -0
- package/lib/compat/express.js +23 -0
- package/lib/compat/heroui.js +12 -0
- package/lib/compat/lodash.js +33 -0
- package/lib/compat/logger.js +118 -0
- package/lib/compat/node-karin.js +1053 -0
- package/lib/compat/phosphor-icons.js +27 -0
- package/lib/compat/qqcard.js +256 -0
- package/lib/compat/root.js +44 -0
- package/lib/compat/runtime.js +87 -0
- package/lib/compat/segment.js +167 -0
- package/lib/compat/sqlite3.js +117 -0
- package/lib/compat/start.js +7 -0
- package/lib/compat/syncConfig.js +65 -0
- package/lib/compat/template-react.js +30 -0
- package/lib/compat/text.js +65 -0
- package/lib/compat/yaml.js +13 -0
- package/lib/configBridge.js +91 -0
- package/lib/index.js +775 -0
- package/lib/karin/app.js +34 -0
- package/lib/karin/apps/__ready.js +9 -0
- package/lib/karin/apps/admin.js +136 -0
- package/lib/karin/apps/help.js +233 -0
- package/lib/karin/apps/push.js +291 -0
- package/lib/karin/apps/qrlogin.js +176 -0
- package/lib/karin/apps/statistics.js +209 -0
- package/lib/karin/apps/testPush.js +247 -0
- package/lib/karin/apps/tools.js +473 -0
- package/lib/karin/apps/update.js +252 -0
- package/lib/karin/index.js +37 -0
- package/lib/karin/module/db/bilibili.js +831 -0
- package/lib/karin/module/db/douyin.js +918 -0
- package/lib/karin/module/db/index.js +165 -0
- package/lib/karin/module/db/migration.js +199 -0
- package/lib/karin/module/db/statistics.js +559 -0
- package/lib/karin/module/index.js +94 -0
- package/lib/karin/module/server/constants/routes.js +48 -0
- package/lib/karin/module/server/controllers/bots.js +230 -0
- package/lib/karin/module/server/controllers/config.js +75 -0
- package/lib/karin/module/server/controllers/video.js +208 -0
- package/lib/karin/module/server/index.js +26 -0
- package/lib/karin/module/server/middlewares/auth.js +133 -0
- package/lib/karin/module/server/routes/amagi.js +57 -0
- package/lib/karin/module/server/routes/api.js +40 -0
- package/lib/karin/module/server/routes/index.js +97 -0
- package/lib/karin/module/server/routes/ssr.js +16 -0
- package/lib/karin/module/server/routes/static.js +43 -0
- package/lib/karin/module/utils/Base.js +403 -0
- package/lib/karin/module/utils/CardParser.js +487 -0
- package/lib/karin/module/utils/Common.js +418 -0
- package/lib/karin/module/utils/Config.js +434 -0
- package/lib/karin/module/utils/EmojiReaction.js +242 -0
- package/lib/karin/module/utils/ErrorHandler/handler.js +114 -0
- package/lib/karin/module/utils/ErrorHandler/index.js +55 -0
- package/lib/karin/module/utils/ErrorHandler/render.js +182 -0
- package/lib/karin/module/utils/ErrorHandler/sender.js +206 -0
- package/lib/karin/module/utils/ErrorHandler/strategy.js +41 -0
- package/lib/karin/module/utils/ErrorHandler/types.js +2 -0
- package/lib/karin/module/utils/ErrorHandler/utils.js +108 -0
- package/lib/karin/module/utils/FFmpeg.js +298 -0
- package/lib/karin/module/utils/ImageHelper.js +91 -0
- package/lib/karin/module/utils/ImageSlice.js +429 -0
- package/lib/karin/module/utils/MotionPhoto.js +250 -0
- package/lib/karin/module/utils/Network/Downloader.js +413 -0
- package/lib/karin/module/utils/Network/ImageDownloader.js +250 -0
- package/lib/karin/module/utils/Network/Network.js +332 -0
- package/lib/karin/module/utils/Network/ThrottleStream.js +71 -0
- package/lib/karin/module/utils/Network/constants.js +129 -0
- package/lib/karin/module/utils/Network/helpers.js +174 -0
- package/lib/karin/module/utils/Network/index.js +34 -0
- package/lib/karin/module/utils/Network/types.js +12 -0
- package/lib/karin/module/utils/Networks.js +17 -0
- package/lib/karin/module/utils/ParseLock.js +34 -0
- package/lib/karin/module/utils/ParseOverride.js +88 -0
- package/lib/karin/module/utils/QqCardResolve.js +114 -0
- package/lib/karin/module/utils/QqPanel.js +879 -0
- package/lib/karin/module/utils/Render/coverTheme.js +256 -0
- package/lib/karin/module/utils/Render/index.js +582 -0
- package/lib/karin/module/utils/amagiClient.js +338 -0
- package/lib/karin/module/utils/avatar.js +201 -0
- package/lib/karin/module/utils/bot.js +141 -0
- package/lib/karin/module/utils/build-metadata.js +50 -0
- package/lib/karin/module/utils/changelog.js +166 -0
- package/lib/karin/module/utils/index.js +26 -0
- package/lib/karin/module/utils/loginSession.js +259 -0
- package/lib/karin/module/utils/parseTip.js +93 -0
- package/lib/karin/module/utils/runtime-report.js +140 -0
- package/lib/karin/module/utils/semver.js +72 -0
- package/lib/karin/module/utils/statisticsAggregate.js +415 -0
- package/lib/karin/platform/bilibili/bilibili.js +1805 -0
- package/lib/karin/platform/bilibili/comments.js +294 -0
- package/lib/karin/platform/bilibili/danmaku.js +555 -0
- package/lib/karin/platform/bilibili/dynamic-text.js +827 -0
- package/lib/karin/platform/bilibili/genParams.js +37 -0
- package/lib/karin/platform/bilibili/getID.js +159 -0
- package/lib/karin/platform/bilibili/index.js +24 -0
- package/lib/karin/platform/bilibili/live-status.js +40 -0
- package/lib/karin/platform/bilibili/login.js +132 -0
- package/lib/karin/platform/bilibili/push.js +1397 -0
- package/lib/karin/platform/bilibili/riskControl.js +133 -0
- package/lib/karin/platform/bilibili/search.js +299 -0
- package/lib/karin/platform/common/danmaku-types.js +12 -0
- package/lib/karin/platform/douyin/comments.js +310 -0
- package/lib/karin/platform/douyin/danmaku.js +893 -0
- package/lib/karin/platform/douyin/douyin.js +1145 -0
- package/lib/karin/platform/douyin/getID.js +94 -0
- package/lib/karin/platform/douyin/index.js +24 -0
- package/lib/karin/platform/douyin/login.js +230 -0
- package/lib/karin/platform/douyin/push/favorite.js +75 -0
- package/lib/karin/platform/douyin/push/index.js +21 -0
- package/lib/karin/platform/douyin/push/live.js +69 -0
- package/lib/karin/platform/douyin/push/post.js +60 -0
- package/lib/karin/platform/douyin/push/recommend.js +74 -0
- package/lib/karin/platform/douyin/push/render.js +621 -0
- package/lib/karin/platform/douyin/push/types.js +2 -0
- package/lib/karin/platform/douyin/push.js +1128 -0
- package/lib/karin/platform/douyin/types.js +13 -0
- package/lib/karin/platform/douyin/videoQuality.js +222 -0
- package/lib/karin/platform/douyin/workType.js +182 -0
- package/lib/karin/platform/index.js +20 -0
- package/lib/karin/platform/kuaishou/comments.js +91 -0
- package/lib/karin/platform/kuaishou/getID.js +43 -0
- package/lib/karin/platform/kuaishou/getdata.js +41 -0
- package/lib/karin/platform/kuaishou/index.js +20 -0
- package/lib/karin/platform/kuaishou/kuaishou.js +122 -0
- package/lib/karin/platform/xiaohongshu/comments.js +162 -0
- package/lib/karin/platform/xiaohongshu/getID.js +129 -0
- package/lib/karin/platform/xiaohongshu/index.js +18 -0
- package/lib/karin/platform/xiaohongshu/xiaohongshu.js +643 -0
- package/lib/karin/root.js +73 -0
- package/lib/karin/setup.js +99 -0
- package/lib/karin/types/config/amagi.js +2 -0
- package/lib/karin/types/config/app.js +2 -0
- package/lib/karin/types/config/bilibili.js +2 -0
- package/lib/karin/types/config/cookies.js +2 -0
- package/lib/karin/types/config/douyin.js +2 -0
- package/lib/karin/types/config/index.js +2 -0
- package/lib/karin/types/config/kuaishou.js +2 -0
- package/lib/karin/types/config/pushlist.js +2 -0
- package/lib/karin/types/config/request.js +2 -0
- package/lib/karin/types/config/upload.js +2 -0
- package/lib/karin/types/config/xiaohongshu.js +2 -0
- package/lib/karin/types/datatypes.js +2 -0
- package/lib/karin/types/index.js +18 -0
- package/lib/karin/types/netrorks/NetworksConfigType.js +2 -0
- package/lib/karin/types/netrorks/index.js +17 -0
- package/lib/karin/web.config.js +49 -0
- package/lib/ktr/registry.js +82 -0
- package/lib/ktr/template/_preview/components/PreviewInfoCard.js +10 -0
- package/lib/ktr/template/_preview/components/PreviewLayout.js +10 -0
- package/lib/ktr/template/_preview/components/PreviewVideoCard.js +8 -0
- package/lib/ktr/template/_preview/render.js +142 -0
- package/lib/ktr/template/_preview/types.js +2 -0
- package/lib/ktr/template/_preview/utils/status.js +31 -0
- package/lib/ktr/template/_preview/utils/time.js +12 -0
- package/lib/ktr/template/bilibili/bangumi/components/bangumi.js +127 -0
- package/lib/ktr/template/bilibili/bangumi/components/types.js +2 -0
- package/lib/ktr/template/bilibili/bangumi/index.js +12 -0
- package/lib/ktr/template/bilibili/comment/components/Comment.js +193 -0
- package/lib/ktr/template/bilibili/comment/components/types.js +2 -0
- package/lib/ktr/template/bilibili/comment/index.js +10 -0
- package/lib/ktr/template/bilibili/components/Icons.js +66 -0
- package/lib/ktr/template/bilibili/components/shared.js +93 -0
- package/lib/ktr/template/bilibili/components/types.js +2 -0
- package/lib/ktr/template/bilibili/dynamic/DYNAMIC_TYPE_ARTICLE/components/DYNAMIC_TYPE_ARTICLE.js +66 -0
- package/lib/ktr/template/bilibili/dynamic/DYNAMIC_TYPE_ARTICLE/components/types.js +2 -0
- package/lib/ktr/template/bilibili/dynamic/DYNAMIC_TYPE_ARTICLE/index.js +10 -0
- package/lib/ktr/template/bilibili/dynamic/DYNAMIC_TYPE_AV/components/DYNAMIC_TYPE_AV.js +87 -0
- package/lib/ktr/template/bilibili/dynamic/DYNAMIC_TYPE_AV/components/types.js +2 -0
- package/lib/ktr/template/bilibili/dynamic/DYNAMIC_TYPE_AV/index.js +10 -0
- package/lib/ktr/template/bilibili/dynamic/DYNAMIC_TYPE_DRAW/components/DYNAMIC_TYPE_DRAW.js +93 -0
- package/lib/ktr/template/bilibili/dynamic/DYNAMIC_TYPE_DRAW/components/types.js +2 -0
- package/lib/ktr/template/bilibili/dynamic/DYNAMIC_TYPE_DRAW/index.js +10 -0
- package/lib/ktr/template/bilibili/dynamic/DYNAMIC_TYPE_FORWARD/components/DYNAMIC_TYPE_FORWARD.js +132 -0
- package/lib/ktr/template/bilibili/dynamic/DYNAMIC_TYPE_FORWARD/components/types.js +2 -0
- package/lib/ktr/template/bilibili/dynamic/DYNAMIC_TYPE_FORWARD/index.js +10 -0
- package/lib/ktr/template/bilibili/dynamic/DYNAMIC_TYPE_LIVE_RCMD/components/DYNAMIC_TYPE_LIVE_RCMD.js +84 -0
- package/lib/ktr/template/bilibili/dynamic/DYNAMIC_TYPE_LIVE_RCMD/components/types.js +2 -0
- package/lib/ktr/template/bilibili/dynamic/DYNAMIC_TYPE_LIVE_RCMD/index.js +10 -0
- package/lib/ktr/template/bilibili/dynamic/DYNAMIC_TYPE_WORD/components/DYNAMIC_TYPE_WORD.js +38 -0
- package/lib/ktr/template/bilibili/dynamic/DYNAMIC_TYPE_WORD/components/types.js +2 -0
- package/lib/ktr/template/bilibili/dynamic/DYNAMIC_TYPE_WORD/index.js +10 -0
- package/lib/ktr/template/bilibili/dynamic/components/AdditionalCard.js +59 -0
- package/lib/ktr/template/bilibili/dynamic/components/CommonComponents.js +30 -0
- package/lib/ktr/template/bilibili/dynamic/types.js +2 -0
- package/lib/ktr/template/bilibili/qrcodeImg/components/qrcodeImg.js +48 -0
- package/lib/ktr/template/bilibili/qrcodeImg/components/types.js +3 -0
- package/lib/ktr/template/bilibili/qrcodeImg/index.js +10 -0
- package/lib/ktr/template/bilibili/userlist/components/UserList.js +53 -0
- package/lib/ktr/template/bilibili/userlist/components/types.js +3 -0
- package/lib/ktr/template/bilibili/userlist/index.js +13 -0
- package/lib/ktr/template/bilibili/videoInfo/components/types.js +2 -0
- package/lib/ktr/template/bilibili/videoInfo/components/videoInfo.js +62 -0
- package/lib/ktr/template/bilibili/videoInfo/index.js +10 -0
- package/lib/ktr/template/components/AmbientCover.js +36 -0
- package/lib/ktr/template/components/DefaultLayout.js +33 -0
- package/lib/ktr/template/components/GlowImage.js +276 -0
- package/lib/ktr/template/components/QRCodeWithAvatar.js +34 -0
- package/lib/ktr/template/components/RolldownLogo.js +6 -0
- package/lib/ktr/template/components/ViteLogo.js +6 -0
- package/lib/ktr/template/components/iconRegistry.js +173 -0
- package/lib/ktr/template/douyin/article-work/components/ArticleWork.js +99 -0
- package/lib/ktr/template/douyin/article-work/components/types.js +3 -0
- package/lib/ktr/template/douyin/article-work/index.js +10 -0
- package/lib/ktr/template/douyin/comment/components/Comment.js +189 -0
- package/lib/ktr/template/douyin/comment/components/types.js +2 -0
- package/lib/ktr/template/douyin/comment/index.js +10 -0
- package/lib/ktr/template/douyin/components/Icons.js +29 -0
- package/lib/ktr/template/douyin/components/types.js +3 -0
- package/lib/ktr/template/douyin/dynamic/components/Dynamic.js +90 -0
- package/lib/ktr/template/douyin/dynamic/components/types.js +3 -0
- package/lib/ktr/template/douyin/dynamic/index.js +10 -0
- package/lib/ktr/template/douyin/favorite-list/components/FavoriteList.js +30 -0
- package/lib/ktr/template/douyin/favorite-list/components/types.js +3 -0
- package/lib/ktr/template/douyin/favorite-list/index.js +10 -0
- package/lib/ktr/template/douyin/image-work/components/ImageWork.js +136 -0
- package/lib/ktr/template/douyin/image-work/components/types.js +2 -0
- package/lib/ktr/template/douyin/image-work/index.js +10 -0
- package/lib/ktr/template/douyin/live/components/Live.js +51 -0
- package/lib/ktr/template/douyin/live/components/types.js +3 -0
- package/lib/ktr/template/douyin/live/index.js +10 -0
- package/lib/ktr/template/douyin/musicinfo/components/MusicInfo.js +60 -0
- package/lib/ktr/template/douyin/musicinfo/components/types.js +3 -0
- package/lib/ktr/template/douyin/musicinfo/index.js +10 -0
- package/lib/ktr/template/douyin/qrcodeImg/components/qrcodeImg.js +39 -0
- package/lib/ktr/template/douyin/qrcodeImg/components/types.js +3 -0
- package/lib/ktr/template/douyin/qrcodeImg/index.js +9 -0
- package/lib/ktr/template/douyin/recommend-list/components/RecommendList.js +30 -0
- package/lib/ktr/template/douyin/recommend-list/components/types.js +3 -0
- package/lib/ktr/template/douyin/recommend-list/index.js +10 -0
- package/lib/ktr/template/douyin/user_profile/components/UserVideoList.js +54 -0
- package/lib/ktr/template/douyin/user_profile/components/types.js +3 -0
- package/lib/ktr/template/douyin/user_profile/index.js +9 -0
- package/lib/ktr/template/douyin/userlist/components/UserList.js +50 -0
- package/lib/ktr/template/douyin/userlist/components/types.js +3 -0
- package/lib/ktr/template/douyin/userlist/index.js +13 -0
- package/lib/ktr/template/douyin/video-work/components/VideoWork.js +127 -0
- package/lib/ktr/template/douyin/video-work/components/types.js +2 -0
- package/lib/ktr/template/douyin/video-work/index.js +10 -0
- package/lib/ktr/template/kuaishou/comment/components/Comment.js +88 -0
- package/lib/ktr/template/kuaishou/comment/components/types.js +2 -0
- package/lib/ktr/template/kuaishou/comment/index.js +10 -0
- package/lib/ktr/template/other/changelog/components/changelog.js +117 -0
- package/lib/ktr/template/other/changelog/components/types.js +3 -0
- package/lib/ktr/template/other/changelog/index.js +9 -0
- package/lib/ktr/template/other/handlerError/components/errorTitles.js +227 -0
- package/lib/ktr/template/other/handlerError/components/handlerError.js +392 -0
- package/lib/ktr/template/other/handlerError/components/stackHighlight.js +109 -0
- package/lib/ktr/template/other/handlerError/components/types.js +2 -0
- package/lib/ktr/template/other/handlerError/index.js +9 -0
- package/lib/ktr/template/other/help/components/Help.js +79 -0
- package/lib/ktr/template/other/help/components/types.js +3 -0
- package/lib/ktr/template/other/help/index.js +12 -0
- package/lib/ktr/template/other/live-photo-tip/components/LivePhotoTip.js +80 -0
- package/lib/ktr/template/other/live-photo-tip/components/types.js +3 -0
- package/lib/ktr/template/other/live-photo-tip/index.js +9 -0
- package/lib/ktr/template/other/qrlogin/components/qrlogin.js +58 -0
- package/lib/ktr/template/other/qrlogin/components/types.js +3 -0
- package/lib/ktr/template/other/qrlogin/index.js +9 -0
- package/lib/ktr/template/other/runtime/components/runtime.js +101 -0
- package/lib/ktr/template/other/runtime/components/types.js +3 -0
- package/lib/ktr/template/other/runtime/index.js +9 -0
- package/lib/ktr/template/other/version_warning/components/VersionWarning.js +38 -0
- package/lib/ktr/template/other/version_warning/components/types.js +3 -0
- package/lib/ktr/template/other/version_warning/index.js +9 -0
- package/lib/ktr/template/statistics/components/BoxplotGuide.js +17 -0
- package/lib/ktr/template/statistics/components/ChartCard.js +15 -0
- package/lib/ktr/template/statistics/components/KpiCard.js +14 -0
- package/lib/ktr/template/statistics/components/PlatformTable.js +20 -0
- package/lib/ktr/template/statistics/components/RankList.js +61 -0
- package/lib/ktr/template/statistics/components/SSRChart.js +22 -0
- package/lib/ktr/template/statistics/components/chartPresets.js +630 -0
- package/lib/ktr/template/statistics/components/chartTheme.js +226 -0
- package/lib/ktr/template/statistics/components/format.js +38 -0
- package/lib/ktr/template/statistics/components/types.js +9 -0
- package/lib/ktr/template/statistics/global/components/GlobalStatistics.js +246 -0
- package/lib/ktr/template/statistics/global/components/types.js +3 -0
- package/lib/ktr/template/statistics/global/index.js +9 -0
- package/lib/ktr/template/statistics/group/components/GroupStatistics.js +181 -0
- package/lib/ktr/template/statistics/group/components/types.js +3 -0
- package/lib/ktr/template/statistics/group/index.js +9 -0
- package/lib/ktr/template/types/ctx.js +2 -0
- package/lib/ktr/template/xiaohongshu/comment/components/Comment.js +99 -0
- package/lib/ktr/template/xiaohongshu/comment/components/types.js +3 -0
- package/lib/ktr/template/xiaohongshu/comment/index.js +9 -0
- package/lib/ktr/template/xiaohongshu/noteInfo/components/noteInfo.js +93 -0
- package/lib/ktr/template/xiaohongshu/noteInfo/components/types.js +2 -0
- package/lib/ktr/template/xiaohongshu/noteInfo/index.js +9 -0
- package/lib/ktr/utils/QRCodeAvatar.js +99 -0
- package/lib/ktr/utils/QRcode.js +56 -0
- package/lib/ktr/utils/avatarProxy.js +45 -0
- package/lib/ktr/utils/cn.js +11 -0
- package/lib/ktr/utils/theme.js +12 -0
- package/lib/qqFields.json +75 -0
- package/lib/qqOptions.js +44 -0
- package/lib/richtext/index.js +25 -0
- package/lib/richtext/parse/index.js +232 -0
- package/lib/richtext/react/cn.js +11 -0
- package/lib/richtext/react/index.js +221 -0
- package/lib/richtext/types.js +2 -0
- package/lib/schema.js +352 -0
- package/lib/webui.js +341 -0
- package/package.json +88 -0
- package/resources/image/bilibili/RICH_TEXT_NODE_TYPE/RICH_TEXT_NODE_TYPE_BV.svg +1 -0
- package/resources/image/bilibili/RICH_TEXT_NODE_TYPE/RICH_TEXT_NODE_TYPE_GOODS.svg +1 -0
- package/resources/image/bilibili/RICH_TEXT_NODE_TYPE/RICH_TEXT_NODE_TYPE_LOTTERY.svg +1 -0
- package/resources/image/bilibili/RICH_TEXT_NODE_TYPE/RICH_TEXT_NODE_TYPE_VIEW_PICTURE.svg +1 -0
- package/resources/image/bilibili/RICH_TEXT_NODE_TYPE/RICH_TEXT_NODE_TYPE_VOTE.svg +1 -0
- package/resources/image/bilibili/RICH_TEXT_NODE_TYPE/RICH_TEXT_NODE_TYPE_WEB.svg +1 -0
- package/resources/image/bilibili/bilibili-dark.png +0 -0
- package/resources/image/bilibili/bilibili-light.png +0 -0
- package/resources/image/bilibili/bilibili.png +0 -0
- package/resources/image/bilibili/id-dark.svg +1 -0
- package/resources/image/bilibili/id-light.svg +1 -0
- package/resources/image/bilibili/level/lv0.svg +1 -0
- package/resources/image/bilibili/level/lv1.svg +1 -0
- package/resources/image/bilibili/level/lv2.svg +1 -0
- package/resources/image/bilibili/level/lv3.svg +1 -0
- package/resources/image/bilibili/level/lv4.svg +1 -0
- package/resources/image/bilibili/level/lv5.svg +1 -0
- package/resources/image/bilibili/level/lv6.svg +1 -0
- package/resources/image/bilibili/level/lv7.svg +1 -0
- package/resources/image/bilibili/like-dark.svg +6 -0
- package/resources/image/bilibili/like-light.svg +6 -0
- package/resources/image/bilibili/play.svg +28 -0
- package/resources/image/bilibili/res-local1.png +0 -0
- package/resources/image/bilibili/up_pb.svg +1 -0
- package/resources/image/bilibili//347/233/264/346/222/255/344/270/255.png +0 -0
- package/resources/image/douyin/douyin.png +0 -0
- package/resources/image/douyin/dylogo-dark.svg +23 -0
- package/resources/image/douyin/dylogo-light.svg +38 -0
- package/resources/image/douyin/search-dark.svg +1 -0
- package/resources/image/douyin/search-light.svg +3 -0
- package/resources/image/douyin//346/212/226/351/237/263-/347/233/264/346/222/255/344/270/255.png +0 -0
- package/resources/image/frame-logo.png +0 -0
- package/resources/image/kuaishou/logo.png +0 -0
- package/resources/image/other/handlerError/Milky.png +0 -0
- package/resources/image/other/handlerError/chronocat.svg +43 -0
- package/resources/image/other/handlerError/conwechat.webp +0 -0
- package/resources/image/other/handlerError/gocq-http.webp +0 -0
- package/resources/image/other/handlerError/lagrange.webp +0 -0
- package/resources/image/other/handlerError/llonebot.webp +0 -0
- package/resources/image/other/handlerError/napcat.webp +0 -0
- package/resources/image/other/handlerError/onebot.png +0 -0
- package/resources/image/other/handlerError/satori.png +0 -0
- package/resources/image/xiaohongshu/logo.png +0 -0
- package/resources/template/style.css +23708 -0
- package/resources/vite.svg +130 -0
- package/scripts/build.mjs +129 -0
- package/scripts/check-schema.cjs +55 -0
- package/scripts/deploy.mjs +78 -0
- package/scripts/make-demo-placeholders.mjs +112 -0
- package/scripts/patch-deps.mjs +46 -0
- package/scripts/patch-webui.mjs +112 -0
- package/scripts/smoke-commands.cjs +39 -0
- package/scripts/smoke-config.cjs +122 -0
- package/scripts/smoke-douyin.cjs +170 -0
- package/scripts/smoke-koishi-commands.cjs +210 -0
- package/scripts/smoke-login.cjs +110 -0
- package/scripts/smoke-parse.cjs +54 -0
- package/scripts/smoke-push-mock.cjs +114 -0
- package/scripts/smoke-push.cjs +104 -0
- package/scripts/smoke-qq.cjs +147 -0
- package/scripts/smoke-qqcard.cjs +108 -0
- package/scripts/smoke-qqpanel.cjs +205 -0
- package/scripts/smoke-render.cjs +106 -0
- package/scripts/smoke-server.cjs +94 -0
- package/src/compat/axios.ts +5 -0
- package/src/compat/cron.ts +86 -0
- package/src/compat/esm.ts +13 -0
- package/src/compat/express.ts +5 -0
- package/src/compat/heroui.tsx +53 -0
- package/src/compat/lodash.ts +32 -0
- package/src/compat/logger.ts +121 -0
- package/src/compat/node-karin.ts +1064 -0
- package/src/compat/phosphor-icons.tsx +25 -0
- package/src/compat/qqcard.ts +251 -0
- package/src/compat/root.ts +44 -0
- package/src/compat/runtime.ts +120 -0
- package/src/compat/segment.ts +161 -0
- package/src/compat/sqlite3.ts +123 -0
- package/src/compat/start.ts +3 -0
- package/src/compat/syncConfig.ts +64 -0
- package/src/compat/template-react.ts +54 -0
- package/src/compat/text.ts +58 -0
- package/src/compat/yaml.ts +6 -0
- package/src/configBridge.ts +94 -0
- package/src/index.ts +769 -0
- package/src/karin/app.ts +1 -0
- package/src/karin/apps/__ready.ts +14 -0
- package/src/karin/apps/admin.ts +127 -0
- package/src/karin/apps/help.ts +228 -0
- package/src/karin/apps/push.ts +327 -0
- package/src/karin/apps/qrlogin.ts +160 -0
- package/src/karin/apps/statistics.ts +203 -0
- package/src/karin/apps/testPush.ts +226 -0
- package/src/karin/apps/tools.ts +510 -0
- package/src/karin/apps/update.ts +239 -0
- package/src/karin/index.ts +9 -0
- package/src/karin/module/db/bilibili.ts +1089 -0
- package/src/karin/module/db/douyin.ts +1218 -0
- package/src/karin/module/db/index.ts +159 -0
- package/src/karin/module/db/migration.ts +249 -0
- package/src/karin/module/db/statistics.ts +817 -0
- package/src/karin/module/index.ts +37 -0
- package/src/karin/module/server/constants/routes.ts +53 -0
- package/src/karin/module/server/controllers/bots.ts +289 -0
- package/src/karin/module/server/controllers/config.ts +85 -0
- package/src/karin/module/server/controllers/video.ts +212 -0
- package/src/karin/module/server/index.ts +11 -0
- package/src/karin/module/server/middlewares/auth.ts +147 -0
- package/src/karin/module/server/routes/amagi.ts +57 -0
- package/src/karin/module/server/routes/api.ts +42 -0
- package/src/karin/module/server/routes/index.ts +68 -0
- package/src/karin/module/server/routes/ssr.ts +14 -0
- package/src/karin/module/server/routes/static.ts +43 -0
- package/src/karin/module/utils/Base.ts +497 -0
- package/src/karin/module/utils/CardParser.ts +503 -0
- package/src/karin/module/utils/Common.ts +448 -0
- package/src/karin/module/utils/Config.ts +443 -0
- package/src/karin/module/utils/EmojiReaction.ts +268 -0
- package/src/karin/module/utils/ErrorHandler/handler.ts +124 -0
- package/src/karin/module/utils/ErrorHandler/index.ts +48 -0
- package/src/karin/module/utils/ErrorHandler/render.ts +180 -0
- package/src/karin/module/utils/ErrorHandler/sender.ts +192 -0
- package/src/karin/module/utils/ErrorHandler/strategy.ts +41 -0
- package/src/karin/module/utils/ErrorHandler/types.ts +122 -0
- package/src/karin/module/utils/ErrorHandler/utils.ts +108 -0
- package/src/karin/module/utils/FFmpeg.ts +357 -0
- package/src/karin/module/utils/ImageHelper.ts +90 -0
- package/src/karin/module/utils/ImageSlice.ts +383 -0
- package/src/karin/module/utils/MotionPhoto.ts +297 -0
- package/src/karin/module/utils/Network/Downloader.ts +481 -0
- package/src/karin/module/utils/Network/ImageDownloader.ts +291 -0
- package/src/karin/module/utils/Network/Network.ts +344 -0
- package/src/karin/module/utils/Network/ThrottleStream.ts +76 -0
- package/src/karin/module/utils/Network/constants.ts +135 -0
- package/src/karin/module/utils/Network/helpers.ts +183 -0
- package/src/karin/module/utils/Network/index.ts +28 -0
- package/src/karin/module/utils/Network/types.ts +46 -0
- package/src/karin/module/utils/Networks.ts +20 -0
- package/src/karin/module/utils/ParseLock.ts +33 -0
- package/src/karin/module/utils/ParseOverride.ts +128 -0
- package/src/karin/module/utils/QqCardResolve.ts +116 -0
- package/src/karin/module/utils/QqPanel.ts +880 -0
- package/src/karin/module/utils/Render/coverTheme.ts +294 -0
- package/src/karin/module/utils/Render/index.ts +574 -0
- package/src/karin/module/utils/amagiClient.ts +368 -0
- package/src/karin/module/utils/avatar.ts +220 -0
- package/src/karin/module/utils/bot.ts +115 -0
- package/src/karin/module/utils/build-metadata.ts +60 -0
- package/src/karin/module/utils/changelog.ts +175 -0
- package/src/karin/module/utils/index.ts +10 -0
- package/src/karin/module/utils/loginSession.ts +291 -0
- package/src/karin/module/utils/parseTip.ts +59 -0
- package/src/karin/module/utils/runtime-report.ts +142 -0
- package/src/karin/module/utils/semver.ts +60 -0
- package/src/karin/module/utils/statisticsAggregate.ts +507 -0
- package/src/karin/platform/bilibili/bilibili.ts +2004 -0
- package/src/karin/platform/bilibili/comments.ts +397 -0
- package/src/karin/platform/bilibili/danmaku.ts +653 -0
- package/src/karin/platform/bilibili/dynamic-text.ts +926 -0
- package/src/karin/platform/bilibili/genParams.ts +30 -0
- package/src/karin/platform/bilibili/getID.ts +163 -0
- package/src/karin/platform/bilibili/index.ts +8 -0
- package/src/karin/platform/bilibili/live-status.ts +36 -0
- package/src/karin/platform/bilibili/login.ts +130 -0
- package/src/karin/platform/bilibili/push.ts +1615 -0
- package/src/karin/platform/bilibili/riskControl.ts +114 -0
- package/src/karin/platform/bilibili/search.ts +305 -0
- package/src/karin/platform/common/danmaku-types.ts +18 -0
- package/src/karin/platform/douyin/comments.ts +404 -0
- package/src/karin/platform/douyin/danmaku.ts +1187 -0
- package/src/karin/platform/douyin/douyin.ts +1244 -0
- package/src/karin/platform/douyin/getID.ts +101 -0
- package/src/karin/platform/douyin/index.ts +8 -0
- package/src/karin/platform/douyin/login.ts +251 -0
- package/src/karin/platform/douyin/push/favorite.ts +97 -0
- package/src/karin/platform/douyin/push/index.ts +5 -0
- package/src/karin/platform/douyin/push/live.ts +86 -0
- package/src/karin/platform/douyin/push/post.ts +75 -0
- package/src/karin/platform/douyin/push/recommend.ts +96 -0
- package/src/karin/platform/douyin/push/render.ts +768 -0
- package/src/karin/platform/douyin/push/types.ts +41 -0
- package/src/karin/platform/douyin/push.ts +1241 -0
- package/src/karin/platform/douyin/types.ts +151 -0
- package/src/karin/platform/douyin/videoQuality.ts +234 -0
- package/src/karin/platform/douyin/workType.ts +238 -0
- package/src/karin/platform/index.ts +4 -0
- package/src/karin/platform/kuaishou/comments.ts +114 -0
- package/src/karin/platform/kuaishou/getID.ts +48 -0
- package/src/karin/platform/kuaishou/getdata.ts +78 -0
- package/src/karin/platform/kuaishou/index.ts +4 -0
- package/src/karin/platform/kuaishou/kuaishou.ts +130 -0
- package/src/karin/platform/xiaohongshu/comments.ts +227 -0
- package/src/karin/platform/xiaohongshu/getID.ts +127 -0
- package/src/karin/platform/xiaohongshu/index.ts +2 -0
- package/src/karin/platform/xiaohongshu/xiaohongshu.ts +733 -0
- package/src/karin/root.ts +78 -0
- package/src/karin/setup.ts +77 -0
- package/src/karin/types/config/amagi.ts +46 -0
- package/src/karin/types/config/app.ts +129 -0
- package/src/karin/types/config/bilibili.ts +121 -0
- package/src/karin/types/config/cookies.ts +11 -0
- package/src/karin/types/config/douyin.ts +93 -0
- package/src/karin/types/config/index.ts +41 -0
- package/src/karin/types/config/kuaishou.ts +10 -0
- package/src/karin/types/config/pushlist.ts +55 -0
- package/src/karin/types/config/request.ts +27 -0
- package/src/karin/types/config/upload.ts +49 -0
- package/src/karin/types/config/xiaohongshu.ts +24 -0
- package/src/karin/types/datatypes.ts +42 -0
- package/src/karin/types/index.ts +4 -0
- package/src/karin/types/netrorks/NetworksConfigType.ts +61 -0
- package/src/karin/types/netrorks/index.ts +1 -0
- package/src/karin/web.config.ts +48 -0
- package/src/ktr/registry.ts +55 -0
- package/src/ktr/template/_preview/components/PreviewInfoCard.tsx +33 -0
- package/src/ktr/template/_preview/components/PreviewLayout.tsx +65 -0
- package/src/ktr/template/_preview/components/PreviewVideoCard.tsx +22 -0
- package/src/ktr/template/_preview/render.ts +158 -0
- package/src/ktr/template/_preview/types.ts +15 -0
- package/src/ktr/template/_preview/utils/status.ts +29 -0
- package/src/ktr/template/_preview/utils/time.ts +8 -0
- package/src/ktr/template/bilibili/bangumi/components/bangumi.tsx +462 -0
- package/src/ktr/template/bilibili/bangumi/components/types.ts +36 -0
- package/src/ktr/template/bilibili/bangumi/index.tsx +9 -0
- package/src/ktr/template/bilibili/comment/components/Comment.tsx +657 -0
- package/src/ktr/template/bilibili/comment/components/types.ts +26 -0
- package/src/ktr/template/bilibili/comment/index.tsx +11 -0
- package/src/ktr/template/bilibili/components/Icons.tsx +209 -0
- package/src/ktr/template/bilibili/components/shared.tsx +162 -0
- package/src/ktr/template/bilibili/components/types.ts +388 -0
- package/src/ktr/template/bilibili/dynamic/DYNAMIC_TYPE_ARTICLE/components/DYNAMIC_TYPE_ARTICLE.tsx +251 -0
- package/src/ktr/template/bilibili/dynamic/DYNAMIC_TYPE_ARTICLE/components/types.ts +67 -0
- package/src/ktr/template/bilibili/dynamic/DYNAMIC_TYPE_ARTICLE/index.tsx +12 -0
- package/src/ktr/template/bilibili/dynamic/DYNAMIC_TYPE_AV/components/DYNAMIC_TYPE_AV.tsx +238 -0
- package/src/ktr/template/bilibili/dynamic/DYNAMIC_TYPE_AV/components/types.ts +39 -0
- package/src/ktr/template/bilibili/dynamic/DYNAMIC_TYPE_AV/index.tsx +12 -0
- package/src/ktr/template/bilibili/dynamic/DYNAMIC_TYPE_DRAW/components/DYNAMIC_TYPE_DRAW.tsx +205 -0
- package/src/ktr/template/bilibili/dynamic/DYNAMIC_TYPE_DRAW/components/types.ts +20 -0
- package/src/ktr/template/bilibili/dynamic/DYNAMIC_TYPE_DRAW/index.tsx +11 -0
- package/src/ktr/template/bilibili/dynamic/DYNAMIC_TYPE_FORWARD/components/DYNAMIC_TYPE_FORWARD.tsx +348 -0
- package/src/ktr/template/bilibili/dynamic/DYNAMIC_TYPE_FORWARD/components/types.ts +16 -0
- package/src/ktr/template/bilibili/dynamic/DYNAMIC_TYPE_FORWARD/index.tsx +12 -0
- package/src/ktr/template/bilibili/dynamic/DYNAMIC_TYPE_LIVE_RCMD/components/DYNAMIC_TYPE_LIVE_RCMD.tsx +264 -0
- package/src/ktr/template/bilibili/dynamic/DYNAMIC_TYPE_LIVE_RCMD/components/types.ts +32 -0
- package/src/ktr/template/bilibili/dynamic/DYNAMIC_TYPE_LIVE_RCMD/index.tsx +12 -0
- package/src/ktr/template/bilibili/dynamic/DYNAMIC_TYPE_WORD/components/DYNAMIC_TYPE_WORD.tsx +68 -0
- package/src/ktr/template/bilibili/dynamic/DYNAMIC_TYPE_WORD/components/types.ts +14 -0
- package/src/ktr/template/bilibili/dynamic/DYNAMIC_TYPE_WORD/index.tsx +12 -0
- package/src/ktr/template/bilibili/dynamic/components/AdditionalCard.tsx +165 -0
- package/src/ktr/template/bilibili/dynamic/components/CommonComponents.tsx +142 -0
- package/src/ktr/template/bilibili/dynamic/types.ts +382 -0
- package/src/ktr/template/bilibili/qrcodeImg/components/qrcodeImg.tsx +179 -0
- package/src/ktr/template/bilibili/qrcodeImg/components/types.ts +9 -0
- package/src/ktr/template/bilibili/qrcodeImg/index.tsx +12 -0
- package/src/ktr/template/bilibili/userlist/components/UserList.tsx +246 -0
- package/src/ktr/template/bilibili/userlist/components/types.ts +35 -0
- package/src/ktr/template/bilibili/userlist/index.tsx +11 -0
- package/src/ktr/template/bilibili/videoInfo/components/types.ts +28 -0
- package/src/ktr/template/bilibili/videoInfo/components/videoInfo.tsx +217 -0
- package/src/ktr/template/bilibili/videoInfo/index.tsx +12 -0
- package/src/ktr/template/components/AmbientCover.tsx +50 -0
- package/src/ktr/template/components/DefaultLayout.tsx +166 -0
- package/src/ktr/template/components/GlowImage.tsx +572 -0
- package/src/ktr/template/components/QRCodeWithAvatar.tsx +52 -0
- package/src/ktr/template/components/RolldownLogo.tsx +110 -0
- package/src/ktr/template/components/ViteLogo.tsx +134 -0
- package/src/ktr/template/components/iconRegistry.ts +294 -0
- package/src/ktr/template/douyin/article-work/components/ArticleWork.tsx +301 -0
- package/src/ktr/template/douyin/article-work/components/types.ts +53 -0
- package/src/ktr/template/douyin/article-work/index.tsx +12 -0
- package/src/ktr/template/douyin/comment/components/Comment.tsx +612 -0
- package/src/ktr/template/douyin/comment/components/types.ts +85 -0
- package/src/ktr/template/douyin/comment/index.tsx +11 -0
- package/src/ktr/template/douyin/components/Icons.tsx +75 -0
- package/src/ktr/template/douyin/components/types.ts +83 -0
- package/src/ktr/template/douyin/dynamic/components/Dynamic.tsx +283 -0
- package/src/ktr/template/douyin/dynamic/components/types.ts +46 -0
- package/src/ktr/template/douyin/dynamic/index.tsx +11 -0
- package/src/ktr/template/douyin/favorite-list/components/FavoriteList.tsx +201 -0
- package/src/ktr/template/douyin/favorite-list/components/types.ts +38 -0
- package/src/ktr/template/douyin/favorite-list/index.tsx +12 -0
- package/src/ktr/template/douyin/image-work/components/ImageWork.tsx +437 -0
- package/src/ktr/template/douyin/image-work/components/types.ts +78 -0
- package/src/ktr/template/douyin/image-work/index.tsx +11 -0
- package/src/ktr/template/douyin/live/components/Live.tsx +229 -0
- package/src/ktr/template/douyin/live/components/types.ts +47 -0
- package/src/ktr/template/douyin/live/index.tsx +11 -0
- package/src/ktr/template/douyin/musicinfo/components/MusicInfo.tsx +187 -0
- package/src/ktr/template/douyin/musicinfo/components/types.ts +31 -0
- package/src/ktr/template/douyin/musicinfo/index.tsx +11 -0
- package/src/ktr/template/douyin/qrcodeImg/components/qrcodeImg.tsx +170 -0
- package/src/ktr/template/douyin/qrcodeImg/components/types.ts +9 -0
- package/src/ktr/template/douyin/qrcodeImg/index.tsx +9 -0
- package/src/ktr/template/douyin/recommend-list/components/RecommendList.tsx +201 -0
- package/src/ktr/template/douyin/recommend-list/components/types.ts +37 -0
- package/src/ktr/template/douyin/recommend-list/index.tsx +12 -0
- package/src/ktr/template/douyin/user_profile/components/UserVideoList.tsx +225 -0
- package/src/ktr/template/douyin/user_profile/components/types.ts +71 -0
- package/src/ktr/template/douyin/user_profile/index.tsx +9 -0
- package/src/ktr/template/douyin/userlist/components/UserList.tsx +229 -0
- package/src/ktr/template/douyin/userlist/components/types.ts +35 -0
- package/src/ktr/template/douyin/userlist/index.tsx +11 -0
- package/src/ktr/template/douyin/video-work/components/VideoWork.tsx +398 -0
- package/src/ktr/template/douyin/video-work/components/types.ts +80 -0
- package/src/ktr/template/douyin/video-work/index.tsx +11 -0
- package/src/ktr/template/font.css +6071 -0
- package/src/ktr/template/global.d.ts +69 -0
- package/src/ktr/template/kuaishou/comment/components/Comment.tsx +207 -0
- package/src/ktr/template/kuaishou/comment/components/types.ts +47 -0
- package/src/ktr/template/kuaishou/comment/index.tsx +11 -0
- package/src/ktr/template/other/changelog/components/changelog.tsx +411 -0
- package/src/ktr/template/other/changelog/components/types.ts +21 -0
- package/src/ktr/template/other/changelog/index.tsx +9 -0
- package/src/ktr/template/other/handlerError/components/errorTitles.ts +224 -0
- package/src/ktr/template/other/handlerError/components/handlerError.tsx +1076 -0
- package/src/ktr/template/other/handlerError/components/stackHighlight.ts +127 -0
- package/src/ktr/template/other/handlerError/components/types.ts +124 -0
- package/src/ktr/template/other/handlerError/index.tsx +9 -0
- package/src/ktr/template/other/help/components/Help.tsx +249 -0
- package/src/ktr/template/other/help/components/types.ts +47 -0
- package/src/ktr/template/other/help/index.tsx +9 -0
- package/src/ktr/template/other/live-photo-tip/components/LivePhotoTip.tsx +295 -0
- package/src/ktr/template/other/live-photo-tip/components/types.ts +8 -0
- package/src/ktr/template/other/live-photo-tip/index.tsx +9 -0
- package/src/ktr/template/other/qrlogin/components/qrlogin.tsx +295 -0
- package/src/ktr/template/other/qrlogin/components/types.ts +11 -0
- package/src/ktr/template/other/qrlogin/index.tsx +9 -0
- package/src/ktr/template/other/runtime/components/runtime.tsx +347 -0
- package/src/ktr/template/other/runtime/components/types.ts +75 -0
- package/src/ktr/template/other/runtime/index.tsx +9 -0
- package/src/ktr/template/other/version_warning/components/VersionWarning.tsx +392 -0
- package/src/ktr/template/other/version_warning/components/types.ts +8 -0
- package/src/ktr/template/other/version_warning/index.tsx +9 -0
- package/src/ktr/template/statistics/components/BoxplotGuide.tsx +57 -0
- package/src/ktr/template/statistics/components/ChartCard.tsx +40 -0
- package/src/ktr/template/statistics/components/KpiCard.tsx +39 -0
- package/src/ktr/template/statistics/components/PlatformTable.tsx +83 -0
- package/src/ktr/template/statistics/components/RankList.tsx +127 -0
- package/src/ktr/template/statistics/components/SSRChart.tsx +41 -0
- package/src/ktr/template/statistics/components/chartPresets.ts +700 -0
- package/src/ktr/template/statistics/components/chartTheme.ts +234 -0
- package/src/ktr/template/statistics/components/format.ts +33 -0
- package/src/ktr/template/statistics/components/types.ts +92 -0
- package/src/ktr/template/statistics/global/components/GlobalStatistics.tsx +597 -0
- package/src/ktr/template/statistics/global/components/types.ts +138 -0
- package/src/ktr/template/statistics/global/index.tsx +9 -0
- package/src/ktr/template/statistics/group/components/GroupStatistics.tsx +468 -0
- package/src/ktr/template/statistics/group/components/types.ts +113 -0
- package/src/ktr/template/statistics/group/index.tsx +9 -0
- package/src/ktr/template/style.css +65 -0
- package/src/ktr/template/types/ctx.ts +42 -0
- package/src/ktr/template/xiaohongshu/comment/components/Comment.tsx +264 -0
- package/src/ktr/template/xiaohongshu/comment/components/types.ts +119 -0
- package/src/ktr/template/xiaohongshu/comment/index.tsx +9 -0
- package/src/ktr/template/xiaohongshu/noteInfo/components/noteInfo.tsx +271 -0
- package/src/ktr/template/xiaohongshu/noteInfo/components/types.ts +66 -0
- package/src/ktr/template/xiaohongshu/noteInfo/index.tsx +9 -0
- package/src/ktr/utils/QRCodeAvatar.ts +102 -0
- package/src/ktr/utils/QRcode.ts +65 -0
- package/src/ktr/utils/avatarProxy.ts +42 -0
- package/src/ktr/utils/cn.ts +6 -0
- package/src/ktr/utils/theme.ts +10 -0
- package/src/qqFields.json +75 -0
- package/src/qqOptions.ts +64 -0
- package/src/richtext/index.ts +9 -0
- package/src/richtext/parse/index.ts +271 -0
- package/src/richtext/react/cn.ts +6 -0
- package/src/richtext/react/index.tsx +582 -0
- package/src/richtext/types.ts +320 -0
- package/src/schema.ts +372 -0
- package/src/webui.ts +352 -0
|
@@ -0,0 +1,893 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.getDouyinResolution = getDouyinResolution;
|
|
7
|
+
exports.generateDouyinASS = generateDouyinASS;
|
|
8
|
+
exports.burnDouyinDanmaku = burnDouyinDanmaku;
|
|
9
|
+
/**
|
|
10
|
+
* 抖音弹幕处理模块
|
|
11
|
+
* 负责抖音弹幕数据转换、ASS 字幕生成、视频烧录等功能
|
|
12
|
+
* 抖音弹幕只有滚动弹幕:纯文字弹幕走 ASS \move 烧录;
|
|
13
|
+
* 含平台表情或被选中展示点赞角标的弹幕预渲染为透明 PNG 条,通过 overlay 滤镜叠加(libass 不支持内嵌图片)
|
|
14
|
+
*/
|
|
15
|
+
const node_crypto_1 = __importDefault(require("node:crypto"));
|
|
16
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
17
|
+
const node_os_1 = __importDefault(require("node:os"));
|
|
18
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
19
|
+
const node_karin_1 = require("../../../compat/node-karin.js");
|
|
20
|
+
const utils_1 = require("../../module/utils");
|
|
21
|
+
const amagiClient_1 = require("../../module/utils/amagiClient");
|
|
22
|
+
// ==================== 编码器检测与参数 ====================
|
|
23
|
+
const ENCODER_PRIORITY = {
|
|
24
|
+
h264: ['h264_nvenc', 'h264_qsv', 'h264_amf', 'libx264'],
|
|
25
|
+
h265: ['hevc_nvenc', 'hevc_qsv', 'hevc_amf', 'libx265'],
|
|
26
|
+
av1: ['av1_nvenc', 'av1_qsv', 'av1_amf', 'libsvtav1', 'libaom-av1']
|
|
27
|
+
};
|
|
28
|
+
const SOFTWARE_FALLBACK = {
|
|
29
|
+
h264: 'libx264',
|
|
30
|
+
h265: 'libx265',
|
|
31
|
+
av1: 'libsvtav1'
|
|
32
|
+
};
|
|
33
|
+
const cachedEncoders = {};
|
|
34
|
+
/**
|
|
35
|
+
* 按优先级探测可用的硬件/软件编码器(结果缓存)
|
|
36
|
+
* @param codec 目标编码格式
|
|
37
|
+
* @returns 可用的 ffmpeg 编码器名
|
|
38
|
+
*/
|
|
39
|
+
async function detectEncoder(codec) {
|
|
40
|
+
if (cachedEncoders[codec])
|
|
41
|
+
return cachedEncoders[codec];
|
|
42
|
+
node_karin_1.logger.debug(`[DouyinDanmaku] 开始检测 ${codec.toUpperCase()} 编码器...`);
|
|
43
|
+
for (const encoder of ENCODER_PRIORITY[codec]) {
|
|
44
|
+
try {
|
|
45
|
+
const result = await (0, node_karin_1.ffmpeg)(`-f lavfi -i color=c=black:s=320x240:d=0.1 -c:v ${encoder} -f null -`);
|
|
46
|
+
if (result.status) {
|
|
47
|
+
cachedEncoders[codec] = encoder;
|
|
48
|
+
node_karin_1.logger.info(`[DouyinDanmaku] 使用 ${codec.toUpperCase()} 编码器: ${encoder}`);
|
|
49
|
+
return encoder;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
/* ignore */
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
const fallback = SOFTWARE_FALLBACK[codec];
|
|
57
|
+
cachedEncoders[codec] = fallback;
|
|
58
|
+
node_karin_1.logger.info(`[DouyinDanmaku] 回退到软件编码器: ${fallback}`);
|
|
59
|
+
return fallback;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* 获取视频平均码率
|
|
63
|
+
* @param path 视频文件路径
|
|
64
|
+
* @returns 平均码率(kbps),获取失败返回 0
|
|
65
|
+
*/
|
|
66
|
+
async function getVideoBitrate(path) {
|
|
67
|
+
// 用文件大小和时长计算平均码率,这是最准确的方式
|
|
68
|
+
try {
|
|
69
|
+
const fileSize = node_fs_1.default.statSync(path).size;
|
|
70
|
+
const { stdout } = await (0, node_karin_1.ffprobe)(`-v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 "${path}"`);
|
|
71
|
+
const duration = parseFloat(stdout.trim());
|
|
72
|
+
if (duration > 0 && fileSize > 0) {
|
|
73
|
+
return Math.round((fileSize * 8) / duration / 1000);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
/* ignore */
|
|
78
|
+
}
|
|
79
|
+
// 尝试从流信息获取
|
|
80
|
+
try {
|
|
81
|
+
const { stdout } = await (0, node_karin_1.ffprobe)(`-v error -select_streams v:0 -show_entries stream=bit_rate -of default=noprint_wrappers=1:nokey=1 "${path}"`);
|
|
82
|
+
const bitrate = parseInt(stdout.trim());
|
|
83
|
+
if (bitrate > 0)
|
|
84
|
+
return Math.round(bitrate / 1000);
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
/* ignore */
|
|
88
|
+
}
|
|
89
|
+
return 0;
|
|
90
|
+
}
|
|
91
|
+
/** 各编码器的目标码率模式参数模板 */
|
|
92
|
+
const BITRATE_MODE_PARAMS = {
|
|
93
|
+
h264_nvenc: ({ b, m, u }) => `-c:v h264_nvenc -preset p4 -rc vbr -b:v ${b} -maxrate ${m} -bufsize ${u}`,
|
|
94
|
+
h264_qsv: ({ b, m, u }) => `-c:v h264_qsv -preset medium -b:v ${b} -maxrate ${m} -bufsize ${u}`,
|
|
95
|
+
h264_amf: ({ b, m }) => `-c:v h264_amf -quality balanced -rc vbr_peak -b:v ${b} -maxrate ${m}`,
|
|
96
|
+
libx264: ({ b, m, u, t }) => `-c:v libx264 -preset medium -b:v ${b} -maxrate ${m} -bufsize ${u} -threads ${t}`,
|
|
97
|
+
hevc_nvenc: ({ b, m, u }) => `-c:v hevc_nvenc -preset p4 -rc vbr -b:v ${b} -maxrate ${m} -bufsize ${u}`,
|
|
98
|
+
hevc_qsv: ({ b, m, u }) => `-c:v hevc_qsv -preset medium -b:v ${b} -maxrate ${m} -bufsize ${u}`,
|
|
99
|
+
hevc_amf: ({ b, m }) => `-c:v hevc_amf -quality balanced -rc vbr_peak -b:v ${b} -maxrate ${m}`,
|
|
100
|
+
libx265: ({ b, m, u, t }) => `-c:v libx265 -preset medium -b:v ${b} -maxrate ${m} -bufsize ${u} -threads ${t}`,
|
|
101
|
+
av1_nvenc: ({ b, m, u }) => `-c:v av1_nvenc -preset p4 -rc vbr -b:v ${b} -maxrate ${m} -bufsize ${u}`,
|
|
102
|
+
av1_qsv: ({ b, m, u }) => `-c:v av1_qsv -preset medium -b:v ${b} -maxrate ${m} -bufsize ${u}`,
|
|
103
|
+
av1_amf: ({ b, m }) => `-c:v av1_amf -quality balanced -rc vbr_peak -b:v ${b} -maxrate ${m}`,
|
|
104
|
+
libsvtav1: ({ b, m, u, t }) => `-c:v libsvtav1 -preset 6 -b:v ${b} -maxrate ${m} -bufsize ${u} -threads ${t}`,
|
|
105
|
+
'libaom-av1': ({ b, m, u, t }) => `-c:v libaom-av1 -cpu-used 4 -b:v ${b} -maxrate ${m} -bufsize ${u} -threads ${t}`
|
|
106
|
+
};
|
|
107
|
+
/** 各编码器的 CRF/CQ 兜底模式参数模板(无码率信息时使用) */
|
|
108
|
+
const CRF_MODE_PARAMS = {
|
|
109
|
+
h264_nvenc: () => '-c:v h264_nvenc -preset p4 -rc vbr -cq 23',
|
|
110
|
+
h264_qsv: () => '-c:v h264_qsv -preset medium -global_quality 23',
|
|
111
|
+
h264_amf: () => '-c:v h264_amf -quality balanced -rc cqp -qp_i 23 -qp_p 23',
|
|
112
|
+
libx264: (t) => `-c:v libx264 -crf 23 -preset medium -threads ${t}`,
|
|
113
|
+
hevc_nvenc: () => '-c:v hevc_nvenc -preset p4 -rc vbr -cq 28',
|
|
114
|
+
hevc_qsv: () => '-c:v hevc_qsv -preset medium -global_quality 28',
|
|
115
|
+
hevc_amf: () => '-c:v hevc_amf -quality balanced -rc cqp -qp_i 28 -qp_p 28',
|
|
116
|
+
libx265: (t) => `-c:v libx265 -crf 28 -preset medium -threads ${t}`,
|
|
117
|
+
av1_nvenc: () => '-c:v av1_nvenc -preset p4 -rc vbr -cq 30',
|
|
118
|
+
av1_qsv: () => '-c:v av1_qsv -preset medium -global_quality 30',
|
|
119
|
+
av1_amf: () => '-c:v av1_amf -quality balanced -rc cqp -qp_i 30 -qp_p 30',
|
|
120
|
+
libsvtav1: (t) => `-c:v libsvtav1 -crf 30 -preset 6 -threads ${t}`,
|
|
121
|
+
'libaom-av1': (t) => `-c:v libaom-av1 -crf 30 -cpu-used 4 -threads ${t}`
|
|
122
|
+
};
|
|
123
|
+
/**
|
|
124
|
+
* 获取编码器参数
|
|
125
|
+
* @param encoder detectEncoder 探测到的编码器名
|
|
126
|
+
* @param targetBitrate 源视频平均码率(kbps),为空或 0 时使用 CRF/CQ 兜底模式
|
|
127
|
+
* @returns ffmpeg 视频编码参数串
|
|
128
|
+
*/
|
|
129
|
+
function getEncoderParams(encoder, targetBitrate) {
|
|
130
|
+
const threads = Math.max(1, Math.floor(node_os_1.default.cpus().length / 2));
|
|
131
|
+
// 有码率时用目标码率模式
|
|
132
|
+
// 二次编码会损失画质,目标码率设为原视频的 1.4 倍补偿,确保输出更清晰
|
|
133
|
+
if (targetBitrate && targetBitrate > 0) {
|
|
134
|
+
const adjusted = Math.round(targetBitrate * 1.4);
|
|
135
|
+
const params = {
|
|
136
|
+
b: `${adjusted}k`,
|
|
137
|
+
m: `${Math.round(adjusted * 2.5)}k`,
|
|
138
|
+
u: `${Math.round(adjusted * 4)}k`,
|
|
139
|
+
t: threads
|
|
140
|
+
};
|
|
141
|
+
const build = BITRATE_MODE_PARAMS[encoder] ?? BITRATE_MODE_PARAMS.libx265;
|
|
142
|
+
return build(params);
|
|
143
|
+
}
|
|
144
|
+
const build = CRF_MODE_PARAMS[encoder] ?? CRF_MODE_PARAMS.libx265;
|
|
145
|
+
return build(threads);
|
|
146
|
+
}
|
|
147
|
+
// ==================== 内部工具函数 ====================
|
|
148
|
+
/** 输出帧率:弹幕逐帧重算位置,高帧率下步进更小、观感更顺滑(视频帧复制,内容不变) */
|
|
149
|
+
const OUTPUT_FPS = 60;
|
|
150
|
+
/**
|
|
151
|
+
* 毫秒转 ASS 时间戳(h:mm:ss.cc,厘秒精度)
|
|
152
|
+
* @param ms 毫秒时间
|
|
153
|
+
* @returns ASS 时间戳字符串
|
|
154
|
+
*/
|
|
155
|
+
const toASSTime = (ms) => {
|
|
156
|
+
const s = ms / 1000;
|
|
157
|
+
const h = Math.floor(s / 3600);
|
|
158
|
+
const m = Math.floor((s % 3600) / 60);
|
|
159
|
+
const sec = Math.floor(s % 60);
|
|
160
|
+
const cs = Math.floor((s % 1) * 100);
|
|
161
|
+
return `${h}:${m.toString().padStart(2, '0')}:${sec.toString().padStart(2, '0')}.${cs.toString().padStart(2, '0')}`;
|
|
162
|
+
};
|
|
163
|
+
/**
|
|
164
|
+
* 估算文本宽度(全角字符按 1 倍字号、半角按 0.5 倍)
|
|
165
|
+
* @param text 文本内容
|
|
166
|
+
* @param fontSize 字号(像素)
|
|
167
|
+
* @returns 估算宽度(像素)
|
|
168
|
+
*/
|
|
169
|
+
const estimateWidth = (text, fontSize) => {
|
|
170
|
+
let w = 0;
|
|
171
|
+
for (const c of text) {
|
|
172
|
+
w += c.charCodeAt(0) > 127 ? fontSize : fontSize * 0.5;
|
|
173
|
+
}
|
|
174
|
+
return w;
|
|
175
|
+
};
|
|
176
|
+
/**
|
|
177
|
+
* ASS 文本转义
|
|
178
|
+
* @param text 原始文本
|
|
179
|
+
* @returns 转义后的文本
|
|
180
|
+
*/
|
|
181
|
+
const escapeASS = (text) => text.replace(/\\/g, '\\\\').replace(/\{/g, '\\{').replace(/\}/g, '\\}').replace(/\n/g, '\\N');
|
|
182
|
+
/**
|
|
183
|
+
* HTML 文本转义
|
|
184
|
+
* @param text 原始文本
|
|
185
|
+
* @returns 转义后的文本
|
|
186
|
+
*/
|
|
187
|
+
const escapeHTML = (text) => text.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"').replace(/'/g, ''');
|
|
188
|
+
/**
|
|
189
|
+
* Windows 路径转义为正斜杠形式(供 ffmpeg 滤镜内使用,盘符冒号需转义)
|
|
190
|
+
* @param path 原始路径
|
|
191
|
+
* @returns 转义后的路径
|
|
192
|
+
*/
|
|
193
|
+
const escapeWinPath = (path) => path.replace(/\\/g, '/').replace(/:/g, '\\:');
|
|
194
|
+
/**
|
|
195
|
+
* 判断是否为横屏
|
|
196
|
+
* @param w 宽度
|
|
197
|
+
* @param h 高度
|
|
198
|
+
* @returns 宽大于高时为 true
|
|
199
|
+
*/
|
|
200
|
+
const isLandscape = (w, h) => w > h;
|
|
201
|
+
/**
|
|
202
|
+
* 从 PNG 文件头读取尺寸
|
|
203
|
+
* @param buffer PNG 文件内容
|
|
204
|
+
* @returns 宽高(像素),非 PNG 返回空对象
|
|
205
|
+
*/
|
|
206
|
+
const getPngSize = (buffer) => {
|
|
207
|
+
if (buffer.length >= 24 && buffer.subarray(0, 8).equals(Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]))) {
|
|
208
|
+
return { width: buffer.readUInt32BE(16), height: buffer.readUInt32BE(20) };
|
|
209
|
+
}
|
|
210
|
+
return {};
|
|
211
|
+
};
|
|
212
|
+
// ==================== FFprobe 工具 ====================
|
|
213
|
+
/**
|
|
214
|
+
* 获取视频分辨率
|
|
215
|
+
* @param path 视频文件路径
|
|
216
|
+
* @returns 宽高,获取失败返回抖音默认竖屏 1080x1920
|
|
217
|
+
*/
|
|
218
|
+
async function getDouyinResolution(path) {
|
|
219
|
+
try {
|
|
220
|
+
const { stdout } = await (0, node_karin_1.ffprobe)(`-v error -select_streams v:0 -show_entries stream=width,height -of csv=s=x:p=0 "${path}"`);
|
|
221
|
+
const [w, h] = stdout.trim().split('x').map(Number);
|
|
222
|
+
if (w && h)
|
|
223
|
+
return { width: w, height: h };
|
|
224
|
+
}
|
|
225
|
+
catch {
|
|
226
|
+
/* ignore */
|
|
227
|
+
}
|
|
228
|
+
try {
|
|
229
|
+
const result = await (0, node_karin_1.ffmpeg)(`-i "${path}" -f null -`, { timeout: 5000 });
|
|
230
|
+
const stderr = result.stderr || '';
|
|
231
|
+
const match = stderr.match(/(\d{3,4})x(\d{3,4})/);
|
|
232
|
+
if (match)
|
|
233
|
+
return { width: parseInt(match[1]), height: parseInt(match[2]) };
|
|
234
|
+
}
|
|
235
|
+
catch {
|
|
236
|
+
/* ignore */
|
|
237
|
+
}
|
|
238
|
+
return { width: 1080, height: 1920 }; // 抖音默认竖屏
|
|
239
|
+
}
|
|
240
|
+
// ==================== 平台表情与弹幕条渲染 ====================
|
|
241
|
+
/**
|
|
242
|
+
* 获取抖音平台表情列表
|
|
243
|
+
* 与评论处理一致:name 为文本占位符(如 [捂脸]),url 为表情图地址
|
|
244
|
+
* 获取失败时返回空列表,含表情的弹幕将退化为纯文字烧录
|
|
245
|
+
* @returns 表情列表(按名称长度降序,避免短名抢先匹配)
|
|
246
|
+
*/
|
|
247
|
+
async function fetchDouyinEmojiList() {
|
|
248
|
+
try {
|
|
249
|
+
const res = await amagiClient_1.douyinFetcher.fetchEmojiList();
|
|
250
|
+
const list = res.data?.emoji_list ?? [];
|
|
251
|
+
return list
|
|
252
|
+
.map((item) => ({ name: item.display_name, url: item.emoji_url?.url_list?.[0] ?? '' }))
|
|
253
|
+
.filter((item) => Boolean(item.name) && Boolean(item.url))
|
|
254
|
+
.sort((a, b) => b.name.length - a.name.length);
|
|
255
|
+
}
|
|
256
|
+
catch (err) {
|
|
257
|
+
node_karin_1.logger.warn('[DouyinDanmaku] 获取表情列表失败,表情弹幕将按纯文字处理', err);
|
|
258
|
+
return [];
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
/**
|
|
262
|
+
* 判断弹幕文本是否包含平台表情
|
|
263
|
+
* @param text 弹幕文本
|
|
264
|
+
* @param emojiList 表情列表
|
|
265
|
+
* @returns 包含任一表情占位符时为 true
|
|
266
|
+
*/
|
|
267
|
+
const hasEmoji = (text, emojiList) => emojiList.some((e) => text.includes(e.name));
|
|
268
|
+
/**
|
|
269
|
+
* 把弹幕文本拆成文字/表情片段(长名优先匹配,与评论处理一致)
|
|
270
|
+
* @param text 弹幕文本
|
|
271
|
+
* @param emojiList 表情列表(已按名称长度降序)
|
|
272
|
+
* @returns 有序的片段列表
|
|
273
|
+
*/
|
|
274
|
+
function splitDanmakuSegments(text, emojiList) {
|
|
275
|
+
const segments = [];
|
|
276
|
+
let buffer = '';
|
|
277
|
+
let index = 0;
|
|
278
|
+
const pushBuffer = () => {
|
|
279
|
+
if (buffer.length > 0) {
|
|
280
|
+
segments.push({ type: 'text', content: buffer });
|
|
281
|
+
buffer = '';
|
|
282
|
+
}
|
|
283
|
+
};
|
|
284
|
+
while (index < text.length) {
|
|
285
|
+
const matched = emojiList.find((e) => text.startsWith(e.name, index));
|
|
286
|
+
if (matched) {
|
|
287
|
+
pushBuffer();
|
|
288
|
+
segments.push({ type: 'emoji', name: matched.name, url: matched.url });
|
|
289
|
+
index += matched.name.length;
|
|
290
|
+
continue;
|
|
291
|
+
}
|
|
292
|
+
buffer += text[index];
|
|
293
|
+
index += 1;
|
|
294
|
+
}
|
|
295
|
+
pushBuffer();
|
|
296
|
+
return segments;
|
|
297
|
+
}
|
|
298
|
+
/** 抖音官方点赞图标(同 packages/template/src/components/platforms/douyin/Icons.tsx 的 DouyinLikeIcon) */
|
|
299
|
+
const LIKE_ICON_PATH = 'M453.036 88.712C493.774 30.664 560.66 0 634.251 0 774.403 0 890.972 121.59 890.972 266.137v.054c.01-.02.019-.04.028-.06 0 4.121-.07 7.241-.122 9.537-.072 3.151-.108 4.747.122 5.247-.531 30.403-5.778 55.522-15.101 88.712-5.289 5.96-10.204 17.184-15.1 29.572-7.724 11.998-10.647 17.81-15.101 29.57a546.67 546.67 0 0 1-14.452 22.974c-37.449 56.8-87.537 113.33-137.579 163.509-78.331 79.025-158.123 144.805-192.835 173.422-9.585 7.901-15.732 12.969-17.464 14.7-12.301 12.303-24.603 12.611-36.905 12.619-.324.004-.649.006-.977.006-25.236 0-37.854-12.619-50.472-25.237-.963-.963-3.413-2.984-7.156-5.979-38.233-28.184-124.273-96.997-205.116-180C121.066 542.02 61.622 470.007 29.092 399.588 16.474 374.351.731 314.264 0 280.922c.269-.267.227-1.873.144-5.078C.083 273.498 0 270.297 0 266.137 0 121.524 116.502 0 256.721 0c73.458 0 140.405 30.664 196.315 88.712Z';
|
|
300
|
+
/**
|
|
301
|
+
* 生成弹幕条 HTML:文字 + 内联表情图 + 可选点赞角标(白色图标 + 点赞数),透明背景,样式对齐 ASS(白字黑描边)
|
|
302
|
+
* @param segments 弹幕内容片段
|
|
303
|
+
* @param fontSize CSS 字号(像素)
|
|
304
|
+
* @param fontName 字体名
|
|
305
|
+
* @param opacity 整体不透明度(0-1)
|
|
306
|
+
* @param likeLabel 点赞角标文本(如 "14"、"2.4w"),为 null 时不渲染角标
|
|
307
|
+
* @returns 完整 HTML 文档
|
|
308
|
+
*/
|
|
309
|
+
function buildStripHTML(segments, fontSize, fontName, opacity, likeLabel) {
|
|
310
|
+
const safeFontName = fontName.replace(/['\\]/g, '');
|
|
311
|
+
const strokeW = Math.max(1, Math.round(fontSize / 21));
|
|
312
|
+
const imgSize = Math.round(fontSize * 1.1);
|
|
313
|
+
const body = segments
|
|
314
|
+
.map((seg) => seg.type === 'text'
|
|
315
|
+
? `<span>${escapeHTML(seg.content)}</span>`
|
|
316
|
+
: `<img src="${escapeHTML(seg.url)}" alt="${escapeHTML(seg.name)}" referrerpolicy="no-referrer" crossorigin="anonymous" />`)
|
|
317
|
+
.join('') +
|
|
318
|
+
(likeLabel !== null
|
|
319
|
+
? `<svg viewBox="0 0 891 816" class="like-icon" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill="currentColor" d="${LIKE_ICON_PATH}" /></svg><span class="like-count">${escapeHTML(likeLabel)}</span>`
|
|
320
|
+
: '');
|
|
321
|
+
return `<!DOCTYPE html>
|
|
322
|
+
<html><head><meta charset="utf-8" /><style>
|
|
323
|
+
html,body{margin:0;padding:0;background:transparent}
|
|
324
|
+
#container{display:inline-flex;align-items:center;white-space:nowrap;font-family:'${safeFontName}',sans-serif;font-size:${fontSize}px;line-height:1.2;color:#fff;opacity:${opacity}}
|
|
325
|
+
#container span{paint-order:stroke fill;-webkit-text-stroke:${strokeW}px rgba(0,0,0,0.85)}
|
|
326
|
+
#container img{height:${imgSize}px;width:auto;display:inline-block}
|
|
327
|
+
.like-icon{height:0.95em;width:0.95em;display:inline-block;margin-left:0.35em}
|
|
328
|
+
.like-count{margin-left:0.25em}
|
|
329
|
+
</style></head><body><div id="container">${body}</div></body></html>`;
|
|
330
|
+
}
|
|
331
|
+
/**
|
|
332
|
+
* 把含表情/点赞的弹幕渲染成透明 PNG 条(按内容缓存,相同文本复用同一张图)
|
|
333
|
+
* 渲染失败时返回 null,调用方退化为纯文字 ASS 烧录
|
|
334
|
+
* @param text 弹幕原文
|
|
335
|
+
* @param likeLabel 点赞角标文本,为 null 时不渲染角标
|
|
336
|
+
* @param emojiList 表情列表
|
|
337
|
+
* @param style 渲染样式
|
|
338
|
+
* @param cache 渲染缓存(内容 hash -> 渲染 Promise)
|
|
339
|
+
* @param tempFiles 临时文件收集器(HTML/PNG 路径会追加进去,由调用方统一清理)
|
|
340
|
+
* @returns 弹幕条信息,失败返回 null
|
|
341
|
+
*/
|
|
342
|
+
async function renderDanmakuStrip(text, likeLabel, emojiList, style, cache, tempFiles) {
|
|
343
|
+
const cacheKey = `${style.fontSize}:${style.fontName}:${style.opacity}:${likeLabel ?? ''}:${text}`;
|
|
344
|
+
let pending = cache.get(cacheKey);
|
|
345
|
+
if (!pending) {
|
|
346
|
+
pending = (async () => {
|
|
347
|
+
const hash = node_crypto_1.default.createHash('md5').update(cacheKey).digest('hex').slice(0, 12);
|
|
348
|
+
const htmlPath = `${utils_1.Common.tempDri.video}danmaku_strip_${hash}.html`;
|
|
349
|
+
const pngPath = `${utils_1.Common.tempDri.video}danmaku_strip_${hash}.png`;
|
|
350
|
+
tempFiles.push(htmlPath, pngPath);
|
|
351
|
+
try {
|
|
352
|
+
const segments = splitDanmakuSegments(text, emojiList);
|
|
353
|
+
node_fs_1.default.writeFileSync(htmlPath, buildStripHTML(segments, style.fontSize, style.fontName, style.opacity, likeLabel), 'utf-8');
|
|
354
|
+
const renderResult = (await node_karin_1.render.render({
|
|
355
|
+
name: 'koishi-plugin-kkk/danmaku',
|
|
356
|
+
file: htmlPath,
|
|
357
|
+
selector: '#container',
|
|
358
|
+
fullPage: false,
|
|
359
|
+
type: 'png',
|
|
360
|
+
omitBackground: true,
|
|
361
|
+
pageGotoParams: { waitUntil: 'load', timeout: 15000 }
|
|
362
|
+
}));
|
|
363
|
+
const base64 = Array.isArray(renderResult) ? renderResult[0] : renderResult;
|
|
364
|
+
const pngBuffer = Buffer.from(base64, 'base64');
|
|
365
|
+
const size = getPngSize(pngBuffer);
|
|
366
|
+
if (!size.width || !size.height)
|
|
367
|
+
throw new Error('无法读取弹幕条 PNG 尺寸');
|
|
368
|
+
node_fs_1.default.writeFileSync(pngPath, pngBuffer);
|
|
369
|
+
return { text, pngPath, width: size.width, height: size.height };
|
|
370
|
+
}
|
|
371
|
+
catch (err) {
|
|
372
|
+
node_karin_1.logger.warn(`[DouyinDanmaku] 弹幕条渲染失败,将按纯文字处理: ${text}`, err);
|
|
373
|
+
return null;
|
|
374
|
+
}
|
|
375
|
+
})();
|
|
376
|
+
cache.set(cacheKey, pending);
|
|
377
|
+
}
|
|
378
|
+
return pending;
|
|
379
|
+
}
|
|
380
|
+
/**
|
|
381
|
+
* 预渲染需要 PNG 条的弹幕(含平台表情,或被选中展示点赞角标)
|
|
382
|
+
* 渲染失败的弹幕退化为纯文字 ASS 烧录
|
|
383
|
+
* @param sortedDanmaku 按出现时间升序的弹幕列表
|
|
384
|
+
* @param emojiList 表情列表
|
|
385
|
+
* @param liked 点赞角标选择结果
|
|
386
|
+
* @param style 渲染样式
|
|
387
|
+
* @returns 弹幕 ID -> PNG 条 的映射,以及过程中产生的临时文件
|
|
388
|
+
*/
|
|
389
|
+
async function prepareDanmakuStrips(sortedDanmaku, emojiList, liked, style) {
|
|
390
|
+
const strips = new Map();
|
|
391
|
+
const tempFiles = [];
|
|
392
|
+
const needsStrip = (dm) => liked.ids.has(dm.danmaku_id) || (emojiList.length > 0 && hasEmoji(dm.text, emojiList));
|
|
393
|
+
if (!sortedDanmaku.some(needsStrip))
|
|
394
|
+
return { strips, tempFiles };
|
|
395
|
+
const cache = new Map();
|
|
396
|
+
for (const dm of sortedDanmaku) {
|
|
397
|
+
if (!needsStrip(dm))
|
|
398
|
+
continue;
|
|
399
|
+
const likeLabel = liked.ids.has(dm.danmaku_id) ? formatLikeCount(dm.digg_count ?? 0) : null;
|
|
400
|
+
const strip = await renderDanmakuStrip(dm.text, likeLabel, emojiList, style, cache, tempFiles);
|
|
401
|
+
if (strip)
|
|
402
|
+
strips.set(dm.danmaku_id, strip);
|
|
403
|
+
}
|
|
404
|
+
return { strips, tempFiles };
|
|
405
|
+
}
|
|
406
|
+
// ==================== 点赞角标 ====================
|
|
407
|
+
/**
|
|
408
|
+
* 格式化点赞数:破万保留一位小数并加 w 后缀,只舍不入(四舍不能五入)
|
|
409
|
+
* 如 14 -> "14",24800 -> "2.4w",1001900 -> "100.1w"
|
|
410
|
+
* @param count 点赞数
|
|
411
|
+
* @returns 格式化后的文本
|
|
412
|
+
*/
|
|
413
|
+
const formatLikeCount = (count) => {
|
|
414
|
+
if (count < 10000)
|
|
415
|
+
return String(count);
|
|
416
|
+
return `${(Math.floor(count / 1000) / 10).toFixed(1)}w`;
|
|
417
|
+
};
|
|
418
|
+
/**
|
|
419
|
+
* 从有点赞的弹幕中选出展示点赞角标的子集
|
|
420
|
+
* 全部展示会非常拥挤,目标展示数随弹幕总量按 1.5√N 缩放(100 条弹幕约展示 15 条),
|
|
421
|
+
* 弹幕越多比例越低(不是固定比例);再按点赞量从高到低截取
|
|
422
|
+
* @param sortedDanmaku 按出现时间升序的弹幕列表
|
|
423
|
+
* @returns 选中的弹幕 ID 集合及统计
|
|
424
|
+
*/
|
|
425
|
+
function selectLikedDanmaku(sortedDanmaku) {
|
|
426
|
+
const candidates = sortedDanmaku.filter((dm) => (dm.digg_count ?? 0) > 0);
|
|
427
|
+
const target = Math.min(50, Math.max(5, Math.round(Math.sqrt(sortedDanmaku.length) * 1.5)));
|
|
428
|
+
if (candidates.length === 0)
|
|
429
|
+
return { ids: new Set(), candidateCount: 0, target };
|
|
430
|
+
if (candidates.length <= target)
|
|
431
|
+
return { ids: new Set(candidates.map((dm) => dm.danmaku_id)), candidateCount: candidates.length, target };
|
|
432
|
+
// 按点赞量降序取前 target 条;与第 target 条点赞数并列的一并保留,避免随意截断
|
|
433
|
+
const byDiggDesc = [...candidates].sort((a, b) => (b.digg_count ?? 0) - (a.digg_count ?? 0));
|
|
434
|
+
const cutoff = byDiggDesc[target - 1].digg_count ?? 0;
|
|
435
|
+
return {
|
|
436
|
+
ids: new Set(byDiggDesc.filter((dm) => (dm.digg_count ?? 0) >= cutoff).map((dm) => dm.danmaku_id)),
|
|
437
|
+
candidateCount: candidates.length,
|
|
438
|
+
target
|
|
439
|
+
};
|
|
440
|
+
}
|
|
441
|
+
// ==================== 弹幕布局 ====================
|
|
442
|
+
/** 字号配置映射(1080 高度基准) */
|
|
443
|
+
const FONT_SIZE_MAP = {
|
|
444
|
+
small: { base: 25, trackH: 30 },
|
|
445
|
+
medium: { base: 32, trackH: 38 },
|
|
446
|
+
large: { base: 40, trackH: 46 }
|
|
447
|
+
};
|
|
448
|
+
/**
|
|
449
|
+
* 计算弹幕布局参数
|
|
450
|
+
* @param height 画布高度(像素),所有几何以 1080 高为基准等比缩放
|
|
451
|
+
* @param danmakuArea 弹幕显示区域比例(0.25/0.5/0.75/1)
|
|
452
|
+
* @param danmakuFontSize 弹幕字号档位
|
|
453
|
+
* @param danmakuOpacity 弹幕不透明度(0-100)
|
|
454
|
+
* @returns 布局参数
|
|
455
|
+
*/
|
|
456
|
+
function computeDanmakuLayout(height, danmakuArea, danmakuFontSize, danmakuOpacity) {
|
|
457
|
+
const fontScale = height / 1080;
|
|
458
|
+
const sizeConfig = FONT_SIZE_MAP[danmakuFontSize];
|
|
459
|
+
const fontSize = Math.round(sizeConfig.base * fontScale);
|
|
460
|
+
const trackH = Math.round(sizeConfig.trackH * fontScale);
|
|
461
|
+
const topMargin = Math.round(5 * fontScale);
|
|
462
|
+
const areaHeight = Math.floor(height * danmakuArea) - topMargin;
|
|
463
|
+
const trackCount = Math.max(1, Math.floor((areaHeight - fontSize) / trackH));
|
|
464
|
+
const minGap = Math.round(15 * fontScale);
|
|
465
|
+
// libass 的 Fontsize 近似按点(pt)渲染,同名字号下字形约为 CSS px 的 3/4(实测 32 -> 288/381)
|
|
466
|
+
// 弹幕条要与之视觉对齐,CSS 字号按 0.75 换算
|
|
467
|
+
const stripFontSize = Math.max(1, Math.round(fontSize * 0.75));
|
|
468
|
+
// 将 0-100 的透明度转换为 ASS 的 alpha 值(0-255,0为不透明,255为完全透明)
|
|
469
|
+
const alpha = Math.round((100 - Math.max(0, Math.min(100, danmakuOpacity))) * 2.55)
|
|
470
|
+
.toString(16)
|
|
471
|
+
.padStart(2, '0')
|
|
472
|
+
.toUpperCase();
|
|
473
|
+
return { fontSize, stripFontSize, trackH, topMargin, trackCount, minGap, alpha };
|
|
474
|
+
}
|
|
475
|
+
/**
|
|
476
|
+
* 计算同轨道上后一条弹幕与前一条弹幕的间距是否安全
|
|
477
|
+
* 间距指 next 头部进入屏幕右缘时与 prev 尾部的距离;若 next 更快,还会校验 prev 消失前不被追尾
|
|
478
|
+
* @param prev 先入轨(时间上更早)的弹幕
|
|
479
|
+
* @param next 后入轨(时间上更晚)的弹幕
|
|
480
|
+
* @param canvasWidth 画布宽度(像素)
|
|
481
|
+
* @param minGap 最小间距(像素)
|
|
482
|
+
* @returns 间距(像素),>= 0 表示不重叠
|
|
483
|
+
*/
|
|
484
|
+
const calcTrackDistance = (prev, next, canvasWidth, minGap) => {
|
|
485
|
+
const prevSpeed = (canvasWidth + prev.textWidth) / prev.duration;
|
|
486
|
+
const nextSpeed = (canvasWidth + next.textWidth) / next.duration;
|
|
487
|
+
const elapsed = next.startTime - prev.startTime;
|
|
488
|
+
const prevRightX = canvasWidth - prevSpeed * elapsed + prev.textWidth;
|
|
489
|
+
let dist = canvasWidth - prevRightX - minGap;
|
|
490
|
+
if (nextSpeed > prevSpeed) {
|
|
491
|
+
const totalElapsed = next.startTime + next.duration - prev.startTime;
|
|
492
|
+
const prevRightXAtEnd = canvasWidth - prevSpeed * totalElapsed + prev.textWidth;
|
|
493
|
+
dist = Math.min(dist, -next.textWidth - prevRightXAtEnd - minGap);
|
|
494
|
+
}
|
|
495
|
+
return dist;
|
|
496
|
+
};
|
|
497
|
+
/**
|
|
498
|
+
* 单条弹幕入轨(区间表模型):每条轨道维护按时间排序的已承诺弹幕列表,
|
|
499
|
+
* 新弹幕只需与直接前驱/后继做间距检查(所有弹幕滚动时长相同,起止时间同序,最近邻居必是最强约束)
|
|
500
|
+
* @param lanes 所有轨道的区间表
|
|
501
|
+
* @param entry 待入轨弹幕
|
|
502
|
+
* @param canvasWidth 画布宽度(像素)
|
|
503
|
+
* @param minGap 同轨道相邻弹幕最小间距(像素)
|
|
504
|
+
* @param preferFreeLane true 时优先随机选择空闲轨道(用于点赞角标,使其散布到任意行);
|
|
505
|
+
* false 时优先紧凑复用(普通弹幕,保持"避免阶梯"的观感)
|
|
506
|
+
* @returns 轨道下标,放不下返回 -1
|
|
507
|
+
*/
|
|
508
|
+
const allocateTrack = (lanes, entry, canvasWidth, minGap, preferFreeLane) => {
|
|
509
|
+
const { startTime, duration } = entry;
|
|
510
|
+
let bestIdx = -1;
|
|
511
|
+
let bestDist = Infinity;
|
|
512
|
+
const freeLanes = [];
|
|
513
|
+
for (let i = 0; i < lanes.length; i++) {
|
|
514
|
+
const lane = lanes[i];
|
|
515
|
+
// 找直接前驱(最后一个不晚于当前弹幕的已承诺条目;常见情况是追加在末尾,从后往前扫)
|
|
516
|
+
let predIdx = -1;
|
|
517
|
+
for (let j = lane.length - 1; j >= 0; j--) {
|
|
518
|
+
if (lane[j].startTime <= startTime) {
|
|
519
|
+
predIdx = j;
|
|
520
|
+
break;
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
const pred = predIdx >= 0 ? lane[predIdx] : null;
|
|
524
|
+
const succ = predIdx + 1 < lane.length ? lane[predIdx + 1] : null;
|
|
525
|
+
// 前驱仍在屏时必须保持间距;后继与当前弹幕时间交叠时,当前弹幕也不能挡住后继
|
|
526
|
+
const predActive = pred !== null && pred.startTime + pred.duration > startTime;
|
|
527
|
+
if (predActive && calcTrackDistance(pred, entry, canvasWidth, minGap) < 0)
|
|
528
|
+
continue;
|
|
529
|
+
if (succ !== null && startTime + duration > succ.startTime && calcTrackDistance(entry, succ, canvasWidth, minGap) < 0)
|
|
530
|
+
continue;
|
|
531
|
+
if (!predActive) {
|
|
532
|
+
// 当前时段空闲的轨道(含全空轨道),优先级最低,只有没有紧凑选择时才用
|
|
533
|
+
freeLanes.push(i);
|
|
534
|
+
continue;
|
|
535
|
+
}
|
|
536
|
+
// 紧凑排列:距离最小但仍 >= 0 的轨道,避免"阶梯"效果
|
|
537
|
+
const d = calcTrackDistance(pred, entry, canvasWidth, minGap);
|
|
538
|
+
if (d < bestDist) {
|
|
539
|
+
bestDist = d;
|
|
540
|
+
bestIdx = i;
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
if (preferFreeLane && freeLanes.length > 0)
|
|
544
|
+
return freeLanes[Math.floor(Math.random() * freeLanes.length)];
|
|
545
|
+
if (bestIdx !== -1)
|
|
546
|
+
return bestIdx;
|
|
547
|
+
if (freeLanes.length > 0)
|
|
548
|
+
return freeLanes[0];
|
|
549
|
+
return -1;
|
|
550
|
+
};
|
|
551
|
+
/**
|
|
552
|
+
* 把弹幕按时间序插入轨道区间表
|
|
553
|
+
* @param lane 目标轨道的区间表
|
|
554
|
+
* @param entry 待插入弹幕
|
|
555
|
+
*/
|
|
556
|
+
const insertIntoLane = (lane, entry) => {
|
|
557
|
+
let j = lane.length - 1;
|
|
558
|
+
while (j >= 0 && lane[j].startTime > entry.startTime)
|
|
559
|
+
j--;
|
|
560
|
+
lane.splice(j + 1, 0, entry);
|
|
561
|
+
};
|
|
562
|
+
/**
|
|
563
|
+
* 弹幕滚动宽度基准:ASS 与 overlay 必须使用同一度量,否则两类弹幕速度不一致、会错开
|
|
564
|
+
* 纯文字用估算宽度;PNG 条取估算宽度与真实宽度的较大值(角标图标可能让真实宽度超过估算,
|
|
565
|
+
* 取大者保证条尾在 endTime 前完全滚出屏幕,不会突然消失)
|
|
566
|
+
* @param text 弹幕文本
|
|
567
|
+
* @param strip 预渲染弹幕条(纯文字弹幕为 undefined)
|
|
568
|
+
* @param fontSize ASS 字号(像素)
|
|
569
|
+
* @returns 滚动宽度基准(像素)
|
|
570
|
+
*/
|
|
571
|
+
const resolveMoveWidth = (text, strip, fontSize) => {
|
|
572
|
+
return strip ? Math.max(estimateWidth(text, fontSize), strip.width) : estimateWidth(text, fontSize);
|
|
573
|
+
};
|
|
574
|
+
// ==================== ASS 生成 ====================
|
|
575
|
+
/**
|
|
576
|
+
* 生成 ASS 文件头(Script Info + 样式表 + 事件格式声明)
|
|
577
|
+
* @param width 画布宽度(ASS PlayResX)
|
|
578
|
+
* @param height 画布高度(ASS PlayResY)
|
|
579
|
+
* @param fontName 字体名
|
|
580
|
+
* @param layout 布局参数
|
|
581
|
+
* @returns ASS 头部内容
|
|
582
|
+
*/
|
|
583
|
+
function buildAssHeader(width, height, fontName, layout) {
|
|
584
|
+
return `[Script Info]
|
|
585
|
+
Title: Douyin Danmaku
|
|
586
|
+
ScriptType: v4.00+
|
|
587
|
+
PlayResX: ${width}
|
|
588
|
+
PlayResY: ${height}
|
|
589
|
+
Timer: 100.0000
|
|
590
|
+
|
|
591
|
+
[V4+ Styles]
|
|
592
|
+
Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding
|
|
593
|
+
Style: Scroll,${fontName},${layout.fontSize},&H${layout.alpha}FFFFFF,&H${layout.alpha}FFFFFF,&H${layout.alpha}000000,&H${layout.alpha}000000,0,0,0,0,100,100,0,0,1,0.8,0,2,0,0,0,1
|
|
594
|
+
|
|
595
|
+
[Events]
|
|
596
|
+
Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text
|
|
597
|
+
`;
|
|
598
|
+
}
|
|
599
|
+
/**
|
|
600
|
+
* 生成一条纯文字弹幕的 ASS Dialogue 行(滚动弹幕,\move 从右缘外滚到左缘外)
|
|
601
|
+
* @param text 弹幕文本
|
|
602
|
+
* @param entry 轨道条目(时间/宽度基准)
|
|
603
|
+
* @param trackIdx 轨道下标
|
|
604
|
+
* @param canvasWidth 画布宽度(像素)
|
|
605
|
+
* @param layout 布局参数
|
|
606
|
+
* @returns Dialogue 行(含换行符)
|
|
607
|
+
*/
|
|
608
|
+
function buildScrollDialogue(text, entry, trackIdx, canvasWidth, layout) {
|
|
609
|
+
const y = layout.topMargin + trackIdx * layout.trackH + layout.fontSize;
|
|
610
|
+
const content = escapeASS(text);
|
|
611
|
+
return `Dialogue: 0,${toASSTime(entry.startTime)},${toASSTime(entry.startTime + entry.duration)},Scroll,,0,0,0,,{\\an7}{\\move(${canvasWidth},${y},${-entry.textWidth},${y})}${content}\n`;
|
|
612
|
+
}
|
|
613
|
+
/**
|
|
614
|
+
* 生成一条图片弹幕的 overlay 描述
|
|
615
|
+
* @param strip 预渲染弹幕条
|
|
616
|
+
* @param entry 轨道条目(时间/宽度基准)
|
|
617
|
+
* @param trackIdx 轨道下标
|
|
618
|
+
* @param layout 布局参数
|
|
619
|
+
* @returns overlay 描述
|
|
620
|
+
*/
|
|
621
|
+
function buildOverlay(strip, entry, trackIdx, layout) {
|
|
622
|
+
// ASS \an7 的 y 是文字顶部;条按视觉中心对齐文字中心
|
|
623
|
+
const textTop = layout.topMargin + trackIdx * layout.trackH + layout.fontSize;
|
|
624
|
+
const y = Math.max(0, Math.round(textTop + (layout.fontSize - strip.height) / 2));
|
|
625
|
+
return { pngPath: strip.pngPath, startTime: entry.startTime, endTime: entry.startTime + entry.duration, y, moveW: entry.textWidth };
|
|
626
|
+
}
|
|
627
|
+
/**
|
|
628
|
+
* 生成弹幕 ASS 与图片弹幕(表情/点赞)overlay 列表
|
|
629
|
+
* 纯文字弹幕输出 ASS \move 行;含表情或被选中展示点赞角标的弹幕预渲染为 PNG 条,统一参与轨道分配后转为 overlay
|
|
630
|
+
* @param danmakuList 弹幕列表
|
|
631
|
+
* @param width 画布宽度(像素)
|
|
632
|
+
* @param height 画布高度(像素)
|
|
633
|
+
* @param options 烧录配置
|
|
634
|
+
* @returns ASS 内容、overlay 列表、临时文件与分类统计
|
|
635
|
+
*/
|
|
636
|
+
async function generateDouyinASS(danmakuList, width, height, options = {}) {
|
|
637
|
+
const { scrollTime = 8, danmakuOpacity = 70, fontName = 'Microsoft YaHei', danmakuArea = 0.5, danmakuFontSize = 'medium' } = options;
|
|
638
|
+
const layout = computeDanmakuLayout(height, danmakuArea, danmakuFontSize, danmakuOpacity);
|
|
639
|
+
// 过滤并排序弹幕
|
|
640
|
+
const sorted = danmakuList.filter((dm) => dm.text && dm.text.trim()).sort((a, b) => a.offset_time - b.offset_time);
|
|
641
|
+
const emojiList = await fetchDouyinEmojiList();
|
|
642
|
+
const liked = selectLikedDanmaku(sorted);
|
|
643
|
+
const stripStyle = { fontSize: layout.stripFontSize, fontName, opacity: Math.max(0, Math.min(100, danmakuOpacity)) / 100 };
|
|
644
|
+
const { strips, tempFiles } = await prepareDanmakuStrips(sorted, emojiList, liked, stripStyle);
|
|
645
|
+
const assLines = [];
|
|
646
|
+
const overlays = [];
|
|
647
|
+
let likedOverlayCount = 0;
|
|
648
|
+
let emojiOverlayCount = 0;
|
|
649
|
+
/**
|
|
650
|
+
* 弹幕入轨并上屏:写入 ASS(纯文字)或 overlay 列表(PNG 条)
|
|
651
|
+
* @param dm 弹幕元素
|
|
652
|
+
* @param preferFreeLane 是否优先随机选择空闲轨道(点赞角标弹幕为 true)
|
|
653
|
+
*/
|
|
654
|
+
const placeDanmaku = (dm, preferFreeLane) => {
|
|
655
|
+
const strip = strips.get(dm.danmaku_id);
|
|
656
|
+
const entry = {
|
|
657
|
+
startTime: dm.offset_time,
|
|
658
|
+
duration: scrollTime * 1000,
|
|
659
|
+
textWidth: resolveMoveWidth(dm.text, strip, layout.fontSize)
|
|
660
|
+
};
|
|
661
|
+
const trackIdx = allocateTrack(lanes, entry, width, layout.minGap, preferFreeLane);
|
|
662
|
+
if (trackIdx === -1)
|
|
663
|
+
return;
|
|
664
|
+
insertIntoLane(lanes[trackIdx], entry);
|
|
665
|
+
if (strip) {
|
|
666
|
+
overlays.push(buildOverlay(strip, entry, trackIdx, layout));
|
|
667
|
+
if (liked.ids.has(dm.danmaku_id))
|
|
668
|
+
likedOverlayCount++;
|
|
669
|
+
else
|
|
670
|
+
emojiOverlayCount++;
|
|
671
|
+
return;
|
|
672
|
+
}
|
|
673
|
+
assLines.push(buildScrollDialogue(dm.text, entry, trackIdx, width, layout));
|
|
674
|
+
};
|
|
675
|
+
// 所有轨道共享,点赞角标弹幕按时间序先提交(获得优先级),普通弹幕后提交并绕开已承诺的角标。
|
|
676
|
+
// 弹幕密集时普通轨道会按比例丢弃弹幕,先提交保证被选中的高点赞弹幕不被丢弃;
|
|
677
|
+
// 角标优先随机选择空闲轨道,使其像普通弹幕一样散布在任意行
|
|
678
|
+
const lanes = Array.from({ length: layout.trackCount }, () => []);
|
|
679
|
+
// 第一遍:点赞角标弹幕(时间序),优先随机空闲轨道
|
|
680
|
+
for (const dm of sorted) {
|
|
681
|
+
if (liked.ids.has(dm.danmaku_id))
|
|
682
|
+
placeDanmaku(dm, true);
|
|
683
|
+
}
|
|
684
|
+
// 第二遍:普通弹幕(时间序),紧凑复用,绕开已承诺的角标弹幕
|
|
685
|
+
for (const dm of sorted) {
|
|
686
|
+
if (!liked.ids.has(dm.danmaku_id))
|
|
687
|
+
placeDanmaku(dm, false);
|
|
688
|
+
}
|
|
689
|
+
return {
|
|
690
|
+
ass: buildAssHeader(width, height, fontName, layout) + assLines.join(''),
|
|
691
|
+
overlays,
|
|
692
|
+
tempFiles,
|
|
693
|
+
stats: {
|
|
694
|
+
likedOverlays: likedOverlayCount,
|
|
695
|
+
emojiOverlays: emojiOverlayCount,
|
|
696
|
+
likedCandidates: liked.candidateCount,
|
|
697
|
+
likedTarget: liked.target
|
|
698
|
+
}
|
|
699
|
+
};
|
|
700
|
+
}
|
|
701
|
+
const MAX_OUTPUT_WIDTH = 2160;
|
|
702
|
+
/**
|
|
703
|
+
* 计算画布尺寸(竖屏适配)
|
|
704
|
+
* 抖音视频大多是竖屏,但也可能有横屏视频需要转竖屏
|
|
705
|
+
* @param origW 源视频宽度(像素)
|
|
706
|
+
* @param origH 源视频高度(像素)
|
|
707
|
+
* @param verticalMode 横屏转竖屏模式
|
|
708
|
+
* @returns 画布信息
|
|
709
|
+
*/
|
|
710
|
+
function calcCanvas(origW, origH, verticalMode) {
|
|
711
|
+
if (verticalMode === 'off') {
|
|
712
|
+
return { width: origW, height: origH, offsetY: 0, isVertical: false };
|
|
713
|
+
}
|
|
714
|
+
const ratio = origW / origH;
|
|
715
|
+
const isWide = isLandscape(origW, origH);
|
|
716
|
+
if (verticalMode === 'force') {
|
|
717
|
+
const targetRatio = 16 / 9;
|
|
718
|
+
if (isWide) {
|
|
719
|
+
const newW = Math.min(origH, MAX_OUTPUT_WIDTH);
|
|
720
|
+
const newH = Math.round(newW * targetRatio);
|
|
721
|
+
const scaledH = Math.round(newW / ratio);
|
|
722
|
+
const offsetY = Math.round((newH - scaledH) / 2);
|
|
723
|
+
return { width: newW, height: newH, offsetY, isVertical: true, scale: newW / origW };
|
|
724
|
+
}
|
|
725
|
+
else {
|
|
726
|
+
const newW = Math.min(origW, MAX_OUTPUT_WIDTH);
|
|
727
|
+
const scaleRatio = newW / origW;
|
|
728
|
+
const scaledOrigH = Math.round(origH * scaleRatio);
|
|
729
|
+
const newH = Math.round(newW * targetRatio);
|
|
730
|
+
const offsetY = Math.round((newH - scaledOrigH) / 2);
|
|
731
|
+
return { width: newW, height: newH, offsetY: Math.max(0, offsetY), isVertical: true, scale: scaleRatio };
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
// standard 模式:只对宽屏视频进行转换
|
|
735
|
+
if (isWide && ratio >= 1.7) {
|
|
736
|
+
const newW = Math.min(origH, MAX_OUTPUT_WIDTH);
|
|
737
|
+
const scaleRatio = newW / origW;
|
|
738
|
+
const newH = Math.round(origW * scaleRatio);
|
|
739
|
+
const scaledH = Math.round(newW / ratio);
|
|
740
|
+
const offsetY = Math.round((newH - scaledH) / 2);
|
|
741
|
+
return { width: newW, height: newH, offsetY, isVertical: true, scale: newW / origW };
|
|
742
|
+
}
|
|
743
|
+
return { width: origW, height: origH, offsetY: 0, isVertical: false };
|
|
744
|
+
}
|
|
745
|
+
/**
|
|
746
|
+
* 构建 FFmpeg 滤镜(无图片弹幕时的 -vf 简单滤镜链)
|
|
747
|
+
* fps=60 放最前:视频帧复制(内容不变),libass 在每个新帧重新求值 \move,弹幕步进更小更顺滑
|
|
748
|
+
* @param canvas 画布信息
|
|
749
|
+
* @param assPath ASS 字幕文件路径
|
|
750
|
+
* @returns -vf 滤镜链字符串
|
|
751
|
+
*/
|
|
752
|
+
function buildFilter(canvas, assPath) {
|
|
753
|
+
const escaped = escapeWinPath(assPath);
|
|
754
|
+
if (canvas.isVertical) {
|
|
755
|
+
if (canvas.scale && canvas.scale !== 1 && canvas.scale < 1) {
|
|
756
|
+
return `fps=${OUTPUT_FPS},scale=${canvas.width}:-1,pad=${canvas.width}:${canvas.height}:0:${canvas.offsetY}:black,subtitles='${escaped}'`;
|
|
757
|
+
}
|
|
758
|
+
return `fps=${OUTPUT_FPS},pad=${canvas.width}:${canvas.height}:0:${canvas.offsetY}:black,subtitles='${escaped}'`;
|
|
759
|
+
}
|
|
760
|
+
return `fps=${OUTPUT_FPS},subtitles='${escaped}'`;
|
|
761
|
+
}
|
|
762
|
+
/**
|
|
763
|
+
* 构建 FFmpeg filter_complex(含图片弹幕时的叠加链)
|
|
764
|
+
* 每条图片弹幕对应一个 PNG 输入(内容相同的弹幕条复用同一输入,见 burnDouyinDanmaku),
|
|
765
|
+
* 用 overlay 按 \move 同语义滚动:x(t) = width - (t - start) * speed,speed = (width + moveW) / scrollTime
|
|
766
|
+
* @param canvas 画布信息
|
|
767
|
+
* @param assPath ASS 字幕文件路径
|
|
768
|
+
* @param overlays 图片弹幕 overlay 列表
|
|
769
|
+
* @param inputIndices 每条 overlay 对应的 ffmpeg 输入序号(与去重后的 -i 顺序一致)
|
|
770
|
+
* @param scrollTime 滚动时间(秒)
|
|
771
|
+
* @returns filter_complex 字符串
|
|
772
|
+
*/
|
|
773
|
+
function buildFilterComplex(canvas, assPath, overlays, inputIndices, scrollTime) {
|
|
774
|
+
const escaped = escapeWinPath(assPath);
|
|
775
|
+
let base = `[0:v]fps=${OUTPUT_FPS}`;
|
|
776
|
+
if (canvas.isVertical) {
|
|
777
|
+
if (canvas.scale && canvas.scale !== 1 && canvas.scale < 1) {
|
|
778
|
+
base += `,scale=${canvas.width}:-1`;
|
|
779
|
+
}
|
|
780
|
+
base += `,pad=${canvas.width}:${canvas.height}:0:${canvas.offsetY}:black`;
|
|
781
|
+
}
|
|
782
|
+
base += `,subtitles='${escaped}'[base]`;
|
|
783
|
+
const parts = [base];
|
|
784
|
+
let prev = 'base';
|
|
785
|
+
overlays.forEach((o, i) => {
|
|
786
|
+
const speed = ((canvas.width + o.moveW) / scrollTime).toFixed(3);
|
|
787
|
+
const s = (o.startTime / 1000).toFixed(3);
|
|
788
|
+
const e = (o.endTime / 1000).toFixed(3);
|
|
789
|
+
const label = i === overlays.length - 1 ? 'vout' : `v${i}`;
|
|
790
|
+
parts.push(`[${prev}][${inputIndices[i]}:v]overlay=x='${canvas.width}-(t-${s})*${speed}':y=${o.y}:enable='between(t,${s},${e})'[${label}]`);
|
|
791
|
+
prev = label;
|
|
792
|
+
});
|
|
793
|
+
return parts.join(';');
|
|
794
|
+
}
|
|
795
|
+
/**
|
|
796
|
+
* 生成相对路径转换器:能相对化的转相对路径(正斜杠),否则保留绝对路径
|
|
797
|
+
* 用于把 ffmpeg 命令行长度控制在 Windows cmd 的 8191 字符上限内
|
|
798
|
+
* @param workDir 工作目录(ffmpeg 的 cwd)
|
|
799
|
+
* @returns 路径转换函数
|
|
800
|
+
*/
|
|
801
|
+
const createRelativizer = (workDir) => {
|
|
802
|
+
return (p) => {
|
|
803
|
+
const rel = node_path_1.default.relative(workDir, p).replace(/\\/g, '/');
|
|
804
|
+
return rel && !rel.startsWith('..') ? rel : p.replace(/\\/g, '/');
|
|
805
|
+
};
|
|
806
|
+
};
|
|
807
|
+
/**
|
|
808
|
+
* 规划图片弹幕的 ffmpeg 输入:内容相同的弹幕条复用同一个输入,减少命令行长度
|
|
809
|
+
* @param videoPath 视频文件路径(相对化后)
|
|
810
|
+
* @param overlays 图片弹幕 overlay 列表
|
|
811
|
+
* @param toRel 相对路径转换器
|
|
812
|
+
* @returns 输入参数列表与每条 overlay 的输入序号
|
|
813
|
+
*/
|
|
814
|
+
function planOverlayInputs(videoPath, overlays, toRel) {
|
|
815
|
+
const inputIndexByPath = new Map();
|
|
816
|
+
const inputArgs = [`-i "${toRel(videoPath)}"`];
|
|
817
|
+
const inputIndices = overlays.map((o) => {
|
|
818
|
+
let idx = inputIndexByPath.get(o.pngPath);
|
|
819
|
+
if (idx === undefined) {
|
|
820
|
+
idx = inputIndexByPath.size + 1;
|
|
821
|
+
inputIndexByPath.set(o.pngPath, idx);
|
|
822
|
+
inputArgs.push(`-i "${toRel(o.pngPath)}"`);
|
|
823
|
+
}
|
|
824
|
+
return idx;
|
|
825
|
+
});
|
|
826
|
+
return { inputArgs, inputIndices };
|
|
827
|
+
}
|
|
828
|
+
/**
|
|
829
|
+
* 烧录抖音弹幕到视频
|
|
830
|
+
* @param videoPath 源视频文件路径
|
|
831
|
+
* @param danmakuList 弹幕列表
|
|
832
|
+
* @param outputPath 输出视频文件路径
|
|
833
|
+
* @param options 烧录配置
|
|
834
|
+
* @returns 是否烧录成功
|
|
835
|
+
*/
|
|
836
|
+
async function burnDouyinDanmaku(videoPath, danmakuList, outputPath, options = {}) {
|
|
837
|
+
const { removeSource = false, verticalMode = 'off', videoCodec = 'h265', scrollTime = 8 } = options;
|
|
838
|
+
if (!node_fs_1.default.existsSync(videoPath)) {
|
|
839
|
+
node_karin_1.logger.error(`[DouyinDanmaku] 视频文件不存在: ${videoPath}`);
|
|
840
|
+
return false;
|
|
841
|
+
}
|
|
842
|
+
const resolution = await getDouyinResolution(videoPath);
|
|
843
|
+
const sourceBitrate = await getVideoBitrate(videoPath);
|
|
844
|
+
const canvas = calcCanvas(resolution.width, resolution.height, verticalMode);
|
|
845
|
+
if (canvas.isVertical) {
|
|
846
|
+
node_karin_1.logger.debug(`[DouyinDanmaku] 竖屏模式: ${resolution.width}x${resolution.height} -> ${canvas.width}x${canvas.height}`);
|
|
847
|
+
}
|
|
848
|
+
node_karin_1.logger.debug(`[DouyinDanmaku] 分辨率: ${canvas.width}x${canvas.height}, 码率: ${sourceBitrate}kbps`);
|
|
849
|
+
// 生成 ASS 与图片弹幕 overlay(使用画布尺寸)
|
|
850
|
+
const { ass, overlays, tempFiles, stats } = await generateDouyinASS(danmakuList, canvas.width, canvas.height, options);
|
|
851
|
+
const assPath = videoPath.replace(/\.[^.]+$/, '_danmaku.ass');
|
|
852
|
+
node_fs_1.default.writeFileSync(assPath, ass, 'utf-8');
|
|
853
|
+
node_karin_1.logger.debug(`[DouyinDanmaku] 弹幕字幕已生成: ${assPath},共 ${danmakuList.length} 条,` +
|
|
854
|
+
`图片弹幕 ${overlays.length} 条(点赞角标 ${stats.likedOverlays} 条 [候选 ${stats.likedCandidates}/目标 ${stats.likedTarget}],表情 ${stats.emojiOverlays} 条)`);
|
|
855
|
+
// 编码(使用原视频码率作为目标)
|
|
856
|
+
const encoder = await detectEncoder(videoCodec);
|
|
857
|
+
const encoderParams = getEncoderParams(encoder, sourceBitrate);
|
|
858
|
+
let command;
|
|
859
|
+
let execCwd;
|
|
860
|
+
if (overlays.length === 0) {
|
|
861
|
+
const filter = buildFilter(canvas, assPath);
|
|
862
|
+
command = `-y -i "${videoPath}" -vf "${filter}" -r ${OUTPUT_FPS} ${encoderParams} -c:a copy "${outputPath}"`;
|
|
863
|
+
}
|
|
864
|
+
else {
|
|
865
|
+
// PNG 是单帧输入,overlay 默认 eof_action=repeat 会保持最后一帧,无需 -loop(looped 输入会导致编码无法终止)
|
|
866
|
+
// 图片弹幕多的时候命令行会超过 Windows cmd 的 8191 字符上限,因此:
|
|
867
|
+
// 1. 以视频所在目录为 cwd,所有路径转相对路径
|
|
868
|
+
// 2. 内容相同的弹幕条复用同一个输入
|
|
869
|
+
// 3. filter_complex 写入脚本文件,用 -filter_complex_script 引用
|
|
870
|
+
const workDir = node_path_1.default.dirname(videoPath);
|
|
871
|
+
execCwd = workDir;
|
|
872
|
+
const toRel = createRelativizer(workDir);
|
|
873
|
+
const { inputArgs, inputIndices } = planOverlayInputs(videoPath, overlays, toRel);
|
|
874
|
+
const graphPath = videoPath.replace(/\.[^.]+$/, '_danmaku.graph.txt');
|
|
875
|
+
node_fs_1.default.writeFileSync(graphPath, buildFilterComplex(canvas, toRel(assPath), overlays, inputIndices, scrollTime), 'utf-8');
|
|
876
|
+
tempFiles.push(graphPath);
|
|
877
|
+
command = `-y ${inputArgs.join(' ')} -filter_complex_script "${toRel(graphPath)}" -map "[vout]" -map "0:a?" -r ${OUTPUT_FPS} ${encoderParams} -c:a copy "${toRel(outputPath)}"`;
|
|
878
|
+
}
|
|
879
|
+
const result = await (0, node_karin_1.ffmpeg)(command, execCwd ? { cwd: execCwd } : undefined);
|
|
880
|
+
utils_1.Common.removeFile(assPath, true);
|
|
881
|
+
for (const file of tempFiles) {
|
|
882
|
+
utils_1.Common.removeFile(file, true);
|
|
883
|
+
}
|
|
884
|
+
if (result.status) {
|
|
885
|
+
node_karin_1.logger.mark(`[DouyinDanmaku] 弹幕烧录成功: ${outputPath}`);
|
|
886
|
+
if (removeSource)
|
|
887
|
+
utils_1.Common.removeFile(videoPath);
|
|
888
|
+
}
|
|
889
|
+
else {
|
|
890
|
+
node_karin_1.logger.error('[DouyinDanmaku] 弹幕烧录失败', result);
|
|
891
|
+
}
|
|
892
|
+
return result.status;
|
|
893
|
+
}
|