From 68c205819095a5489178db599821a1e07e2d162c Mon Sep 17 00:00:00 2001 From: XC-Pro <19840+xc-pro@noreply.example.org> Date: Fri, 14 Aug 2026 11:25:10 +0200 Subject: [PATCH] =?UTF-8?q?=E4=B8=8A=E4=BC=A0=E6=96=87=E4=BB=B6=E8=87=B3?= =?UTF-8?q?=E3=80=8Cpy=E3=80=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- py/剧爸爸.py | 249 ++++++++++++++++++++++ py/华数.py | 546 +++++++++++++++++++++++++++++++++++++++++++++++++ py/即看.py | 245 ++++++++++++++++++++++ py/红果果.py | 256 +++++++++++++++++++++++ py/金牌影视.py | 225 ++++++++++++++++++++ 5 files changed, 1521 insertions(+) create mode 100644 py/剧爸爸.py create mode 100644 py/华数.py create mode 100644 py/即看.py create mode 100644 py/红果果.py create mode 100644 py/金牌影视.py diff --git a/py/剧爸爸.py b/py/剧爸爸.py new file mode 100644 index 0000000..1326758 --- /dev/null +++ b/py/剧爸爸.py @@ -0,0 +1,249 @@ +import requests +from bs4 import BeautifulSoup +import re +import sys +import json +import urllib.parse +from base.spider import Spider +from urllib.parse import quote, urljoin + +sys.path.append('..') + +xurl = "http://www.dgpengcheng.com" +headers = { + 'User-Agent': 'Mozilla/5.0 (Linux; Android 13; M2102J2SC Build/TKQ1.221114.001; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/144.0.7559.31 Mobile Safari/537.36', + 'Referer': xurl, +} + +class Spider(Spider): + global xurl, headers + + def getName(self): + return "星辰影院" + + def init(self, extend): + pass + + def isVideoFormat(self, url): + pass + + def manualVideoCheck(self): + pass + + def homeContent(self, filter): + classes = [ + {"type_id": "1", "name": "电影"}, + {"type_id": "2", "name": "电视剧"}, + {"type_id": "3", "name": "综艺"}, + {"type_id": "4", "name": "动漫"}, + {"type_id": "28", "name": "纪录片"} + ] + return {'class': classes} + + def _parse_video_items(self, soup): + videos = [] + seen_ids = set() + + for li in soup.select('li'): + a_tag = li.select_one('a.stui-vodlist__thumb') + if not a_tag: + continue + + href = a_tag.get('href', '') + if not href: + continue + vod_id = href if 'http' in href else xurl + href + if vod_id in seen_ids: + continue + seen_ids.add(vod_id) + + img = a_tag.select_one('img') + title = a_tag.get('title', '') + if not title and img: + title = img.get('alt', '') + if not title: + title = a_tag.get_text(strip=True) + if not title: + continue + + pic = a_tag.get('data-original', '') + if not pic and img: + pic = img.get('data-original') or img.get('src', '') + if pic and not pic.startswith('http'): + pic = xurl + ('' if pic.startswith('/') else '/') + pic + + remark = '' + remark_span = a_tag.select_one('span.pic-text') + if remark_span: + remark = remark_span.get_text(strip=True) + + videos.append({ + "vod_id": vod_id, + "vod_name": title, + "vod_pic": pic, + "vod_remarks": remark + }) + return videos + + def homeVideoContent(self): + return {'list': self._parse_video_items(BeautifulSoup(requests.get(xurl, headers=headers).text, 'lxml'))} + + def categoryContent(self, cid, pg, filter, ext): + page = int(pg) if pg else 1 + url = f"{xurl}/vtype/{cid}-{page}.html" + videos = self._parse_video_items(BeautifulSoup(requests.get(url, headers=headers).text, 'lxml')) + return { + 'list': videos, + 'page': page, + 'pagecount': 999, + 'limit': 90, + 'total': 9999 + } + + def detailContent(self, ids): + did = ids[0] + if not did.startswith('http'): + did = urljoin(xurl, did) + resp = requests.get(did, headers=headers, timeout=10) + soup = BeautifulSoup(resp.text, 'lxml') + info = {'vod_id': did} + + thumb = soup.select_one('.stui-content__thumb img') + if thumb: + pic = thumb.get('data-original') or thumb.get('src', '') + if pic and pic.startswith('//'): + pic = 'https:' + pic + elif pic and not pic.startswith('http'): + pic = urljoin(xurl, pic) + info['vod_pic'] = pic + + detail_div = soup.select_one('.stui-content') + if detail_div: + title_h1 = detail_div.select_one('h3.title') + if title_h1: + raw_title = title_h1.get_text(strip=True).replace('\n', '') + info['vod_name'] = raw_title + + for p in detail_div.select('p.data'): + text = p.get_text(strip=True) + if '主演:' in text: + info['vod_actor'] = text.split('主演:')[-1].strip() + if '类型:' in text: + info['type_name'] = text.split('类型:')[-1].strip() + if '导演:' in text: + info['vod_director'] = text.split('导演:')[-1].strip() + if '状态:' in text: + info['vod_remarks'] = text.split('状态:')[-1].strip() + if '年代:' in text: + info['vod_year'] = text.split('年代:')[-1].strip() + if '地区:' in text: + info['vod_area'] = text.split('地区:')[-1].strip() + + content_div = soup.select_one('.detail-content') + if content_div: + p = content_div.find('p') + if p: + text = p.get_text(strip=True) + else: + text = content_div.get_text(strip=True) + text = re.sub(r'^简介[::]\s*', '', text) + info['vod_content'] = text + else: + info['vod_content'] = '' + + filter_lines = ['猜您喜欢', '同类型'] + filter_titles = ['1080P', 'дрр滈凊'] + ktabs = [] + klists = [] + seen_lines = set() + + line_items = soup.select('.stui-pannel__head h3') + playlist_containers = soup.select('.stui-content__playlist') + + for idx, tab in enumerate(line_items): + if idx >= len(playlist_containers): + break + line_name = tab.get_text(strip=True) + if not line_name or line_name in filter_lines: + continue + if line_name in seen_lines: + continue + container = playlist_containers[idx] + episode_links = container.select('li a') + if not episode_links: + continue + klist = [] + for ep in episode_links: + ep_name = ep.get_text(strip=True) + ep_link = ep.get('href', '') + if ep_name in filter_titles: + continue + if ep_name and ep_link: + klist.append(f'{ep_name}${ep_link}') + if klist: + seen_lines.add(line_name) + ktabs.append(line_name) + klists.append('#'.join(klist)) + + info["vod_play_from"] = '$$$'.join(ktabs) + info["vod_play_url"] = '$$$'.join(klists) + + return {'list': [info]} + + def playerContent(self, flag, id, vipFlags): + try: + play_url = id if id.startswith(('http://', 'https://')) else xurl + id + html = requests.get(play_url, headers=headers, timeout=10).text + + pattern = r'var\s+player_\w+\s*=\s*(\{[^;]+?\})\s*(?:;| 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..1ae14b8 --- /dev/null +++ b/py/即看.py @@ -0,0 +1,245 @@ +# -*- coding: utf-8 -*- +import re, json, base64 +import urllib.parse +from base.spider import Spider +from bs4 import BeautifulSoup + + +class Spider(Spider): + headers = { + "User-Agent": "Mozilla/5.0 (Linux; Android 15; Pixel 9) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/150.0.0.0 Mobile Safari/537.36", + "Referer": "https://www.jkan.app/" + } + + def init(self, extend=""): + self.host = "https://www.jkan.app" + self._timeout = 15 + + def _soup(self, html): + return BeautifulSoup(html, "html.parser") if html else None + + def _fetch(self, url): + return self.fetch(url, headers=self.headers, timeout=self._timeout, verify=False) + + def _fix(self, u): + if not u: return "" + if u.startswith("//"): return "https:" + u + if u.startswith("/"): return self.host + u + return u + + def _parse_list(self, html): + videos, seen = [], set() + soup = self._soup(html) + if soup is None: + return videos + for a in soup.select('a[href*="/video/"]'): + href = a.get("href", "") + if not href or href in seen: + continue + if a.get("class") and "text_muted" in a.get("class"): + continue + seen.add(href) + title = a.get("title", "") or a.get_text(strip=True)[:100] + pic = a.get("data-original", "") or a.get("data-background-image", "") + if not pic: + bg = re.search(r"background(?:-image)?\s*:\s*url\([\"']?([^\"')]+)[\"']?\)", a.get("style", "") or "") + if bg: + pic = bg.group(1) + remark = "" + rt = a.select_one("span.xszxj, span.pic_text") + if rt: + remark = rt.get_text(strip=True) + videos.append({"vod_id": href, "vod_name": title, "vod_pic": self._fix(pic), "vod_remarks": remark}) + if len(videos) >= 50: + break + return videos + + def _get_filters(self, tid): + return [ + {"key": "class", "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": "记录"}, + ]}, + {"key": "area", "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": "印度"}, + ]}, + {"key": "year", "name": "年份", "value": + [{"n": "全部", "v": ""}] + [{"n": str(y), "v": str(y)} for y in range(2026, 1999, -1)]}, + {"key": "sort", "name": "排序", "value": [ + {"n": "按时间", "v": "time"}, {"n": "按人气", "v": "hits"}, {"n": "按评分", "v": "score"}, + ]}, + ] + + def homeContent(self, filter): + return { + "class": [ + {"type_id": "1", "type_name": "电影"}, + {"type_id": "2", "type_name": "电视剧"}, + {"type_id": "3", "type_name": "动漫"}, + {"type_id": "4", "type_name": "综艺"}, + ], + "filters": {tid: self._get_filters(tid) for tid in ["1", "2", "3", "4"]} + } + + def homeVideoContent(self): + try: + rsp = self._fetch(self.host) + return {"list": self._parse_list(rsp.text)} + except: + return {"list": []} + + def _build_vodshow_url(self, tid, pg, extend): + segs = [""] * 12 + segs[0] = tid + if extend.get("area"): segs[1] = extend["area"] + if extend.get("sort"): segs[2] = extend["sort"] + if extend.get("class"): segs[3] = extend["class"] + if extend.get("year"): segs[11] = extend["year"] + url = f"{self.host}/show/{'-'.join(segs)}.html" + if pg != "1": + url = f"{self.host}/show/{'-'.join(segs)}-{pg}.html" + return url + + def categoryContent(self, tid, pg, filter, extend): + try: + if filter and any(v for v in extend.values()): + url = self._build_vodshow_url(tid, pg, extend) + else: + url = f"{self.host}/show/{tid}-----------{pg}.html" + rsp = self._fetch(url) + return {"list": self._parse_list(rsp.text), "page": str(pg), "pagecount": 999} + except: + return {"list": [], "page": str(pg), "pagecount": 0} + + def _get_detail(self, vid): + if vid.startswith("/"): + url = self._fix(vid) + elif vid.startswith("http"): + url = vid + else: + url = f"{self.host}/video/{vid}.html" + try: + rsp = self._fetch(url) + except: + return {"vod_id": vid} + html = rsp.text + if re.search(r"(模板文件不存在|系统安全验证)", html, re.I): + return {"vod_id": "0", "vod_name": ""} + vod = {"vod_id": vid} + soup = self._soup(html) + if soup is None: + return vod + vod["vod_name"] = self._og(soup, "og:title") + vod["vod_pic"] = self._og(soup, "og:image") + vod["vod_content"] = self._og(soup, "og:description") + vod["vod_year"] = self._og(soup, "og:video:release_date") + vod["vod_area"] = self._og(soup, "og:video:area") + vod["vod_director"] = self._og(soup, "og:video:director") + vod["vod_actor"] = self._og(soup, "og:video:actor") + vod["type_name"] = self._og(soup, "og:video:class") + score = self._og(soup, "og:video:score") + if score: vod["vod_score"] = score + # extract play sources and episodes + src_tabs = soup.select_one(".play_source_tab") + src_names = [a.get("alt", "").strip() or a.get_text(strip=True) for a in src_tabs.select("a")] if src_tabs else [] + for i, ul in enumerate(soup.select("ul.content_playlist, ul.playlist")): + source = src_names[i] if i < len(src_names) else "默认" + episodes = [] + seen_urls = set() + for a in ul.select("a[href]"): + ep_url = a.get("href", "") + if ep_url in seen_urls: + continue + seen_urls.add(ep_url) + ep_name = a.get_text(strip=True) or "播放" + episodes.append(f"{ep_name}${self._fix(ep_url)}") + if episodes: + vod.setdefault("vod_play_from", []).append(source) + vod.setdefault("vod_play_url", []).append("#".join(episodes)) + if "vod_play_from" in vod: + vod["vod_play_from"] = "$$$".join(vod.pop("vod_play_from")) + vod["vod_play_url"] = "$$$".join(vod.pop("vod_play_url")) + else: + vod["vod_play_from"] = "默认线路" + vod["vod_play_url"] = "" + return vod + + def _og(self, soup, prop): + m = soup.select_one(f'meta[property="{prop}"]') + if m: return m.get("content", "") + m = soup.select_one(f'meta[name="{prop}"]') + if m: return m.get("content", "") + return "" + + def detailContent(self, ids): + return {"list": [self._get_detail(ids[0])]} + + def playerContent(self, flag, id, vipFlags): + play_url = self._fix(id) + if self.isVideoFormat(play_url): + return {"parse": 0, "url": play_url, "header": self.headers} + try: + rsp = self._fetch(play_url) + return self._parse_player(rsp.text, play_url) + except: + return {"parse": 0, "url": play_url, "header": self.headers} + + def _parse_player(self, html, play_url): + match = re.search(r'player_aaaa\s*=\s*(\{[\s\S]*?\});?\s*', html, re.I) + if not match: + return {"parse": 1, "url": play_url, "header": self.headers} + try: + data = json.loads(match.group(1)) + url_enc = data.get("url", "") + enc_type = str(data.get("encrypt", "0")) + if enc_type == "1": + url_enc = urllib.parse.unquote(url_enc) + elif enc_type == "2": + url_enc = urllib.parse.unquote(base64.b64decode(url_enc).decode()) + real_url = self._unescape(url_enc) + return {"parse": 0, "url": real_url or play_url, "header": self.headers} + except: + return {"parse": 0, "url": play_url, "header": self.headers} + + def _unescape(self, s): + result, i, n = [], 0, len(s) + while i < n: + if s[i] == '%' and i + 5 < n and s[i + 1] == 'u': + result.append(chr(int(s[i + 2:i + 6], 16))) + i += 6 + elif s[i] == '%' and i + 2 < n: + result.append(chr(int(s[i + 1:i + 3], 16))) + i += 3 + else: + result.append(s[i]) + i += 1 + return ''.join(result) + + def searchContent(self, key, quick, pg="1"): + # 搜索翻页格式: ---------{N}--- (page1: 13 dashes, pageN: 10 dashes + N + 3 dashes) + try: + kw = urllib.parse.quote(key) + if pg == "1": + url = f"{self.host}/search/{kw}-------------.html" + else: + url = f"{self.host}/search/{kw}----------{pg}---.html" + rsp = self._fetch(url) + videos = self._parse_list(rsp.text) + return {"list": videos, "page": str(pg), "pagecount": 99} + except: + return {"list": [], "pagecount": 0} + + def isVideoFormat(self, url): + return bool(re.search(r'\.(mp4|m3u8|flv|avi|mkv|ts|webm|mpg|mpeg)\b', url, re.I)) + + def destroy(self): + pass + + diff --git a/py/红果果.py b/py/红果果.py new file mode 100644 index 0000000..242348e --- /dev/null +++ b/py/红果果.py @@ -0,0 +1,256 @@ +from base.spider import Spider +import requests +import re +import json +import urllib.parse + +host = "https://www.hongguoguo.tv" +headers = { + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.0.0 Safari/537.36', + 'Referer': host +} +timeout = 10 + +# TVBox 兼容的 header 字符串格式 +def _header_str(): + return f"User-Agent={headers['User-Agent']}&Referer={headers['Referer']}" + + +class Spider(Spider): + def getName(self): + return "红果果短剧" + + def init(self, extend): + pass + + def isVideoFormat(self, url): + pass + + def manualVideoCheck(self): + pass + + def homeContent(self, filter): + # 福利短剧(fuliduanju)需要会员权限,已移除 + classes = [ + {"type_id": "jingxuanduanju", "type_name": "精选短剧"} + ] + return {"class": classes} + + def homeVideoContent(self): + try: + r = requests.get(host, headers=headers, timeout=timeout) + return {'list': self._parse_list(r.text)} + except Exception as e: + print(f"[homeVideoContent] error: {e}") + return {'list': []} + + def categoryContent(self, cid, pg, filter, ext): + page = int(pg) if pg else 1 + if page == 1: + url = f"{host}/vod/show/id/{cid}.html" + else: + url = f"{host}/vod/show/id/{cid}/page/{page}.html" + try: + r = requests.get(url, headers=headers, timeout=timeout) + videos = self._parse_list(r.text) + return { + 'list': videos, + 'page': pg, + 'pagecount': 9999, + 'limit': 24, + 'total': 999999 + } + except Exception as e: + print(f"[categoryContent] error: {e}") + return {'list': []} + + def detailContent(self, ids): + did = str(ids[0]) + try: + play_page = f"{host}/vod/play/id/{did}/sid/1/nid/1.html" + r = requests.get(play_page, headers=headers, timeout=timeout) + text = r.text + + # 权限检查 + if '没有权限' in text or '升级会员' in text: + print(f"[detailContent] ID={did} 需要会员权限") + return {'list': []} + + # 提取 player_aaaa(支持多种闭合标签) + player = None + for pattern in [ + r'var player_aaaa=(\{.*?\})', + r'var player_aaaa=(\{.*?\});', + r'player_aaaa=(\{.*?\})', + ]: + m = re.search(pattern, text, re.DOTALL) + if m: + try: + player = json.loads(m.group(1)) + break + except json.JSONDecodeError: + continue + + if not player: + print(f"[detailContent] ID={did} 未找到 player_aaaa") + return {'list': []} + + vod_data = player.get('vod_data', {}) + play_url = player.get('url', '').replace('\\/', '/') + play_from = player.get('from', 'm3u8') + + if not play_url: + print(f"[detailContent] ID={did} 无播放地址") + return {'list': []} + + # 提取封面 + pic = '' + pic_m = re.search(r'"thumbnailUrl":"([^"]+)"', text) + if pic_m: + pic = pic_m.group(1).replace('\\/', '/') + + # 提取简介 + content = '' + desc_m = re.search(r']*class="[^"]*desc[^"]*"[^>]*>(.*?)

