mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-09-05 23:47:41 +08:00
feat(auth): integrate Plex auxiliary authentication support
This commit is contained in:
+2
-2
@@ -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:
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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报文体
|
||||||
|
|||||||
@@ -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]]:
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -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="认证令牌")
|
||||||
|
|
||||||
# 输出参数
|
# 输出参数
|
||||||
|
|||||||
Reference in New Issue
Block a user