mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-08-21 00:07:32 +08:00
refactor(media): unify media identity and chain responsibilities
This commit is contained in:
@@ -8,7 +8,13 @@ from app.helper.scraper import MediaScraperHelper
|
||||
from app.log import logger
|
||||
from app.modules import _ModuleBase
|
||||
from app.modules.anilist.anilist import AniListApi
|
||||
from app.schemas.types import MediaRecognizeType, MediaSource, MediaType, ModuleType
|
||||
from app.schemas.types import (
|
||||
MediaRecognizeType,
|
||||
MediaSource,
|
||||
MediaSourceSelection,
|
||||
MediaType,
|
||||
ModuleType,
|
||||
)
|
||||
from app.utils.media import is_media_source_enabled
|
||||
|
||||
|
||||
@@ -327,7 +333,7 @@ class AniListModule(_ModuleBase):
|
||||
return None
|
||||
|
||||
def search_medias(
|
||||
self, meta: MetaBase, media_source: Optional[MediaSource] = None
|
||||
self, meta: MetaBase, media_source: Optional[MediaSourceSelection] = None
|
||||
) -> Optional[List[MediaInfo]]:
|
||||
"""
|
||||
搜索 AniList 动画媒体信息。
|
||||
@@ -336,7 +342,7 @@ class AniListModule(_ModuleBase):
|
||||
:param media_source: 请求级搜索数据源
|
||||
:return: 统一媒体信息列表
|
||||
"""
|
||||
if not is_media_source_enabled(media_source, "anilist"):
|
||||
if not is_media_source_enabled(media_source, MediaSource.AniList):
|
||||
return None
|
||||
if not meta or not meta.name:
|
||||
return []
|
||||
@@ -347,7 +353,7 @@ class AniListModule(_ModuleBase):
|
||||
]
|
||||
|
||||
async def async_search_medias(
|
||||
self, meta: MetaBase, media_source: Optional[MediaSource] = None
|
||||
self, meta: MetaBase, media_source: Optional[MediaSourceSelection] = None
|
||||
) -> Optional[List[MediaInfo]]:
|
||||
"""
|
||||
异步搜索 AniList 动画媒体信息。
|
||||
@@ -356,7 +362,7 @@ class AniListModule(_ModuleBase):
|
||||
:param media_source: 请求级搜索数据源
|
||||
:return: 统一媒体信息列表
|
||||
"""
|
||||
if not is_media_source_enabled(media_source, "anilist"):
|
||||
if not is_media_source_enabled(media_source, MediaSource.AniList):
|
||||
return None
|
||||
if not meta or not meta.name:
|
||||
return []
|
||||
|
||||
@@ -8,7 +8,13 @@ from app.helper.scraper import MediaScraperHelper
|
||||
from app.log import logger
|
||||
from app.modules import _ModuleBase
|
||||
from app.modules.bangumi.bangumi import BangumiApi
|
||||
from app.schemas.types import MediaRecognizeType, MediaSource, MediaType, ModuleType
|
||||
from app.schemas.types import (
|
||||
MediaRecognizeType,
|
||||
MediaSource,
|
||||
MediaSourceSelection,
|
||||
MediaType,
|
||||
ModuleType,
|
||||
)
|
||||
from app.utils.http import RequestUtils
|
||||
from app.utils.media import is_media_source_enabled
|
||||
|
||||
@@ -221,7 +227,7 @@ class BangumiModule(_ModuleBase):
|
||||
return None
|
||||
|
||||
def search_medias(
|
||||
self, meta: MetaBase, media_source: Optional[MediaSource] = None
|
||||
self, meta: MetaBase, media_source: Optional[MediaSourceSelection] = None
|
||||
) -> Optional[List[MediaInfo]]:
|
||||
"""
|
||||
搜索媒体信息
|
||||
@@ -229,7 +235,7 @@ class BangumiModule(_ModuleBase):
|
||||
:param media_source: 请求级搜索数据源
|
||||
:return: 媒体信息
|
||||
"""
|
||||
if not is_media_source_enabled(media_source, "bangumi"):
|
||||
if not is_media_source_enabled(media_source, MediaSource.Bangumi):
|
||||
return None
|
||||
if not meta.name:
|
||||
return []
|
||||
@@ -241,7 +247,7 @@ class BangumiModule(_ModuleBase):
|
||||
return []
|
||||
|
||||
async def async_search_medias(
|
||||
self, meta: MetaBase, media_source: Optional[MediaSource] = None
|
||||
self, meta: MetaBase, media_source: Optional[MediaSourceSelection] = None
|
||||
) -> Optional[List[MediaInfo]]:
|
||||
"""
|
||||
搜索媒体信息(异步版本)
|
||||
@@ -249,7 +255,7 @@ class BangumiModule(_ModuleBase):
|
||||
:param media_source: 请求级搜索数据源
|
||||
:return: 媒体信息
|
||||
"""
|
||||
if not is_media_source_enabled(media_source, "bangumi"):
|
||||
if not is_media_source_enabled(media_source, MediaSource.Bangumi):
|
||||
return None
|
||||
if not meta.name:
|
||||
return []
|
||||
|
||||
@@ -21,6 +21,7 @@ from app.schemas.types import (
|
||||
MUSIC_ENTITY_ALBUM,
|
||||
MUSIC_ENTITY_RECORDING,
|
||||
MediaSource,
|
||||
MediaSourceSelection,
|
||||
MediaType,
|
||||
ModuleType,
|
||||
MediaRecognizeType,
|
||||
@@ -63,7 +64,7 @@ class DoubanModule(_ModuleBase):
|
||||
return "豆瓣"
|
||||
|
||||
@staticmethod
|
||||
def get_music_source() -> str:
|
||||
def get_music_source() -> MediaSource:
|
||||
"""返回音乐识别使用的数据源标识。"""
|
||||
return DoubanModule._music_source
|
||||
|
||||
@@ -92,7 +93,7 @@ class DoubanModule(_ModuleBase):
|
||||
self,
|
||||
meta: MetaMusic,
|
||||
limit: int = 20,
|
||||
media_source: Optional[str] = None,
|
||||
media_source: Optional[MediaSourceSelection] = None,
|
||||
) -> Optional[List[MusicInfo]]:
|
||||
"""按请求来源搜索豆瓣音乐专辑,并转换为统一音乐候选。"""
|
||||
if not is_media_source_selected(media_source, self._music_source):
|
||||
@@ -105,7 +106,7 @@ class DoubanModule(_ModuleBase):
|
||||
|
||||
def recognize_music(
|
||||
self,
|
||||
media_source: str,
|
||||
media_source: MediaSource,
|
||||
media_id: str,
|
||||
music_type: Optional[str] = None,
|
||||
) -> Optional[MusicInfo]:
|
||||
@@ -130,7 +131,11 @@ class DoubanModule(_ModuleBase):
|
||||
)
|
||||
return album.to_music_info()
|
||||
|
||||
def music_album(self, media_source: str, media_id: str) -> Optional[MusicAlbumInfo]:
|
||||
def music_album(
|
||||
self,
|
||||
media_source: MediaSource,
|
||||
media_id: str,
|
||||
) -> Optional[MusicAlbumInfo]:
|
||||
"""按豆瓣音乐专辑 ID 获取标准化专辑详情和曲目。"""
|
||||
if media_source != self._music_source or not media_id:
|
||||
return None
|
||||
@@ -139,7 +144,7 @@ class DoubanModule(_ModuleBase):
|
||||
|
||||
def music_discover(
|
||||
self,
|
||||
media_source: str,
|
||||
media_source: MediaSource,
|
||||
page: int = 1,
|
||||
count: int = 30,
|
||||
entity: str = MUSIC_ENTITY_ALBUM,
|
||||
@@ -187,7 +192,7 @@ class DoubanModule(_ModuleBase):
|
||||
|
||||
def music_album_related(
|
||||
self,
|
||||
media_source: str,
|
||||
media_source: MediaSource,
|
||||
media_id: str,
|
||||
count: int = 24,
|
||||
) -> Optional[List[MusicInfo]]:
|
||||
@@ -204,7 +209,7 @@ class DoubanModule(_ModuleBase):
|
||||
def _recognize_music_media(
|
||||
self,
|
||||
meta: Optional[MetaMusic],
|
||||
media_source: Optional[str],
|
||||
media_source: Optional[MediaSource],
|
||||
media_id: Optional[str],
|
||||
music_type: Optional[str] = None,
|
||||
) -> Optional[MusicInfo]:
|
||||
@@ -248,7 +253,7 @@ class DoubanModule(_ModuleBase):
|
||||
async def _async_recognize_music_media(
|
||||
self,
|
||||
meta: Optional[MetaMusic],
|
||||
media_source: Optional[str],
|
||||
media_source: Optional[MediaSource],
|
||||
media_id: Optional[str],
|
||||
music_type: Optional[str] = None,
|
||||
) -> Optional[MusicInfo]:
|
||||
@@ -385,7 +390,7 @@ class DoubanModule(_ModuleBase):
|
||||
title = cls._douban_music_text(target.get("title") or target.get("name"))
|
||||
if not media_id or not title:
|
||||
continue
|
||||
artists = cls._douban_music_artists(target)
|
||||
artists = cls._douban_music_search_artists(target)
|
||||
release_date = cls._douban_music_date(target)
|
||||
cover_url = cls._douban_music_cover(target)
|
||||
candidate = MusicInfo(
|
||||
@@ -551,6 +556,20 @@ class DoubanModule(_ModuleBase):
|
||||
artists.append(text)
|
||||
return artists
|
||||
|
||||
@classmethod
|
||||
def _douban_music_search_artists(cls, info: dict[str, Any]) -> List[str]:
|
||||
"""提取搜索候选艺术家,缺少结构化字段时回退到卡片副标题首段。"""
|
||||
artists = cls._douban_music_artists(info)
|
||||
if artists:
|
||||
return artists
|
||||
subtitle = cls._douban_music_text(info.get("card_subtitle"))
|
||||
if not subtitle:
|
||||
return []
|
||||
artist = re.split(r"\s+/\s+", subtitle, maxsplit=1)[0].strip()
|
||||
if not artist or re.fullmatch(r"\d{4}(?:-\d{1,2}(?:-\d{1,2})?)?", artist):
|
||||
return []
|
||||
return [artist]
|
||||
|
||||
@classmethod
|
||||
def _douban_music_cover(cls, info: dict[str, Any]) -> Optional[str]:
|
||||
"""从豆瓣多种图片字段中提取清晰封面。"""
|
||||
@@ -1425,7 +1444,7 @@ class DoubanModule(_ModuleBase):
|
||||
return []
|
||||
|
||||
def search_medias(
|
||||
self, meta: MetaBase, media_source: Optional[MediaSource] = None
|
||||
self, meta: MetaBase, media_source: Optional[MediaSourceSelection] = None
|
||||
) -> Optional[List[MediaInfo]]:
|
||||
"""
|
||||
搜索媒体信息
|
||||
@@ -1433,7 +1452,7 @@ class DoubanModule(_ModuleBase):
|
||||
:param media_source: 请求级搜索数据源
|
||||
:return: 媒体信息
|
||||
"""
|
||||
if not is_media_source_enabled(media_source, "douban"):
|
||||
if not is_media_source_enabled(media_source, MediaSource.Douban):
|
||||
return None
|
||||
if not meta.name:
|
||||
return []
|
||||
@@ -1444,7 +1463,7 @@ class DoubanModule(_ModuleBase):
|
||||
return self._build_search_medias_result(meta, result.get("items"))
|
||||
|
||||
async def async_search_medias(
|
||||
self, meta: MetaBase, media_source: Optional[MediaSource] = None
|
||||
self, meta: MetaBase, media_source: Optional[MediaSourceSelection] = None
|
||||
) -> Optional[List[MediaInfo]]:
|
||||
"""
|
||||
搜索媒体信息(异步版本)
|
||||
@@ -1452,7 +1471,7 @@ class DoubanModule(_ModuleBase):
|
||||
:param media_source: 请求级搜索数据源
|
||||
:return: 媒体信息
|
||||
"""
|
||||
if not is_media_source_enabled(media_source, "douban"):
|
||||
if not is_media_source_enabled(media_source, MediaSource.Douban):
|
||||
return None
|
||||
if not meta.name:
|
||||
return []
|
||||
@@ -1463,7 +1482,7 @@ class DoubanModule(_ModuleBase):
|
||||
return self._build_search_medias_result(meta, result.get("items"))
|
||||
|
||||
def search_persons(
|
||||
self, name: str, media_source: Optional[MediaSource] = None
|
||||
self, name: str, media_source: Optional[MediaSourceSelection] = None
|
||||
) -> Optional[List[MediaPerson]]:
|
||||
"""
|
||||
搜索人物信息
|
||||
@@ -1471,7 +1490,7 @@ class DoubanModule(_ModuleBase):
|
||||
:param media_source: 请求级搜索数据源
|
||||
:return: 人物信息列表
|
||||
"""
|
||||
if not is_media_source_enabled(media_source, "douban"):
|
||||
if not is_media_source_enabled(media_source, MediaSource.Douban):
|
||||
return None
|
||||
if not name:
|
||||
return []
|
||||
@@ -1488,7 +1507,7 @@ class DoubanModule(_ModuleBase):
|
||||
return []
|
||||
|
||||
async def async_search_persons(
|
||||
self, name: str, media_source: Optional[MediaSource] = None
|
||||
self, name: str, media_source: Optional[MediaSourceSelection] = None
|
||||
) -> Optional[List[MediaPerson]]:
|
||||
"""
|
||||
搜索人物信息(异步版本)
|
||||
@@ -1496,7 +1515,7 @@ class DoubanModule(_ModuleBase):
|
||||
:param media_source: 请求级搜索数据源
|
||||
:return: 人物信息列表
|
||||
"""
|
||||
if not is_media_source_enabled(media_source, "douban"):
|
||||
if not is_media_source_enabled(media_source, MediaSource.Douban):
|
||||
return None
|
||||
if not name:
|
||||
return []
|
||||
|
||||
@@ -1150,7 +1150,9 @@ class Emby:
|
||||
eventItem.item_id = message.get('Item', {}).get('Id')
|
||||
|
||||
eventItem.item_path = message.get('Item', {}).get('Path')
|
||||
eventItem.tmdb_id = message.get('Item', {}).get('ProviderIds', {}).get('Tmdb')
|
||||
eventItem.media_source, eventItem.media_id = MediaServerIdentityHelper.from_provider_ids(
|
||||
message.get('Item', {}).get('ProviderIds')
|
||||
)
|
||||
if message.get('Item', {}).get('Overview') and len(message.get('Item', {}).get('Overview')) > 100:
|
||||
eventItem.overview = str(message.get('Item', {}).get('Overview'))[:100] + "..."
|
||||
else:
|
||||
@@ -1171,7 +1173,9 @@ class Emby:
|
||||
eventItem.item_type = message.get("item_type")
|
||||
eventItem.item_name = message.get("item_name")
|
||||
eventItem.item_path = message.get("item_path")
|
||||
eventItem.tmdb_id = message.get("tmdb_id")
|
||||
eventItem.media_source, eventItem.media_id = MediaServerIdentityHelper.from_provider_ids({
|
||||
"tmdb_id": message.get("tmdb_id"),
|
||||
})
|
||||
eventItem.season_id = message.get("season_id")
|
||||
eventItem.episode_id = message.get("episode_id")
|
||||
|
||||
|
||||
@@ -825,7 +825,11 @@ class Jellyfin:
|
||||
channel="jellyfin"
|
||||
)
|
||||
eventItem.item_id = message.get('ItemId')
|
||||
eventItem.tmdb_id = message.get('Provider_tmdb')
|
||||
eventItem.media_source, eventItem.media_id = MediaServerIdentityHelper.from_provider_ids({
|
||||
key.removeprefix("Provider_"): value
|
||||
for key, value in message.items()
|
||||
if key.startswith("Provider_")
|
||||
})
|
||||
eventItem.overview = message.get('Overview')
|
||||
eventItem.item_favorite = message.get('Favorite')
|
||||
eventItem.save_reason = message.get('SaveReason')
|
||||
|
||||
@@ -24,6 +24,7 @@ from app.schemas.types import (
|
||||
MUSIC_ENTITY_RECORDING,
|
||||
MediaRecognizeType,
|
||||
MediaSource,
|
||||
MediaSourceSelection,
|
||||
MediaType,
|
||||
ModuleType,
|
||||
)
|
||||
@@ -123,7 +124,7 @@ class MusicBrainzModule(_ModuleBase):
|
||||
return "MusicBrainz"
|
||||
|
||||
@staticmethod
|
||||
def get_music_source() -> str:
|
||||
def get_music_source() -> MediaSource:
|
||||
"""返回音乐识别使用的数据源标识。"""
|
||||
return MusicBrainzModule._source
|
||||
|
||||
@@ -146,7 +147,7 @@ class MusicBrainzModule(_ModuleBase):
|
||||
self,
|
||||
meta: MetaMusic,
|
||||
limit: int = 20,
|
||||
media_source: Optional[str] = None,
|
||||
media_source: Optional[MediaSourceSelection] = None,
|
||||
) -> Optional[list[MusicInfo]]:
|
||||
"""搜索单曲、专辑和艺术家,并交错返回可浏览的 MusicBrainz 候选。"""
|
||||
if not is_media_source_selected(media_source, self._source):
|
||||
@@ -747,7 +748,7 @@ class MusicBrainzModule(_ModuleBase):
|
||||
self,
|
||||
meta: MetaBase = None,
|
||||
mtype: MediaType = None,
|
||||
media_source: Optional[str] = None,
|
||||
media_source: Optional[MediaSource] = None,
|
||||
media_id: Optional[str] = None,
|
||||
**kwargs,
|
||||
) -> Optional[MusicInfo]:
|
||||
@@ -846,7 +847,7 @@ class MusicBrainzModule(_ModuleBase):
|
||||
self,
|
||||
meta: MetaBase = None,
|
||||
mtype: MediaType = None,
|
||||
media_source: Optional[str] = None,
|
||||
media_source: Optional[MediaSource] = None,
|
||||
media_id: Optional[str] = None,
|
||||
**kwargs,
|
||||
) -> Optional[MusicInfo]:
|
||||
@@ -902,7 +903,12 @@ class MusicBrainzModule(_ModuleBase):
|
||||
return result
|
||||
|
||||
@classmethod
|
||||
def _select_candidate(cls, meta: MetaMusic, candidates: Iterable[MusicInfo], media_source: str) -> Optional[MusicInfo]:
|
||||
def _select_candidate(
|
||||
cls,
|
||||
meta: MetaMusic,
|
||||
candidates: Iterable[MusicInfo],
|
||||
media_source: MediaSource,
|
||||
) -> Optional[MusicInfo]:
|
||||
"""按标题、艺术家和专辑匹配度选择最可信的搜索候选。"""
|
||||
normalized_source = cls._normalize_text(media_source).casefold()
|
||||
# 资源标题携带的音质标记先剥离,再与候选曲名比对;
|
||||
@@ -1144,7 +1150,7 @@ class MusicBrainzModule(_ModuleBase):
|
||||
|
||||
def recognize_music(
|
||||
self,
|
||||
media_source: str,
|
||||
media_source: MediaSource,
|
||||
media_id: str,
|
||||
music_type: Optional[str] = None,
|
||||
) -> Optional[MusicInfo]:
|
||||
@@ -1169,7 +1175,7 @@ class MusicBrainzModule(_ModuleBase):
|
||||
|
||||
async def async_recognize_music(
|
||||
self,
|
||||
media_source: str,
|
||||
media_source: MediaSource,
|
||||
media_id: str,
|
||||
music_type: Optional[str] = None,
|
||||
) -> Optional[MusicInfo]:
|
||||
@@ -1193,7 +1199,7 @@ class MusicBrainzModule(_ModuleBase):
|
||||
|
||||
async def _async_music_album(
|
||||
self,
|
||||
media_source: str,
|
||||
media_source: MediaSource,
|
||||
media_id: str,
|
||||
) -> Optional[MusicAlbumInfo]:
|
||||
"""异步按 MusicBrainz Release Group ID 获取专辑详情及曲目。"""
|
||||
@@ -1218,7 +1224,11 @@ class MusicBrainzModule(_ModuleBase):
|
||||
)
|
||||
return album
|
||||
|
||||
def music_album(self, media_source: str, media_id: str) -> Optional[MusicAlbumInfo]:
|
||||
def music_album(
|
||||
self,
|
||||
media_source: MediaSource,
|
||||
media_id: str,
|
||||
) -> Optional[MusicAlbumInfo]:
|
||||
"""按 MusicBrainz Release Group ID 获取标准化专辑详情及曲目。"""
|
||||
if media_source != self._source or not media_id:
|
||||
return None
|
||||
@@ -1238,7 +1248,11 @@ class MusicBrainzModule(_ModuleBase):
|
||||
album.tracks = self._album_tracks(album, payload.get("releases") or [])
|
||||
return album
|
||||
|
||||
def music_artist(self, media_source: str, media_id: str) -> Optional[MusicArtistInfo]:
|
||||
def music_artist(
|
||||
self,
|
||||
media_source: MediaSource,
|
||||
media_id: str,
|
||||
) -> Optional[MusicArtistInfo]:
|
||||
"""按 MusicBrainz Artist ID 获取标准化艺术家详情。"""
|
||||
if media_source != self._source or not media_id:
|
||||
return None
|
||||
@@ -1250,7 +1264,7 @@ class MusicBrainzModule(_ModuleBase):
|
||||
|
||||
def music_artist_albums(
|
||||
self,
|
||||
media_source: str,
|
||||
media_source: MediaSource,
|
||||
media_id: str,
|
||||
page: int = 1,
|
||||
count: int = 30,
|
||||
@@ -1281,7 +1295,7 @@ class MusicBrainzModule(_ModuleBase):
|
||||
|
||||
def music_artist_related(
|
||||
self,
|
||||
media_source: str,
|
||||
media_source: MediaSource,
|
||||
media_id: str,
|
||||
count: int = 24,
|
||||
) -> list[MusicArtistInfo]:
|
||||
|
||||
@@ -15,6 +15,7 @@ from app.schemas.types import (
|
||||
MUSIC_ENTITY_RECORDING,
|
||||
MediaRecognizeType,
|
||||
MediaSource,
|
||||
MediaSourceSelection,
|
||||
MediaType,
|
||||
ModuleType,
|
||||
)
|
||||
@@ -50,7 +51,7 @@ class TheAudioDbModule(_ModuleBase):
|
||||
return "TheAudioDB"
|
||||
|
||||
@staticmethod
|
||||
def get_music_source() -> str:
|
||||
def get_music_source() -> MediaSource:
|
||||
"""返回音乐识别使用的数据源标识。"""
|
||||
return TheAudioDbModule._source
|
||||
|
||||
@@ -73,7 +74,7 @@ class TheAudioDbModule(_ModuleBase):
|
||||
self,
|
||||
meta: MetaMusic,
|
||||
limit: int = 20,
|
||||
media_source: Optional[str] = None,
|
||||
media_source: Optional[MediaSourceSelection] = None,
|
||||
) -> Optional[list[MusicInfo]]:
|
||||
"""按请求来源搜索 TheAudioDB 单曲、专辑和艺术家。"""
|
||||
if not is_media_source_selected(media_source, self._source):
|
||||
@@ -93,7 +94,7 @@ class TheAudioDbModule(_ModuleBase):
|
||||
self,
|
||||
meta: MetaBase = None,
|
||||
mtype: MediaType = None,
|
||||
media_source: Optional[str] = None,
|
||||
media_source: Optional[MediaSource] = None,
|
||||
media_id: Optional[str] = None,
|
||||
**kwargs,
|
||||
) -> Optional[MusicInfo]:
|
||||
@@ -131,7 +132,7 @@ class TheAudioDbModule(_ModuleBase):
|
||||
self,
|
||||
meta: MetaBase = None,
|
||||
mtype: MediaType = None,
|
||||
media_source: Optional[str] = None,
|
||||
media_source: Optional[MediaSource] = None,
|
||||
media_id: Optional[str] = None,
|
||||
**kwargs,
|
||||
) -> Optional[MusicInfo]:
|
||||
@@ -171,7 +172,7 @@ class TheAudioDbModule(_ModuleBase):
|
||||
|
||||
def recognize_music(
|
||||
self,
|
||||
media_source: str,
|
||||
media_source: MediaSource,
|
||||
media_id: str,
|
||||
music_type: Optional[str] = None,
|
||||
) -> Optional[MusicInfo]:
|
||||
@@ -190,7 +191,7 @@ class TheAudioDbModule(_ModuleBase):
|
||||
|
||||
async def async_recognize_music(
|
||||
self,
|
||||
media_source: str,
|
||||
media_source: MediaSource,
|
||||
media_id: str,
|
||||
music_type: Optional[str] = None,
|
||||
) -> Optional[MusicInfo]:
|
||||
@@ -209,7 +210,7 @@ class TheAudioDbModule(_ModuleBase):
|
||||
|
||||
async def _async_music_album(
|
||||
self,
|
||||
media_source: str,
|
||||
media_source: MediaSource,
|
||||
media_id: str,
|
||||
) -> Optional[MusicAlbumInfo]:
|
||||
"""异步按 TheAudioDB 专辑 ID 获取标准化专辑详情和曲目。"""
|
||||
@@ -228,7 +229,11 @@ class TheAudioDbModule(_ModuleBase):
|
||||
]
|
||||
return album
|
||||
|
||||
def music_album(self, media_source: str, media_id: str) -> Optional[MusicAlbumInfo]:
|
||||
def music_album(
|
||||
self,
|
||||
media_source: MediaSource,
|
||||
media_id: str,
|
||||
) -> Optional[MusicAlbumInfo]:
|
||||
"""按 TheAudioDB 专辑 ID 获取标准化专辑详情和曲目。"""
|
||||
if media_source != self._source or not media_id:
|
||||
return None
|
||||
@@ -245,7 +250,11 @@ class TheAudioDbModule(_ModuleBase):
|
||||
]
|
||||
return album
|
||||
|
||||
def music_artist(self, media_source: str, media_id: str) -> Optional[MusicArtistInfo]:
|
||||
def music_artist(
|
||||
self,
|
||||
media_source: MediaSource,
|
||||
media_id: str,
|
||||
) -> Optional[MusicArtistInfo]:
|
||||
"""按 TheAudioDB 艺术家 ID 获取标准化艺术家详情。"""
|
||||
if media_source != self._source or not media_id:
|
||||
return None
|
||||
@@ -255,7 +264,7 @@ class TheAudioDbModule(_ModuleBase):
|
||||
|
||||
def music_artist_albums(
|
||||
self,
|
||||
media_source: str,
|
||||
media_source: MediaSource,
|
||||
media_id: str,
|
||||
page: int = 1,
|
||||
count: int = 30,
|
||||
@@ -277,7 +286,7 @@ class TheAudioDbModule(_ModuleBase):
|
||||
|
||||
def music_album_related(
|
||||
self,
|
||||
media_source: str,
|
||||
media_source: MediaSource,
|
||||
media_id: str,
|
||||
count: int = 24,
|
||||
) -> Optional[list[MusicInfo]]:
|
||||
@@ -304,11 +313,10 @@ class TheAudioDbModule(_ModuleBase):
|
||||
def _search_tracks(self, meta: MetaMusic) -> list[MusicInfo]:
|
||||
"""使用曲名和艺术家搜索 TheAudioDB 单曲。"""
|
||||
title = meta.title
|
||||
if not title:
|
||||
artist = meta.artists[0] if meta.artists else meta.album_artist
|
||||
if not title or not artist:
|
||||
return []
|
||||
params = {"t": title}
|
||||
if meta.artists:
|
||||
params["s"] = meta.artists[0]
|
||||
params = {"t": title, "s": artist}
|
||||
payload = self._request_json("searchtrack.php", params)
|
||||
return [
|
||||
info
|
||||
@@ -319,11 +327,10 @@ class TheAudioDbModule(_ModuleBase):
|
||||
async def _async_search_tracks(self, meta: MetaMusic) -> list[MusicInfo]:
|
||||
"""异步使用曲名和艺术家搜索 TheAudioDB 单曲。"""
|
||||
title = meta.title
|
||||
if not title:
|
||||
artist = meta.artists[0] if meta.artists else meta.album_artist
|
||||
if not title or not artist:
|
||||
return []
|
||||
params = {"t": title}
|
||||
if meta.artists:
|
||||
params["s"] = meta.artists[0]
|
||||
params = {"t": title, "s": artist}
|
||||
payload = await self._async_request_json("searchtrack.php", params)
|
||||
return [
|
||||
info
|
||||
@@ -334,11 +341,10 @@ class TheAudioDbModule(_ModuleBase):
|
||||
def _search_albums(self, meta: MetaMusic) -> list[MusicAlbumInfo]:
|
||||
"""使用专辑名和艺术家搜索 TheAudioDB 专辑。"""
|
||||
album_name = meta.album or meta.title
|
||||
if not album_name:
|
||||
artist = meta.artists[0] if meta.artists else meta.album_artist
|
||||
if not album_name or not artist:
|
||||
return []
|
||||
params = {"a": album_name}
|
||||
if meta.artists:
|
||||
params["s"] = meta.artists[0]
|
||||
params = {"a": album_name, "s": artist}
|
||||
payload = self._request_json("searchalbum.php", params)
|
||||
return [self._album_to_info(item) for item in self._entities(payload, "album", "albums")]
|
||||
|
||||
@@ -348,11 +354,10 @@ class TheAudioDbModule(_ModuleBase):
|
||||
) -> list[MusicAlbumInfo]:
|
||||
"""异步使用专辑名和艺术家搜索 TheAudioDB 专辑。"""
|
||||
album_name = meta.album or meta.title
|
||||
if not album_name:
|
||||
artist = meta.artists[0] if meta.artists else meta.album_artist
|
||||
if not album_name or not artist:
|
||||
return []
|
||||
params = {"a": album_name}
|
||||
if meta.artists:
|
||||
params["s"] = meta.artists[0]
|
||||
params = {"a": album_name, "s": artist}
|
||||
payload = await self._async_request_json("searchalbum.php", params)
|
||||
return [
|
||||
self._album_to_info(item)
|
||||
@@ -564,14 +569,25 @@ class TheAudioDbModule(_ModuleBase):
|
||||
url=f"{cls._base_url}/{api_key}/{endpoint}",
|
||||
params=params or {},
|
||||
)
|
||||
if not response or response.status_code != 200:
|
||||
if response is None:
|
||||
return None
|
||||
try:
|
||||
payload = response.json()
|
||||
except ValueError as err:
|
||||
logger.error(f"TheAudioDB 响应解析失败:{str(err)}")
|
||||
return None
|
||||
return payload if isinstance(payload, dict) else None
|
||||
if response.status_code != 200:
|
||||
return None
|
||||
diagnostic = cls._response_diagnostic(response, endpoint)
|
||||
if getattr(response, "content", None) in (b"", ""):
|
||||
logger.warning(f"TheAudioDB 返回空响应:{diagnostic}")
|
||||
return None
|
||||
try:
|
||||
payload = response.json()
|
||||
except (TypeError, ValueError) as err:
|
||||
logger.warning(
|
||||
f"TheAudioDB 响应解析失败:{diagnostic},错误:{str(err)}"
|
||||
)
|
||||
return None
|
||||
return payload if isinstance(payload, dict) else None
|
||||
finally:
|
||||
response.close()
|
||||
|
||||
@classmethod
|
||||
@cached(
|
||||
@@ -603,13 +619,31 @@ class TheAudioDbModule(_ModuleBase):
|
||||
try:
|
||||
if response.status_code != 200:
|
||||
return None
|
||||
payload = response.json()
|
||||
except ValueError as err:
|
||||
logger.error(f"TheAudioDB 响应解析失败:{str(err)}")
|
||||
return None
|
||||
diagnostic = cls._response_diagnostic(response, endpoint)
|
||||
if getattr(response, "content", None) in (b"", ""):
|
||||
logger.warning(f"TheAudioDB 返回空响应:{diagnostic}")
|
||||
return None
|
||||
try:
|
||||
payload = response.json()
|
||||
except (TypeError, ValueError) as err:
|
||||
logger.warning(
|
||||
f"TheAudioDB 响应解析失败:{diagnostic},错误:{str(err)}"
|
||||
)
|
||||
return None
|
||||
return payload if isinstance(payload, dict) else None
|
||||
finally:
|
||||
await response.aclose()
|
||||
return payload if isinstance(payload, dict) else None
|
||||
|
||||
@staticmethod
|
||||
def _response_diagnostic(response: Any, endpoint: str) -> str:
|
||||
"""生成不包含 API Key 的 TheAudioDB 响应诊断摘要。"""
|
||||
headers = getattr(response, "headers", {}) or {}
|
||||
content_type = headers.get("Content-Type", "") if hasattr(headers, "get") else ""
|
||||
body = str(getattr(response, "text", "") or "").replace("\n", " ")[:200]
|
||||
return (
|
||||
f"endpoint={endpoint}, HTTP={getattr(response, 'status_code', '')}, "
|
||||
f"Content-Type={content_type}, body={body!r}"
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _entities(
|
||||
|
||||
@@ -18,11 +18,16 @@ from app.schemas.types import (
|
||||
MediaImageType,
|
||||
MediaRecognizeType,
|
||||
MediaSource,
|
||||
MediaSourceSelection,
|
||||
MediaType,
|
||||
ModuleType,
|
||||
)
|
||||
from app.utils.http import RequestUtils
|
||||
from app.utils.media import is_media_source_enabled, is_media_source_selected
|
||||
from app.utils.media import (
|
||||
is_media_source_enabled,
|
||||
is_media_source_selected,
|
||||
normalize_media_source,
|
||||
)
|
||||
from app.utils.zhconv import convert as zhconv_convert
|
||||
|
||||
|
||||
@@ -102,7 +107,7 @@ class TheMovieDbModule(_ModuleBase):
|
||||
def _validate_recognize_params(
|
||||
meta: MetaBase,
|
||||
tmdbid: Optional[int],
|
||||
media_source: Optional[str] = None,
|
||||
media_source: Optional[MediaSource] = None,
|
||||
) -> bool:
|
||||
"""
|
||||
验证识别参数
|
||||
@@ -115,7 +120,8 @@ class TheMovieDbModule(_ModuleBase):
|
||||
if not tmdbid and not meta:
|
||||
return False
|
||||
|
||||
if meta and not tmdbid and (media_source or settings.RECOGNIZE_SOURCE) != "themoviedb":
|
||||
selected_source = normalize_media_source(media_source or settings.RECOGNIZE_SOURCE)
|
||||
if meta and not tmdbid and selected_source != MediaSource.TMDB:
|
||||
return False
|
||||
|
||||
if meta and not meta.name and not tmdbid:
|
||||
@@ -769,7 +775,7 @@ class TheMovieDbModule(_ModuleBase):
|
||||
}
|
||||
|
||||
def search_medias(
|
||||
self, meta: MetaBase, media_source: Optional[MediaSource] = None
|
||||
self, meta: MetaBase, media_source: Optional[MediaSourceSelection] = None
|
||||
) -> Optional[List[MediaInfo]]:
|
||||
"""
|
||||
搜索媒体信息
|
||||
@@ -777,7 +783,7 @@ class TheMovieDbModule(_ModuleBase):
|
||||
:param media_source: 请求级搜索数据源
|
||||
:return: 媒体信息列表
|
||||
"""
|
||||
if not is_media_source_enabled(media_source, "themoviedb"):
|
||||
if not is_media_source_enabled(media_source, MediaSource.TMDB):
|
||||
return None
|
||||
if not meta.name:
|
||||
return []
|
||||
@@ -801,7 +807,7 @@ class TheMovieDbModule(_ModuleBase):
|
||||
return self._build_search_medias_result(meta, results)
|
||||
|
||||
def search_persons(
|
||||
self, name: str, media_source: Optional[MediaSource] = None
|
||||
self, name: str, media_source: Optional[MediaSourceSelection] = None
|
||||
) -> Optional[List[schemas.MediaPerson]]:
|
||||
"""
|
||||
搜索人物信息
|
||||
@@ -809,7 +815,7 @@ class TheMovieDbModule(_ModuleBase):
|
||||
:param media_source: 请求级搜索数据源
|
||||
:return: 人物信息列表
|
||||
"""
|
||||
if not is_media_source_enabled(media_source, "themoviedb"):
|
||||
if not is_media_source_enabled(media_source, MediaSource.TMDB):
|
||||
return None
|
||||
if not name:
|
||||
return []
|
||||
@@ -819,7 +825,7 @@ class TheMovieDbModule(_ModuleBase):
|
||||
return []
|
||||
|
||||
async def async_search_persons(
|
||||
self, name: str, media_source: Optional[MediaSource] = None
|
||||
self, name: str, media_source: Optional[MediaSourceSelection] = None
|
||||
) -> Optional[List[schemas.MediaPerson]]:
|
||||
"""
|
||||
异步搜索人物信息
|
||||
@@ -827,7 +833,7 @@ class TheMovieDbModule(_ModuleBase):
|
||||
:param media_source: 请求级搜索数据源
|
||||
:return: 人物信息列表
|
||||
"""
|
||||
if not is_media_source_enabled(media_source, "themoviedb"):
|
||||
if not is_media_source_enabled(media_source, MediaSource.TMDB):
|
||||
return None
|
||||
if not name:
|
||||
return []
|
||||
@@ -837,7 +843,7 @@ class TheMovieDbModule(_ModuleBase):
|
||||
return []
|
||||
|
||||
def search_collections(
|
||||
self, name: str, media_source: Optional[MediaSource] = None
|
||||
self, name: str, media_source: Optional[MediaSourceSelection] = None
|
||||
) -> Optional[List[MediaInfo]]:
|
||||
"""
|
||||
搜索集合信息
|
||||
@@ -845,7 +851,7 @@ class TheMovieDbModule(_ModuleBase):
|
||||
:param media_source: 请求级搜索数据源
|
||||
:return: 合集信息列表
|
||||
"""
|
||||
if media_source and not is_media_source_selected(media_source, "themoviedb"):
|
||||
if media_source and not is_media_source_selected(media_source, MediaSource.TMDB):
|
||||
return None
|
||||
if not name:
|
||||
return []
|
||||
@@ -855,7 +861,7 @@ class TheMovieDbModule(_ModuleBase):
|
||||
return []
|
||||
|
||||
async def async_search_collections(
|
||||
self, name: str, media_source: Optional[MediaSource] = None
|
||||
self, name: str, media_source: Optional[MediaSourceSelection] = None
|
||||
) -> Optional[List[MediaInfo]]:
|
||||
"""
|
||||
异步搜索集合信息
|
||||
@@ -863,7 +869,7 @@ class TheMovieDbModule(_ModuleBase):
|
||||
:param media_source: 请求级搜索数据源
|
||||
:return: 合集信息列表
|
||||
"""
|
||||
if media_source and not is_media_source_selected(media_source, "themoviedb"):
|
||||
if media_source and not is_media_source_selected(media_source, MediaSource.TMDB):
|
||||
return None
|
||||
if not name:
|
||||
return []
|
||||
@@ -1251,7 +1257,7 @@ class TheMovieDbModule(_ModuleBase):
|
||||
|
||||
# 异步方法
|
||||
async def async_search_medias(
|
||||
self, meta: MetaBase, media_source: Optional[MediaSource] = None
|
||||
self, meta: MetaBase, media_source: Optional[MediaSourceSelection] = None
|
||||
) -> Optional[List[MediaInfo]]:
|
||||
"""
|
||||
搜索媒体信息(异步版本)
|
||||
@@ -1259,7 +1265,7 @@ class TheMovieDbModule(_ModuleBase):
|
||||
:param media_source: 请求级搜索数据源
|
||||
:return: 媒体信息列表
|
||||
"""
|
||||
if not is_media_source_enabled(media_source, "themoviedb"):
|
||||
if not is_media_source_enabled(media_source, MediaSource.TMDB):
|
||||
return None
|
||||
if not meta.name:
|
||||
return []
|
||||
|
||||
@@ -1026,7 +1026,9 @@ class ZSpace:
|
||||
event_item.item_id = message.get('Item', {}).get('Id')
|
||||
|
||||
event_item.item_path = message.get('Item', {}).get('Path')
|
||||
event_item.tmdb_id = message.get('Item', {}).get('ProviderIds', {}).get('Tmdb')
|
||||
event_item.media_source, event_item.media_id = MediaServerIdentityHelper.from_provider_ids(
|
||||
message.get('Item', {}).get('ProviderIds')
|
||||
)
|
||||
if message.get('Item', {}).get('Overview') and len(message.get('Item', {}).get('Overview')) > 100:
|
||||
event_item.overview = str(message.get('Item', {}).get('Overview'))[:100] + "..."
|
||||
else:
|
||||
@@ -1047,7 +1049,9 @@ class ZSpace:
|
||||
event_item.item_type = message.get("item_type")
|
||||
event_item.item_name = message.get("item_name")
|
||||
event_item.item_path = message.get("item_path")
|
||||
event_item.tmdb_id = message.get("tmdb_id")
|
||||
event_item.media_source, event_item.media_id = MediaServerIdentityHelper.from_provider_ids({
|
||||
"tmdb_id": message.get("tmdb_id"),
|
||||
})
|
||||
event_item.season_id = message.get("season_id")
|
||||
event_item.episode_id = message.get("episode_id")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user