diff --git a/README.md b/README.md index 41ab91b..aac14e8 100644 --- a/README.md +++ b/README.md @@ -370,9 +370,9 @@ API 支持返回 Bilibili 标准 XML 格式的弹幕数据,通过查询参数 | VOD_REQUEST_TIMEOUT | 【可选】VOD服务器单个请求超时时间(毫秒),防止慢速或失效的采集站阻塞搜索,默认为`10000`(10秒),建议值:`5000-15000`。由于`fastest`模式只返回最快响应的站点,可以设置较大的超时时间给慢速站点更多机会 | | BILIBILI_COOKIE | 【可选】b站cookie(填入后能抓取完整弹幕和启用港澳台App接口),如 `buvid3=E2BCA ... eao6; theme-avatar-tip-show=SHOWED`,请自行通过浏览器或抓包工具抓取,热心网友测试后,弹幕获取实际最少只需取 `SESSDATA=xxxx` 字段,但如果需要使用港澳台区域稳定的App搜索接口还需要`bili_jct=xxxx`或`access_key=xxxx` 字段,不知道怎么获取cookie的,可以从工具 [cookie-butler](https://cookie-butler.do-u.me) 获取 | | 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,sohu,renren,hanjutv,bahamut,dandan,animeko,custom` | -| PLATFORM_ORDER | 【可选】自动匹配优选平台,按顺序优先返回指定平台弹幕,默认为空,即返回第一个满足条件的平台,示例:`bilibili1,qq`,表示如果有b站的播放源,则优先返回b站的弹幕,否则就返回腾讯的弹幕,两者都没有,则返回第一个满足条件的平台;当前可选择的平台字段有 `qiyi, bilibili1, imgo, youku, qq, sohu, renren, hanjutv, bahamut, dandan, animeko, custom` | -| MERGE_SOURCE_PAIRS | 【可选】源合并配置,配置后将对应源合并同时一起获取弹幕返回,默认为空,格式是`源字段&源字段`,示例:`imgo&iqiyi,dandan&animeko,dandan&bahamut`, 允许多组、允许同时存在、一组配置一对,目前允许合并的源字段有`qiyi, bilibili1, imgo, youku, qq, sohu, renren, hanjutv, bahamut, dandan, animeko` | +| SOURCE_ORDER | 【可选】源排序,用于按源对返回资源的排序(注意:先后顺序会影响自动匹配最终的返回),默认是`360,vod,renren,hanjutv`,表示360数据排在最前,hanjutv数据排在最后,示例:`360,renren`:只返回360数据和renren数据,且360数据靠前;当前可选择的源字段有 `360,vod,tmdb,douban,tencent,youku,iqiyi,imgo,bilibili,sohu,leshi,renren,hanjutv,bahamut,dandan,animeko,custom` | +| PLATFORM_ORDER | 【可选】自动匹配优选平台,按顺序优先返回指定平台弹幕,默认为空,即返回第一个满足条件的平台,示例:`bilibili1,qq`,表示如果有b站的播放源,则优先返回b站的弹幕,否则就返回腾讯的弹幕,两者都没有,则返回第一个满足条件的平台;当前可选择的平台字段有 `qiyi, bilibili1, imgo, youku, qq, sohu, leshi, renren, hanjutv, bahamut, dandan, animeko, custom` | +| MERGE_SOURCE_PAIRS | 【可选】源合并配置,配置后将对应源合并同时一起获取弹幕返回,默认为空,格式是`源字段&源字段`,示例:`imgo&iqiyi,dandan&animeko,dandan&bahamut`, 允许多组、允许同时存在、一组配置一对,目前允许合并的源字段有`qiyi, bilibili1, imgo, youku, qq, sohu, leshi, renren, hanjutv, bahamut, dandan, animeko` | | 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,6 +444,7 @@ API 支持返回 Bilibili 标准 XML 格式的弹幕数据,通过查询参数 | imgo | imgo | | bilibili | bilibili1 | | sohu | sohu | +| leshi | leshi | | renren | renren | | hanjutv | hanjutv | | bahamut | bahamut | @@ -498,6 +499,7 @@ API 支持返回 Bilibili 标准 XML 格式的弹幕数据,通过查询参数 │ │ ├── hanjutv.js # 韩剧TV源 │ │ ├── iqiyi.js # 爱奇艺源 │ │ ├── kan360.js # 360看源 +│ │ ├── leshi.js # 乐视视频源 │ │ ├── mango.js # 芒果TV源 │ │ ├── other.js # 第三方弹幕服务器 │ │ ├── renren.js # 人人视频源 diff --git a/config/.env.example b/config/.env.example index a6d5989..0ae10e0 100644 --- a/config/.env.example +++ b/config/.env.example @@ -68,9 +68,9 @@ YOUKU_CONCURRENCY=8 # ==================== 源排序配置 ==================== -# 数据源查询顺序(可选值:360, vod, tmdb, douban, tencent, youku, iqiyi, imgo, bilibili, renren, hanjutv, bahamut, dandan, sohu, animeko, custom) -# 默认值:360,vod,renren,hanjutv(不包含 tencent, youku, iqiyi, imgo, bilibili 和 sohu) -# 如需使用腾讯源、优酷源、爱奇艺源、芒果源、B站源或搜狐源,请手动添加:SOURCE_ORDER=360,vod,tencent,youku,iqiyi,imgo,bilibili,ren,hanjutv,sohu +# 数据源查询顺序(可选值:360, vod, tmdb, douban, tencent, youku, iqiyi, imgo, bilibili, renren, hanjutv, bahamut, dandan, sohu, leshi, animeko, custom) +# 默认值:360,vod,renren,hanjutv(不包含 tencent, youku, iqiyi, imgo, bilibili, sohu 和 leshi) +# 如需使用腾讯源、优酷源、爱奇艺源、芒果源、B站源、搜狐源或乐视源,请手动添加:SOURCE_ORDER=360,vod,tencent,youku,iqiyi,imgo,bilibili,ren,hanjutv,sohu,leshi # 如需使用巴哈姆特源,请手动添加:SOURCE_ORDER=360,vod,renren,hanjutv,bahamut # 如需使用弹弹play源,请手动添加:SOURCE_ORDER=360,vod,renren,hanjutv,dandan # 如需使用豆瓣源,请手动添加:SOURCE_ORDER=360,vod,douban,renren,hanjutv @@ -81,11 +81,11 @@ SOURCE_ORDER=360,vod,renren,hanjutv # 平台优先级排序(用于自动匹配) # 默认值:空(自动匹配) -# 可选值:qiyi, bilibili1, imgo, youku, qq, sohu, renren, hanjutv, bahamut, dandan, animeko, custom +# 可选值:qiyi, bilibili1, imgo, youku, qq, sohu, leshi, renren, hanjutv, bahamut, dandan, animeko, custom # PLATFORM_ORDER= # ==================== 源合并配置 ==================== -# 数据源合并配置(可选值:tencent, youku, iqiyi, imgo, bilibili, renren, hanjutv, bahamut, dandan, sohu, animeko) +# 数据源合并配置(可选值:tencent, youku, iqiyi, imgo, bilibili, renren, hanjutv, bahamut, dandan, sohu, leshi, animeko) # 默认值:空 # 说明:配置后将对应源合并同时一起获取弹幕返回,允许多组、允许一个源同时存在、一组配置一对 # 格式:源字段&源字段 diff --git a/danmu_api/apis/dandan-api.js b/danmu_api/apis/dandan-api.js index 85f57dc..bfd6830 100644 --- a/danmu_api/apis/dandan-api.js +++ b/danmu_api/apis/dandan-api.js @@ -26,6 +26,7 @@ import MangoSource from "../sources/mango.js"; import BilibiliSource from "../sources/bilibili.js"; import YoukuSource from "../sources/youku.js"; import SohuSource from "../sources/sohu.js"; +import LeshiSource from "../sources/leshi.js"; import AnimekoSource from "../sources/animeko.js"; import OtherSource from "../sources/other.js"; import { Anime, AnimeMatch, Episodes, Bangumi } from "../models/dandan-model.js"; @@ -47,6 +48,7 @@ const iqiyiSource = new IqiyiSource(); const mangoSource = new MangoSource(); const bilibiliSource = new BilibiliSource(); const sohuSource = new SohuSource(); +const leshiSource = new LeshiSource(); const animekoSource = new AnimekoSource(); const otherSource = new OtherSource(); const doubanSource = new DoubanSource(tencentSource, iqiyiSource, youkuSource, bilibiliSource); @@ -201,6 +203,7 @@ export async function searchAnime(url, preferAnimeId = null, preferSource = null if (source === "imgo") return mangoSource.search(queryTitle); if (source === "bilibili") return bilibiliSource.search(queryTitle); if (source === "sohu") return sohuSource.search(queryTitle); + if (source === "leshi") return leshiSource.search(queryTitle); if (source === "animeko") return animekoSource.search(queryTitle); }); @@ -220,7 +223,7 @@ export async function searchAnime(url, preferAnimeId = null, preferSource = null vod: animesVodResults, 360: animes360, tmdb: animesTmdb, douban: animesDouban, renren: animesRenren, hanjutv: animesHanjutv, bahamut: animesBahamut, dandan: animesDandan, custom: animesCustom, tencent: animesTencent, youku: animesYouku, iqiyi: animesIqiyi, imgo: animesImgo, bilibili: animesBilibili, - sohu: animesSohu, animeko: animesAnimeko + sohu: animesSohu, leshi: animesLeshi, animeko: animesAnimeko } = resultData; // 按顺序处理每个来源的结果 @@ -276,6 +279,9 @@ export async function searchAnime(url, preferAnimeId = null, preferSource = null } else if (key === 'sohu') { // 等待处理Sohu来源 await sohuSource.handleAnimes(animesSohu, queryTitle, curAnimes); + } else if (key === 'leshi') { + // 等待处理Leshi来源 + await leshiSource.handleAnimes(animesLeshi, queryTitle, curAnimes); } else if (key === 'animeko') { // 等待处理Animeko来源 await animekoSource.handleAnimes(animesAnimeko, queryTitle, curAnimes); @@ -989,6 +995,7 @@ async function fetchMergedComments(url) { else if (sourceName === 'imgo') sourceInstance = mangoSource; else if (sourceName === 'bilibili') sourceInstance = bilibiliSource; else if (sourceName === 'sohu') sourceInstance = sohuSource; + else if (sourceName === 'leshi') sourceInstance = leshiSource; else if (sourceName === 'animeko') sourceInstance = animekoSource; // 如有新增允许的源合并,在此处添加 @@ -1079,6 +1086,8 @@ export async function getComment(path, queryFormat, segmentFlag) { danmus = await youkuSource.getComments(url, plat, segmentFlag); } else if (url.includes('.sohu.com')) { danmus = await sohuSource.getComments(url, plat, segmentFlag); + } else if (url.includes('.le.com')) { + danmus = await leshiSource.getComments(url, plat, segmentFlag); } // 请求其他平台弹幕 @@ -1188,6 +1197,8 @@ export async function getCommentByUrl(videoUrl, queryFormat, segmentFlag) { danmus = await youkuSource.getComments(url, "youku", segmentFlag); } else if (url.includes('.sohu.com')) { danmus = await sohuSource.getComments(url, "sohu", segmentFlag); + } else if (url.includes('.le.com')) { + danmus = await leshiSource.getComments(url, "leshi", segmentFlag); } else { // 如果不是已知平台,尝试第三方弹幕服务器 const urlPattern = /^(https?:\/\/)?([\w.-]+)\.([a-z]{2,})(\/.*)?$/i; @@ -1269,6 +1280,8 @@ export async function getSegmentComment(segment, queryFormat) { danmus = await youkuSource.getSegmentComments(segment); } else if (platform === "sohu") { danmus = await sohuSource.getSegmentComments(segment); + } else if (platform === "leshi") { + danmus = await leshiSource.getSegmentComments(segment); } else if (platform === "hanjutv") { danmus = await hanjutvSource.getSegmentComments(segment); } else if (platform === "bahamut") { @@ -1277,7 +1290,7 @@ export async function getSegmentComment(segment, queryFormat) { danmus = await renrenSource.getSegmentComments(segment); } else if (platform === "dandan") { danmus = await dandanSource.getSegmentComments(segment); - } else if (platform === "animeko") { + } else if (platform === "animeko") { danmus = await animekoSource.getSegmentComments(segment); } else if (platform === "custom") { danmus = await customSource.getSegmentComments(segment); diff --git a/danmu_api/configs/envs.js b/danmu_api/configs/envs.js index f39c73f..abae485 100644 --- a/danmu_api/configs/envs.js +++ b/danmu_api/configs/envs.js @@ -9,10 +9,10 @@ export class Envs { static originalEnvVars = new Map(); static accessedEnvVars = new Map(); - static VOD_ALLOWED_PLATFORMS = ['qiyi', 'bilibili1', 'imgo', 'youku', 'qq', 'sohu']; // vod允许的播放平台 - static ALLOWED_PLATFORMS = ['qiyi', 'bilibili1', 'imgo', 'youku', 'qq', 'renren', 'hanjutv', 'bahamut', 'dandan', 'sohu', 'animeko', 'custom']; // 全部源允许的播放平台 - static ALLOWED_SOURCES = ['360', 'vod', 'tmdb', 'douban', 'tencent', 'youku', 'iqiyi', 'imgo', 'bilibili', 'renren', 'hanjutv', 'bahamut', 'dandan', 'sohu', 'animeko', 'custom']; // 允许的源 - static MERGE_ALLOWED_SOURCES = ['tencent', 'youku', 'iqiyi', 'imgo', 'bilibili', 'renren', 'hanjutv', 'bahamut', 'dandan', 'sohu', 'animeko']; // 允许的源合并 + static VOD_ALLOWED_PLATFORMS = ['qiyi', 'bilibili1', 'imgo', 'youku', 'qq', 'sohu', 'leshi']; // vod允许的播放平台 + static ALLOWED_PLATFORMS = ['qiyi', 'bilibili1', 'imgo', 'youku', 'qq', 'renren', 'hanjutv', 'bahamut', 'dandan', 'sohu', 'leshi', 'animeko', 'custom']; // 全部源允许的播放平台 + static ALLOWED_SOURCES = ['360', 'vod', 'tmdb', 'douban', 'tencent', 'youku', 'iqiyi', 'imgo', 'bilibili', 'renren', 'hanjutv', 'bahamut', 'dandan', 'sohu', 'leshi', 'animeko', 'custom']; // 允许的源 + static MERGE_ALLOWED_SOURCES = ['tencent', 'youku', 'iqiyi', 'imgo', 'bilibili', 'renren', 'hanjutv', 'bahamut', 'dandan', 'sohu', 'leshi', 'animeko']; // 允许的源合并 /** * 获取环境变量 diff --git a/danmu_api/configs/globals.js b/danmu_api/configs/globals.js index a22d014..1eddb2b 100644 --- a/danmu_api/configs/globals.js +++ b/danmu_api/configs/globals.js @@ -13,7 +13,7 @@ export const Globals = { accessedEnvVars: {}, // 静态常量 - VERSION: '1.12.0', + VERSION: '1.12.1', MAX_LOGS: 500, // 日志存储,最多保存 500 行 MAX_ANIMES: 100, diff --git a/danmu_api/sources/leshi.js b/danmu_api/sources/leshi.js new file mode 100644 index 0000000..5731b55 --- /dev/null +++ b/danmu_api/sources/leshi.js @@ -0,0 +1,765 @@ +import BaseSource from './base.js'; +import { globals } from '../configs/globals.js'; +import { log } from "../utils/log-util.js"; +import { httpGet, buildQueryString } from "../utils/http-util.js"; +import { convertToAsciiSum } from "../utils/codec-util.js"; +import { generateValidStartDate } from "../utils/time-util.js"; +import { addAnime, removeEarliestAnime } from "../utils/cache-util.js"; +import { printFirst200Chars, titleMatches } from "../utils/common-util.js"; +import { SegmentListResponse } from '../models/dandan-model.js'; + +// 媒体类型映射 +const typeMap = { + 'tv': '电视剧', + 'movie': '电影', + 'cartoon': '动漫', + 'comic': '动漫' +}; + +// ===================== +// 获取乐视网弹幕 +// ===================== +export default class LeshiSource extends BaseSource { + constructor() { + super(); + + // 弹幕位置映射: + this.positionMap = { + 4: 1, // 滚动弹幕 + 3: 4, // 底部弹幕 + 1: 5, // 顶部弹幕 + 2: 1, // 其他 -> 滚动 + }; + } + + /** + * 过滤乐视网搜索项 + * @param {Object} item - 搜索项 + * @param {string} keyword - 搜索关键词 + * @returns {Object|null} 过滤后的结果 + */ + filterLeshiSearchItem(item, keyword) { + if (!item.pid || !item.title) { + return null; + } + + // 清理标题中的高亮标记 + let title = item.title; + + // 映射类型 + const resultType = typeMap[item.type] || '电视剧'; + + return { + mediaId: String(item.pid), + title: title, + type: resultType, + year: item.year || null, + imageUrl: item.imageUrl || null, + episodeCount: item.episodeCount || 0 + }; + } + + async search(keyword) { + try { + log("info", `[Leshi] 开始搜索: ${keyword}`); + + // 构造搜索URL + const params = { + 'wd': keyword, + 'from': 'pc', + 'ref': 'click', + 'click_area': 'search_button', + 'query': keyword, + 'is_default_query': '0', + 'module': 'search_rst_page' + }; + + // 设置请求头,模拟浏览器 + const headers = { + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36', + 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8', + 'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8', + 'Accept-Encoding': 'gzip, deflate, br', + 'Referer': 'https://so.le.com/', + 'Connection': 'keep-alive', + 'Upgrade-Insecure-Requests': '1', + 'Sec-Fetch-Dest': 'document', + 'Sec-Fetch-Mode': 'navigate', + 'Sec-Fetch-Site': 'same-origin' + }; + + const searchUrl = `https://so.le.com/s?${buildQueryString(params)}`; + + const response = await httpGet(searchUrl, { headers, timeout: 15000 }); + + if (!response || !response.data) { + log("info", "[Leshi] 搜索响应为空"); + return []; + } + + const htmlContent = response.data; + + log("debug", `[Leshi] 搜索请求成功,响应长度: ${htmlContent.length} 字符`); + + // 解析HTML,提取data-info属性 + const results = []; + + // 使用正则表达式提取所有 data-info 属性 + // 格式:data-info="{pid:'10026580',type:'tv',...}" + const pattern = /