From 4e07d2daa46168346042ec63202e5ffc19482326 Mon Sep 17 00:00:00 2001 From: huangxd- <649924757@qq.com> Date: Fri, 31 Oct 2025 00:12:38 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=96=B0=E5=A2=9E=E8=B1=86=E7=93=A3?= =?UTF-8?q?=E6=BA=90=E6=94=AF=E6=8C=81=20(#81)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 单独提取出REMEMBER_LAST_SELECT环境变量:是否记住手动选择结果,用于match自动匹配时优选上次的选择,默认为`true`,表示记住 * 再增加部分数据模型校验 * 新增豆瓣源支持 --- .env.example | 11 ++- README.md | 7 +- danmu_api/apis/dandan-api.js | 15 +++- danmu_api/configs/envs.js | 6 +- danmu_api/configs/globals.js | 3 +- danmu_api/models/dandan-model.js | 42 +++++++++- danmu_api/sources/bahamut.js | 2 +- danmu_api/sources/base.js | 6 ++ danmu_api/sources/douban.js | 134 +++++++++++++++++++++++++++++++ danmu_api/sources/hanjutv.js | 2 +- danmu_api/sources/iqiyi.js | 4 +- danmu_api/sources/kan360.js | 4 +- danmu_api/sources/renren.js | 2 +- danmu_api/sources/tencent.js | 2 +- danmu_api/sources/vod.js | 4 +- danmu_api/sources/youku.js | 2 +- danmu_api/utils/common-util.js | 4 +- 17 files changed, 222 insertions(+), 28 deletions(-) create mode 100644 danmu_api/sources/douban.js diff --git a/.env.example b/.env.example index 9754246..5b43d86 100644 --- a/.env.example +++ b/.env.example @@ -54,10 +54,11 @@ YOUKU_CONCURRENCY=8 # ==================== 源排序配置 ==================== -# 数据源查询顺序(可选值:360, vod, tencent, youku, iqiyi, imgo, bilibili, renren, hanjutv, bahamut) +# 数据源查询顺序(可选值:360, vod, douban, tencent, youku, iqiyi, imgo, bilibili, renren, hanjutv, bahamut) # 默认值: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 +# 如需使用豆瓣源,请手动添加:SOURCE_ORDER=360,vod,douban,renren,hanjutv SOURCE_ORDER=360,vod,renren,hanjutv # ==================== 平台排序配置 ==================== @@ -196,10 +197,16 @@ SEARCH_CACHE_MINUTES=1 # 可根据需要调整 COMMENT_CACHE_MINUTES=1 +# 是否记住手动选择结果 +# 默认值:true +# 说明:是否记住手动选择结果,用于match自动匹配时优选上次的选择 +# 用于存储查询关键字上次选择的 animeId,下次 match 自动匹配时优先选择该 anime +REMEMBER_LAST_SELECT=true + # 用户偏好记忆缓存大小限制 # 默认值:100 # 说明:lastSelectMap 最多保存的条目数,超过限制时删除最早的条目(FIFO) -# 用于存储查询关键字上次选择的 animeId,下次 match 自动匹配时优先选择该 anime +# 用于存储查询关键字上次选择的 animeId,下次 match 自动匹配时优先选择该 anime MAX_LAST_SELECT_MAP=100 # ==================== Redis 配置(Upstash)==================== diff --git a/README.md b/README.md index 559eda6..0b8363b 100644 --- a/README.md +++ b/README.md @@ -311,7 +311,7 @@ API 支持返回 Bilibili 标准 XML 格式的弹幕数据,通过查询参数 | 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,tencent,youku,iqiyi,imgo,bilibili,renren,hanjutv,bahamut` | +| SOURCE_ORDER | 【可选】源排序,用于按源对返回资源的排序(注意:先后顺序会影响自动匹配最终的返回),默认是`360,vod,renren,hanjutv`,表示360数据排在最前,hanjutv数据排在最后,示例:`360,renren`:只返回360数据和renren数据,且360数据靠前;当前可选择的源字段有 `360,vod,douban,tencent,youku,iqiyi,imgo,bilibili,renren,hanjutv,bahamut` | | PLATFORM_ORDER | 【可选】自动匹配优选平台,按顺序优先返回指定平台弹幕,默认为空,即返回第一个满足条件的平台,示例:`bilibili1,qq`,表示如果有b站的播放源,则优先返回b站的弹幕,否则就返回腾讯的弹幕,两者都没有,则返回第一个满足条件的平台;当前可选择的平台字段有 `qiyi, bilibili1, imgo, youku, qq, renren, hanjutv, bahamut` | | EPISODE_TITLE_FILTER | 【可选】剧集标题正则过滤,按正则关键字对剧集或综艺的集标题进行过滤,适用于过滤一些预告或综艺非正式集,只支持match自动匹配,默认值如下 | | ENABLE_EPISODE_FILTER | 【可选】是否在手动选择接口中启用集标题过滤,默认为`false`(禁用),启用后 GET /api/v2/bangumi/{id} 和 GET /api/v2/search/anime 接口会过滤掉预告、花絮等特殊集,以及名称包含特殊关键词的动漫。 | @@ -328,6 +328,7 @@ API 支持返回 Bilibili 标准 XML 格式的弹幕数据,通过查询参数 | LOG_LEVEL | 【可选】日志级别,默认为`info`,可选值:`error`(仅错误)、`warn`(错误和警告)、`info`(所有日志),生产环境建议使用`warn`,调试时使用`info` | | SEARCH_CACHE_MINUTES | 【可选】搜索结果缓存时间(分钟),默认为`1`,避免短期内重复的不必要API请求,同时保证获取最新的结果列表,可根据需要调整:Vercel/Cloudflare建议`1-5`分钟,Docker可设置`5-30`分钟,设置为`0`表示不缓存 | | COMMENT_CACHE_MINUTES | 【可选】弹幕缓存时间(分钟),默认为`1`,弹幕数据的缓存时间,独立于搜索结果缓存 | +| REMEMBER_LAST_SELECT | 【可选】是否记住手动选择结果,用于match自动匹配时优选上次的选择,默认为`true`,表示记住,请注意,该功能为实验性功能,会记住某个剧上次选择的结果作为下次自动匹配的优选,如不需要,请关闭 | | MAX_LAST_SELECT_MAP | 【可选】最后选择映射缓存大小限制,默认为`100`,lastSelectMap最多保存的条目数,超过限制时删除最早的条目(FIFO),用于存储查询关键字上次选择的animeId | | UPSTASH_REDIS_REST_URL | 【可选】Upstash redis url,需配合UPSTASH_REDIS_REST_TOKEN使用,用于持久化存储,不会因为冷启动而丢失过去的查询信息(在cf/eo/claw上配置后应该能更稳定点,也能解决小幻掉匹配的问题,但会稍微影响请求速度),获取方法请参考:`https://cloud.tencent.cn/developer/article/2424508` | | UPSTASH_REDIS_REST_TOKEN | 【可选】Upstash redis token,需配合UPSTASH_REDIS_REST_URL使用,用于持久化存储,不会因为冷启动而丢失过去的查询信息(在cf/eo/claw上配置后应该能更稳定点,也能解决小幻掉匹配的问题,但会稍微影响请求速度),获取方法请参考:`https://cloud.tencent.cn/developer/article/2424508` | @@ -368,6 +369,7 @@ API 支持返回 Bilibili 标准 XML 格式的弹幕数据,通过查询参数 | ----------- | ----------- | | 360 | qiyi, bilibili1, imgo, youku, qq | | vod | qiyi, bilibili1, imgo, youku, qq | +| douban | qiyi, bilibili1, youku, qq | | tencent | qq | | youku | youku | | iqiyi | qiyi | @@ -390,10 +392,13 @@ danmu_api/ │ └── configs/ │ ├── envs.js # 环境变量处理脚本 │ └── globals.js # 全局变量处理脚本 +│ └── models/ +│ └── dandan-model.js # 弹弹play数据模型 │ └── sources/ │ ├── bahamut.js # 巴哈姆特源 │ ├── base.js # 弹幕源获取基类 │ ├── bilibili.js # b站源 +│ ├── douban.js # 豆瓣源 │ ├── hanjutv.js # 韩剧TV源 │ ├── iqiyi.js # 爱奇艺源 │ ├── kan360.js # 360看源 diff --git a/danmu_api/apis/dandan-api.js b/danmu_api/apis/dandan-api.js index 98a49bb..9bc8738 100644 --- a/danmu_api/apis/dandan-api.js +++ b/danmu_api/apis/dandan-api.js @@ -10,6 +10,7 @@ import { formatDanmuResponse } from "../utils/danmu-util.js"; import { extractTitle, convertChineseNumber, parseFileName, createDynamicPlatformOrder } from "../utils/common-util.js"; import Kan360Source from "../sources/kan360.js"; import VodSource from "../sources/vod.js"; +import DoubanSource from "../sources/douban.js"; import RenrenSource from "../sources/renren.js"; import HanjutvSource from "../sources/hanjutv.js"; import BahamutSource from "../sources/bahamut.js"; @@ -36,6 +37,7 @@ const iqiyiSource = new IqiyiSource(); const mangoSource = new MangoSource(); const bilibiliSource = new BilibiliSource(); const otherSource = new OtherSource(); +const doubanSource = new DoubanSource(tencentSource, iqiyiSource, youkuSource, bilibiliSource); function matchSeason(anime, queryTitle, season) { if (anime.animeTitle.includes(queryTitle)) { @@ -139,6 +141,7 @@ export async function searchAnime(url) { const requestPromises = globals.sourceOrderArr.map(source => { if (source === "360") return kan360Source.search(queryTitle); if (source === "vod") return vodSource.search(queryTitle); + if (source === "douban") return doubanSource.search(queryTitle); if (source === "renren") return renrenSource.search(queryTitle); if (source === "hanjutv") return hanjutvSource.search(queryTitle); if (source === "bahamut") return bahamutSource.search(queryTitle); @@ -162,8 +165,9 @@ export async function searchAnime(url) { // 解构出返回的结果 const { - vod: animesVodResults, 360: animes360, renren: animesRenren, hanjutv: animesHanjutv, bahamut: animesBahamut, - tencent: animesTencent, youku: animesYouku, iqiyi: animesIqiyi, imgo: animesImgo, bilibili: animesBilibili + vod: animesVodResults, 360: animes360, douban: animesDouban, renren: animesRenren, hanjutv: animesHanjutv, + bahamut: animesBahamut, tencent: animesTencent, youku: animesYouku, iqiyi: animesIqiyi, imgo: animesImgo, + bilibili: animesBilibili } = resultData; // 按顺序处理每个来源的结果 @@ -180,6 +184,9 @@ export async function searchAnime(url) { } } } + } else if (key === 'douban') { + // 等待处理Douban来源 + await doubanSource.handleAnimes(animesDouban, queryTitle, curAnimes); } else if (key === 'renren') { // 等待处理Renren来源 await renrenSource.handleAnimes(animesRenren, queryTitle, curAnimes); @@ -283,7 +290,7 @@ async function matchAniAndEp(season, episode, searchData, title, req, platform, if (season && episode) { // 判断剧集 for (const anime of searchData.animes) { - if (preferAnimeId && anime.bangumiId.toString() !== preferAnimeId.toString()) continue; + if (globals.rememberLastSelect && preferAnimeId && anime.bangumiId.toString() !== preferAnimeId.toString()) continue; if (anime.animeTitle.includes(title)) { let originBangumiUrl = new URL(req.url.replace("/match", `bangumi/${anime.bangumiId}`)); const bangumiRes = await getBangumi(originBangumiUrl.pathname); @@ -325,7 +332,7 @@ async function matchAniAndEp(season, episode, searchData, title, req, platform, } else { // 判断电影 for (const anime of searchData.animes) { - if (preferAnimeId && anime.bangumiId.toString() !== preferAnimeId.toString()) continue; + if (globals.rememberLastSelect && preferAnimeId && anime.bangumiId.toString() !== preferAnimeId.toString()) continue; const animeTitle = anime.animeTitle.split("(")[0].trim(); if (animeTitle === title) { let originBangumiUrl = new URL(req.url.replace("/match", `bangumi/${anime.bangumiId}`)); diff --git a/danmu_api/configs/envs.js b/danmu_api/configs/envs.js index c84a2e9..a9349ef 100644 --- a/danmu_api/configs/envs.js +++ b/danmu_api/configs/envs.js @@ -10,7 +10,7 @@ export class Envs { static VOD_ALLOWED_PLATFORMS = ['qiyi', 'bilibili1', 'imgo', 'youku', 'qq']; // vod允许的播放平台 static ALLOWED_PLATFORMS = ['qiyi', 'bilibili1', 'imgo', 'youku', 'qq', 'renren', 'hanjutv', 'bahamut']; // 全部源允许的播放平台 - static ALLOWED_SOURCES = ['360', 'vod', 'tencent', 'youku', 'iqiyi', 'imgo', 'bilibili', 'renren', 'hanjutv', 'bahamut']; // 允许的源 + static ALLOWED_SOURCES = ['360', 'vod', 'douban', 'tencent', 'youku', 'iqiyi', 'imgo', 'bilibili', 'renren', 'hanjutv', 'bahamut']; // 允许的源 /** * 获取环境变量 @@ -207,7 +207,9 @@ export class Envs { convertTopBottomToScroll: this.get('CONVERT_TOP_BOTTOM_TO_SCROLL', false, 'boolean'), // 顶部/底部弹幕转换为浮动弹幕配置(默认 false,禁用转换) convertColorToWhite: this.get('CONVERT_COLOR_TO_WHITE', false, 'boolean'), // 彩色弹幕转换为纯白弹幕配置(默认 false,禁用转换) danmuOutputFormat: this.get('DANMU_OUTPUT_FORMAT', 'json', 'string'), // 弹幕输出格式配置(默认 json,可选值:json, xml) - strictTitleMatch: this.get('STRICT_TITLE_MATCH', false, 'boolean') // 严格标题匹配模式配置(默认 false,宽松模糊匹配) + strictTitleMatch: this.get('STRICT_TITLE_MATCH', false, 'boolean'), // 严格标题匹配模式配置(默认 false,宽松模糊匹配) + rememberLastSelect: this.get('REMEMBER_LAST_SELECT', true, 'boolean'), // 是否记住手动选择结果,用于match自动匹配时优选上次的选择(默认 true,记住) + MAX_LAST_SELECT_MAP: this.get('MAX_LAST_SELECT_MAP', 100, 'number'), // 记住上次选择映射缓存大小限制(默认 100) }; } } diff --git a/danmu_api/configs/globals.js b/danmu_api/configs/globals.js index 7627df3..bee1b2b 100644 --- a/danmu_api/configs/globals.js +++ b/danmu_api/configs/globals.js @@ -11,10 +11,9 @@ export const Globals = { accessedEnvVars: {}, // 静态常量 - VERSION: '1.6.3', + VERSION: '1.6.4', MAX_LOGS: 500, // 日志存储,最多保存 500 行 MAX_ANIMES: 100, - MAX_LAST_SELECT_MAP: 100, // 运行时状态 animes: [], diff --git a/danmu_api/models/dandan-model.js b/danmu_api/models/dandan-model.js index 880495f..f7f8164 100644 --- a/danmu_api/models/dandan-model.js +++ b/danmu_api/models/dandan-model.js @@ -20,17 +20,51 @@ export class Anime { validateType(isFavorited, "boolean"); validateType(links, "array"); + // 将 links 转换为 Link 实例数组 + this.links = links.map(linkData => Link.fromJson(linkData)); + // 直接解构并赋值给 this Object.assign(this, { animeId, bangumiId, animeTitle, type, typeDescription, imageUrl, startDate, - episodeCount, rating, isFavorited, links }); + episodeCount, rating, isFavorited }); } - // ---- 静态方法:从 JSON 创建 User 对象 ---- + // ---- 静态方法:从 JSON 创建 Anime 对象 ---- static fromJson(json) { if (typeof json !== "object" || json === null) { throw new TypeError("fromJson 参数必须是对象"); } - return new Anime(json); + + const links = json.links.map(link => Link.fromJson(link)); + return new Anime({ ...json, links }); + } + + // ---- 转换为纯 JSON ---- + toJson() { + return { + ...this, // 将 this 中的其他属性直接展开 + links: this.links.map(link => link.toJson()) // 转换每个 link 为 JSON + }; + } +} + +// 定义 Link 模型 +class Link { + constructor({ name = "", url = "", title = "", id = 10001 } = {}) { + validateType(name, "string"); + validateType(url, "string"); + validateType(title, "string"); + validateType(id, "number"); + + // 直接解构并赋值给 this + Object.assign(this, { name, url, title, id }); + } + + // ---- 静态方法:从 JSON 创建 Link 对象 ---- + static fromJson(json) { + if (typeof json !== "object" || json === null) { + throw new TypeError("fromJson 参数必须是对象"); + } + return new Link(json); } // ---- 转换为纯 JSON ---- @@ -161,7 +195,7 @@ export class Season { // 数据模型:BangumiEpisode // ===================== export class BangumiEpisode { - constructor({ seasonId = "", episodeId = "", episodeTitle = "", episodeNumber = "", + constructor({ seasonId = "", episodeId = 10001, episodeTitle = "", episodeNumber = "", airDate = "" } = {}) { validateType(seasonId, "string"); validateType(episodeId, "number"); diff --git a/danmu_api/sources/bahamut.js b/danmu_api/sources/bahamut.js index 4a40c0a..e8eb685 100644 --- a/danmu_api/sources/bahamut.js +++ b/danmu_api/sources/bahamut.js @@ -312,7 +312,7 @@ export default class BahamutSource extends BaseSource { for (const ep of eps) { const epTitle = `第${ep.episode}集`; links.push({ - "name": ep.episode, + "name": ep.episode.toString(), "url": ep.videoSn.toString(), "title": `【bahamut】 ${epTitle}` }); diff --git a/danmu_api/sources/base.js b/danmu_api/sources/base.js index b89de9b..bb47d98 100644 --- a/danmu_api/sources/base.js +++ b/danmu_api/sources/base.js @@ -11,26 +11,32 @@ export default class BaseSource { // 构造函数,初始化通用配置 } + // 搜索关键字 async search(keyword) { throw new Error("Method 'search' must be implemented"); } + // 获取剧集详情 async getEpisodes(id) { throw new Error("Method 'Episodes' must be implemented"); } + // 处理animes结果,用数据模型Anime存储 async handleAnimes(sourceAnimes, queryTitle, curAnimes, vodName) { throw new Error("Method 'handleAnimes' must be implemented"); } + // 获取某集的弹幕 async getEpisodeDanmu(id) { throw new Error("Method 'getEpisodeDanmu' must be implemented"); } + // 格式化弹幕 formatComments(comments) { throw new Error("Method 'formatComments' must be implemented"); } + // 获取弹幕流水线方法(获取某集弹幕 -> 格式化弹幕 -> 弹幕处理,如去重/屏蔽字等) async getComments(id, sourceName, progressCallback=null) { if(progressCallback) await progressCallback(5, `开始获取弹幕${sourceName}弹幕`); log("info", `开始获取弹幕${sourceName}弹幕`); diff --git a/danmu_api/sources/douban.js b/danmu_api/sources/douban.js new file mode 100644 index 0000000..9d58031 --- /dev/null +++ b/danmu_api/sources/douban.js @@ -0,0 +1,134 @@ +import BaseSource from './base.js'; +import { log } from "../utils/log-util.js"; +import { httpGet } from "../utils/http-util.js"; + +// ===================== +// 获取豆瓣源播放链接 +// ===================== +export default class DoubanSource extends BaseSource { + constructor(tencentSource, iqiyiSource, youkuSource, bilibiliSource) { + super('BaseSource'); + this.tencentSource = tencentSource; + this.iqiyiSource = iqiyiSource; + this.youkuSource = youkuSource; + this.bilibiliSource = bilibiliSource; + } + + async search(keyword) { + try { + const response = await httpGet( + `https://m.douban.com/rexxar/api/v2/search?q=${keyword}&start=0&count=20&type=movie`, + { + headers: { + "Referer": "https://m.douban.com/movie/", + "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", + }, + } + ); + + const data = response.data; + + let tmpAnimes = []; + if (data?.subjects?.items?.length > 0) { + tmpAnimes = [...tmpAnimes, ...data.subjects.items]; + } + + if (data?.smart_box?.length > 0) { + tmpAnimes = [...tmpAnimes, ...data.smart_box]; + } + + log("info", `douban animes.length: ${tmpAnimes.length}`); + + return tmpAnimes; + } catch (error) { + log("error", "getDoubanAnimes error:", { + message: error.message, + name: error.name, + stack: error.stack, + }); + return []; + } + } + + async getEpisodes(id) {} + + async handleAnimes(sourceAnimes, queryTitle, curAnimes, vodName) { + const processDoubanAnimes = await Promise.all(sourceAnimes.map(async (anime) => { + const doubanId = anime.target_id; + log("info", "doubanId: ", doubanId); + + // 获取平台详情页面url + const response = await httpGet( + `https://m.douban.com/rexxar/api/v2/movie/${doubanId}?for_mobile=1`, + { + headers: { + "Referer": `https://m.douban.com/movie/subject/${doubanId}/?dt_dapp=1`, + "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", + }, + } + ); + + const results = []; + + for (const vendor of response.data?.vendors ?? []) { + if (!vendor) { + continue; + } + log("info", "vendor uri: ", vendor.uri); + const tmpAnimes = [{ + title: response.data?.title, + year: response.data?.year, + type: anime?.type_name, + imageUrl: anime?.target?.cover_url, + }]; + switch (vendor.id) { + case "qq": { + const cid = new URL(vendor.uri).searchParams.get('cid'); + if (cid) { + tmpAnimes[0].provider = "tencent"; + tmpAnimes[0].mediaId = cid; + await this.tencentSource.handleAnimes(tmpAnimes, queryTitle, curAnimes) + } + break; + } + case "iqiyi": { + const tvid = new URL(vendor.uri).searchParams.get('tvid'); + if (tvid) { + tmpAnimes[0].provider = "iqiyi"; + tmpAnimes[0].mediaId = anime?.type_name === '电影' ? `movie_${tvid}` : tvid; + await this.iqiyiSource.handleAnimes(tmpAnimes, queryTitle, curAnimes) + } + break; + } + case "youku": { + const showId = new URL(vendor.uri).searchParams.get('showid'); + if (showId) { + tmpAnimes[0].provider = "youku"; + tmpAnimes[0].mediaId = showId; + await this.youkuSource.handleAnimes(tmpAnimes, queryTitle, curAnimes) + } + break; + } + case "bilibili": { + const seasonId = new URL(vendor.uri).pathname.split('/').pop(); + if (seasonId) { + tmpAnimes[0].provider = "bilibili"; + tmpAnimes[0].mediaId = `ss${seasonId}`; + await this.bilibiliSource.handleAnimes(tmpAnimes, queryTitle, curAnimes) + } + break; + } + } + } + return results; + })); + + return processDoubanAnimes; + } + + async getEpisodeDanmu(id) {} + + formatComments(comments) {} +} \ No newline at end of file diff --git a/danmu_api/sources/hanjutv.js b/danmu_api/sources/hanjutv.js index 8b01dfb..42df57f 100644 --- a/danmu_api/sources/hanjutv.js +++ b/danmu_api/sources/hanjutv.js @@ -145,7 +145,7 @@ export default class HanjutvSource extends BaseSource { for (const ep of eps) { const epTitle = ep.title && ep.title.trim() !== "" ? `第${ep.serialNo}集:${ep.title}` : `第${ep.serialNo}集`; links.push({ - "name": ep.title, + "name": ep.title.toString(), "url": ep.pid, "title": `【hanjutv】 ${epTitle}` }); diff --git a/danmu_api/sources/iqiyi.js b/danmu_api/sources/iqiyi.js index a9040d5..588e218 100644 --- a/danmu_api/sources/iqiyi.js +++ b/danmu_api/sources/iqiyi.js @@ -264,7 +264,7 @@ export default class IqiyiSource extends BaseSource { } // 第一步:将 video_id 转换为 entity_id - const entityId = this._videoIdToEntityId(id); + const entityId = /^\d+$/.test(id) ? id : this._videoIdToEntityId(id); if (!entityId) { log("error", `[iQiyi] 无法将 media_id '${id}' 转换为 entity_id`); return []; @@ -641,7 +641,7 @@ export default class IqiyiSource extends BaseSource { for (const ep of eps) { const fullUrl = ep.link || `https://www.iqiyi.com/v_${anime.mediaId}.html`; links.push({ - "name": ep.order, + "name": ep.order.toString(), "url": fullUrl, "title": `【iqiyi】 ${ep.title}` }); diff --git a/danmu_api/sources/kan360.js b/danmu_api/sources/kan360.js index 6d596fe..881fbea 100644 --- a/danmu_api/sources/kan360.js +++ b/danmu_api/sources/kan360.js @@ -116,7 +116,7 @@ export default class Kan360Source extends BaseSource { for (const key of Object.keys(anime.playlinks)) { if (globals.vodAllowedPlatforms.includes(key)) { links.push({ - "name": key, + "name": key.toString(), "url": anime.playlinks[key], "title": `【${key}】 ${anime.titleTxt}(${anime.year})` }); @@ -127,7 +127,7 @@ export default class Kan360Source extends BaseSource { for (let i = 0; i < anime.seriesPlaylinks.length; i++) { const item = anime.seriesPlaylinks[i]; links.push({ - "name": i + 1, + "name": (i + 1).toString(), "url": item.url, "title": `【${anime.seriesSite}】 第${i + 1}集` }); diff --git a/danmu_api/sources/renren.js b/danmu_api/sources/renren.js index fc0086e..7baa7ca 100644 --- a/danmu_api/sources/renren.js +++ b/danmu_api/sources/renren.js @@ -354,7 +354,7 @@ export default class RenrenSource extends BaseSource { let links = []; for (const ep of eps) { links.push({ - "name": ep.episodeIndex, + "name": ep.episodeIndex.toString(), "url": ep.episodeId, "title": `【${ep.provider}】 ${ep.title}` }); diff --git a/danmu_api/sources/tencent.js b/danmu_api/sources/tencent.js index 5a0aa1b..a491a99 100644 --- a/danmu_api/sources/tencent.js +++ b/danmu_api/sources/tencent.js @@ -353,7 +353,7 @@ export default class TencentSource extends BaseSource { // 构建完整URL: https://v.qq.com/x/cover/{cid}/{vid}.html const fullUrl = `https://v.qq.com/x/cover/${anime.mediaId}/${ep.vid}.html`; links.push({ - "name": i + 1, + "name": (i + 1).toString(), "url": fullUrl, "title": `【qq】 ${epTitle}` }); diff --git a/danmu_api/sources/vod.js b/danmu_api/sources/vod.js index c7165ac..06b4753 100644 --- a/danmu_api/sources/vod.js +++ b/danmu_api/sources/vod.js @@ -94,7 +94,7 @@ export default class VodSource extends BaseSource { } async search(keyword) { - if (!globals.vodServers || globals.vodServers.length === 0) { + if (!globals.vodServers || globals.vodServers.length === 0) { return []; } @@ -135,7 +135,7 @@ export default class VodSource extends BaseSource { const epInfo = ep.split("$"); count++; links.push({ - "name": count, + "name": count.toString(), "url": epInfo[1], "title": `【${platform}】 ${epInfo[0]}` }); diff --git a/danmu_api/sources/youku.js b/danmu_api/sources/youku.js index e874bbc..b2628c3 100644 --- a/danmu_api/sources/youku.js +++ b/danmu_api/sources/youku.js @@ -222,7 +222,7 @@ export default class YoukuSource extends BaseSource { for (const ep of formattedEps) { const fullUrl = ep.link || `https://v.youku.com/v_show/id_${ep.vid}.html`; links.push({ - "name": ep.episodeIndex, + "name": ep.episodeIndex.toString(), "url": fullUrl, "title": `【youku】 ${ep.title}` }); diff --git a/danmu_api/utils/common-util.js b/danmu_api/utils/common-util.js index c482a84..f7d52ee 100644 --- a/danmu_api/utils/common-util.js +++ b/danmu_api/utils/common-util.js @@ -237,9 +237,9 @@ export function validateType(value, expectedType) { const fieldName = value?.constructor?.name; // 获取字段名 if (expectedType === "array") { if (!Array.isArray(value)) { - throw new TypeError(`${fieldName} 必须是一个数组`); + throw new TypeError(`${value} 必须是一个数组,但传入的是 ${fieldName}`); } } else if (typeof value !== expectedType) { - throw new TypeError(`${fieldName} 必须是 ${expectedType}`); + throw new TypeError(`${value} 必须是 ${expectedType},但传入的是 ${fieldName}`); } } \ No newline at end of file