diff --git a/茶杯狐.js b/茶杯狐.js new file mode 100644 index 0000000..acd41cc --- /dev/null +++ b/茶杯狐.js @@ -0,0 +1,481 @@ +// ============================================ +// 茶杯狐 TVBox JS版 - 完整可用 +// 完全复制Python版请求行为 +// ============================================ + +let siteUrl = 'http://www.bnjxjd.com'; +let headers = { + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', + 'Referer': 'http://www.bnjxjd.com' +}; + +// ============ 工具函数 ============ + +// 完全复制Python版fetch行为 +async function fetchUrl(url) { + try { + let res = await req(url, { + method: 'get', + headers: headers, + timeout: 15000 + }); + + let content = res.content || ''; + + // 如果返回的是Buffer/ArrayBuffer,转成UTF-8字符串 + if (typeof content === 'object' && !(content instanceof String)) { + try { + content = new TextDecoder('utf-8').decode(content); + } catch (e) { + content = String(content); + } + } + + // 处理可能的乱码 + if (typeof content === 'string') { + // 移除BOM头 + if (content.charCodeAt(0) === 0xFEFF) { + content = content.slice(1); + } + } + + return content; + } catch (e) { + console.log('请求失败:', url, e.message); + return ''; + } +} + +function cleanText(text) { + if (!text) return ''; + return text.replace(/ /g, ' ') + .replace(/\n/g, ' ') + .replace(/\r/g, ' ') + .replace(/\s+/g, ' ') + .trim(); +} + +// ============ 核心函数 ============ + +async function init(cfg) { + // 支持外部传入地址 + if (cfg && cfg.ext && typeof cfg.ext === 'string' && cfg.ext.startsWith('http')) { + siteUrl = cfg.ext; + } + if (siteUrl.endsWith('/')) { + siteUrl = siteUrl.slice(0, -1); + } + headers.Referer = siteUrl; +} + +async function home(filter) { + let classes = [ + { type_id: '1', type_name: '电影' }, + { type_id: '2', type_name: '电视剧' }, + { type_id: '3', type_name: '综艺' }, + { type_id: '4', type_name: '动漫' } + ]; + + let filters = { + '1': [ + { key: 'class', name: '类型', value: [ + {n:'全部',v:'1'},{n:'动作片',v:'6'},{n:'喜剧片',v:'7'},{n:'爱情片',v:'8'}, + {n:'科幻片',v:'9'},{n:'恐怖片',v:'10'},{n:'剧情片',v:'11'},{n:'战争片',v:'12'}, + {n:'记录片',v:'13'},{n:'悬疑片',v:'14'},{n:'犯罪片',v:'15'},{n:'奇幻片',v:'16'},{n:'动画片',v:'31'} + ]}, + { key: 'area', name: '地区', value: [ + {n:'全部',v:'0'},{n:'大陆',v:'大陆'},{n:'香港',v:'香港'},{n:'台湾',v:'台湾'}, + {n:'美国',v:'美国'},{n:'日本',v:'日本'},{n:'韩国',v:'韩国'},{n:'英国',v:'英国'}, + {n:'法国',v:'法国'},{n:'德国',v:'德国'},{n:'泰国',v:'泰国'},{n:'印度',v:'印度'}, + {n:'加拿大',v:'加拿大'},{n:'其他',v:'其他'} + ]}, + { key: 'year', name: '年份', value: [ + {n:'全部',v:'0'},{n:'2026',v:'2026'},{n:'2025',v:'2025'},{n:'2024',v:'2024'}, + {n:'2023',v:'2023'},{n:'2022',v:'2022'},{n:'2021',v:'2021'},{n:'2020',v:'2020'}, + {n:'2019',v:'2019'},{n:'2018',v:'2018'},{n:'2017',v:'2017'},{n:'2016',v:'2016'}, + {n:'2015',v:'2015'},{n:'2014',v:'2014'},{n:'更早',v:'2009'} + ]}, + { key: 'sort', name: '排序', value: [ + {n:'时间',v:'time'},{n:'人气',v:'hits'},{n:'评分',v:'score'} + ]} + ], + '2': [ + { key: 'class', name: '类型', value: [ + {n:'全部',v:'2'},{n:'国产剧',v:'17'},{n:'欧美剧',v:'21'},{n:'日韩剧',v:'20'}, + {n:'港台剧',v:'18'},{n:'海外剧',v:'22'} + ]}, + { key: 'area', name: '地区', value: [ + {n:'全部',v:'0'},{n:'大陆',v:'大陆'},{n:'香港',v:'香港'},{n:'台湾',v:'台湾'}, + {n:'美国',v:'美国'},{n:'日本',v:'日本'},{n:'韩国',v:'韩国'},{n:'英国',v:'英国'} + ]}, + { key: 'year', name: '年份', value: [ + {n:'全部',v:'0'},{n:'2026',v:'2026'},{n:'2025',v:'2025'},{n:'2024',v:'2024'}, + {n:'2023',v:'2023'},{n:'2022',v:'2022'},{n:'2021',v:'2021'},{n:'2020',v:'2020'}, + {n:'2019',v:'2019'},{n:'2018',v:'2018'},{n:'2017',v:'2017'},{n:'2016',v:'2016'}, + {n:'2015',v:'2015'},{n:'2014',v:'2014'},{n:'更早',v:'2009'} + ]}, + { key: 'sort', name: '排序', value: [ + {n:'时间',v:'time'},{n:'人气',v:'hits'},{n:'评分',v:'score'} + ]} + ], + '3': [ + { key: 'year', name: '年份', value: [ + {n:'全部',v:'0'},{n:'2026',v:'2026'},{n:'2025',v:'2025'},{n:'2024',v:'2024'} + ]}, + { key: 'sort', name: '排序', value: [ + {n:'时间',v:'time'},{n:'人气',v:'hits'} + ]} + ], + '4': [ + { key: 'area', name: '地区', value: [ + {n:'全部',v:'0'},{n:'大陆',v:'大陆'},{n:'日本',v:'日本'},{n:'美国',v:'美国'} + ]}, + { key: 'year', name: '年份', value: [ + {n:'全部',v:'0'},{n:'2026',v:'2026'},{n:'2025',v:'2025'},{n:'2024',v:'2024'} + ]}, + { key: 'sort', name: '排序', value: [ + {n:'时间',v:'time'},{n:'人气',v:'hits'} + ]} + ] + }; + + return JSON.stringify({ + class: classes, + filters: filters + }); +} + +async function homeVod() { + let html = await fetchUrl(siteUrl); + if (!html) return JSON.stringify({ list: [] }); + + let videos = parseVideoList(html); + return JSON.stringify({ list: videos }); +} + +async function category(tid, pg, filter, extend) { + let page = parseInt(pg) || 1; + extend = extend || {}; + + let classVal = extend['class'] || ''; + let areaVal = extend['area'] || '0'; + let yearVal = extend['year'] || '0'; + let sortVal = extend['sort'] || 'time'; + + let c = (classVal && classVal !== '1' && classVal !== '2') ? classVal : ''; + let a = areaVal !== '0' ? areaVal : ''; + let y = yearVal !== '0' ? yearVal : ''; + + let url; + if (c || a || y) { + url = `${siteUrl}/movie/fenlei${tid}--${c}--${a}--${y}---${page}---.html`; + } else { + url = `${siteUrl}/movie/fenlei${tid}---${page}.html`; + } + + let html = await fetchUrl(url); + let videos = html ? parseVideoList(html) : []; + + return JSON.stringify({ + list: videos, + page: page, + pagecount: 9999, + limit: 90, + total: 999999 + }); +} + +async function detail(id) { + let url = id.startsWith('http') ? id : siteUrl + (id.startsWith('/') ? '' : '/') + id; + let html = await fetchUrl(url); + if (!html) return JSON.stringify({ list: [] }); + + const $ = load(html); + let info = { vod_id: id }; + + // 标题 + let title = $('h1.title').text().trim() || $('h1').text().trim(); + if (title) info.vod_name = cleanText(title); + + // 简介 + let content = $('span.detail-content').html() || $('p.col-pd').html() || ''; + if (content) { + info.vod_content = cleanText(content.replace(/<[^>]+>/g, '')); + } + + // 海报 + let pic = $('img.img-responsive').attr('src') || ''; + if (!pic) { + $('img.lazy').each((i, n) => { + let src = $(n).attr('src') || $(n).attr('data-original') || ''; + if (src) pic = src; + }); + } + if (pic && !pic.startsWith('http')) { + pic = siteUrl + (pic.startsWith('/') ? '' : '/') + pic; + } + if (pic) info.vod_pic = pic; + + // 用正则提取详情信息(和Python版一致) + let actorMatch = html.match(/主演:<\/span>([\s\S]*?)<\/p>/); + if (actorMatch) info.vod_actor = cleanText(actorMatch[1].replace(/<[^>]+>/g, '')); + + let directorMatch = html.match(/导演:<\/span>([\s\S]*?)<\/p>/); + if (directorMatch) info.vod_director = cleanText(directorMatch[1].replace(/<[^>]+>/g, '')); + + let areaMatch = html.match(/地区:<\/span>([\s\S]*?)<\/p>/); + if (areaMatch) info.vod_area = cleanText(areaMatch[1].replace(/<[^>]+>/g, '')); + + let yearMatch = html.match(/年份:<\/span>([\s\S]*?)<\/p>/); + if (yearMatch) info.vod_year = cleanText(yearMatch[1].replace(/<[^>]+>/g, '')); + + let typeMatch = html.match(/类型:<\/span>([\s\S]*?)<\/p>/); + if (typeMatch) info.type_name = cleanText(typeMatch[1].replace(/<[^>]+>/g, '')); + + // 播放列表(和Python版逻辑一致) + let ktabs = []; + let klists = []; + + // 提取线路名称 + let tabs = []; + let tabMatches = html.match(/]*>.*?]*href="#playlist\d+"[^>]*>([\s\S]*?)<\/a>/g) || []; + tabMatches.forEach(tab => { + let nameMatch = tab.match(/]*>([\s\S]*?)<\/a>/); + if (nameMatch) { + let name = cleanText(nameMatch[1].replace(/<[^>]+>/g, '')); + if (name && !['猜您喜欢','同类型','大家还在看','影片评论','播放地址'].includes(name)) { + tabs.push(name); + } + } + }); + + // 提取剧集 + let ulMatches = html.match(/]*class="stui-content__playlist[^"]*"[^>]*>([\s\S]*?)<\/ul>/g) || []; + ulMatches.forEach((ulContent, idx) => { + let liMatches = ulContent.match(/]*>.*?]*href="([^"]+)"[^>]*>([\s\S]*?)<\/a>/g) || []; + let episodes = []; + liMatches.forEach(li => { + let hrefMatch = li.match(/href="([^"]+)"/); + let nameMatch = li.match(/]*>([\s\S]*?)<\/a>/); + if (hrefMatch && nameMatch) { + let epLink = hrefMatch[1]; + let epName = cleanText(nameMatch[1]); + if (epName && epLink) { + if (!epLink.startsWith('http')) { + epLink = siteUrl + (epLink.startsWith('/') ? '' : '/') + epLink; + } + episodes.push(`${epName}$${epLink}`); + } + } + }); + if (episodes.length > 0) { + let sourceName = tabs[idx] || `播放源${idx+1}`; + ktabs.push(sourceName); + klists.push(episodes.join('#')); + } + }); + + // 备用:直接找所有播放链接 + if (ktabs.length === 0) { + let allLinks = html.match(/]*href="([^"]*)"[^>]*>([^<]+)<\/a>/g) || []; + let episodes = []; + allLinks.forEach(link => { + let hrefMatch = link.match(/href="([^"]+)"/); + let nameMatch = link.match(/]*>([^<]+)<\/a>/); + if (hrefMatch && nameMatch) { + let epLink = hrefMatch[1]; + let epName = cleanText(nameMatch[1]); + if (epLink && epName && epLink.includes('/play/')) { + if (!['立即播放','播放','观看'].includes(epName)) { + if (!epLink.startsWith('http')) { + epLink = siteUrl + (epLink.startsWith('/') ? '' : '/') + epLink; + } + episodes.push(`${epName}$${epLink}`); + } + } + } + }); + if (episodes.length > 0) { + ktabs.push('茶杯狐'); + klists.push(episodes.join('#')); + } + } + + info.vod_play_from = ktabs.join('$$$') || '茶杯狐'; + info.vod_play_url = klists.join('$$$') || ''; + + return JSON.stringify({ list: [info] }); +} + +async function play(flag, id, flags) { + let url = id.startsWith('http') ? id : siteUrl + (id.startsWith('/') ? '' : '/') + id; + let html = await fetchUrl(url); + if (!html) { + return JSON.stringify({ parse: 1, url: url }); + } + + // 提取m3u8地址(和Python版一致) + let realUrl = extractM3u8(html); + + if (realUrl) { + realUrl = realUrl.replace(/\\\//g, '/'); + if (realUrl.startsWith('//')) { + realUrl = 'https:' + realUrl; + } + let parse = /\.(m3u8|mp4|mkv|flv|ts)$/i.test(realUrl) ? 0 : 1; + return JSON.stringify({ + parse: parse, + url: realUrl, + header: { + 'User-Agent': headers['User-Agent'], + 'Referer': siteUrl + } + }); + } + + // 如果没找到,尝试iframe(和Python版一致) + let iframeMatch = html.match(/]*src=["']([^"']+)["']/i); + if (iframeMatch) { + let src = iframeMatch[1]; + if (src.startsWith('//')) src = 'https:' + src; + else if (src.startsWith('/')) src = siteUrl + src; + let iframeHtml = await fetchUrl(src); + if (iframeHtml) { + let iframeUrl = extractM3u8(iframeHtml); + if (iframeUrl) { + return JSON.stringify({ + parse: /\.(m3u8|mp4|mkv|flv|ts)$/i.test(iframeUrl) ? 0 : 1, + url: iframeUrl, + header: { + 'User-Agent': headers['User-Agent'], + 'Referer': siteUrl + } + }); + } + } + } + + return JSON.stringify({ + parse: 1, + url: url, + header: { + 'User-Agent': headers['User-Agent'], + 'Referer': siteUrl + } + }); +} + +async function search(wd, quick, pg) { + let page = parseInt(pg) || 1; + let encodedKey = encodeURIComponent(wd); + let url = `${siteUrl}/vodsearch/wd/${encodedKey}.html`; + + let html = await fetchUrl(url); + let videos = html ? parseSearchList(html) : []; + + return JSON.stringify({ + list: videos, + page: page, + pagecount: 9999, + limit: 90, + total: 999999 + }); +} + +// ============ 辅助解析函数 ============ + +function parseVideoList(html) { + let videos = []; + const $ = load(html); + + $('.stui-vodlist__box, .stui-vodlist__item').each((i, n) => { + let link = $(n).find('.stui-vodlist__thumb'); + if (!link.length) return; + + let name = cleanText(link.attr('title') || ''); + let pic = link.attr('data-original') || link.attr('src') || ''; + let href = link.attr('href') || ''; + let remark = $(n).find('.pic-text.text-right').text().trim() || ''; + + if (pic && !pic.startsWith('http')) { + pic = siteUrl + (pic.startsWith('/') ? '' : '/') + pic; + } + if (href && !href.startsWith('http')) { + href = siteUrl + (href.startsWith('/') ? '' : '/') + href; + } + + if (name && href) { + videos.push({ + vod_id: href, + vod_name: name, + vod_pic: pic, + vod_remarks: cleanText(remark) + }); + } + }); + + return videos; +} + +function parseSearchList(html) { + let videos = []; + const $ = load(html); + + $('.stui-vodlist__box, .stui-vodlist__media li, .stui-vodlist__item').each((i, n) => { + let link = $(n).find('.stui-vodlist__thumb'); + if (!link.length) return; + + let name = cleanText(link.attr('title') || ''); + let pic = link.attr('data-original') || link.attr('src') || ''; + let href = link.attr('href') || ''; + let remark = $(n).find('.pic-text.text-right, .pic-text').text().trim() || ''; + + if (pic && !pic.startsWith('http')) { + pic = siteUrl + (pic.startsWith('/') ? '' : '/') + pic; + } + if (href && !href.startsWith('http')) { + href = siteUrl + (href.startsWith('/') ? '' : '/') + href; + } + + if (name && href) { + videos.push({ + vod_id: href, + vod_name: name, + vod_pic: pic, + vod_remarks: cleanText(remark) + }); + } + }); + + return videos; +} + +function extractM3u8(html) { + let patterns = [ + /"url"\s*:\s*"([^"]+\.m3u8[^"]*)"/, + /url\s*:\s*"([^"]+\.m3u8[^"]*)"/, + /"url"\s*:\s*'([^']+\.m3u8[^']*)'/, + /https?:\/\/[^"']+\.m3u8[^"']*/ + ]; + + for (let p of patterns) { + let match = html.match(p); + if (match) { + return match[1] || match[0]; + } + } + return null; +} + +// ============ 导出 ============ +export function __jsEvalReturn() { + return { + init: init, + home: home, + homeVod: homeVod, + category: category, + detail: detail, + play: play, + search: search + }; +} \ No newline at end of file