独播库.py
This commit is contained in:
@@ -4,8 +4,12 @@ from importlib.machinery import SourceFileLoader
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
from Cryptodome.Cipher import AES
|
||||
from Cryptodome.Util.Padding import pad
|
||||
try:
|
||||
from Crypto.Cipher import AES
|
||||
from Crypto.Util.Padding import pad
|
||||
except ModuleNotFoundError:
|
||||
from Cryptodome.Cipher import AES
|
||||
from Cryptodome.Util.Padding import pad
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
+81
-8
@@ -32,7 +32,7 @@ class TestDBKUSpider(unittest.TestCase):
|
||||
cards,
|
||||
[
|
||||
{
|
||||
"vod_id": "https://www.dbku.tv/voddetail/123.html",
|
||||
"vod_id": "123",
|
||||
"vod_name": "示例影片",
|
||||
"vod_pic": "https://img.example/dbku.jpg",
|
||||
"vod_remarks": "更新至10集",
|
||||
@@ -93,7 +93,7 @@ class TestDBKUSpider(unittest.TestCase):
|
||||
</div>
|
||||
"""
|
||||
result = self.spider.searchContent("繁花", False, "1")
|
||||
self.assertEqual(result["list"][0]["vod_id"], "https://www.dbku.tv/voddetail/321.html")
|
||||
self.assertEqual(result["list"][0]["vod_id"], "321")
|
||||
self.assertEqual(result["list"][0]["vod_name"], "搜索影片")
|
||||
|
||||
def test_parse_detail_page_extracts_meta_and_episodes(self):
|
||||
@@ -112,12 +112,66 @@ class TestDBKUSpider(unittest.TestCase):
|
||||
<a href="/vodplay/100-1-1.html">第1集</a>
|
||||
<a href="/vodplay/100-1-2.html">第2集</a>
|
||||
"""
|
||||
result = self.spider._parse_detail_page(html, "https://www.dbku.tv/voddetail/100.html")
|
||||
result = self.spider._parse_detail_page(html, "100")
|
||||
vod = result["list"][0]
|
||||
self.assertEqual(vod["vod_id"], "100")
|
||||
self.assertEqual(vod["vod_name"], "独播剧")
|
||||
self.assertEqual(vod["vod_year"], "2025")
|
||||
self.assertEqual(vod["vod_play_from"], "独播库")
|
||||
self.assertIn("第1集$https://www.dbku.tv/vodplay/100-1-1.html", vod["vod_play_url"])
|
||||
self.assertIn("第1集$100-1-1", vod["vod_play_url"])
|
||||
|
||||
def test_parse_detail_page_extracts_structured_fields_from_detail_block(self):
|
||||
html = """
|
||||
<div class="myui-content__thumb">
|
||||
<img data-original="/poster-ai.jpg" />
|
||||
</div>
|
||||
<div class="myui-content__detail">
|
||||
<h1 class="title">AI教我谈恋爱</h1>
|
||||
<div id="rating" class="score" data-mid="1" data-id="143508" data-score="3">
|
||||
<span class="branch">6</span>
|
||||
</div>
|
||||
<p class="data">
|
||||
<span class="text-muted">分类:</span><a href="/vodshow/21-----------.html">短剧</a>
|
||||
<span class="split-line"></span>
|
||||
<span class="text-muted hidden-xs">地区:</span><a href="/vodshow/21-大陆----------.html">大陆</a>
|
||||
<span class="split-line"></span>
|
||||
<span class="text-muted hidden-xs">年份:</span><a href="/vodshow/21-----------2026.html">2026</a>
|
||||
</p>
|
||||
<p class="data hidden-sm"><span class="text-muted">更新:</span><span class="text-red">2026-04-18 09:59:00</span></p>
|
||||
<p class="data">
|
||||
<span class="text-muted">主演:</span>
|
||||
<a href="/vodsearch/-%E7%AB%A0%E7%85%9C%E5%A5%87------------.html" target="_blank">章煜奇</a>
|
||||
<a href="/vodsearch/-%E8%A2%81%E4%BC%8A------------.html" target="_blank">袁伊</a>
|
||||
<a href="/vodsearch/-%E9%83%AD%E5%AD%90%E6%B8%9D------------.html" target="_blank">郭子渝</a>
|
||||
<a href="/vodsearch/-%E5%88%98%E5%B8%8C%E5%A9%A7------------.html" target="_blank">刘希婧</a>
|
||||
</p>
|
||||
<p class="data">
|
||||
<span class="text-muted">导演:</span>
|
||||
<a href="/vodsearch/-----%E5%BC%A0%E4%B8%96%E5%8D%9A--------.html" target="_blank">张世博</a>
|
||||
<a href="/vodsearch/-----%E6%BA%90%E8%AF%97%E5%98%89--------.html" target="_blank">源诗嘉</a>
|
||||
</p>
|
||||
<p class="data hidden-xs"><span class="text-muted">简介:</span>《AI教我谈恋爱》线上看,共26集,《AI教我谈恋爱》简介:漫画家袁七柚意外绑定...<a href="#desc">详情</a></p>
|
||||
</div>
|
||||
<a href="/vodplay/143508-1-1.html">第1集</a>
|
||||
"""
|
||||
result = self.spider._parse_detail_page(html, "143508")
|
||||
vod = result["list"][0]
|
||||
self.assertEqual(vod["vod_id"], "143508")
|
||||
self.assertEqual(vod["path"], "https://www.dbku.tv/voddetail/143508.html")
|
||||
self.assertEqual(vod["vod_pic"], "https://www.dbku.tv/poster-ai.jpg")
|
||||
self.assertEqual(vod["type_name"], "短剧")
|
||||
self.assertEqual(vod["vod_area"], "大陆")
|
||||
self.assertEqual(vod["vod_year"], "2026")
|
||||
self.assertEqual(vod["vod_time"], "2026-04-18 09:59:00")
|
||||
self.assertEqual(vod["vod_actor"], "章煜奇,袁伊,郭子渝,刘希婧")
|
||||
self.assertEqual(vod["vod_director"], "张世博,源诗嘉")
|
||||
self.assertEqual(vod["vod_content"], "《AI教我谈恋爱》线上看,共26集,《AI教我谈恋爱》简介:漫画家袁七柚意外绑定...")
|
||||
self.assertEqual(vod["vod_play_url"], "第1集$143508-1-1")
|
||||
self.assertEqual(vod["vod_tag"], "")
|
||||
self.assertEqual(vod["vod_remarks"], "6")
|
||||
self.assertEqual(vod["vod_lang"], "")
|
||||
self.assertNotIn("dbid", vod)
|
||||
self.assertNotIn("type", vod)
|
||||
|
||||
@patch.object(Spider, "_request_html")
|
||||
def test_detail_content_reads_from_vod_id_url(self, mock_request_html):
|
||||
@@ -125,8 +179,9 @@ class TestDBKUSpider(unittest.TestCase):
|
||||
<h1 class="title">详情影片</h1>
|
||||
<a href="/vodplay/200-1-1.html">第1集</a>
|
||||
"""
|
||||
result = self.spider.detailContent(["https://www.dbku.tv/voddetail/200.html"])
|
||||
self.assertEqual(result["list"][0]["vod_id"], "https://www.dbku.tv/voddetail/200.html")
|
||||
result = self.spider.detailContent(["200"])
|
||||
self.assertEqual(mock_request_html.call_args.args[0], "https://www.dbku.tv/voddetail/200.html")
|
||||
self.assertEqual(result["list"][0]["vod_id"], "200")
|
||||
self.assertEqual(result["list"][0]["vod_name"], "详情影片")
|
||||
|
||||
def test_parse_player_data_reads_json_block(self):
|
||||
@@ -162,18 +217,36 @@ class TestDBKUSpider(unittest.TestCase):
|
||||
var player_data = {"url":"https://video.example/final.m3u8","encrypt":"0"};
|
||||
</script>
|
||||
"""
|
||||
result = self.spider.playerContent("独播库", "https://www.dbku.tv/vodplay/100-1-1.html", {})
|
||||
result = self.spider.playerContent("独播库", "100-1-1", {})
|
||||
self.assertEqual(result["parse"], 0)
|
||||
self.assertEqual(result["url"], "https://video.example/final.m3u8")
|
||||
self.assertEqual(result["header"]["Referer"], "https://www.dbku.tv/vodplay/100-1-1.html")
|
||||
|
||||
@patch.object(Spider, "log")
|
||||
@patch.object(Spider, "_request_html")
|
||||
def test_player_content_logs_resolved_player_fields(self, mock_request_html, mock_log):
|
||||
mock_request_html.return_value = """
|
||||
<script>
|
||||
var player_data = {"url":"aHR0cHM6Ly92aWRlby5leGFtcGxlL2ZpbmFsLm0zdTg=","encrypt":"2"};
|
||||
</script>
|
||||
"""
|
||||
self.spider.playerContent("独播库", "100-1-1", {})
|
||||
mock_log.assert_called()
|
||||
payload = mock_log.call_args.args[0]
|
||||
self.assertEqual(payload["stage"], "playerContent")
|
||||
self.assertEqual(payload["input_url"], "100-1-1")
|
||||
self.assertEqual(payload["request_url"], "https://www.dbku.tv/vodplay/100-1-1.html")
|
||||
self.assertEqual(payload["encrypt"], 2)
|
||||
self.assertEqual(payload["decoded_url"], "https://video.example/final.m3u8")
|
||||
self.assertEqual(payload["final_url"], "https://video.example/final.m3u8")
|
||||
|
||||
@patch.object(Spider, "_request_html")
|
||||
def test_player_content_follows_internal_jump(self, mock_request_html):
|
||||
mock_request_html.side_effect = [
|
||||
'<script>var player_data = {"url":"/vodplay/100-1-2.html","encrypt":"0"};</script>',
|
||||
'<script>var player_data = {"url":"https://video.example/jump-final.m3u8","encrypt":"0"};</script>',
|
||||
]
|
||||
result = self.spider.playerContent("独播库", "https://www.dbku.tv/vodplay/100-1-1.html", {})
|
||||
result = self.spider.playerContent("独播库", "100-1-1", {})
|
||||
self.assertEqual(result["parse"], 0)
|
||||
self.assertEqual(result["url"], "https://video.example/jump-final.m3u8")
|
||||
|
||||
|
||||
+6
-2
@@ -5,8 +5,12 @@ import re
|
||||
import sys
|
||||
from urllib.parse import quote, urljoin
|
||||
|
||||
from Cryptodome.Cipher import AES
|
||||
from Cryptodome.Util.Padding import unpad
|
||||
try:
|
||||
from Crypto.Cipher import AES
|
||||
from Crypto.Util.Padding import unpad
|
||||
except ModuleNotFoundError:
|
||||
from Cryptodome.Cipher import AES
|
||||
from Cryptodome.Util.Padding import unpad
|
||||
|
||||
from base.spider import Spider as BaseSpider
|
||||
|
||||
|
||||
@@ -64,6 +64,42 @@ class Spider(BaseSpider):
|
||||
return self.host + raw
|
||||
return self.host + "/" + raw
|
||||
|
||||
def _extract_play_id(self, href):
|
||||
raw = str(href or "").strip()
|
||||
matched = re.search(r"/vodplay/(\d+-\d+-\d+)\.html", raw)
|
||||
if matched:
|
||||
return matched.group(1)
|
||||
if re.fullmatch(r"\d+-\d+-\d+", raw):
|
||||
return raw
|
||||
return ""
|
||||
|
||||
def _extract_vod_id(self, href):
|
||||
raw = str(href or "").strip()
|
||||
matched = re.search(r"/voddetail/(\d+)\.html", raw)
|
||||
if matched:
|
||||
return matched.group(1)
|
||||
if re.fullmatch(r"\d+", raw):
|
||||
return raw
|
||||
return ""
|
||||
|
||||
def _build_detail_request_url(self, id_or_url):
|
||||
raw = str(id_or_url or "").strip()
|
||||
if not raw:
|
||||
return ""
|
||||
vod_id = self._extract_vod_id(raw)
|
||||
if vod_id:
|
||||
return f"{self.host}/voddetail/{vod_id}.html"
|
||||
return self._build_url(raw)
|
||||
|
||||
def _build_play_request_url(self, id_or_url):
|
||||
raw = str(id_or_url or "").strip()
|
||||
if not raw:
|
||||
return ""
|
||||
play_id = self._extract_play_id(raw)
|
||||
if play_id:
|
||||
return f"{self.host}/vodplay/{play_id}.html"
|
||||
return self._build_url(raw)
|
||||
|
||||
def _parse_list_cards(self, html):
|
||||
root = self.html(html)
|
||||
results = []
|
||||
@@ -79,7 +115,7 @@ class Spider(BaseSpider):
|
||||
for anchor in card.xpath(".//a[@href]"):
|
||||
raw_href = (anchor.xpath("./@href") or [""])[0].strip()
|
||||
if "/voddetail/" in raw_href:
|
||||
href = self._build_url(raw_href)
|
||||
href = self._extract_vod_id(raw_href)
|
||||
title = (
|
||||
(anchor.xpath("./@title") or [""])[0].strip()
|
||||
or "".join(anchor.xpath(".//text()")).strip()
|
||||
@@ -173,14 +209,62 @@ class Spider(BaseSpider):
|
||||
return clean.split(":", 1)[-1].strip()
|
||||
return ""
|
||||
|
||||
def _clean_text(self, text):
|
||||
return re.sub(r"\s+", " ", str(text or "").replace("\xa0", " ")).strip()
|
||||
|
||||
def _extract_detail_text(self, detail_root, html, label, joiner=""):
|
||||
value = self._extract_detail_field(detail_root, label, joiner=joiner)
|
||||
if value:
|
||||
return value
|
||||
return self._extract_text_by_prefix(html, [f"{label}:"])
|
||||
|
||||
def _extract_detail_field(self, detail_root, label, joiner=""):
|
||||
if detail_root is None:
|
||||
return ""
|
||||
nodes = detail_root.xpath(f'.//span[contains(normalize-space(.), "{label}:")]')
|
||||
if not nodes:
|
||||
return ""
|
||||
label_node = nodes[0]
|
||||
values = []
|
||||
if self._clean_text(label_node.tail):
|
||||
values.append(self._clean_text(label_node.tail))
|
||||
for sibling in label_node.itersiblings():
|
||||
classes = " ".join(sibling.xpath("./@class"))
|
||||
if sibling.tag == "span" and "text-muted" in classes:
|
||||
break
|
||||
if sibling.tag == "span" and "split-line" in classes:
|
||||
continue
|
||||
if sibling.tag == "a" and (sibling.xpath("./@href") or [""])[0].startswith("#"):
|
||||
break
|
||||
text = self._clean_text("".join(sibling.xpath(".//text()")))
|
||||
if text:
|
||||
values.append(text)
|
||||
tail = self._clean_text(sibling.tail)
|
||||
if tail:
|
||||
values.append(tail)
|
||||
|
||||
cleaned = []
|
||||
for value in values:
|
||||
if value and value not in cleaned:
|
||||
cleaned.append(value)
|
||||
if joiner:
|
||||
return joiner.join(cleaned)
|
||||
return self._clean_text("".join(cleaned))
|
||||
|
||||
def _parse_detail_page(self, html, vod_id):
|
||||
root = self.html(html)
|
||||
title = ((root.xpath("//*[contains(@class,'title')][1]//text()") or [""])[0]).strip()
|
||||
detail_root = (
|
||||
root.xpath("//*[contains(concat(' ', normalize-space(@class), ' '), ' myui-content__detail ')][1]")
|
||||
or [root]
|
||||
)[0]
|
||||
title = ((detail_root.xpath(".//*[contains(@class,'title')][1]//text()") or [""])[0]).strip()
|
||||
pic = (
|
||||
(root.xpath("//*[contains(@class,'myui-content__thumb')]//img/@data-original") or [""])[0].strip()
|
||||
or (root.xpath("//*[contains(@class,'myui-content__thumb')]//img/@src") or [""])[0].strip()
|
||||
)
|
||||
content = "".join(root.xpath("//*[contains(@class,'data')][1]//text()")).strip()
|
||||
content = self._extract_detail_field(detail_root, "简介") or "".join(
|
||||
root.xpath("//*[contains(@class,'data')][1]//text()")
|
||||
).strip()
|
||||
|
||||
episodes = []
|
||||
seen = set()
|
||||
@@ -189,30 +273,42 @@ class Spider(BaseSpider):
|
||||
html,
|
||||
re.I,
|
||||
):
|
||||
url = self._build_url(href)
|
||||
play_id = self._extract_play_id(href)
|
||||
name = re.sub(r"<[^>]*>", "", label).strip()
|
||||
if not url or not name or "立即播放" in name or url in seen:
|
||||
if not play_id or not name or "立即播放" in name or play_id in seen:
|
||||
continue
|
||||
seen.add(url)
|
||||
episodes.append(f"{name}${url}")
|
||||
seen.add(play_id)
|
||||
episodes.append(f"{name}${play_id}")
|
||||
|
||||
vod = {
|
||||
"vod_id": vod_id,
|
||||
"path": self._build_detail_request_url(vod_id),
|
||||
"vod_name": title,
|
||||
"vod_pic": self._build_url(pic),
|
||||
"vod_year": self._extract_text_by_prefix(html, ["年份:"]),
|
||||
"vod_area": self._extract_text_by_prefix(html, ["地区:"]),
|
||||
"vod_actor": self._extract_text_by_prefix(html, ["主演:"]),
|
||||
"vod_director": self._extract_text_by_prefix(html, ["导演:"]),
|
||||
"vod_content": content,
|
||||
"vod_tag": "",
|
||||
"vod_time": self._extract_detail_text(detail_root, html, "更新"),
|
||||
"vod_remarks": self._clean_text(
|
||||
((detail_root.xpath('.//*[@id="rating"]//*[contains(@class,"branch")][1]//text()') or [""])[0]).strip()
|
||||
or ((detail_root.xpath('.//*[@id="ratewords"][1]//text()') or [""])[0]).strip()
|
||||
),
|
||||
"vod_play_from": "独播库",
|
||||
"vod_play_url": "#".join(episodes),
|
||||
"type_name": self._extract_detail_text(detail_root, html, "分类"),
|
||||
"vod_content": content,
|
||||
"vod_year": self._extract_detail_text(detail_root, html, "年份"),
|
||||
"vod_area": self._extract_detail_text(detail_root, html, "地区"),
|
||||
"vod_lang": "",
|
||||
"vod_director": self._extract_detail_text(detail_root, html, "导演", joiner=","),
|
||||
"vod_actor": self._extract_detail_text(detail_root, html, "主演", joiner=","),
|
||||
}
|
||||
return {"list": [vod]}
|
||||
|
||||
def detailContent(self, ids):
|
||||
vod_id = ids[0]
|
||||
html = self._request_html(vod_id, expect_xpath="//*[contains(@class,'title')]|//a[contains(@href,'/vodplay/')]")
|
||||
html = self._request_html(
|
||||
self._build_detail_request_url(vod_id),
|
||||
expect_xpath="//*[contains(@class,'title')]|//a[contains(@href,'/vodplay/')]",
|
||||
)
|
||||
return self._parse_detail_page(html, vod_id)
|
||||
|
||||
def _parse_player_data(self, html):
|
||||
@@ -256,7 +352,8 @@ class Spider(BaseSpider):
|
||||
def playerContent(self, flag, id, vipFlags):
|
||||
current = id
|
||||
for _ in range(3):
|
||||
html = self._request_html(current, referer=self.host)
|
||||
request_url = self._build_play_request_url(current)
|
||||
html = self._request_html(request_url, referer=self.host)
|
||||
data = self._parse_player_data(html)
|
||||
if not data:
|
||||
return {"parse": 0, "playUrl": "", "url": ""}
|
||||
@@ -271,6 +368,6 @@ class Spider(BaseSpider):
|
||||
"parse": 0,
|
||||
"playUrl": "",
|
||||
"url": play_url,
|
||||
"header": self._build_player_headers(current),
|
||||
"header": self._build_player_headers(request_url),
|
||||
}
|
||||
return {"parse": 0, "playUrl": "", "url": ""}
|
||||
|
||||
Reference in New Issue
Block a user