Files
MoviePilot/app/modules/navidrome/__init__.py
2026-08-10 07:21:06 +08:00

204 lines
8.3 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Navidrome 媒体服务器模块。"""
from typing import Any, Dict, Generator, List, Optional, Tuple, Union
from app import schemas
from app.core.context import MediaInfo
from app.core.event import eventmanager
from app.helper.mediaserver import MusicMediaServerHelper
from app.log import logger
from app.modules import _MediaServerBase, _ModuleBase
from app.modules.navidrome.navidrome import Navidrome
from app.schemas import AuthCredentials, AuthInterceptCredentials
from app.schemas.types import ChainEventType, MediaServerType, MediaType, ModuleType
class NavidromeModule(_ModuleBase, _MediaServerBase[Navidrome]):
"""将 Navidrome 接入 MoviePilot 媒体服务器统一接口。"""
def init_module(self) -> None:
"""读取配置并初始化启用的 Navidrome 实例。"""
super().init_service(
service_name=Navidrome.__name__.lower(),
service_type=lambda conf: Navidrome(**conf.config, sync_libraries=conf.sync_libraries),
)
@staticmethod
def get_name() -> str:
"""返回模块显示名称。"""
return "Navidrome"
@staticmethod
def get_type() -> ModuleType:
"""返回模块类型。"""
return ModuleType.MediaServer
@staticmethod
def get_subtype() -> MediaServerType:
"""返回媒体服务器子类型。"""
return MediaServerType.Navidrome
@staticmethod
def get_priority() -> int:
"""返回模块优先级。"""
return 7
def stop(self) -> None:
"""释放 Navidrome 模块资源。"""
def init_setting(self) -> Optional[Tuple[str, Union[str, bool]]]:
"""Navidrome 与其它媒体服务器一致,由服务配置控制启用,无需系统开关。"""
return None
def scheduler_job(self) -> None:
"""定时检查 Navidrome 连接状态并尝试重连。"""
for name, server in self.get_instances().items():
if server.is_inactive():
logger.info(f"Navidrome {name} 服务器连接断开,尝试重连 ...")
server.reconnect()
def test(self) -> Optional[Tuple[bool, str]]:
"""测试所有已启用 Navidrome 配置的连通性。"""
if not self.get_instances():
return None
for name, server in self.get_instances().items():
if server.is_inactive():
server.reconnect()
if not server.get_user():
return False, f"无法连接Navidrome服务器{name}"
return True, ""
def user_authenticate(
self, credentials: AuthCredentials, service_name: Optional[str] = None
) -> Optional[AuthCredentials]:
"""使用 Navidrome 用户凭据完成辅助认证。"""
if not credentials or credentials.grant_type != "password":
return None
servers = (
[(service_name, self.get_instance(service_name))]
if service_name and self.get_instance(service_name)
else list(self.get_instances().items())
)
for name, server in servers:
if not server:
continue
intercept_event = eventmanager.send_event(
etype=ChainEventType.AuthIntercept,
data=AuthInterceptCredentials(
username=credentials.username,
channel=self.get_name(),
service=name,
status="triggered",
),
)
if intercept_event and intercept_event.event_data and intercept_event.event_data.cancel:
continue
token = server.authenticate(credentials.username, credentials.password)
if token:
credentials.channel = self.get_name()
credentials.service = name
credentials.token = token
return credentials
return None
def media_exists(
self, mediainfo: MediaInfo, itemid: Optional[str] = None, server: Optional[str] = None
) -> Optional[schemas.ExistMediaInfo]:
"""判断音乐是否已存在于 Navidrome 音乐库。"""
if mediainfo.type != MediaType.MUSIC:
return None
servers = (
[(server, self.get_instance(server))]
if server
else list(self.get_instances().items())
)
for name, service in servers:
if not service:
continue
item = service.get_iteminfo(str(itemid)) if itemid else None
if item and MusicMediaServerHelper.item_matches(mediainfo, item):
return schemas.ExistMediaInfo(
type=MediaType.MUSIC,
server_type="navidrome",
server=name,
itemid=itemid,
)
matches = service.search_music(**MusicMediaServerHelper.search_params(mediainfo))
match = MusicMediaServerHelper.find_match(mediainfo, matches)
if match:
return schemas.ExistMediaInfo(
type=MediaType.MUSIC,
server_type="navidrome",
server=name,
itemid=match.item_id,
)
return None
def media_statistic(self, server: Optional[str] = None) -> Optional[List[schemas.Statistic]]:
"""返回 Navidrome 音乐数量统计。"""
servers = [self.get_instance(server)] if server else list(self.get_instances().values())
result: List[schemas.Statistic] = []
for service in servers:
if not service:
continue
statistic = service.get_medias_count()
statistic.user_count = service.get_user_count()
result.append(statistic)
return result
def mediaserver_librarys(
self, server: str, username: Optional[str] = None, hidden: Optional[bool] = False
) -> Optional[List[schemas.MediaServerLibrary]]:
"""返回 Navidrome 的虚拟音乐库。"""
service = self.get_instance(server)
return service.get_librarys(hidden=hidden) if service else None
def mediaserver_items(
self, server: str, library_id: Union[str, int], start_index: Optional[int] = 0,
limit: Optional[int] = -1,
) -> Optional[Generator]:
"""获取 Navidrome 专辑条目。"""
service = self.get_instance(server)
return service.get_items(start_index, limit) if service else None
def mediaserver_items_count(self, server: str, library_id: Union[str, int]) -> Optional[int]:
"""获取 Navidrome 音乐库条目数量。"""
service = self.get_instance(server)
return service.get_items_count(str(library_id)) if service else None
def mediaserver_iteminfo(self, server: str, item_id: str) -> Optional[schemas.MediaServerItem]:
"""获取 Navidrome 专辑详情。"""
service = self.get_instance(server)
return service.get_iteminfo(item_id) if service else None
def mediaserver_tv_episodes(self, server: str, item_id: Union[str, int]) -> List[schemas.MediaServerSeasonInfo]:
"""音乐服务器没有剧集信息,返回空列表。"""
return []
def mediaserver_playing(
self, server: str, count: Optional[int] = 20, username: Optional[str] = None
) -> Optional[List[schemas.MediaServerPlayItem]]:
"""获取 Navidrome 当前播放条目。"""
service = self.get_instance(server)
return service.get_resume(count) if service else None
def mediaserver_play_url(self, server: str, item_id: Union[str, int]) -> Optional[str]:
"""生成 Navidrome 音频流地址。"""
service = self.get_instance(server)
return service.get_play_url(str(item_id)) if service else None
def mediaserver_latest(
self, server: Optional[str] = None, count: Optional[int] = 20,
username: Optional[str] = None,
) -> Optional[List[schemas.MediaServerPlayItem]]:
"""获取 Navidrome 最近新增专辑。"""
service = self.get_instance(server)
return service.get_latest(count) if service else None
def mediaserver_latest_images(
self, server: Optional[str] = None, count: Optional[int] = 10,
username: Optional[str] = None, remote: Optional[bool] = False,
) -> List[str]:
"""获取最近新增专辑封面。"""
return [item.image for item in (self.mediaserver_latest(server, count, username) or []) if item.image]