From 0f4d32b5b7a070db71beb48c12989977dee616a9 Mon Sep 17 00:00:00 2001
From: Harold <8866033@gmail.com>
Date: Fri, 24 Apr 2026 20:49:21 +0800
Subject: [PATCH] feat: scaffold sjmusic spider
---
py/tests/test_世纪音乐.py | 61 +++++++++++++++++++++++++++
py/世纪音乐.py | 89 +++++++++++++++++++++++++++++++++++++++
2 files changed, 150 insertions(+)
create mode 100644 py/tests/test_世纪音乐.py
create mode 100644 py/世纪音乐.py
diff --git a/py/tests/test_世纪音乐.py b/py/tests/test_世纪音乐.py
new file mode 100644
index 0000000..e5c0d65
--- /dev/null
+++ b/py/tests/test_世纪音乐.py
@@ -0,0 +1,61 @@
+import unittest
+from importlib.machinery import SourceFileLoader
+from pathlib import Path
+from types import SimpleNamespace
+from unittest.mock import patch
+
+
+ROOT = Path(__file__).resolve().parents[1]
+MODULE = SourceFileLoader("sjmusic_spider", str(ROOT / "世纪音乐.py")).load_module()
+Spider = MODULE.Spider
+
+
+HOME_HTML = """
+
+
+
+
+
+
+"""
+
+
+class TestSJMusicSpider(unittest.TestCase):
+ def setUp(self):
+ Spider._instance = None
+ self.spider = Spider()
+ self.spider.init()
+
+ @patch.object(Spider, "fetch")
+ def test_home_content_returns_classes_filters_and_home_items(self, mock_fetch):
+ mock_fetch.return_value = SimpleNamespace(status_code=200, text=HOME_HTML)
+ result = self.spider.homeContent(False)
+ self.assertEqual(
+ [item["type_id"] for item in result["class"]],
+ ["home", "rank_list", "playlist", "singer", "mv"],
+ )
+ self.assertIn("singer", result["filters"])
+ self.assertEqual(result["list"][0]["vod_id"], "song:123")
+ self.assertEqual(result["list"][0]["vod_name"], "周杰伦 - 夜曲")
+ self.assertEqual(result["list"][1]["vod_id"], "mv:456")
+
+ @patch.object(Spider, "fetch")
+ def test_home_video_content_reuses_home_list(self, mock_fetch):
+ mock_fetch.return_value = SimpleNamespace(status_code=200, text=HOME_HTML)
+ result = self.spider.homeVideoContent()
+ self.assertEqual([item["vod_id"] for item in result["list"]], ["song:123", "mv:456"])
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/py/世纪音乐.py b/py/世纪音乐.py
new file mode 100644
index 0000000..481c6a3
--- /dev/null
+++ b/py/世纪音乐.py
@@ -0,0 +1,89 @@
+# coding=utf-8
+import re
+import sys
+from urllib.parse import urljoin
+
+from base.spider import Spider as BaseSpider
+
+sys.path.append("..")
+
+
+class Spider(BaseSpider):
+ def __init__(self):
+ self.name = "世纪音乐"
+ self.host = "https://www.4c44.com"
+ self.headers = {
+ "User-Agent": (
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
+ "AppleWebKit/537.36 (KHTML, like Gecko) "
+ "Chrome/120.0.0.0 Safari/537.36"
+ ),
+ "Referer": self.host + "/",
+ }
+ self.classes = [
+ {"type_id": "home", "type_name": "首页推荐"},
+ {"type_id": "rank_list", "type_name": "排行榜"},
+ {"type_id": "playlist", "type_name": "歌单"},
+ {"type_id": "singer", "type_name": "歌手"},
+ {"type_id": "mv", "type_name": "MV"},
+ ]
+
+ def init(self, extend=""):
+ return None
+
+ def getName(self):
+ return self.name
+
+ def _build_url(self, path):
+ return urljoin(self.host + "/", str(path or "").strip())
+
+ def _clean_text(self, text):
+ return re.sub(r"\s+", " ", str(text or "")).strip()
+
+ def _load_html(self, html):
+ return self.html(str(html or "").strip() or "")
+
+ def _fetch_html(self, path):
+ response = self.fetch(self._build_url(path), headers=dict(self.headers), timeout=10, verify=False)
+ return response.text if getattr(response, "status_code", 0) == 200 else ""
+
+ def _encode_vod_id(self, href):
+ href = str(href or "")
+ if "/mp3/" in href:
+ return "song:" + href.rsplit("/", 1)[-1].replace(".html", "")
+ if "/mp4/" in href:
+ return "mv:" + href.rsplit("/", 1)[-1].replace(".html", "")
+ return ""
+
+ def _build_filters(self):
+ return {"singer": [], "mv": [], "playlist": []}
+
+ def _parse_home_items(self, html):
+ root = self._load_html(html)
+ items = []
+ for node in root.xpath("//*[@id='datalist']//li | //*[contains(@class,'video_list')]//li"):
+ href = "".join(node.xpath(".//a[1]/@href")).strip()
+ vod_id = self._encode_vod_id(href)
+ if not vod_id:
+ continue
+ name = self._clean_text("".join(node.xpath(".//div[contains(@class,'name')]//a[1]//text()")))
+ singer = self._clean_text("".join(node.xpath(".//*[contains(@class,'singer')][1]//text()")))
+ pic = self._build_url("".join(node.xpath(".//img[1]/@src")))
+ if vod_id.startswith("song:") and singer:
+ name = f"{singer} - {name}"
+ items.append(
+ {
+ "vod_id": vod_id,
+ "vod_name": name,
+ "vod_pic": pic,
+ "vod_remarks": "",
+ }
+ )
+ return items
+
+ def homeContent(self, filter):
+ items = self._parse_home_items(self._fetch_html("/"))
+ return {"class": list(self.classes), "filters": self._build_filters(), "list": items}
+
+ def homeVideoContent(self):
+ return {"list": self.homeContent(False).get("list", [])}