feat: add 四万影视 spider scaffold

This commit is contained in:
Harold
2026-04-24 18:17:21 +08:00
parent dcf57a11e1
commit c17dd94b2d
2 changed files with 224 additions and 0 deletions
+58
View File
@@ -0,0 +1,58 @@
import json
import unittest
from importlib.machinery import SourceFileLoader
from pathlib import Path
from types import SimpleNamespace
from unittest.mock import patch
ROOT = Path(__file__).resolve().parents[1]
MODULE = SourceFileLoader("siwan_spider", str(ROOT / "四万影视.py")).load_module()
Spider = MODULE.Spider
class TestSiWanSpider(unittest.TestCase):
def setUp(self):
Spider._instance = None
self.spider = Spider()
self.spider.init()
def _response(self, status_code=200, payload=None, text=None):
body = text if text is not None else json.dumps(payload or {}, ensure_ascii=False)
return SimpleNamespace(status_code=status_code, text=body)
def test_home_content_exposes_expected_classes_and_filters(self):
content = self.spider.homeContent(False)
self.assertEqual(
[item["type_id"] for item in content["class"]],
["20", "30", "39", "45", "32"],
)
self.assertEqual(
[item["key"] for item in content["filters"]["20"]],
["subType", "year", "sort"],
)
self.assertEqual(content["filters"]["20"][0]["init"], "20")
self.assertEqual(content["filters"]["20"][1]["value"][0], {"n": "全部", "v": ""})
self.assertEqual(content["filters"]["20"][2]["value"][0], {"n": "时间", "v": "time"})
def test_home_video_content_returns_empty_list(self):
self.assertEqual(self.spider.homeVideoContent(), {"list": []})
@patch.object(Spider, "fetch")
def test_api_get_requests_maccms_endpoint_with_browser_headers(self, mock_fetch):
mock_fetch.return_value = self._response(payload={"list": []})
data = self.spider._api_get({"ac": "detail", "pg": 1})
self.assertEqual(data, {"list": []})
self.assertEqual(mock_fetch.call_args.args[0], "https://40000.me/api/maccms")
self.assertEqual(mock_fetch.call_args.kwargs["params"], {"ac": "detail", "pg": 1})
self.assertEqual(mock_fetch.call_args.kwargs["headers"]["Referer"], "https://40000.me/")
@patch.object(Spider, "fetch")
def test_api_get_rejects_non_200_and_non_object_body(self, mock_fetch):
mock_fetch.return_value = self._response(status_code=500, payload={"msg": "bad"})
with self.assertRaises(ValueError):
self.spider._api_get({"ac": "detail"})
mock_fetch.return_value = self._response(text="[]")
with self.assertRaises(ValueError):
self.spider._api_get({"ac": "detail"})
+166
View File
@@ -0,0 +1,166 @@
# 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://40000.me"
self.api = self.host + "/api/maccms"
self.fallback_pic = self.host + "/public/favicon.png"
self.headers = {
"User-Agent": (
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
"AppleWebKit/537.36 (KHTML, like Gecko) "
"Chrome/146.0.0.0 Safari/537.36 Edg/146.0.0.0"
),
"Referer": self.host + "/",
"Accept": "*/*",
}
self.classes = [
{"type_id": "20", "type_name": "电影"},
{"type_id": "30", "type_name": "电视剧"},
{"type_id": "39", "type_name": "动漫"},
{"type_id": "45", "type_name": "综艺"},
{"type_id": "32", "type_name": "欧美"},
]
self.filter_type_options = {
"20": [
{"n": "全部", "v": "20"},
{"n": "动作片", "v": "21"},
{"n": "喜剧片", "v": "22"},
{"n": "恐怖片", "v": "23"},
{"n": "科幻片", "v": "24"},
{"n": "爱情片", "v": "25"},
{"n": "剧情片", "v": "26"},
{"n": "战争片", "v": "27"},
{"n": "纪录片", "v": "28"},
{"n": "理论片", "v": "29"},
{"n": "预告片", "v": "52"},
{"n": "电影解说", "v": "51"},
],
"30": [
{"n": "全部", "v": "30"},
{"n": "国产剧", "v": "31"},
{"n": "欧美剧", "v": "32"},
{"n": "香港剧", "v": "33"},
{"n": "韩国剧", "v": "34"},
{"n": "台湾剧", "v": "35"},
{"n": "日本剧", "v": "36"},
{"n": "海外剧", "v": "37"},
{"n": "泰国剧", "v": "38"},
{"n": "短剧大全", "v": "58"},
],
"39": [
{"n": "全部", "v": "39"},
{"n": "国产动漫", "v": "40"},
{"n": "日韩动漫", "v": "41"},
{"n": "欧美动漫", "v": "42"},
{"n": "港台动漫", "v": "43"},
{"n": "海外动漫", "v": "44"},
{"n": "动画片", "v": "50"},
],
"45": [
{"n": "全部", "v": "45"},
{"n": "大陆综艺", "v": "46"},
{"n": "港台综艺", "v": "47"},
{"n": "日韩综艺", "v": "48"},
{"n": "欧美综艺", "v": "49"},
],
"32": [
{"n": "全部", "v": "32"},
{"n": "欧美剧", "v": "32"},
{"n": "欧美动漫", "v": "42"},
{"n": "欧美综艺", "v": "49"},
{"n": "海外剧", "v": "37"},
],
}
self.type_name_map = {item["type_id"]: item["type_name"] for item in self.classes}
self.subtype_parent_map = {
"20": "20",
"21": "20",
"22": "20",
"23": "20",
"24": "20",
"25": "20",
"26": "20",
"27": "20",
"28": "20",
"29": "20",
"51": "20",
"52": "20",
"30": "30",
"31": "30",
"32": "32",
"33": "30",
"34": "30",
"35": "30",
"36": "30",
"37": "32",
"38": "30",
"58": "30",
"39": "39",
"40": "39",
"41": "39",
"42": "32",
"43": "39",
"44": "39",
"50": "39",
"45": "45",
"46": "45",
"47": "45",
"48": "45",
"49": "32",
}
self.filters = self._build_filters()
def init(self, extend=""):
return None
def getName(self):
return self.name
def _build_filters(self):
years = [{"n": "全部", "v": ""}]
for year in range(2026, 1999, -1):
years.append({"n": str(year), "v": str(year)})
sort_values = [
{"n": "时间", "v": "time"},
{"n": "人气", "v": "hits"},
{"n": "评分", "v": "score"},
{"n": "点赞", "v": "up"},
]
filters = {}
for item in self.classes:
type_id = item["type_id"]
filters[type_id] = [
{"key": "subType", "name": "分类", "init": type_id, "value": self.filter_type_options[type_id]},
{"key": "year", "name": "年代", "init": "", "value": years},
{"key": "sort", "name": "排序", "init": "time", "value": list(sort_values)},
]
return filters
def homeContent(self, filter):
return {"class": self.classes, "filters": self.filters}
def homeVideoContent(self):
return {"list": []}
def _api_get(self, params):
response = self.fetch(
self.api,
params=params,
headers=dict(self.headers),
timeout=10,
)
if response.status_code != 200:
raise ValueError("api request failed")
data = json.loads(response.text or "{}")
if not isinstance(data, dict):
raise ValueError("api response is not object")
return data