feat: scaffold wooyun spider menu parsing
This commit is contained in:
@@ -0,0 +1,113 @@
|
|||||||
|
import unittest
|
||||||
|
from importlib.machinery import SourceFileLoader
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[1]
|
||||||
|
MODULE = SourceFileLoader("wooyun_spider", str(ROOT / "乌云影视.py")).load_module()
|
||||||
|
Spider = MODULE.Spider
|
||||||
|
|
||||||
|
|
||||||
|
class TestWooyunSpider(unittest.TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
Spider._instance = None
|
||||||
|
self.spider = Spider()
|
||||||
|
self.spider.init()
|
||||||
|
|
||||||
|
def test_home_content_builds_classes_and_filters_from_menu(self):
|
||||||
|
menu = [
|
||||||
|
{"code": "movie", "name": "电影"},
|
||||||
|
{"code": "tv_series", "name": "电视剧"},
|
||||||
|
{"code": "animation", "name": "动画"},
|
||||||
|
{"code": "variety", "name": "综艺"},
|
||||||
|
{"code": "short_drama", "name": "短剧"},
|
||||||
|
{
|
||||||
|
"nameEn": "year",
|
||||||
|
"children": [
|
||||||
|
{"code": "THIS_YEAR", "name": "今年"},
|
||||||
|
{"code": "LAST_YEAR", "name": "去年"},
|
||||||
|
{"code": "2024", "name": "2024"},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"nameEn": "region",
|
||||||
|
"children": [{"code": "CN", "name": "大陆"}],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"nameEn": "genre",
|
||||||
|
"children": [{"code": "COMEDY", "name": "喜剧"}],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"nameEn": "language",
|
||||||
|
"children": [{"code": "ZH", "name": "国语"}],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
self.spider._request_json = lambda path, method="GET", data=None, headers=None: menu
|
||||||
|
content = self.spider.homeContent(False)
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
[item["type_id"] for item in content["class"]],
|
||||||
|
["movie", "tv_series", "animation", "variety", "short_drama", "THIS_YEAR", "LAST_YEAR"],
|
||||||
|
)
|
||||||
|
self.assertEqual(content["filters"]["movie"][0]["key"], "year")
|
||||||
|
self.assertEqual(content["filters"]["movie"][1]["key"], "region")
|
||||||
|
self.assertEqual(content["filters"]["movie"][2]["key"], "genre")
|
||||||
|
self.assertEqual(content["filters"]["movie"][3]["key"], "lang")
|
||||||
|
self.assertEqual(content["filters"]["movie"][4]["key"], "sort")
|
||||||
|
|
||||||
|
def test_request_json_uses_post_for_json_payload(self):
|
||||||
|
calls = {}
|
||||||
|
|
||||||
|
class FakeResponse:
|
||||||
|
def __init__(self):
|
||||||
|
self.status_code = 200
|
||||||
|
self.text = ""
|
||||||
|
|
||||||
|
def json(self):
|
||||||
|
return {"data": {"records": []}}
|
||||||
|
|
||||||
|
def fake_post(
|
||||||
|
url,
|
||||||
|
json=None,
|
||||||
|
headers=None,
|
||||||
|
timeout=5,
|
||||||
|
verify=True,
|
||||||
|
stream=False,
|
||||||
|
allow_redirects=True,
|
||||||
|
params=None,
|
||||||
|
data=None,
|
||||||
|
cookies=None,
|
||||||
|
):
|
||||||
|
calls["url"] = url
|
||||||
|
calls["json"] = json
|
||||||
|
calls["headers"] = headers
|
||||||
|
return FakeResponse()
|
||||||
|
|
||||||
|
self.spider.post = fake_post
|
||||||
|
result = self.spider._request_json(
|
||||||
|
"/movie/media/search",
|
||||||
|
method="POST",
|
||||||
|
data={"pageIndex": "1"},
|
||||||
|
headers={"Content-Type": "application/json"},
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(calls["url"], "https://wooyun.tv/movie/media/search")
|
||||||
|
self.assertEqual(calls["json"], {"pageIndex": "1"})
|
||||||
|
self.assertEqual(calls["headers"]["Origin"], "https://wooyun.tv")
|
||||||
|
self.assertEqual(result, {"records": []})
|
||||||
|
|
||||||
|
def test_build_category_body_handles_year_shortcuts_and_filters(self):
|
||||||
|
body = self.spider._build_category_body(
|
||||||
|
"THIS_YEAR",
|
||||||
|
"3",
|
||||||
|
{"region": "CN", "year": "2025", "lang": "ZH", "sort": "hot"},
|
||||||
|
)
|
||||||
|
self.assertEqual(body["menuCodeList"], ["THIS_YEAR", "CN", "2025", "ZH"])
|
||||||
|
self.assertEqual(body["pageIndex"], "3")
|
||||||
|
self.assertEqual(body["sortCode"], "hot")
|
||||||
|
self.assertEqual(body["topCode"], "movie")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
+190
@@ -0,0 +1,190 @@
|
|||||||
|
# coding=utf-8
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from base.spider import Spider as BaseSpider
|
||||||
|
|
||||||
|
sys.path.append("..")
|
||||||
|
|
||||||
|
|
||||||
|
class Spider(BaseSpider):
|
||||||
|
def __init__(self):
|
||||||
|
self.name = "乌云影视"
|
||||||
|
self.host = "https://wooyun.tv"
|
||||||
|
self.page_size = 24
|
||||||
|
self.home_limit = 12
|
||||||
|
self.headers = {
|
||||||
|
"Accept": "application/json, text/plain, */*",
|
||||||
|
"Origin": self.host,
|
||||||
|
"Referer": self.host + "/",
|
||||||
|
"User-Agent": (
|
||||||
|
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
|
||||||
|
"AppleWebKit/537.36 (KHTML, like Gecko) "
|
||||||
|
"Chrome/124.0.0.0 Safari/537.36"
|
||||||
|
),
|
||||||
|
}
|
||||||
|
self.sort_options = [
|
||||||
|
{"n": "默认", "v": "default"},
|
||||||
|
{"n": "最新", "v": "latest"},
|
||||||
|
{"n": "最热", "v": "hot"},
|
||||||
|
{"n": "评分", "v": "score"},
|
||||||
|
]
|
||||||
|
|
||||||
|
def init(self, extend=""):
|
||||||
|
return None
|
||||||
|
|
||||||
|
def getName(self):
|
||||||
|
return self.name
|
||||||
|
|
||||||
|
def _stringify(self, value):
|
||||||
|
return "" if value is None else str(value)
|
||||||
|
|
||||||
|
def _ensure_list(self, value):
|
||||||
|
return value if isinstance(value, list) else []
|
||||||
|
|
||||||
|
def _normalize_ext(self, extend):
|
||||||
|
if isinstance(extend, dict):
|
||||||
|
return extend
|
||||||
|
if not extend:
|
||||||
|
return {}
|
||||||
|
try:
|
||||||
|
return json.loads(str(extend))
|
||||||
|
except Exception:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
def _request_json(self, path, method="GET", data=None, headers=None):
|
||||||
|
url = path if str(path).startswith("http") else self.host + path
|
||||||
|
merged_headers = dict(self.headers)
|
||||||
|
if headers:
|
||||||
|
merged_headers.update(headers)
|
||||||
|
|
||||||
|
if method == "POST":
|
||||||
|
response = self.post(url, json=data, headers=merged_headers, timeout=15)
|
||||||
|
else:
|
||||||
|
response = self.fetch(url, headers=merged_headers, timeout=15)
|
||||||
|
|
||||||
|
if response.status_code < 200 or response.status_code >= 300:
|
||||||
|
raise ValueError(f"{method} {path} => HTTP {response.status_code}")
|
||||||
|
|
||||||
|
payload = response.json()
|
||||||
|
if payload.get("code") not in (None, 200) and payload.get("isSuccess") is False:
|
||||||
|
raise ValueError(payload.get("resultMsg") or str(payload.get("code")))
|
||||||
|
return payload.get("data")
|
||||||
|
|
||||||
|
def _build_classes(self, menu):
|
||||||
|
classes = []
|
||||||
|
for code, fallback in [
|
||||||
|
("movie", "电影"),
|
||||||
|
("tv_series", "电视剧"),
|
||||||
|
("animation", "动画"),
|
||||||
|
("variety", "综艺"),
|
||||||
|
("short_drama", "短剧"),
|
||||||
|
]:
|
||||||
|
found = next((item for item in self._ensure_list(menu) if item.get("code") == code), None)
|
||||||
|
classes.append(
|
||||||
|
{"type_id": code, "type_name": self._stringify((found or {}).get("name") or fallback)}
|
||||||
|
)
|
||||||
|
|
||||||
|
year_group = next((item for item in self._ensure_list(menu) if item.get("nameEn") == "year"), {})
|
||||||
|
year_children = self._ensure_list(year_group.get("children"))
|
||||||
|
this_year = next((item for item in year_children if item.get("code") == "THIS_YEAR"), {})
|
||||||
|
last_year = next((item for item in year_children if item.get("code") == "LAST_YEAR"), {})
|
||||||
|
classes.append({"type_id": "THIS_YEAR", "type_name": self._stringify(this_year.get("name") or "今年")})
|
||||||
|
classes.append({"type_id": "LAST_YEAR", "type_name": self._stringify(last_year.get("name") or "去年")})
|
||||||
|
return classes
|
||||||
|
|
||||||
|
def _build_filters(self, menu, classes):
|
||||||
|
option_map = {"genre": [], "region": [], "year": [], "language": []}
|
||||||
|
for group in self._ensure_list(menu):
|
||||||
|
values = []
|
||||||
|
for item in self._ensure_list(group.get("children")):
|
||||||
|
code = self._stringify(item.get("code"))
|
||||||
|
name = self._stringify(item.get("name") or item.get("nameEn") or code)
|
||||||
|
if code and name and code not in ("THIS_YEAR", "LAST_YEAR"):
|
||||||
|
values.append({"n": name, "v": code})
|
||||||
|
if group.get("nameEn") in option_map:
|
||||||
|
option_map[group.get("nameEn")] = values
|
||||||
|
|
||||||
|
filters = {}
|
||||||
|
for cls in classes:
|
||||||
|
items = []
|
||||||
|
if option_map["year"]:
|
||||||
|
items.append(
|
||||||
|
{
|
||||||
|
"key": "year",
|
||||||
|
"name": "年份",
|
||||||
|
"init": "",
|
||||||
|
"value": [{"n": "全部", "v": ""}] + option_map["year"],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
if option_map["region"]:
|
||||||
|
items.append(
|
||||||
|
{
|
||||||
|
"key": "region",
|
||||||
|
"name": "地区",
|
||||||
|
"init": "",
|
||||||
|
"value": [{"n": "全部", "v": ""}] + option_map["region"],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
if option_map["genre"] and cls["type_id"] in (
|
||||||
|
"movie",
|
||||||
|
"tv_series",
|
||||||
|
"animation",
|
||||||
|
"variety",
|
||||||
|
"short_drama",
|
||||||
|
):
|
||||||
|
items.append(
|
||||||
|
{
|
||||||
|
"key": "genre",
|
||||||
|
"name": "类型",
|
||||||
|
"init": "",
|
||||||
|
"value": [{"n": "全部", "v": ""}] + option_map["genre"],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
if option_map["language"]:
|
||||||
|
items.append(
|
||||||
|
{
|
||||||
|
"key": "lang",
|
||||||
|
"name": "语言",
|
||||||
|
"init": "",
|
||||||
|
"value": [{"n": "全部", "v": ""}] + option_map["language"],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
items.append({"key": "sort", "name": "排序", "init": "default", "value": self.sort_options})
|
||||||
|
filters[cls["type_id"]] = items
|
||||||
|
return filters
|
||||||
|
|
||||||
|
def _map_top_code(self, category_id):
|
||||||
|
mapping = {
|
||||||
|
"movie": "movie",
|
||||||
|
"tv_series": "tv_series",
|
||||||
|
"animation": "animation",
|
||||||
|
"variety": "variety",
|
||||||
|
"short_drama": "short_drama",
|
||||||
|
"THIS_YEAR": "movie",
|
||||||
|
"LAST_YEAR": "movie",
|
||||||
|
}
|
||||||
|
return mapping.get(category_id, "movie")
|
||||||
|
|
||||||
|
def _build_category_body(self, category_id, page, extend):
|
||||||
|
filters = self._normalize_ext(extend)
|
||||||
|
menu_code_list = []
|
||||||
|
if category_id not in ("movie", "tv_series", "animation", "variety", "short_drama"):
|
||||||
|
menu_code_list.append(category_id)
|
||||||
|
for key in ("genre", "region", "year", "lang", "other"):
|
||||||
|
value = self._stringify(filters.get(key))
|
||||||
|
if value and value not in menu_code_list:
|
||||||
|
menu_code_list.append(value)
|
||||||
|
return {
|
||||||
|
"menuCodeList": menu_code_list,
|
||||||
|
"pageIndex": self._stringify(page or 1),
|
||||||
|
"pageSize": self.page_size,
|
||||||
|
"searchKey": "",
|
||||||
|
"sortCode": "" if filters.get("sort") in (None, "", "default") else self._stringify(filters.get("sort")),
|
||||||
|
"topCode": self._map_top_code(category_id),
|
||||||
|
}
|
||||||
|
|
||||||
|
def homeContent(self, filter):
|
||||||
|
menu = self._request_json("/movie/category/menu")
|
||||||
|
classes = self._build_classes(menu)
|
||||||
|
return {"class": classes, "filters": self._build_filters(menu, classes)}
|
||||||
Reference in New Issue
Block a user