# -*- coding: utf-8 -*- import re import json import hashlib import base64 import time from urllib.parse import quote from base.spider import Spider # ---------- 纯Python实现的RC4和AES(无需第三方库) ---------- def rc4_crypt(data, key): S = list(range(256)) j = 0 for i in range(256): j = (j + S[i] + key[i % len(key)]) % 256 S[i], S[j] = S[j], S[i] i = j = 0 out = bytearray() for ch in data: i = (i + 1) % 256 j = (j + S[i]) % 256 S[i], S[j] = S[j], S[i] out.append(ch ^ S[(S[i] + S[j]) % 256]) return out def aes_cbc_decrypt(data, key, iv): raise NotImplementedError("AES解密需要 pycryptodome 库") # 尝试导入官方库 try: from Crypto.Cipher import ARC4, AES from Crypto.Util.Padding import unpad def rc4_crypt(data, key): cipher = ARC4.new(key) return cipher.decrypt(data) def aes_cbc_decrypt(data, key, iv): cipher = AES.new(key.encode('utf-8'), AES.MODE_CBC, iv.encode('utf-8')) decrypted = unpad(cipher.decrypt(base64.b64decode(data)), AES.block_size) return decrypted.decode('utf-8') CRYPTO_AVAILABLE = True except ImportError: CRYPTO_AVAILABLE = False print("[歪比影视] 提示: pycryptodome未安装,播放解密可能失败") class Spider(Spider): BASE_URL = "https://wbbb1.com" PARSE_DOMAIN = "xn--qvr2v.850088.xyz" def_headers = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36", "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8", "Accept-Language": "zh-CN,zh;q=0.9", "Referer": BASE_URL + "/", "Connection": "keep-alive", } play_headers = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36", "Referer": BASE_URL + "/", "Accept": "*/*", } CATEGORY_MAP = {"1": "1", "2": "2", "3": "3", "4": "4"} CATEGORY_NAMES = {"1": "电影", "2": "剧集", "3": "动漫", "4": "综艺"} _cookies = "" # ---------- 加密工具 ---------- def _md5(self, s): return hashlib.md5(s.encode('utf-8')).hexdigest() def _rc4_encrypt(self, data, key): key_bytes = key.encode('utf-8') if isinstance(key, str) else key data_bytes = data.encode('utf-8') if isinstance(data, str) else data encrypted = rc4_crypt(data_bytes, key_bytes) return base64.b64encode(encrypted).decode('utf-8') def _rc4_decrypt(self, data, key): key_bytes = key.encode('utf-8') if isinstance(key, str) else key data_bytes = base64.b64decode(data) decrypted = rc4_crypt(data_bytes, key_bytes) return decrypted.decode('utf-8') def _aes_decrypt(self, data, key, iv): if not CRYPTO_AVAILABLE: raise Exception("AES解密需要 pycryptodome 库") return aes_cbc_decrypt(data, key, iv) # ---------- 页面请求 ---------- def _fetch_cookies(self): try: headers = {"User-Agent": self.def_headers["User-Agent"], "Accept": "text/html", "Referer": self.BASE_URL + "/"} resp = self.fetch(self.BASE_URL, headers=headers) cookie_list = [] # 尝试多种方式获取Set-Cookie if hasattr(resp, 'cookies') and resp.cookies: try: for cookie in resp.cookies: cookie_list.append(f"{cookie.name}={cookie.value}") except: pass if not cookie_list: if hasattr(resp.headers, "get_all"): raw = resp.headers.get_all("Set-Cookie") for c in raw: cookie_list.append(c.split(";")[0]) elif "Set-Cookie" in resp.headers: raw = resp.headers["Set-Cookie"] if isinstance(raw, list): for c in raw: cookie_list.append(c.split(";")[0]) else: cookie_list.append(raw.split(";")[0]) self._cookies = "; ".join(cookie_list) if self._cookies: print(f"[歪比影视] Cookie获取成功") except Exception as e: print(f"[歪比影视] Cookie获取失败: {e}") self._cookies = "" def _fetch_html(self, url): headers = self.def_headers.copy() if self._cookies: headers["Cookie"] = self._cookies try: resp = self.fetch(url, headers=headers) return resp.text except Exception as e: print(f"[歪比影视] 请求失败: {url}, {e}") # 如果失败且没有cookie,尝试重新获取cookie后重试 if not self._cookies: print("[歪比影视] 尝试重新获取Cookie...") self._fetch_cookies() if self._cookies: headers["Cookie"] = self._cookies try: resp = self.fetch(url, headers=headers) return resp.text except Exception as e2: print(f"[歪比影视] 重试失败: {e2}") return "" # ---------- 工具函数:清理HTML标签 ---------- def _clean_html(self, text): if not text: return "" text = re.sub(r'<[^>]+>', '', text) text = re.sub(r'\s+', ' ', text).strip() return text # ---------- 解析函数 ---------- def _parse_video_list(self, html): """通用解析(用于首页/分类页)""" videos = [] # 使用更宽松的正则匹配,兼容class顺序变化 pattern = r']*href="/detail/(\d+\.html)"[^>]*class="[^"]*module-poster-item[^"]*"[^>]*>.*?]*class="[^"]*module-item-note[^"]*"[^>]*>([^<]*).*?]*data-original="([^"]+)"[^>]*>.*?]*class="[^"]*module-poster-item-title[^"]*"[^>]*>([^<]*)' for match in re.finditer(pattern, html, re.DOTALL): detail_url = match.group(1) vod_id = detail_url.replace(".html", "") vod_pic = match.group(3).strip() # 处理协议相对URL if vod_pic.startswith("//"): vod_pic = "https:" + vod_pic videos.append({ "vod_id": vod_id, "vod_name": match.group(4).strip(), "vod_pic": vod_pic, "vod_remarks": match.group(2).strip(), }) return videos def _parse_search_list(self, html): """专门解析搜索页(参考JS选择器)""" videos = [] # 使用更宽松的正则匹配 items = re.finditer(r']*class="[^"]*module-card-item[^"]*"[^>]*>(.*?)\s*(?=]*class="[^"]*module-card-item|$)', html, re.DOTALL) for item in items: block = item.group(1) # 提取链接 link_match = re.search(r']*href="(/detail/[^"]+\.html)"', block) if not link_match: continue vod_id = link_match.group(1).replace("/detail/", "").replace(".html", "") # 提取标题 title_match = re.search(r']*class="[^"]*module-card-item-title[^"]*"[^>]*>.*?([^<]*)', block, re.DOTALL) vod_name = title_match.group(1).strip() if title_match else "未知" # 提取图片 pic_match = re.search(r']*data-original="([^"]+)"', block) vod_pic = pic_match.group(1) if pic_match else "" if vod_pic and vod_pic.startswith("//"): vod_pic = "https:" + vod_pic # 提取备注 note_match = re.search(r']*class="[^"]*module-item-note[^"]*"[^>]*>([^<]*)', block) vod_remarks = note_match.group(1).strip() if note_match else "" videos.append({ "vod_id": vod_id, "vod_name": vod_name, "vod_pic": vod_pic, "vod_remarks": vod_remarks, }) return videos def _parse_play_sources(self, html, vod_id): """ 提取每个线路的集数,线路名称准确提取 """ sources = [] # 1. 提取线路名称(按顺序)- 优先使用 data-dropdown-value source_names = [] for m in re.finditer(r'data-dropdown-value="([^"]+)"', html): name = m.group(1).strip() if name and name not in source_names: source_names.append(name) if not source_names: tab_box = re.search(r']*class="[^"]*module-tab-items-box[^"]*"[^>]*>(.*?)', html, re.DOTALL) if tab_box: for m in re.finditer(r']*class="[^"]*tab-item[^"]*"[^>]*>.*?]*>([^<]*)', tab_box.group(1)): name = m.group(1).strip() if name and name not in source_names: source_names.append(name) if not source_names: for m in re.finditer(r']*class="[^"]*module-tab-value"[^>]*>([^<]*)', html): name = m.group(1).strip() if name and name not in source_names: source_names.append(name) if not source_names: source_names = ["默认"] # 2. 提取所有 module-list 块 list_blocks = [] start_tag = '
' pos = 0 while True: start = html.find(start_tag, pos) if start == -1: break start += len(start_tag) depth = 0 end = None i = start while i < len(html): if html[i:i+5] == '
': if depth == 0: end = i + 6 break else: depth -= 1 i += 6 else: i += 1 if end is not None: block_html = html[start:end] if '/vplay/' in block_html: list_blocks.append(block_html) pos = end else: pos = start + 1 if not list_blocks: blocks = [] start_tag2 = '
': if depth == 0: end = i + 6 break else: depth -= 1 i += 6 else: i += 1 if end is not None: block_html = html[start:end] if '/vplay/' in block_html: list_blocks.append(block_html) pos = end else: pos = start + 1 if not list_blocks: simple = re.findall(r'
]*>(.*?)
', html, re.DOTALL) for b in simple: if '/vplay/' in b: list_blocks.append(b) # 3. 对齐名称与块数量 if len(source_names) > len(list_blocks): source_names = source_names[:len(list_blocks)] while len(source_names) < len(list_blocks): source_names.append(f"源{len(source_names)+1}") # 4. 解析每个块的集数 for idx, block in enumerate(list_blocks): eps = [] for m in re.finditer(r']*href="(/vplay/(\d+)-(\d+)-(\d+)\.html)"[^>]*>.*?([^<]*)', block): link = m.group(1) id_ = m.group(2) sid = m.group(3) nid = m.group(4) name = m.group(5).strip() eps.append({"name": name, "link": f"{id_}-{sid}-{nid}"}) if eps: sources.append({ "source_name": source_names[idx] if idx < len(source_names) else f"源{idx+1}", "episodes": eps }) if not sources: eps = [] for m in re.finditer(r']*href="(/vplay/(\d+)-(\d+)-(\d+)\.html)"[^>]*>.*?([^<]*)', html): link = m.group(1) id_ = m.group(2) sid = m.group(3) nid = m.group(4) name = m.group(5).strip() eps.append({"name": name, "link": f"{id_}-{sid}-{nid}"}) if eps: sources.append({"source_name": "默认", "episodes": eps}) return sources # ---------- 播放地址获取 ---------- def _get_play_url(self, vod_id, sid, nid): try: play_page = f"{self.BASE_URL}/vplay/{vod_id}-{sid}-{nid}.html" html = self._fetch_html(play_page) if not html: return None # 检查iframe iframe_match = re.search(r']*src="([^"]+)"', html) if iframe_match: iframe_url = iframe_match.group(1) if iframe_url.startswith('//'): iframe_url = 'https:' + iframe_url elif iframe_url.startswith('/'): iframe_url = self.BASE_URL + iframe_url print(f"[歪比影视] 发现iframe: {iframe_url}") return iframe_url # 提取加密URL - 使用更安全的大括号匹配算法,支持嵌套JSON enc_url = None m1 = re.search(r'var\s+player_aaaa\s*=\s*', html) if m1: start = m1.end() # 跳过空白字符 while start < len(html) and html[start] in ' \t\n\r': start += 1 # 找到匹配的大括号 if start < len(html) and html[start] == '{': depth = 1 i = start + 1 while i < len(html) and depth > 0: if html[i] == '{': depth += 1 elif html[i] == '}': depth -= 1 i += 1 if depth == 0: json_str = html[start:i] try: player_data = json.loads(json_str) enc_url = player_data.get("url", "") print(f"[歪比影视] 提取到player_aaaa.url: {enc_url[:50]}...") except Exception as e: print(f"[歪比影视] player_aaaa JSON解析失败: {e}") # 兜底:直接搜索url字段 if not enc_url: m2 = re.search(r'"url"\s*:\s*"([^"]+)"', html) if m2: enc_url = m2.group(1) print(f"[歪比影视] 兜底提取到url: {enc_url[:50]}...") if not enc_url: print("[歪比影视] 未能提取到加密URL") return None # 如果无加密库,返回解析页面 if not CRYPTO_AVAILABLE: fallback_url = f"https://{self.PARSE_DOMAIN}/player/?url={enc_url}&next=//&title=" print(f"[歪比影视] 无加密库,使用解析页面: {fallback_url}") return fallback_url try: domain = self.PARSE_DOMAIN l = (self._md5(enc_url) + " P")[-22:] key = l.encode('utf-8') h = self._rc4_encrypt(self._md5(enc_url + "stray"), key) timestamp = str(int(time.time())) u = self._rc4_encrypt(timestamp + self._md5(key.decode('utf-8') + "stray"), key) y = self._rc4_encrypt(self._md5(domain + "stray"), key) api_url = f"https://{domain}/player/api.php" headers = { "User-Agent": self.play_headers["User-Agent"], "Accept": "application/json, text/javascript, */*; q=0.01", "Origin": f"https://{domain}", "Referer": f"https://{domain}/player/?url={enc_url}", "X-Requested-With": "XMLHttpRequest", "Content-Type": "application/x-www-form-urlencoded" } if self._cookies: headers["Cookie"] = self._cookies post_data = {"url": enc_url, "key": h, "vkey": u, "ckey": y} print(f"[歪比影视] 请求API: {api_url}") resp = self.post(api_url, data=post_data, headers=headers) result = json.loads(resp.text) print(f"[歪比影视] API返回: code={result.get('code')}") if result.get("code") == 200: aes_key = self._rc4_decrypt(result["aes_key"], key) aes_iv = self._rc4_decrypt(result["aes_iv"], key) enc_play = result["url"] play_url = self._aes_decrypt(enc_play, aes_key, aes_iv) print(f"[歪比影视] 解密成功: {play_url[:80]}...") return play_url else: fallback_url = f"https://{domain}/player/?url={enc_url}&next=//&title=" print(f"[歪比影视] API返回非200,使用解析页面: {fallback_url}") return fallback_url except Exception as e: print(f"[歪比影视] 解密异常: {e}") fallback_url = f"https://{self.PARSE_DOMAIN}/player/?url={enc_url}&next=//&title=" return fallback_url except Exception as e: print(f"[歪比影视] 获取播放地址异常: {e}") return None # ---------- TVBox接口 ---------- def init(self, extend=''): self._fetch_cookies() print("[歪比影视] 初始化完成") def homeContent(self, filter): return {"class": [{"type_id": tid, "type_name": name} for tid, name in self.CATEGORY_NAMES.items()]} def homeVideoContent(self): try: html = self._fetch_html(self.BASE_URL) if not html: return {"list": []} # 尝试匹配"正在热映" block = re.search(r'
.*?]*class="[^"]*module-title[^"]*"[^>]*>正在热映.*?
(.*?)
\s*
', html, re.DOTALL) if not block: # 兜底:尝试匹配第一个module块 block = re.search(r'
(.*?)
\s*
', html, re.DOTALL) if not block: return {"list": []} videos = self._parse_video_list(block.group(1)) return {"list": videos[:20]} except Exception as e: print(f"[歪比影视] homeVideoContent 异常: {e}") return {"list": []} def categoryContent(self, tid, pg, filter, extend): try: pg = int(pg) if tid not in self.CATEGORY_MAP: return {"list": [], "pagecount": 1, "page": pg} if pg == 1: url = f"{self.BASE_URL}/show/{tid}-----------.html" else: url = f"{self.BASE_URL}/show/{tid}--------{pg}---.html" print(f"[歪比影视] 分类请求: {url}") html = self._fetch_html(url) if not html: return {"list": [], "pagecount": 1, "page": pg} videos = self._parse_video_list(html) last = re.search(r']*href="/show/\d+--------(\d+)---\.html"[^>]*>尾页', html) pagecount = int(last.group(1)) if last else 1 return {"list": videos, "pagecount": pagecount, "page": pg} except Exception as e: print(f"[歪比影视] categoryContent 异常: {e}") return {"list": [], "pagecount": 1, "page": pg} def searchContent(self, key, quick, pg='1'): try: pg = int(pg) # 对搜索关键词进行URL编码,修复中文搜索失败 encoded_key = quote(key) url = f"{self.BASE_URL}/search/{encoded_key}-------------.html" print(f"[歪比影视] 搜索请求: {url}") html = self._fetch_html(url) if not html: return {"list": [], "page": pg} videos = self._parse_search_list(html) return {"list": videos, "page": pg} except Exception as e: print(f"[歪比影视] searchContent 异常: {e}") return {"list": [], "page": pg} def detailContent(self, ids): try: vod_id = ids[0] url = f"{self.BASE_URL}/detail/{vod_id}.html" print(f"[歪比影视] 详情请求: {url}") html = self._fetch_html(url) if not html: return {"list": []} title = re.search(r'

