Compare commits

...

36 Commits

Author SHA1 Message Date
jxxghp
d4514edba6 v1.7.3
- `捷径`新增消息中心功能
- 内建支持CookieCloud本地化服务器,Cookie数据加密后保存在用户配置目录中,可在`设定`-`站点`中选择开启
- 优化了推荐详情页面,豆瓣推荐详情直接展示豆瓣数据源
- 修复了`蜜柑`无法搜索的问题
2024-03-17 09:09:21 +08:00
jxxghp
0c581565ad 更新 message.py 2024-03-16 22:21:12 +08:00
jxxghp
350def0a6f 更新 message.py 2024-03-16 22:20:14 +08:00
jxxghp
5b3027c0a7 fix reload 2024-03-16 21:06:52 +08:00
jxxghp
e4b90ca8f7 fix #1694 2024-03-16 20:40:02 +08:00
jxxghp
d917b00055 Merge pull request #1694 from lingjiameng/main
CookieCloud配置支持实时更新
2024-03-16 20:36:05 +08:00
s0mE
cc94c6c367 Merge branch 'jxxghp:main' into main 2024-03-16 19:24:25 +08:00
ljmeng
6410051e3a CookieCloud配置支持实时加载 2024-03-16 19:23:06 +08:00
jxxghp
aaa1b80edf fix 资源包更新Bug 2024-03-16 18:38:25 +08:00
jxxghp
f345d94009 fix README.md 2024-03-16 18:28:09 +08:00
jxxghp
550fe26d76 Merge pull request #1693 from lingjiameng/main
集成CookieCloud服务器端
2024-03-16 17:52:49 +08:00
jxxghp
7ad498b3a3 fix 2024-03-16 17:06:24 +08:00
jxxghp
20eb0b4635 fix message 2024-03-16 16:29:14 +08:00
ljmeng
747dc3fafe 默认关闭本地CookieCloud服务 2024-03-16 15:40:10 +08:00
s0mE
4708fbb3cb Merge branch 'jxxghp:main' into main 2024-03-16 15:36:20 +08:00
ljmeng
6ba40edeb4 Merge branch 'main' of github.com:lingjiameng/MoviePilot 2024-03-16 15:35:02 +08:00
ljmeng
79cb28faf9 默认配置关闭本地cookiecloud服务 2024-03-16 15:34:46 +08:00
jxxghp
9acf05f334 fix #1691 2024-03-16 15:31:04 +08:00
jxxghp
d0af1bf075 Merge pull request #1691 from hoey94/main 2024-03-16 13:53:10 +08:00
hoey94
f8a95cec4a fix: TR远程控制插件限速问题 104 2024-03-16 12:37:21 +08:00
jxxghp
3cd672fa8d fix 2024-03-16 08:40:36 +08:00
jxxghp
fe03638552 fix api 2024-03-16 08:39:57 +08:00
ljmeng
1ae220c654 集成CookieCloud服务端 2024-03-16 04:48:34 +08:00
jxxghp
75c7e71ee6 Merge pull request #1689 from hoey94/main 2024-03-15 19:14:26 +08:00
hoey94
4619158b99 fix: 限速开关BUG 104 2024-03-15 18:23:44 +08:00
jxxghp
3f88907ba9 fix bug 2024-03-15 18:17:04 +08:00
jxxghp
ae6440bd0a Merge pull request #1683 from lingjiameng/main 2024-03-15 07:55:01 +08:00
s0mE
261f5fc0c6 Merge branch 'jxxghp:main' into main 2024-03-14 23:26:58 +08:00
jxxghp
a5d044d535 fix message 2024-03-14 20:36:15 +08:00
jxxghp
6e607ca89f fix 优化推荐跳转
feat 消息落库
2024-03-14 19:44:15 +08:00
jxxghp
06e4b9ad83 Merge remote-tracking branch 'origin/main' 2024-03-14 19:15:22 +08:00
jxxghp
c755dc9b85 fix 优化推荐跳转
feat 消息落库
2024-03-14 19:15:13 +08:00
jxxghp
209451d5f9 Merge pull request #1678 from HankunYu/main 2024-03-14 06:57:31 +08:00
HankunYu
60b2d30f42 Update README.md
增加使用反代的描述,解决使用https反代时日志加载时间过长(十几分钟)不可用的问题。
2024-03-13 18:54:55 +00:00
ljmeng
399d26929d CookieCloud改为本地解密,增强安全性 2024-03-14 02:35:22 +08:00
jxxghp
f50c2e59a9 fix #1674 2024-03-13 14:54:37 +08:00
35 changed files with 719 additions and 126 deletions

2
.gitignore vendored
View File

