mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-07-05 14:51:28 +08:00
Compare commits
89 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8e309e8658 | ||
|
|
3400a9f87a | ||
|
|
c6830059b2 | ||
|
|
7e4a18b365 | ||
|
|
9ecc8c14d8 | ||
|
|
91ba71ad23 | ||
|
|
5ae8914060 | ||
|
|
77c8f1244f | ||
|
|
5d5c8a0af7 | ||
|
|
dcaf3e6678 | ||
|
|
c0170a173c | ||
|
|
d182a7079d | ||
|
|
b5cc5653b2 | ||
|
|
bdbd908b3a | ||
|
|
11fedb1ffc | ||
|
|
7de82f6c0d | ||
|
|
782829c992 | ||
|
|
6ab76453d4 | ||
|
|
56767b92d7 | ||
|
|
621df40c66 | ||
|
|
ba7cb76640 | ||
|
|
d353853472 | ||
|
|
1fcf5f4709 | ||
|
|
0ec4630461 | ||
|
|
fa45dea1aa | ||
|
|
2217583052 | ||
|
|
f4dc7a133e | ||
|
|
26b1e64bad | ||
|
|
a1d8af6521 | ||
|
|
9fb3d093ff | ||
|
|
8c9b37a12f | ||
|
|
73e4596d1a | ||
|
|
83798e6823 | ||
|
|
6d9595b643 | ||
|
|
dc047d949d | ||
|
|
a31b4bc0a1 | ||
|
|
94b8633803 | ||
|
|
107e85033f | ||
|
|
eea8060182 | ||
|
|
83f7869de4 | ||
|
|
4f0eff8b88 | ||
|
|
58b438c345 | ||
|
|
bc57bb1a78 | ||
|
|
e08ab0dd33 | ||
|
|
64bfa246ae | ||
|
|
cde4db1a56 | ||
|
|
29ae910953 | ||
|
|
314f90cc40 | ||
|
|
1c22e3d024 | ||
|
|
233d62479f | ||
|
|
6974f2ebd7 | ||
|
|
c030166cf5 | ||
|
|
4c511eaea6 | ||
|
|
6e443a1127 | ||
|
|
896e473c41 | ||
|
|
12f10ebedf | ||
|
|
ba9f85747c | ||
|
|
2954c02a7c | ||
|
|
312e602f12 | ||
|
|
ed37fcbb07 | ||
|
|
6acf8fbf00 | ||
|
|
a1e178c805 | ||
|
|
922e2fc446 | ||
|
|
db4c8cb3f2 | ||
|
|
1c578746fe | ||
|
|
68f88117b6 | ||
|
|
108c0a89f6 | ||
|
|
92dacdf6a2 | ||
|
|
6aa684d6a5 | ||
|
|
efece8cc56 | ||
|
|
383c8ca19a | ||
|
|
0a73681280 | ||
|
|
c1ecda280c | ||
|
|
825fc35134 | ||
|
|
8f543ca602 | ||
|
|
f0ecc1a497 | ||
|
|
71f170a1ad | ||
|
|
3709b65b0e | ||
|
|
9d6eb0f1e1 | ||
|
|
c93306147b | ||
|
|
5e8f924a2f | ||
|
|
54988d6397 | ||
|
|
112761dc4c | ||
|
|
ef20508840 | ||
|
|
589a1765ed | ||
|
|
2c666e24f3 | ||
|
|
168e3c5533 | ||
|
|
cda8b2573a | ||
|
|
4cb4eb23b8 |
3
.github/workflows/build.yml
vendored
3
.github/workflows/build.yml
vendored
@@ -5,10 +5,7 @@ on:
|
|||||||
branches:
|
branches:
|
||||||
- v2
|
- v2
|
||||||
paths:
|
paths:
|
||||||
- '.github/workflows/build.yml'
|
|
||||||
- 'Dockerfile'
|
|
||||||
- 'version.py'
|
- 'version.py'
|
||||||
- 'requirements.in'
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
Docker-build:
|
Docker-build:
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ def download(
|
|||||||
torrent_info=torrentinfo
|
torrent_info=torrentinfo
|
||||||
)
|
)
|
||||||
did = DownloadChain().download_single(context=context, username=current_user.name,
|
did = DownloadChain().download_single(context=context, username=current_user.name,
|
||||||
downloader=downloader, save_path=save_path)
|
downloader=downloader, save_path=save_path, source="Manual")
|
||||||
if not did:
|
if not did:
|
||||||
return schemas.Response(success=False, message="任务添加失败")
|
return schemas.Response(success=False, message="任务添加失败")
|
||||||
return schemas.Response(success=True, data={
|
return schemas.Response(success=True, data={
|
||||||
@@ -84,7 +84,7 @@ def add(
|
|||||||
torrent_info=torrentinfo
|
torrent_info=torrentinfo
|
||||||
)
|
)
|
||||||
did = DownloadChain().download_single(context=context, username=current_user.name,
|
did = DownloadChain().download_single(context=context, username=current_user.name,
|
||||||
downloader=downloader, save_path=save_path)
|
downloader=downloader, save_path=save_path, source="Manual")
|
||||||
if not did:
|
if not did:
|
||||||
return schemas.Response(success=False, message="任务添加失败")
|
return schemas.Response(success=False, message="任务添加失败")
|
||||||
return schemas.Response(success=True, data={
|
return schemas.Response(success=True, data={
|
||||||
|
|||||||
@@ -111,7 +111,7 @@ def scrape(fileitem: schemas.FileItem,
|
|||||||
scrape_path = Path(fileitem.path)
|
scrape_path = Path(fileitem.path)
|
||||||
meta = MetaInfoPath(scrape_path)
|
meta = MetaInfoPath(scrape_path)
|
||||||
mediainfo = chain.recognize_by_meta(meta)
|
mediainfo = chain.recognize_by_meta(meta)
|
||||||
if not media_info:
|
if not mediainfo:
|
||||||
return schemas.Response(success=False, message="刮削失败,无法识别媒体信息")
|
return schemas.Response(success=False, message="刮削失败,无法识别媒体信息")
|
||||||
if storage == "local":
|
if storage == "local":
|
||||||
if not scrape_path.exists():
|
if not scrape_path.exists():
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ from app import schemas
|
|||||||
from app.chain.subscribe import SubscribeChain
|
from app.chain.subscribe import SubscribeChain
|
||||||
from app.core.config import settings
|
from app.core.config import settings
|
||||||
from app.core.context import MediaInfo
|
from app.core.context import MediaInfo
|
||||||
|
from app.core.event import eventmanager
|
||||||
from app.core.metainfo import MetaInfo
|
from app.core.metainfo import MetaInfo
|
||||||
from app.core.security import verify_token, verify_apitoken
|
from app.core.security import verify_token, verify_apitoken
|
||||||
from app.db import get_db
|
from app.db import get_db
|
||||||
@@ -17,7 +18,7 @@ from app.db.models.user import User
|
|||||||
from app.db.user_oper import get_current_active_user
|
from app.db.user_oper import get_current_active_user
|
||||||
from app.helper.subscribe import SubscribeHelper
|
from app.helper.subscribe import SubscribeHelper
|
||||||
from app.scheduler import Scheduler
|
from app.scheduler import Scheduler
|
||||||
from app.schemas.types import MediaType
|
from app.schemas.types import MediaType, EventType
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
@@ -124,6 +125,27 @@ def update_subscribe(
|
|||||||
return schemas.Response(success=True)
|
return schemas.Response(success=True)
|
||||||
|
|
||||||
|
|
||||||
|
@router.put("/status/{subid}", summary="更新订阅状态", response_model=schemas.Response)
|
||||||
|
def update_subscribe_status(
|
||||||
|
subid: int,
|
||||||
|
state: str,
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
|
||||||
|
"""
|
||||||
|
更新订阅状态
|
||||||
|
"""
|
||||||
|
subscribe = Subscribe.get(db, subid)
|
||||||
|
if not subscribe:
|
||||||
|
return schemas.Response(success=False, message="订阅不存在")
|
||||||
|
valid_states = ["R", "P", "S"]
|
||||||
|
if state not in valid_states:
|
||||||
|
return schemas.Response(success=False, message="无效的订阅状态")
|
||||||
|
subscribe.update(db, {
|
||||||
|
"state": state
|
||||||
|
})
|
||||||
|
return schemas.Response(success=True)
|
||||||
|
|
||||||
|
|
||||||
@router.get("/media/{mediaid}", summary="查询订阅", response_model=schemas.Subscribe)
|
@router.get("/media/{mediaid}", summary="查询订阅", response_model=schemas.Subscribe)
|
||||||
def subscribe_mediaid(
|
def subscribe_mediaid(
|
||||||
mediaid: str,
|
mediaid: str,
|
||||||
@@ -186,8 +208,9 @@ def reset_subscribes(
|
|||||||
subscribe = Subscribe.get(db, subid)
|
subscribe = Subscribe.get(db, subid)
|
||||||
if subscribe:
|
if subscribe:
|
||||||
subscribe.update(db, {
|
subscribe.update(db, {
|
||||||
"note": "",
|
"note": [],
|
||||||
"lack_episode": subscribe.total_episode
|
"lack_episode": subscribe.total_episode,
|
||||||
|
"state": "R"
|
||||||
})
|
})
|
||||||
return schemas.Response(success=True)
|
return schemas.Response(success=True)
|
||||||
return schemas.Response(success=False, message="订阅不存在")
|
return schemas.Response(success=False, message="订阅不存在")
|
||||||
@@ -252,17 +275,27 @@ def delete_subscribe_by_mediaid(
|
|||||||
"""
|
"""
|
||||||
根据TMDBID或豆瓣ID删除订阅 tmdb:/douban:
|
根据TMDBID或豆瓣ID删除订阅 tmdb:/douban:
|
||||||
"""
|
"""
|
||||||
|
delete_subscribes = []
|
||||||
if mediaid.startswith("tmdb:"):
|
if mediaid.startswith("tmdb:"):
|
||||||
tmdbid = mediaid[5:]
|
tmdbid = mediaid[5:]
|
||||||
if not tmdbid or not str(tmdbid).isdigit():
|
if not tmdbid or not str(tmdbid).isdigit():
|
||||||
return schemas.Response(success=False)
|
return schemas.Response(success=False)
|
||||||
Subscribe().delete_by_tmdbid(db, int(tmdbid), season)
|
subscribes = Subscribe().get_by_tmdbid(db, int(tmdbid), season)
|
||||||
|
delete_subscribes.extend(subscribes)
|
||||||
elif mediaid.startswith("douban:"):
|
elif mediaid.startswith("douban:"):
|
||||||
doubanid = mediaid[7:]
|
doubanid = mediaid[7:]
|
||||||
if not doubanid:
|
if not doubanid:
|
||||||
return schemas.Response(success=False)
|
return schemas.Response(success=False)
|
||||||
Subscribe().delete_by_doubanid(db, doubanid)
|
subscribe = Subscribe().get_by_doubanid(db, doubanid)
|
||||||
|
if subscribe:
|
||||||
|
delete_subscribes.append(subscribe)
|
||||||
|
for subscribe in delete_subscribes:
|
||||||
|
Subscribe().delete(db, subscribe.id)
|
||||||
|
# 发送事件
|
||||||
|
eventmanager.send_event(EventType.SubscribeDeleted, {
|
||||||
|
"subscribe_id": subscribe.id,
|
||||||
|
"subscribe": subscribe.to_dict()
|
||||||
|
})
|
||||||
return schemas.Response(success=True)
|
return schemas.Response(success=True)
|
||||||
|
|
||||||
|
|
||||||
@@ -485,6 +518,11 @@ def delete_subscribe(
|
|||||||
subscribe = Subscribe.get(db, subscribe_id)
|
subscribe = Subscribe.get(db, subscribe_id)
|
||||||
if subscribe:
|
if subscribe:
|
||||||
subscribe.delete(db, subscribe_id)
|
subscribe.delete(db, subscribe_id)
|
||||||
|
# 发送事件
|
||||||
|
eventmanager.send_event(EventType.SubscribeDeleted, {
|
||||||
|
"subscribe_id": subscribe_id,
|
||||||
|
"subscribe": subscribe.to_dict()
|
||||||
|
})
|
||||||
# 统计订阅
|
# 统计订阅
|
||||||
SubscribeHelper().sub_done_async({
|
SubscribeHelper().sub_done_async({
|
||||||
"tmdbid": subscribe.tmdbid,
|
"tmdbid": subscribe.tmdbid,
|
||||||
|
|||||||
@@ -388,6 +388,8 @@ def ruletest(title: str,
|
|||||||
|
|
||||||
# 根据标题查询媒体信息
|
# 根据标题查询媒体信息
|
||||||
media_info = SearchChain().recognize_media(MetaInfo(title=title, subtitle=subtitle))
|
media_info = SearchChain().recognize_media(MetaInfo(title=title, subtitle=subtitle))
|
||||||
|
if not media_info:
|
||||||
|
return schemas.Response(success=False, message="未识别到媒体信息!")
|
||||||
|
|
||||||
# 过滤
|
# 过滤
|
||||||
result = SearchChain().filter_torrents(rule_groups=[rulegroup.name],
|
result = SearchChain().filter_torrents(rule_groups=[rulegroup.name],
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, Optional
|
from typing import Any
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends
|
from fastapi import APIRouter, Depends
|
||||||
from pydantic import BaseModel
|
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
from app import schemas
|
from app import schemas
|
||||||
@@ -14,50 +13,11 @@ from app.core.security import verify_token, verify_apitoken
|
|||||||
from app.db import get_db
|
from app.db import get_db
|
||||||
from app.db.models.transferhistory import TransferHistory
|
from app.db.models.transferhistory import TransferHistory
|
||||||
from app.db.user_oper import get_current_active_superuser
|
from app.db.user_oper import get_current_active_superuser
|
||||||
from app.schemas import MediaType, FileItem
|
from app.schemas import MediaType, FileItem, ManualTransferItem
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
class ManualTransferItem(BaseModel):
|
|
||||||
# 文件项
|
|
||||||
fileitem: FileItem = None
|
|
||||||
# 日志ID
|
|
||||||
logid: Optional[int] = None
|
|
||||||
# 目标存储
|
|
||||||
target_storage: Optional[str] = None
|
|
||||||
# 目标路径
|
|
||||||
target_path: Optional[str] = None
|
|
||||||
# TMDB ID
|
|
||||||
tmdbid: Optional[int] = None
|
|
||||||
# 豆瓣ID
|
|
||||||
doubanid: Optional[str] = None
|
|
||||||
# 类型
|
|
||||||
type_name: Optional[str] = None
|
|
||||||
# 季号
|
|
||||||
season: Optional[int] = None
|
|
||||||
# 整理方式
|
|
||||||
transfer_type: Optional[str] = None
|
|
||||||
# 自定义格式
|
|
||||||
episode_format: Optional[str] = None
|
|
||||||
# 指定集数
|
|
||||||
episode_detail: Optional[str] = None
|
|
||||||
# 指定PART
|
|
||||||
episode_part: Optional[str] = None
|
|
||||||
# 集数偏移
|
|
||||||
episode_offset: Optional[str] = None
|
|
||||||
# 最小文件大小
|
|
||||||
min_filesize: Optional[int] = 0
|
|
||||||
# 刮削
|
|
||||||
scrape: bool = False
|
|
||||||
# 媒体库类型子目录
|
|
||||||
library_type_folder: Optional[bool] = None
|
|
||||||
# 媒体库类别子目录
|
|
||||||
library_category_folder: Optional[bool] = None
|
|
||||||
# 复用历史识别信息
|
|
||||||
from_history: Optional[bool] = False
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/name", summary="查询整理后的名称", response_model=schemas.Response)
|
@router.get("/name", summary="查询整理后的名称", response_model=schemas.Response)
|
||||||
def query_name(path: str, filetype: str,
|
def query_name(path: str, filetype: str,
|
||||||
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
|
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
|
||||||
|
|||||||
@@ -20,7 +20,8 @@ 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 ExistMediaInfo, NotExistMediaInfo, DownloadingTorrent, Notification
|
from app.schemas import ExistMediaInfo, NotExistMediaInfo, DownloadingTorrent, Notification
|
||||||
from app.schemas.types import MediaType, TorrentStatus, EventType, MessageChannel, NotificationType
|
from app.schemas.event import ResourceSelectionEventData, ResourceDownloadEventData
|
||||||
|
from app.schemas.types import MediaType, TorrentStatus, EventType, MessageChannel, NotificationType, ChainEventType
|
||||||
from app.utils.http import RequestUtils
|
from app.utils.http import RequestUtils
|
||||||
from app.utils.string import StringUtils
|
from app.utils.string import StringUtils
|
||||||
|
|
||||||
@@ -191,7 +192,7 @@ class DownloadChain(ChainBase):
|
|||||||
logger.error(f"下载种子文件失败:{torrent.title} - {torrent_url}")
|
logger.error(f"下载种子文件失败:{torrent.title} - {torrent_url}")
|
||||||
self.post_message(Notification(
|
self.post_message(Notification(
|
||||||
channel=channel,
|
channel=channel,
|
||||||
source=source,
|
source=source if channel else None,
|
||||||
mtype=NotificationType.Manual,
|
mtype=NotificationType.Manual,
|
||||||
title=f"{torrent.title} 种子下载失败!",
|
title=f"{torrent.title} 种子下载失败!",
|
||||||
text=f"错误信息:{error_msg}\n站点:{torrent.site_name}",
|
text=f"错误信息:{error_msg}\n站点:{torrent.site_name}",
|
||||||
@@ -203,7 +204,8 @@ class DownloadChain(ChainBase):
|
|||||||
|
|
||||||
def download_single(self, context: Context, torrent_file: Path = None,
|
def download_single(self, context: Context, torrent_file: Path = None,
|
||||||
episodes: Set[int] = None,
|
episodes: Set[int] = None,
|
||||||
channel: MessageChannel = None, source: str = None,
|
channel: MessageChannel = None,
|
||||||
|
source: str = None,
|
||||||
downloader: str = None,
|
downloader: str = None,
|
||||||
save_path: str = None,
|
save_path: str = None,
|
||||||
userid: Union[str, int] = None,
|
userid: Union[str, int] = None,
|
||||||
@@ -215,13 +217,38 @@ class DownloadChain(ChainBase):
|
|||||||
:param torrent_file: 种子文件路径
|
:param torrent_file: 种子文件路径
|
||||||
:param episodes: 需要下载的集数
|
:param episodes: 需要下载的集数
|
||||||
:param channel: 通知渠道
|
:param channel: 通知渠道
|
||||||
:param source: 通知来源
|
:param source: 来源(消息通知、Subscribe、Manual等)
|
||||||
:param downloader: 下载器
|
:param downloader: 下载器
|
||||||
:param save_path: 保存路径
|
:param save_path: 保存路径
|
||||||
:param userid: 用户ID
|
:param userid: 用户ID
|
||||||
:param username: 调用下载的用户名/插件名
|
:param username: 调用下载的用户名/插件名
|
||||||
:param media_category: 自定义媒体类别
|
:param media_category: 自定义媒体类别
|
||||||
"""
|
"""
|
||||||
|
# 发送资源下载事件,允许外部拦截下载
|
||||||
|
event_data = ResourceDownloadEventData(
|
||||||
|
context=context,
|
||||||
|
episodes=episodes or context.meta_info.episode_list,
|
||||||
|
channel=channel,
|
||||||
|
origin=source,
|
||||||
|
downloader=downloader,
|
||||||
|
options={
|
||||||
|
"save_path": save_path,
|
||||||
|
"userid": userid,
|
||||||
|
"username": username,
|
||||||
|
"media_category": media_category
|
||||||
|
}
|
||||||
|
)
|
||||||
|
# 触发资源下载事件
|
||||||
|
event = eventmanager.send_event(ChainEventType.ResourceDownload, event_data)
|
||||||
|
if event and event.event_data:
|
||||||
|
event_data: ResourceDownloadEventData = event.event_data
|
||||||
|
# 如果事件被取消,跳过资源下载
|
||||||
|
if event_data.cancel:
|
||||||
|
logger.debug(
|
||||||
|
f"Resource download canceled by event: {event_data.source},"
|
||||||
|
f"Reason: {event_data.reason}")
|
||||||
|
return None
|
||||||
|
|
||||||
_torrent = context.torrent_info
|
_torrent = context.torrent_info
|
||||||
_media = context.media_info
|
_media = context.media_info
|
||||||
_meta = context.meta_info
|
_meta = context.meta_info
|
||||||
@@ -318,7 +345,8 @@ class DownloadChain(ChainBase):
|
|||||||
username=username,
|
username=username,
|
||||||
channel=channel.value if channel else None,
|
channel=channel.value if channel else None,
|
||||||
date=time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()),
|
date=time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()),
|
||||||
media_category=media_category
|
media_category=media_category,
|
||||||
|
note={"source": source}
|
||||||
)
|
)
|
||||||
|
|
||||||
# 登记下载文件
|
# 登记下载文件
|
||||||
@@ -355,7 +383,9 @@ class DownloadChain(ChainBase):
|
|||||||
"hash": _hash,
|
"hash": _hash,
|
||||||
"context": context,
|
"context": context,
|
||||||
"username": username,
|
"username": username,
|
||||||
"downloader": _downloader
|
"downloader": _downloader,
|
||||||
|
"episodes": episodes or _meta.episode_list,
|
||||||
|
"source": source
|
||||||
})
|
})
|
||||||
else:
|
else:
|
||||||
# 下载失败
|
# 下载失败
|
||||||
@@ -364,7 +394,7 @@ class DownloadChain(ChainBase):
|
|||||||
# 只发送给对应渠道和用户
|
# 只发送给对应渠道和用户
|
||||||
self.post_message(Notification(
|
self.post_message(Notification(
|
||||||
channel=channel,
|
channel=channel,
|
||||||
source=source,
|
source=source if channel else None,
|
||||||
mtype=NotificationType.Manual,
|
mtype=NotificationType.Manual,
|
||||||
title="添加下载任务失败:%s %s"
|
title="添加下载任务失败:%s %s"
|
||||||
% (_media.title_year, _meta.season_episode),
|
% (_media.title_year, _meta.season_episode),
|
||||||
@@ -392,7 +422,7 @@ class DownloadChain(ChainBase):
|
|||||||
:param no_exists: 缺失的剧集信息
|
:param no_exists: 缺失的剧集信息
|
||||||
:param save_path: 保存路径
|
:param save_path: 保存路径
|
||||||
:param channel: 通知渠道
|
:param channel: 通知渠道
|
||||||
:param source: 通知来源
|
:param source: 来源(消息通知、订阅、手工下载等)
|
||||||
:param userid: 用户ID
|
:param userid: 用户ID
|
||||||
:param username: 调用下载的用户名/插件名
|
:param username: 调用下载的用户名/插件名
|
||||||
:param media_category: 自定义媒体类别
|
:param media_category: 自定义媒体类别
|
||||||
@@ -457,6 +487,22 @@ class DownloadChain(ChainBase):
|
|||||||
return 9999
|
return 9999
|
||||||
return no_exist[season].total_episode
|
return no_exist[season].total_episode
|
||||||
|
|
||||||
|
# 发送资源选择事件,允许外部修改上下文数据
|
||||||
|
logger.debug(f"Initial contexts: {len(contexts)} items, Downloader: {downloader}")
|
||||||
|
event_data = ResourceSelectionEventData(
|
||||||
|
contexts=contexts,
|
||||||
|
downloader=downloader,
|
||||||
|
origin=source
|
||||||
|
)
|
||||||
|
event = eventmanager.send_event(ChainEventType.ResourceSelection, event_data)
|
||||||
|
# 如果事件修改了上下文数据,使用更新后的数据
|
||||||
|
if event and event.event_data:
|
||||||
|
event_data: ResourceSelectionEventData = event.event_data
|
||||||
|
if event_data.updated and event_data.updated_contexts is not None:
|
||||||
|
logger.debug(f"Contexts updated by event: "
|
||||||
|
f"{len(event_data.updated_contexts)} items (source: {event_data.source})")
|
||||||
|
contexts = event_data.updated_contexts
|
||||||
|
|
||||||
# 分组排序
|
# 分组排序
|
||||||
contexts = TorrentHelper().sort_group_torrents(contexts)
|
contexts = TorrentHelper().sort_group_torrents(contexts)
|
||||||
|
|
||||||
|
|||||||
@@ -477,7 +477,7 @@ class MediaChain(ChainBase, metaclass=Singleton):
|
|||||||
if not file_meta.begin_episode:
|
if not file_meta.begin_episode:
|
||||||
logger.warn(f"{filepath.name} 无法识别文件集数!")
|
logger.warn(f"{filepath.name} 无法识别文件集数!")
|
||||||
return
|
return
|
||||||
file_mediainfo = self.recognize_media(meta=file_meta)
|
file_mediainfo = self.recognize_media(meta=file_meta, tmdbid=mediainfo.tmdb_id)
|
||||||
if not file_mediainfo:
|
if not file_mediainfo:
|
||||||
logger.warn(f"{filepath.name} 无法识别文件媒体信息!")
|
logger.warn(f"{filepath.name} 无法识别文件媒体信息!")
|
||||||
return
|
return
|
||||||
@@ -552,6 +552,29 @@ class MediaChain(ChainBase, metaclass=Singleton):
|
|||||||
if not parent:
|
if not parent:
|
||||||
parent = self.storagechain.get_parent_item(fileitem)
|
parent = self.storagechain.get_parent_item(fileitem)
|
||||||
__save_file(_fileitem=parent, _path=image_path, _content=content)
|
__save_file(_fileitem=parent, _path=image_path, _content=content)
|
||||||
|
# 额外fanart季图片:poster thumb banner
|
||||||
|
image_dict = self.metadata_img(mediainfo=mediainfo)
|
||||||
|
if image_dict:
|
||||||
|
for image_name, image_url in image_dict.items():
|
||||||
|
if image_name.startswith("season"):
|
||||||
|
image_path = filepath.with_name(image_name)
|
||||||
|
# 只下载当前刮削季的图片
|
||||||
|
image_season = "00" if "specials" in image_name else image_name[6:8]
|
||||||
|
if image_season != str(season_meta.begin_season).rjust(2, '0'):
|
||||||
|
logger.info(f"当前刮削季为:{season_meta.begin_season},跳过文件:{image_path}")
|
||||||
|
continue
|
||||||
|
if not overwrite and self.storagechain.get_file_item(storage=fileitem.storage,
|
||||||
|
path=image_path):
|
||||||
|
logger.info(f"已存在图片文件:{image_path}")
|
||||||
|
continue
|
||||||
|
# 下载图片
|
||||||
|
content = __download_image(image_url)
|
||||||
|
# 保存图片文件到当前目录
|
||||||
|
if content:
|
||||||
|
if not parent:
|
||||||
|
parent = self.storagechain.get_parent_item(fileitem)
|
||||||
|
__save_file(_fileitem=parent, _path=image_path, _content=content)
|
||||||
|
|
||||||
# 判断当前目录是不是剧集根目录
|
# 判断当前目录是不是剧集根目录
|
||||||
if not season_meta.season:
|
if not season_meta.season:
|
||||||
# 是否已存在
|
# 是否已存在
|
||||||
@@ -570,6 +593,9 @@ class MediaChain(ChainBase, metaclass=Singleton):
|
|||||||
image_dict = self.metadata_img(mediainfo=mediainfo)
|
image_dict = self.metadata_img(mediainfo=mediainfo)
|
||||||
if image_dict:
|
if image_dict:
|
||||||
for image_name, image_url in image_dict.items():
|
for image_name, image_url in image_dict.items():
|
||||||
|
# 不下载季图片
|
||||||
|
if image_name.startswith("season"):
|
||||||
|
continue
|
||||||
image_path = filepath / image_name
|
image_path = filepath / image_name
|
||||||
if not overwrite and self.storagechain.get_file_item(storage=fileitem.storage,
|
if not overwrite and self.storagechain.get_file_item(storage=fileitem.storage,
|
||||||
path=image_path):
|
path=image_path):
|
||||||
|
|||||||
@@ -111,6 +111,8 @@ class MessageChain(ChainBase):
|
|||||||
info = self.message_parser(source=source, body=body, form=form, args=args)
|
info = self.message_parser(source=source, body=body, form=form, args=args)
|
||||||
if not info:
|
if not info:
|
||||||
return
|
return
|
||||||
|
# 更新消息来源
|
||||||
|
source = info.source
|
||||||
# 渠道
|
# 渠道
|
||||||
channel = info.channel
|
channel = info.channel
|
||||||
# 用户ID
|
# 用户ID
|
||||||
|
|||||||
@@ -87,7 +87,8 @@ class SiteChain(ChainBase):
|
|||||||
link=site.get("url")
|
link=site.get("url")
|
||||||
))
|
))
|
||||||
# 低分享率警告
|
# 低分享率警告
|
||||||
if userdata.ratio and float(userdata.ratio) < 1:
|
if userdata.ratio and float(userdata.ratio) < 1 and not bool(
|
||||||
|
re.search(r"(贵宾|VIP?)", userdata.user_level, re.IGNORECASE)):
|
||||||
self.post_message(Notification(
|
self.post_message(Notification(
|
||||||
mtype=NotificationType.SiteMessage,
|
mtype=NotificationType.SiteMessage,
|
||||||
title=f"【站点分享率低预警】",
|
title=f"【站点分享率低预警】",
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import copy
|
import copy
|
||||||
|
import json
|
||||||
import random
|
import random
|
||||||
|
import threading
|
||||||
import time
|
import time
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import Dict, List, Optional, Union, Tuple
|
from typing import Dict, List, Optional, Union, Tuple
|
||||||
@@ -28,15 +30,17 @@ from app.log import logger
|
|||||||
from app.schemas import NotExistMediaInfo, Notification, SubscrbieInfo, SubscribeEpisodeInfo, SubscribeDownloadFileInfo, \
|
from app.schemas import NotExistMediaInfo, Notification, SubscrbieInfo, SubscribeEpisodeInfo, SubscribeDownloadFileInfo, \
|
||||||
SubscribeLibraryFileInfo
|
SubscribeLibraryFileInfo
|
||||||
from app.schemas.types import MediaType, SystemConfigKey, MessageChannel, NotificationType, EventType
|
from app.schemas.types import MediaType, SystemConfigKey, MessageChannel, NotificationType, EventType
|
||||||
|
from app.utils.singleton import Singleton
|
||||||
|
|
||||||
|
|
||||||
class SubscribeChain(ChainBase):
|
class SubscribeChain(ChainBase, metaclass=Singleton):
|
||||||
"""
|
"""
|
||||||
订阅管理处理链
|
订阅管理处理链
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
|
self._rlock = threading.RLock()
|
||||||
self.downloadchain = DownloadChain()
|
self.downloadchain = DownloadChain()
|
||||||
self.downloadhis = DownloadHistoryOper()
|
self.downloadhis = DownloadHistoryOper()
|
||||||
self.searchchain = SearchChain()
|
self.searchchain = SearchChain()
|
||||||
@@ -234,14 +238,17 @@ class SubscribeChain(ChainBase):
|
|||||||
"""
|
"""
|
||||||
订阅搜索
|
订阅搜索
|
||||||
:param sid: 订阅ID,有值时只处理该订阅
|
:param sid: 订阅ID,有值时只处理该订阅
|
||||||
:param state: 订阅状态 N:未搜索 R:已搜索
|
:param state: 订阅状态 N:新建, R:订阅中, P:待定, S:暂停
|
||||||
:param manual: 是否手动搜索
|
:param manual: 是否手动搜索
|
||||||
:return: 更新订阅状态为R或删除订阅
|
:return: 更新订阅状态为R或删除订阅
|
||||||
"""
|
"""
|
||||||
|
with self._rlock:
|
||||||
|
logger.debug(f"search lock acquired at {datetime.now()}")
|
||||||
if sid:
|
if sid:
|
||||||
subscribes = [self.subscribeoper.get(sid)]
|
subscribe = self.subscribeoper.get(sid)
|
||||||
|
subscribes = [subscribe] if subscribe else []
|
||||||
else:
|
else:
|
||||||
subscribes = self.subscribeoper.list(state)
|
subscribes = self.subscribeoper.list(self.get_states_for_search(state))
|
||||||
# 遍历订阅
|
# 遍历订阅
|
||||||
for subscribe in subscribes:
|
for subscribe in subscribes:
|
||||||
if global_vars.is_system_stopped:
|
if global_vars.is_system_stopped:
|
||||||
@@ -256,14 +263,12 @@ class SubscribeChain(ChainBase):
|
|||||||
logger.debug(f"订阅标题:{subscribe.name} 新增小于1分钟,暂不搜索...")
|
logger.debug(f"订阅标题:{subscribe.name} 新增小于1分钟,暂不搜索...")
|
||||||
continue
|
continue
|
||||||
# 随机休眠1-5分钟
|
# 随机休眠1-5分钟
|
||||||
if not sid and state == 'R':
|
if not sid and state in ['R', 'P']:
|
||||||
sleep_time = random.randint(60, 300)
|
sleep_time = random.randint(60, 300)
|
||||||
logger.info(f'订阅搜索随机休眠 {sleep_time} 秒 ...')
|
logger.info(f'订阅搜索随机休眠 {sleep_time} 秒 ...')
|
||||||
time.sleep(sleep_time)
|
time.sleep(sleep_time)
|
||||||
|
try:
|
||||||
logger.info(f'开始搜索订阅,标题:{subscribe.name} ...')
|
logger.info(f'开始搜索订阅,标题:{subscribe.name} ...')
|
||||||
# 如果状态为N则更新为R
|
|
||||||
if subscribe.state == 'N':
|
|
||||||
self.subscribeoper.update(subscribe.id, {'state': 'R'})
|
|
||||||
# 生成元数据
|
# 生成元数据
|
||||||
meta = MetaInfo(subscribe.name)
|
meta = MetaInfo(subscribe.name)
|
||||||
meta.year = subscribe.year
|
meta.year = subscribe.year
|
||||||
@@ -283,65 +288,23 @@ class SubscribeChain(ChainBase):
|
|||||||
f'未识别到媒体信息,标题:{subscribe.name},tmdbid:{subscribe.tmdbid},doubanid:{subscribe.doubanid}')
|
f'未识别到媒体信息,标题:{subscribe.name},tmdbid:{subscribe.tmdbid},doubanid:{subscribe.doubanid}')
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# 非洗版状态
|
# 如果媒体已存在或已下载完毕,跳过当前订阅处理
|
||||||
if not subscribe.best_version:
|
exist_flag, no_exists = self.check_and_handle_existing_media(subscribe=subscribe, meta=meta,
|
||||||
# 每季总集数
|
|
||||||
totals = {}
|
|
||||||
if subscribe.season and subscribe.total_episode:
|
|
||||||
totals = {
|
|
||||||
subscribe.season: subscribe.total_episode
|
|
||||||
}
|
|
||||||
# 查询媒体库缺失的媒体信息
|
|
||||||
exist_flag, no_exists = self.downloadchain.get_no_exists_info(
|
|
||||||
meta=meta,
|
|
||||||
mediainfo=mediainfo,
|
mediainfo=mediainfo,
|
||||||
totals=totals
|
mediakey=mediakey)
|
||||||
)
|
|
||||||
else:
|
|
||||||
# 洗版状态
|
|
||||||
exist_flag = False
|
|
||||||
if meta.type == MediaType.TV:
|
|
||||||
no_exists = {
|
|
||||||
mediakey: {
|
|
||||||
subscribe.season: NotExistMediaInfo(
|
|
||||||
season=subscribe.season,
|
|
||||||
episodes=[],
|
|
||||||
total_episode=subscribe.total_episode,
|
|
||||||
start_episode=subscribe.start_episode or 1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else:
|
|
||||||
no_exists = {}
|
|
||||||
|
|
||||||
# 已存在
|
|
||||||
if exist_flag:
|
if exist_flag:
|
||||||
logger.info(f'{mediainfo.title_year} 媒体库中已存在')
|
|
||||||
self.finish_subscribe_or_not(subscribe=subscribe, meta=meta, mediainfo=mediainfo, force=True)
|
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# 电视剧订阅处理缺失集
|
|
||||||
if meta.type == MediaType.TV:
|
|
||||||
# 实际缺失集与订阅开始结束集范围进行整合,同时剔除已下载的集数
|
|
||||||
no_exists = self.__get_subscribe_no_exits(
|
|
||||||
subscribe_name=f'{subscribe.name} {meta.season}',
|
|
||||||
no_exists=no_exists,
|
|
||||||
mediakey=mediakey,
|
|
||||||
begin_season=meta.begin_season,
|
|
||||||
total_episode=subscribe.total_episode,
|
|
||||||
start_episode=subscribe.start_episode,
|
|
||||||
downloaded_episodes=self.__get_downloaded_episodes(subscribe)
|
|
||||||
)
|
|
||||||
|
|
||||||
# 站点范围
|
# 站点范围
|
||||||
sites = self.get_sub_sites(subscribe)
|
sites = self.get_sub_sites(subscribe)
|
||||||
|
|
||||||
# 优先级过滤规则
|
# 优先级过滤规则
|
||||||
if subscribe.best_version:
|
if subscribe.best_version:
|
||||||
rule_groups = subscribe.filter_groups \
|
rule_groups = subscribe.filter_groups \
|
||||||
or self.systemconfig.get(SystemConfigKey.BestVersionFilterRuleGroups)
|
or self.systemconfig.get(SystemConfigKey.BestVersionFilterRuleGroups) or []
|
||||||
else:
|
else:
|
||||||
rule_groups = subscribe.filter_groups \
|
rule_groups = subscribe.filter_groups \
|
||||||
or self.systemconfig.get(SystemConfigKey.SubscribeFilterRuleGroups)
|
or self.systemconfig.get(SystemConfigKey.SubscribeFilterRuleGroups) or []
|
||||||
|
|
||||||
# 搜索,同时电视剧会过滤掉不需要的剧集
|
# 搜索,同时电视剧会过滤掉不需要的剧集
|
||||||
contexts = self.searchchain.process(mediainfo=mediainfo,
|
contexts = self.searchchain.process(mediainfo=mediainfo,
|
||||||
@@ -375,7 +338,8 @@ class SubscribeChain(ChainBase):
|
|||||||
# 洗版时,优先级小于等于已下载优先级的不要
|
# 洗版时,优先级小于等于已下载优先级的不要
|
||||||
if subscribe.current_priority \
|
if subscribe.current_priority \
|
||||||
and torrent_info.pri_order <= subscribe.current_priority:
|
and torrent_info.pri_order <= subscribe.current_priority:
|
||||||
logger.info(f'{subscribe.name} 正在洗版,{torrent_info.title} 优先级低于或等于已下载优先级')
|
logger.info(
|
||||||
|
f'{subscribe.name} 正在洗版,{torrent_info.title} 优先级低于或等于已下载优先级')
|
||||||
continue
|
continue
|
||||||
matched_contexts.append(context)
|
matched_contexts.append(context)
|
||||||
|
|
||||||
@@ -394,18 +358,31 @@ class SubscribeChain(ChainBase):
|
|||||||
save_path=subscribe.save_path,
|
save_path=subscribe.save_path,
|
||||||
media_category=subscribe.media_category,
|
media_category=subscribe.media_category,
|
||||||
downloader=subscribe.downloader,
|
downloader=subscribe.downloader,
|
||||||
|
source=self.get_subscribe_source_keyword(subscribe)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# 同步外部修改,更新订阅信息
|
||||||
|
subscribe = self.subscribeoper.get(subscribe.id)
|
||||||
|
|
||||||
# 判断是否应完成订阅
|
# 判断是否应完成订阅
|
||||||
|
if subscribe:
|
||||||
self.finish_subscribe_or_not(subscribe=subscribe, meta=meta, mediainfo=mediainfo,
|
self.finish_subscribe_or_not(subscribe=subscribe, meta=meta, mediainfo=mediainfo,
|
||||||
downloads=downloads, lefts=lefts)
|
downloads=downloads, lefts=lefts)
|
||||||
|
finally:
|
||||||
|
# 如果状态为N则更新为R
|
||||||
|
if subscribe and subscribe.state == 'N':
|
||||||
|
self.subscribeoper.update(subscribe.id, {'state': 'R'})
|
||||||
|
|
||||||
# 手动触发时发送系统消息
|
# 手动触发时发送系统消息
|
||||||
if manual:
|
if manual:
|
||||||
|
if subscribes:
|
||||||
if sid:
|
if sid:
|
||||||
self.message.put(f'{subscribes[0].name} 搜索完成!', title="订阅搜索", role="system")
|
self.message.put(f'{subscribes[0].name} 搜索完成!', title="订阅搜索", role="system")
|
||||||
else:
|
else:
|
||||||
self.message.put('所有订阅搜索完成!', title="订阅搜索", role="system")
|
self.message.put('所有订阅搜索完成!', title="订阅搜索", role="system")
|
||||||
|
else:
|
||||||
|
self.message.put('没有找到订阅!', title="订阅搜索", role="system")
|
||||||
|
logger.debug(f"search Lock released at {datetime.now()}")
|
||||||
|
|
||||||
def update_subscribe_priority(self, subscribe: Subscribe, meta: MetaInfo,
|
def update_subscribe_priority(self, subscribe: Subscribe, meta: MetaInfo,
|
||||||
mediainfo: MediaInfo, downloads: List[Context]):
|
mediainfo: MediaInfo, downloads: List[Context]):
|
||||||
@@ -440,25 +417,20 @@ class SubscribeChain(ChainBase):
|
|||||||
no_lefts = not lefts or not lefts.get(mediakey)
|
no_lefts = not lefts or not lefts.get(mediakey)
|
||||||
# 是否完成订阅
|
# 是否完成订阅
|
||||||
if not subscribe.best_version:
|
if not subscribe.best_version:
|
||||||
# 非洗板
|
# 订阅存在待定策略,不管是否已完成,均需更新订阅信息
|
||||||
|
# 更新订阅已下载信息
|
||||||
|
self.__update_subscribe_note(subscribe=subscribe, downloads=downloads)
|
||||||
|
# 更新订阅剩余集数和时间
|
||||||
|
self.__update_lack_episodes(lefts=lefts, subscribe=subscribe, mediainfo=mediainfo,
|
||||||
|
update_date=bool(downloads))
|
||||||
|
# 判断是否需要完成订阅
|
||||||
if ((no_lefts and meta.type == MediaType.TV)
|
if ((no_lefts and meta.type == MediaType.TV)
|
||||||
or (downloads and meta.type == MediaType.MOVIE)
|
or (downloads and meta.type == MediaType.MOVIE)
|
||||||
or force):
|
or force):
|
||||||
# 完成订阅
|
|
||||||
self.__finish_subscribe(subscribe=subscribe, meta=meta, mediainfo=mediainfo)
|
self.__finish_subscribe(subscribe=subscribe, meta=meta, mediainfo=mediainfo)
|
||||||
elif downloads and meta.type == MediaType.TV:
|
|
||||||
# 电视剧更新已下载集数
|
|
||||||
self.__update_subscribe_note(subscribe=subscribe, downloads=downloads)
|
|
||||||
# 更新订阅剩余集数和时间
|
|
||||||
self.__update_lack_episodes(lefts=lefts, subscribe=subscribe,
|
|
||||||
mediainfo=mediainfo, update_date=True)
|
|
||||||
else:
|
else:
|
||||||
# 未下载到内容且不完整
|
# 未下载到内容且不完整
|
||||||
logger.info(f'{mediainfo.title_year} 未下载完整,继续订阅 ...')
|
logger.info(f'{mediainfo.title_year} 未下载完整,继续订阅 ...')
|
||||||
if meta.type == MediaType.TV:
|
|
||||||
# 更新订阅剩余集数
|
|
||||||
self.__update_lack_episodes(lefts=lefts, subscribe=subscribe,
|
|
||||||
mediainfo=mediainfo, update_date=False)
|
|
||||||
elif downloads:
|
elif downloads:
|
||||||
# 洗板,下载到了内容,更新资源优先级
|
# 洗板,下载到了内容,更新资源优先级
|
||||||
self.update_subscribe_priority(subscribe=subscribe, meta=meta,
|
self.update_subscribe_priority(subscribe=subscribe, meta=meta,
|
||||||
@@ -508,7 +480,7 @@ class SubscribeChain(ChainBase):
|
|||||||
:return: 返回[]代表所有站点命中,返回None代表没有订阅
|
:return: 返回[]代表所有站点命中,返回None代表没有订阅
|
||||||
"""
|
"""
|
||||||
# 查询所有订阅
|
# 查询所有订阅
|
||||||
subscribes = self.subscribeoper.list('R')
|
subscribes = self.subscribeoper.list(self.get_states_for_search('R'))
|
||||||
if not subscribes:
|
if not subscribes:
|
||||||
return None
|
return None
|
||||||
ret_sites = []
|
ret_sites = []
|
||||||
@@ -533,8 +505,10 @@ class SubscribeChain(ChainBase):
|
|||||||
# 记录重新识别过的种子
|
# 记录重新识别过的种子
|
||||||
_recognize_cached = []
|
_recognize_cached = []
|
||||||
|
|
||||||
|
with self._rlock:
|
||||||
|
logger.debug(f"match lock acquired at {datetime.now()}")
|
||||||
# 所有订阅
|
# 所有订阅
|
||||||
subscribes = self.subscribeoper.list('R')
|
subscribes = self.subscribeoper.list(self.get_states_for_search('R'))
|
||||||
# 遍历订阅
|
# 遍历订阅
|
||||||
for subscribe in subscribes:
|
for subscribe in subscribes:
|
||||||
if global_vars.is_system_stopped:
|
if global_vars.is_system_stopped:
|
||||||
@@ -563,55 +537,14 @@ class SubscribeChain(ChainBase):
|
|||||||
logger.warn(
|
logger.warn(
|
||||||
f'未识别到媒体信息,标题:{subscribe.name},tmdbid:{subscribe.tmdbid},doubanid:{subscribe.doubanid}')
|
f'未识别到媒体信息,标题:{subscribe.name},tmdbid:{subscribe.tmdbid},doubanid:{subscribe.doubanid}')
|
||||||
continue
|
continue
|
||||||
# 非洗版
|
|
||||||
if not subscribe.best_version:
|
# 如果媒体已存在或已下载完毕,跳过当前订阅处理
|
||||||
# 每季总集数
|
exist_flag, no_exists = self.check_and_handle_existing_media(subscribe=subscribe, meta=meta,
|
||||||
totals = {}
|
|
||||||
if subscribe.season and subscribe.total_episode:
|
|
||||||
totals = {
|
|
||||||
subscribe.season: subscribe.total_episode
|
|
||||||
}
|
|
||||||
# 查询缺失的媒体信息
|
|
||||||
exist_flag, no_exists = self.downloadchain.get_no_exists_info(
|
|
||||||
meta=meta,
|
|
||||||
mediainfo=mediainfo,
|
mediainfo=mediainfo,
|
||||||
totals=totals
|
mediakey=mediakey)
|
||||||
)
|
|
||||||
else:
|
|
||||||
# 洗版
|
|
||||||
exist_flag = False
|
|
||||||
if meta.type == MediaType.TV:
|
|
||||||
no_exists = {
|
|
||||||
mediakey: {
|
|
||||||
subscribe.season: NotExistMediaInfo(
|
|
||||||
season=subscribe.season,
|
|
||||||
episodes=[],
|
|
||||||
total_episode=subscribe.total_episode,
|
|
||||||
start_episode=subscribe.start_episode or 1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else:
|
|
||||||
no_exists = {}
|
|
||||||
|
|
||||||
# 已存在
|
|
||||||
if exist_flag:
|
if exist_flag:
|
||||||
logger.info(f'{mediainfo.title_year} 媒体库中已存在')
|
|
||||||
self.finish_subscribe_or_not(subscribe=subscribe, meta=meta, mediainfo=mediainfo, force=True)
|
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# 电视剧订阅
|
|
||||||
if meta.type == MediaType.TV:
|
|
||||||
# 整合实际缺失集与订阅开始集结束集,同时剔除已下载的集数
|
|
||||||
no_exists = self.__get_subscribe_no_exits(
|
|
||||||
subscribe_name=f'{subscribe.name} {meta.season}',
|
|
||||||
no_exists=no_exists,
|
|
||||||
mediakey=mediakey,
|
|
||||||
begin_season=meta.begin_season,
|
|
||||||
total_episode=subscribe.total_episode,
|
|
||||||
start_episode=subscribe.start_episode,
|
|
||||||
downloaded_episodes=self.__get_downloaded_episodes(subscribe)
|
|
||||||
)
|
|
||||||
|
|
||||||
# 遍历缓存种子
|
# 遍历缓存种子
|
||||||
_match_context = []
|
_match_context = []
|
||||||
for domain, contexts in torrents.items():
|
for domain, contexts in torrents.items():
|
||||||
@@ -751,7 +684,8 @@ class SubscribeChain(ChainBase):
|
|||||||
if subscribe.best_version:
|
if subscribe.best_version:
|
||||||
if subscribe.current_priority \
|
if subscribe.current_priority \
|
||||||
and torrent_info.pri_order <= subscribe.current_priority:
|
and torrent_info.pri_order <= subscribe.current_priority:
|
||||||
logger.info(f'{subscribe.name} 正在洗版,{torrent_info.title} 优先级低于或等于已下载优先级')
|
logger.info(
|
||||||
|
f'{subscribe.name} 正在洗版,{torrent_info.title} 优先级低于或等于已下载优先级')
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# 匹配成功
|
# 匹配成功
|
||||||
@@ -773,10 +707,18 @@ class SubscribeChain(ChainBase):
|
|||||||
username=subscribe.username,
|
username=subscribe.username,
|
||||||
save_path=subscribe.save_path,
|
save_path=subscribe.save_path,
|
||||||
media_category=subscribe.media_category,
|
media_category=subscribe.media_category,
|
||||||
downloader=subscribe.downloader)
|
downloader=subscribe.downloader,
|
||||||
|
source=self.get_subscribe_source_keyword(subscribe)
|
||||||
|
)
|
||||||
|
|
||||||
|
# 同步外部修改,更新订阅信息
|
||||||
|
subscribe = self.subscribeoper.get(subscribe.id)
|
||||||
|
|
||||||
# 判断是否要完成订阅
|
# 判断是否要完成订阅
|
||||||
|
if subscribe:
|
||||||
self.finish_subscribe_or_not(subscribe=subscribe, meta=meta, mediainfo=mediainfo,
|
self.finish_subscribe_or_not(subscribe=subscribe, meta=meta, mediainfo=mediainfo,
|
||||||
downloads=downloads, lefts=lefts)
|
downloads=downloads, lefts=lefts)
|
||||||
|
logger.debug(f"match Lock released at {datetime.now()}")
|
||||||
|
|
||||||
def check(self):
|
def check(self):
|
||||||
"""
|
"""
|
||||||
@@ -837,7 +779,7 @@ class SubscribeChain(ChainBase):
|
|||||||
|
|
||||||
def __update_subscribe_note(self, subscribe: Subscribe, downloads: List[Context]):
|
def __update_subscribe_note(self, subscribe: Subscribe, downloads: List[Context]):
|
||||||
"""
|
"""
|
||||||
更新已下载集数到note字段
|
更新已下载信息到note字段
|
||||||
"""
|
"""
|
||||||
# 查询现有Note
|
# 查询现有Note
|
||||||
if not downloads:
|
if not downloads:
|
||||||
@@ -848,48 +790,66 @@ class SubscribeChain(ChainBase):
|
|||||||
for context in downloads:
|
for context in downloads:
|
||||||
meta = context.meta_info
|
meta = context.meta_info
|
||||||
mediainfo = context.media_info
|
mediainfo = context.media_info
|
||||||
if mediainfo.type != MediaType.TV:
|
|
||||||
continue
|
|
||||||
if subscribe.tmdbid and mediainfo.tmdb_id \
|
if subscribe.tmdbid and mediainfo.tmdb_id \
|
||||||
and mediainfo.tmdb_id != subscribe.tmdbid:
|
and mediainfo.tmdb_id != subscribe.tmdbid:
|
||||||
continue
|
continue
|
||||||
if subscribe.doubanid and mediainfo.douban_id \
|
if subscribe.doubanid and mediainfo.douban_id \
|
||||||
and mediainfo.douban_id != subscribe.doubanid:
|
and mediainfo.douban_id != subscribe.doubanid:
|
||||||
continue
|
continue
|
||||||
episodes = meta.episode_list
|
items = []
|
||||||
if not episodes:
|
if mediainfo.type == MediaType.TV:
|
||||||
|
# 电视剧有集数,使用 episode_list
|
||||||
|
items = meta.episode_list
|
||||||
|
elif mediainfo.type == MediaType.MOVIE:
|
||||||
|
# 电影只有一个条目,设置为 [1]
|
||||||
|
items = [1]
|
||||||
|
if not items:
|
||||||
continue
|
continue
|
||||||
# 合并已下载集
|
# 合并已下载的集数或电影项(去重)
|
||||||
note = list(set(note).union(set(episodes)))
|
note = list(set(note).union(set(items)))
|
||||||
# 更新订阅
|
# 更新订阅
|
||||||
|
if note:
|
||||||
self.subscribeoper.update(subscribe.id, {
|
self.subscribeoper.update(subscribe.id, {
|
||||||
"note": note
|
"note": note
|
||||||
})
|
})
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def __get_downloaded_episodes(subscribe: Subscribe) -> List[int]:
|
def __get_downloaded(subscribe: Subscribe) -> List[int]:
|
||||||
"""
|
"""
|
||||||
获取已下载过的集数
|
获取已下载过的集数或电影
|
||||||
"""
|
"""
|
||||||
if not subscribe.note:
|
note = subscribe.note or []
|
||||||
|
if not note:
|
||||||
return []
|
return []
|
||||||
if subscribe.type != MediaType.TV.value:
|
# 针对 TV 类型,返回已下载的集数
|
||||||
|
if subscribe.type == MediaType.TV.value:
|
||||||
|
logger.info(f'订阅 {subscribe.name} 第{subscribe.season}季 已下载集数:{note}')
|
||||||
|
return note
|
||||||
|
# 针对 Movie 类型,直接返回已下载的电影
|
||||||
|
if subscribe.type == MediaType.MOVIE.value:
|
||||||
|
logger.info(f'订阅 {subscribe.name} 已下载内容:{note}')
|
||||||
|
return note
|
||||||
return []
|
return []
|
||||||
episodes = subscribe.note or []
|
|
||||||
logger.info(f'订阅 {subscribe.name} 第{subscribe.season}季 已下载集数:{episodes}')
|
|
||||||
return episodes
|
|
||||||
|
|
||||||
def __update_lack_episodes(self, lefts: Dict[Union[int, str], Dict[int, NotExistMediaInfo]],
|
def __update_lack_episodes(self, lefts: Dict[Union[int, str], Dict[int, NotExistMediaInfo]],
|
||||||
subscribe: Subscribe,
|
subscribe: Subscribe,
|
||||||
mediainfo: MediaInfo,
|
mediainfo: MediaInfo,
|
||||||
update_date: bool = False):
|
update_date: bool = False):
|
||||||
"""
|
"""
|
||||||
更新订阅剩余集数
|
更新订阅剩余集数及时间
|
||||||
"""
|
"""
|
||||||
|
update_data = {}
|
||||||
|
if update_date:
|
||||||
|
update_data["last_update"] = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
||||||
|
if subscribe.type == MediaType.TV.value:
|
||||||
if not lefts:
|
if not lefts:
|
||||||
return
|
# 如果 lefts 为空,表示没有缺失集数,直接设置 lack_episode 为 0
|
||||||
|
lack_episode = 0
|
||||||
|
logger.info(f'{mediainfo.title_year} 没有缺失集数,直接更新为 0 ...')
|
||||||
|
else:
|
||||||
mediakey = subscribe.tmdbid or subscribe.doubanid
|
mediakey = subscribe.tmdbid or subscribe.doubanid
|
||||||
left_seasons = lefts.get(mediakey)
|
left_seasons = lefts.get(mediakey)
|
||||||
|
lack_episode = 0
|
||||||
if left_seasons:
|
if left_seasons:
|
||||||
for season_info in left_seasons.values():
|
for season_info in left_seasons.values():
|
||||||
season = season_info.season
|
season = season_info.season
|
||||||
@@ -899,23 +859,21 @@ class SubscribeChain(ChainBase):
|
|||||||
lack_episode = season_info.total_episode
|
lack_episode = season_info.total_episode
|
||||||
else:
|
else:
|
||||||
lack_episode = len(left_episodes)
|
lack_episode = len(left_episodes)
|
||||||
logger.info(f'{mediainfo.title_year} 季 {season} 更新缺失集数为{lack_episode} ...')
|
logger.info(f"{mediainfo.title_year} 季 {season} 更新缺失集数为{lack_episode} ...")
|
||||||
if update_date:
|
break
|
||||||
# 同时更新最后时间
|
update_data = {"lack_episode": lack_episode}
|
||||||
self.subscribeoper.update(subscribe.id, {
|
# 更新数据库
|
||||||
"lack_episode": lack_episode,
|
if update_data:
|
||||||
"last_update": datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
self.subscribeoper.update(subscribe.id, update_data)
|
||||||
})
|
|
||||||
else:
|
|
||||||
self.subscribeoper.update(subscribe.id, {
|
|
||||||
"lack_episode": lack_episode
|
|
||||||
})
|
|
||||||
|
|
||||||
def __finish_subscribe(self, subscribe: Subscribe, mediainfo: MediaInfo,
|
def __finish_subscribe(self, subscribe: Subscribe, mediainfo: MediaInfo,
|
||||||
meta: MetaBase, bestversion: bool = False):
|
meta: MetaBase, bestversion: bool = False):
|
||||||
"""
|
"""
|
||||||
完成订阅
|
完成订阅
|
||||||
"""
|
"""
|
||||||
|
# 如果订阅状态为待定(P),说明订阅信息尚未完全更新,无法完成订阅
|
||||||
|
if subscribe.state == "P":
|
||||||
|
return
|
||||||
# 完成订阅
|
# 完成订阅
|
||||||
msgstr = "订阅"
|
msgstr = "订阅"
|
||||||
if bestversion:
|
if bestversion:
|
||||||
@@ -1015,7 +973,7 @@ class SubscribeChain(ChainBase):
|
|||||||
total_episode: int,
|
total_episode: int,
|
||||||
start_episode: int,
|
start_episode: int,
|
||||||
downloaded_episodes: List[int] = None
|
downloaded_episodes: List[int] = None
|
||||||
) -> Dict[Union[int, str], Dict[int, NotExistMediaInfo]]:
|
) -> Tuple[bool, Dict[Union[int, str], Dict[int, NotExistMediaInfo]]]:
|
||||||
"""
|
"""
|
||||||
根据订阅开始集数和总集数,结合TMDB信息计算当前订阅的缺失集数
|
根据订阅开始集数和总集数,结合TMDB信息计算当前订阅的缺失集数
|
||||||
:param subscribe_name: 订阅名称
|
:param subscribe_name: 订阅名称
|
||||||
@@ -1028,7 +986,7 @@ class SubscribeChain(ChainBase):
|
|||||||
"""
|
"""
|
||||||
# 使用订阅的总集数和开始集数替换no_exists
|
# 使用订阅的总集数和开始集数替换no_exists
|
||||||
if not no_exists or not no_exists.get(mediakey):
|
if not no_exists or not no_exists.get(mediakey):
|
||||||
return no_exists
|
return False, no_exists
|
||||||
no_exists_item = no_exists.get(mediakey)
|
no_exists_item = no_exists.get(mediakey)
|
||||||
if total_episode or start_episode:
|
if total_episode or start_episode:
|
||||||
logger.info(f'订阅 {subscribe_name} 设定的开始集数:{start_episode}、总集数:{total_episode}')
|
logger.info(f'订阅 {subscribe_name} 设定的开始集数:{start_episode}、总集数:{total_episode}')
|
||||||
@@ -1053,7 +1011,7 @@ class SubscribeChain(ChainBase):
|
|||||||
if not start_episode \
|
if not start_episode \
|
||||||
and not total_episode:
|
and not total_episode:
|
||||||
# 无需调整
|
# 无需调整
|
||||||
return no_exists
|
return False, no_exists
|
||||||
if not start_episode:
|
if not start_episode:
|
||||||
# 没有自定义开始集
|
# 没有自定义开始集
|
||||||
start_episode = start
|
start_episode = start
|
||||||
@@ -1088,25 +1046,32 @@ class SubscribeChain(ChainBase):
|
|||||||
episode_list = list(range(start, total + 1))
|
episode_list = list(range(start, total + 1))
|
||||||
# 更新剧集列表
|
# 更新剧集列表
|
||||||
episodes = list(set(episode_list).difference(set(downloaded_episodes)))
|
episodes = list(set(episode_list).difference(set(downloaded_episodes)))
|
||||||
|
# 如果存在已下载剧集,则差集为空时,说明所有均已存在
|
||||||
|
if not episodes:
|
||||||
|
return True, {}
|
||||||
# 更新集合
|
# 更新集合
|
||||||
no_exists[mediakey][begin_season] = NotExistMediaInfo(
|
no_exists[mediakey][begin_season] = NotExistMediaInfo(
|
||||||
season=begin_season,
|
season=begin_season,
|
||||||
episodes=episodes,
|
episodes=episodes,
|
||||||
total_episode=total,
|
total_episode=total,
|
||||||
start_episode=start
|
start_episode=start,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
# 开始集数
|
# 开始集数
|
||||||
start = start_episode or 1
|
start = start_episode or 1
|
||||||
# 不存在的季
|
# 更新剧集列表
|
||||||
|
episodes = list(set(range(start, total_episode + 1)).difference(set(downloaded_episodes)))
|
||||||
|
# 如果存在已下载剧集,则差集为空时,说明所有均已存在
|
||||||
|
if not episodes:
|
||||||
|
return True, {}
|
||||||
no_exists[mediakey][begin_season] = NotExistMediaInfo(
|
no_exists[mediakey][begin_season] = NotExistMediaInfo(
|
||||||
season=begin_season,
|
season=begin_season,
|
||||||
episodes=list(set(range(start, total_episode + 1)).difference(set(downloaded_episodes))),
|
episodes=episodes,
|
||||||
total_episode=total_episode,
|
total_episode=total_episode,
|
||||||
start_episode=start
|
start_episode=start,
|
||||||
)
|
)
|
||||||
logger.info(f'订阅 {subscribe_name} 缺失剧集数更新为:{no_exists}')
|
logger.info(f'订阅 {subscribe_name} 缺失剧集数更新为:{no_exists}')
|
||||||
return no_exists
|
return False, no_exists
|
||||||
|
|
||||||
@eventmanager.register(EventType.SiteDeleted)
|
@eventmanager.register(EventType.SiteDeleted)
|
||||||
def remove_site(self, event: Event):
|
def remove_site(self, event: Event):
|
||||||
@@ -1126,7 +1091,7 @@ class SubscribeChain(ChainBase):
|
|||||||
if not subscribe.sites:
|
if not subscribe.sites:
|
||||||
continue
|
continue
|
||||||
self.subscribeoper.update(subscribe.id, {
|
self.subscribeoper.update(subscribe.id, {
|
||||||
"sites": ""
|
"sites": []
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
# 从选中的rss站点中移除
|
# 从选中的rss站点中移除
|
||||||
@@ -1294,3 +1259,119 @@ class SubscribeChain(ChainBase):
|
|||||||
subscribe_info.subscribe = Subscribe(**subscribe.to_dict())
|
subscribe_info.subscribe = Subscribe(**subscribe.to_dict())
|
||||||
subscribe_info.episodes = episodes
|
subscribe_info.episodes = episodes
|
||||||
return subscribe_info
|
return subscribe_info
|
||||||
|
|
||||||
|
def check_and_handle_existing_media(self, subscribe: Subscribe, meta: MetaInfo,
|
||||||
|
mediainfo: MediaInfo, mediakey: str):
|
||||||
|
"""
|
||||||
|
检查媒体是否已经存在,并根据情况执行相应的操作
|
||||||
|
1. 查询缺失的媒体信息
|
||||||
|
2. 判断是否已经下载完毕
|
||||||
|
3. 根据媒体类型(电视剧或电影)执行不同的处理
|
||||||
|
|
||||||
|
:param subscribe: 订阅信息对象
|
||||||
|
:param meta: 媒体元数据
|
||||||
|
:param mediainfo: 媒体信息
|
||||||
|
:param mediakey: 媒体标识符
|
||||||
|
:return:
|
||||||
|
- exist_flag (bool): 布尔值,表示媒体是否已经完全下载或已存在
|
||||||
|
- no_exists (dict): 缺失的媒体信息,包含缺失的集数或其他相关信息
|
||||||
|
"""
|
||||||
|
# 非洗版
|
||||||
|
if not subscribe.best_version:
|
||||||
|
# 每季总集数
|
||||||
|
totals = {}
|
||||||
|
if subscribe.season and subscribe.total_episode:
|
||||||
|
totals = {
|
||||||
|
subscribe.season: subscribe.total_episode
|
||||||
|
}
|
||||||
|
# 查询媒体库缺失的媒体信息
|
||||||
|
exist_flag, no_exists = self.downloadchain.get_no_exists_info(
|
||||||
|
meta=meta,
|
||||||
|
mediainfo=mediainfo,
|
||||||
|
totals=totals
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# 洗版
|
||||||
|
exist_flag = False
|
||||||
|
if meta.type == MediaType.TV:
|
||||||
|
# 对于电视剧,构造缺失的媒体信息
|
||||||
|
no_exists = {
|
||||||
|
mediakey: {
|
||||||
|
subscribe.season: NotExistMediaInfo(
|
||||||
|
season=subscribe.season,
|
||||||
|
episodes=[],
|
||||||
|
total_episode=subscribe.total_episode,
|
||||||
|
start_episode=subscribe.start_episode or 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
no_exists = {}
|
||||||
|
|
||||||
|
# 如果媒体已存在,执行订阅完成操作
|
||||||
|
if exist_flag:
|
||||||
|
logger.info(f'{mediainfo.title_year} 媒体库中已存在')
|
||||||
|
self.finish_subscribe_or_not(subscribe=subscribe, meta=meta, mediainfo=mediainfo, force=True)
|
||||||
|
return True, no_exists
|
||||||
|
|
||||||
|
# 获取已下载的集数或电影
|
||||||
|
downloaded = self.__get_downloaded(subscribe)
|
||||||
|
if meta.type == MediaType.TV:
|
||||||
|
# 对于电视剧类型,整合缺失集数并剔除已下载的集数
|
||||||
|
exist_flag, no_exists = self.__get_subscribe_no_exits(
|
||||||
|
subscribe_name=f'{subscribe.name} {meta.season}',
|
||||||
|
no_exists=no_exists,
|
||||||
|
mediakey=mediakey,
|
||||||
|
begin_season=meta.begin_season,
|
||||||
|
total_episode=subscribe.total_episode,
|
||||||
|
start_episode=subscribe.start_episode,
|
||||||
|
downloaded_episodes=downloaded
|
||||||
|
)
|
||||||
|
elif meta.type == MediaType.MOVIE:
|
||||||
|
# 对于电影类型,直接根据是否已下载判断
|
||||||
|
exist_flag = bool(downloaded)
|
||||||
|
|
||||||
|
# 如果已下载完毕,执行订阅完成操作
|
||||||
|
if exist_flag:
|
||||||
|
logger.info(f'{mediainfo.title_year} 已全部下载')
|
||||||
|
self.finish_subscribe_or_not(subscribe=subscribe, meta=meta, mediainfo=mediainfo, force=True)
|
||||||
|
return True, no_exists
|
||||||
|
|
||||||
|
# 返回结果,表示媒体未完全下载或存在
|
||||||
|
return False, no_exists
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_states_for_search(state: str) -> str:
|
||||||
|
"""
|
||||||
|
根据给定的状态返回实际需要搜索的状态列表,支持多个状态用逗号分隔
|
||||||
|
:param state: 订阅状态
|
||||||
|
N: New(新建,未处理)
|
||||||
|
R: Resolved(订阅中)
|
||||||
|
P: Pending(待定,信息待进一步更新,允许搜索,不允许完成)
|
||||||
|
S: Suspended(暂停,订阅不参与任何动作,暂时停止处理)
|
||||||
|
:return: 需要查询的状态列表(多个状态用逗号分隔)
|
||||||
|
"""
|
||||||
|
# 如果状态是 R 或 P,则视为一起搜索,返回 R,P 作为查询条件
|
||||||
|
if state in ["R", "P"]:
|
||||||
|
return "R,P"
|
||||||
|
return state
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_subscribe_source_keyword(subscribe: Subscribe) -> str:
|
||||||
|
"""
|
||||||
|
构造用于订阅来源的关键字字符串
|
||||||
|
:param subscribe: Subscribe 对象
|
||||||
|
:return: 格式化的订阅来源关键字字符串,格式为 "Subscribe|{...}"
|
||||||
|
"""
|
||||||
|
source_keyword = {
|
||||||
|
'id': subscribe.id,
|
||||||
|
'name': subscribe.name,
|
||||||
|
'year': subscribe.year,
|
||||||
|
'type': subscribe.type,
|
||||||
|
'season': subscribe.season,
|
||||||
|
'tmdbid': subscribe.tmdbid,
|
||||||
|
'imdbid': subscribe.imdbid,
|
||||||
|
'tvdbid': subscribe.tvdbid,
|
||||||
|
'doubanid': subscribe.doubanid,
|
||||||
|
'bangumiid': subscribe.bangumiid
|
||||||
|
}
|
||||||
|
return f"Subscribe|{json.dumps(source_keyword, ensure_ascii=False)}"
|
||||||
|
|||||||
@@ -330,7 +330,7 @@ class TransferChain(ChainBase):
|
|||||||
# 自定义识别
|
# 自定义识别
|
||||||
if formaterHandler:
|
if formaterHandler:
|
||||||
# 开始集、结束集、PART
|
# 开始集、结束集、PART
|
||||||
begin_ep, end_ep, part = formaterHandler.split_episode(file_path.name)
|
begin_ep, end_ep, part = formaterHandler.split_episode(file_name=file_path.name, file_meta=file_meta)
|
||||||
if begin_ep is not None:
|
if begin_ep is not None:
|
||||||
file_meta.begin_episode = begin_ep
|
file_meta.begin_episode = begin_ep
|
||||||
file_meta.part = part
|
file_meta.part = part
|
||||||
@@ -392,6 +392,7 @@ class TransferChain(ChainBase):
|
|||||||
download_hash = download_file.download_hash
|
download_hash = download_file.download_hash
|
||||||
|
|
||||||
# 查询整理目标目录
|
# 查询整理目标目录
|
||||||
|
dir_info = None
|
||||||
if not target_directory:
|
if not target_directory:
|
||||||
if src_match:
|
if src_match:
|
||||||
# 按源目录匹配,以便找到更合适的目录配置
|
# 按源目录匹配,以便找到更合适的目录配置
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import re
|
import re
|
||||||
from dataclasses import dataclass, field, asdict
|
from dataclasses import dataclass, field, asdict
|
||||||
|
from datetime import datetime
|
||||||
from typing import List, Dict, Any, Tuple
|
from typing import List, Dict, Any, Tuple
|
||||||
|
|
||||||
from app.core.config import settings
|
from app.core.config import settings
|
||||||
@@ -123,6 +124,20 @@ class TorrentInfo:
|
|||||||
return ""
|
return ""
|
||||||
return StringUtils.diff_time_str(self.freedate)
|
return StringUtils.diff_time_str(self.freedate)
|
||||||
|
|
||||||
|
def pub_minutes(self) -> float:
|
||||||
|
"""
|
||||||
|
返回发布时间距离当前时间的分钟数
|
||||||
|
"""
|
||||||
|
if not self.pubdate:
|
||||||
|
return 0
|
||||||
|
try:
|
||||||
|
pub_date = datetime.strptime(self.pubdate, "%Y-%m-%d %H:%M:%S")
|
||||||
|
now_datetime = datetime.now()
|
||||||
|
return (now_datetime - pub_date).total_seconds() // 60
|
||||||
|
except Exception as e:
|
||||||
|
print(f"种子发布时间获取失败: {e}")
|
||||||
|
return 0
|
||||||
|
|
||||||
def to_dict(self):
|
def to_dict(self):
|
||||||
"""
|
"""
|
||||||
返回字典
|
返回字典
|
||||||
|
|||||||
@@ -347,8 +347,17 @@ class EventManager(metaclass=Singleton):
|
|||||||
if not handlers:
|
if not handlers:
|
||||||
logger.debug(f"No handlers found for chain event: {event}")
|
logger.debug(f"No handlers found for chain event: {event}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
# 过滤出启用的处理器
|
||||||
|
enabled_handlers = {handler_id: (priority, handler) for handler_id, (priority, handler) in handlers.items()
|
||||||
|
if self.__is_handler_enabled(handler)}
|
||||||
|
|
||||||
|
if not enabled_handlers:
|
||||||
|
logger.debug(f"No enabled handlers found for chain event: {event}. Skipping execution.")
|
||||||
|
return False
|
||||||
|
|
||||||
self.__log_event_lifecycle(event, "Started")
|
self.__log_event_lifecycle(event, "Started")
|
||||||
for handler_id, (priority, handler) in handlers.items():
|
for handler_id, (priority, handler) in enabled_handlers.items():
|
||||||
start_time = time.time()
|
start_time = time.time()
|
||||||
self.__safe_invoke_handler(handler, event)
|
self.__safe_invoke_handler(handler, event)
|
||||||
logger.debug(
|
logger.debug(
|
||||||
@@ -493,13 +502,15 @@ class EventManager(metaclass=Singleton):
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
def register(self, etype: Union[EventType, ChainEventType, List[Union[EventType, ChainEventType]], type]):
|
def register(self, etype: Union[EventType, ChainEventType, List[Union[EventType, ChainEventType]], type],
|
||||||
|
priority: int = DEFAULT_EVENT_PRIORITY):
|
||||||
"""
|
"""
|
||||||
事件注册装饰器,用于将函数注册为事件的处理器
|
事件注册装饰器,用于将函数注册为事件的处理器
|
||||||
:param etype:
|
:param etype:
|
||||||
- 单个事件类型成员 (如 EventType.MetadataScrape, ChainEventType.PluginAction)
|
- 单个事件类型成员 (如 EventType.MetadataScrape, ChainEventType.PluginAction)
|
||||||
- 事件类型类 (EventType, ChainEventType)
|
- 事件类型类 (EventType, ChainEventType)
|
||||||
- 或事件类型成员的列表
|
- 或事件类型成员的列表
|
||||||
|
:param priority: 可选,链式事件的优先级,默认为 DEFAULT_EVENT_PRIORITY
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def decorator(f: Callable):
|
def decorator(f: Callable):
|
||||||
@@ -507,11 +518,6 @@ class EventManager(metaclass=Singleton):
|
|||||||
if isinstance(etype, list):
|
if isinstance(etype, list):
|
||||||
# 传入的已经是列表,直接使用
|
# 传入的已经是列表,直接使用
|
||||||
event_list = etype
|
event_list = etype
|
||||||
elif etype is EventType:
|
|
||||||
# 订阅所有事件
|
|
||||||
event_list = []
|
|
||||||
for et in etype:
|
|
||||||
event_list.append(et)
|
|
||||||
else:
|
else:
|
||||||
# 不是列表则包裹成单一元素的列表
|
# 不是列表则包裹成单一元素的列表
|
||||||
event_list = [etype]
|
event_list = [etype]
|
||||||
@@ -519,11 +525,11 @@ class EventManager(metaclass=Singleton):
|
|||||||
# 遍历列表,处理每个事件类型
|
# 遍历列表,处理每个事件类型
|
||||||
for event in event_list:
|
for event in event_list:
|
||||||
if isinstance(event, (EventType, ChainEventType)):
|
if isinstance(event, (EventType, ChainEventType)):
|
||||||
self.add_event_listener(event, f)
|
self.add_event_listener(event, f, priority)
|
||||||
elif isinstance(event, type) and issubclass(event, (EventType, ChainEventType)):
|
elif isinstance(event, type) and issubclass(event, (EventType, ChainEventType)):
|
||||||
# 如果是 EventType 或 ChainEventType 类,提取该类中的所有成员
|
# 如果是 EventType 或 ChainEventType 类,提取该类中的所有成员
|
||||||
for et in event.__members__.values():
|
for et in event.__members__.values():
|
||||||
self.add_event_listener(et, f)
|
self.add_event_listener(et, f, priority)
|
||||||
else:
|
else:
|
||||||
raise ValueError(f"无效的事件类型: {event}")
|
raise ValueError(f"无效的事件类型: {event}")
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
import traceback
|
import traceback
|
||||||
from typing import Generator, Optional, Tuple, Any
|
from typing import Generator, Optional, Tuple, Any, Union
|
||||||
|
|
||||||
from app.core.config import settings
|
from app.core.config import settings
|
||||||
from app.core.event import eventmanager
|
from app.core.event import eventmanager
|
||||||
from app.helper.module import ModuleHelper
|
from app.helper.module import ModuleHelper
|
||||||
from app.log import logger
|
from app.log import logger
|
||||||
from app.schemas.types import EventType, ModuleType
|
from app.schemas.types import EventType, ModuleType, DownloaderType, MediaServerType, MessageChannel, StorageSchema, \
|
||||||
|
OtherModulesType
|
||||||
from app.utils.object import ObjectUtils
|
from app.utils.object import ObjectUtils
|
||||||
from app.utils.singleton import Singleton
|
from app.utils.singleton import Singleton
|
||||||
|
|
||||||
@@ -19,6 +20,8 @@ class ModuleManager(metaclass=Singleton):
|
|||||||
_modules: dict = {}
|
_modules: dict = {}
|
||||||
# 运行态模块列表
|
# 运行态模块列表
|
||||||
_running_modules: dict = {}
|
_running_modules: dict = {}
|
||||||
|
# 子模块类型集合
|
||||||
|
SubType = Union[DownloaderType, MediaServerType, MessageChannel, StorageSchema, OtherModulesType]
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.load_modules()
|
self.load_modules()
|
||||||
@@ -135,6 +138,17 @@ class ModuleManager(metaclass=Singleton):
|
|||||||
and module.get_type() == module_type:
|
and module.get_type() == module_type:
|
||||||
yield module
|
yield module
|
||||||
|
|
||||||
|
def get_running_subtype_module(self, module_subtype: SubType) -> Generator:
|
||||||
|
"""
|
||||||
|
获取指定子类型的模块
|
||||||
|
"""
|
||||||
|
if not self._running_modules:
|
||||||
|
return []
|
||||||
|
for _, module in self._running_modules.items():
|
||||||
|
if hasattr(module, 'get_subtype') \
|
||||||
|
and module.get_subtype() == module_subtype:
|
||||||
|
yield module
|
||||||
|
|
||||||
def get_module(self, module_id: str) -> Any:
|
def get_module(self, module_id: str) -> Any:
|
||||||
"""
|
"""
|
||||||
根据模块id获取模块
|
根据模块id获取模块
|
||||||
|
|||||||
@@ -53,7 +53,9 @@ class DownloadHistory(Base):
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
@db_query
|
@db_query
|
||||||
def get_by_hash(db: Session, download_hash: str):
|
def get_by_hash(db: Session, download_hash: str):
|
||||||
return db.query(DownloadHistory).filter(DownloadHistory.download_hash == download_hash).first()
|
return db.query(DownloadHistory).filter(DownloadHistory.download_hash == download_hash).order_by(
|
||||||
|
DownloadHistory.date.desc()
|
||||||
|
).first()
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@db_query
|
@db_query
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ class SiteUserData(Base):
|
|||||||
func.max(SiteUserData.updated_day).label('latest_update_day')
|
func.max(SiteUserData.updated_day).label('latest_update_day')
|
||||||
)
|
)
|
||||||
.group_by(SiteUserData.domain)
|
.group_by(SiteUserData.domain)
|
||||||
.filter(SiteUserData.err_msg == None)
|
.filter(SiteUserData.err_msg.is_(None))
|
||||||
.subquery()
|
.subquery()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ class Subscribe(Base):
|
|||||||
lack_episode = Column(Integer)
|
lack_episode = Column(Integer)
|
||||||
# 附加信息
|
# 附加信息
|
||||||
note = Column(JSON)
|
note = Column(JSON)
|
||||||
# 状态:N-新建, R-订阅中
|
# 状态:N-新建 R-订阅中 P-待定 S-暂停
|
||||||
state = Column(String, nullable=False, index=True, default='N')
|
state = Column(String, nullable=False, index=True, default='N')
|
||||||
# 最后更新时间
|
# 最后更新时间
|
||||||
last_update = Column(String)
|
last_update = Column(String)
|
||||||
@@ -98,7 +98,13 @@ class Subscribe(Base):
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
@db_query
|
@db_query
|
||||||
def get_by_state(db: Session, state: str):
|
def get_by_state(db: Session, state: str):
|
||||||
result = db.query(Subscribe).filter(Subscribe.state == state).all()
|
# 如果 state 为空或 None,返回所有订阅
|
||||||
|
if not state:
|
||||||
|
result = db.query(Subscribe).all()
|
||||||
|
else:
|
||||||
|
# 如果传入的状态不为空,拆分成多个状态
|
||||||
|
states = state.split(',')
|
||||||
|
result = db.query(Subscribe).filter(Subscribe.state.in_(states)).all()
|
||||||
return list(result)
|
return list(result)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
|||||||
@@ -83,6 +83,7 @@ class SubscribeOper(DbOper):
|
|||||||
更新订阅
|
更新订阅
|
||||||
"""
|
"""
|
||||||
subscribe = self.get(sid)
|
subscribe = self.get(sid)
|
||||||
|
if subscribe:
|
||||||
subscribe.update(self._db, payload)
|
subscribe.update(self._db, payload)
|
||||||
return subscribe
|
return subscribe
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ class PlaywrightHelper:
|
|||||||
"""
|
"""
|
||||||
sync_stealth(page, pure=True)
|
sync_stealth(page, pure=True)
|
||||||
page.goto(url)
|
page.goto(url)
|
||||||
return sync_cf_retry(page)
|
return sync_cf_retry(page)[0]
|
||||||
|
|
||||||
def action(self, url: str,
|
def action(self, url: str,
|
||||||
callback: Callable,
|
callback: Callable,
|
||||||
|
|||||||
@@ -59,7 +59,6 @@ class DirectoryHelper:
|
|||||||
:param include_unsorted: 包含不整理目录
|
:param include_unsorted: 包含不整理目录
|
||||||
:param storage: 源存储类型
|
:param storage: 源存储类型
|
||||||
:param target_storage: 目标存储类型
|
:param target_storage: 目标存储类型
|
||||||
:param fileitem: 文件项,使用文件路径匹配
|
|
||||||
:param src_path: 源目录,有值时直接匹配
|
:param src_path: 源目录,有值时直接匹配
|
||||||
:param dest_path: 目标目录,有值时直接匹配
|
:param dest_path: 目标目录,有值时直接匹配
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ from typing import Tuple, Optional
|
|||||||
|
|
||||||
import parse
|
import parse
|
||||||
|
|
||||||
|
from app.core.meta.metabase import MetaBase
|
||||||
|
|
||||||
|
|
||||||
class FormatParser(object):
|
class FormatParser(object):
|
||||||
_key = ""
|
_key = ""
|
||||||
@@ -77,7 +79,7 @@ class FormatParser(object):
|
|||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def split_episode(self, file_name: str) -> Tuple[Optional[int], Optional[int], Optional[str]]:
|
def split_episode(self, file_name: str, file_meta: MetaBase) -> Tuple[Optional[int], Optional[int], Optional[str]]:
|
||||||
"""
|
"""
|
||||||
拆分集数,返回开始集数,结束集数,Part信息
|
拆分集数,返回开始集数,结束集数,Part信息
|
||||||
"""
|
"""
|
||||||
@@ -94,7 +96,9 @@ class FormatParser(object):
|
|||||||
start_ep = self.__offset.replace("EP", str(self._start_ep))
|
start_ep = self.__offset.replace("EP", str(self._start_ep))
|
||||||
return int(eval(start_ep)), None, self.part
|
return int(eval(start_ep)), None, self.part
|
||||||
if not self._format:
|
if not self._format:
|
||||||
return self._start_ep, self._end_ep, self.part
|
start_ep = eval(self.__offset.replace("EP", str(file_meta.begin_episode))) if file_meta.begin_episode else None
|
||||||
|
end_ep = eval(self.__offset.replace("EP", str(file_meta.end_episode))) if file_meta.end_episode else None
|
||||||
|
return int(start_ep) if start_ep else None, int(end_ep) if end_ep else None, self.part
|
||||||
else:
|
else:
|
||||||
s, e = self.__handle_single(file_name)
|
s, e = self.__handle_single(file_name)
|
||||||
start_ep = self.__offset.replace("EP", str(s)) if s else None
|
start_ep = self.__offset.replace("EP", str(s)) if s else None
|
||||||
|
|||||||
@@ -64,10 +64,10 @@ class TorrentHelper(metaclass=Singleton):
|
|||||||
if not req.content:
|
if not req.content:
|
||||||
return None, None, "", [], "未下载到种子数据"
|
return None, None, "", [], "未下载到种子数据"
|
||||||
# 解析内容格式
|
# 解析内容格式
|
||||||
if req.text and str(req.text).startswith("magnet:"):
|
if req.content.startswith(b"magnet:"):
|
||||||
# 磁力链接
|
# 磁力链接
|
||||||
return None, req.text, "", [], f"获取到磁力链接"
|
return None, req.text, "", [], f"获取到磁力链接"
|
||||||
elif req.text and "下载种子文件" in req.text:
|
if "下载种子文件".encode("utf-8") in req.content:
|
||||||
# 首次下载提示页面
|
# 首次下载提示页面
|
||||||
skip_flag = False
|
skip_flag = False
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -2,8 +2,9 @@ from abc import abstractmethod, ABCMeta
|
|||||||
from typing import Generic, Tuple, Union, TypeVar, Type, Dict, Optional, Callable
|
from typing import Generic, Tuple, Union, TypeVar, Type, Dict, Optional, Callable
|
||||||
|
|
||||||
from app.helper.service import ServiceConfigHelper
|
from app.helper.service import ServiceConfigHelper
|
||||||
from app.schemas import Notification, MessageChannel, NotificationConf, MediaServerConf, DownloaderConf
|
from app.schemas import Notification, NotificationConf, MediaServerConf, DownloaderConf
|
||||||
from app.schemas.types import ModuleType
|
from app.schemas.types import ModuleType, DownloaderType, MediaServerType, MessageChannel, StorageSchema, \
|
||||||
|
OtherModulesType
|
||||||
|
|
||||||
|
|
||||||
class _ModuleBase(metaclass=ABCMeta):
|
class _ModuleBase(metaclass=ABCMeta):
|
||||||
@@ -43,6 +44,14 @@ class _ModuleBase(metaclass=ABCMeta):
|
|||||||
"""
|
"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
@abstractmethod
|
||||||
|
def get_subtype() -> Union[DownloaderType, MediaServerType, MessageChannel, StorageSchema, OtherModulesType]:
|
||||||
|
"""
|
||||||
|
获取模块子类型(下载器、媒体服务器、消息通道、存储类型、其他杂项模块类型)
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def get_priority() -> int:
|
def get_priority() -> int:
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ from app.core.meta import MetaBase
|
|||||||
from app.log import logger
|
from app.log import logger
|
||||||
from app.modules import _ModuleBase
|
from app.modules import _ModuleBase
|
||||||
from app.modules.bangumi.bangumi import BangumiApi
|
from app.modules.bangumi.bangumi import BangumiApi
|
||||||
from app.schemas.types import ModuleType
|
from app.schemas.types import ModuleType, MediaRecognizeType
|
||||||
from app.utils.http import RequestUtils
|
from app.utils.http import RequestUtils
|
||||||
|
|
||||||
|
|
||||||
@@ -45,6 +45,13 @@ class BangumiModule(_ModuleBase):
|
|||||||
"""
|
"""
|
||||||
return ModuleType.MediaRecognize
|
return ModuleType.MediaRecognize
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_subtype() -> MediaRecognizeType:
|
||||||
|
"""
|
||||||
|
获取模块子类型
|
||||||
|
"""
|
||||||
|
return MediaRecognizeType.Bangumi
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_priority() -> int:
|
def get_priority() -> int:
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ from app.modules.douban.apiv2 import DoubanApi
|
|||||||
from app.modules.douban.douban_cache import DoubanCache
|
from app.modules.douban.douban_cache import DoubanCache
|
||||||
from app.modules.douban.scraper import DoubanScraper
|
from app.modules.douban.scraper import DoubanScraper
|
||||||
from app.schemas import MediaPerson, APIRateLimitException
|
from app.schemas import MediaPerson, APIRateLimitException
|
||||||
from app.schemas.types import MediaType, ModuleType
|
from app.schemas.types import MediaType, ModuleType, MediaRecognizeType
|
||||||
from app.utils.common import retry
|
from app.utils.common import retry
|
||||||
from app.utils.http import RequestUtils
|
from app.utils.http import RequestUtils
|
||||||
from app.utils.limit import rate_limit_exponential
|
from app.utils.limit import rate_limit_exponential
|
||||||
@@ -59,6 +59,13 @@ class DoubanModule(_ModuleBase):
|
|||||||
"""
|
"""
|
||||||
return ModuleType.MediaRecognize
|
return ModuleType.MediaRecognize
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_subtype() -> MediaRecognizeType:
|
||||||
|
"""
|
||||||
|
获取模块子类型
|
||||||
|
"""
|
||||||
|
return MediaRecognizeType.Douban
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_priority() -> int:
|
def get_priority() -> int:
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ 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, AuthInterceptCredentials
|
from app.schemas.event import AuthCredentials, AuthInterceptCredentials
|
||||||
from app.schemas.types import MediaType, ModuleType, ChainEventType
|
from app.schemas.types import MediaType, ModuleType, ChainEventType, MediaServerType
|
||||||
|
|
||||||
|
|
||||||
class EmbyModule(_ModuleBase, _MediaServerBase[Emby]):
|
class EmbyModule(_ModuleBase, _MediaServerBase[Emby]):
|
||||||
@@ -30,6 +30,13 @@ class EmbyModule(_ModuleBase, _MediaServerBase[Emby]):
|
|||||||
"""
|
"""
|
||||||
return ModuleType.MediaServer
|
return ModuleType.MediaServer
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_subtype() -> MediaServerType:
|
||||||
|
"""
|
||||||
|
获取模块子类型
|
||||||
|
"""
|
||||||
|
return MediaServerType.Emby
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_priority() -> int:
|
def get_priority() -> int:
|
||||||
"""
|
"""
|
||||||
@@ -66,16 +73,26 @@ class EmbyModule(_ModuleBase, _MediaServerBase[Emby]):
|
|||||||
logger.info(f"Emby服务器 {name} 连接断开,尝试重连 ...")
|
logger.info(f"Emby服务器 {name} 连接断开,尝试重连 ...")
|
||||||
server.reconnect()
|
server.reconnect()
|
||||||
|
|
||||||
def user_authenticate(self, credentials: AuthCredentials) -> Optional[AuthCredentials]:
|
def user_authenticate(self, credentials: AuthCredentials, service_name: Optional[str] = None) \
|
||||||
|
-> Optional[AuthCredentials]:
|
||||||
"""
|
"""
|
||||||
使用Emby用户辅助完成用户认证
|
使用Emby用户辅助完成用户认证
|
||||||
:param credentials: 认证数据
|
:param credentials: 认证数据
|
||||||
|
:param service_name: 指定要认证的媒体服务器名称,若为 None 则认证所有服务
|
||||||
:return: 认证数据
|
:return: 认证数据
|
||||||
"""
|
"""
|
||||||
# Emby认证
|
# 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():
|
# 确定要认证的服务器列表
|
||||||
|
if service_name:
|
||||||
|
# 如果指定了服务名,获取该服务实例
|
||||||
|
servers = [(service_name, server)] if (server := self.get_instance(service_name)) else []
|
||||||
|
else:
|
||||||
|
# 如果没有指定服务名,遍历所有服务
|
||||||
|
servers = self.get_instances().items()
|
||||||
|
# 遍历要认证的服务器
|
||||||
|
for name, server in servers:
|
||||||
# 触发认证拦截事件
|
# 触发认证拦截事件
|
||||||
intercept_event = eventmanager.send_event(
|
intercept_event = eventmanager.send_event(
|
||||||
etype=ChainEventType.AuthIntercept,
|
etype=ChainEventType.AuthIntercept,
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ from cachetools import TTLCache, cached
|
|||||||
from app.core.context import MediaInfo, settings
|
from app.core.context import MediaInfo, settings
|
||||||
from app.log import logger
|
from app.log import logger
|
||||||
from app.modules import _ModuleBase
|
from app.modules import _ModuleBase
|
||||||
from app.schemas.types import MediaType, ModuleType
|
from app.schemas.types import MediaType, ModuleType, OtherModulesType
|
||||||
from app.utils.http import RequestUtils
|
from app.utils.http import RequestUtils
|
||||||
|
|
||||||
|
|
||||||
@@ -343,6 +343,13 @@ class FanartModule(_ModuleBase):
|
|||||||
"""
|
"""
|
||||||
return ModuleType.Other
|
return ModuleType.Other
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_subtype() -> OtherModulesType:
|
||||||
|
"""
|
||||||
|
获取模块子类型
|
||||||
|
"""
|
||||||
|
return OtherModulesType.Fanart
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_priority() -> int:
|
def get_priority() -> int:
|
||||||
"""
|
"""
|
||||||
@@ -377,19 +384,29 @@ class FanartModule(_ModuleBase):
|
|||||||
continue
|
continue
|
||||||
if not isinstance(images, list):
|
if not isinstance(images, list):
|
||||||
continue
|
continue
|
||||||
# 按欢迎程度倒排
|
|
||||||
|
# 图片属性xx_path
|
||||||
|
image_name = self.__name(name)
|
||||||
|
if image_name.startswith("season"):
|
||||||
|
# 季图片,图片格式seasonxx-xxxx/season-specials-xxxx
|
||||||
|
for image_obj in images:
|
||||||
|
image_season = image_obj.get('season')
|
||||||
|
if image_season is not None:
|
||||||
|
# 包括poster,thumb,banner
|
||||||
|
if image_season == '0':
|
||||||
|
season_image = f"season-specials-{image_name[6:]}"
|
||||||
|
else:
|
||||||
|
season_image = f"season{str(image_season).rjust(2, '0')}-{image_name[6:]}"
|
||||||
|
# 设置图片,没有图片才设置
|
||||||
|
if not mediainfo.get_image(season_image):
|
||||||
|
mediainfo.set_image(season_image, image_obj.get('url'))
|
||||||
|
else:
|
||||||
|
# 其他图片,按欢迎程度倒排
|
||||||
images.sort(key=lambda x: int(x.get('likes', 0)), reverse=True)
|
images.sort(key=lambda x: int(x.get('likes', 0)), reverse=True)
|
||||||
# 取第一张图片
|
# 取第一张图片
|
||||||
image_obj = images[0]
|
image_obj = images[0]
|
||||||
# 图片属性xx_path
|
# 设置图片,没有图片才设置
|
||||||
image_name = self.__name(name)
|
|
||||||
image_season = image_obj.get('season')
|
|
||||||
# 设置图片
|
|
||||||
if image_name.startswith("season") and image_season:
|
|
||||||
# 季图片格式 seasonxx-poster
|
|
||||||
image_name = f"season{str(image_season).rjust(2, '0')}-{image_name[6:]}"
|
|
||||||
if not mediainfo.get_image(image_name):
|
if not mediainfo.get_image(image_name):
|
||||||
# 没有图片才设置
|
|
||||||
mediainfo.set_image(image_name, image_obj.get('url'))
|
mediainfo.set_image(image_name, image_obj.get('url'))
|
||||||
|
|
||||||
return mediainfo
|
return mediainfo
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ from app.modules import _ModuleBase
|
|||||||
from app.modules.filemanager.storages import StorageBase
|
from app.modules.filemanager.storages import StorageBase
|
||||||
from app.schemas import TransferInfo, ExistMediaInfo, TmdbEpisode, TransferDirectoryConf, FileItem, StorageUsage
|
from app.schemas import TransferInfo, ExistMediaInfo, TmdbEpisode, TransferDirectoryConf, FileItem, StorageUsage
|
||||||
from app.schemas.event import TransferRenameEventData
|
from app.schemas.event import TransferRenameEventData
|
||||||
from app.schemas.types import MediaType, ModuleType, ChainEventType
|
from app.schemas.types import MediaType, ModuleType, ChainEventType, OtherModulesType
|
||||||
from app.utils.system import SystemUtils
|
from app.utils.system import SystemUtils
|
||||||
|
|
||||||
lock = Lock()
|
lock = Lock()
|
||||||
@@ -52,6 +52,13 @@ class FileManagerModule(_ModuleBase):
|
|||||||
"""
|
"""
|
||||||
return ModuleType.Other
|
return ModuleType.Other
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_subtype() -> OtherModulesType:
|
||||||
|
"""
|
||||||
|
获取模块子类型
|
||||||
|
"""
|
||||||
|
return OtherModulesType.FileManager
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_priority() -> int:
|
def get_priority() -> int:
|
||||||
"""
|
"""
|
||||||
@@ -695,7 +702,7 @@ class FileManagerModule(_ModuleBase):
|
|||||||
return False, errmsg
|
return False, errmsg
|
||||||
except Exception as error:
|
except Exception as error:
|
||||||
logger.info(f"字幕 {new_file} 出错了,原因: {str(error)}")
|
logger.info(f"字幕 {new_file} 出错了,原因: {str(error)}")
|
||||||
return False, ""
|
return True, ""
|
||||||
|
|
||||||
def __transfer_audio_track_files(self, fileitem: FileItem, target_storage: str, target_file: Path,
|
def __transfer_audio_track_files(self, fileitem: FileItem, target_storage: str, target_file: Path,
|
||||||
transfer_type: str) -> Tuple[bool, str]:
|
transfer_type: str) -> Tuple[bool, str]:
|
||||||
@@ -719,7 +726,7 @@ class FileManagerModule(_ModuleBase):
|
|||||||
file_list: List[FileItem] = storage_oper.list(parent_item)
|
file_list: List[FileItem] = storage_oper.list(parent_item)
|
||||||
# 匹配音轨文件
|
# 匹配音轨文件
|
||||||
pending_file_list: List[FileItem] = [file for file in file_list
|
pending_file_list: List[FileItem] = [file for file in file_list
|
||||||
if Path(file.name).stem == org_path.name
|
if Path(file.name).stem == org_path.stem
|
||||||
and file.type == "file" and file.extension
|
and file.type == "file" and file.extension
|
||||||
and f".{file.extension.lower()}" in settings.RMT_AUDIOEXT]
|
and f".{file.extension.lower()}" in settings.RMT_AUDIOEXT]
|
||||||
if len(pending_file_list) == 0:
|
if len(pending_file_list) == 0:
|
||||||
|
|||||||
@@ -553,15 +553,15 @@ class Alist(StorageBase, metaclass=Singleton):
|
|||||||
:param new_name: 上传后文件名
|
:param new_name: 上传后文件名
|
||||||
:param task: 是否为任务,默认为False避免未完成上传时对文件进行操作
|
:param task: 是否为任务,默认为False避免未完成上传时对文件进行操作
|
||||||
"""
|
"""
|
||||||
encoded_path = UrlUtils.quote(fileitem.path)
|
encoded_path = UrlUtils.quote(fileitem.path + path.name)
|
||||||
headers = self.__get_header_with_token()
|
headers = self.__get_header_with_token()
|
||||||
headers.setdefault("Content-Type", "multipart/form-data")
|
headers.setdefault("Content-Type", "application/octet-stream")
|
||||||
headers.setdefault("As-Task", str(task).lower())
|
headers.setdefault("As-Task", str(task).lower())
|
||||||
headers.setdefault("File-Path", encoded_path)
|
headers.setdefault("File-Path", encoded_path)
|
||||||
with open(path, "rb") as f:
|
with open(path, "rb") as f:
|
||||||
resp: Response = RequestUtils(headers=headers).put_res(
|
resp: Response = RequestUtils(headers=headers).put_res(
|
||||||
self.__get_api_url("/api/fs/form"),
|
self.__get_api_url("/api/fs/put"),
|
||||||
data={"file": f},
|
data=f,
|
||||||
)
|
)
|
||||||
|
|
||||||
if resp.status_code != 200:
|
if resp.status_code != 200:
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import copy
|
|
||||||
import json
|
import json
|
||||||
import subprocess
|
import subprocess
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
@@ -57,21 +56,6 @@ class Rclone(StorageBase):
|
|||||||
else:
|
else:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def __get_fileitem(self, path: Path):
|
|
||||||
"""
|
|
||||||
获取文件项
|
|
||||||
"""
|
|
||||||
return schemas.FileItem(
|
|
||||||
storage=self.schema.value,
|
|
||||||
type="file",
|
|
||||||
path=str(path).replace("\\", "/"),
|
|
||||||
name=path.name,
|
|
||||||
basename=path.stem,
|
|
||||||
extension=path.suffix[1:],
|
|
||||||
size=path.stat().st_size,
|
|
||||||
modify_time=path.stat().st_mtime,
|
|
||||||
)
|
|
||||||
|
|
||||||
def __get_rcloneitem(self, item: dict, parent: str = "/") -> schemas.FileItem:
|
def __get_rcloneitem(self, item: dict, parent: str = "/") -> schemas.FileItem:
|
||||||
"""
|
"""
|
||||||
获取rclone文件项
|
获取rclone文件项
|
||||||
@@ -146,12 +130,12 @@ class Rclone(StorageBase):
|
|||||||
retcode = subprocess.run(
|
retcode = subprocess.run(
|
||||||
[
|
[
|
||||||
'rclone', 'mkdir',
|
'rclone', 'mkdir',
|
||||||
f'MP:{fileitem.path}/{name}'
|
f'MP:{Path(fileitem.path) / name}'
|
||||||
],
|
],
|
||||||
startupinfo=self.__get_hidden_shell()
|
startupinfo=self.__get_hidden_shell()
|
||||||
).returncode
|
).returncode
|
||||||
if retcode == 0:
|
if retcode == 0:
|
||||||
return self.get_item(Path(f"{fileitem.path}/{name}"))
|
return self.get_item(Path(fileitem.path) / name)
|
||||||
except Exception as err:
|
except Exception as err:
|
||||||
logger.error(f"rclone创建目录失败:{err}")
|
logger.error(f"rclone创建目录失败:{err}")
|
||||||
return None
|
return None
|
||||||
@@ -200,16 +184,19 @@ class Rclone(StorageBase):
|
|||||||
ret = subprocess.run(
|
ret = subprocess.run(
|
||||||
[
|
[
|
||||||
'rclone', 'lsjson',
|
'rclone', 'lsjson',
|
||||||
f'MP:{path}'
|
f'MP:{path.parent}'
|
||||||
],
|
],
|
||||||
capture_output=True,
|
capture_output=True,
|
||||||
startupinfo=self.__get_hidden_shell()
|
startupinfo=self.__get_hidden_shell()
|
||||||
)
|
)
|
||||||
if ret.returncode == 0:
|
if ret.returncode == 0:
|
||||||
items = json.loads(ret.stdout)
|
items = json.loads(ret.stdout)
|
||||||
return self.__get_rcloneitem(items[0])
|
for item in items:
|
||||||
|
if item.get("Name") == path.name:
|
||||||
|
return self.__get_rcloneitem(item, parent=str(path.parent) + "/")
|
||||||
|
return None
|
||||||
except Exception as err:
|
except Exception as err:
|
||||||
logger.error(f"rclone获取文件失败:{err}")
|
logger.debug(f"rclone获取文件项失败:{err}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def delete(self, fileitem: schemas.FileItem) -> bool:
|
def delete(self, fileitem: schemas.FileItem) -> bool:
|
||||||
@@ -239,7 +226,7 @@ class Rclone(StorageBase):
|
|||||||
[
|
[
|
||||||
'rclone', 'moveto',
|
'rclone', 'moveto',
|
||||||
f'MP:{fileitem.path}',
|
f'MP:{fileitem.path}',
|
||||||
f'MP:{Path(fileitem.path).parent}/{name}'
|
f'MP:{Path(fileitem.path).parent / name}'
|
||||||
],
|
],
|
||||||
startupinfo=self.__get_hidden_shell()
|
startupinfo=self.__get_hidden_shell()
|
||||||
).returncode
|
).returncode
|
||||||
@@ -287,7 +274,7 @@ class Rclone(StorageBase):
|
|||||||
startupinfo=self.__get_hidden_shell()
|
startupinfo=self.__get_hidden_shell()
|
||||||
).returncode
|
).returncode
|
||||||
if retcode == 0:
|
if retcode == 0:
|
||||||
return self.__get_fileitem(new_path)
|
return self.get_item(new_path)
|
||||||
except Exception as err:
|
except Exception as err:
|
||||||
logger.error(f"rclone上传文件失败:{err}")
|
logger.error(f"rclone上传文件失败:{err}")
|
||||||
return None
|
return None
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ from app.helper.rule import RuleHelper
|
|||||||
from app.log import logger
|
from app.log import logger
|
||||||
from app.modules import _ModuleBase
|
from app.modules import _ModuleBase
|
||||||
from app.modules.filter.RuleParser import RuleParser
|
from app.modules.filter.RuleParser import RuleParser
|
||||||
from app.schemas.types import ModuleType
|
from app.schemas.types import ModuleType, OtherModulesType
|
||||||
from app.utils.string import StringUtils
|
from app.utils.string import StringUtils
|
||||||
|
|
||||||
|
|
||||||
@@ -167,6 +167,13 @@ class FilterModule(_ModuleBase):
|
|||||||
"""
|
"""
|
||||||
return ModuleType.Other
|
return ModuleType.Other
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_subtype() -> OtherModulesType:
|
||||||
|
"""
|
||||||
|
获取模块子类型
|
||||||
|
"""
|
||||||
|
return OtherModulesType.Filter
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_priority() -> int:
|
def get_priority() -> int:
|
||||||
"""
|
"""
|
||||||
@@ -359,6 +366,8 @@ class FilterModule(_ModuleBase):
|
|||||||
seeders = self.rule_set[rule_name].get("seeders")
|
seeders = self.rule_set[rule_name].get("seeders")
|
||||||
# FREE规则
|
# FREE规则
|
||||||
downloadvolumefactor = self.rule_set[rule_name].get("downloadvolumefactor")
|
downloadvolumefactor = self.rule_set[rule_name].get("downloadvolumefactor")
|
||||||
|
# 发布时间规则
|
||||||
|
pubdate: str = self.rule_set[rule_name].get("publish_time")
|
||||||
if includes and not any(re.search(r"%s" % include, content, re.IGNORECASE) for include in includes):
|
if includes and not any(re.search(r"%s" % include, content, re.IGNORECASE) for include in includes):
|
||||||
# 未发现任何包含项
|
# 未发现任何包含项
|
||||||
logger.debug(f"种子 {torrent.site_name} - {torrent.title} 不包含任何项 {includes}")
|
logger.debug(f"种子 {torrent.site_name} - {torrent.title} 不包含任何项 {includes}")
|
||||||
@@ -385,6 +394,22 @@ class FilterModule(_ModuleBase):
|
|||||||
logger.debug(
|
logger.debug(
|
||||||
f"种子 {torrent.site_name} - {torrent.title} FREE值 {torrent.downloadvolumefactor} 不是 {downloadvolumefactor}")
|
f"种子 {torrent.site_name} - {torrent.title} FREE值 {torrent.downloadvolumefactor} 不是 {downloadvolumefactor}")
|
||||||
return False
|
return False
|
||||||
|
if pubdate:
|
||||||
|
# 种子发布时间
|
||||||
|
pub_minutes = torrent.pub_minutes()
|
||||||
|
# 发布时间规则
|
||||||
|
pub_times = [float(t) for t in pubdate.split("-")]
|
||||||
|
if len(pub_times) == 1:
|
||||||
|
# 发布时间小于规则
|
||||||
|
if pub_minutes < pub_times[0]:
|
||||||
|
logger.debug(f"种子 {torrent.site_name} - {torrent.title} 发布时间 {pub_minutes} 小于 {pub_times[0]}")
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
# 区间
|
||||||
|
if not (pub_times[0] <= pub_minutes <= pub_times[1]):
|
||||||
|
logger.debug(f"种子 {torrent.site_name} - {torrent.title} 发布时间 {pub_minutes} 不在 {pub_times[0]}-{pub_times[1]} 时间区间")
|
||||||
|
return False
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def __match_tmdb(self, tmdb: dict) -> bool:
|
def __match_tmdb(self, tmdb: dict) -> bool:
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ from app.modules.indexer.spider.tnode import TNodeSpider
|
|||||||
from app.modules.indexer.spider.torrentleech import TorrentLeech
|
from app.modules.indexer.spider.torrentleech import TorrentLeech
|
||||||
from app.modules.indexer.spider.yema import YemaSpider
|
from app.modules.indexer.spider.yema import YemaSpider
|
||||||
from app.schemas import SiteUserData
|
from app.schemas import SiteUserData
|
||||||
from app.schemas.types import MediaType, ModuleType
|
from app.schemas.types import MediaType, ModuleType, OtherModulesType
|
||||||
from app.utils.string import StringUtils
|
from app.utils.string import StringUtils
|
||||||
|
|
||||||
|
|
||||||
@@ -47,6 +47,13 @@ class IndexerModule(_ModuleBase):
|
|||||||
"""
|
"""
|
||||||
return ModuleType.Indexer
|
return ModuleType.Indexer
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_subtype() -> OtherModulesType:
|
||||||
|
"""
|
||||||
|
获取模块子类型
|
||||||
|
"""
|
||||||
|
return OtherModulesType.Indexer
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_priority() -> int:
|
def get_priority() -> int:
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ 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, AuthInterceptCredentials
|
from app.schemas.event import AuthCredentials, AuthInterceptCredentials
|
||||||
from app.schemas.types import MediaType, ModuleType, ChainEventType
|
from app.schemas.types import MediaType, ModuleType, ChainEventType, MediaServerType
|
||||||
|
|
||||||
|
|
||||||
class JellyfinModule(_ModuleBase, _MediaServerBase[Jellyfin]):
|
class JellyfinModule(_ModuleBase, _MediaServerBase[Jellyfin]):
|
||||||
@@ -30,6 +30,13 @@ class JellyfinModule(_ModuleBase, _MediaServerBase[Jellyfin]):
|
|||||||
"""
|
"""
|
||||||
return ModuleType.MediaServer
|
return ModuleType.MediaServer
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_subtype() -> MediaServerType:
|
||||||
|
"""
|
||||||
|
获取模块子类型
|
||||||
|
"""
|
||||||
|
return MediaServerType.Jellyfin
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_priority() -> int:
|
def get_priority() -> int:
|
||||||
"""
|
"""
|
||||||
@@ -66,16 +73,26 @@ class JellyfinModule(_ModuleBase, _MediaServerBase[Jellyfin]):
|
|||||||
return False, f"无法连接Jellyfin服务器:{name}"
|
return False, f"无法连接Jellyfin服务器:{name}"
|
||||||
return True, ""
|
return True, ""
|
||||||
|
|
||||||
def user_authenticate(self, credentials: AuthCredentials) -> Optional[AuthCredentials]:
|
def user_authenticate(self, credentials: AuthCredentials, service_name: Optional[str] = None) \
|
||||||
|
-> Optional[AuthCredentials]:
|
||||||
"""
|
"""
|
||||||
使用Jellyfin用户辅助完成用户认证
|
使用Jellyfin用户辅助完成用户认证
|
||||||
:param credentials: 认证数据
|
:param credentials: 认证数据
|
||||||
|
:param service_name: 指定要认证的媒体服务器名称,若为 None 则认证所有服务
|
||||||
:return: 认证数据
|
:return: 认证数据
|
||||||
"""
|
"""
|
||||||
# Jellyfin认证
|
# 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():
|
# 确定要认证的服务器列表
|
||||||
|
if service_name:
|
||||||
|
# 如果指定了服务名,获取该服务实例
|
||||||
|
servers = [(service_name, server)] if (server := self.get_instance(service_name)) else []
|
||||||
|
else:
|
||||||
|
# 如果没有指定服务名,遍历所有服务
|
||||||
|
servers = self.get_instances().items()
|
||||||
|
# 遍历要认证的服务器
|
||||||
|
for name, server in servers:
|
||||||
# 触发认证拦截事件
|
# 触发认证拦截事件
|
||||||
intercept_event = eventmanager.send_event(
|
intercept_event = eventmanager.send_event(
|
||||||
etype=ChainEventType.AuthIntercept,
|
etype=ChainEventType.AuthIntercept,
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ 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.event import AuthCredentials, AuthInterceptCredentials
|
from app.schemas.event import AuthCredentials, AuthInterceptCredentials
|
||||||
from app.schemas.types import MediaType, ModuleType, ChainEventType
|
from app.schemas.types import MediaType, ModuleType, ChainEventType, MediaServerType
|
||||||
|
|
||||||
|
|
||||||
class PlexModule(_ModuleBase, _MediaServerBase[Plex]):
|
class PlexModule(_ModuleBase, _MediaServerBase[Plex]):
|
||||||
@@ -30,6 +30,13 @@ class PlexModule(_ModuleBase, _MediaServerBase[Plex]):
|
|||||||
"""
|
"""
|
||||||
return ModuleType.MediaServer
|
return ModuleType.MediaServer
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_subtype() -> MediaServerType:
|
||||||
|
"""
|
||||||
|
获取模块子类型
|
||||||
|
"""
|
||||||
|
return MediaServerType.Plex
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_priority() -> int:
|
def get_priority() -> int:
|
||||||
"""
|
"""
|
||||||
@@ -66,16 +73,26 @@ 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]:
|
def user_authenticate(self, credentials: AuthCredentials, service_name: Optional[str] = None) \
|
||||||
|
-> Optional[AuthCredentials]:
|
||||||
"""
|
"""
|
||||||
使用Plex用户辅助完成用户认证
|
使用Plex用户辅助完成用户认证
|
||||||
:param credentials: 认证数据
|
:param credentials: 认证数据
|
||||||
|
:param service_name: 指定要认证的媒体服务器名称,若为 None 则认证所有服务
|
||||||
:return: 认证数据
|
:return: 认证数据
|
||||||
"""
|
"""
|
||||||
# Plex认证
|
# Plex认证
|
||||||
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():
|
# 确定要认证的服务器列表
|
||||||
|
if service_name:
|
||||||
|
# 如果指定了服务名,获取该服务实例
|
||||||
|
servers = [(service_name, server)] if (server := self.get_instance(service_name)) else []
|
||||||
|
else:
|
||||||
|
# 如果没有指定服务名,遍历所有服务
|
||||||
|
servers = self.get_instances().items()
|
||||||
|
# 遍历要认证的服务器
|
||||||
|
for name, server in servers:
|
||||||
# 触发认证拦截事件
|
# 触发认证拦截事件
|
||||||
intercept_event = eventmanager.send_event(
|
intercept_event = eventmanager.send_event(
|
||||||
etype=ChainEventType.AuthIntercept,
|
etype=ChainEventType.AuthIntercept,
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ from app.log import logger
|
|||||||
from app.modules import _ModuleBase, _DownloaderBase
|
from app.modules import _ModuleBase, _DownloaderBase
|
||||||
from app.modules.qbittorrent.qbittorrent import Qbittorrent
|
from app.modules.qbittorrent.qbittorrent import Qbittorrent
|
||||||
from app.schemas import TransferTorrent, DownloadingTorrent
|
from app.schemas import TransferTorrent, DownloadingTorrent
|
||||||
from app.schemas.types import TorrentStatus, ModuleType
|
from app.schemas.types import TorrentStatus, ModuleType, DownloaderType
|
||||||
from app.utils.string import StringUtils
|
from app.utils.string import StringUtils
|
||||||
|
|
||||||
|
|
||||||
@@ -35,6 +35,13 @@ class QbittorrentModule(_ModuleBase, _DownloaderBase[Qbittorrent]):
|
|||||||
"""
|
"""
|
||||||
return ModuleType.Downloader
|
return ModuleType.Downloader
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_subtype() -> DownloaderType:
|
||||||
|
"""
|
||||||
|
获取模块子类型
|
||||||
|
"""
|
||||||
|
return DownloaderType.Qbittorrent
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_priority() -> int:
|
def get_priority() -> int:
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -31,6 +31,13 @@ class SlackModule(_ModuleBase, _MessageBase[Slack]):
|
|||||||
"""
|
"""
|
||||||
return ModuleType.Notification
|
return ModuleType.Notification
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_subtype() -> MessageChannel:
|
||||||
|
"""
|
||||||
|
获取模块子类型
|
||||||
|
"""
|
||||||
|
return MessageChannel.Slack
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_priority() -> int:
|
def get_priority() -> int:
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ from app.core.context import Context
|
|||||||
from app.helper.torrent import TorrentHelper
|
from app.helper.torrent import TorrentHelper
|
||||||
from app.log import logger
|
from app.log import logger
|
||||||
from app.modules import _ModuleBase
|
from app.modules import _ModuleBase
|
||||||
from app.schemas.types import ModuleType
|
from app.schemas.types import ModuleType, OtherModulesType
|
||||||
from app.utils.http import RequestUtils
|
from app.utils.http import RequestUtils
|
||||||
from app.utils.string import StringUtils
|
from app.utils.string import StringUtils
|
||||||
from app.utils.system import SystemUtils
|
from app.utils.system import SystemUtils
|
||||||
@@ -40,6 +40,13 @@ class SubtitleModule(_ModuleBase):
|
|||||||
"""
|
"""
|
||||||
return ModuleType.Other
|
return ModuleType.Other
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_subtype() -> OtherModulesType:
|
||||||
|
"""
|
||||||
|
获取模块子类型
|
||||||
|
"""
|
||||||
|
return OtherModulesType.Subtitle
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_priority() -> int:
|
def get_priority() -> int:
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -29,6 +29,13 @@ class SynologyChatModule(_ModuleBase, _MessageBase[SynologyChat]):
|
|||||||
"""
|
"""
|
||||||
return ModuleType.Notification
|
return ModuleType.Notification
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_subtype() -> MessageChannel:
|
||||||
|
"""
|
||||||
|
获取模块子类型
|
||||||
|
"""
|
||||||
|
return MessageChannel.SynologyChat
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_priority() -> int:
|
def get_priority() -> int:
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -34,6 +34,13 @@ class TelegramModule(_ModuleBase, _MessageBase[Telegram]):
|
|||||||
"""
|
"""
|
||||||
return ModuleType.Notification
|
return ModuleType.Notification
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_subtype() -> MessageChannel:
|
||||||
|
"""
|
||||||
|
获取模块子类型
|
||||||
|
"""
|
||||||
|
return MessageChannel.Telegram
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_priority() -> int:
|
def get_priority() -> int:
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ from app.modules.themoviedb.scraper import TmdbScraper
|
|||||||
from app.modules.themoviedb.tmdb_cache import TmdbCache
|
from app.modules.themoviedb.tmdb_cache import TmdbCache
|
||||||
from app.modules.themoviedb.tmdbapi import TmdbApi
|
from app.modules.themoviedb.tmdbapi import TmdbApi
|
||||||
from app.schemas import MediaPerson
|
from app.schemas import MediaPerson
|
||||||
from app.schemas.types import MediaType, MediaImageType, ModuleType
|
from app.schemas.types import MediaType, MediaImageType, ModuleType, MediaRecognizeType
|
||||||
from app.utils.http import RequestUtils
|
from app.utils.http import RequestUtils
|
||||||
|
|
||||||
|
|
||||||
@@ -49,6 +49,13 @@ class TheMovieDbModule(_ModuleBase):
|
|||||||
"""
|
"""
|
||||||
return ModuleType.MediaRecognize
|
return ModuleType.MediaRecognize
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_subtype() -> MediaRecognizeType:
|
||||||
|
"""
|
||||||
|
获取模块子类型
|
||||||
|
"""
|
||||||
|
return MediaRecognizeType.TMDB
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_priority() -> int:
|
def get_priority() -> int:
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -161,7 +161,7 @@ class TmdbApi:
|
|||||||
season_number: int = None) -> Optional[dict]:
|
season_number: int = None) -> Optional[dict]:
|
||||||
"""
|
"""
|
||||||
搜索tmdb中的媒体信息,匹配返回一条尽可能正确的信息
|
搜索tmdb中的媒体信息,匹配返回一条尽可能正确的信息
|
||||||
:param name: 剑索的名称
|
:param name: 检索的名称
|
||||||
:param mtype: 类型:电影、电视剧
|
:param mtype: 类型:电影、电视剧
|
||||||
:param year: 年份,如要是季集需要是首播年份(first_air_date)
|
:param year: 年份,如要是季集需要是首播年份(first_air_date)
|
||||||
:param season_year: 当前季集年份
|
:param season_year: 当前季集年份
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ from app.core.config import settings
|
|||||||
from app.log import logger
|
from app.log import logger
|
||||||
from app.modules import _ModuleBase
|
from app.modules import _ModuleBase
|
||||||
from app.modules.thetvdb import tvdbapi
|
from app.modules.thetvdb import tvdbapi
|
||||||
from app.schemas.types import ModuleType
|
from app.schemas.types import ModuleType, MediaRecognizeType
|
||||||
from app.utils.http import RequestUtils
|
from app.utils.http import RequestUtils
|
||||||
|
|
||||||
|
|
||||||
@@ -28,6 +28,13 @@ class TheTvDbModule(_ModuleBase):
|
|||||||
"""
|
"""
|
||||||
return ModuleType.MediaRecognize
|
return ModuleType.MediaRecognize
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_subtype() -> MediaRecognizeType:
|
||||||
|
"""
|
||||||
|
获取模块子类型
|
||||||
|
"""
|
||||||
|
return MediaRecognizeType.TVDB
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_priority() -> int:
|
def get_priority() -> int:
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ from app.log import logger
|
|||||||
from app.modules import _ModuleBase, _DownloaderBase
|
from app.modules import _ModuleBase, _DownloaderBase
|
||||||
from app.modules.transmission.transmission import Transmission
|
from app.modules.transmission.transmission import Transmission
|
||||||
from app.schemas import TransferTorrent, DownloadingTorrent
|
from app.schemas import TransferTorrent, DownloadingTorrent
|
||||||
from app.schemas.types import TorrentStatus, ModuleType
|
from app.schemas.types import TorrentStatus, ModuleType, DownloaderType
|
||||||
from app.utils.string import StringUtils
|
from app.utils.string import StringUtils
|
||||||
|
|
||||||
|
|
||||||
@@ -35,6 +35,13 @@ class TransmissionModule(_ModuleBase, _DownloaderBase[Transmission]):
|
|||||||
"""
|
"""
|
||||||
return ModuleType.Downloader
|
return ModuleType.Downloader
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_subtype() -> DownloaderType:
|
||||||
|
"""
|
||||||
|
获取模块子类型
|
||||||
|
"""
|
||||||
|
return DownloaderType.Transmission
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_priority() -> int:
|
def get_priority() -> int:
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -30,6 +30,13 @@ class VoceChatModule(_ModuleBase, _MessageBase[VoceChat]):
|
|||||||
"""
|
"""
|
||||||
return ModuleType.Notification
|
return ModuleType.Notification
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_subtype() -> MessageChannel:
|
||||||
|
"""
|
||||||
|
获取模块子类型
|
||||||
|
"""
|
||||||
|
return MessageChannel.VoceChat
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_priority() -> int:
|
def get_priority() -> int:
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -30,6 +30,13 @@ class WebPushModule(_ModuleBase, _MessageBase):
|
|||||||
"""
|
"""
|
||||||
return ModuleType.Notification
|
return ModuleType.Notification
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_subtype() -> MessageChannel:
|
||||||
|
"""
|
||||||
|
获取模块子类型
|
||||||
|
"""
|
||||||
|
return MessageChannel.WebPush
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_priority() -> int:
|
def get_priority() -> int:
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -36,6 +36,13 @@ class WechatModule(_ModuleBase, _MessageBase[WeChat]):
|
|||||||
"""
|
"""
|
||||||
return ModuleType.Notification
|
return ModuleType.Notification
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_subtype() -> MessageChannel:
|
||||||
|
"""
|
||||||
|
获取模块的子类型
|
||||||
|
"""
|
||||||
|
return MessageChannel.Wechat
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_priority() -> int:
|
def get_priority() -> int:
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -225,7 +225,7 @@ class _PluginBase(metaclass=ABCMeta):
|
|||||||
return self.plugindata.del_data(plugin_id, key)
|
return self.plugindata.del_data(plugin_id, key)
|
||||||
|
|
||||||
def post_message(self, channel: MessageChannel = None, mtype: NotificationType = None, title: str = None,
|
def post_message(self, channel: MessageChannel = None, mtype: NotificationType = None, title: str = None,
|
||||||
text: str = None, image: str = None, link: str = None, userid: str = None):
|
text: str = None, image: str = None, link: str = None, userid: str = None, username: str = None):
|
||||||
"""
|
"""
|
||||||
发送消息
|
发送消息
|
||||||
"""
|
"""
|
||||||
@@ -233,7 +233,7 @@ class _PluginBase(metaclass=ABCMeta):
|
|||||||
link = settings.MP_DOMAIN(f"#/plugins?tab=installed&id={self.__class__.__name__}")
|
link = settings.MP_DOMAIN(f"#/plugins?tab=installed&id={self.__class__.__name__}")
|
||||||
self.chain.post_message(Notification(
|
self.chain.post_message(Notification(
|
||||||
channel=channel, mtype=mtype, title=title, text=text,
|
channel=channel, mtype=mtype, title=title, text=text,
|
||||||
image=image, link=link, userid=userid
|
image=image, link=link, userid=userid, username=username
|
||||||
))
|
))
|
||||||
|
|
||||||
def close(self):
|
def close(self):
|
||||||
|
|||||||
@@ -297,7 +297,8 @@ class Scheduler(metaclass=Singleton):
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
# 站点数据刷新,每隔30分钟
|
# 站点数据刷新
|
||||||
|
if settings.SITEDATA_REFRESH_INTERVAL:
|
||||||
self._scheduler.add_job(
|
self._scheduler.add_job(
|
||||||
self.start,
|
self.start,
|
||||||
"interval",
|
"interval",
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Optional, Dict, Any
|
from typing import Optional, Dict, Any, List, Set
|
||||||
|
|
||||||
from pydantic import BaseModel, Field, root_validator
|
from pydantic import BaseModel, Field, root_validator
|
||||||
|
|
||||||
|
from app.core.context import Context
|
||||||
|
from app.schemas import MessageChannel
|
||||||
|
|
||||||
|
|
||||||
class BaseEventData(BaseModel):
|
class BaseEventData(BaseModel):
|
||||||
"""
|
"""
|
||||||
@@ -143,3 +146,60 @@ class TransferRenameEventData(ChainEventData):
|
|||||||
updated: bool = Field(False, description="是否已更新")
|
updated: bool = Field(False, description="是否已更新")
|
||||||
updated_str: Optional[str] = Field(None, description="更新后的字符串")
|
updated_str: Optional[str] = Field(None, description="更新后的字符串")
|
||||||
source: Optional[str] = Field("未知拦截源", description="拦截源")
|
source: Optional[str] = Field("未知拦截源", description="拦截源")
|
||||||
|
|
||||||
|
|
||||||
|
class ResourceSelectionEventData(BaseModel):
|
||||||
|
"""
|
||||||
|
ResourceSelection 事件的数据模型
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
# 输入参数
|
||||||
|
contexts (List[Context]): 当前待选择的资源上下文列表
|
||||||
|
source (str): 事件源,指示事件的触发来源
|
||||||
|
|
||||||
|
# 输出参数
|
||||||
|
updated (bool): 是否已更新,默认值为 False
|
||||||
|
updated_contexts (Optional[List[Context]]): 已更新的资源上下文列表,默认值为 None
|
||||||
|
source (str): 更新源,默认值为 "未知更新源"
|
||||||
|
"""
|
||||||
|
# 输入参数
|
||||||
|
contexts: Any = Field(None, description="待选择的资源上下文列表")
|
||||||
|
downloader: Optional[str] = Field(None, description="下载器")
|
||||||
|
origin: Optional[str] = Field(None, description="来源")
|
||||||
|
|
||||||
|
# 输出参数
|
||||||
|
updated: bool = Field(False, description="是否已更新")
|
||||||
|
updated_contexts: Optional[List[Context]] = Field(None, description="已更新的资源上下文列表")
|
||||||
|
source: Optional[str] = Field("未知拦截源", description="拦截源")
|
||||||
|
|
||||||
|
|
||||||
|
class ResourceDownloadEventData(ChainEventData):
|
||||||
|
"""
|
||||||
|
ResourceDownload 事件的数据模型
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
# 输入参数
|
||||||
|
context (Context): 当前资源上下文
|
||||||
|
episodes (Set[int]): 需要下载的集数
|
||||||
|
channel (MessageChannel): 通知渠道
|
||||||
|
origin (str): 来源(消息通知、Subscribe、Manual等)
|
||||||
|
downloader (str): 下载器
|
||||||
|
options (dict): 其他参数
|
||||||
|
|
||||||
|
# 输出参数
|
||||||
|
cancel (bool): 是否取消下载,默认值为 False
|
||||||
|
source (str): 拦截源,默认值为 "未知拦截源"
|
||||||
|
reason (str): 拦截原因,描述拦截的具体原因
|
||||||
|
"""
|
||||||
|
# 输入参数
|
||||||
|
context: Any = Field(None, description="当前资源上下文")
|
||||||
|
episodes: Optional[Set[int]] = Field(None, description="需要下载的集数")
|
||||||
|
channel: Optional[MessageChannel] = Field(None, description="通知渠道")
|
||||||
|
origin: Optional[str] = Field(None, description="来源")
|
||||||
|
downloader: Optional[str] = Field(None, description="下载器")
|
||||||
|
options: Optional[dict] = Field(None, description="其他参数")
|
||||||
|
|
||||||
|
# 输出参数
|
||||||
|
cancel: bool = Field(False, description="是否取消下载")
|
||||||
|
source: str = Field("未知拦截源", description="拦截源")
|
||||||
|
reason: str = Field("", description="拦截原因")
|
||||||
|
|||||||
@@ -89,3 +89,42 @@ class EpisodeFormat(BaseModel):
|
|||||||
detail: Optional[str] = None
|
detail: Optional[str] = None
|
||||||
part: Optional[str] = None
|
part: Optional[str] = None
|
||||||
offset: Optional[str] = None
|
offset: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
|
class ManualTransferItem(BaseModel):
|
||||||
|
# 文件项
|
||||||
|
fileitem: FileItem = None
|
||||||
|
# 日志ID
|
||||||
|
logid: Optional[int] = None
|
||||||
|
# 目标存储
|
||||||
|
target_storage: Optional[str] = None
|
||||||
|
# 目标路径
|
||||||
|
target_path: Optional[str] = None
|
||||||
|
# TMDB ID
|
||||||
|
tmdbid: Optional[int] = None
|
||||||
|
# 豆瓣ID
|
||||||
|
doubanid: Optional[str] = None
|
||||||
|
# 类型
|
||||||
|
type_name: Optional[str] = None
|
||||||
|
# 季号
|
||||||
|
season: Optional[int] = None
|
||||||
|
# 整理方式
|
||||||
|
transfer_type: Optional[str] = None
|
||||||
|
# 自定义格式
|
||||||
|
episode_format: Optional[str] = None
|
||||||
|
# 指定集数
|
||||||
|
episode_detail: Optional[str] = None
|
||||||
|
# 指定PART
|
||||||
|
episode_part: Optional[str] = None
|
||||||
|
# 集数偏移
|
||||||
|
episode_offset: Optional[str] = None
|
||||||
|
# 最小文件大小
|
||||||
|
min_filesize: Optional[int] = 0
|
||||||
|
# 刮削
|
||||||
|
scrape: bool = False
|
||||||
|
# 媒体库类型子目录
|
||||||
|
library_type_folder: Optional[bool] = None
|
||||||
|
# 媒体库类别子目录
|
||||||
|
library_category_folder: Optional[bool] = None
|
||||||
|
# 复用历史识别信息
|
||||||
|
from_history: Optional[bool] = False
|
||||||
|
|||||||
@@ -48,6 +48,8 @@ class EventType(Enum):
|
|||||||
NoticeMessage = "notice.message"
|
NoticeMessage = "notice.message"
|
||||||
# 订阅已添加
|
# 订阅已添加
|
||||||
SubscribeAdded = "subscribe.added"
|
SubscribeAdded = "subscribe.added"
|
||||||
|
# 订阅已删除
|
||||||
|
SubscribeDeleted = "subscribe.deleted"
|
||||||
# 订阅已完成
|
# 订阅已完成
|
||||||
SubscribeComplete = "subscribe.complete"
|
SubscribeComplete = "subscribe.complete"
|
||||||
# 系统错误
|
# 系统错误
|
||||||
@@ -70,6 +72,10 @@ class ChainEventType(Enum):
|
|||||||
CommandRegister = "command.register"
|
CommandRegister = "command.register"
|
||||||
# 整理重命名
|
# 整理重命名
|
||||||
TransferRename = "transfer.rename"
|
TransferRename = "transfer.rename"
|
||||||
|
# 资源选择
|
||||||
|
ResourceSelection = "resource.selection"
|
||||||
|
# 资源下载
|
||||||
|
ResourceDownload = "resource.download"
|
||||||
|
|
||||||
|
|
||||||
# 系统配置Key字典
|
# 系统配置Key字典
|
||||||
@@ -178,6 +184,52 @@ class MessageChannel(Enum):
|
|||||||
WebPush = "WebPush"
|
WebPush = "WebPush"
|
||||||
|
|
||||||
|
|
||||||
|
# 下载器类型
|
||||||
|
class DownloaderType(Enum):
|
||||||
|
# Qbittorrent
|
||||||
|
Qbittorrent = "Qbittorrent"
|
||||||
|
# Transmission
|
||||||
|
Transmission = "Transmission"
|
||||||
|
# Aria2
|
||||||
|
# Aria2 = "Aria2"
|
||||||
|
|
||||||
|
|
||||||
|
# 媒体服务器类型
|
||||||
|
class MediaServerType(Enum):
|
||||||
|
# Emby
|
||||||
|
Emby = "Emby"
|
||||||
|
# Jellyfin
|
||||||
|
Jellyfin = "Jellyfin"
|
||||||
|
# Plex
|
||||||
|
Plex = "Plex"
|
||||||
|
|
||||||
|
|
||||||
|
# 识别器类型
|
||||||
|
class MediaRecognizeType(Enum):
|
||||||
|
# 豆瓣
|
||||||
|
Douban = "豆瓣"
|
||||||
|
# TMDB
|
||||||
|
TMDB = "TheMovieDb"
|
||||||
|
# TVDB
|
||||||
|
TVDB = "TheTvDb"
|
||||||
|
# bangumi
|
||||||
|
Bangumi = "Bangumi"
|
||||||
|
|
||||||
|
|
||||||
|
# 其他杂项模块类型
|
||||||
|
class OtherModulesType(Enum):
|
||||||
|
# 字幕
|
||||||
|
Subtitle = "站点字幕"
|
||||||
|
# Fanart
|
||||||
|
Fanart = "Fanart"
|
||||||
|
# 文件整理
|
||||||
|
FileManager = "文件整理"
|
||||||
|
# 过滤器
|
||||||
|
Filter = "过滤器"
|
||||||
|
# 站点索引
|
||||||
|
Indexer = "站点索引"
|
||||||
|
|
||||||
|
|
||||||
# 用户配置Key字典
|
# 用户配置Key字典
|
||||||
class UserConfigKey(Enum):
|
class UserConfigKey(Enum):
|
||||||
# 监控面板
|
# 监控面板
|
||||||
|
|||||||
@@ -58,6 +58,6 @@ pystray~=0.19.5
|
|||||||
pyotp~=2.9.0
|
pyotp~=2.9.0
|
||||||
Pinyin2Hanzi~=0.1.1
|
Pinyin2Hanzi~=0.1.1
|
||||||
pywebpush~=2.0.0
|
pywebpush~=2.0.0
|
||||||
python-115~=0.0.9.8.7
|
python-115~=0.0.9.8.8.3
|
||||||
aligo~=6.2.4
|
aligo~=6.2.4
|
||||||
aiofiles~=24.1.0
|
aiofiles~=24.1.0
|
||||||
@@ -1,2 +1,2 @@
|
|||||||
APP_VERSION = 'v2.1.1'
|
APP_VERSION = 'v2.1.3'
|
||||||
FRONTEND_VERSION = 'v2.1.1'
|
FRONTEND_VERSION = 'v2.1.4'
|
||||||
|
|||||||
Reference in New Issue
Block a user