diff --git a/py/tests/test_飞快TV.py b/py/tests/test_飞快TV.py
index 6d6f103..d9542b8 100644
--- a/py/tests/test_飞快TV.py
+++ b/py/tests/test_飞快TV.py
@@ -1,6 +1,7 @@
import unittest
from importlib.machinery import SourceFileLoader
from pathlib import Path
+from unittest.mock import patch
ROOT = Path(__file__).resolve().parents[1]
@@ -28,3 +29,50 @@ class TestFeikuaiSpider(unittest.TestCase):
def test_home_video_content_returns_empty_list(self):
self.assertEqual(self.spider.homeVideoContent(), {"list": []})
+
+ @patch.object(Spider, "_request_html")
+ def test_category_content_parses_short_vod_id(self, mock_request_html):
+ mock_request_html.return_value = """
+
+
+ 更新至10集
+
+ """
+ result = self.spider.categoryContent("2", "3", False, {})
+ self.assertEqual(
+ mock_request_html.call_args.args[0],
+ "https://feikuai.tv/vodshow/2--------3---.html",
+ )
+ self.assertEqual(
+ result["list"],
+ [
+ {
+ "vod_id": "/voddetail/12345.html",
+ "vod_name": "分类影片",
+ "vod_pic": "https://feikuai.tv/cover.jpg",
+ "vod_remarks": "更新至10集",
+ }
+ ],
+ )
+ self.assertNotIn("pagecount", result)
+
+ @patch.object(Spider, "_request_html")
+ def test_search_content_parses_cards_and_blank_keyword(self, mock_request_html):
+ blank = self.spider.searchContent("", False, "1")
+ self.assertEqual(blank, {"page": 1, "limit": 0, "total": 0, "list": []})
+ mock_request_html.assert_not_called()
+
+ mock_request_html.return_value = """
+
+ """
+ result = self.spider.searchContent("繁花", False, "2")
+ self.assertEqual(
+ mock_request_html.call_args.args[0],
+ "https://feikuai.tv/label/search_ajax.html?wd=%E7%B9%81%E8%8A%B1&by=time&order=desc&page=2",
+ )
+ self.assertEqual(result["list"][0]["vod_id"], "/voddetail/67890.html")
diff --git a/py/飞快TV.py b/py/飞快TV.py
index 5ed55b1..89640ab 100644
--- a/py/飞快TV.py
+++ b/py/飞快TV.py
@@ -1,5 +1,7 @@
# coding=utf-8
+import re
import sys
+from urllib.parse import quote, urljoin
from base.spider import Spider as BaseSpider
@@ -38,3 +40,96 @@ class Spider(BaseSpider):
def homeVideoContent(self):
return {"list": []}
+
+ def _build_url(self, value):
+ raw = str(value or "").strip()
+ if not raw:
+ return ""
+ if raw.startswith(("http://", "https://")):
+ return raw
+ if raw.startswith("//"):
+ return "https:" + raw
+ return urljoin(self.host + "/", raw)
+
+ def _clean_text(self, text):
+ return re.sub(r"\s+", " ", str(text or "").replace("\xa0", " ")).strip()
+
+ def _request_html(self, path_or_url):
+ target = path_or_url if str(path_or_url).startswith("http") else self._build_url(path_or_url)
+ response = self.fetch(target, headers=self.headers, timeout=10)
+ if response.status_code != 200:
+ return ""
+ return str(response.text or "")
+
+ def _parse_category_cards(self, html):
+ root = self.html(html or "")
+ if root is None:
+ return []
+ items = []
+ for node in root.xpath("//a[contains(@class,'module-poster-item')]"):
+ vod_id = self._clean_text("".join(node.xpath("./@href")))
+ vod_name = self._clean_text("".join(node.xpath("./@title"))) or self._clean_text(
+ "".join(node.xpath(".//*[contains(@class,'module-poster-item-title')][1]//text()"))
+ )
+ vod_pic = self._clean_text(
+ "".join(node.xpath(".//img[contains(@class,'lazy')][1]/@data-original"))
+ )
+ vod_remarks = self._clean_text(
+ "".join(node.xpath(".//*[contains(@class,'module-item-note')][1]//text()"))
+ )
+ if vod_id and vod_name:
+ items.append(
+ {
+ "vod_id": vod_id,
+ "vod_name": vod_name,
+ "vod_pic": self._build_url(vod_pic),
+ "vod_remarks": vod_remarks,
+ }
+ )
+ return items
+
+ def _parse_search_cards(self, html):
+ root = self.html(html or "")
+ if root is None:
+ return []
+ items = []
+ for node in root.xpath(
+ "//*[contains(@class,'module-card-item') and contains(@class,'module-item')]"
+ ):
+ vod_id = self._clean_text(
+ "".join(node.xpath(".//a[contains(@class,'module-card-item-poster')][1]/@href"))
+ )
+ vod_name = self._clean_text(
+ "".join(node.xpath(".//*[contains(@class,'module-card-item-title')][1]//strong/text()"))
+ )
+ vod_pic = self._clean_text(
+ "".join(node.xpath(".//*[contains(@class,'module-item-pic')]//img[1]/@data-original"))
+ )
+ vod_remarks = self._clean_text(
+ "".join(node.xpath(".//*[contains(@class,'module-item-note')][1]//text()"))
+ )
+ if vod_id and vod_name:
+ items.append(
+ {
+ "vod_id": vod_id,
+ "vod_name": vod_name,
+ "vod_pic": self._build_url(vod_pic),
+ "vod_remarks": vod_remarks,
+ }
+ )
+ return items
+
+ def categoryContent(self, tid, pg, filter, extend):
+ page = max(1, int(pg))
+ url = self.host + f"/vodshow/{tid}--------{page}---.html"
+ items = self._parse_category_cards(self._request_html(url))
+ return {"page": page, "limit": len(items), "total": len(items), "list": items}
+
+ def searchContent(self, key, quick, pg="1"):
+ page = max(1, int(pg))
+ keyword = self._clean_text(key)
+ if not keyword:
+ return {"page": page, "limit": 0, "total": 0, "list": []}
+ url = self.host + "/label/search_ajax.html?wd=" + quote(keyword) + f"&by=time&order=desc&page={page}"
+ items = self._parse_search_cards(self._request_html(url))
+ return {"page": page, "limit": len(items), "total": len(items), "list": items}