From f199f69d8b803fd4e5b508fc6506506f2c653c07 Mon Sep 17 00:00:00 2001 From: huangxd- <649924757@qq.com> Date: Sun, 28 Dec 2025 11:02:50 +0800 Subject: [PATCH] =?UTF-8?q?=E6=94=AF=E6=8C=81=E8=87=AA=E5=AE=9A=E4=B9=89?= =?UTF-8?q?=E5=BC=B9=E5=B9=95=E6=BA=90=20(#121)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 解决api配置反代后的ui路径问题 * ui修改body和header背景色 * 修复segmentcomment接口调试时format选择xml不生效的问题 * Revert "修复segmentcomment接口调试时format选择xml不生效的问题" This reverts commit 997a101cb0537d95ebdb62e9f1267af7e94475a9. * 修复分片弹幕请求xml还是返回json的问题 * 支持自定义弹幕源 * 修复在反代场景下api端点显示 * 修复导航栏标签切换问题 * 修复iqiyi源的弹幕emoji不显示的问题 * 完善readme中的项目结构 * 修改小版本号 --- README.md | 27 ++-- config/.env.example | 8 +- config/config.yaml.example | 8 +- danmu_api/apis/dandan-api.js | 14 +- danmu_api/configs/envs.js | 6 +- danmu_api/configs/globals.js | 2 +- danmu_api/sources/custom.js | 203 +++++++++++++++++++++++++++++ danmu_api/sources/iqiyi.js | 4 +- danmu_api/ui/css/base.css.js | 4 +- danmu_api/ui/css/components.css.js | 3 +- danmu_api/ui/js/apitest.js | 10 ++ danmu_api/ui/js/main.js | 45 +++++-- danmu_api/ui/js/preview.js | 2 +- danmu_api/ui/js/systemsettings.js | 4 +- danmu_api/ui/template.js | 22 ++-- danmu_api/utils/codec-util.js | 34 ++++- forward/forward-widget.js | 38 ++++-- forward/forward-widget.test.js | 1 + 18 files changed, 368 insertions(+), 67 deletions(-) create mode 100644 danmu_api/sources/custom.js diff --git a/README.md b/README.md index 1d173e5..d80c243 100644 --- a/README.md +++ b/README.md @@ -303,7 +303,7 @@ LogVar 弹幕 API 服务器 > cf部署可能不稳定,推荐用vercel/netlify部署。 ## API食用指南 -支持 forward/senplayer/hills/小幻/yamby/eplayerx/afusekt/uz影视/dscloud/lenna/danmaku-anywhere/omnibox/ChaiChaiEmbyTV 等支持弹幕API的播放器。 +支持 forward/senplayer/hills/小幻/yamby/eplayerx/afusekt/uz影视/dscloud/lenna/danmaku-anywhere/omnibox/ChaiChaiEmbyTV/moontv 等支持弹幕API的播放器。 配合 dd-danmaku 扩展新增对 Emby Web 端弹幕的支持,具体使用方法参考 [PR #98](https://github.com/huangxd-/danmu_api/pull/98) 。 @@ -364,13 +364,14 @@ API 支持返回 Bilibili 标准 XML 格式的弹幕数据,通过查询参数 | TOKEN | 【可选】自定义用户token,不填默认为`87654321` | | ADMIN_TOKEN | 【可选】系统管理访问令牌,如果未配置此值,则无法访问系统管理功能,需要先配置后在URL中填入此token才能打开系统管理 | | OTHER_SERVER | 【可选】兜底第三方弹幕服务器,不填默认为`https://api.danmu.icu`,其他可选:`https://fc.lyz05.cn`,`https://dmku.hls.one`,`https://se.678.ooo`,`https://danmu.56uxi.com`,`https://dm.lxlad.com` | +| CUSTOM_SOURCE_API_URL | 【可选】自定义弹幕源API地址,默认为空,配置后还需在SOURCE_ORDER添加custom源 | | VOD_SERVERS | 【可选】VOD服务器列表,支持多个服务器并发查询,格式:`名称@URL,名称@URL,...`,示例:`金蝉@https://zy.jinchancaiji.com,789@https://www.caiji.cyou,听风@https://gctf.tfdh.top`,不填默认为`金蝉@https://zy.jinchancaiji.com,789@https://www.caiji.cyou,听风@https://gctf.tfdh.top` | | VOD_RETURN_MODE | 【可选】VOD返回模式,可选值:`all`(返回所有站点结果)、`fastest`(只返回最快的站点结果),默认为`fastest`。当配置多个VOD站点时,`all`模式会返回所有站点的结果(结果较多),`fastest`模式只返回首先响应成功的站点结果(结果较少,避免重复) | | VOD_REQUEST_TIMEOUT | 【可选】VOD服务器单个请求超时时间(毫秒),防止慢速或失效的采集站阻塞搜索,默认为`10000`(10秒),建议值:`5000-15000`。由于`fastest`模式只返回最快响应的站点,可以设置较大的超时时间给慢速站点更多机会 | | BILIBILI_COOKIE | 【可选】b站cookie(填入后能抓取完整弹幕),如 `buvid3=E2BCA ... eao6; theme-avatar-tip-show=SHOWED`,请自行通过浏览器或抓包工具抓取,热心网友测试后,实际最少只需取 `SESSDATA=xxxx` 字段 | | YOUKU_CONCURRENCY | 【可选】youku弹幕请求并发数,用于加快youku弹幕请求速度,不填默认为`8`,最高`16` | -| SOURCE_ORDER | 【可选】源排序,用于按源对返回资源的排序(注意:先后顺序会影响自动匹配最终的返回),默认是`360,vod,renren,hanjutv`,表示360数据排在最前,hanjutv数据排在最后,示例:`360,renren`:只返回360数据和renren数据,且360数据靠前;当前可选择的源字段有 `360,vod,tmdb,douban,tencent,youku,iqiyi,imgo,bilibili,renren,hanjutv,bahamut,dandan` | -| PLATFORM_ORDER | 【可选】自动匹配优选平台,按顺序优先返回指定平台弹幕,默认为空,即返回第一个满足条件的平台,示例:`bilibili1,qq`,表示如果有b站的播放源,则优先返回b站的弹幕,否则就返回腾讯的弹幕,两者都没有,则返回第一个满足条件的平台;当前可选择的平台字段有 `qiyi, bilibili1, imgo, youku, qq, renren, hanjutv, bahamut, dandan` | +| SOURCE_ORDER | 【可选】源排序,用于按源对返回资源的排序(注意:先后顺序会影响自动匹配最终的返回),默认是`360,vod,renren,hanjutv`,表示360数据排在最前,hanjutv数据排在最后,示例:`360,renren`:只返回360数据和renren数据,且360数据靠前;当前可选择的源字段有 `360,vod,tmdb,douban,tencent,youku,iqiyi,imgo,bilibili,renren,hanjutv,bahamut,dandan,custom` | +| PLATFORM_ORDER | 【可选】自动匹配优选平台,按顺序优先返回指定平台弹幕,默认为空,即返回第一个满足条件的平台,示例:`bilibili1,qq`,表示如果有b站的播放源,则优先返回b站的弹幕,否则就返回腾讯的弹幕,两者都没有,则返回第一个满足条件的平台;当前可选择的平台字段有 `qiyi, bilibili1, imgo, youku, qq, renren, hanjutv, bahamut, dandan, custom` | | EPISODE_TITLE_FILTER | 【可选】剧集标题正则过滤,按正则关键字对剧集或综艺的集标题进行过滤,适用于过滤一些预告或综艺非正式集,只支持match自动匹配,默认值如下 | | ENABLE_EPISODE_FILTER | 【可选】是否在手动选择接口中启用集标题过滤,默认为`false`(禁用),启用后 GET /api/v2/bangumi/{id} 和 GET /api/v2/search/anime 接口会过滤掉预告、花絮等特殊集,以及名称包含特殊关键词的动漫。 | | STRICT_TITLE_MATCH | 【可选】是否启用严格标题匹配模式,默认为`false`(宽松模糊匹配),启用后只匹配标题开头或完全匹配的结果。例如:搜索"遮天"时,`false`会匹配"古惑仔3之只手遮天",`true`只匹配"遮天"、"遮天 第一季"等。可选值:`true`、`false` | @@ -444,18 +445,12 @@ API 支持返回 Bilibili 标准 XML 格式的弹幕数据,通过查询参数 | hanjutv | hanjutv | | bahamut | bahamut | | dandan | dandan | +| custom | custom | ## 项目结构 ``` -danmu_api/ -├── .github/ -│ └── workflows/ -│ ├── docker-image.yml # Docker镜像构建和推送工作流 -│ └── sync_fork.yml # Fork同步上游仓库工作流(vercel自动同步配置文件) -├── config/ -│ ├── .env.example # .env 配置文件示例 -│ └── config.yaml.example # YAML 配置文件示例(无法编辑 .env 时使用) ├── .gitignore +├── build-forward-widget.js # 构建forward弹幕插件脚本 ├── Dockerfile ├── edgeone.json # edgeone pages 配置文件 ├── LICENSE @@ -464,6 +459,9 @@ danmu_api/ ├── README.md ├── vercel.json # vercel 配置文件 ├── wrangler.toml # cloudflare worker 配置文件 +├── config/ +│ ├── .env.example # .env 配置文件示例 +│ └── config.yaml.example # YAML 配置文件示例(无法编辑 .env 时使用) ├── danmu_api/ │ ├── esm-shim.js # Node.js低版本兼容层 │ ├── server.js # 本地node启动脚本 @@ -490,6 +488,7 @@ danmu_api/ │ │ ├── bahamut.js # 巴哈姆特源 │ │ ├── base.js # 弹幕源获取基类 │ │ ├── bilibili.js # b站源 +│ │ ├── custom.js # 自定义弹幕源 │ │ ├── dandan.js # 弹弹play源 │ │ ├── douban.js # 豆瓣源 │ │ ├── hanjutv.js # 韩剧TV源 @@ -530,6 +529,10 @@ danmu_api/ │ ├── time-util.js # 时间日期工具 │ ├── tmdb-util.js # TMDB API请求处理工具 │ └── zh-util.js # 中文繁简转换工具 +├── forward/ +│ ├── custom-polyfill.js # 自定义polyfill +│ ├── forward-widget.js # forward弹幕插件 +│ └── forward-widget.test.js # forward弹幕插件测试文件 ├── netlify/ │ └── functions/ │ └── api.js # netlify 中间处理逻辑 @@ -555,7 +558,9 @@ danmu_api/ - 一键部署需要将项目推送到公开的 Git 仓库(如 GitHub),并更新按钮中的仓库地址。 - 运行 Docker 容器时,需通过 `-e TOKEN=87654321` 传递 `TOKEN` 环境变量。 - cloudflare貌似被哔风控了。 +- cloudflare貌似有单次请求数量限制,会导致后半部分没有弹幕。 - 如果想更换兜底第三方弹幕服务器,请添加环境变量`OTHER_SERVER`,示例`https://api.danmu.icu`。 +- 如果想使用自定义弹幕源,请添加环境变量`CUSTOM_SOURCE_API_URL`,并在`SOURCE_ORDER`环境变量中添加`custom`源。 - 如果想更换vod站点,请添加环境变量`VOD_SERVERS`,示例`金蝉@https://zy.jinchancaiji.com,789@https://www.caiji.cyou,听风@https://gctf.tfdh.top`(支持多个服务器并发查询)。 - 当配置多个VOD站点时,可通过`VOD_RETURN_MODE`环境变量控制返回结果方式:`all`(返回所有站点结果)或`fastest`(默认,只返回最快的站点结果,避免结果过多)。 - 推荐vercel/netlify部署,cloudflare/edgeone/claw不稳定,当然最稳定还是自己本地docker部署最佳。 diff --git a/config/.env.example b/config/.env.example index c816999..04e99f4 100644 --- a/config/.env.example +++ b/config/.env.example @@ -30,6 +30,10 @@ TOKEN=87654321 # 默认值:https://api.danmu.icu OTHER_SERVER=https://api.danmu.icu +# 自定义弹幕源API地址 +# 默认为空,配置后还需在SOURCE_ORDER添加custom源 +# CUSTOM_SOURCE_API_URL= + # ==================== VOD 配置 ==================== # VOD 服务器配置(支持多服务器) @@ -68,7 +72,7 @@ YOUKU_CONCURRENCY=8 # ==================== 源排序配置 ==================== -# 数据源查询顺序(可选值:360, vod, tmdb, douban, tencent, youku, iqiyi, imgo, bilibili, renren, hanjutv, bahamut, dandan) +# 数据源查询顺序(可选值:360, vod, tmdb, douban, tencent, youku, iqiyi, imgo, bilibili, renren, hanjutv, bahamut, dandan, custom) # 默认值:360,vod,renren,hanjutv(不包含 tencent, youku, iqiyi, imgo 和 bilibili) # 如需使用腾讯源、优酷源、爱奇艺源、芒果源或B站源,请手动添加:SOURCE_ORDER=360,vod,tencent,youku,iqiyi,imgo,bilibili,renren,hanjutv # 如需使用巴哈姆特源,请手动添加:SOURCE_ORDER=360,vod,renren,hanjutv,bahamut @@ -81,7 +85,7 @@ SOURCE_ORDER=360,vod,renren,hanjutv # 平台优先级排序(用于自动匹配) # 默认值:空(自动匹配) -# 可选值:qiyi, bilibili1, imgo, youku, qq, renren, hanjutv, bahamut, dandan +# 可选值:qiyi, bilibili1, imgo, youku, qq, renren, hanjutv, bahamut, dandan, custom # PLATFORM_ORDER= # ==================== 剧集标题过滤 ==================== diff --git a/config/config.yaml.example b/config/config.yaml.example index e5a932c..c7e6002 100644 --- a/config/config.yaml.example +++ b/config/config.yaml.example @@ -26,6 +26,10 @@ TOKEN: "87654321" # 默认值:https://api.danmu.icu OTHER_SERVER: "https://api.danmu.icu" +# 自定义弹幕源API地址 +# 默认为空,配置后还需在SOURCE_ORDER添加custom源 +# CUSTOM_SOURCE_API_URL: + # ==================== VOD 配置 ==================== # VOD 服务器配置(支持多服务器) @@ -64,7 +68,7 @@ YOUKU_CONCURRENCY: 8 # ==================== 源排序配置 ==================== -# 数据源查询顺序(可选值:360, vod, tmdb, douban, tencent, youku, iqiyi, imgo, bilibili, renren, hanjutv, bahamut, dandan) +# 数据源查询顺序(可选值:360, vod, tmdb, douban, tencent, youku, iqiyi, imgo, bilibili, renren, hanjutv, bahamut, dandan, custom) # 默认值:360,vod,renren,hanjutv(不包含 tencent, youku, iqiyi, imgo 和 bilibili) # 如需使用腾讯源、优酷源、爱奇艺源、芒果源或B站源,请手动添加:SOURCE_ORDER=360,vod,tencent,youku,iqiyi,imgo,bilibili,renren,hanjutv # 如需使用巴哈姆特源,请手动添加:SOURCE_ORDER=360,vod,renren,hanjutv,bahamut @@ -77,7 +81,7 @@ SOURCE_ORDER: "360,vod,renren,hanjutv" # 平台优先级排序(用于自动匹配) # 默认值:空(自动匹配) -# 可选值:qiyi, bilibili1, imgo, youku, qq, renren, hanjutv, bahamut, dandan +# 可选值:qiyi, bilibili1, imgo, youku, qq, renren, hanjutv, bahamut, dandan, custom # PLATFORM_ORDER: "" # ==================== 剧集标题过滤 ==================== diff --git a/danmu_api/apis/dandan-api.js b/danmu_api/apis/dandan-api.js index 8fcae55..b6b83e9 100644 --- a/danmu_api/apis/dandan-api.js +++ b/danmu_api/apis/dandan-api.js @@ -18,6 +18,7 @@ import RenrenSource from "../sources/renren.js"; import HanjutvSource from "../sources/hanjutv.js"; import BahamutSource from "../sources/bahamut.js"; import DandanSource from "../sources/dandan.js"; +import CustomSource from "../sources/custom.js"; import TencentSource from "../sources/tencent.js"; import IqiyiSource from "../sources/iqiyi.js"; import MangoSource from "../sources/mango.js"; @@ -36,6 +37,7 @@ const renrenSource = new RenrenSource(); const hanjutvSource = new HanjutvSource(); const bahamutSource = new BahamutSource(); const dandanSource = new DandanSource(); +const customSource = new CustomSource(); const tencentSource = new TencentSource(); const youkuSource = new YoukuSource(); const iqiyiSource = new IqiyiSource(); @@ -184,6 +186,7 @@ export async function searchAnime(url, preferAnimeId = null, preferSource = null if (source === "hanjutv") return hanjutvSource.search(queryTitle); if (source === "bahamut") return bahamutSource.search(queryTitle); if (source === "dandan") return dandanSource.search(queryTitle); + if (source === "custom") return customSource.search(queryTitle); if (source === "tencent") return tencentSource.search(queryTitle); if (source === "youku") return youkuSource.search(queryTitle); if (source === "iqiyi") return iqiyiSource.search(queryTitle); @@ -205,8 +208,8 @@ export async function searchAnime(url, preferAnimeId = null, preferSource = null // 解构出返回的结果 const { vod: animesVodResults, 360: animes360, tmdb: animesTmdb, douban: animesDouban, renren: animesRenren, - hanjutv: animesHanjutv, bahamut: animesBahamut, dandan: animesDandan, tencent: animesTencent, youku: animesYouku, - iqiyi: animesIqiyi, imgo: animesImgo, bilibili: animesBilibili + hanjutv: animesHanjutv, bahamut: animesBahamut, dandan: animesDandan, custom: animesCustom, + tencent: animesTencent, youku: animesYouku, iqiyi: animesIqiyi, imgo: animesImgo, bilibili: animesBilibili } = resultData; // 按顺序处理每个来源的结果 @@ -241,6 +244,9 @@ export async function searchAnime(url, preferAnimeId = null, preferSource = null } else if (key === 'dandan') { // 等待处理弹弹play来源 await dandanSource.handleAnimes(animesDandan, queryTitle, curAnimes); + } else if (key === 'custom') { + // 等待处理自定义弹幕源来源 + await customSource.handleAnimes(animesCustom, queryTitle, curAnimes); } else if (key === 'tencent') { // 等待处理Tencent来源 await tencentSource.handleAnimes(animesTencent, queryTitle, curAnimes); @@ -935,6 +941,8 @@ export async function getComment(path, queryFormat, segmentFlag) { danmus = await bahamutSource.getComments(url, plat, segmentFlag); } else if (plat === "dandan") { danmus = await dandanSource.getComments(url, plat, segmentFlag); + } else if (plat === "custom") { + danmus = await customSource.getComments(url, plat, segmentFlag); } } @@ -1105,6 +1113,8 @@ export async function getSegmentComment(segment, queryFormat) { danmus = await renrenSource.getSegmentComments(segment); } else if (platform === "dandan") { danmus = await dandanSource.getSegmentComments(segment); + } else if (platform === "custom") { + danmus = await customSource.getSegmentComments(segment); } else if (platform === "other_server") { danmus = await otherSource.getSegmentComments(segment); } diff --git a/danmu_api/configs/envs.js b/danmu_api/configs/envs.js index d2105d2..4c0f88a 100644 --- a/danmu_api/configs/envs.js +++ b/danmu_api/configs/envs.js @@ -10,8 +10,8 @@ export class Envs { static accessedEnvVars = new Map(); static VOD_ALLOWED_PLATFORMS = ['qiyi', 'bilibili1', 'imgo', 'youku', 'qq']; // vod允许的播放平台 - static ALLOWED_PLATFORMS = ['qiyi', 'bilibili1', 'imgo', 'youku', 'qq', 'renren', 'hanjutv', 'bahamut', 'dandan']; // 全部源允许的播放平台 - static ALLOWED_SOURCES = ['360', 'vod', 'tmdb', 'douban', 'tencent', 'youku', 'iqiyi', 'imgo', 'bilibili', 'renren', 'hanjutv', 'bahamut', 'dandan']; // 允许的源 + static ALLOWED_PLATFORMS = ['qiyi', 'bilibili1', 'imgo', 'youku', 'qq', 'renren', 'hanjutv', 'bahamut', 'dandan', 'custom']; // 全部源允许的播放平台 + static ALLOWED_SOURCES = ['360', 'vod', 'tmdb', 'douban', 'tencent', 'youku', 'iqiyi', 'imgo', 'bilibili', 'renren', 'hanjutv', 'bahamut', 'dandan', 'custom']; // 允许的源 /** * 获取环境变量 @@ -206,6 +206,7 @@ export class Envs { // 源配置 'SOURCE_ORDER': { category: 'source', type: 'multi-select', options: this.ALLOWED_SOURCES, description: '源排序配置,默认360,vod,renren,hanjutv' }, 'OTHER_SERVER': { category: 'source', type: 'text', description: '第三方弹幕服务器,默认https://api.danmu.icu' }, + 'CUSTOM_SOURCE_API_URL': { category: 'source', type: 'text', description: '自定义弹幕源API地址,默认为空,配置后还需在SOURCE_ORDER添加custom源' }, 'VOD_SERVERS': { category: 'source', type: 'text', description: 'VOD站点配置,格式:名称@URL,名称@URL,默认金蝉@https://zy.jinchancaiji.com,789@https://www.caiji.cyou,听风@https://gctf.tfdh.top' }, 'VOD_RETURN_MODE': { category: 'source', type: 'select', options: ['all', 'fastest'], description: 'VOD返回模式:all(所有站点)或 fastest(最快的站点),默认fastest' }, 'VOD_REQUEST_TIMEOUT': { category: 'source', type: 'number', description: 'VOD请求超时时间,默认10000', min: 5000, max: 30000 }, @@ -254,6 +255,7 @@ export class Envs { adminToken: this.get('ADMIN_TOKEN', '', 'string', true), // admin token,用于系统管理访问控制 sourceOrderArr: this.resolveSourceOrder(), // 源排序 otherServer: this.get('OTHER_SERVER', 'https://api.danmu.icu', 'string'), // 第三方弹幕服务器 + customSourceApiUrl: this.get('CUSTOM_SOURCE_API_URL', '', 'string', true), // 自定义弹幕源API地址,默认为空,配置后还需在SOURCE_ORDER添加custom源 vodServers: this.resolveVodServers(), // vod站点配置,格式:名称@URL,名称@URL vodReturnMode: this.get('VOD_RETURN_MODE', 'fastest', 'string').toLowerCase(), // vod返回模式:all(所有站点)或 fastest(最快的站点) vodRequestTimeout: this.get('VOD_REQUEST_TIMEOUT', '10000', 'string'), // vod超时时间(默认10秒) diff --git a/danmu_api/configs/globals.js b/danmu_api/configs/globals.js index c7ead25..16131c0 100644 --- a/danmu_api/configs/globals.js +++ b/danmu_api/configs/globals.js @@ -13,7 +13,7 @@ export const Globals = { accessedEnvVars: {}, // 静态常量 - VERSION: '1.10.0', + VERSION: '1.10.1', MAX_LOGS: 500, // 日志存储,最多保存 500 行 MAX_ANIMES: 100, diff --git a/danmu_api/sources/custom.js b/danmu_api/sources/custom.js new file mode 100644 index 0000000..ff246fc --- /dev/null +++ b/danmu_api/sources/custom.js @@ -0,0 +1,203 @@ +import BaseSource from './base.js'; +import { globals } from '../configs/globals.js'; +import { log } from "../utils/log-util.js"; +import { httpGet } from "../utils/http-util.js"; +import { addAnime, removeEarliestAnime } from "../utils/cache-util.js"; +import { simplized } from "../utils/zh-util.js"; +import { SegmentListResponse } from '../models/dandan-model.js'; + +// ===================== +// 获取自定义源弹幕 +// ===================== +export default class CustomSource extends BaseSource { + async search(keyword) { + try { + const resp = await httpGet(`${globals.customSourceApiUrl}/api/v2/search/anime?keyword=${keyword}`, { + headers: { + "Content-Type": "application/json", + "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36", + }, + }); + + // 判断 resp 和 resp.data 是否存在 + if (!resp || !resp.data) { + log("info", "customSourceSearchresp: 请求失败或无数据返回"); + return []; + } + + // 判断 seriesData 是否存在 + if (!resp.data.animes) { + log("info", "customSourceSearchresp: seriesData 或 seriesList 不存在"); + return []; + } + + // 正常情况下输出 JSON 字符串 + log("info", `customnSourceSearchresp: ${JSON.stringify(resp.data.animes)}`); + + return resp.data.animes; + } catch (error) { + // 捕获请求中的错误 + log("error", "getCustomSourceAnimes error:", { + message: error.message, + name: error.name, + stack: error.stack, + }); + return []; + } + } + + async getEpisodes(id) { + try { + const resp = await httpGet(`${globals.customSourceApiUrl}/api/v2/bangumi/${id}`, { + headers: { + "Content-Type": "application/json", + "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36", + }, + }); + + // 判断 resp 和 resp.data 是否存在 + if (!resp || !resp.data) { + log("info", "getCustomSourceEposides: 请求失败或无数据返回"); + return []; + } + + // 判断 seriesData 是否存在 + if (!resp.data.bangumi || !resp.data.bangumi.episodes) { + log("info", `getCustomSourceEposides: episodes 不存在. Response: ${JSON.stringify(resp.data)}`); + return []; + } + + // 正常情况下输出 JSON 字符串 + log("info", `getCustomSourceEposides: ${JSON.stringify(resp.data.bangumi.episodes)}`); + + return resp.data.bangumi.episodes; + } catch (error) { + // 捕获请求中的错误 + log("error", "getCustomSourceEposides error:", { + message: error.message, + name: error.name, + stack: error.stack, + }); + return []; + } + } + + async handleAnimes(sourceAnimes, queryTitle, curAnimes) { + const tmpAnimes = []; + + // 添加错误处理,确保sourceAnimes是数组 + if (!sourceAnimes || !Array.isArray(sourceAnimes)) { + log("error", "[Custom Source] sourceAnimes is not a valid array"); + return []; + } + + // 使用 map 和 async 时需要返回 Promise 数组,并等待所有 Promise 完成 + const processCustomSourceAnimes = await Promise.all(sourceAnimes + .map(async (anime) => { + try { + const eps = await this.getEpisodes(anime.bangumiId); + let links = []; + for (const ep of eps) { + const epTitle = ep.episodeTitle && ep.episodeTitle.trim() !== "" ? `${ep.episodeTitle}` : `第${ep.episodeNumber}集`; + links.push({ + "name": epTitle, + "url": ep.episodeId.toString(), + "title": `【custom】 ${epTitle}` + }); + } + + if (links.length > 0) { + let transformedAnime = { + animeId: anime.animeId, + bangumiId: String(anime.bangumiId), + animeTitle: `${anime.animeTitle}(${new Date(anime.startDate).getFullYear()})【${anime.typeDescription}】from custom`, + type: anime.type, + typeDescription: anime.typeDescription, + imageUrl: anime.imageUrl, + startDate: anime.startDate, + episodeCount: links.length, + rating: anime.rating, + isFavorited: true, + source: "custom", + }; + + tmpAnimes.push(transformedAnime); + + addAnime({...transformedAnime, links: links}); + + if (globals.animes.length > globals.MAX_ANIMES) removeEarliestAnime(); + } + } catch (error) { + log("error", `[Custom Source] Error processing anime: ${error.message}`); + } + }) + ); + + this.sortAndPushAnimesByYear(tmpAnimes, curAnimes); + + return processCustomSourceAnimes; + } + + async getEpisodeDanmu(id) { + let allDanmus = []; + + try { + const resp = await httpGet(`${globals.customSourceApiUrl}/api/v2/comment/${id}`, { + headers: { + "Content-Type": "application/json", + "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36", + }, + retries: 1, + }); + + // 将当前请求的 episodes 拼接到总数组 + if (resp.data && resp.data.comments) { + allDanmus = resp.data.comments; + } + + return allDanmus; + } catch (error) { + // 捕获请求中的错误 + log("error", "fetchCustomSourceEpisodeDanmu error:", { + message: error.message, + name: error.name, + stack: error.stack, + }); + return allDanmus; // 返回已收集的 episodes + } + } + + async getEpisodeDanmuSegments(id) { + log("info", "获取Custom Source弹幕分段列表...", id); + + return new SegmentListResponse({ + "type": "custom", + "segmentList": [{ + "type": "custom", + "segment_start": 0, + "segment_end": 30000, + "url": id + }] + }); + } + + async getEpisodeSegmentDanmu(segment) { + return this.getEpisodeDanmu(segment.url); + } + + formatComments(comments) { + return comments.map(c => ({ + cid: c.cid, + p: `${c.p.replace(/([A-Za-z]+)([0-9a-fA-F]{6})/, (_, platform, hexColor) => { + // 转换 hexColor 为十进制颜色值 + const r = parseInt(hexColor.substring(0, 2), 16); + const g = parseInt(hexColor.substring(2, 4), 16); + const b = parseInt(hexColor.substring(4, 6), 16); + const decimalColor = r * 256 * 256 + g * 256 + b; + return `${platform}${decimalColor}`; + })}`, + // 根据 globals.danmuSimplified 控制是否繁转简 + m: globals.danmuSimplified ? simplized(c.m) : c.m, + })); + } +} \ No newline at end of file diff --git a/danmu_api/sources/iqiyi.js b/danmu_api/sources/iqiyi.js index 79b1c51..736f959 100644 --- a/danmu_api/sources/iqiyi.js +++ b/danmu_api/sources/iqiyi.js @@ -2,7 +2,7 @@ import BaseSource from './base.js'; import { log } from "../utils/log-util.js"; import { buildQueryString, httpGet} from "../utils/http-util.js"; import { printFirst200Chars, titleMatches } from "../utils/common-util.js"; -import { md5, convertToAsciiSum } from "../utils/codec-util.js"; +import { md5, convertToAsciiSum, decodeHtmlEntities } from "../utils/codec-util.js"; import { generateValidStartDate } from "../utils/time-util.js"; import { addAnime, removeEarliestAnime } from "../utils/cache-util.js"; import { globals } from '../configs/globals.js'; @@ -898,7 +898,7 @@ export default class IqiyiSource extends BaseSource { }; content.timepoint = parseFloat(item["showTime"]); content.color = parseInt(item["color"], 16); - content.content = item["content"]; + content.content = decodeHtmlEntities(item["content"]); content.size = 25; return content; }); diff --git a/danmu_api/ui/css/base.css.js b/danmu_api/ui/css/base.css.js index a8dc7dc..4089311 100644 --- a/danmu_api/ui/css/base.css.js +++ b/danmu_api/ui/css/base.css.js @@ -9,7 +9,7 @@ export const baseCssContent = /* css */ ` body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; - background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + background: linear-gradient(135deg, #a0b9e8ff 0%, #e39db4ff 100%); min-height: 100vh; padding: 20px; } @@ -24,7 +24,7 @@ body { } .header { - background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + background: linear-gradient(135deg, #1a2980 0%, #26d0ce 100%); color: white; padding: 20px; display: flex; diff --git a/danmu_api/ui/css/components.css.js b/danmu_api/ui/css/components.css.js index 26dd8ee..7beadfd 100644 --- a/danmu_api/ui/css/components.css.js +++ b/danmu_api/ui/css/components.css.js @@ -24,7 +24,8 @@ export const componentsCssContent = /* css */ ` .nav-btn.active { background: white; - color: #667eea; + color: #1a2980; + font-weight: bold; } /* 环境变量样式 */ diff --git a/danmu_api/ui/js/apitest.js b/danmu_api/ui/js/apitest.js index 193a294..072f70b 100644 --- a/danmu_api/ui/js/apitest.js +++ b/danmu_api/ui/js/apitest.js @@ -192,6 +192,16 @@ function testApi() { if (config.method === 'GET') { const queryString = new URLSearchParams(params).toString(); url = url + '?' + queryString; + } else if (config.method === 'POST' && apiKey === 'getSegmentComment') { + // 对于 getSegmentComment 接口,需要将 format 参数添加到 URL 查询参数中 + const queryParams = {}; + if (params.format) { + queryParams.format = params.format; + } + if (Object.keys(queryParams).length > 0) { + const queryString = new URLSearchParams(queryParams).toString(); + url = url + '?' + queryString; + } } } diff --git a/danmu_api/ui/js/main.js b/danmu_api/ui/js/main.js index 81ea8a3..da3e10d 100644 --- a/danmu_api/ui/js/main.js +++ b/danmu_api/ui/js/main.js @@ -118,12 +118,16 @@ let originalToken = ''; // 构建带token的API请求路径 function buildApiUrl(path, isSystemPath = false) { - // 如果是系统管理路径且有admin token,则使用admin token + let res; + let _reverseProxy = ""; + // 如果是系统管理路径且有admin token,则使用admin token if (isSystemPath && currentAdminToken && currentAdminToken.trim() !== '' && currentAdminToken.trim() !== '*'.repeat(currentAdminToken.length)) { - return '/' + currentAdminToken + path; + res = '/' + currentAdminToken + path; + } else { + // 否则使用普通token + res = (currentToken ? '/' + currentToken : "") + path; } - // 否则使用普通token - return (currentToken ? '/' + currentToken : "") + path; + return _reverseProxy + res; } // 从API加载真实环境变量数据 @@ -180,6 +184,8 @@ function updateApiEndpoint() { return fetch(buildApiUrl('/api/config', true)) .then(response => response.json()) .then(config => { + let _reverseProxy = ""; + // 获取当前页面的协议、主机和端口 const protocol = window.location.protocol; const host = window.location.host; @@ -187,7 +193,7 @@ function updateApiEndpoint() { const adminToken = config.originalEnvVars?.ADMIN_TOKEN; // 获取URL路径并提取token - const urlPath = window.location.pathname; + const urlPath = window.location.pathname.replace(_reverseProxy, ""); const pathParts = urlPath.split('/').filter(part => part !== ''); const urlToken = pathParts.length > 0 ? pathParts[0] : ''; let apiToken = '********'; @@ -204,7 +210,7 @@ function updateApiEndpoint() { } // 构造API端点URL - const apiEndpoint = protocol + '//' + host + '/' + apiToken; + const apiEndpoint = protocol + '//' + host + _reverseProxy + '/' + apiToken; const apiEndpointElement = document.getElementById('api-endpoint'); if (apiEndpointElement) { apiEndpointElement.textContent = apiEndpoint; @@ -216,7 +222,7 @@ function updateApiEndpoint() { // 出错时显示默认值 const protocol = window.location.protocol; const host = window.location.host; - const apiEndpoint = protocol + '//' + host + '/********'; + const apiEndpoint = protocol + '//' + host + _reverseProxy + '/********'; const apiEndpointElement = document.getElementById('api-endpoint'); if (apiEndpointElement) { apiEndpointElement.textContent = apiEndpoint; @@ -250,11 +256,13 @@ function getDockerVersion() { } // 切换导航 -function switchSection(section) { +function switchSection(section, event = null) { // 检查是否尝试访问受token保护的section(日志查看、接口调试、系统配置需要token访问) if (section === 'logs' || section === 'api' || section === 'env' || section === 'push') { + let _reverseProxy = ""; + // 获取URL路径并提取token - const urlPath = window.location.pathname; + const urlPath = window.location.pathname.replace(_reverseProxy, ""); const pathParts = urlPath.split('/').filter(part => part !== ''); const urlToken = pathParts.length > 0 ? pathParts[0] : ''; @@ -285,7 +293,10 @@ function switchSection(section) { document.querySelectorAll('.nav-btn').forEach(b => b.classList.remove('active')); document.getElementById(\`\${section}-section\`).classList.add('active'); - event.target.classList.add('active'); + // 在异步回调中使用传入的event参数来设置按钮的active状态 + if (event && event.target) { + event.target.classList.add('active'); + } addLog(\`切换到\${section === 'env' ? '环境变量' : section === 'preview' ? '配置预览' : section === 'logs' ? '日志查看' : section === 'push' ? '推送弹幕' : '接口调试'}模块\`, 'info'); } @@ -296,7 +307,9 @@ function switchSection(section) { document.querySelectorAll('.nav-btn').forEach(b => b.classList.remove('active')); document.getElementById(\`\${section}-section\`).classList.add('active'); - event.target.classList.add('active'); + if (event && event.target) { + event.target.classList.add('active'); + } addLog(\`切换到\${section === 'env' ? '环境变量' : section === 'preview' ? '配置预览' : section === 'logs' ? '日志查看' : section === 'push' ? '推送弹幕' : '接口调试'}模块\`, 'info'); @@ -313,17 +326,21 @@ function switchSection(section) { document.querySelectorAll('.nav-btn').forEach(b => b.classList.remove('active')); document.getElementById(\`\${section}-section\`).classList.add('active'); - event.target.classList.add('active'); + if (event && event.target) { + event.target.classList.add('active'); + } addLog(\`切换到\${section === 'env' ? '环境变量' : section === 'preview' ? '配置预览' : section === 'logs' ? '日志查看' : section === 'push' ? '推送弹幕' : '接口调试'}模块\`, 'info'); } } // 切换类别 -function switchCategory(category) { +function switchCategory(category, event = null) { currentCategory = category; document.querySelectorAll('.category-btn').forEach(b => b.classList.remove('active')); - event.target.classList.add('active'); + if (event && event.target) { + event.target.classList.add('active'); + } renderEnvList(); } diff --git a/danmu_api/ui/js/preview.js b/danmu_api/ui/js/preview.js index a37a892..4250597 100644 --- a/danmu_api/ui/js/preview.js +++ b/danmu_api/ui/js/preview.js @@ -5,7 +5,7 @@ function renderPreview() { const preview = document.getElementById('preview-area'); // 从API获取真实配置数据 - fetch('/api/config') + fetch(buildApiUrl('/api/config')) .then(response => response.json()) .then(config => { // 使用从API获取的分类环境变量 diff --git a/danmu_api/ui/js/systemsettings.js b/danmu_api/ui/js/systemsettings.js index 8584f9a..bb1342b 100644 --- a/danmu_api/ui/js/systemsettings.js +++ b/danmu_api/ui/js/systemsettings.js @@ -255,8 +255,10 @@ function confirmDeploymentByLogs() { // 检查URL中的token是否与currentAdminToken匹配 function checkAdminToken() { + let _reverseProxy = ""; + // 获取URL路径并提取token - const urlPath = window.location.pathname; + const urlPath = window.location.pathname.replace(_reverseProxy, ""); const pathParts = urlPath.split('/').filter(part => part !== ''); const urlToken = pathParts.length > 0 ? pathParts[0] : currentToken; // 如果没有路径段,使用默认token diff --git a/danmu_api/ui/template.js b/danmu_api/ui/template.js index 32453a8..29b0050 100644 --- a/danmu_api/ui/template.js +++ b/danmu_api/ui/template.js @@ -49,11 +49,11 @@ export const HTML_TEMPLATE = /* html */ `
@@ -200,12 +200,12 @@ export const HTML_TEMPLATE = /* html */ `