([^<]*)

', html) vod_name = title.group(1).strip() if title else "未知" # 更宽松的图片匹配,兼容class变化 pic = re.search(r']*class="[^"]*module-item-pic[^"]*"[^>]*>.*?]*data-original="([^"]+)"', html, re.DOTALL) vod_pic = pic.group(1) if pic else "" if vod_pic.startswith("//"): vod_pic = "https:" + vod_pic desc = re.search(r']*class="[^"]*module-info-introduction-content[^"]*"[^>]*>(.*?)
', html, re.DOTALL) vod_content = self._clean_html(desc.group(1)) if desc else "" actor = re.search(r'主演:.*?]*class="[^"]*module-info-item-content[^"]*"[^>]*>(.*?)
', html, re.DOTALL) vod_actor = self._clean_html(actor.group(1)) if actor else "" director = re.search(r'导演:.*?]*class="[^"]*module-info-item-content[^"]*"[^>]*>(.*?)
', html, re.DOTALL) vod_director = self._clean_html(director.group(1)) if director else "" year = re.search(r']*title="(\d{4})"', html) vod_year = year.group(1) if year else "" sources = self._parse_play_sources(html, vod_id) if not sources: print(f"[歪比影视] 未能解析到播放源") return {"list": []} from_list = [] url_list = [] for src in sources: from_list.append(src["source_name"]) eps_str = "#".join([f"{ep['name']}${ep['link']}" for ep in src["episodes"]]) url_list.append(eps_str) vod_play_from = "$$$".join(from_list) vod_play_url = "$$$".join(url_list) video = { "vod_id": vod_id, "vod_name": vod_name, "vod_pic": vod_pic, "vod_year": vod_year, "vod_area": "", "vod_actor": vod_actor, "vod_director": vod_director, "vod_content": vod_content, "vod_play_from": vod_play_from, "vod_play_url": vod_play_url, } print(f"[歪比影视] 详情解析成功: {vod_name}, 线路: {vod_play_from}") return {"list": [video]} except Exception as e: print(f"[歪比影视] detailContent 异常: {e}") return {"list": []} def playerContent(self, flag, vid, vip_flags): try: parts = vid.split("-") if len(parts) != 3: return {"jx": 0, "parse": 0, "url": "", "header": ""} vod_id, sid, nid = parts play_url = self._get_play_url(vod_id, sid, nid) if play_url: # 判断URL类型,决定parse标志 # 如果是直接的视频文件或m3u8,parse=0;否则parse=1(需要TVBox嗅探/解析) is_direct = any(ext in play_url.lower() for ext in ['.m3u8', '.mp4', '.flv', '.ts', '.mkv', '.avi', '.mov', '.wmv']) is_direct = is_direct or 'm3u8' in play_url.lower() or 'mp4' in play_url.lower() parse_flag = 0 if is_direct else 1 # header必须是JSON字符串 header_str = json.dumps(self.play_headers) print(f"[歪比影视] 播放URL: {play_url[:80]}..., parse={parse_flag}") return {"jx": 0, "parse": parse_flag, "url": play_url, "header": header_str} return {"jx": 0, "parse": 0, "url": "", "header": ""} except Exception as e: print(f"[歪比影视] playerContent 异常: {e}") return {"jx": 0, "parse": 0, "url": "", "header": ""} def getName(self): return "歪比影视" def isVideoFormat(self, url): pass def manualVideoCheck(self): pass def destroy(self): pass def localProxy(self, param): pass