diff --git a/py/山楂影视.py b/py/山楂影视.py
new file mode 100644
index 0000000..c8c7ab1
--- /dev/null
+++ b/py/山楂影视.py
@@ -0,0 +1,350 @@
+ # -*- coding: utf-8 -*-
+# 本资源来源于互联网公开渠道,仅可用于个人学习爬虫技术。
+# 严禁将其用于任何商业用途,下载后请于 24 小时内删除,搜索结果均来自源站,本人不承担任何责任。
+#junyouyun
+
+import sys
+import json
+import time
+import base64
+import hashlib
+import urllib3
+import concurrent.futures
+from urllib.parse import quote
+from base.spider import Spider
+from Crypto.PublicKey import RSA
+from Crypto.Cipher import PKCS1_v1_5
+
+urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
+sys.path.append('..')
+
+class Spider(Spider):
+ host, userid, episode_list = '', '', []
+
+ # ---------- 加密与签名相关常量 ----------
+ PUB_KEY_B64 = "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCoYt0BP77U+DM08BiI/QbSRIfxijXo85BTPqIM1Ow8BNwhLETzRIZ+dEwdWDbydG/PspgBAfRpGaYVdJYtvaC2JnoO8+Ik6qMWojfEJxSFLa0Pb0A892tun4gsxoEMjcreZ+YGyaBxAfqX0BSMfdrOgIYaZQjYrw9TRLlUT31QoQIDAQAB"
+ APP_SIGN_SHA1 = "09a8dc51639a31801af5f6418caebfabc695eb24"
+ DEVICE_ID = "2d590b9842d064a1"
+
+ # RSA 私钥(用于解密响应)
+ PRIV_KEY_B64 = """MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCquQQ5r6+yJI8CDFkXRp8vUsdD45ov8EP12ooLs56ca2DQXaSNGS9910bAPVA9chkp0mKIvKqjAsHz5Tl9EeNPblarGEeJUIxpxZtiSqNTpvtiD/TjhpzuHYic7RAfQ/h7p/ypE8ymU42pYjsB5t26Mv6XgkLV+jzrSf73HlCuS0iMyLmt6zz3Mw9izM13EpB8iFLtfbbYymycKTx4RAmPQLwhNGex/AlUIYxXP4R2yyaa4W6mEtc6aME2QuzJFxPgP3HJ9NBx/LWVn4skxWjZ7zg+VRQRHnjyVaSLu3Z5gN5ITWCyE32qaHJa6WBahZj5jWhRyAG1bQ+xKJa8lBL5AgMBAAECggEAUwv9SjJ0PSwbhNuM2w23kcWquROWhYtTA91zGY4esehqB/IFgb2mpIh8Gje5OKqwIu/8jpd4SiOlRYdUF8sD0DfUYRZGdj2AkFNX6tBz8tVfo6wvbB6naA1lzzBij1L5JO3qsjS3cJFkb+kg2yP66AC2Z+0tpfk8eRhdtshAZwfcd1DEGt1uAvYL1eaUK9HRvpt9lPeGcHERDl2hBd4uyaF0K1O+zF9y59nYbTySWPxRZq3sFEE85xRMlstD7YZi7W2gKvMFRD4/FKmrZ3m7aKJRITtyKOyyPcYmepNv3Qv7kk59Pg38n2WWQ0Ra/bCH3E48YNCnQvZMpitkTfJhoQKBgQDbnROOYTP8OTJ6f/qhoGjxeO3x1VOaOp8l0x7b0SCfoqNGS0Cyiqj72BmJtPMPqSTjn6MmNzqbg1KOdhXyzNozs+i5ccW1M56j96mr5I/Z0FpE3oyIHNfDDBlf9M8YQqEF9oYxniYYft9oapO7cRQkHER6qpvnHTavwlv4m78CXwKBgQDHAjs2YlpKDdI1lcbZJCc7TwtH+Pd2bUki8YXafWNcPhITQHbOZjr310eK1QJC6GJncjkOqbX7yv3ivvTO35FZTQhuA1xEG1P00FG8bE0tHYPIwQHi9y0eA5cieMdo8E6XYria1mw/3fqSQEsfZyJlR32JQIoGAipM8iO1X2nZpwKBgDkMFIhnt5lNQk+P7wsNIDWZtDWdtJnboHuy29E+Abt2A/O+mI/IdRz2hau/1WO8DFkUnszOi+rZshhPlGP90rCbi1igtTrcrdjp/KkqNjPea5R4OwkgdOu1uOG0NheXNzzVTQaWjk7Opjn5dWa7eP/oV+GFb/oZHJuLYVizHGsBAoGADA7rjZEKDYCm4w5PPSr+oY5ZjaPdQrS+gLqHtMRyN82fBMGcMUdqfUfzEstzVqCEDeaS5HuOBlK3bXzKkppjUTjksN3NQmcxgBz7RuJ9DqXCLXDcb2cwuafYCYOt+YLOEEgwDVm+t2P44dG5e46hO+fICH/7nP+WlpD5buz4GfMCgYB57r3g/6hi9WUDnfc7ZAzWMqR0EhJVYKYy+KFEtdIPzhkkIHq5RASe88E9kzoGoZFdb3tIjvGZWcHerirrqWkMsuQtP/Qi0zjieid5tAPj+r4kbiCVTw0E0jnmPBzGInQi7lpeTTKnG1fbyS5lBS+WmHfIuzpECgCkxhaT+LJJkg=="""
+
+ headers = {
+ 'User-Agent': "okhttp/4.12.0",
+ 'Connection': "Keep-Alive",
+ 'Accept-Encoding': "gzip",
+ 'Content-Type': "application/json;charset=UTF-8",
+ 'Cache-Control': "no-cache",
+ 'token': "",
+ 'deviceId': DEVICE_ID,
+ 'client': "app",
+ 'deviceType': "Android"
+ }
+
+ # ---------- RSA 加密 ----------
+ def rsa_encrypt(self, data: str) -> str:
+ key = RSA.import_key(base64.b64decode(self.PUB_KEY_B64))
+ cipher = PKCS1_v1_5.new(key)
+ encrypted = cipher.encrypt(data.encode('utf-8'))
+ return base64.b64encode(encrypted).decode('utf-8')
+
+ # ---------- RSA 解密(支持分块) ----------
+ def rsa_decrypt(self, encrypted_b64: str) -> str:
+ key = RSA.import_key(base64.b64decode(self.PRIV_KEY_B64))
+ cipher = PKCS1_v1_5.new(key)
+ encrypted_bytes = base64.b64decode(encrypted_b64)
+ block_size = 256
+ decrypted_parts = []
+ for i in range(0, len(encrypted_bytes), block_size):
+ block = encrypted_bytes[i:i+block_size]
+ decrypted_parts.append(cipher.decrypt(block, None))
+ return b''.join(decrypted_parts).decode('utf-8')
+
+ # ---------- 构建签名参数 ----------
+ def build_params_string(self, episode_id="", episode_index="", vid="", player_id="", type_id="", user_id=""):
+ return (f"episodeId{episode_id}"
+ f"episodeIndex{episode_index}"
+ f"id{vid}"
+ f"playerId{player_id}"
+ f"source0"
+ f"typeId{type_id}"
+ f"userId{user_id}")
+
+ def generate_sign(self, timestamp: str, params_str: str, device_id: str) -> str:
+ raw = f"SaltLSFBTimestamp{timestamp}Params{params_str}ClientappDeviceId{device_id}"
+ b64 = base64.b64encode(raw.encode('utf-8')).decode('utf-8')
+ md5 = hashlib.md5(b64.encode('utf-8')).hexdigest().upper()
+ return md5
+
+ def build_encrypted_headers(self, body_json: str, params_str: str) -> dict:
+ timestamp = str(int(time.time()))
+ encrypted_key = self.rsa_encrypt(body_json)
+ snjm = self.rsa_encrypt("113")
+ appsign = self.rsa_encrypt(self.APP_SIGN_SHA1)
+ sign = self.generate_sign(timestamp, params_str, self.DEVICE_ID)
+
+ headers = {
+ "snjm": snjm,
+ "appsign": appsign,
+ "timestamp": timestamp,
+ "sign": sign,
+ "deviceId": self.DEVICE_ID,
+ "token": self.headers.get('token', ''),
+ "client": "app",
+ "deviceType": "Android",
+ "Content-Type": "application/json;charset=UTF-8",
+ "Cache-Control": "no-cache",
+ "User-Agent": "okhttp/4.12.0"
+ }
+ return headers, {"key": encrypted_key}
+
+ # ---------- 原有接口(保持不变) ----------
+ def init(self, extend=''):
+ self.headers['deviceId'] = self.DEVICE_ID
+ self.host = 'http://qkys.qukanwh.com'
+ response = self.fetch(f'{self.host}/api/v1/app/user/visitorInfo', headers=self.headers).json()
+ self.userid = response['data']['id']
+ token = response['data']['token']
+ self.headers['token'] = token
+
+ def homeContent(self, filter):
+ response = self.post(f'{self.host}/api/v1/app/screen/screenType', headers=self.headers).json()
+ data = response['data']
+ classes = []
+ for i in data:
+ classes.append({'type_id': i['id'], 'type_name': i['name']})
+ return {'class': classes}
+
+ def homeVideoContent(self):
+ response = self.post(f'{self.host}/api/v1/app/recommend/recommendList', headers=self.headers).json()
+ data = response['data']
+ videos = []
+ with concurrent.futures.ThreadPoolExecutor() as executor:
+ future_to_id = {
+ executor.submit(
+ self.post,
+ f'{self.host}/api/v1/app/recommend/recommendSubList',
+ data=json.dumps({
+ "condition": item['id'],
+ "pageNum": 1,
+ "pageSize": 6
+ }),
+ headers=self.headers
+ ): item['id'] for item in data
+ }
+ for future in concurrent.futures.as_completed(future_to_id):
+ try:
+ response = future.result().json()
+ for video in response['data']['records']:
+ videos.append({
+ "vod_id": video['id'],
+ "vod_name": video['name'],
+ "vod_pic": video['cover']
+ })
+ except Exception as e:
+ print(f"Request failed for item {future_to_id[future]}: {str(e)}")
+ return {'list': videos}
+
+ def categoryContent(self, tid, pg, filter, extend):
+ payload = {
+ "condition": {
+ "classify": "",
+ "region": "",
+ "sreecnTypeEnum": "NEWEST",
+ "typeId": tid,
+ "year": ""
+ },
+ "pageNum": pg,
+ "pageSize": 40
+ }
+ response = self.post(f'{self.host}/api/v1/app/screen/screenMovie', data=json.dumps(payload), headers=self.headers).json()
+ videos = []
+ for i in response['data']['records']:
+ videos.append({
+ "vod_id": i['id'],
+ "vod_name": i['name'],
+ "vod_pic": i['cover'],
+ "vod_remarks": i['area'],
+ "vod_year": i['year']
+ })
+ return {'list': videos, 'page': pg}
+
+ def searchContent(self, key, quick, pg='1'):
+ payload = {
+ "condition": {
+ "value": key
+ },
+ "pageNum": pg,
+ "pageSize": 40
+ }
+ response = self.post(f'{self.host}/api/v1/app/search/searchMovie', data=json.dumps(payload), headers=self.headers).json()
+ videos = []
+ for i in response['data']['records']:
+ videos.append({
+ 'vod_id': i['id'],
+ 'vod_name': i['name'],
+ 'vod_pic': i['cover'],
+ 'vod_remarks': i['area'],
+ 'vod_year': i['year'],
+ 'vod_area': i['area'],
+ 'vod_content': i['desc']
+ })
+ return {'list': videos, 'page': pg}
+
+ # ---------- 详情页(已集成解密) ----------
+ def detailContent(self, ids):
+ type_id = "M15" # 注意:原脚本写死为 M17,可根据需要修改
+ vid = ids[0]
+ body = {
+ "id": vid,
+ "source": 0,
+ "typeId": type_id,
+ "userId": self.userid,
+ "episodeId": "",
+ "episodeIndex": "",
+ "playerId": ""
+ }
+ body_json = json.dumps(body, separators=(',', ':'))
+ params_str = self.build_params_string(
+ episode_id="",
+ episode_index="",
+ vid=str(vid),
+ player_id="",
+ type_id=type_id,
+ user_id=str(self.userid)
+ )
+ headers, payload = self.build_encrypted_headers(body_json, params_str)
+
+ # 发送加密请求
+ resp_raw = self.post(f'{self.host}/api/v1/app/play/movieDetails', data=json.dumps(payload), headers=headers).json()
+ encrypted_data = resp_raw.get('data')
+ if not encrypted_data:
+ raise Exception("响应中 data 为空")
+ # 解密 data 字段
+ decrypted_json_str = self.rsa_decrypt(encrypted_data)
+ data = json.loads(decrypted_json_str)
+
+ # 后续处理与原脚本相同
+ currentplayerid = data['playerId']
+ play_urls = []
+ play_url = []
+ show = []
+ for i in data['episodeList']:
+ play_url.append(f"{i['episode']}${ids[0]}@{currentplayerid}@{i['id']}@episode")
+ play_urls.append('#'.join(play_url))
+ moviePlayerList = data['moviePlayerList']
+ for i2 in moviePlayerList:
+ if i2['id'] == currentplayerid:
+ show.append(i2['moviePlayerName'])
+ for j in moviePlayerList:
+ playerid = j['id']
+ episodeTotal = j.get('episodeTotal')
+ if playerid == currentplayerid or episodeTotal is None:
+ continue
+ play_url = []
+ for k in range(1, episodeTotal + 1):
+ play_url.append(f"第{k}集${k}@{playerid}@{ids[0]}@virtual")
+ play_urls.append('#'.join(play_url))
+ if j['moviePlayerName'] not in show:
+ show.append(j['moviePlayerName'])
+
+ # 获取简介(此接口可能无需加密,保持原样)
+ payload_desc = {
+ "id": ids[0],
+ "typeId": type_id
+ }
+ response_desc = self.post(f'{self.host}/api/v1/app/play/movieDesc', data=json.dumps(payload_desc), headers=self.headers).json()
+ data2 = response_desc['data']
+
+ video = {
+ 'vod_id': data2['id'],
+ 'vod_name': data2['name'],
+ 'vod_pic': data2['cover'],
+ 'vod_content': data2['introduce'],
+ 'vod_year': data2['year'],
+ 'vod_area': data2['area'],
+ 'vod_remarks': '',
+ 'vod_score': data2['score'],
+ 'type_name': data2['classify'],
+ 'vod_director': data2['director'],
+ 'vod_actor': data2['star'],
+ 'vod_play_from': '$$$'.join(show),
+ 'vod_play_url': '$$$'.join(play_urls)
+ }
+ return {'list': [video]}
+
+ # ---------- 播放页(已集成解密) ----------
+ def playerContent(self, flag, id, vipflags):
+ param, playerid, param2, param3 = id.split('@')
+ if param3 == 'virtual':
+ payload = {
+ "episodeIndex": str(int(param) - 1),
+ "id": int(param2),
+ "playerId": playerid,
+ "source": 0,
+ "typeId": "M15",
+ "userId": self.userid,
+ "episodeId": ""
+ }
+ else:
+ payload = {
+ "episodeId": param2,
+ "id": int(param),
+ "playerId": playerid,
+ "source": 0,
+ "typeId": "M15",
+ "userId": self.userid,
+ "episodeIndex": ""
+ }
+ body_json = json.dumps(payload, separators=(',', ':'))
+ print(body_json)
+ params_str = self.build_params_string(
+ episode_id=payload.get("episodeId", ""),
+ episode_index=payload.get("episodeIndex", ""),
+ vid=str(payload["id"]),
+ player_id=payload["playerId"],
+ type_id=payload["typeId"],
+ user_id=str(payload["userId"])
+ )
+ print(params_str)
+ headers, encrypted_payload = self.build_encrypted_headers(body_json, params_str)
+ print(headers)
+ print(encrypted_payload)
+ # 获取播放信息(加密响应)
+ resp_raw = self.post(f'{self.host}/api/v1/app/play/movieDetails', data=json.dumps(encrypted_payload), headers=headers).json()
+ encrypted_data = resp_raw.get('data')
+ if not encrypted_data:
+ raise Exception("响应中 data 为空")
+ decrypted_json_str = self.rsa_decrypt(encrypted_data)
+ data = json.loads(decrypted_json_str)
+ print(data)
+ parse_url = data['url']
+ playerid = data['playerId']
+
+ # 调用分析接口(注:analysisMovieUrl 的响应可能也是加密的,但原脚本直接取 data,这里暂不做额外解密)
+ analysis_body = {
+ "playerUrl": parse_url,
+ "playerId": playerid
+ }
+ analysis_json = json.dumps(analysis_body, separators=(',', ':'))
+ # analysisMovieUrl 接口的参数拼接?理论上也需要签名,但原脚本是 GET 方式,为了兼容,我们沿用原脚本的 GET 方式
+ # 原脚本使用 fetch GET 带参数,并未加密。这里也采用 GET 方式,不使用加密 headers
+ resp_analysis = self.fetch(f"{self.host}/api/v1/app/play/analysisMovieUrl?playerUrl={quote(parse_url,safe='')}&playerId={playerid}", headers=self.headers).json()
+ url = resp_analysis.get('data')
+
+ return {'jx': '0', 'parse': '0', 'url': url, 'header': {'User-Agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 13_2_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.3 Mobile/15E148 Safari/604.1'}}
+
+ def getName(self):
+ pass
+
+ def isVideoFormat(self, url):
+ pass
+
+ def manualVideoCheck(self):
+ pass
+
+ def destroy(self):
+ pass
+
+ def localProxy(self, param):
+ pass
\ No newline at end of file
diff --git a/py/悟空影院.py b/py/悟空影院.py
new file mode 100644
index 0000000..fb0673a
--- /dev/null
+++ b/py/悟空影院.py
@@ -0,0 +1,545 @@
+# -*- coding: utf-8 -*-
+"""
+悟空影视爬虫
+站点: https://www.yikucun.com
+基于 MacCMS (苹果CMS) 程序,HTML 解析方式
+OK影视 / 海阔视界 兼容版
+"""
+
+import re
+import time
+import json
+import urllib.parse
+
+import requests
+
+try:
+ from base.spider import Spider as BaseSpider
+except ImportError:
+ class BaseSpider:
+ pass
+
+
+class Spider(BaseSpider):
+ BASE_URL = "https://www.yikucun.com"
+
+ # 分类映射
+ TYPE_MAP = {
+ "1": "电影",
+ "2": "电视剧",
+ "3": "综艺",
+ "4": "动漫",
+ "5": "短剧",
+ }
+
+ # 筛选条件 - 各分类的类型和地区
+ FILTERS = {
+ "1": { # 电影
+ "类型": ["全部", "动作", "喜剧", "爱情", "科幻", "恐怖", "剧情", "战争", "犯罪", "奇幻", "悬疑", "动画", "恐怖", "纪录片", "其他"],
+ "地区": ["全部", "大陆", "香港", "台湾", "日本", "韩国", "美国", "法国", "英国", "德国", "泰国", "印度", "其他"],
+ "年份": ["全部", "2026", "2025", "2024", "2023", "2022", "2021", "2020", "2019", "2018", "2017", "2016", "2015", "2014", "2013", "2012", "2011", "2010"],
+ },
+ "2": { # 电视剧
+ "类型": ["全部", "古装", "战争", "青春偶像", "喜剧", "家庭", "犯罪", "动作", "奇幻", "剧情", "历史", "经典", "乡村", "情景", "商战", "网剧", "其他"],
+ "地区": ["全部", "内地", "韩国", "香港", "台湾", "日本", "美国", "泰国", "英国", "新加坡", "其他"],
+ "年份": ["全部", "2026", "2025", "2024", "2023", "2022", "2021", "2020", "2019", "2018", "2017", "2016", "2015", "2014", "2013", "2012", "2011", "2010"],
+ },
+ "3": { # 综艺
+ "类型": ["全部", "真人秀", "脱口秀", "访谈", "美食", "旅游", "选秀", "情感", "音乐", "舞蹈", "其他"],
+ "地区": ["全部", "大陆", "香港", "台湾", "日本", "韩国", "欧美", "其他"],
+ "年份": ["全部", "2026", "2025", "2024", "2023", "2022", "2021", "2020", "2019", "2018", "2017", "2016", "2015"],
+ },
+ "4": { # 动漫
+ "类型": ["全部", "日本动漫", "国产动漫", "欧美动漫", "其他"],
+ "地区": ["全部", "日本", "大陆", "美国", "其他"],
+ "年份": ["全部", "2026", "2025", "2024", "2023", "2022", "2021", "2020", "2019", "2018", "2017", "2016", "2015"],
+ },
+ "5": { # 短剧
+ "类型": ["全部", "其他"],
+ "地区": ["全部", "其他"],
+ "年份": ["全部", "2026", "2025", "2024", "2023"],
+ },
+ }
+
+ def __init__(self):
+ super().__init__()
+ self.name = ""
+ self.error_play_url = "https://kjjsaas-sh.oss-cn-shanghai.aliyuncs.com/u/3401405881/20240818-936952-fc31b16575e80a7562cdb1f81a39c6b0.mp4"
+ 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",
+ "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8",
+ "Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8",
+ "Referer": "https://www.yikucun.com/",
+ "Connection": "keep-alive",
+ })
+ self._init_cookies()
+
+ def getName(self):
+ return self.name
+
+ def init(self, extend="{}"):
+ try:
+ self.extend = json.loads(extend)
+ self.name = self.extend.get("name", "")
+ except Exception as e:
+ print(e)
+ self.extend = {}
+
+ def _init_cookies(self):
+ """初始化 cookies(处理 508 反爬)"""
+ try:
+ r = self.session.get(self.BASE_URL, timeout=10, verify=False)
+ if r.status_code == 508:
+ time.sleep(1)
+ self.session.get(self.BASE_URL, timeout=10, verify=False)
+ except Exception:
+ pass
+
+ def _get(self, url, **kwargs):
+ """GET 请求,自动处理 508"""
+ try:
+ r = self.session.get(url, timeout=15, verify=False, **kwargs)
+ if r.status_code == 508:
+ time.sleep(0.5)
+ r = self.session.get(url, timeout=15, verify=False, **kwargs)
+ r.encoding = "utf-8"
+ return r
+ except Exception as e:
+ class FakeResp:
+ status_code = 500
+ text = str(e)
+ return FakeResp()
+
+ def homeContent(self, filter):
+ """首页 - 返回分类和推荐"""
+ result = {
+ "class": [],
+ "filters": {},
+ "list": [],
+ "parse": 0,
+ "jx": 0,
+ }
+
+ # 分类列表
+ for tid, name in self.TYPE_MAP.items():
+ result["class"].append({
+ "type_id": tid,
+ "type_name": name,
+ })
+
+ # 筛选条件
+ for tid, flist in self.FILTERS.items():
+ result["filters"][tid] = []
+ for fname, fvalues in flist.items():
+ result["filters"][tid].append({
+ "key": fname,
+ "name": fname,
+ "value": [{"n": v, "v": v} for v in fvalues],
+ })
+
+ # 首页推荐
+ try:
+ r = self._get(f"{self.BASE_URL}/")
+ if r.status_code == 200:
+ result["list"] = self._parse_list(r.text)
+ except Exception:
+ pass
+
+ return result
+
+ def categoryContent(self, tid, pg, filter, extend):
+ """分类页"""
+ result = {
+ "list": [],
+ "parse": 0,
+ "jx": 0,
+ }
+
+ # URL 格式: 12个位置,用 - 分隔
+ # 位置: 1=tid, 2=地区, 3=空, 4=类型, 5=空, 6=空, 7=空, 8=空, 9=页码, 10=空, 11=空, 12=年份
+ area = extend.get("地区", "") if isinstance(extend, dict) else ""
+ type_val = extend.get("类型", "") if isinstance(extend, dict) else ""
+ year = extend.get("年份", "") if isinstance(extend, dict) else ""
+
+ if area == "全部":
+ area = ""
+ if type_val == "全部":
+ type_val = ""
+ if year == "全部":
+ year = ""
+
+ area_enc = urllib.parse.quote(area) if area else ""
+ type_enc = urllib.parse.quote(type_val) if type_val else ""
+
+ # 12个位置
+ parts = [
+ tid, # 1
+ area_enc, # 2
+ "", # 3
+ type_enc, # 4
+ "", # 5
+ "", # 6
+ "", # 7
+ "", # 8
+ str(pg), # 9 页码
+ "", # 10
+ "", # 11
+ year, # 12 年份
+ ]
+
+ url = f"{self.BASE_URL}/ucusw/{'-'.join(parts)}.html"
+
+ try:
+ r = self._get(url)
+ if r.status_code == 200:
+ result["list"] = self._parse_list(r.text)
+ except Exception:
+ pass
+
+ return result
+
+ def detailContent(self, ids):
+ """详情页"""
+ result = {
+ "list": [],
+ "parse": 0,
+ "jx": 0,
+ }
+
+ vid = ids[0]
+ url = f"{self.BASE_URL}/ucudt/{vid}.html"
+
+ try:
+ r = self._get(url)
+ if r.status_code == 200:
+ detail = self._parse_detail(r.text, vid)
+ if detail:
+ result["list"].append(detail)
+ except Exception:
+ pass
+
+ return result
+
+ def searchContent(self, key, quick, pg="1"):
+ """搜索"""
+ result = {
+ "list": [],
+ "parse": 0,
+ "jx": 0,
+ }
+
+ try:
+ # 搜索 URL: /ucusc/-------------.html?wd=关键词
+ # 分页: /ucusc/-------------(页码).html?wd=关键词
+ if int(pg) > 1:
+ search_url = f"{self.BASE_URL}/ucusc/-------------{pg}.html?wd={urllib.parse.quote(key)}"
+ else:
+ search_url = f"{self.BASE_URL}/ucusc/-------------.html?wd={urllib.parse.quote(key)}"
+ r = self._get(search_url)
+ if r.status_code == 200:
+ result["list"] = self._parse_list(r.text)
+ except Exception:
+ pass
+
+ return result
+
+ def playerContent(self, flag, id, vipFlags):
+ """播放页 - 获取播放地址"""
+ result = {
+ "url": self.error_play_url,
+ "parse": 0,
+ "jx": 0,
+ "header": {},
+ }
+
+ try:
+ # id 格式: 视频ID-线路ID-集数ID
+ parts = id.split("-")
+ if len(parts) >= 3:
+ vid, sid, nid = parts[0], parts[1], parts[2]
+ play_url = f"{self.BASE_URL}/ucupy/{vid}-{sid}-{nid}.html"
+ r = self._get(play_url)
+ if r.status_code == 200:
+ url = self._parse_play_url(r.text)
+ if url:
+ result["url"] = url
+ result["parse"] = 0
+ except Exception:
+ pass
+
+ return result
+
+ def localProxy(self, params):
+ return 0
+
+ def _parse_list(self, html):
+ """解析列表页"""
+ items = []
+
+ # 找到所有 class="name" 的标题,然后往前找最近的图片
+ name_pattern = r'class="name"[^>]*>\s*]*href="/ucudt/(\d+)\.html"[^>]*>([^<]+)'
+
+ # 先找出所有 name 的位置
+ name_matches = list(re.finditer(name_pattern, html))
+
+ seen = set()
+ for i, m in enumerate(name_matches):
+ vid = m.group(1)
+ name = m.group(2).strip()
+
+ if vid in seen:
+ continue
+ seen.add(vid)
+
+ # 往前找最近的图片(在这个 name 之前)
+ pos = m.start()
+ start = max(0, pos - 2000)
+ segment = html[start:pos]
+
+ # 找所有图片
+ img_matches = re.findall(r']*src="([^"]+)"', segment)
+ pic = img_matches[-1] if img_matches else ""
+
+ if pic and not pic.startswith("http"):
+ pic = "https:" + pic if pic.startswith("//") else pic
+
+ # 找备注
+ remarks = ""
+ rgba_match = re.search(r'class="rgba[^"]*"[^>]*>([^<]+)', segment)
+ if rgba_match:
+ remarks = rgba_match.group(1).strip()
+
+ items.append({
+ "vod_id": vid,
+ "vod_name": name,
+ "vod_pic": pic,
+ "vod_remarks": remarks,
+ })
+
+ return items[:30]
+
+ def _parse_detail(self, html, vid):
+ """解析详情页"""
+ detail = {
+ "vod_id": vid,
+ "vod_name": "",
+ "vod_pic": "",
+ "type_name": "",
+ "vod_year": "",
+ "vod_area": "",
+ "vod_remarks": "",
+ "vod_actor": "",
+ "vod_director": "",
+ "vod_content": "",
+ "vod_play_from": "",
+ "vod_play_url": "",
+ }
+
+ # 标题
+ title_match = re.search(r'