feat: 按媒体类型筛选搜索站点

This commit is contained in:
jxxghp
2026-08-09 18:12:29 +08:00
parent 1f8d4adba0
commit 36666313b7
5 changed files with 167 additions and 2 deletions
+82
View File
@@ -36,6 +36,42 @@ from app.utils.string import StringUtils
router = APIRouter()
def _indexer_supports_media_type(indexer: dict, media_type: MediaType) -> bool:
"""
判断站点索引器是否支持指定媒体类型。
:param indexer: 站点索引器配置
:param media_type: 待搜索的媒体类型
:return: 是否应在该媒体类型的站点选择列表中显示
"""
declared_media_type = indexer.get("media_type")
if isinstance(declared_media_type, MediaType):
site_media_type = declared_media_type
elif isinstance(declared_media_type, str):
site_media_type = MediaType.from_agent(declared_media_type)
else:
site_media_type = None
if site_media_type:
return site_media_type == media_type
categories = indexer.get("category") or {}
if not isinstance(categories, dict):
return media_type != MediaType.MUSIC
category_key = media_type.to_agent()
if media_type == MediaType.MUSIC:
return bool(categories.get(category_key))
declared_category_keys = {
item.to_agent()
for item in (MediaType.MOVIE, MediaType.TV, MediaType.MUSIC)
if categories.get(item.to_agent())
}
if declared_category_keys:
return category_key in declared_category_keys
return True
@router.get("/", summary="所有站点", response_model=List[schemas.Site])
async def read_sites(
db: AsyncSession = Depends(get_async_db),
@@ -47,6 +83,52 @@ async def read_sites(
return await Site.async_list_order_by_pri(db)
@router.get(
"/media/{media_type}",
summary="按媒体类型获取可搜索站点",
response_model=List[schemas.Site],
)
async def read_sites_by_media_type(
media_type: str,
db: AsyncSession = Depends(get_async_db),
_: User = Depends(get_current_active_manage_user_async),
) -> List[Site]:
"""
获取支持指定媒体类型的已配置启用站点。
:param media_type: Agent 媒体类型名称或中文媒体类型
:param db: 异步数据库会话
:return: 按优先级排序的可搜索站点
"""
target_media_type = MediaType.from_agent(media_type)
if not target_media_type:
try:
target_media_type = MediaType(media_type)
except ValueError as error:
raise HTTPException(status_code=400, detail="不支持的媒体类型") from error
if target_media_type not in (MediaType.MOVIE, MediaType.TV, MediaType.MUSIC):
raise HTTPException(status_code=400, detail="不支持的媒体类型")
supported_ids = set()
supported_domains = set()
for indexer in await SitesHelper().async_get_indexers() or []:
if not _indexer_supports_media_type(indexer, target_media_type):
continue
if indexer.get("id") is not None:
supported_ids.add(str(indexer.get("id")))
domain = StringUtils.get_url_domain(indexer.get("domain"))
if domain:
supported_domains.add(domain)
sites = await Site.async_list_order_by_pri(db)
return [
site
for site in sites
if site.is_active
and (str(site.id) in supported_ids or site.domain in supported_domains)
]
@router.post("/", summary="新增站点", response_model=schemas.Response)
async def add_site(
*,
+6
View File
@@ -147,6 +147,12 @@ FastAPI 的 HTTP 异常在 v1、v2 均统一使用 `message`,不再返回顶
| POST | `/api/v1/transfer/manual/history` | 查询文件、批量文件或目录命中的成功整理历史摘要,用于进入手动整理界面时显示重新整理状态 |
| POST | `/api/v1/transfer/manual` | 手动整理;请求体可用 `media_source` + `media_id` 指定本次识别与刮削数据源,同时兼容 `tmdbid``doubanid``bangumiid``anilistid`;命中失败历史时自动清理旧目标和记录后重试,`reorganize=true` 时清理命中的成功历史和非移动模式旧目标后重新整理 |
#### 站点
| 方法 | 路径 | 说明 |
| :--- | :--- | :--- |
| GET | `/api/v1/site/media/{media_type}` | 按媒体类型查询已配置且启用的可搜索站点;`media_type` 支持 `movie``tv``music` 或对应中文类型,音乐仅返回明确声明音乐能力的站点,影视不返回纯音乐站点 |
#### 搜索 / 种子 / 字幕
| 方法 | 路径 | 说明 |
+3 -2
View File
@@ -1,6 +1,6 @@
---
name: moviepilot-api
version: 9
version: 10
description: >-
Use this skill when you need to call MoviePilot REST API endpoints directly
with the bundled Python client. Covers MoviePilot HTTP endpoints across media
@@ -252,11 +252,12 @@ Streaming search sends `{"type":"heartbeat"}` every 15 seconds without business
| GET | `/api/v1/subscribe/shares` | List shared subscriptions. Params: `name`, `page`, `count`, `genre_id`, `min_rating`, `max_rating`, `sort_type` |
| GET | `/api/v1/subscribe/share/statistics` | Share statistics |
### Site (25 endpoints)
### Site (26 endpoints)
| Method | Path | Description |
|--------|------|-------------|
| GET | `/api/v1/site/` | List all sites |
| GET | `/api/v1/site/media/{media_type}` | List configured active sites compatible with `movie`, `tv`, or `music` searches |
| POST | `/api/v1/site/` | Add site. Body: Site JSON |
| PUT | `/api/v1/site/` | Update site. Body: Site JSON |
| GET | `/api/v1/site/{site_id}` | Site detail by ID |
+1
View File
@@ -102,6 +102,7 @@ def test_manage_page_endpoints_accept_manage_permission():
]
async_endpoints = [
site_endpoint.read_sites,
site_endpoint.read_sites_by_media_type,
site_endpoint.add_site,
site_endpoint.update_site,
site_endpoint.update_sites_priority,
+75
View File
@@ -0,0 +1,75 @@
import asyncio
from types import SimpleNamespace
from unittest.mock import AsyncMock
import pytest
from fastapi import HTTPException
from app.api.endpoints import site as site_endpoint
from app.schemas.types import MediaType
@pytest.mark.parametrize(
("indexer", "media_type", "expected"),
[
({"media_type": "music"}, MediaType.MUSIC, True),
({"media_type": "music"}, MediaType.MOVIE, False),
({"category": {"music": [{"id": "3"}]}}, MediaType.MUSIC, True),
({"category": {"music": [{"id": "3"}]}}, MediaType.TV, False),
({"category": {"movie": [{"id": "1"}]}}, MediaType.MOVIE, True),
({"category": {}}, MediaType.MOVIE, True),
({"category": {}}, MediaType.MUSIC, False),
],
)
def test_indexer_supports_requested_media_type(indexer, media_type, expected):
"""站点媒体声明和分类配置应生成正确的媒体类型兼容结果。"""
assert site_endpoint._indexer_supports_media_type(indexer, media_type) is expected
@pytest.mark.parametrize(
("media_type", "expected_ids"),
[
("music", [2, 3]),
("movie", [1, 3, 4]),
("tv", [4]),
],
)
def test_read_sites_by_media_type_filters_configured_active_sites(monkeypatch, media_type, expected_ids):
"""按媒体类型查询时应保留兼容启用站点,并维持数据库优先级顺序。"""
sites = [
SimpleNamespace(id=1, domain="movie.example", is_active=True),
SimpleNamespace(id=2, domain="music.example", is_active=True),
SimpleNamespace(id=3, domain="mixed.example", is_active=True),
SimpleNamespace(id=4, domain="generic.example", is_active=True),
SimpleNamespace(id=5, domain="inactive.example", is_active=False),
]
indexers = [
{"id": 1, "category": {"movie": [{"id": "1"}]}},
{"id": 2, "media_type": "music"},
{"id": 3, "category": {"movie": [{"id": "1"}], "music": [{"id": "3"}]}},
{"id": 4, "category": {}},
{"id": 5, "media_type": "music"},
]
list_sites = AsyncMock(return_value=sites)
get_indexers = AsyncMock(return_value=indexers)
monkeypatch.setattr(site_endpoint.Site, "async_list_order_by_pri", list_sites)
monkeypatch.setattr(
site_endpoint,
"SitesHelper",
lambda: SimpleNamespace(async_get_indexers=get_indexers),
)
result = asyncio.run(site_endpoint.read_sites_by_media_type(media_type, db=AsyncMock()))
assert [site.id for site in result] == expected_ids
list_sites.assert_awaited_once()
get_indexers.assert_awaited_once()
def test_read_sites_by_media_type_rejects_unknown_type():
"""未知媒体类型应返回明确的客户端参数错误。"""
with pytest.raises(HTTPException) as error:
asyncio.run(site_endpoint.read_sites_by_media_type("podcast", db=AsyncMock()))
assert error.value.status_code == 400
assert error.value.detail == "不支持的媒体类型"