Files
tvbox_collect/jaychouqq/yingshi/js1/Pan_tgs_api.js
T
2026-06-29 03:41:14 +00:00

583 lines
17 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//@name:[盘] TG搜²
//@version:9
//@webSite:豆儿盘@douerpan&豆儿123@zyfb123&天翼日更@tianyirigeng&天翼臻影@tyysypzypd&百度@bdwpzhpd&夸克百度@Aliyun_4K_Movies&夸克UC@ucquark&夸克电影@Q_dianying&夸克剧集@Q_dianshiju&夸克动漫@Q_dongman
//@env:TG搜API地址##https://tgsou.252035.xyz
//@remark:🍃豆儿出品,不属精品!免代理纯搜索,格式 频道名称@频道id|搜索数量&频道名称@频道id,支持自定义每频道搜索数量,默认3个
//@order: A18
// ignore
// 不支持导入,这里只是本地开发用于代码提示
// 如需添加通用依赖,请联系 https://t.me/uzVideoAppbot
import {
FilterLabel,
FilterTitle,
VideoClass,
VideoSubclass,
VideoDetail,
RepVideoClassList,
RepVideoSubclassList,
RepVideoList,
RepVideoDetail,
RepVideoPlayUrl,
UZArgs,
UZSubclassVideoListArgs,
} from '../../core/core/uzVideo.js'
import {
UZUtils,
ProData,
ReqResponseType,
ReqAddressType,
req,
getEnv,
setEnv,
goToVerify,
openWebToBindEnv,
toast,
kIsDesktop,
kIsAndroid,
kIsIOS,
kIsWindows,
kIsMacOS,
kIsTV,
kLocale,
kAppVersion,
formatBackData,
} from '../../core/core/uzUtils.js'
import { cheerio, Crypto, Encrypt, JSONbig } from '../../core/core/uz3lib.js'
// ignore
const appConfig = {
_webSite: '豆儿盘@douerpan&豆儿123@zyfb123&天翼日更@tianyirigeng&天翼臻影@tyysypzypd&百度@bdwpzhpd&夸克百度@Aliyun_4K_Movies&夸克UC@ucquark&夸克电影@Q_dianying&夸克剧集@Q_dianshiju&夸克动漫@Q_dongman',
/**
* 网站主页,uz 调用每个函数前都会进行赋值操作
* 如果不想被改变 请自定义一个变量
*/
get webSite() {
return this._webSite
},
set webSite(value) {
this._webSite = value
},
// TG搜索API地址,替代原来的 t.me/s/
tgs: 'https://tgsou.252035.xyz',
_uzTag: '',
/**
* 扩展标识,初次加载时,uz 会自动赋值,请勿修改
* 用于读取环境变量
*/
get uzTag() {
return this._uzTag
},
set uzTag(value) {
this._uzTag = value
},
}
// --- 全局常量/配置 ---
// 统一的网盘配置 - UZ应用兼容的网盘
const CLOUD_PROVIDERS = {
tianyi: {
name: '天翼',
domains: ['189.cn']
},
quark: {
name: '夸克',
domains: ['pan.quark.cn']
},
uc: {
name: 'UC',
domains: ['drive.uc.cn']
},
baidu: {
name: '百度',
domains: ['pan.baidu.com', 'yun.baidu.com']
},
pan123: {
name: '123',
domains: ['123pan.com', '123pan.cn', '123684.com', '123912.com', '123865.com']
},
yidong: {
name: '移动',
domains: ['caiyun.139.com', 'yun.139.com']
},
'115': {
name: '115',
domains: ['115cdn.com', '115.com', 'anxia.com']
},
pikpak: {
name: 'PikPak',
domains: ['pikpak.me']
}
};
// 从统一配置自动生成所需数组
const panUrlsExt = Object.values(CLOUD_PROVIDERS).flatMap(provider => provider.domains);
// 预编译网盘提供商正则表达式,提高匹配性能
const providerRegexMap = Object.values(CLOUD_PROVIDERS).map(provider => ({
name: provider.name,
// 将多个域名组合成一个正则,用 | 分隔,转义点号
regex: new RegExp(provider.domains.map(domain =>
domain.replace(/\./g, '\\.')
).join('|'), 'i')
}));
// 预编译剧集信息提取正则表达式
const EPISODE_COMBINED_REGEX = /((?:更新至|全|第)\s*\d+\s*集)|((?:更新至|全|第)\s*[一二三四五六七八九十百千万亿]+\s*集)|((?:更至|更)\s*(?:EP)?\s*\d+)/;
/**
* 异步获取分类列表的方法。
* @param {UZArgs} args
* @returns {@Promise<JSON.stringify(new RepVideoClassList())>}
*/
async function getClassList(args) {
var backData = new RepVideoClassList()
try {
// 纯搜索版本,不提供分类功能
// 返回空分类列表
} catch (error) {
backData.error = error.toString()
}
return JSON.stringify(backData)
}
/**
* 获取二级分类列表筛选列表的方法。
* @param {UZArgs} args
* @returns {@Promise<JSON.stringify(new RepVideoSubclassList())>}
*/
async function getSubclassList(args) {
var backData = new RepVideoSubclassList()
try {
// 纯搜索版本,不提供二级分类功能
} catch (error) {
backData.error = error.toString()
}
return JSON.stringify(backData)
}
/**
* 获取分类视频列表
* @param {UZArgs} args
* @returns {@Promise<JSON.stringify(new RepVideoList())>}
*/
async function getVideoList(args) {
var backData = new RepVideoList()
try {
// 纯搜索版本,不提供分类浏览功能
} catch (error) {
backData.error = error.toString()
}
return JSON.stringify(backData)
}
/**
* 获取二级分类视频列表 或 筛选视频列表
* @param {UZSubclassVideoListArgs} args
* @returns {@Promise<JSON.stringify(new RepVideoList())>}
*/
async function getSubclassVideoList(args) {
var backData = new RepVideoList()
try {
// 纯搜索版本,不提供筛选功能
} catch (error) {
backData.error = error.toString()
}
return JSON.stringify(backData)
}
/**
* 获取视频详情
* @param {UZArgs} args
* @returns {@Promise<JSON.stringify(new RepVideoDetail())>}
*/
async function getVideoDetail(args) {
var backData = new RepVideoDetail()
try {
backData.data = {
panUrls: JSON.parse(args.url),
}
} catch (error) {
backData.error = error.toString()
}
return JSON.stringify(backData)
}
/**
* 获取视频的播放地址
* @param {UZArgs} args
* @returns {@Promise<JSON.stringify(new RepVideoPlayUrl())>}
*/
async function getVideoPlayUrl(args) {
var backData = new RepVideoPlayUrl()
try {
// 纯搜索版本,不提供播放功能
} catch (error) {
backData.error = error.toString()
}
return JSON.stringify(backData)
}
/**
* 解析频道配置,支持自定义搜索数量
* @param {string} webSite - 配置字符串
* @returns {Array} 频道配置数组
*/
function parseChannelConfig(webSite) {
const channels = []
const items = webSite.split('&')
for (const item of items) {
if (!item.trim()) continue
const parts = item.split('@')
if (parts.length < 2) continue
const channelName = parts[0].trim()
const channelIdAndCount = parts[1].trim()
// 检查是否有自定义数量 (使用 | 分隔符)
let channelId, count = 3 // 默认数量为3
if (channelIdAndCount.includes('|')) {
const idCountParts = channelIdAndCount.split('|')
channelId = idCountParts[0].trim()
const customCount = parseInt(idCountParts[1])
if (!isNaN(customCount) && customCount > 0) {
count = customCount
}
} else {
channelId = channelIdAndCount
}
channels.push({
name: channelName,
id: channelId,
count: count
})
}
return channels
}
/**
* 搜索视频 - 核心功能
* @param {UZArgs} args
* @returns {@Promise<JSON.stringify(new RepVideoList())>}
*/
async function searchVideo(args) {
var backData = new RepVideoList()
try {
// 获取环境变量中的API地址
var tgsApi = await getEnv(appConfig.uzTag, "TG搜API地址")
if (tgsApi && tgsApi.length > 0) {
appConfig.tgs = tgsApi
}
// 解析频道配置
const channels = parseChannelConfig(appConfig.webSite)
if (channels.length === 0) {
console.log('没有有效的频道配置')
return JSON.stringify(backData)
}
console.log('频道配置:', channels)
// 创建频道限制映射
const channelLimits = {}
channels.forEach(channel => {
channelLimits[channel.id] = channel.count
})
// 🚀 并发优化:同时搜索所有频道
const searchPromises = channels.map(async (channel) => {
try {
// 构建单个频道的搜索API请求URL
// 🖼️ 测试启用图片功能
const searchUrl = `${appConfig.tgs}?pic=true&count=${channel.count}&channelUsername=${encodeURIComponent(channel.id)}&keyword=${encodeURIComponent(args.searchWord)}`
console.log(`搜索频道 ${channel.name}(${channel.id}) 数量:${channel.count}`)
console.log('搜索URL:', searchUrl)
// 调用TG搜索API
const res = await req(searchUrl)
if (res.data && res.data.results) {
// 解析API返回的数据,传递频道限制
const videoList = parseAPIResults(res.data.results, args.searchWord, channelLimits)
return {
channel: channel.name,
videoList: videoList,
success: true
}
}
return {
channel: channel.name,
videoList: [],
success: true
}
} catch (channelError) {
console.error(`频道 ${channel.name} 搜索失败:`, channelError)
return {
channel: channel.name,
videoList: [],
success: false,
error: channelError.message
}
}
})
// 🚀 并发执行所有搜索请求,设置10秒超时(API请求可能比直接访问慢)
const results = await Promise.allSettled(
searchPromises.map(promise =>
Promise.race([
promise,
new Promise((_, reject) =>
setTimeout(() => reject(new Error('API请求超时')), 10000)
)
])
)
)
// 处理并发结果
const allVideoLists = []
let successCount = 0
let failCount = 0
results.forEach((result, index) => {
if (result.status === 'fulfilled' && result.value.success) {
allVideoLists.push(...result.value.videoList)
successCount++
console.log(`频道 ${result.value.channel} 搜索成功,获得 ${result.value.videoList.length} 个结果`)
} else {
failCount++
const channelName = channels[index]?.name || '未知频道'
if (result.status === 'rejected') {
console.error(`频道 ${channelName} 请求失败:`, result.reason?.message || result.reason)
} else {
console.error(`频道 ${channelName} 搜索失败:`, result.value?.error || '未知错误')
}
}
})
console.log(`搜索完成: 成功 ${successCount} 个频道,失败 ${failCount} 个频道`)
// 合并所有结果并去重
backData.data = deduplicateVideoListByLinks(allVideoLists)
console.log(`总共获取到 ${backData.data.length} 个去重后的结果`)
} catch (error) {
console.error('搜索错误:', error)
backData.error = error.toString()
}
return JSON.stringify(backData)
}
/**
* 解析TG搜索API返回的结果
* @param {Array} results - API返回的results数组
* @param {string} searchWord - 搜索关键词
* @param {Object} channelLimits - 频道限制映射 {channelId: count}
* @returns {Array} 视频列表
*/
function parseAPIResults(results, searchWord, channelLimits = {}) {
const videoList = []
// 创建频道名称映射 - 使用新的解析逻辑
const channelMap = new Map()
const channels = parseChannelConfig(appConfig.webSite)
channels.forEach(channel => {
channelMap.set(channel.id, channel.name) // 键: id, 值: name
})
for (const result of results) {
if (!result || typeof result !== 'string') continue
// 🖼️ 解析格式: "频道名$$$链接1@图片1$$标题1##链接2@图片2$$标题2##..."
// 注意:启用图片后,链接和图片用@分隔
const parts = result.split('$$$')
if (parts.length !== 2) continue
const channelName = parts[0]
const contentStr = parts[1]
if (!contentStr) continue
// 解析内容项: "链接1@图片1$$标题1##链接2@图片2$$标题2##..."
let items = contentStr.split('##')
// 根据频道限制截取结果数量
const channelId = Object.keys(channelLimits).find(id =>
channelName.includes(id) || id === channelName
)
if (channelId && channelLimits[channelId]) {
items = items.slice(0, channelLimits[channelId])
console.log(`频道 ${channelName} 限制为 ${channelLimits[channelId]} 个结果,实际处理 ${items.length} 个`)
}
for (const item of items) {
if (!item.trim()) continue
const itemParts = item.split('$$')
let linkAndPic = itemParts[0]?.trim() || ''
let title = itemParts[1]?.trim()
// 🖼️ 解析链接和图片(格式:链接@图片URL)
let link = ''
let picUrl = ''
if (linkAndPic.includes('@')) {
const linkPicParts = linkAndPic.split('@')
link = linkPicParts[0]?.trim() || ''
picUrl = linkPicParts[1]?.trim() || ''
} else {
link = linkAndPic
}
// 如果没有标题,使用搜索关键词
if (!title) {
title = searchWord
}
// 验证是否为有效的网盘链接
if (!link || !isValidPanUrl(link)) continue
// 创建视频对象
const video = new VideoDetail()
video.vod_id = JSON.stringify([link])
video.vod_name = cleanTitle(title)
// 识别网盘提供商
const providers = identifyProviders([link])
// 提取剧集信息
const episodeInfo = extractEpisodeInfo(title)
if (episodeInfo) {
video.vod_name = title.replace(episodeInfo, '').trim()
}
// 构建备注
const remarkParts = []
if (providers.length > 0) {
remarkParts.push(providers.join('/'))
}
if (channelName && channelName !== '未知频道') {
remarkParts.push(channelName)
}
if (episodeInfo) {
remarkParts.push(episodeInfo)
}
video.vod_remarks = remarkParts.length > 0 ? remarkParts.join('|') : '资源'
// 🖼️ 设置图片URL(如果有的话)
video.vod_pic = picUrl || ''
videoList.push(video)
}
}
return videoList
}
/**
* 验证是否为有效的网盘URL
* @param {string} url
* @returns {boolean}
*/
function isValidPanUrl(url) {
if (!url || typeof url !== 'string') return false
for (const domain of panUrlsExt) {
if (url.includes(domain)) {
return true
}
}
return false
}
/**
* 识别网盘提供商
* @param {Array} urls
* @returns {Array} 提供商名称数组
*/
function identifyProviders(urls) {
const providers = new Set()
for (const url of urls) {
for (const provider of providerRegexMap) {
if (provider.regex.test(url)) {
providers.add(provider.name)
break
}
}
}
return Array.from(providers)
}
/**
* 清理标题
* @param {string} title
* @returns {string}
*/
function cleanTitle(title) {
if (!title) return ''
return title
.replace(/^(名称[:])/, '')
.replace(/\s+/g, ' ')
.trim()
}
/**
* 提取剧集信息
* @param {string} title
* @returns {string|null}
*/
function extractEpisodeInfo(title) {
if (!title) return null
const match = title.match(EPISODE_COMBINED_REGEX)
return match ? match[0] : null
}
/**
* 去重函数 - 基于链接去重
* @param {Array} videoList
* @returns {Array}
*/
function deduplicateVideoListByLinks(videoList) {
const map = new Map()
for (const video of videoList) {
let ids
try {
ids = JSON.parse(video.vod_id || '[]')
if (!Array.isArray(ids)) {
ids = []
}
} catch (e) {
ids = []
}
// 通过排序后的链接创建唯一键
const key = JSON.stringify(ids.sort())
// 如果键不存在,添加到map中
if (!map.has(key)) {
map.set(key, video)
}
}
return Array.from(map.values())
}