feat: 新增豆瓣源支持 (#81)

* 单独提取出REMEMBER_LAST_SELECT环境变量:是否记住手动选择结果,用于match自动匹配时优选上次的选择,默认为`true`,表示记住

* 再增加部分数据模型校验

* 新增豆瓣源支持
This commit is contained in:
huangxd- authored and GitHub committed 2025-10-31 00:12:38 +08:00
1 parent b7afcf08dd
commit 4e07d2daa4
17 files changed
+222 -28

No files matched your search

+9 -2
View File
@@ -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)====================
+6 -1
View File
@@ -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看源
+11 -4
View File
@@ -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}`));
+4 -2
View File
@@ -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)
};
}
}
+1 -2
View File
@@ -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: [],
+38 -4
View File
@@ -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");
+1 -1
View File
@@ -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}`
});
+6
View File
@@ -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}弹幕`);
+134
View File
@@ -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) {}
}
+1 -1
View File
@@ -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}`
});
+2 -2
View File
@@ -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}`
});
+2 -2
View File
@@ -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}集`
});
+1 -1
View File
@@ -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}`
});
+1 -1
View File
@@ -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}`
});
+2 -2
View File
@@ -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]}`
});
+1 -1
View File
@@ -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}`
});
+2 -2
View File
@@ -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}`);
}
}