mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-05-11 18:10:15 +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,9 +518,14 @@ 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)
|
||||||
# 统计订阅
|
# 发送事件
|
||||||
SubscribeHelper().sub_done_async({
|
eventmanager.send_event(EventType.SubscribeDeleted, {
|
||||||
"tmdbid": subscribe.tmdbid,
|
"subscribe_id": subscribe_id,
|
||||||
"doubanid": subscribe.doubanid
|
"subscribe": subscribe.to_dict()
|
||||||
})
|
})
|
||||||
|
# 统计订阅
|
||||||
|
SubscribeHelper().sub_done_async({
|
||||||
|
"tmdbid": subscribe.tmdbid,
|
||||||
|
"doubanid": subscribe.doubanid
|
||||||
|
})
|
||||||
return schemas.Response(success=True)
|
return schemas.Response(success=True)
|
||||||
|
|||||||
@@ -387,7 +387,9 @@ def ruletest(title: str,
|
|||||||
return schemas.Response(success=False, message=f"过滤规则组 {rulegroup_name} 不存在!")
|
return schemas.Response(success=False, message=f"过滤规则组 {rulegroup_name} 不存在!")
|
||||||
|
|
||||||
# 根据标题查询媒体信息
|
# 根据标题查询媒体信息
|
||||||
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"【站点分享率低预警】",
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -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,23 +392,24 @@ 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:
|
||||||
# 按源目录匹配,以便找到更合适的目录配置
|
# 按源目录匹配,以便找到更合适的目录配置
|
||||||
dir_info = self.directoryhelper.get_dir(media=file_mediainfo,
|
dir_info = self.directoryhelper.get_dir(media=file_mediainfo,
|
||||||
storage=file_item.storage,
|
storage=file_item.storage,
|
||||||
src_path=file_path,
|
src_path=file_path,
|
||||||
target_storage=target_storage)
|
target_storage=target_storage)
|
||||||
elif target_path:
|
elif target_path:
|
||||||
# 指定目标路径,`手动整理`场景下使用,忽略源目录匹配,使用指定目录匹配
|
# 指定目标路径,`手动整理`场景下使用,忽略源目录匹配,使用指定目录匹配
|
||||||
dir_info = self.directoryhelper.get_dir(media=file_mediainfo,
|
dir_info = self.directoryhelper.get_dir(media=file_mediainfo,
|
||||||
dest_path=target_path,
|
dest_path=target_path,
|
||||||
target_storage=target_storage)
|
target_storage=target_storage)
|
||||||
else:
|
else:
|
||||||
# 未指定目标路径,根据媒体信息获取目标目录
|
# 未指定目标路径,根据媒体信息获取目标目录
|
||||||
dir_info = self.directoryhelper.get_dir(file_mediainfo,
|
dir_info = self.directoryhelper.get_dir(file_mediainfo,
|
||||||
storage=file_item.storage,
|
storage=file_item.storage,
|
||||||
target_storage=target_storage)
|
target_storage=target_storage)
|
||||||
|
|
||||||
# 执行整理
|
# 执行整理
|
||||||
transferinfo: TransferInfo = self.transfer(fileitem=file_item,
|
transferinfo: TransferInfo = self.transfer(fileitem=file_item,
|
||||||
|
|||||||
@@ -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,23 +518,18 @@ 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]
|
||||||
|
|
||||||
# 遍历列表,处理每个事件类型
|
# 遍历列表,处理每个事件类型
|
||||||
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,7 +83,8 @@ class SubscribeOper(DbOper):
|
|||||||
更新订阅
|
更新订阅
|
||||||
"""
|
"""
|
||||||
subscribe = self.get(sid)
|
subscribe = self.get(sid)
|
||||||
subscribe.update(self._db, payload)
|
if subscribe:
|
||||||
|
subscribe.update(self._db, payload)
|
||||||
return subscribe
|
return subscribe
|
||||||
|
|
||||||
def list_by_tmdbid(self, tmdbid: int, season: int = None) -> List[Subscribe]:
|
def list_by_tmdbid(self, tmdbid: int, season: int = None) -> List[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
|
||||||
|
|
||||||
|
|
||||||
@@ -44,6 +44,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,20 +384,30 @@ class FanartModule(_ModuleBase):
|
|||||||
continue
|
continue
|
||||||
if not isinstance(images, list):
|
if not isinstance(images, list):
|
||||||
continue
|
continue
|
||||||
# 按欢迎程度倒排
|
|
||||||
images.sort(key=lambda x: int(x.get('likes', 0)), reverse=True)
|
|
||||||
# 取第一张图片
|
|
||||||
image_obj = images[0]
|
|
||||||
# 图片属性xx_path
|
# 图片属性xx_path
|
||||||
image_name = self.__name(name)
|
image_name = self.__name(name)
|
||||||
image_season = image_obj.get('season')
|
if image_name.startswith("season"):
|
||||||
# 设置图片
|
# 季图片,图片格式seasonxx-xxxx/season-specials-xxxx
|
||||||
if image_name.startswith("season") and image_season:
|
for image_obj in images:
|
||||||
# 季图片格式 seasonxx-poster
|
image_season = image_obj.get('season')
|
||||||
image_name = f"season{str(image_season).rjust(2, '0')}-{image_name[6:]}"
|
if image_season is not None:
|
||||||
if not mediainfo.get_image(image_name):
|
# 包括poster,thumb,banner
|
||||||
# 没有图片才设置
|
if image_season == '0':
|
||||||
mediainfo.set_image(image_name, image_obj.get('url'))
|
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)
|
||||||
|
# 取第一张图片
|
||||||
|
image_obj = images[0]
|
||||||
|
# 设置图片,没有图片才设置
|
||||||
|
if not mediainfo.get_image(image_name):
|
||||||
|
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:
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -97,7 +97,7 @@ class YemaSpider:
|
|||||||
results = res.json().get('data', []) or []
|
results = res.json().get('data', []) or []
|
||||||
for result in results:
|
for result in results:
|
||||||
category_value = result.get('categoryId')
|
category_value = result.get('categoryId')
|
||||||
if category_value in self._tv_category :
|
if category_value in self._tv_category:
|
||||||
category = MediaType.TV.value
|
category = MediaType.TV.value
|
||||||
elif category_value in self._movie_category:
|
elif category_value in self._movie_category:
|
||||||
category = MediaType.MOVIE.value
|
category = MediaType.MOVIE.value
|
||||||
|
|||||||
@@ -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,17 +297,18 @@ class Scheduler(metaclass=Singleton):
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
# 站点数据刷新,每隔30分钟
|
# 站点数据刷新
|
||||||
self._scheduler.add_job(
|
if settings.SITEDATA_REFRESH_INTERVAL:
|
||||||
self.start,
|
self._scheduler.add_job(
|
||||||
"interval",
|
self.start,
|
||||||
id="sitedata_refresh",
|
"interval",
|
||||||
name="站点数据刷新",
|
id="sitedata_refresh",
|
||||||
minutes=settings.SITEDATA_REFRESH_INTERVAL * 60,
|
name="站点数据刷新",
|
||||||
kwargs={
|
minutes=settings.SITEDATA_REFRESH_INTERVAL * 60,
|
||||||
'job_id': 'sitedata_refresh'
|
kwargs={
|
||||||
}
|
'job_id': 'sitedata_refresh'
|
||||||
)
|
}
|
||||||
|
)
|
||||||
|
|
||||||
self.init_plugin_jobs()
|
self.init_plugin_jobs()
|
||||||
|
|
||||||
|
|||||||
@@ -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