From 3907dc932f624f86149001746b7d0fe623402fb7 Mon Sep 17 00:00:00 2001 From: qist <87984115@qq.com> Date: Mon, 11 May 2026 15:50:43 +0800 Subject: [PATCH] =?UTF-8?q?=E7=94=A8py=20=E7=9C=8B=E7=9C=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- py/ITalkBBTV.py | 293 +++++++++++++++++++++++++++++++++++++++++++++ tools/dianshi.json | 2 +- tools/jsm.json | 2 +- 3 files changed, 295 insertions(+), 2 deletions(-) create mode 100644 py/ITalkBBTV.py diff --git a/py/ITalkBBTV.py b/py/ITalkBBTV.py new file mode 100644 index 00000000..ce3ae756 --- /dev/null +++ b/py/ITalkBBTV.py @@ -0,0 +1,293 @@ +# -*- coding: utf-8 -*- +# by @Qist +""" +ITalkBB TV - 海外华人影视 +""" +import re +import json +import subprocess +import requests +from base.spider import Spider + + +class Spider(Spider): + def getName(self): + return 'ITalkBB TV' + + def init(self, extend=""): + pass + + def __init__(self): + self.host = 'https://www.italkbbtv.com' + self.header = { + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36 Edg/136.0.0.0', + 'Referer': 'https://www.italkbbtv.com/' + } + self.timeout = 20 + + self.class_names = '电视剧&电影&综艺&动画'.split('&') + self.class_urls = 'drama/62c670dc1dca2d424404499c&movie/62ac4ef36e0b5a13ed291544&variety/62ce7417c7daaa4a5d3fea14&cartoon/62ac4e6e4beefe53586478ca'.split('&') + + self.key_map = { + 'drama': 'dramaSeriesLists', + 'movie': 'movieSeriesLists', + 'variety': 'varietySeriesLists', + 'cartoon': 'cartoonSeriesLists', + 'shorts': 'shortsSeriesLists' + } + + def get_nuxt_data(self, html): + """从HTML中提取并解析__NUXT__数据""" + m = re.search(r'window\.__NUXT__=([\s\S]*?);', html) + if not m: + return None + js_expr = m.group(1) + try: + result = subprocess.run( + ['node', '-e', f'process.stdout.write(JSON.stringify({js_expr}))'], + capture_output=True, timeout=15 + ) + if result.returncode == 0 and result.stdout: + return json.loads(result.stdout) + except Exception as e: + print(f"get_nuxt_data error: {e}") + return None + + def pick_pic(self, obj): + """提取图片URL""" + if not obj: + return '' + images = obj.get('images', {}) or {} + poster = (images.get('poster') or [''])[0] if images.get('poster') else '' + landscape = (images.get('landscape') or [''])[0] if images.get('landscape') else '' + return poster or landscape or obj.get('imgUrl', '') + + def join_stars(self, stars): + """拼接演员/导演名""" + if not isinstance(stars, list): + return '' + return '/'.join([s.get('name', '') for s in stars if s.get('name')]) + + def make_vod_from_series(self, series, fallback_name=''): + """从series对象构建vod字典""" + if not series: + return None + category_id = series.get('category_id', []) + if isinstance(category_id, list): + category_id = category_id[0] if category_id else '' + root_id = series.get('root_id', '') + series_id = series.get('id', '') or series.get('series_id', '') + route = 'shortsPlay' if root_id == '66b1d25cf2dde82c215f9b59' else 'play' + type_name = series.get('rootName', '') or series.get('categoryName', '') + remark = (series.get('latest_episode_name', '') or + series.get('latest_episode_shortname', '') or + ('更新至' + str(series.get('episode_count', ''))) if series.get('episode_count') else '') + released_at = series.get('released_at') + vod_year = '' + if released_at: + try: + from datetime import datetime + vod_year = str(datetime.fromtimestamp(released_at).year) + except: + pass + stars = series.get('stars', {}) or {} + return { + 'vod_id': route + '$' + series_id, + 'vod_name': series.get('name', '') or fallback_name, + 'vod_pic': self.pick_pic(series), + 'vod_remarks': remark, + 'vod_year': vod_year, + 'type_name': type_name, + 'vod_content': series.get('description', ''), + 'vod_actor': self.join_stars(stars.get('actor', []) or []), + 'vod_director': self.join_stars(stars.get('director', []) or []) + } + + def make_vod_from_card(self, card): + """从card对象构建vod字典(搜索用)""" + if not card: + return None + series = card.get('series') or card.get('target') or card + fallback = card.get('name', '') or card.get('title', '') + vod = self.make_vod_from_series(series, fallback) + if not vod: + return None + if not vod['vod_name']: + vod['vod_name'] = fallback + if not vod['vod_pic']: + vod['vod_pic'] = (self.pick_pic(card) or + ((card.get('image', {}) or {}).get('poster', '')) or + ((card.get('image', {}) or {}).get('landscape', ''))) + if not vod['vod_remarks']: + vod['vod_remarks'] = card.get('description', '') + return vod + + def unique_by_id(self, vod_list): + """按vod_id去重""" + seen = {} + result = [] + for v in vod_list: + vid = v.get('vod_id', '') if v else '' + if vid and vid not in seen: + seen[vid] = 1 + result.append(v) + return result + + def fetch(self, url): + """发送GET请求""" + try: + resp = requests.get(url, headers=self.header, timeout=self.timeout) + resp.encoding = 'utf-8' + return resp.text + except Exception as e: + print(f"fetch error: {e}") + return '' + + def homeContent(self, filter): + """首页推荐""" + result = {'class': [], 'list': []} + for name, cid in zip(self.class_names, self.class_urls): + result['class'].append({'type_name': name, 'type_id': cid}) + + # 默认加载电视剧列表 + url = f"{self.host}/drama/62c670dc1dca2d424404499c" + html = self.fetch(url) + nuxt = self.get_nuxt_data(html) + if nuxt: + try: + store = nuxt.get('state', {}).get('pageList', {}).get('dramaSeriesLists', {}) + data = store.get('62c670dc1dca2d424404499c', {}) + series_list = data.get('series', []) + vods = [] + for s in series_list: + v = self.make_vod_from_series(s) + if v: + vods.append(v) + result['list'] = self.unique_by_id(vods) + except Exception as e: + print(f"homeContent parse error: {e}") + return result + + def homeVideoContent(self): + """首页视频内容""" + return {} + + def categoryContent(self, tid, pg, filter, extend): + """分类内容""" + result = {'list': [], 'page': int(pg), 'pagecount': 999, 'limit': 24, 'total': 999999} + parts = tid.split('/') + alias = parts[0] + cid = parts[1] if len(parts) > 1 else '' + url = f"{self.host}/{tid}" + if int(pg) > 1: + url += f"?page={pg}" + html = self.fetch(url) + nuxt = self.get_nuxt_data(html) + if nuxt: + try: + store = nuxt.get('state', {}).get('pageList', {}).get(self.key_map.get(alias, ''), {}) + data = store.get(cid, {}) + series_list = data.get('series', []) + vods = [] + for s in series_list: + v = self.make_vod_from_series(s) + if v: + vods.append(v) + result['list'] = self.unique_by_id(vods) + except Exception as e: + print(f"categoryContent parse error: {e}") + return result + + def detailContent(self, ids): + """详情页""" + if not ids or not ids[0]: + return {'list': []} + vid = ids[0] + parts = vid.split('$') + route = parts[0] if len(parts) > 1 else 'play' + sid = parts[1] if len(parts) > 1 else vid + url = f"{self.host}/{route}/{sid}" + html = self.fetch(url) + nuxt = self.get_nuxt_data(html) + if not nuxt: + return {'list': []} + try: + play = nuxt.get('state', {}).get('play', {}) + info = play.get('SeriesInfo', {}) + eps = play.get('EpisodeList', []) + vod = self.make_vod_from_series(info) or {'vod_id': vid} + tabs = 'ITalkBB短剧' if route == 'shortsPlay' else 'ITalkBB' + play_urls = [] + for ep in eps: + name = ep.get('shortname', '') or ep.get('name', '') + ep_id = ep.get('id', '') + play_urls.append(f"{name}${route}@{sid}@{ep_id}") + vod['vod_id'] = vid + vod['vod_name'] = info.get('name', '') or vod.get('vod_name', '') + vod['vod_pic'] = self.pick_pic(info) or vod.get('vod_pic', '') + vod['type_name'] = info.get('rootName', '') or info.get('categoryName', '') or vod.get('type_name', '') + vod['vod_content'] = info.get('description', '') or vod.get('vod_content', '') + stars = info.get('stars', {}) or {} + vod['vod_actor'] = self.join_stars(stars.get('actor', []) or []) + vod['vod_director'] = self.join_stars(stars.get('director', []) or []) + vod['vod_remarks'] = (info.get('latest_episode_name', '') or + info.get('latest_episode_shortname', '') or + vod.get('vod_remarks', '')) + vod['vod_play_from'] = tabs + vod['vod_play_url'] = '#'.join(play_urls) + return {'list': [vod]} + except Exception as e: + print(f"detailContent parse error: {e}") + return {'list': []} + + def searchContent(self, key, quick, pg="1"): + """搜索""" + url = f"{self.host}/?keyword={key}" + html = self.fetch(url) + nuxt = self.get_nuxt_data(html) + vod_list = [] + if nuxt: + try: + data = nuxt.get('data', [{}])[0] + banners = data.get('bannerData', []) or [] + groups = data.get('serverGroupDataList', []) or [] + for card in banners: + v = self.make_vod_from_card(card) + if v: + vod_list.append(v) + for g in groups: + for card in (g.get('list', []) or []): + v = self.make_vod_from_card(card) + if v: + vod_list.append(v) + except Exception as e: + print(f"searchContent parse error: {e}") + # 客户端过滤 + filtered = [] + for v in vod_list: + if not v or not v.get('vod_name'): + continue + text = ' '.join([v.get('vod_name', ''), v.get('vod_remarks', ''), + v.get('vod_content', ''), v.get('type_name', '')]) + if key in text: + filtered.append(v) + return {'list': self.unique_by_id(filtered)} + + def playerContent(self, flag, id, vipFlags): + """播放""" + parts = id.split('@') + route = parts[0] if len(parts) > 1 else 'play' + sid = parts[1] if len(parts) > 1 else '' + eid = parts[2] if len(parts) > 2 else '' + url = f"{self.host}/{route}/{sid}" + if eid: + url += f"?ep={eid}" + return { + 'jx': 0, + 'parse': 1, + 'url': url, + 'header': self.header + } + + def localProxy(self, param): + return None diff --git a/tools/dianshi.json b/tools/dianshi.json index 57e3ff5a..dc3c9b2f 100644 --- a/tools/dianshi.json +++ b/tools/dianshi.json @@ -14,7 +14,7 @@ {"key":"闪雷","name":"闪雷┃MP4","type":3,"api":"./lib/drpy2.min.js","ext":"./js/678.js","header":{"User-Agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36 Edg/136.0.0.0"}}, {"key":"fyyy","name":"飞宇影院","type":3,"api":"csp_XBPQ","searchable":1,"quickSearch":1,"filterable":1,"changeable":1,"ext":{"分类url":"http://ntfeiyu.com/nt/{cateId}/area/{area}/by/{by}/class/{class}/lang/{lang}/page/{catePg}/year/{year}.html","分类":"电影$1#电视剧$2#综艺$3#动漫$4"},"header":{"User-Agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36 Edg/136.0.0.0"}}, {"key":"cupfox_in","name":"茶杯狐┃cupfox.in","type":3,"api":"./lib/drpy2.min.js","ext":"./js/茶杯狐.js","searchable":1,"quickSearch":1,"filterable":0,"changeable":1,"header":{"User-Agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36 Edg/136.0.0.0"}}, -{"key":"italkbbtv","name":"ITalkBB TV","type":3,"api":"./lib/drpy2.min.js","ext":"./js/ITalkBBTV.js","searchable":1,"quickSearch":1,"filterable":0,"changeable":1,"header":{"User-Agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36 Edg/136.0.0.0"}}, +{"key":"italkbbtv","name":"ITalkBB TV","type":3,"api":"./py/ITalkBBTV.py","searchable":1,"quickSearch":1,"filterable":0,"changeable":1}, {"key":"qiletv","name":"奇乐影视","type":3,"api":"csp_XBPQ","searchable":1,"quickSearch":1,"filterable":1,"changeable":1,"ext":"./json/奇乐.json","header":{"User-Agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36 Edg/136.0.0.0"}}, diff --git a/tools/jsm.json b/tools/jsm.json index c5493782..7724885b 100644 --- a/tools/jsm.json +++ b/tools/jsm.json @@ -15,7 +15,7 @@ {"key":"新闪雷","name":"新闪雷┃MP4","type":3,"api":"./lib/drpy2.min.js","ext":"./js/新闪雷.js","header":{"User-Agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36 Edg/136.0.0.0"}}, {"key":"fyyy","name":"飞宇影院","type":3,"api":"csp_XBPQ","searchable":1,"quickSearch":1,"filterable":1,"changeable":1,"ext":{"分类url":"http://ntfeiyu.com/nt/{cateId}/area/{area}/by/{by}/class/{class}/lang/{lang}/page/{catePg}/year/{year}.html","分类":"电影$1#电视剧$2#综艺$3#动漫$4"},"header":{"User-Agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36 Edg/136.0.0.0"}}, {"key":"cupfox_in","name":"茶杯狐┃cupfox.in","type":3,"api":"./lib/drpy2.min.js","ext":"./js/茶杯狐.js","searchable":1,"quickSearch":1,"filterable":0,"changeable":1,"header":{"User-Agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36 Edg/136.0.0.0"}}, -{"key":"italkbbtv","name":"ITalkBB TV","type":3,"api":"./lib/drpy2.min.js","ext":"./js/ITalkBBTV.js","searchable":1,"quickSearch":1,"filterable":0,"changeable":1,"header":{"User-Agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36 Edg/136.0.0.0"}}, +{"key":"italkbbtv","name":"ITalkBB TV","type":3,"api":"./py/ITalkBBTV.py","searchable":1,"quickSearch":1,"filterable":0,"changeable":1}, {"key":"qiletv","name":"奇乐影视","type":3,"api":"csp_XBPQ","searchable":1,"quickSearch":1,"filterable":1,"changeable":1,"ext":"./json/奇乐.json","header":{"User-Agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36 Edg/136.0.0.0"}}, {"key": "电影天堂","name": "影视 | 电影天堂","type": 1,"api": "http://caiji.dyttzyapi.com/api.php/provide/vod","searchable": 1,"quickSearch": 1,"filterable": 1,"changeable": 1,"playerType":2},