Files
MoviePilot/app/modules/navidrome/__init__.py
freeman f3ad81b39e style: apply safe ruff auto-fixes and exempt startup late imports
Run `ruff check app tests scripts --fix` with the configured rule set
(E4/E7/E9/F/I): import sorting (I001), f-strings without placeholders
(F541), lambda/variable/unused-import cleanups that ruff marks safe.
Nine files whose only ruff debt sits next to pre-existing pylint
import-resolution errors (lazy exports, sys.path script imports) or
whose "unused" imports are reached through attribute access by tests are
left untouched so the changed-file pylint gate and the benchmark tests
stay green; their diagnostics remain in the baseline.

app/startup/lifecycle/__init__.py and app/main.py import business
modules after cache composition, import-path and stdio setup on purpose
(the source comments document the contract), so declare E402
per-file-ignores for them instead of adding per-line noqa.

Ruff ratchet watermark drops from 498 to 100; sync the baseline fixture
and the optimization checklist figure.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-09 13:29:09 +08:00

213 lines
9.0 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.application.mediaserver import MusicMediaServerHelper
from app.domain.context import MediaInfo
from app.modules import _MediaServerBase, _ModuleBase
from app.modules.navidrome.navidrome import Navidrome
from app.runtime.events import eventmanager
from app.runtime.log import logger
from app.schemas.dashboard import Statistic as _SchemaStatistic
from app.schemas.event import AuthCredentials, AuthInterceptCredentials
from app.schemas.mediaserver import ExistMediaInfo as _SchemaExistMediaInfo
from app.schemas.mediaserver import MediaServerItem as _SchemaMediaServerItem
from app.schemas.mediaserver import MediaServerLibrary as _SchemaMediaServerLibrary
from app.schemas.mediaserver import MediaServerPlayItem as _SchemaMediaServerPlayItem
from app.schemas.mediaserver import MediaServerSeasonInfo as _SchemaMediaServerSeasonInfo
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[_SchemaExistMediaInfo]:
"""判断音乐是否已存在于 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 _SchemaExistMediaInfo(
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 _SchemaExistMediaInfo(
type=MediaType.MUSIC,
server_type="navidrome",
server=name,
itemid=match.item_id,
)
return None
def media_statistic(self, server: Optional[str] = None) -> Optional[List[_SchemaStatistic]]:
"""返回 Navidrome 音乐数量统计。"""
servers = [self.get_instance(server)] if server else list(self.get_instances().values())
result: List[_SchemaStatistic] = []
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[_SchemaMediaServerLibrary]]:
"""返回 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[_SchemaMediaServerItem]:
"""获取 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[_SchemaMediaServerSeasonInfo]:
"""音乐服务器没有剧集信息,返回空列表。"""
return []
def mediaserver_playing(
self, server: str, count: Optional[int] = 20, username: Optional[str] = None
) -> Optional[List[_SchemaMediaServerPlayItem]]:
"""获取 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[_SchemaMediaServerPlayItem]]:
"""获取 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.type in (MediaType.MOVIE.value, MediaType.TV.value) and item.image
]