diff --git a/app/chain/user.py b/app/chain/user.py index 5a2650b8..6a36aef2 100644 --- a/app/chain/user.py +++ b/app/chain/user.py @@ -202,9 +202,9 @@ class UserChain(ChainBase, metaclass=Singleton): # 触发认证通过的拦截事件 intercept_event = self.eventmanager.send_event( etype=ChainEventType.AuthIntercept, - data=AuthInterceptCredentials(username=username, channel=channel, service=service, token=token) + data=AuthInterceptCredentials(username=username, channel=channel, service=service, + token=token, status="completed") ) - if intercept_event and intercept_event.event_data: intercept_data: AuthInterceptCredentials = intercept_event.event_data if intercept_data.cancel: diff --git a/app/modules/emby/__init__.py b/app/modules/emby/__init__.py index 11f8dfa6..5e7e9ed3 100644 --- a/app/modules/emby/__init__.py +++ b/app/modules/emby/__init__.py @@ -2,11 +2,12 @@ from typing import Any, Generator, List, Optional, Tuple, Union from app import schemas from app.core.context import MediaInfo +from app.core.event import eventmanager from app.log import logger from app.modules import _MediaServerBase, _ModuleBase from app.modules.emby.emby import Emby -from app.schemas.event import AuthCredentials -from app.schemas.types import MediaType, ModuleType +from app.schemas.event import AuthCredentials, AuthInterceptCredentials +from app.schemas.types import MediaType, ModuleType, ChainEventType class EmbyModule(_ModuleBase, _MediaServerBase[Emby]): @@ -75,6 +76,16 @@ class EmbyModule(_ModuleBase, _MediaServerBase[Emby]): if not credentials or credentials.grant_type != "password": return None for name, server in self.get_instances().items(): + # 触发认证拦截事件 + 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: + intercept_data: AuthInterceptCredentials = intercept_event.event_data + if intercept_data.cancel: + continue token = server.authenticate(credentials.username, credentials.password) if token: credentials.channel = self.get_name() diff --git a/app/modules/jellyfin/__init__.py b/app/modules/jellyfin/__init__.py index 595e6e75..8afe61e8 100644 --- a/app/modules/jellyfin/__init__.py +++ b/app/modules/jellyfin/__init__.py @@ -2,11 +2,12 @@ from typing import Any, Generator, List, Optional, Tuple, Union from app import schemas from app.core.context import MediaInfo +from app.core.event import eventmanager from app.log import logger from app.modules import _MediaServerBase, _ModuleBase from app.modules.jellyfin.jellyfin import Jellyfin -from app.schemas.event import AuthCredentials -from app.schemas.types import MediaType, ModuleType +from app.schemas.event import AuthCredentials, AuthInterceptCredentials +from app.schemas.types import MediaType, ModuleType, ChainEventType class JellyfinModule(_ModuleBase, _MediaServerBase[Jellyfin]): @@ -75,6 +76,16 @@ class JellyfinModule(_ModuleBase, _MediaServerBase[Jellyfin]): if not credentials or credentials.grant_type != "password": return None for name, server in self.get_instances().items(): + # 触发认证拦截事件 + 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: + intercept_data: AuthInterceptCredentials = intercept_event.event_data + if intercept_data.cancel: + continue token = server.authenticate(credentials.username, credentials.password) if token: credentials.channel = self.get_name() diff --git a/app/modules/plex/__init__.py b/app/modules/plex/__init__.py index 7c739f54..29a44f0c 100644 --- a/app/modules/plex/__init__.py +++ b/app/modules/plex/__init__.py @@ -2,10 +2,12 @@ from typing import Optional, Tuple, Union, Any, List, Generator from app import schemas from app.core.context import MediaInfo +from app.core.event import eventmanager from app.log import logger from app.modules import _ModuleBase, _MediaServerBase from app.modules.plex.plex import Plex -from app.schemas.types import MediaType, ModuleType +from app.schemas.event import AuthCredentials, AuthInterceptCredentials +from app.schemas.types import MediaType, ModuleType, ChainEventType class PlexModule(_ModuleBase, _MediaServerBase[Plex]): @@ -64,6 +66,37 @@ class PlexModule(_ModuleBase, _MediaServerBase[Plex]): logger.info(f"Plex {name} 服务器连接断开,尝试重连 ...") server.reconnect() + def user_authenticate(self, credentials: AuthCredentials) -> Optional[AuthCredentials]: + """ + 使用Plex用户辅助完成用户认证 + :param credentials: 认证数据 + :return: 认证数据 + """ + # Plex认证 + if not credentials or credentials.grant_type != "password": + return None + for name, server in self.get_instances().items(): + # 触发认证拦截事件 + 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: + intercept_data: AuthInterceptCredentials = intercept_event.event_data + if intercept_data.cancel: + continue + auth_result = server.authenticate(credentials.username, credentials.password) + if auth_result: + token, username = auth_result + credentials.channel = self.get_name() + credentials.service = name + credentials.token = token + # Plex 传入可能为邮箱,这里调整为用户名返回 + credentials.username = username + return credentials + return None + def webhook_parser(self, body: Any, form: Any, args: Any) -> Optional[schemas.WebhookEventInfo]: """ 解析Webhook报文体 diff --git a/app/modules/plex/plex.py b/app/modules/plex/plex.py index ee9c5adf..f64304d1 100644 --- a/app/modules/plex/plex.py +++ b/app/modules/plex/plex.py @@ -5,6 +5,7 @@ from urllib.parse import quote_plus from cachetools import TTLCache, cached from plexapi import media +from plexapi.myplex import MyPlexAccount from plexapi.server import PlexServer from requests import Response, Session @@ -61,6 +62,27 @@ class Plex: self._plex = None logger.error(f"Plex服务器连接失败:{str(e)}") + def authenticate(self, username: str, password: str) -> Optional[Tuple[str, str]]: + """ + 用户认证 + :param username: 用户名 + :param password: 密码 + :return: 认证成功返回 (token, 用户名),否则返回 None + """ + if not username or not password: + return None + try: + account = MyPlexAccount(username=username, password=password, remember=False) + if account: + plex = PlexServer(self._host, account.authToken) + if not plex: + return None + return account.authToken, account.username + except Exception as e: + # 处理认证失败或网络错误等情况 + logger.error(f"Authentication failed: {e}") + return None + @cached(cache=TTLCache(maxsize=100, ttl=86400)) def __get_library_images(self, library_key: str, mtype: int) -> Optional[List[str]]: """ diff --git a/app/schemas/event.py b/app/schemas/event.py index 1a5251c5..3490ab7a 100644 --- a/app/schemas/event.py +++ b/app/schemas/event.py @@ -74,15 +74,17 @@ class AuthInterceptCredentials(ChainEventData): channel (str): 认证渠道 service (str): 服务名称 token (str): 认证令牌 + status (str): 认证状态,"triggered" 和 "completed" 两个状态 # 输出参数 source (str): 拦截源,默认值为 "未知拦截源" cancel (bool): 是否取消认证,默认值为 False """ # 输入参数 - username: str = Field(..., description="用户名") + username: Optional[str] = Field(..., description="用户名") channel: str = Field(..., description="认证渠道") service: str = Field(..., description="服务名称") + status: str = Field(..., description="认证状态, 包含 'triggered' 表示认证触发,'completed' 表示认证成功") token: Optional[str] = Field(None, description="认证令牌") # 输出参数