diff --git a/app/modules/indexer/spider/haidan.py b/app/modules/indexer/spider/haidan.py index cffb16f97..ea32bc56b 100644 --- a/app/modules/indexer/spider/haidan.py +++ b/app/modules/indexer/spider/haidan.py @@ -28,6 +28,8 @@ class HaiDanSpider: # 电影分类 _movie_category = ['401', '404', '405'] _tv_category = ['402', '403', '404', '405'] + # 音乐分类:408 为海胆 HQ Audio(音乐) 分区;406 MV 属于视频,不计入音乐 + _music_category = ['408'] # 足销状态 1-普通,2-免费,3-2X,4-2X免费,5-50%,6-2X50%,7-30% _dl_state = { @@ -88,6 +90,8 @@ class HaiDanSpider: categories = [] elif mtype == MediaType.TV: categories = self._tv_category + elif mtype == MediaType.MUSIC: + categories = self._music_category else: categories = self._movie_category @@ -113,8 +117,11 @@ class HaiDanSpider: torrents = [] data = result.get('data') or {} for tid, item in data.items(): - category_value = result.get('category') - if category_value in self._tv_category \ + # 分类字段在每条种子内,接口顶层没有 category 字段 + category_value = str(item.get('category') or '') + if category_value in self._music_category: + category = MediaType.MUSIC.value + elif category_value in self._tv_category \ and category_value not in self._movie_category: category = MediaType.TV.value elif category_value in self._movie_category: diff --git a/app/modules/indexer/spider/hddolby.py b/app/modules/indexer/spider/hddolby.py index 324b0b353..63c5d2dd8 100644 --- a/app/modules/indexer/spider/hddolby.py +++ b/app/modules/indexer/spider/hddolby.py @@ -28,6 +28,8 @@ class HddolbySpider: # 分类 _movie_category = [401, 405] _tv_category = [402, 403, 404, 405] + # 音乐分类:408 为杜比音乐唱片分区;406 演唱会蓝光属于视频,不计入音乐 + _music_category = [408] # 标签 _labels = { @@ -88,6 +90,8 @@ class HddolbySpider: categories = self._tv_category elif mtype == MediaType.MOVIE: categories = self._movie_category + elif mtype == MediaType.MUSIC: + categories = self._music_category else: categories = list(set(self._movie_category + self._tv_category)) @@ -137,7 +141,9 @@ class HddolbySpider: """ # 类别 category_value = result.get('category') - if category_value in self._tv_category: + if category_value in self._music_category: + category = MediaType.MUSIC.value + elif category_value in self._tv_category: category = MediaType.TV.value elif category_value in self._movie_category: category = MediaType.MOVIE.value diff --git a/app/modules/indexer/spider/mtorrent.py b/app/modules/indexer/spider/mtorrent.py index 7759d52e3..2434a9b79 100644 --- a/app/modules/indexer/spider/mtorrent.py +++ b/app/modules/indexer/spider/mtorrent.py @@ -34,7 +34,10 @@ class MTorrentSpider: # 电影分类 _movie_category = ['401', '419', '420', '421', '439', '405', '404'] + # 电视剧分类 _tv_category = ['403', '402', '435', '438', '404', '405'] + # 音乐分类:406 为馒头 Music(音樂) 分区 + _music_category = ['406'] # API KEY _apikey = None @@ -84,6 +87,8 @@ class MTorrentSpider: categories = [] elif mtype == MediaType.TV: categories = self._tv_category + elif mtype == MediaType.MUSIC: + categories = self._music_category else: categories = self._movie_category # mtorrent搜索imdb需要输入完整imdb链接,参见 https://wiki.m-team.cc/zh-tw/imdbtosearch @@ -107,7 +112,9 @@ class MTorrentSpider: for result in results: category_value = result.get('category') - if category_value in self._tv_category \ + if category_value in self._music_category: + category = MediaType.MUSIC.value + elif category_value in self._tv_category \ and category_value not in self._movie_category: category = MediaType.TV.value elif category_value in self._movie_category: diff --git a/app/modules/indexer/spider/rousi.py b/app/modules/indexer/spider/rousi.py index 78c3d290a..211117de2 100644 --- a/app/modules/indexer/spider/rousi.py +++ b/app/modules/indexer/spider/rousi.py @@ -35,6 +35,7 @@ class RousiSpider: # API 不支持多分类搜索,每次只使用一个分类 _movie_category = 'movie' _tv_category = 'tv' + _music_category = 'music' # API KEY _apikey = None @@ -67,7 +68,7 @@ class RousiSpider: 构建 API 请求参数 :param keyword: 搜索关键词 - :param mtype: 媒体类型 (MOVIE/TV) + :param mtype: 媒体类型 (MOVIE/TV/MUSIC) :param cat: 用户选择的分类 ID(逗号分隔的字符串) :param page: 页码(从 0 开始,API 需要从 1 开始) :return: 请求参数字典 @@ -93,6 +94,8 @@ class RousiSpider: params["category"] = self._movie_category elif mtype == MediaType.TV: params["category"] = self._tv_category + elif mtype == MediaType.MUSIC: + params["category"] = self._music_category return params @@ -112,6 +115,7 @@ class RousiSpider: '2': 'tv', '3': 'documentary', '4': 'animation', + '5': 'music', '6': 'variety' } @@ -178,6 +182,8 @@ class RousiSpider: category = MediaType.MOVIE.value elif cat_val == self._tv_category: category = MediaType.TV.value + elif cat_val == self._music_category: + category = MediaType.MUSIC.value else: category = MediaType.UNKNOWN.value @@ -219,7 +225,7 @@ class RousiSpider: 同步搜索种子 :param keyword: 搜索关键词 - :param mtype: 媒体类型 (MOVIE/TV) + :param mtype: 媒体类型 (MOVIE/TV/MUSIC) :param cat: 用户选择的分类 ID(逗号分隔) :param page: 页码(从 0 开始) :return: (是否发生错误, 种子列表) @@ -247,7 +253,7 @@ class RousiSpider: 异步搜索种子 :param keyword: 搜索关键词 - :param mtype: 媒体类型 (MOVIE/TV) + :param mtype: 媒体类型 (MOVIE/TV/MUSIC) :param cat: 用户选择的分类 ID(逗号分隔) :param page: 页码(从 0 开始) :return: (是否发生错误, 种子列表) diff --git a/app/modules/indexer/spider/sunnypt.py b/app/modules/indexer/spider/sunnypt.py index b5d231546..511856ac7 100644 --- a/app/modules/indexer/spider/sunnypt.py +++ b/app/modules/indexer/spider/sunnypt.py @@ -54,12 +54,12 @@ class SunnyPTSpider: @staticmethod def _parse_configured_categories(category_config: dict) -> dict: """ - 从站点索引配置提取电影和电视剧分类 ID,作为分类接口不可用时的兜底 + 从站点索引配置提取电影、电视剧和音乐分类 ID,作为分类接口不可用时的兜底 :param category_config: Build 站点配置中的分类段 :return: 按 API media_type 索引的分类 ID """ - category_map = {"movie": [], "tv": []} + category_map = {"movie": [], "tv": [], "music": []} for media_type in category_map: for item in category_config.get(media_type) or []: category_id = item.get("id") if isinstance(item, dict) else item @@ -75,7 +75,7 @@ class SunnyPTSpider: :param items: SunnyPT 分类接口 data 数组 :return: 按 API media_type 索引的分类 ID """ - category_map = {"movie": [], "tv": []} + category_map = {"movie": [], "tv": [], "music": []} for item in items or []: if not isinstance(item, dict) or item.get("id") is None: continue @@ -199,12 +199,14 @@ class SunnyPTSpider: 将 MoviePilot 媒体类型转换为 SunnyPT API 枚举 :param media_type: MoviePilot 媒体类型 - :return: movie、tv 或 None + :return: movie、tv、music 或 None """ if media_type == MediaType.MOVIE: return "movie" if media_type == MediaType.TV: return "tv" + if media_type == MediaType.MUSIC: + return "music" return None def _build_params( @@ -259,6 +261,8 @@ class SunnyPTSpider: category = MediaType.MOVIE.value elif media_type == "tv": category = MediaType.TV.value + elif media_type == "music": + category = MediaType.MUSIC.value else: category = MediaType.UNKNOWN.value diff --git a/tests/test_haidan_spider.py b/tests/test_haidan_spider.py new file mode 100644 index 000000000..16a6370ea --- /dev/null +++ b/tests/test_haidan_spider.py @@ -0,0 +1,60 @@ +# -*- coding: utf-8 -*- +import pytest + +from app.modules.indexer.spider import haidan as haidan_module +from app.modules.indexer.spider.haidan import HaiDanSpider +from app.schemas import MediaType + + +def _build_indexer() -> dict: + """构造海胆 API Spider 所需的最小站点配置。""" + return { + "id": "haidan", + "name": "海胆之家", + "domain": "https://www.haidan.cc/", + "cookie": "haidan-cookie", + "ua": "MoviePilot-Test", + "proxy": False, + } + + +@pytest.fixture() +def haidan_spider(monkeypatch): + """构造不依赖真实数据库配置的 HaiDanSpider。""" + monkeypatch.setattr(haidan_module, "SystemConfigOper", lambda: None) + return HaiDanSpider(_build_indexer()) + + +def test_music_search_uses_music_categories(haidan_spider): + """音乐搜索应只提交海胆 HQ Audio 分区分类。""" + params = haidan_spider._HaiDanSpider__get_params("张学友", MediaType.MUSIC) + + assert "cat=408" in params + assert "401" not in params + + +def test_movie_search_keeps_movie_categories(haidan_spider): + """电影搜索行为不受音乐分类新增影响。""" + params = haidan_spider._HaiDanSpider__get_params("流浪地球", MediaType.MOVIE) + + assert "cat=401%2C404%2C405" in params or "cat=401,404,405" in params + + +def test_parse_result_reads_item_category(haidan_spider): + """分类字段取自每条种子,音乐分区应标记为音乐媒体类型。""" + result = { + "code": 0, + "data": { + "1": {"name": "张学友 - 他在那里 FLAC", "category": 408, "size": "1024"}, + "2": {"name": "流浪地球", "category": 401, "size": "1024"}, + "3": {"name": "剧集 S01", "category": 402, "size": "1024"}, + }, + } + + torrents = haidan_spider._HaiDanSpider__parse_result(result) + + assert [torrent["category"] for torrent in torrents] == [ + MediaType.MUSIC.value, + MediaType.MOVIE.value, + MediaType.TV.value, + ] diff --git a/tests/test_hddolby_spider.py b/tests/test_hddolby_spider.py new file mode 100644 index 000000000..8998a8652 --- /dev/null +++ b/tests/test_hddolby_spider.py @@ -0,0 +1,56 @@ +# -*- coding: utf-8 -*- +import pytest + +from app.modules.indexer.spider import hddolby as hddolby_module +from app.modules.indexer.spider.hddolby import HddolbySpider +from app.schemas import MediaType + + +def _build_indexer() -> dict: + """构造杜比 API Spider 所需的最小站点配置。""" + return { + "id": "hddolby", + "name": "高清杜比", + "domain": "https://www.hddolby.com/", + "apikey": "dolby-secret", + "ua": "MoviePilot-Test", + "proxy": False, + } + + +@pytest.fixture() +def hddolby_spider(monkeypatch): + """构造不依赖真实数据库配置的 HddolbySpider。""" + monkeypatch.setattr(hddolby_module, "SystemConfigOper", lambda: None) + return HddolbySpider(_build_indexer()) + + +def test_music_search_uses_music_categories(hddolby_spider): + """音乐搜索应只提交杜比音乐唱片分区分类。""" + params = hddolby_spider._HddolbySpider__get_params("张学友", MediaType.MUSIC, 0) + + assert params["categories"] == HddolbySpider._music_category + + +def test_movie_search_keeps_movie_categories(hddolby_spider): + """电影搜索行为不受音乐分类新增影响。""" + params = hddolby_spider._HddolbySpider__get_params("流浪地球", MediaType.MOVIE, 0) + + assert params["categories"] == HddolbySpider._movie_category + + +def test_parse_result_marks_music_torrents(hddolby_spider): + """音乐唱片分区种子应标记为音乐媒体类型,演唱会视频仍按原逻辑处理。""" + results = hddolby_spider._HddolbySpider__parse_result([ + {"id": 1, "name": "VA - Chillout 2022 FLAC", "category": 408, "size": 1024}, + {"id": 2, "name": "Epica Live 1080p Blu-ray", "category": 406, "size": 1024}, + {"id": 3, "name": "流浪地球 2160p", "category": 401, "size": 1024}, + {"id": 4, "name": "剧集 S01", "category": 402, "size": 1024}, + ]) + + assert [torrent["category"] for torrent in results] == [ + MediaType.MUSIC.value, + MediaType.UNKNOWN.value, + MediaType.MOVIE.value, + MediaType.TV.value, + ] diff --git a/tests/test_mtorrent_spider.py b/tests/test_mtorrent_spider.py new file mode 100644 index 000000000..8f6755072 --- /dev/null +++ b/tests/test_mtorrent_spider.py @@ -0,0 +1,54 @@ +# -*- coding: utf-8 -*- +import pytest + +from app.modules.indexer.spider import mtorrent as mtorrent_module +from app.modules.indexer.spider.mtorrent import MTorrentSpider +from app.schemas import MediaType + + +def _build_indexer() -> dict: + """构造 M-Team API Spider 所需的最小站点配置。""" + return { + "id": "mteam", + "name": "馒头", + "domain": "https://xp.m-team.io/", + "apikey": "mteam-secret", + "ua": "MoviePilot-Test", + "proxy": False, + } + + +@pytest.fixture() +def mteam_spider(monkeypatch): + """构造不依赖真实数据库配置的 MTorrentSpider。""" + monkeypatch.setattr(mtorrent_module, "SystemConfigOper", lambda: None) + return MTorrentSpider(_build_indexer()) + + +def test_music_search_uses_music_categories(mteam_spider): + """音乐搜索应只提交馒头音乐分区分类,而不是电影分类。""" + params = mteam_spider._MTorrentSpider__get_params("周杰伦 七里香", MediaType.MUSIC) + + assert params["categories"] == MTorrentSpider._music_category + + +def test_movie_search_keeps_movie_categories(mteam_spider): + """电影搜索行为不受音乐分类新增影响。""" + params = mteam_spider._MTorrentSpider__get_params("流浪地球", MediaType.MOVIE) + + assert params["categories"] == MTorrentSpider._movie_category + + +def test_parse_result_marks_music_torrents(mteam_spider): + """音乐分区种子应标记为音乐媒体类型,供音乐搜索链路筛选。""" + results = mteam_spider._MTorrentSpider__parse_result([ + {"id": "1", "name": "周杰伦 - 七里香 [FLAC]", "category": "406", "size": "1024", "status": {}}, + {"id": "2", "name": "流浪地球 2160p", "category": "419", "size": "1024", "status": {}}, + {"id": "3", "name": "其他资源", "category": "999", "size": "1024", "status": {}}, + ]) + + assert [torrent["category"] for torrent in results] == [ + MediaType.MUSIC.value, + MediaType.MOVIE.value, + MediaType.UNKNOWN.value, + ] diff --git a/tests/test_rousi_spider.py b/tests/test_rousi_spider.py new file mode 100644 index 000000000..61c3a29e6 --- /dev/null +++ b/tests/test_rousi_spider.py @@ -0,0 +1,54 @@ +# -*- coding: utf-8 -*- +import pytest + +from app.modules.indexer.spider import rousi as rousi_module +from app.modules.indexer.spider.rousi import RousiSpider +from app.schemas import MediaType + + +def _build_indexer() -> dict: + """构造 Rousi Pro API Spider 所需的最小站点配置。""" + return { + "id": "rousipro", + "name": "Rousi Pro", + "domain": "https://rousi.pro/", + "apikey": "rousi-secret", + "ua": "MoviePilot-Test", + "proxy": False, + } + + +@pytest.fixture() +def rousi_spider(monkeypatch): + """构造不依赖真实数据库配置的 RousiSpider。""" + monkeypatch.setattr(rousi_module, "SystemConfigOper", lambda: None) + return RousiSpider(_build_indexer()) + + +def test_music_search_uses_music_category(rousi_spider): + """音乐搜索应提交 Rousi Pro 的 music 分类参数。""" + params = rousi_spider._RousiSpider__get_params("张学友", MediaType.MUSIC, None, 0) + + assert params["category"] == "music" + + +def test_user_selected_music_category_maps_to_api_name(rousi_spider): + """用户选择音乐分类 ID 时应映射为 API 的 music 分类名。""" + params = rousi_spider._RousiSpider__get_params("张学友", None, "5", 0) + + assert params["category"] == "music" + + +def test_parse_result_marks_music_torrents(rousi_spider): + """music 分类种子应标记为音乐媒体类型。""" + torrents = rousi_spider._RousiSpider__parse_result([ + {"id": 1, "uuid": "u1", "title": "张学友 - 他在那里 FLAC", "category": "music"}, + {"id": 2, "uuid": "u2", "title": "流浪地球", "category": "movie"}, + {"id": 3, "uuid": "u3", "title": "剧集 S01", "category": {"slug": "tv"}}, + ]) + + assert [torrent["category"] for torrent in torrents] == [ + MediaType.MUSIC.value, + MediaType.MOVIE.value, + MediaType.TV.value, + ] diff --git a/tests/test_sunnypt_indexer.py b/tests/test_sunnypt_indexer.py index 1aac3adc1..1f15271bc 100644 --- a/tests/test_sunnypt_indexer.py +++ b/tests/test_sunnypt_indexer.py @@ -473,3 +473,54 @@ def test_indirect_download_does_not_log_or_cache_temporary_url(monkeypatch): ) assert not any("sunny-secret" in message for message in log_messages) assert not any("temporary-credential" in message for message in log_messages) + + +def test_sunnypt_music_search_uses_music_media_type(monkeypatch): + """SunnyPT 音乐搜索应提交 music 媒体类型并将结果标记为音乐。""" + calls = [] + + def fake_get_res(request, url: str, params: dict = None, **_kwargs): + """按请求路径回放分类和种子响应。""" + calls.append((url, params)) + if url.endswith("/categories"): + return _FakeResponse({ + "code": 0, + "msg": "ok", + "data": [ + {"id": 408, "name": "音乐", "media_types": ["music"]}, + ], + }) + return _FakeResponse({ + "code": 0, + "msg": "ok", + "data": { + "items": [{ + "id": 321, + "title": "Album 2026 FLAC", + "media_type": "music", + "size": 1024, + "created_at": "2026-08-10T00:00:00+08:00", + "seeders": 1, + "leechers": 0, + "completed": 1, + }], + }, + }) + + monkeypatch.setattr( + "app.modules.indexer.spider.sunnypt.RequestUtils.get_res", + fake_get_res, + ) + + error, torrents = SunnyPTSpider(_build_indexer()).search( + keyword="Album", + mtype=MediaType.MUSIC, + page=0, + ) + + assert not error + search_url, search_params = calls[-1] + assert search_url == "https://api.sunnypt.top/api/v1/mp/torrents" + assert search_params["media_type"] == "music" + assert search_params["categories"] == "408" + assert torrents[0]["category"] == MediaType.MUSIC.value