diff --git a/py/tests/test_步步影视.py b/py/tests/test_步步影视.py new file mode 100644 index 0000000..4e6dfd5 --- /dev/null +++ b/py/tests/test_步步影视.py @@ -0,0 +1,232 @@ +import unittest +from importlib.machinery import SourceFileLoader +from pathlib import Path +from unittest.mock import patch + + +ROOT = Path(__file__).resolve().parents[1] +MODULE = SourceFileLoader("bbys_spider", str(ROOT / "步步影视.py")).load_module() +Spider = MODULE.Spider + + +class TestBuBuYingShiSpider(unittest.TestCase): + def setUp(self): + Spider._instance = None + self.spider = Spider() + self.spider.init() + + def test_home_content_exposes_expected_classes_and_filters(self): + content = self.spider.homeContent(False) + self.assertEqual( + [item["type_id"] for item in content["class"]], + ["1", "2", "3", "4"], + ) + self.assertEqual( + [item["key"] for item in content["filters"]["1"]], + ["class", "area", "year", "by"], + ) + + @patch("bbys_spider.time.time", return_value=1713523200) + @patch("bbys_spider.random.randint", side_effect=[8, 37]) + def test_build_app_headers_includes_signed_fields(self, mock_randint, mock_time): + headers = self.spider._get_app_headers() + self.assertEqual(headers["x-aid"], "com.sunshine.tv") + self.assertEqual(headers["x-ave"], "4") + self.assertEqual(headers["x-time"], "1713523200") + self.assertEqual(headers["x-nonc"], "837") + self.assertEqual(len(headers["x-sign"]), 64) + + @patch("bbys_spider.time.time", return_value=1713523200) + @patch("bbys_spider.random.randint", side_effect=[8, 37]) + def test_build_web_headers_adds_web_specific_fields(self, mock_randint, mock_time): + headers = self.spider._get_web_headers() + self.assertEqual(headers["web-sign"], "f65f3a83d6d9ad6f") + self.assertEqual(headers["X-Client"], "8f3d2a1c7b6e5d4c9a0b1f2e3d4c5b6a") + + def test_generate_years_for_movie_starts_with_current_year_and_includes_ranges(self): + years = self.spider._generate_years("电影") + self.assertEqual(years[0], {"n": "全部", "v": ""}) + self.assertEqual(years[1]["n"], str(self.spider.current_year)) + self.assertIn({"n": "2015-2011", "v": "2015-2011"}, years) + + def test_convert_json_to_vods_maps_common_fields(self): + items = self.spider._convert_json_to_vods( + [ + { + "vod_id": 11, + "vod_name": "首页片", + "vod_pic": "https://img.example/a.jpg", + "vod_duration": "更新至2集", + "vod_class": ["剧情", "悬疑"], + "vod_year": 2026, + "vod_area": "大陆", + } + ] + ) + self.assertEqual(items[0]["vod_id"], "11") + self.assertEqual(items[0]["type_name"], "剧情/悬疑") + self.assertEqual(items[0]["vod_year"], "2026") + + @patch.object(Spider, "_request_json") + def test_home_video_content_reads_home_categories(self, mock_request_json): + mock_request_json.return_value = { + "data": { + "categories": [ + { + "type_id": "1", + "type_name": "电影", + "videos": [{"vod_id": 7, "vod_name": "首页影片", "vod_pic": "/a.jpg"}], + } + ] + } + } + result = self.spider.homeVideoContent() + self.assertEqual(result["list"][0]["vod_id"], "7") + self.assertEqual(result["list"][0]["vod_name"], "首页影片") + + @patch.object(Spider, "_request_json") + @patch.object(Spider, "_get_class_list") + def test_home_video_content_falls_back_to_main_categories(self, mock_get_class_list, mock_request_json): + mock_request_json.return_value = {"data": {"categories": []}} + mock_get_class_list.side_effect = [ + {"list": [{"vod_id": "1", "vod_name": "电影片"}]}, + {"list": [{"vod_id": "2", "vod_name": "剧集片"}]}, + {"list": []}, + {"list": []}, + ] + result = self.spider.homeVideoContent() + self.assertEqual([item["vod_id"] for item in result["list"]], ["1", "2"]) + + @patch.object(Spider, "_request_json") + def test_category_content_builds_expected_web_request_and_page_shape(self, mock_request_json): + mock_request_json.return_value = { + "data": [{"vod_id": 9, "vod_name": "分类片", "vod_pic": "/cate.jpg", "vod_remarks": "HD"}] + } + result = self.spider.categoryContent("电影", "2", False, {"area": "大陆", "year": "2025", "by": "score"}) + kwargs = mock_request_json.call_args.kwargs + self.assertEqual(kwargs["headers"]["web-sign"], "f65f3a83d6d9ad6f") + self.assertEqual(kwargs["params"]["type_name"], "电影") + self.assertEqual(kwargs["params"]["page"], 2) + self.assertEqual(kwargs["params"]["sort"], "score") + self.assertEqual(result["page"], 2) + self.assertNotIn("pagecount", result) + self.assertEqual(result["list"][0]["vod_id"], "9") + + @patch.object(Spider, "_get_class_list") + def test_category_content_unknown_type_falls_back_to_hot_aggregate(self, mock_get_class_list): + mock_get_class_list.side_effect = [ + {"list": [{"vod_id": "1", "vod_name": "电影片"}]}, + {"list": [{"vod_id": "2", "vod_name": "剧集片"}]}, + {"list": [{"vod_id": "3", "vod_name": "综艺片"}]}, + {"list": [{"vod_id": "4", "vod_name": "动漫片"}]}, + ] + result = self.spider.categoryContent("步步影视", "1", False, {}) + self.assertEqual([item["vod_id"] for item in result["list"]], ["1", "2", "3", "4"]) + + @patch.object(Spider, "_request_json") + def test_search_content_skips_blank_keyword_and_builds_app_request(self, mock_request_json): + blank = self.spider.searchContent("", False, "1") + self.assertEqual(blank, {"page": 1, "limit": 0, "total": 0, "list": []}) + mock_request_json.assert_not_called() + + mock_request_json.return_value = { + "data": [{"vod_id": 13, "vod_name": "搜索片", "vod_pic": "/search.jpg"}] + } + result = self.spider.searchContent("繁花", False, "3") + kwargs = mock_request_json.call_args.kwargs + self.assertEqual(kwargs["params"]["wd"], "繁花") + self.assertEqual(kwargs["params"]["page"], 3) + self.assertEqual(kwargs["params"]["limit"], 15) + self.assertEqual(result["list"][0]["vod_id"], "13") + + def test_clean_html_content_strips_tags_and_keeps_line_breaks(self): + text = self.spider._clean_html_content("

第一段

第二段
第三行

") + self.assertEqual(text, "第一段\n第二段\n第三行") + + def test_build_play_data_packs_group_names_and_urls(self): + data = self.spider._build_play_data( + "线路一$$$线路二", + "第1集$/play/1#第2集$/play/2$$$正片$/movie/main", + ) + self.assertEqual(data["vod_play_from"], "线路一(2)$$$线路二(1)") + self.assertIn("第1集$线路一@1@/play/1", data["vod_play_url"]) + self.assertIn("正片$线路二@1@/movie/main", data["vod_play_url"]) + + @patch.object(Spider, "_request_json") + def test_detail_content_handles_array_payload_and_packs_play_urls(self, mock_request_json): + mock_request_json.return_value = { + "data": [ + { + "vod_id": 18, + "vod_name": "详情片", + "vod_pic": "/detail.jpg", + "vod_remarks": "更新至2集", + "vod_year": "2026", + "vod_area": "大陆", + "vod_actor": "甲/乙", + "vod_director": "丙", + "vod_content": "

一段简介

", + "vod_class": "剧情", + "vod_play_from": "线路一", + "vod_play_url": "第1集$/play/1", + } + ] + } + result = self.spider.detailContent(["18"]) + detail = result["list"][0] + self.assertEqual(detail["vod_id"], "18") + self.assertEqual(detail["vod_content"], "一段简介") + self.assertEqual(detail["type_name"], "剧情") + self.assertEqual(detail["vod_play_from"], "线路一(1)") + self.assertEqual(detail["vod_play_url"], "第1集$线路一@1@/play/1") + + def test_extract_decode_url_accepts_multiple_shapes(self): + self.assertEqual(self.spider._extract_decode_url({"data": "https://a.example/m3u8"}), "https://a.example/m3u8") + self.assertEqual( + self.spider._extract_decode_url({"data": {"url": "https://b.example/m3u8"}}), + "https://b.example/m3u8", + ) + self.assertEqual(self.spider._extract_decode_url({"url": "https://c.example/m3u8"}), "https://c.example/m3u8") + + @patch.object(Spider, "_request_json") + def test_player_content_decodes_encoded_play_id(self, mock_request_json): + mock_request_json.return_value = {"data": {"url": "https://cdn.example/real.m3u8"}} + result = self.spider.playerContent("线路一", "线路一@1@/play/1", {}) + kwargs = mock_request_json.call_args.kwargs + self.assertEqual(kwargs["params"]["url"], "/play/1") + self.assertEqual(kwargs["params"]["vodFrom"], "线路一") + self.assertEqual(result["parse"], 0) + self.assertEqual(result["playUrl"], "") + self.assertEqual(result["url"], "https://cdn.example/real.m3u8") + + @patch.object(Spider, "_request_json") + def test_player_content_sets_jx_for_major_video_sites(self, mock_request_json): + mock_request_json.return_value = {"data": {"url": "https://v.qq.com/x/cover/demo.html"}} + result = self.spider.playerContent("线路一", "线路一@1@/play/1", {}) + self.assertEqual(result["jx"], 1) + + def test_player_content_passthrough_for_direct_url(self): + result = self.spider.playerContent("直连", "https://cdn.example/direct.m3u8", {}) + self.assertEqual(result["url"], "https://cdn.example/direct.m3u8") + self.assertEqual(result["jx"], 0) + + def test_home_content_keeps_reference_filter_values(self): + movie_filters = self.spider.homeContent(False)["filters"]["1"] + self.assertIn({"n": "动作", "v": "动作"}, movie_filters[0]["value"]) + self.assertIn({"n": "大陆", "v": "大陆"}, movie_filters[1]["value"]) + self.assertIn({"n": "评分", "v": "score"}, movie_filters[3]["value"]) + + @patch.object(Spider, "_request_json") + def test_detail_content_returns_empty_list_when_payload_missing(self, mock_request_json): + mock_request_json.return_value = {"data": []} + self.assertEqual(self.spider.detailContent(["404"]), {"list": []}) + + @patch.object(Spider, "_request_json") + def test_player_content_falls_back_to_raw_input_when_decode_fails(self, mock_request_json): + mock_request_json.return_value = {} + result = self.spider.playerContent("线路一", "线路一@1@/play/404", {}) + self.assertEqual(result["url"], "/play/404") + + +if __name__ == "__main__": + unittest.main() diff --git a/py/步步影视.py b/py/步步影视.py new file mode 100644 index 0000000..950ebd9 --- /dev/null +++ b/py/步步影视.py @@ -0,0 +1,488 @@ +# coding=utf-8 +import hashlib +import random +import re +import sys +import time + +from base.spider import Spider as BaseSpider + +sys.path.append("..") + + +class Spider(BaseSpider): + def __init__(self): + self.name = "步步影视" + self.host = "https://bbys.app" + self.pkg = "com.sunshine.tv" + self.ver = "4" + self.finger = "SF-C3B2B41F6EFFFF9869176CF68F6790E8F07506FC88632C94B4F5F0430D5498CA" + self.sk = "SK-thanks" + self.web_sign = "f65f3a83d6d9ad6f" + self.x_client = "8f3d2a1c7b6e5d4c9a0b1f2e3d4c5b6a" + self.current_year = time.localtime().tm_year + self.headers = { + "User-Agent": "okhttp/4.12.0", + "Accept": "application/json", + } + self.classes = [ + {"type_id": "1", "type_name": "电影"}, + {"type_id": "2", "type_name": "剧集"}, + {"type_id": "3", "type_name": "动漫"}, + {"type_id": "4", "type_name": "综艺"}, + ] + self.filters = { + "1": [ + { + "key": "class", + "name": "类型", + "value": [ + {"n": "全部", "v": ""}, + {"n": "动作", "v": "动作"}, + {"n": "喜剧", "v": "喜剧"}, + {"n": "爱情", "v": "爱情"}, + {"n": "科幻", "v": "科幻"}, + {"n": "恐怖", "v": "恐怖"}, + {"n": "悬疑", "v": "悬疑"}, + {"n": "犯罪", "v": "犯罪"}, + {"n": "战争", "v": "战争"}, + {"n": "动画", "v": "动画"}, + {"n": "冒险", "v": "冒险"}, + {"n": "历史", "v": "历史"}, + {"n": "灾难", "v": "灾难"}, + {"n": "纪录", "v": "纪录"}, + {"n": "剧情", "v": "剧情"}, + ], + }, + { + "key": "area", + "name": "地区", + "value": [ + {"n": "全部", "v": ""}, + {"n": "大陆", "v": "大陆"}, + {"n": "香港", "v": "香港"}, + {"n": "台湾", "v": "台湾"}, + {"n": "美国", "v": "美国"}, + {"n": "日本", "v": "日本"}, + {"n": "韩国", "v": "韩国"}, + {"n": "泰国", "v": "泰国"}, + {"n": "印度", "v": "印度"}, + {"n": "英国", "v": "英国"}, + {"n": "法国", "v": "法国"}, + {"n": "德国", "v": "德国"}, + {"n": "加拿大", "v": "加拿大"}, + {"n": "西班牙", "v": "西班牙"}, + {"n": "意大利", "v": "意大利"}, + {"n": "澳大利亚", "v": "澳大利亚"}, + ], + }, + {"key": "year", "name": "年份", "value": self._generate_years("电影")}, + { + "key": "by", + "name": "排序", + "value": [ + {"n": "最热", "v": "hits"}, + {"n": "最新", "v": "time"}, + {"n": "评分", "v": "score"}, + ], + }, + ], + "2": [ + { + "key": "class", + "name": "类型", + "value": [ + {"n": "全部", "v": ""}, + {"n": "爱情", "v": "爱情"}, + {"n": "古装", "v": "古装"}, + {"n": "武侠", "v": "武侠"}, + {"n": "历史", "v": "历史"}, + {"n": "家庭", "v": "家庭"}, + {"n": "喜剧", "v": "喜剧"}, + {"n": "悬疑", "v": "悬疑"}, + {"n": "犯罪", "v": "犯罪"}, + {"n": "战争", "v": "战争"}, + {"n": "奇幻", "v": "奇幻"}, + {"n": "科幻", "v": "科幻"}, + {"n": "恐怖", "v": "恐怖"}, + ], + }, + { + "key": "area", + "name": "地区", + "value": [ + {"n": "全部", "v": ""}, + {"n": "大陆", "v": "大陆"}, + {"n": "香港", "v": "香港"}, + {"n": "台湾", "v": "台湾"}, + {"n": "美国", "v": "美国"}, + {"n": "日本", "v": "日本"}, + {"n": "韩国", "v": "韩国"}, + {"n": "泰国", "v": "泰国"}, + {"n": "英国", "v": "英国"}, + ], + }, + {"key": "year", "name": "年份", "value": self._generate_years("剧集")}, + { + "key": "by", + "name": "排序", + "value": [ + {"n": "最热", "v": "hits"}, + {"n": "最新", "v": "time"}, + {"n": "评分", "v": "score"}, + ], + }, + ], + "3": [ + { + "key": "class", + "name": "类型", + "value": [ + {"n": "全部", "v": ""}, + {"n": "冒险", "v": "冒险"}, + {"n": "奇幻", "v": "奇幻"}, + {"n": "科幻", "v": "科幻"}, + {"n": "武侠", "v": "武侠"}, + {"n": "悬疑", "v": "悬疑"}, + ], + }, + { + "key": "area", + "name": "地区", + "value": [ + {"n": "全部", "v": ""}, + {"n": "大陆", "v": "大陆"}, + {"n": "日本", "v": "日本"}, + {"n": "欧美", "v": "欧美"}, + ], + }, + {"key": "year", "name": "年份", "value": self._generate_years("动漫")}, + { + "key": "by", + "name": "排序", + "value": [ + {"n": "最热", "v": "hits"}, + {"n": "最新", "v": "time"}, + {"n": "评分", "v": "score"}, + ], + }, + ], + "4": [ + { + "key": "class", + "name": "类型", + "value": [ + {"n": "全部", "v": ""}, + {"n": "真人秀", "v": "真人秀"}, + {"n": "音乐", "v": "音乐"}, + {"n": "脱口秀", "v": "脱口秀"}, + {"n": "歌舞", "v": "歌舞"}, + {"n": "爱情", "v": "爱情"}, + ], + }, + { + "key": "area", + "name": "地区", + "value": [ + {"n": "全部", "v": ""}, + {"n": "大陆", "v": "大陆"}, + {"n": "香港", "v": "香港"}, + {"n": "台湾", "v": "台湾"}, + {"n": "美国", "v": "美国"}, + {"n": "日本", "v": "日本"}, + {"n": "韩国", "v": "韩国"}, + ], + }, + {"key": "year", "name": "年份", "value": self._generate_years("综艺")}, + { + "key": "by", + "name": "排序", + "value": [ + {"n": "最热", "v": "hits"}, + {"n": "最新", "v": "time"}, + {"n": "评分", "v": "score"}, + ], + }, + ], + } + + def init(self, extend=""): + return None + + def getName(self): + return self.name + + def homeContent(self, filter): + return {"class": self.classes, "filters": self.filters} + + def homeVideoContent(self): + payload = self._request_json("/api.php/web/index/home", headers=self._get_web_headers()) + categories = ((payload.get("data") or {}).get("categories") or []) + videos = [] + for category in categories: + videos.extend(self._convert_json_to_vods(category.get("videos") or [])) + if not videos: + for type_name in ("电影", "剧集", "综艺", "动漫"): + videos.extend(self._get_class_list(type_name, 1, {"by": "hits"}).get("list", [])) + return {"list": videos} + + def categoryContent(self, tid, pg, filter, extend): + type_name = self._map_type_name(tid) + if type_name not in ("电影", "剧集", "动漫", "综艺"): + videos = [] + for name in ("电影", "剧集", "综艺", "动漫"): + videos.extend(self._get_class_list(name, 1, {"by": "hits"}).get("list", [])) + return {"page": 1, "limit": len(videos), "total": len(videos), "list": videos} + return self._get_class_list(type_name, int(pg), extend or {}) + + def detailContent(self, ids): + vod_id = self._stringify(ids[0] if isinstance(ids, list) and ids else ids).strip() + if not vod_id: + return {"list": []} + payload = self._request_json( + "/api.php/web/vod/get_detail", + headers=self._get_web_headers(), + params={"vod_id": vod_id}, + ) + detail = payload.get("data") + if isinstance(detail, list): + detail = detail[0] if detail else None + if not detail: + return {"list": []} + play_data = self._build_play_data(detail.get("vod_play_from", ""), detail.get("vod_play_url", "")) + return { + "list": [ + { + "vod_id": self._stringify(detail.get("vod_id") or vod_id), + "vod_name": self._stringify(detail.get("vod_name")), + "vod_pic": self._stringify(detail.get("vod_pic")), + "vod_remarks": self._stringify(detail.get("vod_remarks") or detail.get("vod_duration")), + "vod_year": self._stringify(detail.get("vod_year")), + "vod_area": self._stringify(detail.get("vod_area")), + "vod_actor": self._stringify(detail.get("vod_actor")), + "vod_director": self._stringify(detail.get("vod_director")), + "vod_content": self._clean_html_content(detail.get("vod_content")), + "type_name": self._stringify(detail.get("vod_class") or detail.get("type_name")), + "vod_play_from": play_data["vod_play_from"], + "vod_play_url": play_data["vod_play_url"], + } + ] + } + + def searchContent(self, key, quick, pg="1"): + keyword = self._stringify(key).strip() + page = int(pg) + if not keyword: + return {"page": page, "limit": 0, "total": 0, "list": []} + payload = self._request_json( + "/api.php/app/search/index", + headers=self._get_app_headers(), + params={"wd": keyword, "page": page, "limit": 15}, + ) + items = self._convert_json_to_vods(payload.get("data") or []) + return {"page": page, "limit": len(items), "total": len(items), "list": items} + + def playerContent(self, flag, id, vipFlags): + raw = self._stringify(id).strip() + if not raw: + return {"parse": 0, "playUrl": "", "url": "", "jx": 0} + + play_from = "" + play_url = raw + need_decode = False + matched = re.match(r"^([^@]+)@([01])@([\s\S]+)$", raw) + if matched: + play_from = matched.group(1) + need_decode = matched.group(2) == "1" + play_url = matched.group(3) + elif not raw.startswith(("http://", "https://")): + need_decode = True + + final_url = play_url + if need_decode: + payload = self._request_json( + "/api.php/app/decode/url/", + headers=self._get_app_headers(), + params={"url": play_url, "vodFrom": play_from}, + ) + decoded = self._extract_decode_url(payload) + if decoded: + final_url = decoded + + need_jx = 1 if re.search(r"(iqiyi\.com|v\.qq\.com|youku\.com|mgtv\.com|bilibili\.com)", final_url, re.I) else 0 + return { + "parse": 0, + "playUrl": "", + "url": final_url, + "jx": need_jx, + "header": {"User-Agent": "okhttp/4.12.0"}, + } + + def _stringify(self, value): + return "" if value is None else str(value) + + def _join_text(self, value): + if isinstance(value, list): + return "/".join([self._stringify(item) for item in value if self._stringify(item)]) + return self._stringify(value) + + def _clean_html_content(self, content): + if not content: + return "" + text = str(content).replace("

", "").replace("

", "\n") + text = re.sub(r"", "\n", text, flags=re.I) + text = re.sub(r"<[^>]+>", "", text) + return text.strip() + + def _safe_split(self, value, sep): + if value or value == "": + return str(value).split(sep) + return [] + + def _build_play_data(self, vod_play_from, vod_play_url): + from_list = self._safe_split(vod_play_from, "$$$") + url_groups = self._safe_split(vod_play_url, "$$$") + final_from = [] + final_url = [] + for index in range(max(len(from_list), len(url_groups))): + raw_from = (from_list[index] if index < len(from_list) else f"line{index + 1}").strip() or f"line{index + 1}" + group = (url_groups[index] if index < len(url_groups) else "").strip() + if not group: + continue + episodes = [] + for item in [part.strip() for part in group.split("#") if part.strip()]: + if "$" in item: + title, raw_url = item.split("$", 1) + else: + title, raw_url = "播放", item + title = title.strip() or "播放" + raw_url = raw_url.strip() + if raw_url: + episodes.append(f"{title}${raw_from}@1@{raw_url}") + if episodes: + final_from.append(f"{raw_from}({len(episodes)})") + final_url.append("#".join(episodes)) + return {"vod_play_from": "$$$".join(final_from), "vod_play_url": "$$$".join(final_url)} + + def _generate_years(self, type_name): + years = [{"n": "全部", "v": ""}] + if type_name == "电影": + for year in range(self.current_year, 2015, -1): + years.append({"n": str(year), "v": str(year)}) + years.extend( + [ + {"n": "2015-2011", "v": "2015-2011"}, + {"n": "2010-2000", "v": "2010-2000"}, + {"n": "90年代", "v": "90年代"}, + {"n": "80年代", "v": "80年代"}, + {"n": "更早", "v": "更早"}, + ] + ) + return years + + floor = 2021 if type_name == "剧集" else 2011 + for year in range(self.current_year, floor - 1, -1): + years.append({"n": str(year), "v": str(year)}) + years.append({"n": "更早", "v": "更早"}) + return years + + def _create_sign_data(self): + timestamp = str(int(time.time())) + nonce = f"{random.randint(0, 9)}{random.randint(10, 99)}" + sign_source = ( + f"finger={self.finger}&id={self.pkg}&nonce={nonce}" + f"&sk={self.sk}&time={timestamp}&v={self.ver}" + ) + sign = hashlib.sha256(sign_source.encode("utf-8")).hexdigest().upper() + return {"timestamp": timestamp, "nonce": nonce, "sign": sign} + + def _get_app_headers(self): + sign_data = self._create_sign_data() + headers = dict(self.headers) + headers.update( + { + "x-aid": self.pkg, + "x-time": sign_data["timestamp"], + "x-sign": sign_data["sign"], + "x-nonc": sign_data["nonce"], + "x-ave": self.ver, + } + ) + return headers + + def _get_web_headers(self): + headers = self._get_app_headers() + headers.update( + { + "web-sign": self.web_sign, + "X-Client": self.x_client, + } + ) + return headers + + def _request_json(self, path, headers=None, params=None): + url = path if str(path).startswith("http") else self.host + path + response = self.fetch(url, headers=headers, params=params, timeout=15, verify=False) + if response.status_code != 200: + return {} + return response.json() + + def _convert_json_to_vods(self, video_list): + items = [] + for item in video_list or []: + items.append( + { + "vod_id": self._stringify(item.get("vod_id") or item.get("id")), + "vod_name": self._stringify(item.get("vod_name") or item.get("name")), + "vod_pic": self._stringify(item.get("vod_pic") or item.get("pic")), + "vod_remarks": self._stringify( + item.get("vod_remarks") or item.get("vod_duration") or item.get("remark") + ), + "type_name": self._join_text(item.get("vod_class") or item.get("type_name") or item.get("class")), + "vod_year": self._join_text(item.get("vod_year") or item.get("year")), + "vod_area": self._join_text(item.get("vod_area") or item.get("area")), + } + ) + return items + + def _map_type_name(self, value): + mapping = { + "1": "电影", + "2": "剧集", + "3": "动漫", + "4": "综艺", + "电影": "电影", + "剧集": "剧集", + "动漫": "动漫", + "综艺": "综艺", + } + return mapping.get(self._stringify(value), self._stringify(value)) + + def _get_class_list(self, tid, page=1, extend=None): + type_name = self._map_type_name(tid) + ext = extend or {} + payload = self._request_json( + "/api.php/web/filter/vod", + headers=self._get_web_headers(), + params={ + "type_name": type_name, + "page": int(page), + "sort": ext.get("by", "hits"), + "class": ext.get("class", ""), + "area": ext.get("area", ""), + "year": ext.get("year", ""), + }, + ) + items = self._convert_json_to_vods(payload.get("data") or []) + return {"page": int(page), "limit": len(items), "total": len(items), "list": items} + + def _extract_decode_url(self, payload): + if isinstance(payload, dict): + data = payload.get("data") + if isinstance(data, str) and data: + return data + if isinstance(data, dict) and isinstance(data.get("url"), str) and data.get("url"): + return data.get("url") + if isinstance(payload.get("url"), str) and payload.get("url"): + return payload.get("url") + return ""