diff --git a/py/华数.py b/py/华数.py new file mode 100644 index 0000000..9348d46 --- /dev/null +++ b/py/华数.py @@ -0,0 +1,546 @@ +# coding=utf-8 +# !/usr/bin/python + +""" + +作者 精彩一瞬间 内容均从互联网收集而来 仅供交流学习使用 严禁用于商业用途 请于24小时内删除 + ====================Diudiumiao==================== + +""" + +from Crypto.Util.Padding import unpad +from Crypto.Util.Padding import pad +from urllib.parse import urlparse +from urllib.parse import unquote +from Crypto.Cipher import ARC4 +from urllib.parse import quote +from base.spider import Spider +from Crypto.Cipher import AES +from datetime import datetime +from bs4 import BeautifulSoup +from base64 import b64decode +import concurrent.futures +import urllib.request +import urllib.parse +import datetime +import binascii +import requests +import hashlib +import base64 +import json +import time +import hmac +import sys +import re +import os + +sys.path.append('..') + +xurl = "https://www.wasu.cn" + +headerx = { + 'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/50.0.2661.87 Safari/537.36' + } + +headerz = { + 'accept': '*/*', + 'accept-language': 'zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6', + 'cache-control': 'no-cache', + 'origin': xurl, + 'pragma': 'no-cache', + 'priority': 'u=1, i', + 'referer': xurl, + 'sec-ch-ua': '"Microsoft Edge";v="143", "Chromium";v="143", "Not A(Brand";v="24"', + 'sec-ch-ua-mobile': '?0', + 'sec-ch-ua-platform': '"Windows"', + 'sec-fetch-dest': 'empty', + 'sec-fetch-mode': 'cors', + 'sec-fetch-site': 'cross-site', + 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Safari/537.36 Edg/143.0.0.0', + } + +class Spider(Spider): + + def getName(self): + return "精彩" + + def init(self, extend): + pass + + def isVideoFormat(self, url): + pass + + def manualVideoCheck(self): + pass + + def homeVideoContent(self): + pass + + def homeContent(self, filter): + result = {} + result = {"class": [{"type_id": "961", "type_name": "电影"}, + {"type_id": "962", "type_name": "剧集"}, + {"type_id": "963", "type_name": "少儿"}, + {"type_id": "965", "type_name": "栏目"}, + {"type_id": "966", "type_name": "新闻"}], + "list": [], + "filters": {"961": [{"key": "地区", + "name": "地区", + "value": [{"n": "全部", "v": ""}, + {"n": "内地", "v": "内地"}, + {"n": "港台", "v": "港台"}, + {"n": "欧美", "v": "欧美"}, + {"n": "日韩", "v": "日韩"}, + {"n": "泰国", "v": "泰国"}, + {"n": "其他", "v": "其他"}]}, + {"key": "类型", + "name": "类型", + "value": [{"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": "恐怖"}, + {"n": "战争", "v": "战争"}, + {"n": "武侠", "v": "武侠"}]}, + {"key": "年代", + "name": "年代", + "value": [{"n": "全部", "v": ""}, + {"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": "2013", "v": "2013"}, + {"n": "2012", "v": "2012"}, + {"n": "2011", "v": "2011"}, + {"n": "2010", "v": "2010"}]}], + "962": [{"key": "地区", + "name": "地区", + "value": [{"n": "全部", "v": ""}, + {"n": "内地", "v": "内地"}, + {"n": "港台", "v": "港台"}, + {"n": "日韩", "v": "日韩"}, + {"n": "欧美", "v": "欧美"}, + {"n": "泰国", "v": "泰国"}, + {"n": "其他", "v": "其他"}]}, + {"key": "类型", + "name": "类型", + "value": [{"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": "武侠"}, + {"n": "历史", "v": "历史"}]}, + {"key": "年代", + "name": "年代", + "value": [{"n": "全部", "v": ""}, + {"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": "2013", "v": "2013"}, + {"n": "2012", "v": "2012"}, + {"n": "2011", "v": "2011"}, + {"n": "2010", "v": "2010"}]}], + "963": [{"key": "地区", + "name": "地区", + "value": [{"n": "全部", "v": ""}, + {"n": "内地", "v": "内地"}, + {"n": "日韩", "v": "日韩"}, + {"n": "欧美", "v": "欧美"}, + {"n": "港台", "v": "港台"}, + {"n": "其他", "v": "其他"}]}, + {"key": "类型", + "name": "类型", + "value": [{"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": "治愈"}, + {"n": "儿歌", "v": "儿歌"}, + {"n": "教育", "v": "教育"}, + {"n": "校园", "v": "校园"}, + {"n": "童话", "v": "童话"}, + {"n": "推理", "v": "推理"}, + {"n": "怀旧", "v": "怀旧"}, + {"n": "宠物", "v": "宠物"}, + {"n": "舞蹈", "v": "舞蹈"}]}, + {"key": "年代", + "name": "年代", + "value": [{"n": "全部", "v": ""}, + {"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": "2013", "v": "2013"}, + {"n": "2012", "v": "2012"}, + {"n": "2011", "v": "2011"}, + {"n": "2010", "v": "2010"}]}], + "965": [{"key": "地区", + "name": "地区", + "value": [{"n": "全部", "v": ""}, + {"n": "内地", "v": "内地"}, + {"n": "欧美", "v": "欧美"}]}, + {"key": "类型", + "name": "类型", + "value": [{"n": "全部", "v": ""}, + {"n": "文化", "v": "文化"}, + {"n": "纪实", "v": "纪实"}, + {"n": "访谈", "v": "访谈"}, + {"n": "历史", "v": "历史"}, + {"n": "美食", "v": "美食"}, + {"n": "旅游", "v": "旅游"}, + {"n": "时尚", "v": "时尚"}, + {"n": "情感", "v": "情感"}, + {"n": "生活", "v": "生活"}, + {"n": "真人秀", "v": "真人秀"}]}, + {"key": "年代", + "name": "年代", + "value": [{"n": "全部", "v": ""}, + {"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": "2013", "v": "2013"}, + {"n": "2012", "v": "2012"}, + {"n": "2011", "v": "2011"}, + {"n": "2010", "v": "2010"}]}], + "966": [{"key": "类型", + "name": "类型", + "value": [{"n": "全部", "v": ""}, + {"n": "国内视野", "v": "国内视野"}, + {"n": "国际纵览", "v": "国际纵览"}, + {"n": "军事话题", "v": "军事话题"}, + {"n": "社会百态", "v": "社会百态"}, + {"n": "央视频", "v": "央视频"}]}]}} + return result + + def generate_x_sign(self, secret_b64): + secret_key = self.decode_secret_key(secret_b64) + data = self.get_data_string() + signature = self.compute_signature(secret_key, data) + x_sign = self.encode_signature(signature) + return x_sign + + def decode_secret_key(self, secret_b64): + return base64.b64decode(secret_b64) + + def get_data_string(self): + return "{}" + + def compute_signature(self, secret_key, data): + return hmac.new(secret_key, data.encode('utf-8'), hashlib.sha256).digest() + + def encode_signature(self, signature): + return base64.b64encode(signature).decode('utf-8') + + def get_current_app_key(self, xurl, headerx): + resp = self.fetch_index_page(xurl, headerx) + js_path = self.extract_js_path(resp.text) + js_url = f"{xurl}{js_path}" + js_content = self.fetch_js_content(js_url, headerx) + target_key_b64 = self.extract_target_key(js_content) + return target_key_b64 + + def fetch_index_page(self, xurl, headerx): + resp = requests.get(xurl, headers=headerx) + resp.raise_for_status() + return resp + + def extract_js_path(self, html_content): + js_path_pattern = r'src="(/[\d\.]+/assets/js/index-[\w\.-]+\.js)"' + match = re.search(js_path_pattern, html_content) + return match.group(1) + + def fetch_js_content(self, js_url, headerx): + js_resp = requests.get(js_url, headers=headerx) + js_resp.raise_for_status() + return js_resp.text + + def extract_target_key(self, js_content): + key_pattern = r'const \w+="([^"]+)",\w+="([^"]+)",\w+="([^"]+)";' + key_match = re.search(key_pattern, js_content) + return key_match.group(2) + + def get_headers(self, sign1, xurl): + return { + 'accept': 'application/json, text/plain, */*', + 'accept-language': 'zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6', + 'cache-control': 'no-cache', + 'launchchannel': 'web_channel', + 'origin': xurl, + 'pragma': 'no-cache', + 'priority': 'u=1, i', + 'referer': xurl, + 'sec-ch-ua': '"Microsoft Edge";v="143", "Chromium";v="143", "Not A(Brand";v="24"', + 'sec-ch-ua-mobile': '?0', + 'sec-ch-ua-platform': '"Windows"', + 'sec-fetch-dest': 'empty', + 'sec-fetch-mode': 'cors', + 'sec-fetch-site': 'cross-site', + 'siteid': '1000101', + 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Safari/537.36 Edg/143.0.0.0', + 'x-sign': sign1, + } + + def categoryContent(self, cid, pg, filter, ext): + secret_b64 = self.get_current_app_key(xurl, headerx) + sign1 = self.generate_x_sign(secret_b64) + page = self.get_page_number(pg) + NdType, DqType, LxType = self.extract_filter_types(ext) + headers = self.get_headers(sign1, xurl) + params = self.build_category_params(cid, page, NdType, DqType, LxType) + data = self.fetch_category_data(params, headers) + videos = self.parse_category_videos(data) + return self.build_category_result(videos, pg) + + def get_page_number(self, pg): + return int(pg) if pg else 1 + + def extract_filter_types(self, ext): + NdType = ext.get('年代', '全部') + DqType = ext.get('地区', '全部') + LxType = ext.get('类型', '全部') + return NdType, DqType, LxType + + def build_category_params(self, cid, page, NdType, DqType, LxType): + return {'functionName': 'getNewsSearchedByCondition','nodeId': cid,'nodeTag': LxType,'yearTag': NdType,'countryTag': DqType,'orderType': '0','pageSize': '40','page': page,'keyword': '','siteId': '1000101',} + + def fetch_category_data(self, params, headers): + detail = requests.get('https://ups.5g.wasu.tv/rmp-user-suggest/1000101/hzhs/searchServlet', params=params,headers=headers) + detail.encoding = "utf-8" + return detail.json() + + def parse_category_videos(self, data): + videos = [] + for vod in data['data']: + videos.append(self.parse_category_video(vod)) + return videos + + def parse_category_video(self, vod): + return {"vod_id": f"{vod['nodeId']}@{vod['newsId']}","vod_name": vod['title'],"vod_pic": vod['hPic'],"vod_year": vod.get('pubTime', '暂无日期'),"vod_remarks": vod.get('episodeDesc', '暂无备注')} + + def build_category_result(self, videos, pg): + result = {'list': videos} + result['page'] = pg + result['pagecount'] = 9999 + result['limit'] = 90 + result['total'] = 999999 + return result + + def detailContent(self, ids): + did = ids[0] + secret_b64 = self.get_current_app_key(xurl, headerx) + sign1 = self.generate_x_sign(secret_b64) + fenge = did.split("@") + headers = self.get_headers(sign1, xurl) + params = self.build_detail_params(fenge) + data = self.fetch_detail_data(params, headers) + content = self.build_content(data) + director = self.extract_detail_field(data, 'director') + actor = self.extract_detail_field(data, 'actor') + remarks = self.extract_detail_field(data, 'episodeDesc') + year = self.extract_detail_field(data, 'pubTime') + area = self.extract_detail_field(data, 'countryTag') + bofang = self.build_play_url(data) + videos = [self.build_video_data(did, director, actor, remarks, year, area, content, bofang)] + return self.build_result(videos) + + def build_detail_params(self, fenge): + return {'siteId': '1000101', 'functionName': 'getCurrentNews', 'nodeId': fenge[0], 'newsId': fenge[1],'platform': 'web',} + + def fetch_detail_data(self, params, headers): + detail = requests.get('https://mcspapp.5g.wasu.tv/bvradio_app/hzhs/newsServlet', params=params, headers=headers) + detail.encoding = "utf-8" + return detail.json() + + def build_content(self, data): + return '介绍剧情📢' + data.get('data', {}).get('newsAbstract', '') + + def extract_detail_field(self, data, field_name): + return data.get('data', {}).get(field_name, '') + + def build_play_url(self, data): + bofang = '' + for vod in data['data']['vodList']: + name = vod['title'] + if len(vod['fileList']) > 1: + id = vod['fileList'][1]['playUrl'] + else: + id = vod['fileList'][0]['playUrl'] + bofang += name + '$' + id + '#' + return bofang[:-1] + + def build_video_data(self, did, director, actor, remarks, year, area, content, bofang): + return {"vod_id": did, "vod_director": director, "vod_actor": actor, "vod_remarks": remarks, "vod_year": year, "vod_area": area, "vod_content": content, "vod_play_from": "华数专线", "vod_play_url": bofang} + + def build_result(self, videos): + result = {} + result['list'] = videos + return result + + def get_x_sign_for_post(self, secret_b64, playUrl): + secret_key = self.decode_secret_key(secret_b64) + payload_dict = self.build_payload_dict(playUrl) + data_string = self.serialize_payload(payload_dict) + signature = self.compute_hmac_signature(secret_key, data_string) + x_sign = self.encode_signature(signature) + return x_sign + + def decode_secret_key(self, secret_b64): + return base64.b64decode(secret_b64) + + def build_payload_dict(self, playUrl): + return {"playUrl": playUrl, "platform": "web"} + + def serialize_payload(self, payload_dict): + return json.dumps(payload_dict, separators=(',', ':'), ensure_ascii=False) + + def compute_hmac_signature(self, secret_key, data_string): + return hmac.new(secret_key, data_string.encode('utf-8'), hashlib.sha256).digest() + + def encode_signature(self, signature): + return base64.b64encode(signature).decode('utf-8') + + def playerContent(self, flag, id, vipFlags): + secret_b64 = self.get_current_app_key(xurl, headerx) + id = self.normalize_play_url(id) + sign2 = self.get_x_sign_for_post(secret_b64, id) + headers = self.get_headers(sign2, xurl) + json_data = self.build_request_json(id) + result = self.post_play_url_request(headers, json_data) + play_url = result['data']['playUrl'] + return self.build_player_result(play_url) + + def normalize_play_url(self, id): + return id.replace('.mp4', '/playlist.m3u8') + + def build_request_json(self, playUrl): + return {'playUrl': playUrl, 'platform': 'web', } + + def post_play_url_request(self, headers, json_data): + response = requests.post('https://mcspapp.5g.wasu.tv/thirdApiFile/file/getPlayUrl', headers=headers, json=json_data) + return response.json() + + def build_player_result(self, play_url): + result = {} + result["parse"] = 0 + result["playUrl"] = '' + result["url"] = play_url + result["header"] = headerz + return result + + def searchContentPage(self, key, quick, pg): + page = self.get_page_number(pg) + secret_b64 = self.get_current_app_key(xurl, headerx) + sign1 = self.generate_x_sign(secret_b64) + headers = self.get_headers(sign1, xurl) + params = self.build_search_params(key, page) + data = self.fetch_search_data(params, headers) + videos = self.parse_search_videos(data) + return self.build_search_result(videos, pg) + + def get_page_number(self, pg): + return int(pg) if pg else 1 + + def build_search_params(self, key, page): + return {'functionName': 'getNewsSearched', 'searchNewsType': '3,4,5', 'keyword': key, 'pageSize': 10, 'page': page, 'siteId': 1000101} + + def fetch_search_data(self, params, headers): + detail = requests.get("https://ups.5g.wasu.tv/rmp-user-suggest/1000101/hzhs/searchServlet", params=params, headers=headers) + detail.encoding = "utf-8" + return detail.json() + + def parse_search_videos(self, data): + videos = [] + for vod in data['data']: + videos.append(self.parse_search_video(vod)) + return videos + + def parse_search_video(self, vod): + return {"vod_id": f"{vod['nodeId']}@{vod['newsId']}", "vod_name": vod['title'], "vod_pic": vod['hPic'], "vod_year": vod.get('pubTime', '暂无日期'), "vod_remarks": vod.get('episodeDesc', '暂无备注')} + + def build_search_result(self, videos, pg): + result = {'list': videos} + result['page'] = pg + result['pagecount'] = 9999 + result['limit'] = 90 + result['total'] = 999999 + return result + + def searchContent(self, key, quick, pg="1"): + return self.searchContentPage(key, quick, '1') + + def localProxy(self, params): + if params['type'] == "m3u8": + return self.proxyM3u8(params) + elif params['type'] == "media": + return self.proxyMedia(params) + elif params['type'] == "ts": + return self.proxyTs(params) + return None + + + + + + + + + + diff --git a/py/可可.py b/py/可可.py new file mode 100644 index 0000000..26ff1cf --- /dev/null +++ b/py/可可.py @@ -0,0 +1,426 @@ +#coding=utf-8 +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +TVBox / 影视仓 Python源脚本 +站点: 可可影视 (103.51.147.112:51120) +""" + +import sys +import re +import json +import requests +import urllib3 +from urllib.parse import quote +from pyquery import PyQuery as pq +sys.path.append('..') +from base.spider import Spider + +# 禁用SSL警告 +urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + +class Spider(Spider): + + def __init__(self): + super().__init__() + self.site = 'https://103.51.147.112:51120' + self.session = requests.Session() + self.session.headers.update({ + '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': 'https://103.51.147.112:51120/' + }) + self.cateManual = { + '电影': '1', + '连续剧': '2', + '动漫': '3', + '综艺纪录': '4', + '短剧': '6' + } + # 隐晦的站点标识 + self._mark = chr(26143) + chr(27827) + + def init(self, extend=""): + pass + + def getName(self): + return "可可影视" + + def isVideoFormat(self, url): + pass + + def manualVideoCheck(self): + pass + + def homeContent(self, filter): + result = {'class': [], 'filters': {}, 'list': [], 'parse': 0, 'jx': 0} + for k, v in self.cateManual.items(): + result['class'].append({ + 'type_id': str(v), + 'type_name': k + }) + return result + + def homeVideoContent(self): + videos = [] + try: + url = f'{self.site}/channel/1.html' + r = self.session.get(url, timeout=15, verify=False) + r.encoding = 'utf-8' + doc = pq(r.text) + items = doc('.module-item') + seen = set() + for item in items.items(): + a = item.find('.v-item') + href = a.attr('href') or '' + vid = self.getVid(href) + if not vid or vid in seen: + continue + seen.add(vid) + + # 标题 + titles = item.find('.v-item-title') + title = '' + for j in range(len(titles)): + t = titles.eq(j).text().strip() + if t and t != '可可影视-kekys.com': + title = t + break + + # 图片 + pic = '' + imgs = item.find('img') + for j in range(len(imgs)): + img = imgs.eq(j) + src = img.attr('data-original') or '' + if src and 'placeholder' not in src and 'logo_placeholder' not in src: + pic = src + break + if pic and pic.startswith('/'): + pic = 'https://vres.zyxpedu.com' + pic + + # 备注 + note = '' + bottom = item.find('.v-item-bottom span') + if bottom.length: + note = bottom.text().strip() + + if title: + videos.append({ + 'vod_id': vid, + 'vod_name': title, + 'vod_pic': pic, + 'vod_remarks': note + }) + except Exception as e: + print(f'homeVideoContent error: {e}') + return {'list': videos[:24], 'parse': 0, 'jx': 0} + + def categoryContent(self, tid, pg, filter, extend): + result = {'list': [], 'parse': 0, 'jx': 0} + page = int(pg) if pg else 1 + try: + url = f'{self.site}/channel/{tid}.html?page={page}' + r = self.session.get(url, timeout=15, verify=False) + r.encoding = 'utf-8' + doc = pq(r.text) + items = doc('.module-item') + for item in items.items(): + a = item.find('.v-item') + href = a.attr('href') or '' + vid = self.getVid(href) + if not vid: + continue + + # 标题 + titles = item.find('.v-item-title') + title = '' + for j in range(len(titles)): + t = titles.eq(j).text().strip() + if t and t != '可可影视-kekys.com': + title = t + break + + # 图片 + pic = '' + imgs = item.find('img') + for j in range(len(imgs)): + img = imgs.eq(j) + src = img.attr('data-original') or '' + if src and 'placeholder' not in src and 'logo_placeholder' not in src: + pic = src + break + if pic and pic.startswith('/'): + pic = 'https://vres.zyxpedu.com' + pic + + # 备注 + note = '' + bottom = item.find('.v-item-bottom span') + if bottom.length: + note = bottom.text().strip() + + if title: + result['list'].append({ + 'vod_id': vid, + 'vod_name': title, + 'vod_pic': pic, + 'vod_remarks': note + }) + except Exception as e: + print(f'categoryContent error: {e}') + + result['page'] = page + # 修复:满页24条说明还有下一页,否则已到尾页 + result['pagecount'] = page + 1 if len(result['list']) >= 24 else page + result['limit'] = len(result['list']) + result['total'] = len(result['list']) + return result + + def detailContent(self, ids): + result = {'list': [], 'parse': 0, 'jx': 0} + vid = ids[0] if ids else '' + if not vid: + return result + try: + url = f'{self.site}/detail/{vid}.html' + r = self.session.get(url, timeout=15, verify=False) + r.encoding = 'utf-8' + html = r.text + + # 标题:从title标签提取,最可靠 + title = '' + title_match = re.search(r'(.+?)', html) + if title_match: + title = title_match.group(1).split('-')[0].strip() + # 去掉特殊字符水印 + title = re.sub(r'[𝕜𝕜𝕪𝕤𝟘𝟙𝕔𝕠𝕞.\s]+', ' ', title).strip() + title = re.sub(r'\s+', ' ', title).strip() + + # 图片:从og:image提取 + pic = '' + og_img = re.search(r']+href="(/play/\d+-(\d+)-(\d+)\.html)"[^>]+class="episode-item"[^>]*>(.*?)', html, re.DOTALL) + # 如果没匹配到,试试class在href前面的情况 + if not all_play: + all_play = re.findall(r']+class="episode-item"[^>]+href="(/play/\d+-(\d+)-(\d+)\.html)"[^>]*>(.*?)', html, re.DOTALL) + for href, sid, nid, link_html in all_play: + text = re.sub(r'<[^>]+>', '', link_html).strip() + if not text: + continue + if sid not in episodes_by_sid: + episodes_by_sid[sid] = [] + episodes_by_sid[sid].append(f'{text}${href}') + if sid not in seen_sids: + seen_sids.add(sid) + sids_in_order.append(sid) + + # 线路名称 + source_labels = [] + all_labels = re.findall(r'class="source-item-label"[^>]*>([^<]+)]+\.(?:m3u8|mp4)[^\s"\'<>]*', r.text) + if all_urls: + for u in all_urls: + if 'index.m3u8' in u or 'video.m3u8' in u or '.mp4' in u: + video_url = u + break + if not video_url: + video_url = all_urls[0] + + # 修复:处理相对路径视频URL + if video_url and not video_url.startswith('http'): + if video_url.startswith('/'): + video_url = self.site + video_url + else: + video_url = self.site + '/' + video_url + + if video_url: + result['parse'] = 0 + result['url'] = video_url + result['jx'] = 0 + result['header'] = { + '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': self.site + '/' + } + else: + result['parse'] = 1 + result['url'] = play_url + result['jx'] = 0 + result['header'] = { + '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': self.site + '/' + } + except Exception as e: + print(f'playerContent error: {e}') + result['parse'] = 1 + result['url'] = id if id.startswith('http') else self.site + id + result['jx'] = 0 + result['header'] = { + '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': self.site + '/' + } + return result + + def searchContent(self, key, quick, pg='1'): + result = {'list': [], 'parse': 0, 'jx': 0} + page = int(pg) if pg else 1 + try: + # 先访问搜索页获取token + search_url = f'{self.site}/search?k={quote(key)}' + r = self.session.get(search_url, timeout=15, verify=False) + r.encoding = 'utf-8' + t_match = re.search(r'name="t" value="([^"]+)"', r.text) + t = t_match.group(1) if t_match else '' + + if t: + url = f'{self.site}/search?k={quote(key)}&t={quote(t)}' + if page > 1: + url += f'&page={page}' + r = self.session.get(url, timeout=15, verify=False) + r.encoding = 'utf-8' + + doc = pq(r.text) + items = doc('.search-result-item') + for item in items.items(): + # 修复:兼容 href 在元素本身或子级 标签上的情况 + href = item.attr('href') or '' + if not href: + a_tag = item.find('a') + if a_tag.length: + href = a_tag.attr('href') or '' + + vid = self.getVid(href) + if not vid: + continue + + # 标题 + title = '' + title_elem = item.find('.title') + if title_elem.length: + title = title_elem.text().strip() + if not title: + img = item.find('img') + if img.length: + title = img.attr('alt') or img.attr('title') or '' + title = title.strip() + + # 图片 + pic = '' + imgs = item.find('img') + for j in range(len(imgs)): + img = imgs.eq(j) + src = img.attr('data-original') or img.attr('src') or '' + if src and 'placeholder' not in src and 'logo_placeholder' not in src: + pic = src + break + if pic and pic.startswith('/'): + pic = 'https://vres.zyxpedu.com' + pic + + if title: + result['list'].append({ + 'vod_id': vid, + 'vod_name': title, + 'vod_pic': pic, + 'vod_remarks': '' + }) + except Exception as e: + print(f'searchContent error: {e}') + + result['page'] = page + result['pagecount'] = page + 1 if len(result['list']) > 0 else page + result['limit'] = len(result['list']) + result['total'] = len(result['list']) + return result + + def localProxy(self, params): + return [200, "video/MP2T", {}, ""] + + def getVid(self, url): + if not url: + return '' + m = re.search(r'/detail/(\d+)\.html', url) + if m: + return m.group(1) + m = re.search(r'/play/(\d+)-', url) + if m: + return m.group(1) + return '' diff --git a/py/咖啡直播.py b/py/咖啡直播.py new file mode 100644 index 0000000..b0c181b --- /dev/null +++ b/py/咖啡直播.py @@ -0,0 +1,375 @@ +# coding=utf-8 +#!/usr/bin/env python3 +# @name 咖啡直播 +# @author 转自 OmniBox JS +# @description 体育赛事录像回放 + 直播(足球/篮球/NBA) +# @version 2.0.0 + +import json +import requests + + +class Spider: + + def getName(self): + return "咖啡直播" + + def getDependence(self): + return [] + + def init(self, extend=""): + self.host = "https://kafeizhibo.cc" + self.headers = { + "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36", + "Accept": "application/json, text/plain, */*", + "Referer": "https://kafeizhibo.com/live/all" + } + + def log(self, msg): + print("[咖啡直播] " + str(msg)) + + # ========================================================= + # 工具 + # ========================================================= + + def _normalize_url(self, path): + if not path: + return "" + if path.startswith("http"): + return path + if path.startswith("//"): + return "https:" + path + return self.host + ("" if path.startswith("/") else "/") + path + + def _get(self, path, params=None, referer=None): + h = dict(self.headers) + if referer: + h["Referer"] = referer + resp = requests.get(self.host + path, headers=h, params=params, timeout=10) + return resp.json() + + # ========================================================= + # 直播部分 + # ========================================================= + + def _fetch_live_all(self): + """GET /api/v1/archor — 返回所有正在直播的频道""" + return self._get("/api/v1/archor", referer=self.host + "/live/all") + + def _parse_live_list(self, items, category_filter=None): + """ + category: 1=足球, 2=篮球, None=全部 + 每个 archor 代表一个独立直播频道(同一场球可能有多个频道) + 合并同 match_id 的频道到一个 vod,多线路在 detail 里处理 + """ + # 按 room_id 去重(同一 room_id 只取第一个,避免重复) + seen_rooms = set() + result = [] + for item in items: + if category_filter and item.get("category") != category_filter: + continue + room_id = str(item.get("room_id", "")) + if room_id in seen_rooms: + continue + seen_rooms.add(room_id) + + home = item.get("home_team", "") + away = item.get("away_team", "") + league = item.get("league_name", "") + h_score = item.get("home_score", 0) + a_score = item.get("away_score", 0) + title = "{} vs {} ({})".format(home, away, league) + + pic = self._normalize_url(item.get("screenshot", "")) + if not pic or "default" in pic: + mi = item.get("match_info") or {} + pic = mi.get("home_team_logo", "") + + result.append({ + "vod_id": "live_{}".format(room_id), + "vod_name": "🔴 " + title, + "vod_pic": pic, + "vod_remarks": "{} - {} | {}".format(h_score, a_score, item.get("name", "")), + }) + return result + + def _detail_live(self, room_id): + """GET /api/v1/room/{room_id} — 获取直播间多线路""" + try: + data = self._get( + "/api/v1/room/{}".format(room_id), + referer=self.host + "/room/{}".format(room_id) + ) + if data.get("code") != 200 or not data.get("data"): + return {"list": []} + + d = data["data"] + room_info = d.get("room_info", {}) + signals = d.get("signals", []) + + home = room_info.get("home_team", "") + away = room_info.get("away_team", "") + league = room_info.get("league", "") + h_score = room_info.get("home_score", 0) + a_score = room_info.get("away_score", 0) + title = "{} vs {} ({})".format(home, away, league) + + teams = d.get("teams", {}) + pic = (teams.get("home") or {}).get("logo", "") + + # 每条 signal 是一个线路(官方直播/原声直播) + episodes = [] + for sig in signals: + url = sig.get("stream_url", "") + if url: + name = sig.get("name", "线路") + episodes.append("{}${}".format(name, url)) + + # 如果 signals 为空,fallback 到 archor + if not episodes: + archor = d.get("archor", {}) + url = archor.get("stream_url", "") + if url: + episodes.append("{}${}".format(archor.get("name", "直播"), url)) + + vod = { + "vod_id": "live_{}".format(room_id), + "vod_name": "🔴 " + title, + "vod_pic": pic, + "vod_content": "{} {} vs {},比分 {} - {}".format( + league, home, away, h_score, a_score + ), + "vod_play_from": "直播线路", + "vod_play_url": "#".join(episodes), + } + return {"list": [vod]} + except Exception as e: + self.log("直播详情失败: " + str(e)) + return {"list": []} + + # ========================================================= + # 录像部分 + # ========================================================= + + def _fetch_recordings(self, page=1, size=30, league=None, type_id=None): + params = {"page": page, "size": size} + if league: + params["league"] = league + elif type_id and type_id not in ("all", "nba", "live_all", "live_1", "live_2"): + params["type"] = type_id + h = dict(self.headers) + h["Referer"] = self.host + "/pc/replay" + resp = requests.get(self.host + "/api/v1/recordings", headers=h, params=params, timeout=10) + return resp.json() + + def _parse_video_list(self, items): + result = [] + for item in items: + title = "{} vs {} ({})".format( + item["home_team"], item["away_team"], item["league_name"] + ) + score = "{} - {}".format(item["home_score"], item["away_score"]) + pic = item.get("cover_image", "") + if pic and not pic.startswith("http"): + pic = self._normalize_url(pic) + if not pic or "default_cover" in pic: + pic = item.get("home_team_logo", "") + remarks = "{} | {} | {}个录像".format( + score, item["start_time"], item.get("recording_count", 0) + ) + result.append({ + "vod_id": str(item["match_id"]), + "vod_name": title, + "vod_pic": pic, + "vod_remarks": remarks, + }) + return result + + def _detail_recording(self, vid): + try: + h = dict(self.headers) + h["Referer"] = self.host + "/pc/replay" + resp = requests.get( + "{}/api/v1/match/{}/recordings".format(self.host, vid), + headers=h, + timeout=10 + ) + data = resp.json() + if data.get("code") != 200 or not data.get("data"): + return {"list": []} + + match = data["data"]["match"] + replays = data["data"].get("replays", []) + highlights = data["data"].get("highlights", []) + + title = "{} vs {} ({})".format( + match["home_team"], match["away_team"], match["league_name"] + ) + pic = match.get("home_team_logo") or match.get("away_team_logo") or "" + + episodes = [] + for idx, rec in enumerate(replays): + if rec.get("video_url"): + name = rec.get("title") or "录像{}".format(idx + 1) + episodes.append("{}${}".format(name, rec["video_url"])) + for idx, rec in enumerate(highlights): + if rec.get("video_url"): + name = rec.get("title") or "集锦{}".format(idx + 1) + episodes.append("{}${}".format(name, rec["video_url"])) + + vod = { + "vod_id": str(vid), + "vod_name": title, + "vod_pic": pic, + "vod_content": "{} {} {} vs {},比分 {} - {},比赛时间:{}".format( + match["league_name"], match.get("match_round", ""), + match["home_team"], match["away_team"], + match["home_score"], match["away_score"], + match["start_time"] + ), + "vod_play_from": "录像源", + "vod_play_url": "#".join(episodes), + } + return {"list": [vod]} + except Exception as e: + self.log("录像详情失败: " + str(e)) + return {"list": []} + + # ========================================================= + # FongMi 接口 + # ========================================================= + + def homeContent(self, filter): + categories = [ + # 直播分类 + {"type_id": "live_all", "type_name": "🔴 直播全部"}, + {"type_id": "live_1", "type_name": "🔴 直播足球"}, + {"type_id": "live_2", "type_name": "🔴 直播篮球"}, + # 录像分类 + {"type_id": "all", "type_name": "录像全部"}, + {"type_id": "1", "type_name": "录像足球"}, + {"type_id": "2", "type_name": "录像篮球"}, + {"type_id": "nba", "type_name": "录像NBA"}, + ] + # 首页展示直播列表 + try: + data = self._fetch_live_all() + vod_list = self._parse_live_list(data.get("data", [])) if data.get("code") == 200 else [] + except Exception as e: + self.log("首页失败: " + str(e)) + vod_list = [] + return {"class": categories, "list": vod_list} + + def homeVideoContent(self): + return {"list": []} + + def categoryContent(self, tid, pg, filter, extend): + pg = int(pg) if pg else 1 + + # ---- 直播分类 ---- + if tid in ("live_all", "live_1", "live_2"): + try: + data = self._fetch_live_all() + if data.get("code") == 200: + cat = None if tid == "live_all" else int(tid.split("_")[1]) + vod_list = self._parse_live_list(data.get("data", []), category_filter=cat) + else: + vod_list = [] + except Exception as e: + self.log("直播分类失败: " + str(e)) + vod_list = [] + return {"list": vod_list, "page": 1, "pagecount": 1, "limit": 100, "total": len(vod_list)} + + # ---- 录像分类 ---- + try: + if tid == "nba": + data = self._fetch_recordings(pg, 20, league="NBA") + size = 20 + elif tid == "all": + data = self._fetch_recordings(pg, 30) + size = 30 + else: + data = self._fetch_recordings(pg, 30, type_id=tid) + size = 30 + + vod_list = [] + pagecount = 1 + if data.get("code") == 200 and data.get("data"): + vod_list = self._parse_video_list(data["data"]) + pagecount = pg + 1 if len(data["data"]) == size else pg + except Exception as e: + self.log("录像分类失败: " + str(e)) + vod_list = [] + pagecount = 1 + + return {"list": vod_list, "page": pg, "pagecount": pagecount, "limit": 30, "total": len(vod_list)} + + def detailContent(self, ids): + vid = ids[0] if isinstance(ids, list) and ids else str(ids) + if vid.startswith("live_"): + room_id = vid[5:] # 去掉 "live_" 前缀 + return self._detail_live(room_id) + else: + return self._detail_recording(vid) + + def searchContent(self, key, quick, pg=1): + if not key: + return {"list": []} + keyword = key.lower() + result = [] + + # 搜索直播 + try: + data = self._fetch_live_all() + if data.get("code") == 200: + for item in data.get("data", []): + if (keyword in item.get("home_team", "").lower() + or keyword in item.get("away_team", "").lower() + or keyword in item.get("league_name", "").lower() + or keyword in item.get("title", "").lower()): + room_id = str(item.get("room_id", "")) + home = item.get("home_team", "") + away = item.get("away_team", "") + league = item.get("league_name", "") + result.append({ + "vod_id": "live_{}".format(room_id), + "vod_name": "🔴 {} vs {} ({})".format(home, away, league), + "vod_pic": "", + "vod_remarks": "直播中", + }) + except Exception as e: + self.log("搜索直播失败: " + str(e)) + + # 搜索录像 + try: + data = self._fetch_recordings(1, 100) + if data.get("code") == 200: + for item in data["data"]: + if (keyword in item["home_team"].lower() + or keyword in item["away_team"].lower() + or keyword in item["league_name"].lower()): + title = "{} vs {} ({})".format( + item["home_team"], item["away_team"], item["league_name"] + ) + result.append({ + "vod_id": str(item["match_id"]), + "vod_name": title, + "vod_pic": "", + "vod_remarks": "{} - {}".format(item["home_score"], item["away_score"]), + }) + except Exception as e: + self.log("搜索录像失败: " + str(e)) + + return {"list": result, "page": 1, "pagecount": 1} + + def playerContent(self, flag, id, vipFlags): + return { + "parse": 0, + "playUrl": "", + "url": id, + "header": json.dumps({ + "User-Agent": self.headers["User-Agent"], + "Referer": self.host, + "Origin": self.host, + }) + } diff --git a/py/瓜子.py b/py/瓜子.py new file mode 100644 index 0000000..89ebead --- /dev/null +++ b/py/瓜子.py @@ -0,0 +1,498 @@ +# coding = utf-8 +#!/usr/bin/python +import re +import sys +import json +import time +import base64 +import hashlib +import random +import string +import urllib.parse +from Crypto.Cipher import AES +from Crypto.Util.Padding import pad, unpad +from Crypto.PublicKey import RSA +from Crypto.Cipher import PKCS1_v1_5 +from base.spider import Spider + +sys.path.append('..') + +class Spider(Spider): + def __init__(self): + self.name = "瓜子" + self.hosts = [ + 'https://apinew.uozvr.com', + 'https://api.w32z7vtd.com', + 'https://api.6a7nnf7.com', + 'https://api.umygrx3.com', + 'https://api.rmedphk.com' + ] + self.host_index = 0 + self.host = self.hosts[self.host_index] + + # AES 固定密钥(与Java版一致) + self.AES_KEY = 'OITxa5OqAYjhswxx' + self.AES_IV = 'rCMNwZASNBKZ8mXV' + + # RSA 公钥/私钥 + self.RSA_PUBLIC_KEY = "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDUM5+/y8sPsWkd1/RQS64X259EUwxFXFE5HlA65MqrxnPs0JqoSRojSDy5QhwvROlaD6TwRQHKMY2OAZ6SnQeUJsChTEFIR9qUkwrs3/MVUMxjsv6JS6Oe/juclyJGTgVmDhB55EafXsD0SQYVj/QXXsxR6ewR5E2kL52yAAD4yQIDAQAB" + self.RSA_PRIVATE_KEY = """-----BEGIN RSA PRIVATE KEY----- +MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGAe6hKrWLi1zQmjTT1 +ozbE4QdFeJGNxubxld6GrFGximxfMsMB6BpJhpcTouAqywAFppiKetUBBbXwYsYU +1wNr648XVmPmCMCy4rY8vdliFnbMUj086DU6Z+/oXBdWU3/b1G0DN3E9wULRSwcK +ZT3wj/cCI1vsCm3gj2R5SqkA9Y0CAwEAAQKBgAJH+4CxV0/zBVcLiBCHvSANm0l7 +HetybTh/j2p0Y1sTXro4ALwAaCTUeqdBjWiLSo9lNwDHFyq8zX90+gNxa7c5EqcW +V9FmlVXr8VhfBzcZo1nXeNdXFT7tQ2yah/odtdcx+vRMSGJd1t/5k5bDd9wAvYdI +DblMAg+wiKKZ5KcdAkEA1cCakEN4NexkF5tHPRrR6XOY/XHfkqXxEhMqmNbB9U34 +saTJnLWIHC8IXys6Qmzz30TtzCjuOqKRRy+FMM4TdwJBAJQZFPjsGC+RqcG5UvVM +iMPhnwe/bXEehShK86yJK/g/UiKrO87h3aEu5gcJqBygTq3BBBoH2md3pr/W+hUM +WBsCQQChfhTIrdDinKi6lRxrdBnn0Ohjg2cwuqK5zzU9p/N+S9x7Ck8wUI53DKm8 +jUJE8WAG7WLj/oCOWEh+ic6NIwTdAkEAj0X8nhx6AXsgCYRql1klbqtVmL8+95KZ +K7PnLWG/IfjQUy3pPGoSaZ7fdquG8bq8oyf5+dzjE/oTXcByS+6XRQJAP/5ciy1b +L3NhUhsaOVy55MHXnPjdcTX0FaLi+ybXZIfIQ2P4rb19mVq1feMbCXhz+L1rG8oa +t5lYKfpe8k83ZA== +-----END RSA PRIVATE KEY-----""" + + self.DEVICE_OLD_KEY = "aLFBMWpxBrIDAD1Si/KVvm41" + + # 设备信息(随机生成) + self.deviceId = str(864150060000000 + random.randint(0, 9999)) + self.deviceKey = ''.join(random.choices('0123456789ABCDEF', k=40)) # 20字节hex大写 + self.token = "" + self.token_id = "" + self.registered = False + + self.header = { + 'User-Agent': 'Lavf/57.83.100', + 'code': 'GZ0369', + 'deviceId': self.deviceId, + 'lang': 'zh_cn', + 'Cache-Control': 'no-cache', + 'Content-Type': 'application/x-www-form-urlencoded', + 'Version': '2604028', + 'PackageName': 'com.ae06aebdbb.y286327f5a.ofe849883320260517', + 'Ver': '3.0.3.2', + 'api-ver': '3.0.3.2', + 'Referer': self.host + } + + self.cache = {} + self.cache_timeout = 300 + + # 初始化token + self.init_token() + + def getName(self): + return self.name + + def init(self, extend=''): + pass + + # ---------- 设备注册与认证 ---------- + def init_token(self): + """初始化token:注册设备 -> 刷新""" + print("===== 初始化设备认证 =====") + try: + if not self.registered: + self.sign_up() + # 刷新获取最终token + self.refresh_token() + except Exception as e: + print(f"初始化token失败: {e}") + # 兜底使用原有硬编码(几乎没用) + self.token = '024212ef0975c5306a1434e113a46463.bc77313e11a248558a6ca244ca980944ec3421fa480c50e0229ad91f1cb15aea582603202cd71796885c9e5163e500f1b72f737059aff1ddb8beea47c5a331d6760540345b7f88b2302a0e6e09589f9dcf3ff9175d8c905f990203f5fc04748008ea7a366571cbf5b09509a873dcfba3cf1d5590385f5f7ef6e01d1850974aa220eb5178c89e61c24411af9b9a19435e.06fde789ece48d9b33c5dc857e04e9b5838f08264d928b87237d3476c4484b46' + + def sign_up(self): + """注册设备""" + print("注册新设备...") + params = { + "new_key": self.deviceKey, + "old_key": self.DEVICE_OLD_KEY, + "phone_type": 1, + "code": "" + } + result = self._auth_request('/App/Authentication/Device/signUp', params) + self._apply_auth(result) + self.registered = True + + def sign_in(self): + """登录设备""" + print("设备登录...") + params = { + "new_key": self.deviceKey, + "old_key": self.DEVICE_OLD_KEY + } + result = self._auth_request('/App/Authentication/Device/signIn', params) + self._apply_auth(result) + + def _apply_auth(self, result): + """从认证响应中提取token""" + new_token = result.get('token', '') + if not new_token: + raise Exception("认证失败,无token返回: {}".format(result)) + self.token = new_token + new_token_id = result.get('app_user_id', '') + if new_token_id: + self.token_id = new_token_id + print(f"获取token成功, token前缀: {self.token[:30]}...") + + def refresh_token(self): + """刷新token""" + print("刷新token...") + result = self._auth_request('/App/Authentication/Authenticator/refresh', {}) + self._apply_auth(result) + + def _auth_request(self, path, params): + """认证类请求(不需要ensure_token)""" + return self._send_encrypted_request(params, path, is_auth=True) + + # ---------- 业务请求核心(修复加密与签名) ---------- + def ensure_token(self): + """确保token有效,如未就绪则重新获取""" + if not self.token or not self.token_id: + if self.registered: + self.sign_in() + else: + self.sign_up() + self.refresh_token() + + def _send_encrypted_request(self, data, path, is_auth=False): + """ + 发送加密请求,返回解密后的字典 + :param data: 业务参数字典 + :param path: 请求路径 + :param is_auth: 是否为认证类请求(signUp/signIn/refresh),此时不使用ensure_token + """ + try: + if not is_auth: + self.ensure_token() + + # 1. 将参数转为JSON并AES加密 + json_params = json.dumps(data) + encrypted = self.aes_encrypt(json_params, self.AES_KEY, self.AES_IV) + request_key = encrypted.upper() # Java中是bytesToHex(encrypted).toUpperCase() + + # 2. 生成keys (RSA加密 iv/key JSON) + key_json = json.dumps({"iv": self.AES_IV, "key": self.AES_KEY}) + keys = self.rsa_encrypt(key_json, self.RSA_PUBLIC_KEY) + + # 3. 生成签名 + t = str(int(time.time())) + sign_str = f"token_id=,token={self.token},phone_type=1,request_key={request_key},app_id=1,time={t},keys={keys}*&zvdvdvddbfikkkumtmdwqppp?|4Y!s!2br" + signature = self.get_md5(sign_str) # 已改为大写 + + # 4. 构建请求体 + body = { + 'token': self.token, + 'token_id': '', + 'phone_type': '1', + 'time': t, + 'phone_model': 'xiaomi-25031', # 与Java版保持一致 + 'keys': keys, + 'request_key': request_key, + 'signature': signature, + 'app_id': '1', + 'ad_version': '1' + } + + # 5. 发送请求 + url = f"{self.host}{path}" + response = self.post(url, headers=self.header, data=body, timeout=10) + + if response.status_code != 200: + raise Exception(f"HTTP {response.status_code}") + + resp_json = response.json() + # 检查业务code(若不为200可能token过期) + if 'code' in resp_json and resp_json['code'] != 200: + print(f"业务错误码: {resp_json['code']}, 信息: {resp_json}") + # 如果不是认证请求,尝试重新获取token后重试一次(这里简单处理,外层get_data已有重试) + raise Exception("业务错误") + + data_section = resp_json.get('data') + if not data_section: + raise Exception("响应缺少data字段") + + encrypted_response = data_section.get('response_key', '') + encrypted_keys = data_section.get('keys', '') + + # 6. 解密响应 + decrypted_keys_json = self.rsa_decrypt(encrypted_keys, self.RSA_PRIVATE_KEY) + key_info = json.loads(decrypted_keys_json) + resp_key = key_info['key'] + resp_iv = key_info['iv'] + decrypted_data = self.aes_decrypt(encrypted_response, resp_key, resp_iv) + return json.loads(decrypted_data) + + except Exception as e: + print(f"请求失败 [{path}]: {e}") + return None + + def get_data(self, data, path, use_cache=True): + """带重试和域名轮询的数据获取(保持原框架)""" + try: + cache_key = f"{path}_{hash(str(data))}" if use_cache else None + if use_cache and cache_key in self.cache: + cached_data, timestamp = self.cache[cache_key] + if time.time() - timestamp < self.cache_timeout: + return cached_data + + for attempt in range(3): + tried = 0 + while tried < len(self.hosts): + self.host = self.hosts[self.host_index] + self.header['Referer'] = self.host + result = self._send_encrypted_request(data, path) + if result is not None: + print(f"请求成功: {path}, 域名: {self.host}") + if use_cache and cache_key: + self.cache[cache_key] = (result, time.time()) + return result + + # 切换到下一个域名 + self.host_index = (self.host_index + 1) % len(self.hosts) + tried += 1 + + # 所有域名失败,尝试重新认证并重试 + if attempt < 2: + print("所有域名失败,尝试重新认证...") + try: + self.ensure_token() + except: + pass + self.host_index = 0 + else: + break + return None + except Exception as e: + print(f"get_data异常: {e}") + return None + + # ---------- 加解密工具 ---------- + def aes_encrypt(self, text, key, iv): + try: + key_bytes = key.encode('utf-8') + iv_bytes = iv.encode('utf-8') + cipher = AES.new(key_bytes, AES.MODE_CBC, iv_bytes) + encrypted = cipher.encrypt(pad(text.encode('utf-8'), AES.block_size)) + return encrypted.hex().upper() + except Exception as e: + print(f"AES加密失败: {e}") + return "" + + def aes_decrypt(self, text, key, iv): + try: + key_bytes = key.encode('utf-8') + iv_bytes = iv.encode('utf-8') + cipher = AES.new(key_bytes, AES.MODE_CBC, iv_bytes) + encrypted_bytes = bytes.fromhex(text) + decrypted = unpad(cipher.decrypt(encrypted_bytes), AES.block_size) + return decrypted.decode('utf-8') + except Exception as e: + print(f"AES解密失败: {e}") + return "" + + def rsa_encrypt(self, text, public_key_str): + """RSA公钥加密(PKCS1v1.5)""" + try: + key = RSA.import_key("-----BEGIN PUBLIC KEY-----\n" + public_key_str + "\n-----END PUBLIC KEY-----") + cipher = PKCS1_v1_5.new(key) + encrypted = cipher.encrypt(text.encode('utf-8')) + return base64.b64encode(encrypted).decode('utf-8') + except Exception as e: + print(f"RSA加密失败: {e}") + return "" + + def rsa_decrypt(self, encrypted_data, private_key_str): + """RSA私钥解密""" + try: + encrypted_bytes = base64.b64decode(encrypted_data) + rsa_key = RSA.import_key(private_key_str) + cipher = PKCS1_v1_5.new(rsa_key) + decrypted = cipher.decrypt(encrypted_bytes, None) + return decrypted.decode('utf-8') if decrypted else "" + except Exception as e: + print(f"RSA解密失败: {e}") + return "" + + def get_md5(self, text): + return hashlib.md5(text.encode()).hexdigest().upper() # 与Java一致大写 + + # ---------- 业务方法(不变) ---------- + def homeContent(self, filter): + result = {} + classes = [ + {"type_name": "电影", "type_id": "1"}, + {"type_name": "电视剧", "type_id": "2"}, + {"type_name": "动漫", "type_id": "4"}, + {"type_name": "综艺", "type_id": "3"}, + {"type_name": "短剧", "type_id": "64"}, + {"type_name": "音乐", "type_id": "72"}, {"type_name": "AI漫剧", "type_id": "74"}, {"type_name": "电影解说", "type_id": "73"}, {"type_name": "体育解说", "type_id": "71"}, {"type_name": "电竞解说", "type_id": "70"} + ] + + result['class'] = classes + filters = {} + for cate in classes: + tid = cate['type_id'] + filters[tid] = [ + {"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": "其他"} + ]}, + {"key": "year", "name": "年份", "value": [ + {"n": "全部", "v": "0"}, {"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": "2013", "v": "2013"}, {"n": "2012", "v": "2012"}, + {"n": "2011", "v": "2011"}, {"n": "2010", "v": "2010"}, {"n": "2009", "v": "2009"}, + {"n": "2008", "v": "2008"}, {"n": "2007", "v": "2007"}, {"n": "2006", "v": "2006"}, + {"n": "2005", "v": "2005"}, {"n": "更早", "v": "2004"} + ]}, + {"key": "sort", "name": "排序", "value": [ + {"n": "最新", "v": "d_id"}, {"n": "最热", "v": "d_hits"}, {"n": "推荐", "v": "d_score"} + ]} + ] + result['filters'] = filters + return result + + def homeVideoContent(self): + return {'list': []} + + def categoryContent(self, tid, pg, filter, extend): + videos = [] + try: + body = { + "area": extend.get('area', '0'), + "year": extend.get('year', '0'), + "pageSize": "30", + "sort": extend.get('sort', 'd_id'), + "page": str(pg), + "tid": tid + } + cache_key = f"category_{tid}_{pg}_{hash(str(body))}" + data = self.get_cached_data(cache_key, body, '/App/IndexList/indexList') + if data and 'list' in data: + for item in data['list']: + vod_continu = item.get('vod_continu', 0) + remarks = '电影' if vod_continu == 0 else f'更新至{vod_continu}集' + video = { + "vod_id": f"{item.get('vod_id', '')}/{vod_continu}", + "vod_name": item.get('vod_name', ''), + "vod_pic": item.get('vod_pic', ''), + "vod_remarks": remarks + } + videos.append(video) + except Exception as e: + print(f"获取分类内容失败: {e}") + return {'list': videos, 'page': int(pg), 'pagecount': 9999, 'limit': 30, 'total': 999999} + + def detailContent(self, ids): + try: + vod_id = ids[0].split('/')[0] + t = str(int(time.time())) + body1 = {"token_id": self.token_id, "vod_id": vod_id, "mobile_time": t, "token": self.token} + qdata = self.get_data(body1, '/App/IndexPlay/playInfo') + body2 = {"vurl_cloud_id": "2", "vod_d_id": vod_id} + jdata = self.get_data(body2, '/App/Resource/Vurl/show') + if not qdata or 'vodInfo' not in qdata: + return {'list': []} + vod = qdata['vodInfo'] + video_detail = { + "vod_id": vod_id, + "vod_name": vod.get('vod_name', ''), + "vod_pic": vod.get('vod_pic', ''), + "vod_year": vod.get('vod_year', ''), + "vod_area": vod.get('vod_area', ''), + "vod_actor": vod.get('vod_actor', ''), + "vod_director": vod.get('vod_director', ''), + "vod_content": vod.get('vod_use_content', '').strip(), + "vod_play_from": "瓜瓜" + } + play_list = [] + if jdata and 'list' in jdata: + for index, item in enumerate(jdata['list']): + if 'play' in item: + n, p = [], [] + for key, value in item['play'].items(): + if 'param' in value and value['param']: + n.append(key) + p.append(value['param']) + if p: + play_name = str(index + 1) if len(jdata['list']) != 1 else vod.get('vod_name', '') + play_url = f"{p[-1]}||{'@'.join(n)}" + play_list.append(f"{play_name}${play_url}") + video_detail["vod_play_url"] = "#".join(play_list) + return {'list': [video_detail]} + except Exception as e: + print(f"获取详情失败: {e}") + return {'list': []} + + def searchContent(self, key, quick, pg=1): + videos = [] + try: + body = {"keywords": key, "order_val": "1", "page": str(pg)} + data = self.get_data(body, '/App/Index/findMoreVod', use_cache=False) + if data and 'list' in data: + for item in data['list']: + vod_continu = item.get('vod_continu', 0) + remarks = '电影' if vod_continu == 0 else f'更新至{vod_continu}集' + videos.append({ + "vod_id": f"{item.get('vod_id', '')}/{vod_continu}", + "vod_name": item.get('vod_name', ''), + "vod_pic": item.get('vod_pic', ''), + "vod_remarks": remarks + }) + except Exception as e: + print(f"搜索失败: {e}") + return {'list': videos, 'page': int(pg), 'pagecount': 9999, 'limit': 30, 'total': 999999} + + def playerContent(self, flag, id, vipFlags): + try: + parts = id.split('||') + if len(parts) < 2: + return {"parse": 0, "playUrl": "", "url": ""} + param_str = parts[0] + resolutions = parts[1].split('@') if len(parts) > 1 else [] + params = {} + for pair in param_str.split('&'): + if '=' in pair: + key, value = pair.split('=', 1) + params[key] = value + if resolutions: + resolutions.sort(key=lambda x: int(x) if x.isdigit() else 0, reverse=True) + params['resolution'] = resolutions[0] + data = self.get_data(params, '/App/Resource/VurlDetail/showOne', use_cache=False) + if data and 'url' in data: + return {"parse": 0, "playUrl": "", "url": data['url'], + "header": json.dumps({"User-Agent": "Lavf/57.83.100", "Referer": "http://WJiZxLXA2.com/"}), 'danmaku': 'http://127.0.0.1:9978/proxy?do=diydanmu'} + return {"parse": 0, "playUrl": "", "url": ""} + except Exception as e: + print(f"播放解析失败: {e}") + return {"parse": 0, "playUrl": "", "url": ""} + + def isVideoFormat(self, url): + video_formats = ['.m3u8', '.mp4', '.avi', '.mkv', '.flv', '.ts'] + return any(url.lower().endswith(fmt) for fmt in video_formats) + + def manualVideoCheck(self): + pass + + def localProxy(self, params): + return None + + def get_cached_data(self, cache_key, data, path): + current_time = time.time() + if cache_key in self.cache: + cached_data, timestamp = self.cache[cache_key] + if current_time - timestamp < self.cache_timeout: + return cached_data + result = self.get_data(data, path) + if result: + self.cache[cache_key] = (result, current_time) + return result + +if __name__ == '__main__': + pass \ No newline at end of file diff --git a/py/金牌影视.py b/py/金牌影视.py new file mode 100644 index 0000000..815951a --- /dev/null +++ b/py/金牌影视.py @@ -0,0 +1,225 @@ +# -*- coding: utf-8 -*- +# by @嗷呜 +import json +import sys +import threading +import uuid +import requests +sys.path.append('..') +from base.spider import Spider +import time +from Crypto.Hash import MD5, SHA1 + +class Spider(Spider): + ''' + 配置示例: + { + "key": "xxxx", + "name": "xxxx", + "type": 3, + "api": ".所在路径/金牌.py", + "searchable": 1, + "quickSearch": 1, + "filterable": 1, + "changeable": 1, + "ext": { + "site": "https://www.jiabaide.cn,域名2,域名3" + } + }, + ''' + def init(self, extend=""): + if extend: + hosts=json.loads(extend)['site'] + self.host = self.host_late(hosts) + pass + + def getName(self): + pass + + def isVideoFormat(self, url): + pass + + def manualVideoCheck(self): + pass + + def destroy(self): + pass + + def homeContent(self, filter): + cdata = self.fetch(f"{self.host}/api/mw-movie/anonymous/get/filer/type", headers=self.getheaders()).json() + fdata = self.fetch(f"{self.host}/api/mw-movie/anonymous/v1/get/filer/list", headers=self.getheaders()).json() + result = {} + classes = [] + filters={} + for k in cdata['data']: + classes.append({ + 'type_name': k['typeName'], + 'type_id': str(k['typeId']), + }) + sort_values = [{"n": "最近更新", "v": "2"},{"n": "人气高低", "v": "3"}, {"n": "评分高低", "v": "4"}] + for tid, d in fdata['data'].items(): + current_sort_values = sort_values.copy() + if tid == '1': + del current_sort_values[0] + filters[tid] = [ + {"key": "type", "name": "类型", + "value": [{"n": i["itemText"], "v": i["itemValue"]} for i in d["typeList"]]}, + + *([] if not d["plotList"] else [{"key": "v_class", "name": "剧情", + "value": [{"n": i["itemText"], "v": i["itemText"]} + for i in d["plotList"]]}]), + + {"key": "area", "name": "地区", + "value": [{"n": i["itemText"], "v": i["itemText"]} for i in d["districtList"]]}, + + {"key": "year", "name": "年份", + "value": [{"n": i["itemText"], "v": i["itemText"]} for i in d["yearList"]]}, + + {"key": "lang", "name": "语言", + "value": [{"n": i["itemText"], "v": i["itemText"]} for i in d["languageList"]]}, + + {"key": "sort", "name": "排序", "value": current_sort_values} + ] + result['class'] = classes + result['filters'] = filters + return result + + def homeVideoContent(self): + data1 = self.fetch(f"{self.host}/api/mw-movie/anonymous/v1/home/all/list", headers=self.getheaders()).json() + data2=self.fetch(f"{self.host}/api/mw-movie/anonymous/home/hotSearch",headers=self.getheaders()).json() + data=[] + for i in data1['data'].values(): + data.extend(i['list']) + data.extend(data2['data']) + vods=self.getvod(data) + return {'list':vods} + + def categoryContent(self, tid, pg, filter, extend): + + params = { + "area": extend.get('area', ''), + "filterStatus": "1", + "lang": extend.get('lang', ''), + "pageNum": pg, + "pageSize": "30", + "sort": extend.get('sort', '1'), + "sortBy": "1", + "type": extend.get('type', ''), + "type1": tid, + "v_class": extend.get('v_class', ''), + "year": extend.get('year', '') + } + data = self.fetch(f"{self.host}/api/mw-movie/anonymous/video/list?{self.js(params)}", headers=self.getheaders(params)).json() + result = {} + result['list'] = self.getvod(data['data']['list']) + result['page'] = pg + result['pagecount'] = 9999 + result['limit'] = 90 + result['total'] = 999999 + return result + + def detailContent(self, ids): + data=self.fetch(f"{self.host}/api/mw-movie/anonymous/video/detail?id={ids[0]}",headers=self.getheaders({'id':ids[0]})).json() + vod=self.getvod([data['data']])[0] + vod['vod_play_from']='金牌' + vod['vod_play_url'] = '#'.join( + f"{i['name'] if len(vod['episodelist']) > 1 else vod['vod_name']}${ids[0]}@@{i['nid']}" for i in + vod['episodelist']) + vod.pop('episodelist', None) + return {'list':[vod]} + + def searchContent(self, key, quick, pg="1"): + params = { + "keyword": key, + "pageNum": pg, + "pageSize": "8", + "sourceCode": "1" + } + data=self.fetch(f"{self.host}/api/mw-movie/anonymous/video/searchByWord?{self.js(params)}",headers=self.getheaders(params)).json() + vods=self.getvod(data['data']['result']['list']) + return {'list':vods,'page':pg} + + def playerContent(self, flag, id, vipFlags): + self.header = { + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; ) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.61 Chrome/126.0.6478.61 Not/A)Brand/8 Safari/537.36', + 'sec-ch-ua-platform': '"Windows"', + 'DNT': '1', + 'sec-ch-ua': '"Not/A)Brand";v="8", "Chromium";v="126", "Google Chrome";v="126"', + 'sec-ch-ua-mobile': '?0', + 'Origin': self.host, + 'Referer': f'{self.host}/' + } + ids=id.split('@@') + pdata = self.fetch(f"{self.host}/api/mw-movie/anonymous/v2/video/episode/url?clientType=1&id={ids[0]}&nid={ids[1]}",headers=self.getheaders({'clientType':'1','id': ids[0], 'nid': ids[1]})).json() + vlist=[] + for i in pdata['data']['list']:vlist.extend([i['resolutionName'],i['url']]) + return {'parse':0,'url':vlist,'header':self.header} + + def localProxy(self, param): + pass + + def host_late(self, url_list): + if isinstance(url_list, str): + urls = [u.strip() for u in url_list.split(',')] + else: + urls = url_list + if len(urls) <= 1: + return urls[0] if urls else '' + + results = {} + threads = [] + + def test_host(url): + try: + start_time = time.time() + response = requests.head(url, timeout=1.0, allow_redirects=False) + delay = (time.time() - start_time) * 1000 + results[url] = delay + except Exception as e: + results[url] = float('inf') + for url in urls: + t = threading.Thread(target=test_host, args=(url,)) + threads.append(t) + t.start() + for t in threads: + t.join() + return min(results.items(), key=lambda x: x[1])[0] + + def md5(self, sign_key): + md5_hash = MD5.new() + md5_hash.update(sign_key.encode('utf-8')) + md5_result = md5_hash.hexdigest() + return md5_result + + def js(self, param): + return '&'.join(f"{k}={v}" for k, v in param.items()) + + def getheaders(self, param=None): + if param is None:param = {} + t=str(int(time.time()*1000)) + param['key']='cb808529bae6b6be45ecfab29a4889bc' + param['t']=t + sha1_hash = SHA1.new() + sha1_hash.update(self.md5(self.js(param)).encode('utf-8')) + sign = sha1_hash.hexdigest() + deviceid = str(uuid.uuid4()) + headers = { + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; ) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.61 Chrome/126.0.6478.61 Not/A)Brand/8 Safari/537.36', + 'Accept': 'application/json, text/plain, */*', + 'sign': sign, + 't': t, + 'deviceid':deviceid + } + return headers + + def convert_field_name(self, field): + field = field.lower() + if field.startswith('vod') and len(field) > 3: + field = field.replace('vod', 'vod_') + if field.startswith('type') and len(field) > 4: + field = field.replace('type', 'type_') + return field + + def getvod(self, array): + return [{self.convert_field_name(k): v for k, v in item.items()} for item in array] +