import re, sys, json, base64 from Crypto.Cipher import AES from urllib.parse import quote from Crypto.Util.Padding import unpad from base.spider import Spider sys.path.append('..') class Spider(Spider): headers = {'User-Agent': 'okhttp/4.12.0'} FIXED_CONFIG = { 'host': 'http://pgcms.lyyytv.cn', 'cmskey': 'wP5btxoc3yv8FoBQENFZumF0EUYr4LTy' } def init(self, extend=''): self.host = self.FIXED_CONFIG['host'] self.cmskey = self.FIXED_CONFIG.get('cmskey', '') # ============ 原来的解析器(虚联、虚移、虚翼使用这个)============ self.external_parser = "http://yoyo.ovg.sh/yt/ttjx.php?url=" # ============ 新增解析器(虚腾、虚果、虚爱、虚酷、虚哔、虚哩使用这个)============ self.need_parse_lines = ["藤藤", "茫茫", "小爱", "小优", "小哩"] # 新增解析源列表(按优先级排序) self.new_parsers = [ "http://nm.4688888.xyz/jiexi.php?data=093207528aec95afa4616e7afb8e2649&url=", # 牛马解析 "http://120.46.190.255//jiexi.php?data=f603bb5f52a0cc1db4064599c3cc9abf&url=", # 小白解析 "http://8.155.50.80/xiayexk.php?url=", # 夏夜解析 "https://qsy.wya6.cn/parse.php?url=", # 无意云解析 "http://zhuimi.摸鱼儿.com/moyu/zhuimi?token=uZBC6q6w&url=", # 摸鱼解析 "http://yoyo168.zabc.net/getjx.php?host=http://154.222.26.58:7788&key=guodan2004031600&api=6d735c167eeda72b836cf382e4863f3f&v=", # 呦呦解析 "https://cachem3u8.2s0.cn:8899/Th_JsonApi.php?url=", #擦车解析 "http://mg.itufm.top/mg.php?url=",#芒果解析 "https://jx.xmflv.com/?url=", # 虾米嗅探 "https://pp1301239612013983305599.tai2tai.sbs/?url=", # 太乙嗅探 "https://jx.m3u8.tv/jiexi/?url=" # m3u8解析 ] self.direct_sniff_keys = [ "tai2tai.sbs", "xmflv.com", "m3u8.tv" ] # ============ 域名替换规则 ============ self.domain_replace_rules = { } # ============ 线路配置开始 ============ self.line_name_mapping = { "移动云": "移不动", "天翼云": "天意也", "联通云": "联不通", "蓝奏云": "拦着揍", "腾讯": "藤藤", "芒果": "茫茫", "爱奇艺": "小爱", "优酷": "小优", "BL线路": "小哩" } self.line_order = [ "移不动", "天意也", "联不通", "拦着揍", "藤藤", "茫茫", "小爱", "小优", "小哩" ] # ============ 新增:线路特定的headers ============ self.line_headers = { "藤藤": { # 腾讯视频 'User-Agent': 'okhttp/4.12.0', 'Referer': 'https://v.qq.com' }, "茫茫": { # 芒果TV 'User-Agent': 'okhttp/4.12.0', 'Referer': '' }, "小爱": { # 爱奇艺 'User-Agent': 'okhttp/4.12.0', 'Referer': 'https://www.iqiyi.com' }, "小优": { # 优酷 'User-Agent': 'okhttp/4.12.0', 'Referer': 'https://v.youku.com' }, "小哩": { # Bilibili需要Referer 'User-Agent': 'okhttp/4.12.0', 'Referer': 'https://www.bilibili.com' } } def homeVideoContent(self): data = self.fetch(f"{self.host}/api.php/app/index_video?token=", headers=self.headers).json() videos = [] for item in data['list']: videos.extend(item['vlist']) return {'list': videos} def homeContent(self, filter): data = self.fetch(f"{self.host}/api.php/app/nav?token=", headers=self.headers).json() keys = ["class", "area", "lang", "year", "letter", "by", "sort"] filters = {} classes = [] for item in data['list']: has_non_empty_field = False jsontype_extend = item["type_extend"] classes.append({"type_name": item["type_name"], "type_id": item["type_id"]}) for key in keys: if key in jsontype_extend and jsontype_extend[key].strip() != "": has_non_empty_field = True break if has_non_empty_field: filters[str(item["type_id"])] = [] for dkey in jsontype_extend: if dkey in keys and jsontype_extend[dkey].strip() != "": values = jsontype_extend[dkey].split(",") value_array = [] for value in values: if value.strip() != "": value_array.append({"n": value.strip(), "v": value.strip()}) filters[str(item["type_id"])].append({"key": dkey, "name": dkey, "value": value_array}) return {"class": classes, "filters": filters} def categoryContent(self, tid, pg, filter, extend): params = { 'tid': tid, 'class': extend.get('class', ''), 'area': extend.get('area', ''), 'lang': extend.get('lang', ''), 'year': extend.get('year', ''), 'limit': '18', 'pg': pg } data = self.fetch(f"{self.host}/api.php/app/video", params=params, headers=self.headers).json() return data def searchContent(self, key, quick, pg="1"): data = self.fetch(f"{self.host}/api.php/app/search?text={key}&pg={pg}", headers=self.headers).json() videos = data['list'] for item in data['list']: item.pop('type', None) return {'list': videos, 'page': pg} def detailContent(self, ids): data = self.fetch(f"{self.host}/api.php/app/video_detail?id={ids[0]}", headers=self.headers).json()['data'] show, paly_urls = [], [] for i in data['vod_url_with_player']: original_name = i['name'].strip() if original_name in self.line_name_mapping and self.line_name_mapping[original_name] == "": continue display_name = self.line_name_mapping.get(original_name, original_name) urls = i['url'].split('#') urls2 = [] for j in urls: if j: if j.startswith('lvdou+'): j = self.lvdou(j) j = self.replace_domains(j) url = j.split('$', 1) if len(url) == 2: urls2.append(f"{url[0]}${url[1]}") else: urls2.append(j) paly_urls.append('#'.join(urls2)) show.append(display_name) data.pop('vod_url_with_player') sorted_show, sorted_paly_urls = self.sort_play_lines(show, paly_urls) data['vod_play_from'] = '$$$'.join(sorted_show) data['vod_play_url'] = '$$$'.join(sorted_paly_urls) return {'list': [data]} def playerContent(self, flag, video_id, vipFlags): print(f"=== playerContent 被调用 ===") print(f"线路标识: {flag}") print(f"原始播放地址: {video_id}") # 获取该线路特定的headers line_header = self.line_headers.get(flag, self.headers) print(f"使用headers: {line_header}") if video_id.startswith('lvdou+'): decrypted_url = self.lvdou(video_id) else: decrypted_url = video_id decrypted_url = self.replace_domains(decrypted_url) print(f"解密/替换后地址: {decrypted_url}") if decrypted_url.endswith('.m3u8') or '.m3u8?' in decrypted_url or '#EXTM3U' in decrypted_url: print(f"✅ 已经是m3u8直链,直连播放") return { 'parse': 0, 'jx': 0, 'url': decrypted_url, 'header': line_header } if flag in self.need_parse_lines: print(f"线路 {flag} 需要解析") return self._try_parsers(decrypted_url, self.new_parsers, flag) elif re.search(r'(?:www\.iqiyi|v\.qq|v\.youku|www\.mgtv|www\.bilibili)\.com', decrypted_url): print(f"视频网站链接,使用解析器") return self._try_parsers(decrypted_url, self.new_parsers, flag) else: print(f"线路 {flag} 使用external_parser直接解析") return self._parse_external_direct(decrypted_url, flag) def replace_domains(self, url): if not url: return url for old_domain, new_domain in self.domain_replace_rules.items(): if old_domain in url: url = url.replace(old_domain, new_domain) return url def _parse_external_direct(self, video_url, flag=None): print(f"使用external_parser直接解析") # 获取该线路特定的headers line_header = self.line_headers.get(flag, self.headers) if flag else self.headers try: parsed_url = f"{self.external_parser}{quote(video_url)}" print(f"解析地址: {parsed_url[:150]}...") response = self.fetch(parsed_url, allow_redirects=True, timeout=10, headers=line_header) if response.status_code != 200: print(f"解析失败,状态码: {response.status_code}") return { 'parse': 1, 'jx': 1, 'url': video_url, 'header': line_header } content = response.text if content.strip().startswith('{'): try: json_response = response.json() print(f"解析器返回JSON格式") possible_fields = ['url', 'm3u8', 'play_url', 'video_url', 'src', 'data'] for field in possible_fields: if field in json_response: if isinstance(json_response[field], str): video_url_result = json_response[field] print(f"从字段 {field} 提取到地址: {video_url_result[:100]}") video_url_result = self.replace_domains(video_url_result) return { 'parse': 0, 'jx': 0, 'url': video_url_result, 'header': line_header } elif isinstance(json_response[field], dict): for sub_field in possible_fields: if sub_field in json_response[field] and isinstance(json_response[field][sub_field], str): video_url_result = json_response[field][sub_field] print(f"从嵌套字段 {field}.{sub_field} 提取到地址: {video_url_result[:100]}") video_url_result = self.replace_domains(video_url_result) return { 'parse': 0, 'jx': 0, 'url': video_url_result, 'header': line_header } except Exception as e: print(f"JSON解析失败: {e}") if '#EXTM3U' in content: print(f"解析器返回m3u8内容") return { 'parse': 0, 'jx': 0, 'url': parsed_url, 'header': line_header } m3u8_matches = re.findall(r'https?://[^\s<>"\']+\.(?:m3u8|mp4)[^\s<>"\']*', content) if m3u8_matches: video_url_result = m3u8_matches[0] print(f"从页面提取到视频地址: {video_url_result[:100]}") video_url_result = self.replace_domains(video_url_result) return { 'parse': 0, 'jx': 0, 'url': video_url_result, 'header': line_header } print(f"未找到视频地址,返回原链让壳子解析") return { 'parse': 1, 'jx': 1, 'url': video_url, 'header': line_header } except Exception as e: print(f"解析器异常: {e}") return { 'parse': 1, 'jx': 1, 'url': video_url, 'header': line_header } def _try_parsers(self, video_url, parsers, flag=None): """ 核心修复:尝试所有解析器,直到找到可用的 只有两种情况下才返回:1.找到视频地址 2.所有解析器都试过了 """ print(f"尝试 {len(parsers)} 个解析器") # 获取该线路特定的headers line_header = self.line_headers.get(flag, self.headers) if flag else self.headers # 按优先级尝试所有解析器 for i, parser in enumerate(parsers): try: parsed_url = f"{parser}{quote(video_url)}" print(f"\n[{i+1}/{len(parsers)}] 尝试解析器: {parser}") print(f"解析地址: {parsed_url[:100]}...") # 检查是否是嗅探解析器 is_direct_sniff = any(key in parser for key in self.direct_sniff_keys) if is_direct_sniff: # 嗅探解析器:返回给壳子处理 print(f"✅ 嗅探解析器,返回给壳子处理 (parse=1)") return { 'parse': 1, 'url': parsed_url, 'header': line_header } # 普通解析器:尝试请求获取视频地址 print(f"普通解析器,尝试获取视频地址...") response = self.fetch(parsed_url, timeout=10, allow_redirects=True, headers=line_header) if response.status_code != 200: print(f"✗ 解析器返回状态码: {response.status_code}") continue # 继续下一个解析器 content = response.text # 检查是否是测试视频 if self._is_test_video(content): print(f"✗ 解析器返回测试视频") continue # 继续下一个解析器 # 1. 检查是否是m3u8内容 if '#EXTM3U' in content: print(f"✓ 解析器返回m3u8内容") # 检查是否可以直接播放 if re.search(r'\.m3u8', content) or 'video/' in response.headers.get('Content-Type', ''): return { 'parse': 0, 'jx': 0, 'url': parsed_url, 'header': line_header } else: # 需要进一步处理 return { 'parse': 1, 'jx': 0, 'url': parsed_url, 'header': line_header } # 2. 检查JSON格式 if content.strip().startswith('{'): try: json_data = json.loads(content) video_url_from_json = self._extract_video_url_from_json(json_data) if video_url_from_json: video_url_from_json = self.replace_domains(video_url_from_json) print(f"✓ 从JSON提取到视频地址: {video_url_from_json[:100]}") return { 'parse': 0, 'jx': 0, 'url': video_url_from_json, 'header': line_header } except Exception as e: print(f"JSON解析失败: {e}") # JSON解析失败,继续下一个解析器 continue # 3. 从HTML中提取视频地址 video_patterns = [ r'src=["\']([^"\']+\.(?:m3u8|mp4)[^"\']*)["\']', r'url["\']?\s*:\s*["\']([^"\']+\.(?:m3u8|mp4)[^"\']*)["\']', r'video["\']?\s*:\s*["\']([^"\']+\.(?:m3u8|mp4)[^"\']*)["\']', r'file["\']?\s*:\s*["\']([^"\']+\.(?:m3u8|mp4)[^"\']*)["\']', ] for pattern in video_patterns: matches = re.findall(pattern, content, re.IGNORECASE) if matches: for match in matches: if match.startswith('http'): video_url_result = match video_url_result = self.replace_domains(video_url_result) print(f"✓ 从HTML提取到视频地址: {video_url_result[:100]}") return { 'parse': 0, 'jx': 0, 'url': video_url_result, 'header': line_header } # 4. 直接查找.m3u8或.mp4链接 direct_matches = re.findall(r'https?://[^\s<>"\']+\.(?:m3u8|mp4)[^\s<>"\']*', content) if direct_matches: video_url_result = direct_matches[0] video_url_result = self.replace_domains(video_url_result) print(f"✓ 直接提取到视频地址: {video_url_result[:100]}") return { 'parse': 0, 'jx': 0, 'url': video_url_result, 'header': line_header } # 5. 如果没有找到视频地址,继续尝试下一个解析器 print(f"✗ 未找到视频地址,继续尝试下一个解析器") continue # 关键修复:这里用continue而不是return except Exception as e: print(f"✗ 解析器失败: {str(e)[:100]}") continue # 继续下一个解析器 # 所有解析器都失败,才返回原链让壳子处理 print(f"✗ 所有 {len(parsers)} 个解析器都尝试过了,全部失败") print(f"返回原链让壳子处理") return { 'parse': 1, 'jx': 1, 'url': video_url, 'header': line_header } def _extract_video_url_from_json(self, json_data): if not json_data: return None possible_fields = ['url', 'm3u8', 'play_url', 'video_url', 'src', 'link'] for field in possible_fields: if field in json_data and json_data[field] and isinstance(json_data[field], str): return json_data[field] if 'data' in json_data and isinstance(json_data['data'], dict): for field in possible_fields: if field in json_data['data'] and json_data['data'][field] and isinstance(json_data['data'][field], str): return json_data['data'][field] return None def _is_test_video(self, text): if not text: return False text_lower = text.lower() test_patterns = [ 'oplist.wya6.cn', 'web.wya6.com', 'k0udeyaayccydz.djvod.ndcimgs.com', 'pan.baidu.re', 'www.panurl.cn', 'test.mp4', 'demo.mp4', 'sample.mp4', 'short.mp4', 'preview.mp4', 'trailer.mp4', 'ad.mp4', '广告.mp4', '/10秒.mp4', '/15秒.mp4', '/30秒.mp4', '/10s.mp4', '/15s.mp4', '/30s.mp4', 'error.html', '404.mp4', 'error.mp4', ] for pattern in test_patterns: if pattern in text_lower: print(f"检测到测试视频特征: {pattern}") return True return False def lvdou(self, text): key = self.cmskey[:16].encode("utf-8") iv = self.cmskey[-16:].encode("utf-8") original_text = text url_prefix = "lvdou+" if original_text.startswith(url_prefix): ciphertext_b64 = original_text[len(url_prefix):] try: cipher = AES.new(key, AES.MODE_CBC, iv) ct_bytes = base64.b64decode(ciphertext_b64) pt_bytes = cipher.decrypt(ct_bytes) return unpad(pt_bytes, AES.block_size).decode('utf-8') except Exception: return original_text else: return original_text def sort_play_lines(self, show_list, url_list): if len(show_list) != len(url_list): return show_list, url_list original_order = {} for idx, name in enumerate(show_list): original_order[name] = idx line_map = {} for name, url in zip(show_list, url_list): line_map[name] = url sorted_show = [] sorted_urls = [] for ordered_name in self.line_order: if ordered_name in line_map: sorted_show.append(ordered_name) sorted_urls.append(line_map[ordered_name]) line_map.pop(ordered_name) remaining_lines = [(name, line_map[name], original_order[name]) for name in line_map.keys()] remaining_lines.sort(key=lambda x: x[2]) for name, url, _ in remaining_lines: sorted_show.append(name) sorted_urls.append(url) return sorted_show, sorted_urls def getName(self): return "小心儿悠悠(修复版)" def isVideoFormat(self, url): return True def fetch(self, url, **kwargs): import requests if 'headers' not in kwargs: kwargs['headers'] = self.headers if 'timeout' not in kwargs: kwargs['timeout'] = 10 try: return requests.get(url, **kwargs) except Exception as e: print(f"请求失败: {str(e)}") from requests.models import Response response = Response() response.status_code = 500 return response