星星短剧
This commit is contained in:
@@ -0,0 +1,144 @@
|
||||
import unittest
|
||||
from importlib.machinery import SourceFileLoader
|
||||
from pathlib import Path
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
MODULE = SourceFileLoader("xingxingduanju_spider", str(ROOT / "星星短剧.py")).load_module()
|
||||
Spider = MODULE.Spider
|
||||
|
||||
|
||||
class TestXingXingDuanJuSpider(unittest.TestCase):
|
||||
def setUp(self):
|
||||
Spider._instance = None
|
||||
self.spider = Spider()
|
||||
self.spider.init()
|
||||
|
||||
def test_home_content_exposes_fixed_categories(self):
|
||||
content = self.spider.homeContent(False)
|
||||
self.assertEqual(content["class"][0]["type_id"], "1287")
|
||||
self.assertEqual(content["class"][-1]["type_name"], "古代")
|
||||
self.assertEqual(content["filters"], {})
|
||||
|
||||
@patch.object(Spider, "_get_json")
|
||||
def test_home_video_content_uses_default_category(self, mock_json):
|
||||
mock_json.return_value = {
|
||||
"data": {
|
||||
"datalist": [
|
||||
{
|
||||
"id": 1001,
|
||||
"name": "闪婚后成了大佬心尖宠",
|
||||
"icon": "http://img/1.jpg",
|
||||
"introduction": "剧情简介",
|
||||
"heat": 291.6,
|
||||
}
|
||||
],
|
||||
"page": {"totalCount": 34},
|
||||
}
|
||||
}
|
||||
result = self.spider.homeVideoContent()
|
||||
self.assertEqual(result["list"][0]["vod_id"], "1001@@闪婚后成了大佬心尖宠@@剧情简介")
|
||||
self.assertEqual(result["list"][0]["vod_name"], "闪婚后成了大佬心尖宠")
|
||||
self.assertEqual(result["list"][0]["vod_remarks"], "291.6万播放")
|
||||
self.assertIn("resourceId=1287", mock_json.call_args.args[0])
|
||||
self.assertIn("pageNum=1", mock_json.call_args.args[0])
|
||||
|
||||
@patch.object(Spider, "_get_json")
|
||||
def test_category_content_builds_query_and_maps_items(self, mock_json):
|
||||
mock_json.return_value = {
|
||||
"data": {
|
||||
"datalist": [
|
||||
{
|
||||
"id": 2002,
|
||||
"name": "宸王的复仇王妃",
|
||||
"icon": "http://img/2.jpg",
|
||||
"introduction": "王妃复仇记",
|
||||
"heat": 398.3,
|
||||
}
|
||||
],
|
||||
"page": {"totalCount": 34},
|
||||
}
|
||||
}
|
||||
result = self.spider.categoryContent("1291", "2", False, {})
|
||||
self.assertEqual(result["page"], 2)
|
||||
self.assertEqual(result["limit"], 1)
|
||||
self.assertEqual(result["total"], 34)
|
||||
self.assertEqual(result["list"][0]["vod_id"], "2002@@宸王的复仇王妃@@王妃复仇记")
|
||||
self.assertNotIn("pagecount", result)
|
||||
self.assertIn("resourceId=1291", mock_json.call_args.args[0])
|
||||
self.assertIn("pageNum=2", mock_json.call_args.args[0])
|
||||
self.assertIn("pageSize=10", mock_json.call_args.args[0])
|
||||
|
||||
@patch.object(Spider, "_get_json")
|
||||
def test_detail_content_builds_single_playlist(self, mock_json):
|
||||
mock_json.return_value = {
|
||||
"data": [
|
||||
{
|
||||
"chapterName": "第1集",
|
||||
"shortPlayList": [
|
||||
{
|
||||
"chapterShortPlayVoList": [
|
||||
{"shortPlayUrl": "http://video/1.m3u8"}
|
||||
]
|
||||
}
|
||||
],
|
||||
},
|
||||
{
|
||||
"chapterName": "第2集",
|
||||
"shortPlayList": [
|
||||
{
|
||||
"chapterShortPlayVoList": [
|
||||
{"shortPlayUrl": "http://video/2.m3u8"}
|
||||
]
|
||||
}
|
||||
],
|
||||
},
|
||||
]
|
||||
}
|
||||
result = self.spider.detailContent(["422599939194881@@穿越诡计爱上你@@一段简介"])
|
||||
vod = result["list"][0]
|
||||
self.assertEqual(vod["vod_id"], "422599939194881@@穿越诡计爱上你@@一段简介")
|
||||
self.assertEqual(vod["vod_name"], "穿越诡计爱上你")
|
||||
self.assertEqual(vod["vod_content"], "一段简介")
|
||||
self.assertEqual(vod["vod_play_from"], "星星短剧")
|
||||
self.assertIn("第1集$http://video/1.m3u8", vod["vod_play_url"])
|
||||
self.assertIn("第2集$http://video/2.m3u8", vod["vod_play_url"])
|
||||
self.assertIn("bookId=422599939194881", mock_json.call_args.args[0])
|
||||
|
||||
@patch.object(Spider, "_get_json")
|
||||
def test_detail_content_returns_empty_when_id_missing(self, mock_json):
|
||||
self.assertEqual(self.spider.detailContent([""]), {"list": []})
|
||||
mock_json.assert_not_called()
|
||||
|
||||
def test_search_content_returns_empty(self):
|
||||
self.assertEqual(
|
||||
self.spider.searchContent("任意关键词", False, "3"),
|
||||
{"page": 3, "limit": 0, "total": 0, "list": []},
|
||||
)
|
||||
|
||||
def test_player_content_returns_direct_url(self):
|
||||
result = self.spider.playerContent("星星短剧", "http://video/play.m3u8", {})
|
||||
self.assertEqual(result["parse"], 0)
|
||||
self.assertEqual(result["url"], "http://video/play.m3u8")
|
||||
self.assertIn("Mozilla/5.0", result["header"]["User-Agent"])
|
||||
|
||||
def test_player_content_upgrades_known_cdn_to_https(self):
|
||||
result = self.spider.playerContent(
|
||||
"星星短剧",
|
||||
"http://img.novel.wsljf.xyz/shortPlay-mp4/demo/output.m3u8",
|
||||
{},
|
||||
)
|
||||
self.assertEqual(
|
||||
result["url"],
|
||||
"https://img.novel.wsljf.xyz/shortPlay-mp4/demo/output.m3u8",
|
||||
)
|
||||
|
||||
@patch.object(Spider, "fetch")
|
||||
def test_get_json_returns_empty_on_invalid_json(self, mock_fetch):
|
||||
mock_fetch.return_value = MagicMock(status_code=200, text="<html>blocked</html>")
|
||||
self.assertEqual(self.spider._get_json("http://example.com/api"), {})
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
+180
@@ -0,0 +1,180 @@
|
||||
# coding=utf-8
|
||||
import json
|
||||
import sys
|
||||
from urllib.parse import urlencode
|
||||
|
||||
from base.spider import Spider as BaseSpider
|
||||
|
||||
sys.path.append("..")
|
||||
|
||||
|
||||
HOST = "http://read.api.duodutek.com"
|
||||
UA = (
|
||||
"Mozilla/5.0 (Windows NT 6.1; WOW64) "
|
||||
"AppleWebKit/537.36 (KHTML, like Gecko) "
|
||||
"Chrome/50.0.2661.87 Safari/537.36"
|
||||
)
|
||||
COMMON_PARAMS = {
|
||||
"productId": "2a8c14d1-72e7-498b-af23-381028eb47c0",
|
||||
"vestId": "2be070e0-c824-4d0e-a67a-8f688890cadb",
|
||||
"channel": "oppo19",
|
||||
"osType": "android",
|
||||
"version": "20",
|
||||
"token": "202509271001001446030204698626",
|
||||
}
|
||||
CLASS_LIST = [
|
||||
{"type_id": "1287", "type_name": "甜宠"},
|
||||
{"type_id": "1288", "type_name": "逆袭"},
|
||||
{"type_id": "1289", "type_name": "热血"},
|
||||
{"type_id": "1290", "type_name": "现代"},
|
||||
{"type_id": "1291", "type_name": "古代"},
|
||||
]
|
||||
|
||||
|
||||
class Spider(BaseSpider):
|
||||
def __init__(self):
|
||||
self.name = "星星短剧"
|
||||
self.host = HOST
|
||||
self.headers = {"User-Agent": UA}
|
||||
self.classes = CLASS_LIST
|
||||
|
||||
def init(self, extend=""):
|
||||
return None
|
||||
|
||||
def getName(self):
|
||||
return self.name
|
||||
|
||||
def homeContent(self, filter):
|
||||
return {"class": self.classes, "filters": {}}
|
||||
|
||||
def homeVideoContent(self):
|
||||
return {"list": self._list_category_items("1287", 1)[:12]}
|
||||
|
||||
def categoryContent(self, tid, pg, filter, extend):
|
||||
page = max(1, int(pg))
|
||||
payload = self._get_json(
|
||||
self._build_url(
|
||||
"/novel-api/app/pageModel/getResourceById",
|
||||
{"resourceId": str(tid), "pageNum": str(page), "pageSize": "10"},
|
||||
)
|
||||
)
|
||||
data = payload.get("data") if isinstance(payload, dict) else {}
|
||||
items = [self._map_vod(item) for item in (data.get("datalist") or [])]
|
||||
total = self._coerce_int(
|
||||
data.get("total")
|
||||
or data.get("totalCount")
|
||||
or (data.get("page") or {}).get("totalCount")
|
||||
or payload.get("total")
|
||||
)
|
||||
return {"page": page, "limit": len(items), "total": total, "list": items}
|
||||
|
||||
def detailContent(self, ids):
|
||||
result = {"list": []}
|
||||
for raw_id in ids:
|
||||
vod_id = str(raw_id or "").strip()
|
||||
if not vod_id:
|
||||
continue
|
||||
book_id, book_name, intro = self._split_vod_id(vod_id)
|
||||
payload = self._get_json(
|
||||
self._build_url(
|
||||
"/novel-api/basedata/book/getChapterList",
|
||||
{"bookId": book_id},
|
||||
)
|
||||
)
|
||||
chapters = payload.get("data") if isinstance(payload, dict) else []
|
||||
episodes = []
|
||||
for index, chapter in enumerate(chapters or [], start=1):
|
||||
play_url = self._extract_play_url(chapter)
|
||||
if not play_url:
|
||||
continue
|
||||
chapter_name = str(chapter.get("chapterName") or f"第{index}集").strip()
|
||||
episodes.append(f"{chapter_name}${play_url}")
|
||||
result["list"].append(
|
||||
{
|
||||
"vod_id": vod_id,
|
||||
"vod_name": book_name,
|
||||
"vod_content": intro,
|
||||
"vod_remarks": f"共{len(episodes)}集" if episodes else "",
|
||||
"vod_play_from": "星星短剧",
|
||||
"vod_play_url": "#".join(episodes),
|
||||
}
|
||||
)
|
||||
return result
|
||||
|
||||
def searchContent(self, key, quick, pg="1"):
|
||||
page = max(1, int(pg))
|
||||
return {"page": page, "limit": 0, "total": 0, "list": []}
|
||||
|
||||
def playerContent(self, flag, id, vipFlags):
|
||||
play_url = self._normalize_play_url(id)
|
||||
if not play_url:
|
||||
return {"parse": 0, "jx": 0, "url": "", "header": {}}
|
||||
return {
|
||||
"parse": 0,
|
||||
"jx": 0,
|
||||
"url": play_url,
|
||||
"header": {"User-Agent": UA},
|
||||
}
|
||||
|
||||
def _list_category_items(self, tid, page):
|
||||
payload = self._get_json(
|
||||
self._build_url(
|
||||
"/novel-api/app/pageModel/getResourceById",
|
||||
{"resourceId": str(tid), "pageNum": str(page), "pageSize": "10"},
|
||||
)
|
||||
)
|
||||
data = payload.get("data") if isinstance(payload, dict) else {}
|
||||
return [self._map_vod(item) for item in (data.get("datalist") or [])]
|
||||
|
||||
def _map_vod(self, item):
|
||||
book_id = str(item.get("id") or "").strip()
|
||||
name = str(item.get("name") or "").strip()
|
||||
intro = str(item.get("introduction") or "").strip()
|
||||
heat = str(item.get("heat") or "").strip()
|
||||
return {
|
||||
"vod_id": f"{book_id}@@{name}@@{intro}",
|
||||
"vod_name": name,
|
||||
"vod_pic": str(item.get("icon") or "").strip(),
|
||||
"vod_remarks": f"{heat}万播放" if heat else "",
|
||||
}
|
||||
|
||||
def _split_vod_id(self, vod_id):
|
||||
parts = vod_id.split("@@", 2)
|
||||
if len(parts) == 3:
|
||||
return parts[0], parts[1], parts[2]
|
||||
if len(parts) == 2:
|
||||
return parts[0], parts[1], ""
|
||||
return vod_id, "", ""
|
||||
|
||||
def _extract_play_url(self, chapter):
|
||||
for play_group in chapter.get("shortPlayList") or []:
|
||||
for play in play_group.get("chapterShortPlayVoList") or []:
|
||||
play_url = self._normalize_play_url(play.get("shortPlayUrl"))
|
||||
if play_url:
|
||||
return play_url
|
||||
return ""
|
||||
|
||||
def _normalize_play_url(self, value):
|
||||
play_url = str(value or "").strip()
|
||||
if play_url.startswith("http://img.novel.wsljf.xyz/"):
|
||||
return "https://" + play_url[len("http://"):]
|
||||
return play_url
|
||||
|
||||
def _build_url(self, path, params=None):
|
||||
query = urlencode({**COMMON_PARAMS, **(params or {})})
|
||||
return f"{self.host}{path}?{query}"
|
||||
|
||||
def _coerce_int(self, value):
|
||||
try:
|
||||
return int(value)
|
||||
except (TypeError, ValueError):
|
||||
return 0
|
||||
|
||||
def _get_json(self, url):
|
||||
response = self.fetch(url, headers=dict(self.headers), timeout=10, verify=False)
|
||||
if response.status_code != 200:
|
||||
return {}
|
||||
try:
|
||||
return json.loads(response.text or "{}")
|
||||
except (json.JSONDecodeError, TypeError, ValueError):
|
||||
return {}
|
||||
Reference in New Issue
Block a user