From f94acdccf7b109289773542cf919381451db3d1f Mon Sep 17 00:00:00 2001
From: Harold <8866033@gmail.com>
Date: Sat, 18 Apr 2026 12:55:09 +0800
Subject: [PATCH] =?UTF-8?q?=E7=8B=AC=E6=92=AD=E5=BA=93.py?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
py/tests/test_czzy.py | 8 ++-
py/tests/test_dbku.py | 89 ++++++++++++++++++++++++++---
py/厂长资源.py | 8 ++-
py/独播库.py | 127 +++++++++++++++++++++++++++++++++++++-----
4 files changed, 205 insertions(+), 27 deletions(-)
diff --git a/py/tests/test_czzy.py b/py/tests/test_czzy.py
index d19d83c..7a66558 100644
--- a/py/tests/test_czzy.py
+++ b/py/tests/test_czzy.py
@@ -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]
diff --git a/py/tests/test_dbku.py b/py/tests/test_dbku.py
index 3dbb220..d94f457 100644
--- a/py/tests/test_dbku.py
+++ b/py/tests/test_dbku.py
@@ -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):
"""
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):
第1集
第2集
"""
- 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 = """
+
+
![]()
+
+
+
AI教我谈恋爱
+
+ 6
+
+
+ 分类:短剧
+
+ 地区:大陆
+
+ 年份:2026
+
+
更新:2026-04-18 09:59:00
+
+ 主演:
+ 章煜奇
+ 袁伊
+ 郭子渝
+ 刘希婧
+
+
+ 导演:
+ 张世博
+ 源诗嘉
+
+
简介:《AI教我谈恋爱》线上看,共26集,《AI教我谈恋爱》简介:漫画家袁七柚意外绑定...详情
+
+ 第1集
+ """
+ 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):
详情影片
第1集
"""
- 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"};
"""
- 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 = """
+
+ """
+ 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 = [
'',
'',
]
- 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")
diff --git a/py/厂长资源.py b/py/厂长资源.py
index 94e3b9f..27616c6 100644
--- a/py/厂长资源.py
+++ b/py/厂长资源.py
@@ -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
diff --git a/py/独播库.py b/py/独播库.py
index d48eb4d..3f6f5f1 100644
--- a/py/独播库.py
+++ b/py/独播库.py
@@ -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": ""}