mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-08-22 00:32:50 +08:00
feat(music): add TheAudioDB and Douban discovery
This commit is contained in:
@@ -136,6 +136,40 @@ class DoubanModule(_ModuleBase):
|
||||
info = self.doubanapi.music_detail(subject_id=str(media_id))
|
||||
return self._douban_music_to_album(info) if info else None
|
||||
|
||||
def music_discover(
|
||||
self,
|
||||
source: str,
|
||||
page: int = 1,
|
||||
count: int = 30,
|
||||
entity: str = MUSIC_ENTITY_ALBUM,
|
||||
country: str = "us",
|
||||
) -> Optional[List[MusicInfo]]:
|
||||
"""分页读取豆瓣音乐推荐合集,并保留豆瓣条目原生身份。"""
|
||||
if source != self._music_source:
|
||||
return None
|
||||
del entity, country
|
||||
result = self.doubanapi.music_single(
|
||||
start=max(page - 1, 0) * max(1, count),
|
||||
count=max(1, count),
|
||||
)
|
||||
return self._build_music_search_results(result)
|
||||
|
||||
def music_album_related(
|
||||
self,
|
||||
source: str,
|
||||
media_id: str,
|
||||
count: int = 24,
|
||||
) -> Optional[List[MusicInfo]]:
|
||||
"""按豆瓣音乐专辑 ID 返回相关推荐条目。"""
|
||||
if source != self._music_source or not media_id:
|
||||
return None
|
||||
result = self.doubanapi.music_recommendations(
|
||||
subject_id=str(media_id),
|
||||
start=0,
|
||||
count=max(1, count),
|
||||
)
|
||||
return self._build_music_search_results(result)
|
||||
|
||||
def _recognize_music_media(
|
||||
self,
|
||||
meta: Optional[MetaMusic],
|
||||
@@ -278,17 +312,39 @@ class DoubanModule(_ModuleBase):
|
||||
return candidates[0]
|
||||
|
||||
@classmethod
|
||||
def _build_music_search_results(cls, result: Optional[dict]) -> List[MusicInfo]:
|
||||
def _build_music_search_results(
|
||||
cls,
|
||||
result: Optional[dict | list],
|
||||
) -> List[MusicInfo]:
|
||||
"""把豆瓣音乐搜索响应转换为专辑候选列表。"""
|
||||
items = (result or {}).get("items") or (result or {}).get("musics") or []
|
||||
payload = result or {}
|
||||
if isinstance(payload, list):
|
||||
items = payload
|
||||
else:
|
||||
items = (
|
||||
payload.get("subject_collection_items")
|
||||
or payload.get("recommendations")
|
||||
or payload.get("subjects")
|
||||
or payload.get("items")
|
||||
or payload.get("musics")
|
||||
or []
|
||||
)
|
||||
candidates = []
|
||||
for item in items:
|
||||
if not isinstance(item, dict):
|
||||
continue
|
||||
target_type = str(item.get("target_type") or item.get("type") or "").casefold()
|
||||
target = item.get("target") if isinstance(item.get("target"), dict) else item
|
||||
target_type = str(item.get("target_type") or "").casefold()
|
||||
if isinstance(item.get("target"), dict):
|
||||
target = item["target"]
|
||||
elif isinstance(item.get("subject"), dict):
|
||||
target = item["subject"]
|
||||
else:
|
||||
target = item
|
||||
type_name = str(target.get("type_name") or target.get("subtype") or "")
|
||||
if target_type and target_type not in {"music", "音乐"}:
|
||||
target_subject_type = str(target.get("type") or "").casefold()
|
||||
if target_type and target_type not in {"music", "音乐", "subject"}:
|
||||
continue
|
||||
if target_subject_type and target_subject_type not in {"music", "音乐"}:
|
||||
continue
|
||||
if type_name and type_name not in {"音乐", "music"}:
|
||||
continue
|
||||
@@ -473,6 +529,7 @@ class DoubanModule(_ModuleBase):
|
||||
cover_img.get("url"),
|
||||
cover.get("large"),
|
||||
cover.get("normal"),
|
||||
cover.get("url"),
|
||||
info.get("cover_url"),
|
||||
info.get("image"),
|
||||
]
|
||||
|
||||
@@ -623,6 +623,44 @@ class DoubanApi(metaclass=WeakSingleton):
|
||||
"""异步获取豆瓣音乐详情。"""
|
||||
return await self.__async_invoke_search(self._urls["music_detail"] + subject_id)
|
||||
|
||||
def music_single(self, start: int = 0, count: int = 20) -> dict:
|
||||
"""分页获取豆瓣音乐推荐合集。"""
|
||||
return self.__invoke_recommend(
|
||||
self._urls["music_single"], start=start, count=count
|
||||
)
|
||||
|
||||
async def async_music_single(self, start: int = 0, count: int = 20) -> dict:
|
||||
"""异步分页获取豆瓣音乐推荐合集。"""
|
||||
return await self.__async_invoke_recommend(
|
||||
self._urls["music_single"], start=start, count=count
|
||||
)
|
||||
|
||||
def music_recommendations(
|
||||
self,
|
||||
subject_id: str,
|
||||
start: int = 0,
|
||||
count: int = 20,
|
||||
) -> dict:
|
||||
"""获取豆瓣音乐条目的相关推荐。"""
|
||||
return self.__invoke_recommend(
|
||||
self._urls["music_recommendations"] % subject_id,
|
||||
start=start,
|
||||
count=count,
|
||||
)
|
||||
|
||||
async def async_music_recommendations(
|
||||
self,
|
||||
subject_id: str,
|
||||
start: int = 0,
|
||||
count: int = 20,
|
||||
) -> dict:
|
||||
"""异步获取豆瓣音乐条目的相关推荐。"""
|
||||
return await self.__async_invoke_recommend(
|
||||
self._urls["music_recommendations"] % subject_id,
|
||||
start=start,
|
||||
count=count,
|
||||
)
|
||||
|
||||
def movie_top250(self, start: Optional[int] = 0, count: Optional[int] = 20,
|
||||
ts=datetime.strftime(datetime.now(), '%Y%m%d')):
|
||||
"""
|
||||
|
||||
@@ -274,6 +274,70 @@ class TheAudioDbModule(_ModuleBase):
|
||||
start = max(page - 1, 0) * max(1, count)
|
||||
return [album.to_music_info() for album in albums[start:start + max(1, count)]]
|
||||
|
||||
def music_discover(
|
||||
self,
|
||||
source: str,
|
||||
page: int = 1,
|
||||
count: int = 30,
|
||||
entity: str = MUSIC_ENTITY_ALBUM,
|
||||
country: str = "us",
|
||||
) -> Optional[list[MusicInfo]]:
|
||||
"""读取 TheAudioDB iTunes 趋势榜并转换为可继续浏览的音乐实体。"""
|
||||
if source != self._source:
|
||||
return None
|
||||
payload = self._request_json(
|
||||
"trending.php",
|
||||
{
|
||||
"country": country.casefold(),
|
||||
"type": "itunes",
|
||||
"format": "singles" if entity == MUSIC_ENTITY_RECORDING else "albums",
|
||||
},
|
||||
)
|
||||
items = self._entities(payload, "trending")
|
||||
items.sort(
|
||||
key=lambda item: self._optional_int(item.get("intChartPlace")) or 10_000
|
||||
)
|
||||
candidates = []
|
||||
for item in items:
|
||||
if entity == MUSIC_ENTITY_RECORDING:
|
||||
info = self._track_to_info(item)
|
||||
else:
|
||||
info = self._album_to_info(item).to_music_info()
|
||||
if not info.media_id or not info.title:
|
||||
continue
|
||||
info.category = self._text(item.get("strType")) or "iTunes"
|
||||
info.raw_data.update(
|
||||
{
|
||||
"chart_position": self._optional_int(item.get("intChartPlace")),
|
||||
"chart_country": self._text(item.get("strCountry")),
|
||||
}
|
||||
)
|
||||
candidates.append(info)
|
||||
start = max(page - 1, 0) * max(1, count)
|
||||
return candidates[start:start + max(1, count)]
|
||||
|
||||
def music_album_related(
|
||||
self,
|
||||
source: str,
|
||||
media_id: str,
|
||||
count: int = 24,
|
||||
) -> Optional[list[MusicInfo]]:
|
||||
"""按专辑主艺术家返回 TheAudioDB 同艺人专辑,供详情页关联浏览。"""
|
||||
if source != self._source or not media_id:
|
||||
return None
|
||||
payload = self._request_json("album.php", {"m": media_id})
|
||||
album_item = self._first_entity(payload, "album", "albums")
|
||||
artist_id = self._text((album_item or {}).get("idArtist"))
|
||||
if not artist_id:
|
||||
return []
|
||||
albums_payload = self._request_json("album.php", {"i": artist_id})
|
||||
albums = [
|
||||
self._album_to_info(item).to_music_info()
|
||||
for item in self._entities(albums_payload, "album", "albums")
|
||||
if self._text(item.get("idAlbum") or item.get("id")) != str(media_id)
|
||||
]
|
||||
return albums[:max(1, count)]
|
||||
|
||||
def clear_cache(self) -> None:
|
||||
"""清除 TheAudioDB 请求缓存。"""
|
||||
self._request_json.cache_clear()
|
||||
|
||||
Reference in New Issue
Block a user