feat(auth): integrate Plex auxiliary authentication support

This commit is contained in:
InfinityPacer
2024-10-23 20:11:23 +08:00
parent d6f8c364bf
commit 874572253c
6 changed files with 87 additions and 8 deletions
+2 -2
View File
@@ -202,9 +202,9 @@ class UserChain(ChainBase, metaclass=Singleton):
# 触发认证通过的拦截事件 # 触发认证通过的拦截事件
intercept_event = self.eventmanager.send_event( intercept_event = self.eventmanager.send_event(
etype=ChainEventType.AuthIntercept, 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: if intercept_event and intercept_event.event_data:
intercept_data: AuthInterceptCredentials = intercept_event.event_data intercept_data: AuthInterceptCredentials = intercept_event.event_data
if intercept_data.cancel: if intercept_data.cancel:
+13 -2
View File
@@ -2,11 +2,12 @@ from typing import Any, Generator, List, Optional, Tuple, Union
from app import schemas from app import schemas
from app.core.context import MediaInfo from app.core.context import MediaInfo
from app.core.event import eventmanager
from app.log import logger from app.log import logger
from app.modules import _MediaServerBase, _ModuleBase from app.modules import _MediaServerBase, _ModuleBase
from app.modules.emby.emby import Emby from app.modules.emby.emby import Emby
from app.schemas.event import AuthCredentials from app.schemas.event import AuthCredentials, AuthInterceptCredentials
from app.schemas.types import MediaType, ModuleType from app.schemas.types import MediaType, ModuleType, ChainEventType
class EmbyModule(_ModuleBase, _MediaServerBase[Emby]): class EmbyModule(_ModuleBase, _MediaServerBase[Emby]):
@@ -75,6 +76,16 @@ class EmbyModule(_ModuleBase, _MediaServerBase[Emby]):
if not credentials or credentials.grant_type != "password": if not credentials or credentials.grant_type != "password":
return None return None
for name, server in self.get_instances().items(): 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) token = server.authenticate(credentials.username, credentials.password)
if token: if token:
credentials.channel = self.get_name() credentials.channel = self.get_name()
+13 -2
View File
@@ -2,11 +2,12 @@ from typing import Any, Generator, List, Optional, Tuple, Union
from app import schemas from app import schemas
from app.core.context import MediaInfo from app.core.context import MediaInfo
from app.core.event import eventmanager
from app.log import logger from app.log import logger
from app.modules import _MediaServerBase, _ModuleBase from app.modules import _MediaServerBase, _ModuleBase
from app.modules.jellyfin.jellyfin import Jellyfin from app.modules.jellyfin.jellyfin import Jellyfin
from app.schemas.event import AuthCredentials from app.schemas.event import AuthCredentials, AuthInterceptCredentials
from app.schemas.types import MediaType, ModuleType from app.schemas.types import MediaType, ModuleType, ChainEventType
class JellyfinModule(_ModuleBase, _MediaServerBase[Jellyfin]): class JellyfinModule(_ModuleBase, _MediaServerBase[Jellyfin]):
@@ -75,6 +76,16 @@ class JellyfinModule(_ModuleBase, _MediaServerBase[Jellyfin]):
if not credentials or credentials.grant_type != "password": if not credentials or credentials.grant_type != "password":
return None return None
for name, server in self.get_instances().items(): 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) token = server.authenticate(credentials.username, credentials.password)
if token: if token:
credentials.channel = self.get_name() credentials.channel = self.get_name()
+34 -1
View File
@@ -2,10 +2,12 @@ from typing import Optional, Tuple, Union, Any, List, Generator
from app import schemas from app import schemas
from app.core.context import MediaInfo from app.core.context import MediaInfo
from app.core.event import eventmanager
from app.log import logger from app.log import logger
from app.modules import _ModuleBase, _MediaServerBase from app.modules import _ModuleBase, _MediaServerBase
from app.modules.plex.plex import Plex 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]): class PlexModule(_ModuleBase, _MediaServerBase[Plex]):
@@ -64,6 +66,37 @@ class PlexModule(_ModuleBase, _MediaServerBase[Plex]):
logger.info(f"Plex {name} 服务器连接断开,尝试重连 ...") logger.info(f"Plex {name} 服务器连接断开,尝试重连 ...")
server.reconnect() 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]: def webhook_parser(self, body: Any, form: Any, args: Any) -> Optional[schemas.WebhookEventInfo]:
""" """
解析Webhook报文体 解析Webhook报文体
+22
View File
@@ -5,6 +5,7 @@ from urllib.parse import quote_plus
from cachetools import TTLCache, cached from cachetools import TTLCache, cached
from plexapi import media from plexapi import media
from plexapi.myplex import MyPlexAccount
from plexapi.server import PlexServer from plexapi.server import PlexServer
from requests import Response, Session from requests import Response, Session
@@ -61,6 +62,27 @@ class Plex:
self._plex = None self._plex = None
logger.error(f"Plex服务器连接失败:{str(e)}") 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)) @cached(cache=TTLCache(maxsize=100, ttl=86400))
def __get_library_images(self, library_key: str, mtype: int) -> Optional[List[str]]: def __get_library_images(self, library_key: str, mtype: int) -> Optional[List[str]]:
""" """
+3 -1
View File
@@ -74,15 +74,17 @@ class AuthInterceptCredentials(ChainEventData):
channel (str): 认证渠道 channel (str): 认证渠道
service (str): 服务名称 service (str): 服务名称
token (str): 认证令牌 token (str): 认证令牌
status (str): 认证状态,"triggered""completed" 两个状态
# 输出参数 # 输出参数
source (str): 拦截源,默认值为 "未知拦截源" source (str): 拦截源,默认值为 "未知拦截源"
cancel (bool): 是否取消认证,默认值为 False cancel (bool): 是否取消认证,默认值为 False
""" """
# 输入参数 # 输入参数
username: str = Field(..., description="用户名") username: Optional[str] = Field(..., description="用户名")
channel: str = Field(..., description="认证渠道") channel: str = Field(..., description="认证渠道")
service: str = Field(..., description="服务名称") service: str = Field(..., description="服务名称")
status: str = Field(..., description="认证状态, 包含 'triggered' 表示认证触发,'completed' 表示认证成功")
token: Optional[str] = Field(None, description="认证令牌") token: Optional[str] = Field(None, description="认证令牌")
# 输出参数 # 输出参数