独播库.py

This commit is contained in:
Harold
2026-04-18 12:55:09 +08:00
parent b72587d86b
commit f94acdccf7
4 changed files with 205 additions and 27 deletions
+6 -2
View File
@@ -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
View File
@@ -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>&nbsp;
<a href="/vodsearch/-%E8%A2%81%E4%BC%8A------------.html" target="_blank">袁伊</a>&nbsp;
<a href="/vodsearch/-%E9%83%AD%E5%AD%90%E6%B8%9D------------.html" target="_blank">郭子渝</a>&nbsp;
<a href="/vodsearch/-%E5%88%98%E5%B8%8C%E5%A9%A7------------.html" target="_blank">刘希婧</a>&nbsp;
</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>&nbsp;
<a href="/vodsearch/-----%E6%BA%90%E8%AF%97%E5%98%89--------.html" target="_blank">源诗嘉</a>&nbsp;
</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
View File
@@ -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
+112 -15
View File
@@ -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": ""}