feat 支持Emby/Jellyfin登录认证

This commit is contained in:
jxxghp 2023-06-20 13:09:11 +08:00
parent 47f5942e22
commit 0776a0b235
17 changed files with 117 additions and 87 deletions

View File

@ -6,9 +6,7 @@ from fastapi import BackgroundTasks
from app import schemas
from app.chain.douban import DoubanChain
from app.core.context import MediaInfo
from app.db.models.user import User
from app.db.userauth import get_current_active_superuser
from app.db.userauth import get_current_active_user
from app.core.security import verify_token
from app.schemas import MediaType
router = APIRouter()
@ -24,7 +22,7 @@ def start_douban_chain():
@router.get("/sync", summary="同步豆瓣想看", response_model=schemas.Response)
async def sync_douban(
background_tasks: BackgroundTasks,
_: User = Depends(get_current_active_superuser)) -> Any:
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
"""
同步豆瓣想看
"""
@ -34,7 +32,7 @@ async def sync_douban(
@router.get("/id", summary="豆瓣ID识别", response_model=schemas.Context)
async def recognize_doubanid(doubanid: str,
_: User = Depends(get_current_active_user)) -> Any:
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
"""
根据豆瓣ID识别媒体信息
"""
@ -44,7 +42,7 @@ async def recognize_doubanid(doubanid: str,
@router.get("/info", summary="查询豆瓣详情", response_model=schemas.MediaInfo)
async def douban_info(doubanid: str) -> Any:
async def douban_info(doubanid: str, _: schemas.TokenPayload = Depends(verify_token)) -> Any:
"""
根据豆瓣ID查询豆瓣媒体信息
"""
@ -60,7 +58,7 @@ async def douban_movies(sort: str = "R",
tags: str = "",
start: int = 0,
count: int = 30,
_: User = Depends(get_current_active_user)) -> Any:
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
"""
浏览豆瓣电影信息
"""
@ -76,7 +74,7 @@ async def douban_tvs(sort: str = "R",
tags: str = "",
start: int = 0,
count: int = 30,
_: User = Depends(get_current_active_user)) -> Any:
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
"""
浏览豆瓣剧集信息
"""
@ -90,7 +88,7 @@ async def douban_tvs(sort: str = "R",
@router.get("/movie_top250", summary="豆瓣电影TOP250", response_model=List[schemas.MediaInfo])
async def movie_top250(page: int = 1,
count: int = 30,
_: User = Depends(get_current_active_user)) -> Any:
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
"""
浏览豆瓣剧集信息
"""
@ -101,7 +99,7 @@ async def movie_top250(page: int = 1,
@router.get("/tv_weekly_chinese", summary="豆瓣国产剧集周榜", response_model=List[schemas.MediaInfo])
async def tv_weekly_chinese(page: int = 1,
count: int = 30,
_: User = Depends(get_current_active_user)) -> Any:
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
"""
中国每周剧集口碑榜
"""
@ -112,7 +110,7 @@ async def tv_weekly_chinese(page: int = 1,
@router.get("/tv_weekly_global", summary="豆瓣全球剧集周榜", response_model=List[schemas.MediaInfo])
async def tv_weekly_global(page: int = 1,
count: int = 30,
_: User = Depends(get_current_active_user)) -> Any:
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
"""
全球每周剧集口碑榜
"""

View File

@ -4,11 +4,10 @@ from fastapi import APIRouter, Depends
from sqlalchemy.orm import Session
from app import schemas
from app.core.security import verify_token
from app.db import get_db
from app.db.models.downloadhistory import DownloadHistory
from app.db.models.transferhistory import TransferHistory
from app.db.models.user import User
from app.db.userauth import get_current_active_user
router = APIRouter()
@ -17,7 +16,7 @@ router = APIRouter()
async def download_history(page: int = 1,
count: int = 30,
db: Session = Depends(get_db),
_: User = Depends(get_current_active_user)) -> Any:
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
"""
查询下载历史记录
"""
@ -29,7 +28,7 @@ async def transfer_history(title: str = None,
page: int = 1,
count: int = 30,
db: Session = Depends(get_db),
_: User = Depends(get_current_active_user)) -> Any:
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
"""
查询转移历史记录
"""

View File

@ -6,10 +6,12 @@ from fastapi.security import OAuth2PasswordRequestForm
from sqlalchemy.orm import Session
from app import schemas
from app.chain.user import UserChain
from app.core import security
from app.core.config import settings
from app.db import get_db
from app.db.models.user import User
from app.log import logger
router = APIRouter()
@ -21,13 +23,21 @@ async def login_access_token(
"""
获取认证Token
"""
# 检查数据库
user = User.authenticate(
db=db,
name=form_data.username,
password=form_data.password
)
if not user:
raise HTTPException(status_code=400, detail="用户名或密码不正确")
# 请求协助认证
logger.warn("登录用户本地不匹配,尝试辅助认证 ...")
token = UserChain().user_authenticate(form_data.username, form_data.password)
if not token:
raise HTTPException(status_code=400, detail="用户名或密码不正确")
else:
logger.info(f"辅助认证成功,用户信息: {token}")
user = schemas.User(id=-1, name=form_data.username, is_active=True, is_superuser=False)
elif not user.is_active:
raise HTTPException(status_code=400, detail="用户未启用")
access_token_expires = timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)

View File

@ -4,8 +4,7 @@ from fastapi import APIRouter, Depends
from app import schemas
from app.chain.media import MediaChain
from app.db.models.user import User
from app.db.userauth import get_current_active_user
from app.core.security import verify_token
router = APIRouter()
@ -13,7 +12,7 @@ router = APIRouter()
@router.get("/recognize", summary="识别媒体信息", response_model=schemas.Context)
async def recognize(title: str,
subtitle: str = None,
_: User = Depends(get_current_active_user)) -> Any:
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
"""
根据标题副标题识别媒体信息
"""
@ -24,7 +23,7 @@ async def recognize(title: str,
@router.get("/search", summary="搜索媒体信息", response_model=List[schemas.MediaInfo])
async def search_by_title(title: str,
_: User = Depends(get_current_active_user)) -> Any:
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
"""
模糊搜索媒体信息列表
"""

View File

@ -4,16 +4,15 @@ from fastapi import APIRouter, Depends
from app import schemas
from app.core.plugin import PluginManager
from app.db.models.user import User
from app.core.security import verify_token
from app.db.systemconfig_oper import SystemConfigOper
from app.db.userauth import get_current_active_user
from app.schemas.types import SystemConfigKey
router = APIRouter()
@router.get("/", summary="所有插件", response_model=List[schemas.Plugin])
async def all_plugins(_: User = Depends(get_current_active_user)) -> Any:
async def all_plugins(_: schemas.TokenPayload = Depends(verify_token)) -> Any:
"""
查询所有插件清单
"""
@ -21,7 +20,7 @@ async def all_plugins(_: User = Depends(get_current_active_user)) -> Any:
@router.get("/installed", summary="已安装插件", response_model=List[str])
async def installed_plugins(_: User = Depends(get_current_active_user)) -> Any:
async def installed_plugins(_: schemas.TokenPayload = Depends(verify_token)) -> Any:
"""
查询用户已安装插件清单
"""
@ -29,7 +28,7 @@ async def installed_plugins(_: User = Depends(get_current_active_user)) -> Any:
@router.get("/{plugin_id}", summary="获取插件配置")
async def plugin_config(plugin_id: str, _: User = Depends(get_current_active_user)) -> dict:
async def plugin_config(plugin_id: str, _: schemas.TokenPayload = Depends(verify_token)) -> dict:
"""
根据插件ID获取插件配置信息
"""
@ -38,7 +37,7 @@ async def plugin_config(plugin_id: str, _: User = Depends(get_current_active_use
@router.put("/{plugin_id}", summary="更新插件配置", response_model=schemas.Response)
async def set_plugin_config(plugin_id: str, conf: dict,
_: User = Depends(get_current_active_user)) -> Any:
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
"""
根据插件ID获取插件配置信息
"""
@ -48,7 +47,7 @@ async def set_plugin_config(plugin_id: str, conf: dict,
@router.post("/{plugin_id}/install", summary="安装插件", response_model=schemas.Response)
async def install_plugin(plugin_id: str,
_: User = Depends(get_current_active_user)) -> Any:
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
"""
安装插件
"""
@ -64,7 +63,7 @@ async def install_plugin(plugin_id: str,
@router.delete("/{plugin_id}", summary="卸载插件", response_model=schemas.Response)
async def uninstall_plugin(plugin_id: str, _: User = Depends(get_current_active_user)) -> Any:
async def uninstall_plugin(plugin_id: str, _: schemas.TokenPayload = Depends(verify_token)) -> Any:
"""
卸载插件
"""

View File

@ -4,8 +4,7 @@ from fastapi import APIRouter, Depends
from app import schemas
from app.chain.search import SearchChain
from app.db.models.user import User
from app.db.userauth import get_current_active_user
from app.core.security import verify_token
from app.schemas.types import MediaType
router = APIRouter()
@ -14,7 +13,7 @@ router = APIRouter()
@router.get("/tmdbid", summary="精确搜索资源", response_model=List[schemas.Context])
async def search_by_tmdbid(tmdbid: int,
mtype: str = None,
_: User = Depends(get_current_active_user)) -> Any:
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
"""
根据TMDBID精确搜索站点资源
"""
@ -26,7 +25,7 @@ async def search_by_tmdbid(tmdbid: int,
@router.get("/title", summary="模糊搜索资源", response_model=List[schemas.TorrentInfo])
async def search_by_title(title: str,
_: User = Depends(get_current_active_user)) -> Any:
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
"""
根据名称模糊搜索站点资源
"""

View File

@ -6,18 +6,17 @@ from sqlalchemy.orm import Session
from app import schemas
from app.chain.cookiecloud import CookieCloudChain
from app.chain.site import SiteChain
from app.core.security import verify_token
from app.db import get_db
from app.db.models.site import Site
from app.db.models.user import User
from app.db.siteicon_oper import SiteIconOper
from app.db.userauth import get_current_active_user, get_current_active_superuser
router = APIRouter()
@router.get("/", summary="所有站点", response_model=List[schemas.Site])
async def read_sites(db: Session = Depends(get_db),
_: User = Depends(get_current_active_user)) -> List[dict]:
_: schemas.TokenPayload = Depends(verify_token)) -> List[dict]:
"""
获取站点列表
"""
@ -29,7 +28,7 @@ async def update_site(
*,
db: Session = Depends(get_db),
site_in: schemas.Site,
_: User = Depends(get_current_active_superuser),
_: schemas.TokenPayload = Depends(verify_token)
) -> Any:
"""
更新站点信息
@ -48,7 +47,7 @@ async def update_site(
async def read_site(
site_id: int,
db: Session = Depends(get_db),
_: User = Depends(get_current_active_user),
_: schemas.TokenPayload = Depends(verify_token)
) -> Any:
"""
获取站点信息
@ -63,7 +62,7 @@ async def read_site(
@router.get("/cookiecloud", summary="CookieCloud同步", response_model=schemas.Response)
async def cookie_cloud_sync(_: User = Depends(get_current_active_user)) -> Any:
async def cookie_cloud_sync(_: schemas.TokenPayload = Depends(verify_token)) -> Any:
"""
运行CookieCloud同步站点信息
"""
@ -79,7 +78,7 @@ async def update_cookie(
username: str,
password: str,
db: Session = Depends(get_db),
_: User = Depends(get_current_active_user)) -> Any:
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
"""
使用用户密码更新站点Cookie
"""
@ -101,7 +100,7 @@ async def update_cookie(
@router.get("/test", summary="连接测试", response_model=schemas.Response)
async def test_site(domain: str, _: User = Depends(get_current_active_user)) -> Any:
async def test_site(domain: str, _: schemas.TokenPayload = Depends(verify_token)) -> Any:
"""
测试站点是否可用
"""
@ -110,7 +109,7 @@ async def test_site(domain: str, _: User = Depends(get_current_active_user)) ->
@router.get("/icon", summary="站点图标", response_model=schemas.Response)
async def site_icon(domain: str, _: User = Depends(get_current_active_user)) -> Any:
async def site_icon(domain: str, _: schemas.TokenPayload = Depends(verify_token)) -> Any:
"""
获取站点图标base64或者url
"""

View File

@ -6,10 +6,9 @@ from sqlalchemy.orm import Session
from app import schemas
from app.chain.subscribe import SubscribeChain
from app.core.config import settings
from app.core.security import verify_token
from app.db import get_db
from app.db.models.subscribe import Subscribe
from app.db.models.user import User
from app.db.userauth import get_current_active_superuser
from app.schemas.types import MediaType
router = APIRouter()
@ -27,7 +26,7 @@ def start_subscribe_chain(title: str, year: str,
@router.get("/", summary="所有订阅", response_model=List[schemas.Subscribe])
async def read_subscribes(
db: Session = Depends(get_db),
_: User = Depends(get_current_active_superuser)) -> Any:
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
"""
查询所有订阅
"""
@ -38,7 +37,7 @@ async def read_subscribes(
async def create_subscribe(
*,
subscribe_in: schemas.Subscribe,
_: User = Depends(get_current_active_superuser),
_: schemas.TokenPayload = Depends(verify_token)
) -> Any:
"""
新增订阅
@ -52,7 +51,7 @@ async def update_subscribe(
*,
db: Session = Depends(get_db),
subscribe_in: schemas.Subscribe,
_: User = Depends(get_current_active_superuser),
_: schemas.TokenPayload = Depends(verify_token)
) -> Any:
"""
更新订阅信息
@ -72,7 +71,7 @@ async def delete_subscribe(
*,
db: Session = Depends(get_db),
subscribe_in: schemas.Subscribe,
_: User = Depends(get_current_active_superuser),
_: schemas.TokenPayload = Depends(verify_token)
) -> Any:
"""
删除订阅信息
@ -136,7 +135,7 @@ async def seerr_subscribe(request: Request, background_tasks: BackgroundTasks,
@router.get("/refresh", summary="刷新订阅", response_model=schemas.Response)
async def refresh_subscribes(
_: User = Depends(get_current_active_superuser)) -> Any:
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
"""
刷新所有订阅
"""
@ -146,7 +145,7 @@ async def refresh_subscribes(
@router.get("/search", summary="搜索订阅", response_model=schemas.Response)
async def search_subscribes(
_: User = Depends(get_current_active_superuser)) -> Any:
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
"""
搜索所有订阅
"""

View File

@ -1,16 +1,18 @@
import asyncio
import json
from fastapi import APIRouter
from fastapi import APIRouter, Depends
from fastapi.responses import StreamingResponse
from app import schemas
from app.core.security import verify_token
from app.helper.progress import ProgressHelper
router = APIRouter()
@router.get("/progress/{process_type}", summary="实时进度")
async def get_progress(process_type: str):
async def get_progress(process_type: str, _: schemas.TokenPayload = Depends(verify_token)):
"""
实时获取处理进度返回格式为SSE
"""

View File

@ -5,8 +5,7 @@ from fastapi import APIRouter, Depends
from app import schemas
from app.chain.tmdb import TmdbChain
from app.core.context import MediaInfo
from app.db.models.user import User
from app.db.userauth import get_current_active_user
from app.core.security import verify_token
from app.schemas.types import MediaType
router = APIRouter()
@ -30,7 +29,7 @@ async def tmdb_movies(sort_by: str = "popularity.desc",
with_genres: str = "",
with_original_language: str = "",
page: int = 1,
_: User = Depends(get_current_active_user)) -> Any:
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
"""
浏览TMDB电影信息
"""
@ -49,7 +48,7 @@ async def tmdb_tvs(sort_by: str = "popularity.desc",
with_genres: str = "",
with_original_language: str = "",
page: int = 1,
_: User = Depends(get_current_active_user)) -> Any:
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
"""
浏览TMDB剧集信息
"""
@ -65,7 +64,7 @@ async def tmdb_tvs(sort_by: str = "popularity.desc",
@router.get("/trending", summary="TMDB流行趋势", response_model=List[schemas.MediaInfo])
async def tmdb_trending(page: int = 1,
_: User = Depends(get_current_active_user)) -> Any:
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
"""
浏览TMDB剧集信息
"""

View File

@ -5,11 +5,11 @@ from app.chain import ChainBase
class UserChain(ChainBase):
def user_authenticate(self, name, password) -> Optional[bool]:
def user_authenticate(self, name, password) -> Optional[str]:
"""
辅助完成用户认证
:param name: 用户名
:param password: 密码
:return: bool
:return: token
"""
return self.run_module("user_authenticate", name=name, password=password)

View File

@ -8,8 +8,11 @@ from typing import Any, Union, Optional
import jwt
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad
from fastapi import HTTPException, status, Depends
from fastapi.security import OAuth2PasswordBearer
from passlib.context import CryptContext
from app import schemas
from app.core.config import settings
from cryptography.fernet import Fernet
@ -36,6 +39,19 @@ def create_access_token(
return encoded_jwt
def verify_token(token: str = Depends(reusable_oauth2)) -> schemas.TokenPayload:
try:
payload = jwt.decode(
token, settings.SECRET_KEY, algorithms=[ALGORITHM]
)
return schemas.TokenPayload(**payload)
except (jwt.DecodeError, jwt.InvalidTokenError, jwt.ImmatureSignatureError):
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="token校验不通过",
)
def verify_password(plain_password: str, hashed_password: str) -> bool:
return pwd_context.verify(plain_password, hashed_password)

View File

@ -1,28 +1,16 @@
import jwt
from fastapi import Depends, HTTPException, status
from fastapi import Depends, HTTPException
from sqlalchemy.orm import Session
from app import schemas
from app.core.config import settings
from app.core import security
from app.core.security import reusable_oauth2
from app.core.security import verify_token
from app.db import get_db
from app.db.models.user import User
def get_current_user(
db: Session = Depends(get_db), token: str = Depends(reusable_oauth2)
db: Session = Depends(get_db),
token_data: schemas.TokenPayload = Depends(verify_token)
) -> User:
try:
payload = jwt.decode(
token, settings.SECRET_KEY, algorithms=[security.ALGORITHM]
)
token_data = schemas.TokenPayload(**payload)
except (jwt.DecodeError, jwt.InvalidTokenError, jwt.ImmatureSignatureError):
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="token校验不通过",
)
user = User.get(db, rid=token_data.sub)
if not user:
raise HTTPException(status_code=404, detail="用户不存在")

View File

@ -21,13 +21,14 @@ class EmbyModule(_ModuleBase):
def init_setting(self) -> Tuple[str, Union[str, bool]]:
return "MEDIASERVER", "emby"
def user_authenticate(self, name, password) -> Optional[bool]:
def user_authenticate(self, name: str, password: str) -> Optional[str]:
"""
使用Emby用户辅助完成用户认证
:param name: 用户名
:param password: 密码
:return: bool
:return: token or None
"""
# Emby认证
return self.emby.authenticate(name, password)
def webhook_parser(self, body: Any, form: Any, args: Any) -> Optional[dict]:

View File

@ -6,10 +6,10 @@ from typing import List, Optional, Union, Dict
from app.core.config import settings
from app.log import logger
from app.schemas import RefreshMediaItem
from app.schemas.types import MediaType
from app.utils.http import RequestUtils
from app.utils.singleton import Singleton
from app.utils.string import StringUtils
from app.schemas.types import MediaType
class Emby(metaclass=Singleton):
@ -87,27 +87,37 @@ class Emby(metaclass=Singleton):
logger.error(f"连接Users出错" + str(e))
return None
def authenticate(self, username: str, password: str) -> Optional[bool]:
def authenticate(self, username: str, password: str) -> Optional[str]:
"""
用户认证
:param username: 用户名
:param password: 密码
:return: 认证token
"""
if not self._host or not self._apikey:
return None
req_url = "%semby/Users/AuthenticateByName" % self._host
try:
res = RequestUtils(content_type="application/json").post_res(
res = RequestUtils(headers={
'X-Emby-Authorization': f'MediaBrowser Client="MoviePilot", '
f'Device="Axios", '
f'DeviceId="1", '
f'Version="10.8.0", '
f'Token="{self._apikey}"',
'Content-Type': 'application/json',
"Accept": "application/json"
}).post_res(
url=req_url,
data=json.dumps({
"Username": username,
"Pw": password,
"KeepMeLoggedIn": False
"Pw": password
})
)
if res:
auth_token = res.json().get("AccessToken")
if auth_token:
logger.info(f"用户 {username} Emby认证成功")
return True
return auth_token
else:
logger.error(f"Users/AuthenticateByName 未获取到返回数据")
except Exception as e:

View File

@ -22,13 +22,14 @@ class JellyfinModule(_ModuleBase):
def init_setting(self) -> Tuple[str, Union[str, bool]]:
return "MEDIASERVER", "jellyfin"
def user_authenticate(self, name, password) -> Optional[bool]:
def user_authenticate(self, name: str, password: str) -> Optional[str]:
"""
使用Emby用户辅助完成用户认证
:param name: 用户名
:param password: 密码
:return: bool
:return: Token or None
"""
# Jellyfin认证
return self.jellyfin.authenticate(name, password)
def webhook_parser(self, body: Any, form: Any, args: Any) -> Optional[dict]:

View File

@ -84,15 +84,26 @@ class Jellyfin(metaclass=Singleton):
logger.error(f"连接Users出错" + str(e))
return None
def authenticate(self, username: str, password: str) -> Optional[bool]:
def authenticate(self, username: str, password: str) -> Optional[str]:
"""
用户认证
:param username: 用户名
:param password: 密码
:return: 认证成功返回token否则返回None
"""
if not self._host or not self._apikey:
return None
req_url = "%sUsers/authenticatebyname" % self._host
try:
res = RequestUtils(content_type="application/json").post_res(
res = RequestUtils(headers={
'X-Emby-Authorization': f'MediaBrowser Client="MoviePilot", '
f'Device="Axios", '
f'DeviceId="1", '
f'Version="10.8.0", '
f'Token="{self._apikey}"',
'Content-Type': 'application/json',
"Accept": "application/json"
}).post_res(
url=req_url,
data=json.dumps({
"Username": username,
@ -103,7 +114,7 @@ class Jellyfin(metaclass=Singleton):
auth_token = res.json().get("AccessToken")
if auth_token:
logger.info(f"用户 {username} Jellyfin认证成功")
return True
return auth_token
else:
logger.error(f"Users/AuthenticateByName 未获取到返回数据")
except Exception as e: