diff --git a/py/tests/test_youknow.py b/py/tests/test_youknow.py
index 5ecff46..bdfa416 100644
--- a/py/tests/test_youknow.py
+++ b/py/tests/test_youknow.py
@@ -21,7 +21,7 @@ class TestYouKnowSpider(unittest.TestCase):
def test_parse_list_cards_extracts_compact_vod_id(self):
html = """
-
+
更新至10集
"""
@@ -38,10 +38,27 @@ class TestYouKnowSpider(unittest.TestCase):
],
)
+ def test_parse_list_cards_prefers_nested_lazyload_original_over_placeholder_src(self):
+ html = """
+
+
+

+
+ 更新中
+
+ """
+ cards = self.spider._parse_list_cards(html)
+ self.assertEqual(cards[0]["vod_pic"], "https://www.youknow.tv/upload/vod/20260409-1/f1d0233129bc59adb6d2e8fa6396e681.jpg")
+
@patch.object(Spider, "_request_html")
def test_home_video_content_uses_today_updates_page(self, mock_request_html):
mock_request_html.return_value = """
-
+
HD
"""
@@ -52,7 +69,7 @@ class TestYouKnowSpider(unittest.TestCase):
@patch.object(Spider, "_request_html")
def test_category_content_builds_page_result(self, mock_request_html):
mock_request_html.return_value = """
-
+
完结
"""
@@ -64,7 +81,7 @@ class TestYouKnowSpider(unittest.TestCase):
@patch.object(Spider, "_request_html")
def test_category_content_uses_page1_path_without_page_number(self, mock_request_html):
mock_request_html.return_value = """
-
+
HD
"""
@@ -75,7 +92,7 @@ class TestYouKnowSpider(unittest.TestCase):
@patch.object(Spider, "_request_html")
def test_search_content_reuses_card_parser(self, mock_request_html):
mock_request_html.return_value = """
-
+
抢先版
"""
@@ -115,7 +132,7 @@ class TestYouKnowSpider(unittest.TestCase):
result = self.spider._parse_detail_page(html, "888")
vod = result["list"][0]
self.assertEqual(vod["vod_id"], "888")
- self.assertEqual(vod["path"], "https://www.youknow.tv/v/888.html")
+ self.assertEqual(vod["path"], "https://www.youknow.tv/d/888/")
self.assertEqual(vod["vod_name"], "示例剧")
self.assertEqual(vod["type_name"], "剧情")
self.assertEqual(vod["vod_area"], "大陆")
@@ -137,7 +154,7 @@ class TestYouKnowSpider(unittest.TestCase):
def test_detail_content_builds_detail_request_url_from_vod_id(self, mock_request_html):
mock_request_html.return_value = '
详情影片
'
result = self.spider.detailContent(["123"])
- self.assertEqual(mock_request_html.call_args.args[0], "https://www.youknow.tv/v/123.html")
+ self.assertEqual(mock_request_html.call_args.args[0], "https://www.youknow.tv/d/123/")
self.assertEqual(result["list"][0]["vod_id"], "123")
def test_extract_player_config_reads_player_aaaa(self):
@@ -145,6 +162,21 @@ class TestYouKnowSpider(unittest.TestCase):
data = self.spider._parse_player_config(html)
self.assertEqual(data["encrypt"], "1")
+ def test_extract_player_config_supports_nested_object_literal(self):
+ html = """
+
+ """
+ data = self.spider._parse_player_config(html)
+ self.assertEqual(data["encrypt"], 2)
+ self.assertEqual(data["vod_data"]["vod_name"], "示例影片")
+
def test_decode_player_url_supports_encrypt_1_and_2(self):
self.assertEqual(
self.spider._decode_player_url("https%3A%2F%2Fvideo.example%2Fa.m3u8", "1"),
@@ -177,6 +209,28 @@ class TestYouKnowSpider(unittest.TestCase):
self.assertEqual(result["url"], "https://video.example/page.m3u8")
self.assertEqual(result["header"]["Referer"], "https://www.youknow.tv/")
+ @patch.object(Spider, "_request_html")
+ def test_player_content_handles_nested_real_world_player_config(self, mock_request_html):
+ payload = self.spider._encode_episode_payload(
+ {
+ "vod_id": "202774",
+ "episode_index": 1,
+ "title": "HD中字",
+ "candidates": [
+ {"source": "线路1", "source_id": "1", "episode_url": "https://www.youknow.tv/p/202774-1-1/"}
+ ],
+ }
+ )
+ mock_request_html.return_value = """
+
+
+ """
+ result = self.spider.playerContent("YouKnowTV", payload, {})
+ self.assertEqual(result["parse"], 0)
+ self.assertEqual(result["url"], "https://vip.dytt-network.com/20260325/21334_18856b47/index.m3u8")
+
@patch.object(Spider, "_request_html")
def test_player_content_tries_next_candidate_when_first_fails(self, mock_request_html):
payload = self.spider._encode_episode_payload(
diff --git a/py/youknow.py b/py/youknow.py
index 61d1178..8c58918 100644
--- a/py/youknow.py
+++ b/py/youknow.py
@@ -1,5 +1,6 @@
# coding=utf-8
import base64
+import ast
import json
import re
import sys
@@ -62,7 +63,7 @@ class Spider(BaseSpider):
def _extract_vod_id(self, href):
raw = str(href or "").strip()
- matched = re.search(r"/v/(\d+)\.html", raw)
+ matched = re.search(r"/d/(\d+)/?$", raw) or re.search(r"/v/(\d+)\.html", raw)
if matched:
return matched.group(1)
if re.fullmatch(r"\d+", raw):
@@ -81,7 +82,7 @@ class Spider(BaseSpider):
}
def _build_detail_request_url(self, vod_id):
- return f"{self.host}/v/{self._extract_vod_id(vod_id)}.html"
+ return f"{self.host}/d/{self._extract_vod_id(vod_id)}/"
def _encode_episode_payload(self, payload):
raw = json.dumps(payload, ensure_ascii=False, separators=(",", ":")).encode("utf-8")
@@ -106,6 +107,10 @@ class Spider(BaseSpider):
pic = (
(card.xpath("./@data-original") or [""])[0].strip()
or (card.xpath("./@data-src") or [""])[0].strip()
+ or (card.xpath(".//img/@data-original") or [""])[0].strip()
+ or (card.xpath(".//img/@data-src") or [""])[0].strip()
+ or (card.xpath(".//@data-original") or [""])[0].strip()
+ or (card.xpath(".//@data-src") or [""])[0].strip()
or (card.xpath(".//@src") or [""])[0].strip()
)
remarks = "".join(
@@ -136,6 +141,7 @@ class Spider(BaseSpider):
def _page_result(self, items, pg):
page = int(pg)
pagecount = page + 1 if items else page
+ self.log(items[0])
return {
"list": items,
"page": page,
@@ -323,13 +329,67 @@ class Spider(BaseSpider):
) and any(ext in text for ext in (".m3u8", ".mp4", ".flv"))
def _parse_player_config(self, html):
- matched = re.search(r"player_aaaa\s*=\s*(\{[\s\S]*?\})\s*;?", str(html or ""), re.I)
+ text = str(html or "")
+ matched = re.search(r"player_aaaa\s*=", text, re.I)
if not matched:
return None
- try:
- return json.loads(matched.group(1))
- except Exception:
+ object_start = text.find("{", matched.end())
+ if object_start < 0:
return None
+ raw_object = self._extract_balanced_object(text, object_start)
+ if not raw_object:
+ return None
+ return self._parse_object_literal(raw_object)
+
+ def _extract_balanced_object(self, text, start_index):
+ raw = str(text or "")
+ if start_index < 0 or start_index >= len(raw) or raw[start_index] != "{":
+ return ""
+
+ depth = 0
+ in_string = False
+ quote_char = ""
+ escaped = False
+
+ for index in range(start_index, len(raw)):
+ char = raw[index]
+ if in_string:
+ if escaped:
+ escaped = False
+ elif char == "\\":
+ escaped = True
+ elif char == quote_char:
+ in_string = False
+ continue
+
+ if char in ('"', "'"):
+ in_string = True
+ quote_char = char
+ continue
+
+ if char == "{":
+ depth += 1
+ continue
+
+ if char == "}":
+ depth -= 1
+ if depth == 0:
+ return raw[start_index : index + 1]
+
+ return ""
+
+ def _parse_object_literal(self, raw):
+ text = str(raw or "").strip()
+ if not text:
+ return None
+ sanitized = re.sub(r",\s*}", "}", text)
+ try:
+ return json.loads(sanitized)
+ except Exception:
+ try:
+ return ast.literal_eval(sanitized)
+ except Exception:
+ return None
def _collect_direct_media_urls(self, html):
text = str(html or "").replace("\\/", "/")