', text, re.DOTALL) + if desc_m: + content = re.sub(r'<[^>]+>', '', desc_m.group(1)).strip() + + # 提取集数按钮 + ep_pattern = r'href="/vod/play/id/' + did + r'/sid/(\d+)/nid/(\d+)\.html"[^>]*>([^<]*)' + episodes = re.findall(ep_pattern, text) + + play_from_list = [] + play_url_list = [] + + if not episodes: + # 单集短剧 + play_from_list.append(play_from) + vod_name = vod_data.get('vod_name', '第1集') + play_url_list.append(f"{vod_name}${play_url}") + else: + # 按线路分组 + lines = {} + for sid, nid, name in episodes: + sid = int(sid) + if sid not in lines: + lines[sid] = [] + lines[sid].append((int(nid), name.strip())) + + for sid in sorted(lines.keys()): + line_eps = sorted(lines[sid], key=lambda x: x[0]) + urls = [] + line_name = None + + for idx, (nid, name) in enumerate(line_eps): + if sid == 1 and nid == 1: + # 当前页面(第一集) + line_name = play_from + urls.append(f"{name}${play_url}") + else: + # 请求其他集页面 + ep_url = f"{host}/vod/play/id/{did}/sid/{sid}/nid/{nid}.html" + try: + rr = requests.get(ep_url, headers=headers, timeout=8) + if '没有权限' in rr.text: + continue + for pattern in [ + r'var player_aaaa=(\{.*?\})', + r'var player_aaaa=(\{.*?\});', + r'player_aaaa=(\{.*?\})', + ]: + mm = re.search(pattern, rr.text, re.DOTALL) + if mm: + try: + ep_player = json.loads(mm.group(1)) + ep_url_real = ep_player.get('url', '').replace('\\/', '/') + if ep_url_real: + urls.append(f"{name}${ep_url_real}") + if line_name is None: + line_name = ep_player.get('from', f'线路{sid}') + break + except json.JSONDecodeError: + continue + except Exception as e: + print(f"[detailContent] 获取集 {sid}/{nid} 失败: {e}") + continue + + if urls and line_name: + play_from_list.append(line_name) + play_url_list.append("#".join(urls)) + + # 如果多线路处理失败,回退到单集 + if not play_from_list: + play_from_list.append(play_from) + vod_name = vod_data.get('vod_name', '第1集') + play_url_list.append(f"{vod_name}${play_url}") + + VOD = { + "vod_id": did, + "vod_name": vod_data.get('vod_name', ''), + "vod_pic": pic, + "vod_actor": vod_data.get('vod_actor', ''), + "vod_director": vod_data.get('vod_director', ''), + "type_name": vod_data.get('vod_class', ''), + "vod_remarks": "短剧", + "vod_content": content, + "vod_play_from": "$$$".join(play_from_list), + "vod_play_url": "$$$".join(play_url_list) + } + return {'list': [VOD]} + except Exception as e: + print(f"[detailContent] 获取详情失败: {e}") + return {'list': []} + + def playerContent(self, flag, id, vipFlags): + print(f"[playerContent] flag={flag}, url={id[:80]}...") + return { + "parse": 0, + "playUrl": '', + "url": id, + "header": _header_str() + } + + def searchContent(self, key, quick, pg=1): + page = int(pg) if pg else 1 + keyword = urllib.parse.quote(key) + if page == 1: + url = f"{host}/vod/search.html?wd={keyword}" + else: + url = f"{host}/vod/search.html?wd={keyword}&page={page}" + try: + r = requests.get(url, headers=headers, timeout=timeout) + videos = self._parse_list(r.text) + return { + 'list': videos, + 'page': page, + 'pagecount': 9999, + 'limit': 24, + 'total': 999999 + } + except Exception as e: + print(f"[searchContent] error: {e}") + return {'list': []} + + def _parse_list(self, text): + videos = [] + pattern = ( + r']*href="/vod/play/id/(\d+)/sid/1/nid/1\.html"[^>]*>' + r'.*?
.*?' + r'
]*>.*?
' + r'.*?([^<]*)' + r'.*?([^<]*)' + r'.*?
.*?' + r'

([^<]*)

' + r'.*?' + ) + matches = re.findall(pattern, text, re.DOTALL) + for m in matches: + vid, pic, score, status, name = m + videos.append({ + "vod_id": vid, + "vod_name": name.strip(), + "vod_pic": pic.strip(), + "vod_remarks": f"{score.strip()} {status.strip()}", + "vod_content": "" + }) + return videos \ 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] +