Files
tvboxzt/py/独播库.py
T
2026-04-19 18:32:57 +08:00

372 lines
14 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# coding=utf-8
import base64
import json
import re
import sys
from urllib.parse import quote, unquote
from lxml import etree
from base.spider import Spider as BaseSpider
sys.path.append("..")
class Spider(BaseSpider):
def __init__(self):
self.name = "独播库"
self.host = "https://www.dbku.tv"
self.headers = {
"User-Agent": (
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
"AppleWebKit/537.36 (KHTML, like Gecko) "
"Chrome/124.0.0.0 Safari/537.36"
)
}
self.categories = [
{"type_name": "连续剧", "type_id": "index"},
{"type_name": "电影", "type_id": "movie"},
{"type_name": "综艺", "type_id": "variety"},
{"type_name": "动漫", "type_id": "anime"},
{"type_name": "港剧", "type_id": "hk"},
{"type_name": "陆剧", "type_id": "luju"},
]
self.category_paths = {
"index": "/vodtype/2--------{pg}---.html",
"movie": "/vodtype/1--------{pg}---.html",
"variety": "/vodtype/3--------{pg}---.html",
"anime": "/vodtype/4--------{pg}---.html",
"hk": "/vodtype/20--------{pg}---.html",
"luju": "/vodtype/13--------{pg}---.html",
}
def init(self, extend=""):
return None
def getName(self):
return self.name
def homeContent(self, filter):
return {"class": self.categories}
def homeVideoContent(self):
return {"list": []}
def _build_url(self, href):
raw = str(href or "").strip()
if not raw:
return ""
if raw.startswith(("http://", "https://")):
return raw
if raw.startswith("//"):
return "https:" + raw
if raw.startswith("/"):
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 = []
if root is None:
return results
cards = root.xpath("//*[contains(@class,'myui-vodlist__box')]")
seen = set()
for card in cards:
href = ""
title = ""
pic = ""
for anchor in card.xpath(".//a[@href]"):
raw_href = (anchor.xpath("./@href") or [""])[0].strip()
if "/voddetail/" in raw_href:
href = self._extract_vod_id(raw_href)
title = (
(anchor.xpath("./@title") or [""])[0].strip()
or "".join(anchor.xpath(".//text()")).strip()
)
pic = (
(anchor.xpath("./@data-original") or [""])[0].strip()
or (anchor.xpath("./@src") or [""])[0].strip()
)
break
if not href or href in seen or not title:
continue
remarks = "".join(card.xpath(".//*[contains(@class,'pic-text')][1]//text()")).strip()
seen.add(href)
results.append(
{
"vod_id": href,
"vod_name": title,
"vod_pic": self._build_url(pic),
"vod_remarks": remarks,
}
)
return results
def _request_html(self, path_or_url, expect_xpath=None, referer=None):
target = path_or_url if path_or_url.startswith("http") else self._build_url(path_or_url)
headers = dict(self.headers)
headers["Referer"] = referer or self.host
headers["Origin"] = self.host
response = self.fetch(target, headers=headers, timeout=10)
if response.status_code != 200:
return ""
html = response.text or ""
if expect_xpath:
root = self.html(html)
if root is None or not root.xpath(expect_xpath):
return ""
return html
def _parse_cards_from_nodes(self, nodes):
results = []
seen = set()
for card in nodes:
snippet = self._parse_list_cards(etree.tostring(card, encoding="unicode"))
for item in snippet:
if item["vod_id"] in seen:
continue
seen.add(item["vod_id"])
results.append(item)
return results
def _parse_search_cards(self, html):
root = self.html(html)
if root is None:
return []
search_list = root.xpath("//*[@id='searchList']")
if search_list:
cards = search_list[0].xpath(".//*[contains(@class,'myui-vodlist__box')]")
parsed = self._parse_cards_from_nodes(cards)
if parsed:
return parsed
return self._parse_list_cards(html)
def _page_result(self, items, pg):
page = int(pg)
return {
"list": items,
"page": page,
"limit": len(items),
"total": page * 30 + len(items),
}
def categoryContent(self, tid, pg, filter, extend):
path = self.category_paths.get(tid, self.category_paths["index"]).format(pg=pg)
html = self._request_html(path, expect_xpath="//*[contains(@class,'myui-vodlist__box')]")
return self._page_result(self._parse_list_cards(html), pg)
def searchContent(self, key, quick, pg="1"):
path = "/vodsearch/-------------.html?wd={0}&submit=".format(quote(key))
html = self._request_html(path, expect_xpath="//*[@id='searchList']|//*[contains(@class,'myui-vodlist__box')]")
return self._page_result(self._parse_search_cards(html), pg)
def _extract_text_by_prefix(self, html, prefixes):
texts = re.findall(r">([^<>]+)<", html)
for text in texts:
clean = text.strip()
for prefix in prefixes:
if clean.startswith(prefix):
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)
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 = self._extract_detail_field(detail_root, "简介") or "".join(
root.xpath("//*[contains(@class,'data')][1]//text()")
).strip()
episodes = []
seen = set()
for href, label in re.findall(
r'<a[^>]+href=["\']([^"\']*/vodplay/\d+-\d+-\d+\.html[^"\']*)["\'][^>]*>([\s\S]*?)</a>',
html,
re.I,
):
play_id = self._extract_play_id(href)
name = re.sub(r"<[^>]*>", "", label).strip()
if not play_id or not name or "立即播放" in name or play_id in seen:
continue
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_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(
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):
matched = re.search(r"var\s+player_data\s*=\s*(\{[\s\S]*?\})\s*;?\s*</script>", html, re.I)
if not matched:
matched = re.search(r"var\s+player_[^=]*\s*=\s*(\{[\s\S]*?\})\s*;?\s*</script>", html, re.I)
if not matched:
return None
try:
return json.loads(matched.group(1))
except Exception:
return None
def _decode_play_url_by_encrypt(self, value, encrypt):
raw = str(value or "")
mode = int(encrypt or 0)
if not raw:
return ""
try:
if mode == 1:
return unquote(raw)
if mode == 2:
decoded = base64.b64decode(raw).decode("utf-8")
return unquote(decoded)
if mode == 3:
text = raw[8:] if len(raw) > 16 else raw
text = base64.b64decode(text).decode("utf-8")
if len(text) > 16:
text = text[8:-8]
return text
return raw
except Exception:
return raw
def _build_player_headers(self, referer):
return {
"User-Agent": self.headers["User-Agent"],
"Referer": referer,
}
def playerContent(self, flag, id, vipFlags):
current = id
for _ in range(3):
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": ""}
decoded = self._decode_play_url_by_encrypt(data.get("url", ""), data.get("encrypt", 0))
play_url = self._build_url(decoded)
if not play_url:
return {"parse": 0, "playUrl": "", "url": ""}
if self.host in play_url and "/vodplay/" in play_url and play_url != current:
current = play_url
continue
return {
"parse": 0,
"playUrl": "",
"url": play_url,
"header": self._build_player_headers(request_url),
}
return {"parse": 0, "playUrl": "", "url": ""}