@@ -10,7 +10,9 @@ app/helper/*.pyd
app/helper/*.bin app/helper/*.bin
app/plugins/** app/plugins/**
!app/plugins/__init__.py !app/plugins/__init__.py
config/cookies/**
config/user.db config/user.db
config/sites/** config/sites/**
*.pyc *.pyc
*.log *.log
.vscode

View File

@@ -21,7 +21,11 @@
### 2. **安装CookieCloud服务端可选** ### 2. **安装CookieCloud服务端可选**
MoviePilot内置了公共CookieCloud服务器如果需要自建服务可参考 [CookieCloud](https://github.com/easychen/CookieCloud) 项目进行搭建docker镜像请点击 [这里](https://hub.docker.com/r/easychen/cookiecloud)。 通过CookieCloud可以快速同步浏览器中保存的站点数据到MoviePilot支持以下服务方式
- 使用公共CookieCloud远程服务器默认服务器地址为https://movie-pilot.org/cookiecloud
- 使用内建的本地Cookie服务`设定` - `站点` 中打开`启用本地CookieCloud服务器`将启用内建的CookieCloud提供服务服务地址为`http://localhost:${NGINX_PORT}/cookiecloud/`, Cookie数据加密保存在配置文件目录下的`cookies`文件中
- 自建服务CookieCloud服务器参考 [CookieCloud](https://github.com/easychen/CookieCloud) 项目进行搭建docker镜像请点击 [这里](https://hub.docker.com/r/easychen/cookiecloud)
**声明:** 本项目不会收集用户敏感数据Cookie同步也是基于CookieCloud项目实现非本项目提供的能力。技术角度上CookieCloud采用端到端加密在个人不泄露`用户KEY``端对端加密密码`的情况下第三方无法窃取任何用户信息(包括服务器持有者)。如果你不放心,可以不使用公共服务或者不使用本项目,但如果使用后发生了任何信息泄露与本项目无关! **声明:** 本项目不会收集用户敏感数据Cookie同步也是基于CookieCloud项目实现非本项目提供的能力。技术角度上CookieCloud采用端到端加密在个人不泄露`用户KEY``端对端加密密码`的情况下第三方无法窃取任何用户信息(包括服务器持有者)。如果你不放心,可以不使用公共服务或者不使用本项目,但如果使用后发生了任何信息泄露与本项目无关!
@@ -219,6 +223,14 @@ location / {
proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Forwarded-Proto $scheme;
} }
``` ```
- 反代使用ssl时需要开启`http2`,否则会导致日志加载时间过长或不可用。以`Nginx`为例:
```nginx configuration
server {
listen 443 ssl;
http2 on;
# ...
}
```
- 新建的企业微信应用需要固定公网IP的代理才能收到消息代理添加以下代码 - 新建的企业微信应用需要固定公网IP的代理才能收到消息代理添加以下代码
```nginx configuration ```nginx configuration
location /cgi-bin/gettoken { location /cgi-bin/gettoken {

View File

@@ -90,7 +90,7 @@ def movie_top250(page: int = 1,
""" """
浏览豆瓣剧集信息 浏览豆瓣剧集信息
""" """
movies = DoubanChain().movie_top250(page=page, count=count) movies = DoubanChain().movie_top250(page=page, count=count) or []
return [MediaInfo(douban_info=movie).to_dict() for movie in movies] return [MediaInfo(douban_info=movie).to_dict() for movie in movies]
@@ -101,7 +101,7 @@ def tv_weekly_chinese(page: int = 1,
""" """
中国每周剧集口碑榜 中国每周剧集口碑榜
""" """
tvs = DoubanChain().tv_weekly_chinese(page=page, count=count) tvs = DoubanChain().tv_weekly_chinese(page=page, count=count) or []
return [MediaInfo(douban_info=tv).to_dict() for tv in tvs] return [MediaInfo(douban_info=tv).to_dict() for tv in tvs]
@@ -112,7 +112,7 @@ def tv_weekly_global(page: int = 1,
""" """
全球每周剧集口碑榜 全球每周剧集口碑榜
""" """
tvs = DoubanChain().tv_weekly_global(page=page, count=count) tvs = DoubanChain().tv_weekly_global(page=page, count=count) or []
return [MediaInfo(douban_info=tv).to_dict() for tv in tvs] return [MediaInfo(douban_info=tv).to_dict() for tv in tvs]
@@ -123,7 +123,7 @@ def tv_animation(page: int = 1,
""" """
热门动画剧集 热门动画剧集
""" """
tvs = DoubanChain().tv_animation(page=page, count=count) tvs = DoubanChain().tv_animation(page=page, count=count) or []
return [MediaInfo(douban_info=tv).to_dict() for tv in tvs] return [MediaInfo(douban_info=tv).to_dict() for tv in tvs]
@@ -134,7 +134,7 @@ def movie_hot(page: int = 1,
""" """
热门电影 热门电影
""" """
movies = DoubanChain().movie_hot(page=page, count=count) movies = DoubanChain().movie_hot(page=page, count=count) or []
return [MediaInfo(douban_info=movie).to_dict() for movie in movies] return [MediaInfo(douban_info=movie).to_dict() for movie in movies]
@@ -145,7 +145,7 @@ def tv_hot(page: int = 1,
""" """
热门电视剧 热门电视剧
""" """
tvs = DoubanChain().tv_hot(page=page, count=count) tvs = DoubanChain().tv_hot(page=page, count=count) or []
return [MediaInfo(douban_info=tv).to_dict() for tv in tvs] return [MediaInfo(douban_info=tv).to_dict() for tv in tvs]

View File

@@ -113,20 +113,6 @@ def media_info(mediaid: str, type_name: str,
doubanid = mediaid[7:] doubanid = mediaid[7:]
if not tmdbid and not doubanid: if not tmdbid and not doubanid:
return schemas.MediaInfo() return schemas.MediaInfo()
if settings.RECOGNIZE_SOURCE == "themoviedb":
if not tmdbid and doubanid:
tmdbinfo = MediaChain().get_tmdbinfo_by_doubanid(doubanid=doubanid, mtype=mtype)
if tmdbinfo:
tmdbid = tmdbinfo.get("id")
else:
return schemas.MediaInfo()
else:
if not doubanid and tmdbid:
doubaninfo = MediaChain().get_doubaninfo_by_tmdbid(tmdbid=tmdbid, mtype=mtype)
if doubaninfo:
doubanid = doubaninfo.get("id")
else:
return schemas.MediaInfo()
mediainfo = MediaChain().recognize_media(tmdbid=tmdbid, doubanid=doubanid, mtype=mtype) mediainfo = MediaChain().recognize_media(tmdbid=tmdbid, doubanid=doubanid, mtype=mtype)
if mediainfo: if mediainfo:
MediaChain().obtain_images(mediainfo) MediaChain().obtain_images(mediainfo)

View File

@@ -1,18 +1,24 @@
import json
from typing import Union, Any, List from typing import Union, Any, List
from fastapi import APIRouter, BackgroundTasks, Depends from fastapi import APIRouter, BackgroundTasks, Depends
from fastapi import Request from fastapi import Request
from sqlalchemy.orm import Session
from starlette.responses import PlainTextResponse from starlette.responses import PlainTextResponse
from app import schemas from app import schemas
from app.chain.message import MessageChain from app.chain.message import MessageChain
from app.core.config import settings from app.core.config import settings
from app.core.security import verify_token from app.core.security import verify_token
from app.db import get_db
from app.db.models import User
from app.db.models.message import Message
from app.db.systemconfig_oper import SystemConfigOper from app.db.systemconfig_oper import SystemConfigOper
from app.db.userauth import get_current_active_superuser
from app.log import logger from app.log import logger
from app.modules.wechat.WXBizMsgCrypt3 import WXBizMsgCrypt from app.modules.wechat.WXBizMsgCrypt3 import WXBizMsgCrypt
from app.schemas import NotificationSwitch from app.schemas import NotificationSwitch
from app.schemas.types import SystemConfigKey, NotificationType from app.schemas.types import SystemConfigKey, NotificationType, MessageChannel
router = APIRouter() router = APIRouter()
@@ -36,6 +42,39 @@ async def user_message(background_tasks: BackgroundTasks, request: Request):
return schemas.Response(success=True) return schemas.Response(success=True)
@router.post("/web", summary="接收WEB消息", response_model=schemas.Response)
def web_message(text: str, current_user: User = Depends(get_current_active_superuser)):
"""
WEB消息响应
"""
MessageChain().handle_message(
channel=MessageChannel.Web,
userid=current_user.name,
username=current_user.name,
text=text
)
return schemas.Response(success=True)
@router.get("/web", summary="获取WEB消息", response_model=List[dict])
def get_web_message(_: schemas.TokenPayload = Depends(verify_token),
db: Session = Depends(get_db),
page: int = 1,
count: int = 20):
"""
获取WEB消息列表
"""
ret_messages = []
messages = Message.list_by_page(db, page=page, count=count)
for message in messages:
try:
ret_messages.append(message.to_dict())
except Exception as e:
logger.error(f"获取WEB消息列表失败: {str(e)}")
continue
return ret_messages
def wechat_verify(echostr: str, msg_signature: str, def wechat_verify(echostr: str, msg_signature: str,
timestamp: Union[str, int], nonce: str) -> Any: timestamp: Union[str, int], nonce: str) -> Any:
""" """
@@ -103,7 +142,7 @@ def read_switchs(_: schemas.TokenPayload = Depends(verify_token)) -> Any:
def set_switchs(switchs: List[NotificationSwitch], def set_switchs(switchs: List[NotificationSwitch],
_: schemas.TokenPayload = Depends(verify_token)) -> Any: _: schemas.TokenPayload = Depends(verify_token)) -> Any:
""" """
查询通知消息渠道开关 设置通知消息渠道开关
""" """
switch_list = [] switch_list = []
for switch in switchs: for switch in switchs:

View File

@@ -138,7 +138,7 @@ def set_setting(key: str, value: Union[list, dict, bool, int, str] = None,
@router.get("/message", summary="实时消息") @router.get("/message", summary="实时消息")
def get_message(token: str): def get_message(token: str, role: str = "sys"):
""" """
实时获取系统消息返回格式为SSE 实时获取系统消息返回格式为SSE
""" """
@@ -152,7 +152,7 @@ def get_message(token: str):
def event_generator(): def event_generator():
while True: while True:
detail = message.get() detail = message.get(role)
yield 'data: %s\n\n' % (detail or '') yield 'data: %s\n\n' % (detail or '')
time.sleep(3) time.sleep(3)
@@ -307,7 +307,6 @@ def reload_module(_: schemas.TokenPayload = Depends(verify_token)):
重新加载模块 重新加载模块
""" """
ModuleManager().reload() ModuleManager().reload()
Scheduler().init()
return schemas.Response(success=True) return schemas.Response(success=True)

105
app/api/servcookie.py Normal file
View File

@@ -0,0 +1,105 @@
import gzip
import json
import os
from typing import Annotated, Any, Callable, Dict
from fastapi import APIRouter, Depends, HTTPException, Path, Request, Response
from fastapi.responses import PlainTextResponse
from fastapi.routing import APIRoute
from app import schemas
from app.core.config import settings
from app.utils.common import get_decrypted_cookie_data
class GzipRequest(Request):
_body: bytes = b""
async def body(self) -> bytes:
if not hasattr(self, "_body"):
body = await super().body()
if "gzip" in self.headers.getlist("Content-Encoding"):
body = gzip.decompress(body)
self._body = body
return self._body
class GzipRoute(APIRoute):
def get_route_handler(self) -> Callable:
original_route_handler = super().get_route_handler()
async def custom_route_handler(request: Request) -> Response:
request = GzipRequest(request.scope, request.receive)
return await original_route_handler(request)
return custom_route_handler
async def verify_server_enabled():
"""
校验CookieCloud服务路由是否打开
"""
if not settings.COOKIECLOUD_ENABLE_LOCAL:
raise HTTPException(status_code=400, detail="本地CookieCloud服务器未启用")
return True
cookie_router = APIRouter(route_class=GzipRoute,
tags=['servcookie'],
dependencies=[Depends(verify_server_enabled)])
@cookie_router.get("/", response_class=PlainTextResponse)
def get_root():
return "Hello World! API ROOT = /cookiecloud"
@cookie_router.post("/", response_class=PlainTextResponse)
def post_root():
return "Hello World! API ROOT = /cookiecloud"
@cookie_router.post("/update")
async def update_cookie(req: schemas.CookieData):
file_path = os.path.join(settings.COOKIE_PATH,
os.path.basename(req.uuid) + ".json")
content = json.dumps({"encrypted": req.encrypted})
with open(file_path, encoding="utf-8", mode="w") as file:
file.write(content)
with open(file_path, encoding="utf-8", mode="r") as file:
read_content = file.read()
if read_content == content:
return {"action": "done"}
else:
return {"action": "error"}
def load_encrypt_data(uuid: str) -> Dict[str, Any]:
file_path = os.path.join(settings.COOKIE_PATH,
os.path.basename(uuid) + ".json")
# 检查文件是否存在
if not os.path.exists(file_path):
raise HTTPException(status_code=404, detail="Item not found")
# 读取文件
with open(file_path, encoding="utf-8", mode="r") as file:
read_content = file.read()
data = json.loads(read_content)
return data
@cookie_router.get("/get/{uuid}")
async def get_cookie(
uuid: Annotated[str, Path(min_length=5, pattern="^[a-zA-Z0-9]+$")]):
return load_encrypt_data(uuid)
@cookie_router.post("/get/{uuid}")
async def post_cookie(
uuid: Annotated[str, Path(min_length=5, pattern="^[a-zA-Z0-9]+$")],
request: schemas.CookiePassword):
data = load_encrypt_data(uuid)
return get_decrypted_cookie_data(uuid, request.password, data["encrypted"])

View File

@@ -15,6 +15,8 @@ from app.core.context import MediaInfo, TorrentInfo
from app.core.event import EventManager from app.core.event import EventManager
from app.core.meta import MetaBase from app.core.meta import MetaBase
from app.core.module import ModuleManager from app.core.module import ModuleManager
from app.db.message_oper import MessageOper
from app.helper.message import MessageHelper
from app.log import logger from app.log import logger
from app.schemas import TransferInfo, TransferTorrent, ExistMediaInfo, DownloadingTorrent, CommingMessage, Notification, \ from app.schemas import TransferInfo, TransferTorrent, ExistMediaInfo, DownloadingTorrent, CommingMessage, Notification, \
WebhookEventInfo, TmdbEpisode WebhookEventInfo, TmdbEpisode
@@ -33,6 +35,8 @@ class ChainBase(metaclass=ABCMeta):
""" """
self.modulemanager = ModuleManager() self.modulemanager = ModuleManager()
self.eventmanager = EventManager() self.eventmanager = EventManager()
self.messageoper = MessageOper()
self.messagehelper = MessageHelper()
@staticmethod @staticmethod
def load_cache(filename: str) -> Any: def load_cache(filename: str) -> Any:
@@ -403,6 +407,10 @@ class ChainBase(metaclass=ABCMeta):
:param message: 消息体 :param message: 消息体
:return: 成功或失败 :return: 成功或失败
""" """
logger.info(f"发送消息channel={message.channel}"
f"title={message.title}, "
f"text={message.text}"
f"userid={message.userid}")
# 发送事件 # 发送事件
self.eventmanager.send_event(etype=EventType.NoticeMessage, self.eventmanager.send_event(etype=EventType.NoticeMessage,
data={ data={
@@ -413,10 +421,13 @@ class ChainBase(metaclass=ABCMeta):
"image": message.image, "image": message.image,
"userid": message.userid, "userid": message.userid,
}) })
logger.info(f"发送消息channel={message.channel}" # 保存消息
f"title={message.title}, " self.messagehelper.put(message, role="user")
f"text={message.text}" self.messageoper.add(channel=message.channel, mtype=message.mtype,
f"userid={message.userid}") title=message.title, text=message.text,
image=message.image, link=message.link,
userid=message.userid, action=1)
# 发送
self.run_module("post_message", message=message) self.run_module("post_message", message=message)
def post_medias_message(self, message: Notification, medias: List[MediaInfo]) -> Optional[bool]: def post_medias_message(self, message: Notification, medias: List[MediaInfo]) -> Optional[bool]:
@@ -426,6 +437,13 @@ class ChainBase(metaclass=ABCMeta):
:param medias: 媒体列表 :param medias: 媒体列表
:return: 成功或失败 :return: 成功或失败
""" """
note_list = [media.to_dict() for media in medias]
self.messagehelper.put(message, role="user", note=note_list)
self.messageoper.add(channel=message.channel, mtype=message.mtype,
title=message.title, text=message.text,
image=message.image, link=message.link,
userid=message.userid, action=1,
note=note_list)
return self.run_module("post_medias_message", message=message, medias=medias) return self.run_module("post_medias_message", message=message, medias=medias)
def post_torrents_message(self, message: Notification, torrents: List[Context]) -> Optional[bool]: def post_torrents_message(self, message: Notification, torrents: List[Context]) -> Optional[bool]:
@@ -435,6 +453,13 @@ class ChainBase(metaclass=ABCMeta):
:param torrents: 种子列表 :param torrents: 种子列表
:return: 成功或失败 :return: 成功或失败
""" """
note_list = [torrent.torrent_info.to_dict() for torrent in torrents]
self.messagehelper.put(message, role="user", note=note_list)
self.messageoper.add(channel=message.channel, mtype=message.mtype,
title=message.title, text=message.text,
image=message.image, link=message.link,
userid=message.userid, action=1,
note=note_list)
return self.run_module("post_torrents_message", message=message, torrents=torrents) return self.run_module("post_torrents_message", message=message, torrents=torrents)
def scrape_metadata(self, path: Path, mediainfo: MediaInfo, transfer_type: str, def scrape_metadata(self, path: Path, mediainfo: MediaInfo, transfer_type: str,

View File

@@ -12,9 +12,11 @@ from app.core.config import settings
from app.core.context import MediaInfo, Context from app.core.context import MediaInfo, Context
from app.core.event import EventManager from app.core.event import EventManager
from app.core.meta import MetaBase from app.core.meta import MetaBase
from app.db.message_oper import MessageOper
from app.helper.message import MessageHelper
from app.helper.torrent import TorrentHelper from app.helper.torrent import TorrentHelper
from app.log import logger from app.log import logger
from app.schemas import Notification, NotExistMediaInfo from app.schemas import Notification, NotExistMediaInfo, CommingMessage
from app.schemas.types import EventType, MessageChannel, MediaType from app.schemas.types import EventType, MessageChannel, MediaType
from app.utils.string import StringUtils from app.utils.string import StringUtils
@@ -43,6 +45,8 @@ class MessageChain(ChainBase):
self.mediachain = MediaChain() self.mediachain = MediaChain()
self.eventmanager = EventManager() self.eventmanager = EventManager()
self.torrenthelper = TorrentHelper() self.torrenthelper = TorrentHelper()
self.messagehelper = MessageHelper()
self.messageoper = MessageOper()
def __get_noexits_info( def __get_noexits_info(
self, self,
@@ -100,10 +104,8 @@ class MessageChain(ChainBase):
def process(self, body: Any, form: Any, args: Any) -> None: def process(self, body: Any, form: Any, args: Any) -> None:
""" """
识别消息内容,执行操作 调用模块识别消息内容
""" """
# 申明全局变量
global _current_page, _current_meta, _current_media
# 获取消息内容 # 获取消息内容
info = self.message_parser(body=body, form=form, args=args) info = self.message_parser(body=body, form=form, args=args)
if not info: if not info:
@@ -122,10 +124,34 @@ class MessageChain(ChainBase):
if not text: if not text:
logger.debug(f'未识别到消息内容::{body}{form}{args}') logger.debug(f'未识别到消息内容::{body}{form}{args}')
return return
# 处理消息
self.handle_message(channel=channel, userid=userid, username=username, text=text)
def handle_message(self, channel: MessageChannel, userid: Union[str, int], username: str, text: str) -> None:
"""
识别消息内容,执行操作
"""
# 申明全局变量
global _current_page, _current_meta, _current_media
# 加载缓存 # 加载缓存
user_cache: Dict[str, dict] = self.load_cache(self._cache_file) or {} user_cache: Dict[str, dict] = self.load_cache(self._cache_file) or {}
# 处理消息 # 处理消息
logger.info(f'收到用户消息内容,用户:{userid},内容:{text}') logger.info(f'收到用户消息内容,用户:{userid},内容:{text}')
# 保存消息
self.messagehelper.put(
CommingMessage(
userid=userid,
username=username,
channel=channel,
text=text
), role="user")
self.messageoper.add(
channel=channel,
userid=username or userid,
text=text,
action=0
)
# 处理消息
if text.startswith('/'): if text.startswith('/'):
# 执行命令 # 执行命令
self.eventmanager.send_event( self.eventmanager.send_event(

View File

@@ -40,11 +40,7 @@ class SiteChain(ChainBase):
self.rsshelper = RssHelper() self.rsshelper = RssHelper()
self.cookiehelper = CookieHelper() self.cookiehelper = CookieHelper()
self.message = MessageHelper() self.message = MessageHelper()
self.cookiecloud = CookieCloudHelper( self.cookiecloud = CookieCloudHelper()
server=settings.COOKIECLOUD_HOST,
key=settings.COOKIECLOUD_KEY,
password=settings.COOKIECLOUD_PASSWORD
)
# 特殊站点登录验证 # 特殊站点登录验证
self.special_site_test = { self.special_site_test = {
@@ -302,20 +298,21 @@ class SiteChain(ChainBase):
if not site_info: if not site_info:
return False, f"站点【{url}】不存在" return False, f"站点【{url}】不存在"
# 特殊站点测试
if self.special_site_test.get(domain):
return self.special_site_test[domain](site_info)
# 通用站点测试
site_url = site_info.url
site_cookie = site_info.cookie
ua = site_info.ua
render = site_info.render
public = site_info.public
proxies = settings.PROXY if site_info.proxy else None
proxy_server = settings.PROXY_SERVER if site_info.proxy else None
# 模拟登录 # 模拟登录
try: try:
# 特殊站点测试
if self.special_site_test.get(domain):
return self.special_site_test[domain](site_info)
# 通用站点测试
site_url = site_info.url
site_cookie = site_info.cookie
ua = site_info.ua
render = site_info.render
public = site_info.public
proxies = settings.PROXY if site_info.proxy else None
proxy_server = settings.PROXY_SERVER if site_info.proxy else None
# 访问链接 # 访问链接
if render: if render:
page_source = PlaywrightHelper().get_page_source(url=site_url, page_source = PlaywrightHelper().get_page_source(url=site_url,

View File

@@ -187,6 +187,8 @@ class Settings(BaseSettings):
PLEX_TOKEN: Optional[str] = None PLEX_TOKEN: Optional[str] = None
# 转移方式 link/copy/move/softlink # 转移方式 link/copy/move/softlink
TRANSFER_TYPE: str = "copy" TRANSFER_TYPE: str = "copy"
# CookieCloud是否启动本地服务
COOKIECLOUD_ENABLE_LOCAL: Optional[bool] = False
# CookieCloud服务器地址 # CookieCloud服务器地址
COOKIECLOUD_HOST: str = "https://movie-pilot.org/cookiecloud" COOKIECLOUD_HOST: str = "https://movie-pilot.org/cookiecloud"
# CookieCloud用户KEY # CookieCloud用户KEY
@@ -275,6 +277,10 @@ class Settings(BaseSettings):
@property @property
def LOG_PATH(self): def LOG_PATH(self):
return self.CONFIG_PATH / "logs" return self.CONFIG_PATH / "logs"
@property
def COOKIE_PATH(self):
return self.CONFIG_PATH / "cookies"
@property @property
def CACHE_CONF(self): def CACHE_CONF(self):
@@ -397,6 +403,9 @@ class Settings(BaseSettings):
with self.LOG_PATH as p: with self.LOG_PATH as p:
if not p.exists(): if not p.exists():
p.mkdir(parents=True, exist_ok=True) p.mkdir(parents=True, exist_ok=True)
with self.COOKIE_PATH as p:
if not p.exists():
p.mkdir(parents=True, exist_ok=True)
class Config: class Config:
case_sensitive = True case_sensitive = True

View File

@@ -615,6 +615,7 @@ class MediaInfo:
""" """
返回字典 返回字典
""" """
self.clear()
dicts = asdict(self) dicts = asdict(self)
dicts["type"] = self.type.value if self.type else None dicts["type"] = self.type.value if self.type else None
dicts["detail_link"] = self.detail_link dicts["detail_link"] = self.detail_link

61
app/db/message_oper.py Normal file
View File

@@ -0,0 +1,61 @@
import json
import time
from typing import Optional
from sqlalchemy.orm import Session
from app.db import DbOper
from app.db.models.message import Message
from app.schemas import MessageChannel, NotificationType
class MessageOper(DbOper):
"""
消息数据管理
"""
def __init__(self, db: Session = None):
super().__init__(db)
def add(self,
channel: MessageChannel = None,
mtype: NotificationType = None,
title: str = None,
text: str = None,
image: str = None,
link: str = None,
userid: str = None,
action: int = 1,
note: dict = None,
**kwargs):
"""
新增媒体服务器数据
:param channel: 消息渠道
:param mtype: 消息类型
:param title: 标题
:param text: 文本内容
:param image: 图片
:param link: 链接
:param userid: 用户ID
:param action: 消息方向0-接收息1-发送消息
:param note: 附件json
"""
kwargs.update({
"channel": channel.value if channel else '',
"mtype": mtype.value if mtype else '',
"title": title,
"text": text,
"image": image,
"link": link,
"userid": userid,
"action": action,
"reg_time": time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()),
"note": json.dumps(note) if note else ''
})
Message(**kwargs).create(self._db)
def list_by_page(self, page: int = 1, count: int = 30) -> Optional[str]:
"""
获取媒体服务器数据ID
"""
return Message.list_by_page(self._db, page, count)

View File

@@ -200,6 +200,7 @@ class DownloadFiles(Base):
result = db.query(DownloadFiles).filter(DownloadFiles.savepath == savepath).all() result = db.query(DownloadFiles).filter(DownloadFiles.savepath == savepath).all()
return list(result) return list(result)
@staticmethod
@db_update @db_update
def delete_by_fullpath(db: Session, fullpath: str): def delete_by_fullpath(db: Session, fullpath: str):
db.query(DownloadFiles).filter(DownloadFiles.fullpath == fullpath, db.query(DownloadFiles).filter(DownloadFiles.fullpath == fullpath,

39
app/db/models/message.py Normal file
View File

@@ -0,0 +1,39 @@
from sqlalchemy import Column, Integer, String, Sequence
from sqlalchemy.orm import Session
from app.db import db_query, Base
class Message(Base):
"""
消息表
"""
id = Column(Integer, Sequence('id'), primary_key=True, index=True)
# 消息渠道
channel = Column(String)
# 消息类型
mtype = Column(String)
# 标题
title = Column(String)
# 文本内容
text = Column(String)
# 图片
image = Column(String)
# 链接
link = Column(String)
# 用户ID
userid = Column(String)
# 登记时间
reg_time = Column(String, index=True)
# 消息方向0-接收息1-发送消息
action = Column(Integer)
# 附件json
note = Column(String)
@staticmethod
@db_query
def list_by_page(db: Session, page: int = 1, count: int = 30):
result = db.query(Message).order_by(Message.reg_time.desc()).offset((page - 1) * count).limit(
count).all()
result.sort(key=lambda x: x.reg_time, reverse=False)
return list(result)

View File

@@ -1,68 +1,127 @@
from typing import Tuple, Optional import json
import os
from hashlib import md5
from typing import Any, Dict, Tuple, Optional
from app.core.config import settings
from app.utils.common import decrypt
from app.utils.http import RequestUtils from app.utils.http import RequestUtils
from app.utils.string import StringUtils from app.utils.string import StringUtils
class CookieCloudHelper: class CookieCloudHelper:
_ignore_cookies: list = ["CookieAutoDeleteBrowsingDataCleanup", "CookieAutoDeleteCleaningDiscarded"] _ignore_cookies: list = ["CookieAutoDeleteBrowsingDataCleanup", "CookieAutoDeleteCleaningDiscarded"]
def __init__(self, server: str, key: str, password: str): def __init__(self):
self._server = server self._sync_setting()
self._key = key
self._password = password
self._req = RequestUtils(content_type="application/json") self._req = RequestUtils(content_type="application/json")
def _sync_setting(self):
self._server = settings.COOKIECLOUD_HOST
self._key = settings.COOKIECLOUD_KEY
self._password = settings.COOKIECLOUD_PASSWORD
self._enable_local = settings.COOKIECLOUD_ENABLE_LOCAL
self._local_path = settings.COOKIE_PATH
def download(self) -> Tuple[Optional[dict], str]: def download(self) -> Tuple[Optional[dict], str]:
""" """
从CookieCloud下载数据 从CookieCloud下载数据
:return: Cookie数据、错误信息 :return: Cookie数据、错误信息
""" """
if not self._server or not self._key or not self._password: # 更新为最新设置
self._sync_setting()
if ((not self._server and not self._enable_local)
or not self._key
or not self._password):
return None, "CookieCloud参数不正确" return None, "CookieCloud参数不正确"
req_url = "%s/get/%s" % (self._server, str(self._key).strip())
ret = self._req.post_res(url=req_url, json={"password": str(self._password).strip()}) if self._enable_local:
if ret and ret.status_code == 200: # 开启本地服务时,从本地直接读取数据
result = ret.json() result = self._load_local_encrypt_data(self._key)
if not result: if not result:
return {}, "下载到数据" return {}, "从本地CookieCloud服务加载到cookie数据请检查服务器设置、用户KEY及加密密码是否正确"
if result.get("cookie_data"):
contents = result.get("cookie_data")
else:
contents = result
# 整理数据,使用domain域名的最后两级作为分组依据
domain_groups = {}
for site, cookies in contents.items():
for cookie in cookies:
domain_key = StringUtils.get_url_domain(cookie.get("domain"))
if not domain_groups.get(domain_key):
domain_groups[domain_key] = [cookie]
else:
domain_groups[domain_key].append(cookie)
# 返回错误
ret_cookies = {}
# 索引器
for domain, content_list in domain_groups.items():
if not content_list:
continue
# 只有cf的cookie过滤掉
cloudflare_cookie = True
for content in content_list:
if content["name"] != "cf_clearance":
cloudflare_cookie = False
break
if cloudflare_cookie:
continue
# 站点Cookie
cookie_str = ";".join(
[f"{content.get('name')}={content.get('value')}"
for content in content_list
if content.get("name") and content.get("name") not in self._ignore_cookies]
)
ret_cookies[domain] = cookie_str
return ret_cookies, ""
elif ret:
return None, f"同步CookieCloud失败错误码{ret.status_code}"
else: else:
return None, "CookieCloud请求失败请检查服务器地址、用户KEY及加密密码是否正确" req_url = "%s/get/%s" % (self._server, str(self._key).strip())
ret = self._req.get_res(url=req_url)
if ret and ret.status_code == 200:
try:
result = ret.json()
if not result:
return {}, f"未从{self._server}下载到cookie数据"
except Exception as err:
return {}, f"{self._server}下载cookie数据错误{str(err)}"
elif ret:
return None, f"远程同步CookieCloud失败错误码{ret.status_code}"
else:
return None, "CookieCloud请求失败请检查服务器地址、用户KEY及加密密码是否正确"
encrypted = result.get("encrypted")
if not encrypted:
return {}, "未获取到cookie密文"
else:
crypt_key = self._get_crypt_key()
try:
decrypted_data = decrypt(encrypted, crypt_key).decode('utf-8')
result = json.loads(decrypted_data)
except Exception as e:
return {}, "cookie解密失败" + str(e)
if not result:
return {}, "cookie解密为空"
if result.get("cookie_data"):
contents = result.get("cookie_data")
else:
contents = result
# 整理数据,使用domain域名的最后两级作为分组依据
domain_groups = {}
for site, cookies in contents.items():
for cookie in cookies:
domain_key = StringUtils.get_url_domain(cookie.get("domain"))
if not domain_groups.get(domain_key):
domain_groups[domain_key] = [cookie]
else:
domain_groups[domain_key].append(cookie)
# 返回错误
ret_cookies = {}
# 索引器
for domain, content_list in domain_groups.items():
if not content_list:
continue
# 只有cf的cookie过滤掉
cloudflare_cookie = True
for content in content_list:
if content["name"] != "cf_clearance":
cloudflare_cookie = False
break
if cloudflare_cookie:
continue
# 站点Cookie
cookie_str = ";".join(
[f"{content.get('name')}={content.get('value')}"
for content in content_list
if content.get("name") and content.get("name") not in self._ignore_cookies]
)
ret_cookies[domain] = cookie_str
return ret_cookies, ""
def _get_crypt_key(self) -> bytes:
"""
使用UUID和密码生成CookieCloud的加解密密钥
"""
md5_generator = md5()
md5_generator.update((str(self._key).strip() + '-' + str(self._password).strip()).encode('utf-8'))
return (md5_generator.hexdigest()[:16]).encode('utf-8')
def _load_local_encrypt_data(self, uuid: str) -> Dict[str, Any]:
file_path = os.path.join(self._local_path, os.path.basename(uuid) + ".json")
# 检查文件是否存在
if not os.path.exists(file_path):
return {}
# 读取文件
with open(file_path, encoding="utf-8", mode="r") as file:
read_content = file.read()
data = json.loads(read_content)
return data

View File

@@ -1,19 +1,46 @@
import json
import queue import queue
import time
from typing import Optional, Any
from app.utils.singleton import Singleton from app.utils.singleton import Singleton
class MessageHelper(metaclass=Singleton): class MessageHelper(metaclass=Singleton):
""" """
消息队列管理器 消息队列管理器,包括系统消息和用户消息
""" """
def __init__(self): def __init__(self):
self.queue = queue.Queue() self.sys_queue = queue.Queue()
self.user_queue = queue.Queue()
def put(self, message: str): def put(self, message: Any, role: str = "sys", note: dict = None):
self.queue.put(message) """
存消息
:param message: 消息
:param role: 消息通道 sys/user
:param note: 附件json
"""
if role == "sys":
self.sys_queue.put(message)
else:
if isinstance(message, str):
self.user_queue.put(message)
elif hasattr(message, "to_dict"):
content = message.to_dict()
content['date'] = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())
content['note'] = json.dumps(note) if note else None
self.user_queue.put(json.dumps(content))
def get(self): def get(self, role: str = "sys") -> Optional[str]:
if not self.queue.empty(): """
return self.queue.get(block=False) 取消息
:param role: 消息通道 sys/user
"""
if role == "sys":
if not self.sys_queue.empty():
return self.sys_queue.get(block=False)
else:
if not self.user_queue.empty():
return self.user_queue.get(block=False)
return None return None

View File

@@ -55,7 +55,7 @@ class ResourceHelper(metaclass=Singleton):
target = resource.get("target") target = resource.get("target")
version = resource.get("version") version = resource.get("version")
# 判断平台 # 判断平台
if platform and platform != SystemUtils.platform: if platform and platform != SystemUtils.platform():
continue continue
# 判断本地是否存在 # 判断本地是否存在
local_path = self._base_dir / target / rname local_path = self._base_dir / target / rname

View File

@@ -53,10 +53,13 @@ def init_routers():
""" """
from app.api.apiv1 import api_router from app.api.apiv1 import api_router
from app.api.servarr import arr_router from app.api.servarr import arr_router
from app.api.servcookie import cookie_router
# API路由 # API路由
App.include_router(api_router, prefix=settings.API_V1_STR) App.include_router(api_router, prefix=settings.API_V1_STR)
# Radarr、Sonarr路由 # Radarr、Sonarr路由
App.include_router(arr_router, prefix="/api/v3") App.include_router(arr_router, prefix="/api/v3")
# CookieCloud路由
App.include_router(cookie_router, prefix="/cookiecloud")
def start_frontend(): def start_frontend():

View File

@@ -57,7 +57,11 @@ class DoubanModule(_ModuleBase):
:param cache: 是否使用缓存 :param cache: 是否使用缓存
:return: 识别的媒体信息,包括剧集信息 :return: 识别的媒体信息,包括剧集信息
""" """
if settings.RECOGNIZE_SOURCE != "douban": if not doubanid and not meta:
return None
if meta and not doubanid \
and settings.RECOGNIZE_SOURCE != "douban":
return None return None
if not meta: if not meta:

View File

@@ -43,7 +43,7 @@ class FileTransferModule(_ModuleBase):
continue continue
download_path = Path(path) download_path = Path(path)
if not download_path.exists(): if not download_path.exists():
return False, f"目录 {download_path} 不存在" return False, f"下载目录 {download_path} 不存在"
download_paths.append(path) download_paths.append(path)
# 下载目录的设备ID # 下载目录的设备ID
download_devids = [Path(path).stat().st_dev for path in download_paths] download_devids = [Path(path).stat().st_dev for path in download_paths]
@@ -54,7 +54,7 @@ class FileTransferModule(_ModuleBase):
for path in settings.LIBRARY_PATHS: for path in settings.LIBRARY_PATHS:
library_path = Path(path) library_path = Path(path)
if not library_path.exists(): if not library_path.exists():
return False, f"目录不存在:{library_path}" return False, f"媒体库目录不存在:{library_path}"
if settings.DOWNLOADER_MONITOR and settings.TRANSFER_TYPE == "link": if settings.DOWNLOADER_MONITOR and settings.TRANSFER_TYPE == "link":
if library_path.stat().st_dev not in download_devids: if library_path.stat().st_dev not in download_devids:
return False, f"媒体库目录 {library_path} " \ return False, f"媒体库目录 {library_path} " \

View File

@@ -133,7 +133,7 @@ class Qbittorrent:
except Exception as err: except Exception as err:
logger.error(f"删除种子Tag出错{str(err)}") logger.error(f"删除种子Tag出错{str(err)}")
return False return False
def remove_torrents_tag(self, ids: Union[str, list], tag: Union[str, list]) -> bool: def remove_torrents_tag(self, ids: Union[str, list], tag: Union[str, list]) -> bool:
""" """
移除种子Tag 移除种子Tag
@@ -148,7 +148,7 @@ class Qbittorrent:
except Exception as err: except Exception as err:
logger.error(f"移除种子Tag出错{str(err)}") logger.error(f"移除种子Tag出错{str(err)}")
return False return False
def set_torrents_tag(self, ids: Union[str, list], tags: list): def set_torrents_tag(self, ids: Union[str, list], tags: list):
""" """
设置种子状态为已整理,以及是否强制做种 设置种子状态为已整理,以及是否强制做种
@@ -372,6 +372,24 @@ class Qbittorrent:
logger.error(f"设置速度限制出错:{str(err)}") logger.error(f"设置速度限制出错:{str(err)}")
return False return False
def get_speed_limit(self) -> Optional[Tuple[float, float]]:
"""
获取QB速度
:return: 返回download_limit 和upload_limit 默认是0
"""
if not self.qbc:
return None
download_limit = 0
upload_limit = 0
try:
download_limit = self.qbc.transfer.download_limit
upload_limit = self.qbc.transfer.upload_limit
except Exception as err:
logger.error(f"获取速度限制出错:{str(err)}")
return download_limit / 1024, upload_limit / 1024
def recheck_torrents(self, ids: Union[str, list]) -> bool: def recheck_torrents(self, ids: Union[str, list]) -> bool:
""" """
重新校验种子 重新校验种子

View File

@@ -67,7 +67,11 @@ class TheMovieDbModule(_ModuleBase):
:param cache: 是否使用缓存 :param cache: 是否使用缓存
:return: 识别的媒体信息,包括剧集信息 :return: 识别的媒体信息,包括剧集信息
""" """
if settings.RECOGNIZE_SOURCE != "themoviedb": if not tmdbid and not meta:
return None
if meta and not tmdbid \
and settings.RECOGNIZE_SOURCE != "themoviedb":
return None return None
if not meta: if not meta:
@@ -182,7 +186,7 @@ class TheMovieDbModule(_ModuleBase):
:param season: 季号 :param season: 季号
""" """
# 搜索 # 搜索
logger.info(f"开始使用 名称:{name}年份:{year} 匹配TMDB信息 ...") logger.info(f"开始使用 名称:{name} 年份:{year} 匹配TMDB信息 ...")
info = self.tmdb.match(name=name, info = self.tmdb.match(name=name,
year=year, year=year,
mtype=mtype, mtype=mtype,

View File

@@ -189,9 +189,16 @@ class TmdbHelper:
season_year, season_year,
season_number) season_number)
if not info: if not info:
logger.debug( year_range = [year]
f"正在识别{mtype.value}{name}, 年份={year} ...") if year:
info = self.__search_tv_by_name(name, year) year_range.append(str(int(year) + 1))
year_range.append(str(int(year) - 1))
for year in year_range:
logger.debug(
f"正在识别{mtype.value}{name}, 年份={year} ...")
info = self.__search_tv_by_name(name, year)
if info:
break
if info: if info:
info['media_type'] = MediaType.TV info['media_type'] = MediaType.TV
# 返回 # 返回

View File

@@ -289,6 +289,29 @@ class Transmission:
logger.error(f"设置速度限制出错:{str(err)}") logger.error(f"设置速度限制出错:{str(err)}")
return False return False
def get_speed_limit(self) -> Optional[Tuple[float, float]]:
"""
获取TR速度
:return: download_limit 下载速度 默认是0
upload_limit 上传速度 默认是0
"""
if not self.trc:
return None
download_limit = 0
upload_limit = 0
try:
download_limit = self.trc.get_session().get('speed_limit_down')
upload_limit = self.trc.get_session().get('speed_limit_up')
except Exception as err:
logger.error(f"获取速度限制出错:{str(err)}")
return (
download_limit,
upload_limit
)
def recheck_torrents(self, ids: Union[str, list]) -> bool: def recheck_torrents(self, ids: Union[str, list]) -> bool:
""" """
重新校验种子 重新校验种子
@@ -372,4 +395,3 @@ class Transmission:
except Exception as err: except Exception as err:
logger.error(f"修改tracker出错{str(err)}") logger.error(f"修改tracker出错{str(err)}")
return False return False

View File

@@ -5,6 +5,7 @@ from .site import *
from .subscribe import * from .subscribe import *
from .context import * from .context import *
from .servarr import * from .servarr import *
from .servcookie import *
from .plugin import * from .plugin import *
from .history import * from .history import *
from .dashboard import * from .dashboard import *

View File

@@ -17,6 +17,20 @@ class CommingMessage(BaseModel):
channel: Optional[MessageChannel] = None channel: Optional[MessageChannel] = None
# 消息体 # 消息体
text: Optional[str] = None text: Optional[str] = None
# 时间
date: Optional[str] = None
# 消息方向
action: Optional[int] = 0
def to_dict(self):
"""
转换为字典
"""
items = self.dict()
for k, v in items.items():
if isinstance(v, MessageChannel):
items[k] = v.value
return items
class Notification(BaseModel): class Notification(BaseModel):
@@ -37,6 +51,21 @@ class Notification(BaseModel):
link: Optional[str] = None link: Optional[str] = None
# 用户ID # 用户ID
userid: Optional[Union[str, int]] = None userid: Optional[Union[str, int]] = None
# 时间
date: Optional[str] = None
# 消息方向
action: Optional[int] = 1
def to_dict(self):
"""
转换为字典
"""
items = self.dict()
for k, v in items.items():
if isinstance(v, MessageChannel) \
or isinstance(v, NotificationType):
items[k] = v.value
return items
class NotificationSwitch(BaseModel): class NotificationSwitch(BaseModel):

13
app/schemas/servcookie.py Normal file
View File

@@ -0,0 +1,13 @@
from typing import Union
from fastapi import Query
from pydantic import BaseModel
class CookieData(BaseModel):
uuid: str = Query(min_length=5, pattern="^[a-zA-Z0-9]+$")
encrypted: str = Query(min_length=1, max_length=1024 * 1024 * 50)
class CookiePassword(BaseModel):
password: str

View File

@@ -117,3 +117,4 @@ class MessageChannel(Enum):
Slack = "Slack" Slack = "Slack"
SynologyChat = "SynologyChat" SynologyChat = "SynologyChat"
VoceChat = "VoceChat" VoceChat = "VoceChat"
Web = "Web"

View File

@@ -1,5 +1,12 @@
import base64
import json
import time import time
from typing import Any
from hashlib import md5
from typing import Any, Dict, Optional
from Crypto import Random
from Crypto.Cipher import AES
def retry(ExceptionToCheck: Any, def retry(ExceptionToCheck: Any,
@@ -32,3 +39,66 @@ def retry(ExceptionToCheck: Any,
return f_retry return f_retry
return deco_retry return deco_retry
def bytes_to_key(data: bytes, salt: bytes, output=48) -> bytes:
# extended from https://gist.github.com/gsakkis/4546068
assert len(salt) == 8, len(salt)
data += salt
key = md5(data).digest()
final_key = key
while len(final_key) < output:
key = md5(key + data).digest()
final_key += key
return final_key[:output]
def encrypt(message: bytes, passphrase: bytes) -> bytes:
"""
CryptoJS 加密原文
This is a modified copy of https://stackoverflow.com/questions/36762098/how-to-decrypt-password-from-javascript-cryptojs-aes-encryptpassword-passphras
"""
salt = Random.new().read(8)
key_iv = bytes_to_key(passphrase, salt, 32 + 16)
key = key_iv[:32]
iv = key_iv[32:]
aes = AES.new(key, AES.MODE_CBC, iv)
length = 16 - (len(message) % 16)
data = message + (chr(length) * length).encode()
return base64.b64encode(b"Salted__" + salt + aes.encrypt(data))
def decrypt(encrypted: str | bytes, passphrase: bytes) -> bytes:
"""
CryptoJS 解密密文
来源同encrypt
"""
encrypted = base64.b64decode(encrypted)
assert encrypted[0:8] == b"Salted__"
salt = encrypted[8:16]
key_iv = bytes_to_key(passphrase, salt, 32 + 16)
key = key_iv[:32]
iv = key_iv[32:]
aes = AES.new(key, AES.MODE_CBC, iv)
data = aes.decrypt(encrypted[16:])
return data[:-(data[-1] if type(data[-1]) == int else ord(data[-1]))]
def get_decrypted_cookie_data(uuid: str, password: str,
encrypted: str) -> Optional[Dict[str, Any]]:
key_md5 = md5()
key_md5.update((uuid + '-' + password).encode('utf-8'))
aes_key = (key_md5.hexdigest()[:16]).encode('utf-8')
if encrypted is not None:
try:
decrypted_data = decrypt(encrypted, aes_key).decode('utf-8')
decrypted_data = json.loads(decrypted_data)
if 'cookie_data' in decrypted_data:
return decrypted_data
except Exception as e:
return None
else:
return None

View File

@@ -71,8 +71,8 @@ class SystemUtils:
""" """
return True if platform.machine() == 'aarch64' else False return True if platform.machine() == 'aarch64' else False
@property @staticmethod
def platform(self) -> str: def platform() -> str:
""" """
获取系统平台 获取系统平台
""" """

View File

@@ -43,4 +43,3 @@ DOWNLOAD_SUBTITLE=true
OCR_HOST=https://movie-pilot.org OCR_HOST=https://movie-pilot.org
# 插件市场仓库地址,多个地址使用`,`分隔,保留最后的/ # 插件市场仓库地址,多个地址使用`,`分隔,保留最后的/
PLUGIN_MARKET=https://github.com/jxxghp/MoviePilot-Plugins PLUGIN_MARKET=https://github.com/jxxghp/MoviePilot-Plugins

View File

@@ -0,0 +1,34 @@
"""1.0.15
Revision ID: 5813aaa7cb3a
Revises: f94cd1217fd7
Create Date: 2024-03-17 09:04:51.785716
"""
import contextlib
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '5813aaa7cb3a'
down_revision = 'f94cd1217fd7'
branch_labels = None
depends_on = None
def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
with contextlib.suppress(Exception):
with op.batch_alter_table("message") as batch_op:
batch_op.add_column(sa.Column('note', sa.String, nullable=True))
try:
op.create_index('ix_message_reg_time', 'message', ['reg_time'], unique=False)
except Exception as err:
pass
# ### end Alembic commands ###
def downgrade() -> None:
pass

View File

@@ -28,4 +28,4 @@ def upgrade() -> None:
def downgrade() -> None: def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ### # ### commands auto generated by Alembic - please adjust! ###
pass pass

View File

@@ -1 +1 @@
APP_VERSION = 'v1.7.2' APP_VERSION = 'v1.7.3'