mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-06-15 23:16:45 +00:00
Compare commits
56 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b23f78e94d | ||
|
|
812a9a55d0 | ||
|
|
2e289e80d1 | ||
|
|
0d3dfdcbda | ||
|
|
87eae72f51 | ||
|
|
17fa7101bd | ||
|
|
312bd53079 | ||
|
|
4bc7d47576 | ||
|
|
71445b56f1 | ||
|
|
9ce9e0a4ef | ||
|
|
ae196f1aeb | ||
|
|
38e09b894d | ||
|
|
247d5ff255 | ||
|
|
0091e462fa | ||
|
|
7b314970b5 | ||
|
|
7ac881e3e3 | ||
|
|
8874723632 | ||
|
|
262bda94c4 | ||
|
|
d6e2cab5ef | ||
|
|
6d3e33a05d | ||
|
|
f2d0bec0ac | ||
|
|
dea78f4bfd | ||
|
|
f85f4b1342 | ||
|
|
d03771f8ab | ||
|
|
4b655dfac4 | ||
|
|
cdfcdd80bf | ||
|
|
64d3942ba9 | ||
|
|
16cce73f82 | ||
|
|
846edff84a | ||
|
|
d038bf31d3 | ||
|
|
376a69af5c | ||
|
|
380bb9bb3d | ||
|
|
f59e10ae1d | ||
|
|
c8d2d80cc5 | ||
|
|
f0bb9ddfca | ||
|
|
9ab86e4a85 | ||
|
|
e33f1a3ffc | ||
|
|
e2213e1ef6 | ||
|
|
bbc4a1bfa5 | ||
|
|
61e7ec9a36 | ||
|
|
534ad0bad6 | ||
|
|
db3040a50e | ||
|
|
8dd74e7dd8 | ||
|
|
206cdb2663 | ||
|
|
ca334813b7 | ||
|
|
5fc93ee8e6 | ||
|
|
9cef7b2615 | ||
|
|
a3916207ae | ||
|
|
b6e1702051 | ||
|
|
2cfc8b1ec7 | ||
|
|
2f7570eec1 | ||
|
|
070481cab0 | ||
|
|
26cd2c6cfe | ||
|
|
1ff571eb46 | ||
|
|
d8fcb4d240 | ||
|
|
778f97c1f3 |
@@ -60,7 +60,6 @@ MoviePilot需要配套下载器和媒体服务器配合使用。
|
||||
- **PUID**:运行程序用户的`uid`,默认`0`(仅支持环境变量配置)
|
||||
- **PGID**:运行程序用户的`gid`,默认`0`(仅支持环境变量配置)
|
||||
- **UMASK**:掩码权限,默认`000`,可以考虑设置为`022`(仅支持环境变量配置)
|
||||
- **WALLPAPER:** 登录首页电影海报,`tmdb`/`bing`,默认`tmdb`
|
||||
- **PROXY_HOST:** 网络代理,访问themoviedb或者重启更新需要使用代理访问,格式为`http(s)://ip:port`、`socks5://user:pass@host:port`(仅支持环境变量配置)
|
||||
- **MOVIEPILOT_AUTO_UPDATE**:重启更新,`true`/`false`,默认`true` **注意:如果出现网络问题可以配置`PROXY_HOST`**(仅支持环境变量配置)
|
||||
- **MOVIEPILOT_AUTO_UPDATE_DEV**:重启时更新到未发布的开发版本代码,`true`/`false`,默认`false`(仅支持环境变量配置)
|
||||
@@ -70,6 +69,7 @@ MoviePilot需要配套下载器和媒体服务器配合使用。
|
||||
- **❗API_TOKEN:** API密钥,默认`moviepilot`,在媒体服务器Webhook、微信回调等地址配置中需要加上`?token=`该值,建议修改为复杂字符串
|
||||
- **TMDB_API_DOMAIN:** TMDB API地址,默认`api.themoviedb.org`,也可配置为`api.tmdb.org`或其它中转代理服务地址,能连通即可
|
||||
- **TMDB_IMAGE_DOMAIN:** TMDB图片地址,默认`image.tmdb.org`,可配置为其它中转代理以加速TMDB图片显示,如:`static-mdb.v.geilijiasu.com`
|
||||
- **WALLPAPER:** 登录首页电影海报,`tmdb`/`bing`,默认`tmdb`
|
||||
---
|
||||
- **SCRAP_METADATA:** 刮削入库的媒体文件,`true`/`false`,默认`true`
|
||||
- **SCRAP_SOURCE:** 刮削元数据及图片使用的数据源,`themoviedb`/`douban`,默认`themoviedb`
|
||||
@@ -143,6 +143,8 @@ MoviePilot需要配套下载器和媒体服务器配合使用。
|
||||
- **QB_USER:** qbittorrent用户名
|
||||
- **QB_PASSWORD:** qbittorrent密码
|
||||
- **QB_CATEGORY:** qbittorrent分类自动管理,`true`/`false`,默认`false`,开启后会将下载二级分类传递到下载器,由下载器管理下载目录,需要同步开启`DOWNLOAD_CATEGORY`
|
||||
- **QB_SEQUENTIAL:** qbittorrent按顺序下载,`true`/`false`,默认`true`
|
||||
- **QB_FORCE_RESUME:** qbittorrent忽略队列限制,强制继续,`true`/`false`,默认 `false`
|
||||
|
||||
- `transmission`设置项:
|
||||
|
||||
@@ -177,6 +179,8 @@ MoviePilot需要配套下载器和媒体服务器配合使用。
|
||||
|
||||
`MoviePilot`需要认证后才能使用,配置`AUTH_SITE`后,需要根据下表配置对应站点的认证参数(**仅能通过环境变量配置**)
|
||||
|
||||
`AUTH_SITE`支持配置多个认证站点,使用`,`分隔,如:`iyuu,hhclub`,会依次执行认证操作,直到有一个站点认证成功。
|
||||
|
||||
- **❗AUTH_SITE:** 认证站点,支持`iyuu`/`hhclub`/`audiences`/`hddolby`/`zmpt`/`freefarm`/`hdfans`/`wintersakura`/`leaves`/`1ptba`/`icc2022`/`ptlsp`/`xingtan`
|
||||
|
||||
| 站点 | 参数 |
|
||||
|
||||
@@ -3,6 +3,8 @@ from typing import Any, List
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
|
||||
from app import schemas
|
||||
from app.db.models.user import User
|
||||
from app.db.userauth import get_current_active_user
|
||||
from app.chain.douban import DoubanChain
|
||||
from app.chain.download import DownloadChain
|
||||
from app.chain.media import MediaChain
|
||||
@@ -27,6 +29,7 @@ def read_downloading(
|
||||
def add_downloading(
|
||||
media_in: schemas.MediaInfo,
|
||||
torrent_in: schemas.TorrentInfo,
|
||||
current_user: User = Depends(get_current_active_user),
|
||||
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
|
||||
"""
|
||||
添加下载任务
|
||||
@@ -45,7 +48,7 @@ def add_downloading(
|
||||
media_info=mediainfo,
|
||||
torrent_info=torrentinfo
|
||||
)
|
||||
did = DownloadChain().download_single(context=context)
|
||||
did = DownloadChain().download_single(context=context, username=current_user.name)
|
||||
return schemas.Response(success=True if did else False, data={
|
||||
"download_id": did
|
||||
})
|
||||
|
||||
@@ -75,10 +75,14 @@ def delete_transfer_history(history_in: schemas.TransferHistory,
|
||||
return schemas.Response(success=False, msg="记录不存在")
|
||||
# 册除媒体库文件
|
||||
if deletedest and history.dest:
|
||||
TransferChain().delete_files(Path(history.dest))
|
||||
state, msg = TransferChain().delete_files(Path(history.dest))
|
||||
if not state:
|
||||
return schemas.Response(success=False, msg=msg)
|
||||
# 删除源文件
|
||||
if deletesrc and history.src:
|
||||
TransferChain().delete_files(Path(history.src))
|
||||
state, msg = TransferChain().delete_files(Path(history.src))
|
||||
if not state:
|
||||
return schemas.Response(success=False, msg=msg)
|
||||
# 发送事件
|
||||
eventmanager.send_event(
|
||||
EventType.DownloadFileDeleted,
|
||||
|
||||
@@ -219,8 +219,5 @@ def execute_command(jobid: str,
|
||||
"""
|
||||
if not jobid:
|
||||
return schemas.Response(success=False, message="命令不能为空!")
|
||||
if jobid == "subscribe_search":
|
||||
Scheduler().start(jobid, state='R')
|
||||
else:
|
||||
Scheduler().start(jobid)
|
||||
Scheduler().start(jobid)
|
||||
return schemas.Response(success=True)
|
||||
|
||||
@@ -120,6 +120,8 @@ class ChainBase(metaclass=ABCMeta):
|
||||
:param tmdbid: tmdbid
|
||||
:return: 识别的媒体信息,包括剧集信息
|
||||
"""
|
||||
if not tmdbid and hasattr(meta, "tmdbid"):
|
||||
tmdbid = meta.tmdbid
|
||||
return self.run_module("recognize_media", meta=meta, mtype=mtype, tmdbid=tmdbid)
|
||||
|
||||
def match_doubaninfo(self, name: str, imdbid: str = None,
|
||||
|
||||
@@ -170,7 +170,8 @@ class DownloadChain(ChainBase):
|
||||
episodes: Set[int] = None,
|
||||
channel: MessageChannel = None,
|
||||
save_path: str = None,
|
||||
userid: Union[str, int] = None) -> Optional[str]:
|
||||
userid: Union[str, int] = None,
|
||||
username: str = None) -> Optional[str]:
|
||||
"""
|
||||
下载及发送通知
|
||||
:param context: 资源上下文
|
||||
@@ -179,6 +180,7 @@ class DownloadChain(ChainBase):
|
||||
:param channel: 通知渠道
|
||||
:param save_path: 保存路径
|
||||
:param userid: 用户ID
|
||||
:param username: 调用下载的用户名/插件名
|
||||
"""
|
||||
_torrent = context.torrent_info
|
||||
_media = context.media_info
|
||||
@@ -267,6 +269,7 @@ class DownloadChain(ChainBase):
|
||||
torrent_description=_torrent.description,
|
||||
torrent_site=_torrent.site_name,
|
||||
userid=userid,
|
||||
username=username,
|
||||
channel=channel.value if channel else None,
|
||||
date=time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())
|
||||
)
|
||||
@@ -321,7 +324,8 @@ class DownloadChain(ChainBase):
|
||||
no_exists: Dict[int, Dict[int, NotExistMediaInfo]] = None,
|
||||
save_path: str = None,
|
||||
channel: MessageChannel = None,
|
||||
userid: str = None) -> Tuple[List[Context], Dict[int, Dict[int, NotExistMediaInfo]]]:
|
||||
userid: str = None,
|
||||
username: str = None) -> Tuple[List[Context], Dict[int, Dict[int, NotExistMediaInfo]]]:
|
||||
"""
|
||||
根据缺失数据,自动种子列表中组合择优下载
|
||||
:param contexts: 资源上下文列表
|
||||
@@ -329,6 +333,7 @@ class DownloadChain(ChainBase):
|
||||
:param save_path: 保存路径
|
||||
:param channel: 通知渠道
|
||||
:param userid: 用户ID
|
||||
:param username: 调用下载的用户名/插件名
|
||||
:return: 已经下载的资源列表、剩余未下载到的剧集 no_exists[tmdb_id] = {season: NotExistMediaInfo}
|
||||
"""
|
||||
# 已下载的项目
|
||||
@@ -395,7 +400,7 @@ class DownloadChain(ChainBase):
|
||||
for context in contexts:
|
||||
if context.media_info.type == MediaType.MOVIE:
|
||||
if self.download_single(context, save_path=save_path,
|
||||
channel=channel, userid=userid):
|
||||
channel=channel, userid=userid, username=username):
|
||||
# 下载成功
|
||||
downloaded_list.append(context)
|
||||
|
||||
@@ -463,12 +468,13 @@ class DownloadChain(ChainBase):
|
||||
torrent_file=content if isinstance(content, Path) else None,
|
||||
save_path=save_path,
|
||||
channel=channel,
|
||||
userid=userid
|
||||
userid=userid,
|
||||
username=username
|
||||
)
|
||||
else:
|
||||
# 下载
|
||||
download_id = self.download_single(context, save_path=save_path,
|
||||
channel=channel, userid=userid)
|
||||
channel=channel, userid=userid, username=username)
|
||||
|
||||
if download_id:
|
||||
# 下载成功
|
||||
@@ -528,7 +534,7 @@ class DownloadChain(ChainBase):
|
||||
if torrent_episodes.issubset(set(need_episodes)):
|
||||
# 下载
|
||||
download_id = self.download_single(context, save_path=save_path,
|
||||
channel=channel, userid=userid)
|
||||
channel=channel, userid=userid, username=username)
|
||||
if download_id:
|
||||
# 下载成功
|
||||
downloaded_list.append(context)
|
||||
@@ -606,7 +612,8 @@ class DownloadChain(ChainBase):
|
||||
episodes=selected_episodes,
|
||||
save_path=save_path,
|
||||
channel=channel,
|
||||
userid=userid
|
||||
userid=userid,
|
||||
username=username
|
||||
)
|
||||
if not download_id:
|
||||
continue
|
||||
|
||||
@@ -87,13 +87,15 @@ class MessageChain(ChainBase):
|
||||
# 发送消息
|
||||
self.post_message(Notification(channel=channel, title="输入有误!", userid=userid))
|
||||
return
|
||||
# 选择的序号
|
||||
_choice = int(text) + _current_page * self._page_size - 1
|
||||
# 缓存类型
|
||||
cache_type: str = cache_data.get('type')
|
||||
# 缓存列表
|
||||
cache_list: list = cache_data.get('items')
|
||||
cache_list: list = copy.deepcopy(cache_data.get('items'))
|
||||
# 选择
|
||||
if cache_type == "Search":
|
||||
mediainfo: MediaInfo = cache_list[int(text) + _current_page * self._page_size - 1]
|
||||
mediainfo: MediaInfo = cache_list[_choice]
|
||||
_current_media = mediainfo
|
||||
# 查询缺失的媒体信息
|
||||
exist_flag, no_exists = self.downloadchain.get_no_exists_info(meta=_current_meta,
|
||||
@@ -158,7 +160,7 @@ class MessageChain(ChainBase):
|
||||
|
||||
elif cache_type == "Subscribe":
|
||||
# 订阅媒体
|
||||
mediainfo: MediaInfo = cache_list[int(text) - 1]
|
||||
mediainfo: MediaInfo = cache_list[_choice]
|
||||
# 查询缺失的媒体信息
|
||||
exist_flag, _ = self.downloadchain.get_no_exists_info(meta=_current_meta,
|
||||
mediainfo=mediainfo)
|
||||
@@ -187,9 +189,9 @@ class MessageChain(ChainBase):
|
||||
username=username)
|
||||
else:
|
||||
# 下载种子
|
||||
context: Context = cache_list[int(text) - 1]
|
||||
context: Context = cache_list[_choice]
|
||||
# 下载
|
||||
self.downloadchain.download_single(context, userid=userid, channel=channel)
|
||||
self.downloadchain.download_single(context, userid=userid, channel=channel, username=username)
|
||||
|
||||
elif text.lower() == "p":
|
||||
# 上一页
|
||||
@@ -343,7 +345,8 @@ class MessageChain(ChainBase):
|
||||
downloads, lefts = self.downloadchain.batch_download(contexts=cache_list,
|
||||
no_exists=no_exists,
|
||||
channel=channel,
|
||||
userid=userid)
|
||||
userid=userid,
|
||||
username=username)
|
||||
if downloads and not lefts:
|
||||
# 全部下载完成
|
||||
logger.info(f'{_current_media.title_year} 下载完成')
|
||||
|
||||
@@ -302,7 +302,7 @@ class SubscribeChain(ChainBase):
|
||||
|
||||
# 自动下载
|
||||
downloads, lefts = self.downloadchain.batch_download(contexts=matched_contexts,
|
||||
no_exists=no_exists)
|
||||
no_exists=no_exists, username=subscribe.username)
|
||||
# 更新已经下载的集数
|
||||
if downloads \
|
||||
and meta.type == MediaType.TV \
|
||||
@@ -415,7 +415,7 @@ class SubscribeChain(ChainBase):
|
||||
}
|
||||
# 订阅默认过滤规则
|
||||
return self.systemconfig.get(SystemConfigKey.DefaultFilterRules) or {}
|
||||
|
||||
|
||||
@staticmethod
|
||||
def check_filter_rule(torrent_info: TorrentInfo, filter_rule: Dict[str, str]) -> bool:
|
||||
"""
|
||||
@@ -621,7 +621,8 @@ class SubscribeChain(ChainBase):
|
||||
logger.info(f'{mediainfo.title_year} 匹配完成,共匹配到{len(_match_context)}个资源')
|
||||
if _match_context:
|
||||
# 批量择优下载
|
||||
downloads, lefts = self.downloadchain.batch_download(contexts=_match_context, no_exists=no_exists)
|
||||
downloads, lefts = self.downloadchain.batch_download(contexts=_match_context, no_exists=no_exists,
|
||||
username=subscribe.username)
|
||||
# 更新已经下载的集数
|
||||
if downloads and meta.type == MediaType.TV:
|
||||
self.__update_subscribe_note(subscribe=subscribe, downloads=downloads)
|
||||
|
||||
@@ -617,14 +617,15 @@ class TransferChain(ChainBase):
|
||||
title=msg_title, text=msg_str, image=mediainfo.get_message_image()))
|
||||
|
||||
@staticmethod
|
||||
def delete_files(path: Path):
|
||||
def delete_files(path: Path) -> Tuple[bool, str]:
|
||||
"""
|
||||
删除转移后的文件以及空目录
|
||||
:param path: 文件路径
|
||||
:return: 成功标识,错误信息
|
||||
"""
|
||||
logger.info(f"开始删除文件以及空目录:{path} ...")
|
||||
if not path.exists():
|
||||
return
|
||||
return True, f"文件或目录不存在:{path}"
|
||||
if path.is_file():
|
||||
# 删除文件、nfo、jpg等同名文件
|
||||
pattern = path.stem.replace('[', '?').replace(']', '?')
|
||||
@@ -636,7 +637,7 @@ class TransferChain(ChainBase):
|
||||
elif str(path.parent) == str(path.root):
|
||||
# 根目录,不删除
|
||||
logger.warn(f"根目录 {path} 不能删除!")
|
||||
return
|
||||
return False, f"根目录 {path} 不能删除!"
|
||||
else:
|
||||
# 非根目录,才删除目录
|
||||
shutil.rmtree(path)
|
||||
@@ -662,5 +663,10 @@ class TransferChain(ChainBase):
|
||||
# 父目录非根目录,才删除父目录
|
||||
if not SystemUtils.exits_files(parent_path, settings.RMT_MEDIAEXT):
|
||||
# 当前路径下没有媒体文件则删除
|
||||
shutil.rmtree(parent_path)
|
||||
try:
|
||||
shutil.rmtree(parent_path)
|
||||
except Exception as e:
|
||||
logger.error(f"删除目录 {parent_path} 失败:{str(e)}")
|
||||
return False, f"删除目录 {parent_path} 失败:{str(e)}"
|
||||
logger.warn(f"目录 {parent_path} 已删除")
|
||||
return True, ""
|
||||
|
||||
@@ -129,6 +129,10 @@ class Settings(BaseSettings):
|
||||
QB_PASSWORD: str = None
|
||||
# Qbittorrent分类自动管理
|
||||
QB_CATEGORY: bool = False
|
||||
# Qbittorrent按顺序下载
|
||||
QB_SEQUENTIAL: bool = True
|
||||
# Qbittorrent忽略队列限制,强制继续
|
||||
QB_FORCE_RESUME: bool = False
|
||||
# Transmission地址,IP:PORT
|
||||
TR_HOST: str = None
|
||||
# Transmission用户名
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
from pathlib import Path
|
||||
from typing import Tuple
|
||||
|
||||
import regex as re
|
||||
|
||||
from app.core.config import settings
|
||||
from app.core.meta import MetaAnime, MetaVideo, MetaBase
|
||||
from app.core.meta.words import WordsMatcher
|
||||
from app.schemas.types import MediaType
|
||||
|
||||
|
||||
def MetaInfo(title: str, subtitle: str = None) -> MetaBase:
|
||||
@@ -18,6 +20,8 @@ def MetaInfo(title: str, subtitle: str = None) -> MetaBase:
|
||||
org_title = title
|
||||
# 预处理标题
|
||||
title, apply_words = WordsMatcher().prepare(title)
|
||||
# 获取标题中媒体信息
|
||||
title, metainfo = find_metainfo(title)
|
||||
# 判断是否处理文件
|
||||
if title and Path(title).suffix.lower() in settings.RMT_MEDIAEXT:
|
||||
isfile = True
|
||||
@@ -29,7 +33,23 @@ def MetaInfo(title: str, subtitle: str = None) -> MetaBase:
|
||||
meta.title = org_title
|
||||
# 记录使用的识别词
|
||||
meta.apply_words = apply_words or []
|
||||
|
||||
# 修正媒体信息
|
||||
if metainfo.get('tmdbid'):
|
||||
meta.tmdbid = metainfo['tmdbid']
|
||||
if metainfo.get('type'):
|
||||
meta.type = metainfo['type']
|
||||
if metainfo.get('begin_season'):
|
||||
meta.begin_season = metainfo['begin_season']
|
||||
if metainfo.get('end_season'):
|
||||
meta.end_season = metainfo['end_season']
|
||||
if metainfo.get('total_season'):
|
||||
meta.total_season = metainfo['total_season']
|
||||
if metainfo.get('begin_episode'):
|
||||
meta.begin_episode = metainfo['begin_episode']
|
||||
if metainfo.get('end_episode'):
|
||||
meta.end_episode = metainfo['end_episode']
|
||||
if metainfo.get('total_episode'):
|
||||
meta.total_episode = metainfo['total_episode']
|
||||
return meta
|
||||
|
||||
|
||||
@@ -65,3 +85,71 @@ def is_anime(name: str) -> bool:
|
||||
if re.search(r'\[[+0-9XVPI-]+]\s*\[', name, re.IGNORECASE):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def find_metainfo(title: str) -> Tuple[str, dict]:
|
||||
"""
|
||||
从标题中提取媒体信息
|
||||
"""
|
||||
metainfo = {
|
||||
'tmdbid': None,
|
||||
'type': None,
|
||||
'begin_season': None,
|
||||
'end_season': None,
|
||||
'total_season': None,
|
||||
'begin_episode': None,
|
||||
'end_episode': None,
|
||||
'total_episode': None,
|
||||
}
|
||||
if not title:
|
||||
return title, metainfo
|
||||
# 从标题中提取媒体信息 格式为{[tmdbid=xxx;type=xxx;s=xxx;e=xxx]}
|
||||
results = re.findall(r'(?<={\[)[\W\w]+(?=]})', title)
|
||||
if not results:
|
||||
return title, metainfo
|
||||
for result in results:
|
||||
tmdbid = re.findall(r'(?<=tmdbid=)\d+', result)
|
||||
# 查找tmdbid信息
|
||||
if tmdbid and tmdbid[0].isdigit():
|
||||
metainfo['tmdbid'] = tmdbid[0]
|
||||
# 查找媒体类型
|
||||
mtype = re.findall(r'(?<=type=)\d+', result)
|
||||
if mtype:
|
||||
match mtype[0]:
|
||||
case "movie":
|
||||
metainfo['type'] = MediaType.MOVIE
|
||||
case "tv":
|
||||
metainfo['type'] = MediaType.TV
|
||||
case _:
|
||||
pass
|
||||
# 查找季信息
|
||||
begin_season = re.findall(r'(?<=s=)\d+', result)
|
||||
if begin_season and begin_season[0].isdigit():
|
||||
metainfo['begin_season'] = int(begin_season[0])
|
||||
end_season = re.findall(r'(?<=s=\d+-)\d+', result)
|
||||
if end_season and end_season[0].isdigit():
|
||||
metainfo['end_season'] = int(end_season[0])
|
||||
# 查找集信息
|
||||
begin_episode = re.findall(r'(?<=e=)\d+', result)
|
||||
if begin_episode and begin_episode[0].isdigit():
|
||||
metainfo['begin_episode'] = int(begin_episode[0])
|
||||
end_episode = re.findall(r'(?<=e=\d+-)\d+', result)
|
||||
if end_episode and end_episode[0].isdigit():
|
||||
metainfo['end_episode'] = int(end_episode[0])
|
||||
# 去除title中该部分
|
||||
if tmdbid or mtype or begin_season or end_season or begin_episode or end_episode:
|
||||
title = title.replace(f"{{[{result}]}}", '')
|
||||
# 计算季集总数
|
||||
if metainfo.get('begin_season') and metainfo.get('end_season'):
|
||||
if metainfo['begin_season'] > metainfo['end_season']:
|
||||
metainfo['begin_season'], metainfo['end_season'] = metainfo['end_season'], metainfo['begin_season']
|
||||
metainfo['total_season'] = metainfo['end_season'] - metainfo['begin_season'] + 1
|
||||
elif metainfo.get('begin_season') and not metainfo.get('end_season'):
|
||||
metainfo['total_season'] = 1
|
||||
if metainfo.get('begin_episode') and metainfo.get('end_episode'):
|
||||
if metainfo['begin_episode'] > metainfo['end_episode']:
|
||||
metainfo['begin_episode'], metainfo['end_episode'] = metainfo['end_episode'], metainfo['begin_episode']
|
||||
metainfo['total_episode'] = metainfo['end_episode'] - metainfo['begin_episode'] + 1
|
||||
elif metainfo.get('begin_episode') and not metainfo.get('end_episode'):
|
||||
metainfo['total_episode'] = 1
|
||||
return title, metainfo
|
||||
|
||||
@@ -108,10 +108,10 @@ class DownloadHistoryOper(DbOper):
|
||||
episode=episode,
|
||||
tmdbid=tmdbid)
|
||||
|
||||
def list_by_user_date(self, date: str, userid: str = None) -> List[DownloadHistory]:
|
||||
def list_by_user_date(self, date: str, username: str = None) -> List[DownloadHistory]:
|
||||
"""
|
||||
查询某用户某时间之后的下载历史
|
||||
查询某用户某时间之前的下载历史
|
||||
"""
|
||||
return DownloadHistory.list_by_user_date(db=self._db,
|
||||
date=date,
|
||||
userid=userid)
|
||||
username=username)
|
||||
|
||||
@@ -38,6 +38,8 @@ class DownloadHistory(Base):
|
||||
torrent_site = Column(String)
|
||||
# 下载用户
|
||||
userid = Column(String)
|
||||
# 下载用户名/插件名
|
||||
username = Column(String)
|
||||
# 下载渠道
|
||||
channel = Column(String)
|
||||
# 创建时间
|
||||
@@ -108,13 +110,13 @@ class DownloadHistory(Base):
|
||||
|
||||
@staticmethod
|
||||
@db_query
|
||||
def list_by_user_date(db: Session, date: str, userid: str = None):
|
||||
def list_by_user_date(db: Session, date: str, username: str = None):
|
||||
"""
|
||||
查询某用户某时间之后的下载历史
|
||||
"""
|
||||
if userid:
|
||||
if username:
|
||||
result = db.query(DownloadHistory).filter(DownloadHistory.date < date,
|
||||
DownloadHistory.userid == userid).order_by(
|
||||
DownloadHistory.username == username).order_by(
|
||||
DownloadHistory.id.desc()).all()
|
||||
else:
|
||||
result = db.query(DownloadHistory).filter(DownloadHistory.date < date).order_by(
|
||||
@@ -165,7 +167,6 @@ class DownloadFiles(Base):
|
||||
result = db.query(DownloadFiles).filter(DownloadFiles.savepath == savepath).all()
|
||||
return list(result)
|
||||
|
||||
@staticmethod
|
||||
@db_update
|
||||
def delete_by_fullpath(db: Session, fullpath: str):
|
||||
db.query(DownloadFiles).filter(DownloadFiles.fullpath == fullpath,
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -26,7 +26,7 @@ class EmbyModule(_ModuleBase):
|
||||
定时任务,每10分钟调用一次
|
||||
"""
|
||||
# 定时重连
|
||||
if not self.emby.is_inactive():
|
||||
if self.emby.is_inactive():
|
||||
self.emby.reconnect()
|
||||
|
||||
def user_authenticate(self, name: str, password: str) -> Optional[str]:
|
||||
|
||||
@@ -23,7 +23,7 @@ class JellyfinModule(_ModuleBase):
|
||||
定时任务,每10分钟调用一次
|
||||
"""
|
||||
# 定时重连
|
||||
if not self.jellyfin.is_inactive():
|
||||
if self.jellyfin.is_inactive():
|
||||
self.jellyfin.reconnect()
|
||||
|
||||
def stop(self):
|
||||
|
||||
@@ -26,7 +26,7 @@ class PlexModule(_ModuleBase):
|
||||
定时任务,每10分钟调用一次
|
||||
"""
|
||||
# 定时重连
|
||||
if not self.plex.is_inactive():
|
||||
if self.plex.is_inactive():
|
||||
self.plex.reconnect()
|
||||
|
||||
def webhook_parser(self, body: Any, form: Any, args: Any) -> Optional[schemas.WebhookEventInfo]:
|
||||
|
||||
@@ -101,9 +101,15 @@ class QbittorrentModule(_ModuleBase):
|
||||
# 选择文件
|
||||
self.qbittorrent.set_files(torrent_hash=torrent_hash, file_ids=file_ids, priority=0)
|
||||
# 开始任务
|
||||
self.qbittorrent.start_torrents(torrent_hash)
|
||||
if settings.QB_FORCE_RESUME:
|
||||
# 强制继续
|
||||
self.qbittorrent.torrents_set_force_start(torrent_hash)
|
||||
else:
|
||||
self.qbittorrent.start_torrents(torrent_hash)
|
||||
return torrent_hash, f"添加下载成功,已选择集数:{sucess_epidised}"
|
||||
else:
|
||||
if settings.QB_FORCE_RESUME:
|
||||
self.qbittorrent.torrents_set_force_start(torrent_hash)
|
||||
return torrent_hash, "添加下载成功"
|
||||
|
||||
def list_torrents(self, status: TorrentStatus = None,
|
||||
@@ -165,6 +171,9 @@ class QbittorrentModule(_ModuleBase):
|
||||
state="paused" if torrent.get('state') == "paused" else "downloading",
|
||||
dlspeed=StringUtils.str_filesize(torrent.get('dlspeed')),
|
||||
upspeed=StringUtils.str_filesize(torrent.get('upspeed')),
|
||||
left_time=StringUtils.str_secends(
|
||||
(torrent.get('total_size') - torrent.get('completed')) / torrent.get('dlspeed')) if torrent.get(
|
||||
'dlspeed') > 0 else ''
|
||||
))
|
||||
else:
|
||||
return None
|
||||
|
||||
@@ -243,7 +243,7 @@ class Qbittorrent(metaclass=Singleton):
|
||||
is_paused=is_paused,
|
||||
tags=tags,
|
||||
use_auto_torrent_management=is_auto,
|
||||
is_sequential_download=True,
|
||||
is_sequential_download=settings.QB_SEQUENTIAL,
|
||||
cookie=cookie,
|
||||
category=category,
|
||||
**kwargs)
|
||||
|
||||
@@ -33,7 +33,7 @@ class TransmissionModule(_ModuleBase):
|
||||
定时任务,每10分钟调用一次
|
||||
"""
|
||||
# 定时重连
|
||||
if not self.transmission.is_inactive():
|
||||
if self.transmission.is_inactive():
|
||||
self.transmission.reconnect()
|
||||
|
||||
def download(self, content: Union[Path, str], download_dir: Path, cookie: str,
|
||||
@@ -151,6 +151,7 @@ class TransmissionModule(_ModuleBase):
|
||||
state="paused" if torrent.status == "stopped" else "downloading",
|
||||
dlspeed=StringUtils.str_filesize(dlspeed),
|
||||
upspeed=StringUtils.str_filesize(upspeed),
|
||||
left_time=StringUtils.str_secends(torrent.left_until_done / dlspeed) if dlspeed > 0 else ''
|
||||
))
|
||||
else:
|
||||
return None
|
||||
|
||||
@@ -9,6 +9,7 @@ import pytz
|
||||
from apscheduler.schedulers.background import BackgroundScheduler
|
||||
from apscheduler.triggers.cron import CronTrigger
|
||||
|
||||
from app import schemas
|
||||
from app.core.config import settings
|
||||
from app.plugins import _PluginBase
|
||||
from typing import Any, List, Dict, Tuple, Optional
|
||||
@@ -103,12 +104,16 @@ class AutoBackup(_PluginBase):
|
||||
bk_path = self.get_data_path()
|
||||
|
||||
# 备份
|
||||
zip_file = self.backup(bk_path=bk_path)
|
||||
zip_file = self.backup_file(bk_path=bk_path)
|
||||
|
||||
if zip_file:
|
||||
logger.info(f"备份完成 备份文件 {zip_file} ")
|
||||
success = True
|
||||
msg = f"备份完成 备份文件 {zip_file}"
|
||||
logger.info(msg)
|
||||
else:
|
||||
logger.error("创建备份失败")
|
||||
success = False
|
||||
msg = "创建备份失败"
|
||||
logger.error(msg)
|
||||
|
||||
# 清理备份
|
||||
bk_cnt = 0
|
||||
@@ -140,8 +145,10 @@ class AutoBackup(_PluginBase):
|
||||
f"清理备份数量 {del_cnt}\n"
|
||||
f"剩余备份数量 {bk_cnt - del_cnt}")
|
||||
|
||||
return success, msg
|
||||
|
||||
@staticmethod
|
||||
def backup(bk_path: Path = None):
|
||||
def backup_file(bk_path: Path = None):
|
||||
"""
|
||||
@param bk_path 自定义备份路径
|
||||
"""
|
||||
@@ -173,7 +180,23 @@ class AutoBackup(_PluginBase):
|
||||
pass
|
||||
|
||||
def get_api(self) -> List[Dict[str, Any]]:
|
||||
pass
|
||||
return [{
|
||||
"path": "/backup",
|
||||
"endpoint": self.__backup,
|
||||
"methods": ["GET"],
|
||||
"summary": "MoviePilot备份",
|
||||
"description": "MoviePilot备份",
|
||||
}]
|
||||
|
||||
def backup(self) -> schemas.Response:
|
||||
"""
|
||||
API调用备份
|
||||
"""
|
||||
success, msg = self.__backup()
|
||||
return schemas.Response(
|
||||
success=success,
|
||||
message=msg
|
||||
)
|
||||
|
||||
def get_form(self) -> Tuple[List[dict], Dict[str, Any]]:
|
||||
"""
|
||||
@@ -285,6 +308,8 @@ class AutoBackup(_PluginBase):
|
||||
{
|
||||
'component': 'VAlert',
|
||||
'props': {
|
||||
'type': 'info',
|
||||
'variant': 'tonal',
|
||||
'text': '备份文件路径默认为本地映射的config/plugins/AutoBackup。'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,8 +49,8 @@ class AutoClean(_PluginBase):
|
||||
_onlyonce = False
|
||||
_notify = False
|
||||
_cleantype = None
|
||||
_cleanuser = None
|
||||
_cleandate = None
|
||||
_cleanuser = None
|
||||
_downloadhis = None
|
||||
_transferhis = None
|
||||
|
||||
@@ -67,8 +67,8 @@ class AutoClean(_PluginBase):
|
||||
self._onlyonce = config.get("onlyonce")
|
||||
self._notify = config.get("notify")
|
||||
self._cleantype = config.get("cleantype")
|
||||
self._cleanuser = config.get("cleanuser")
|
||||
self._cleandate = config.get("cleandate")
|
||||
self._cleanuser = config.get("cleanuser")
|
||||
|
||||
# 加载模块
|
||||
if self._enabled:
|
||||
@@ -96,9 +96,9 @@ class AutoClean(_PluginBase):
|
||||
"onlyonce": False,
|
||||
"cron": self._cron,
|
||||
"cleantype": self._cleantype,
|
||||
"cleandate": self._cleandate,
|
||||
"enabled": self._enabled,
|
||||
"cleanuser": self._cleanuser,
|
||||
"cleandate": self._cleandate,
|
||||
"notify": self._notify,
|
||||
})
|
||||
|
||||
@@ -107,43 +107,78 @@ class AutoClean(_PluginBase):
|
||||
self._scheduler.print_jobs()
|
||||
self._scheduler.start()
|
||||
|
||||
def __get_clean_date(self, deltatime: str = None):
|
||||
# 清理日期
|
||||
current_time = datetime.now()
|
||||
if deltatime:
|
||||
days_ago = current_time - timedelta(days=int(deltatime))
|
||||
else:
|
||||
days_ago = current_time - timedelta(days=int(self._cleandate))
|
||||
return days_ago.strftime("%Y-%m-%d")
|
||||
|
||||
def __clean(self):
|
||||
"""
|
||||
定时清理媒体库
|
||||
"""
|
||||
if not self._cleandate:
|
||||
logger.error("未配置清理媒体库时间,停止运行")
|
||||
logger.error("未配置媒体库全局清理时间,停止运行")
|
||||
return
|
||||
|
||||
# 清理日期
|
||||
current_time = datetime.now()
|
||||
days_ago = current_time - timedelta(days=int(self._cleandate))
|
||||
clean_date = days_ago.strftime("%Y-%m-%d")
|
||||
|
||||
# 查询用户清理日期之后的下载历史
|
||||
# 查询用户清理日期之前的下载历史,不填默认清理全部用户的下载
|
||||
if not self._cleanuser:
|
||||
clean_date = self.__get_clean_date()
|
||||
downloadhis_list = self._downloadhis.list_by_user_date(date=clean_date)
|
||||
logger.info(f'获取到日期 {clean_date} 之后的下载历史 {len(downloadhis_list)} 条')
|
||||
logger.info(f'获取到日期 {clean_date} 之前的下载历史 {len(downloadhis_list)} 条')
|
||||
self.__clean_history(date=clean_date, clean_type=self._cleantype, downloadhis_list=downloadhis_list)
|
||||
|
||||
self.__clean_history(date=clean_date, downloadhis_list=downloadhis_list)
|
||||
# 根据填写的信息判断怎么清理
|
||||
else:
|
||||
for userid in str(self._cleanuser).split(","):
|
||||
downloadhis_list = self._downloadhis.list_by_user_date(date=clean_date,
|
||||
userid=userid)
|
||||
logger.info(
|
||||
f'获取到用户 {userid} 日期 {clean_date} 之后的下载历史 {len(downloadhis_list)} 条')
|
||||
self.__clean_history(date=clean_date, downloadhis_list=downloadhis_list, userid=userid)
|
||||
# username:days#cleantype
|
||||
clean_type = self._cleantype
|
||||
clean_date = self._cleandate
|
||||
|
||||
def __clean_history(self, date: str, downloadhis_list: List[DownloadHistory], userid: str = None):
|
||||
# 1.3.7版本及之前处理多位用户
|
||||
if str(self._cleanuser).count(','):
|
||||
for username in str(self._cleanuser).split(","):
|
||||
downloadhis_list = self._downloadhis.list_by_user_date(date=clean_date,
|
||||
username=username)
|
||||
logger.info(
|
||||
f'获取到用户 {username} 日期 {clean_date} 之前的下载历史 {len(downloadhis_list)} 条')
|
||||
self.__clean_history(date=clean_date, clean_type=self._cleantype, downloadhis_list=downloadhis_list)
|
||||
return
|
||||
|
||||
for userinfo in str(self._cleanuser).split("\n"):
|
||||
if userinfo.count('#'):
|
||||
clean_type = userinfo.split('#')[1]
|
||||
username_and_days = userinfo.split('#')[0]
|
||||
else:
|
||||
username_and_days = userinfo
|
||||
if username_and_days.count(':'):
|
||||
clean_date = username_and_days.split(':')[1]
|
||||
username = username_and_days.split(':')[0]
|
||||
else:
|
||||
username = userinfo
|
||||
|
||||
# 转strftime
|
||||
clean_date = self.__get_clean_date(clean_date)
|
||||
logger.info(f'{username} 使用 {clean_type} 清理方式,清理 {clean_date} 之前的下载历史')
|
||||
downloadhis_list = self._downloadhis.list_by_user_date(date=clean_date,
|
||||
username=username)
|
||||
logger.info(
|
||||
f'获取到用户 {username} 日期 {clean_date} 之前的下载历史 {len(downloadhis_list)} 条')
|
||||
self.__clean_history(date=clean_date, clean_type=clean_type,
|
||||
downloadhis_list=downloadhis_list)
|
||||
|
||||
def __clean_history(self, date: str, clean_type: str, downloadhis_list: List[DownloadHistory]):
|
||||
"""
|
||||
清理下载历史、转移记录
|
||||
"""
|
||||
if not downloadhis_list:
|
||||
logger.warn(f"未获取到日期 {date} 之后的下载记录,停止运行")
|
||||
logger.warn(f"未获取到日期 {date} 之前的下载记录,停止运行")
|
||||
return
|
||||
|
||||
# 读取历史记录
|
||||
history = self.get_data('history') or []
|
||||
pulgin_history = self.get_data('history') or []
|
||||
|
||||
# 创建一个字典来保存分组结果
|
||||
downloadhis_grouped_dict: Dict[tuple, List[DownloadHistory]] = defaultdict(list)
|
||||
@@ -159,10 +194,9 @@ class AutoClean(_PluginBase):
|
||||
# 输出分组结果
|
||||
for key, downloadhis_list in downloadhis_grouped_dict.items():
|
||||
logger.info(f"开始清理 {key}")
|
||||
|
||||
del_transferhis_cnt = 0
|
||||
del_media_name = downloadhis_list[0].title
|
||||
del_media_user = downloadhis_list[0].userid
|
||||
del_media_user = downloadhis_list[0].username
|
||||
del_media_type = downloadhis_list[0].type
|
||||
del_media_year = downloadhis_list[0].year
|
||||
del_media_season = downloadhis_list[0].seasons
|
||||
@@ -180,12 +214,12 @@ class AutoClean(_PluginBase):
|
||||
|
||||
for history in transferhis_list:
|
||||
# 册除媒体库文件
|
||||
if str(self._cleantype == "dest") or str(self._cleantype == "all"):
|
||||
if clean_type in ["dest", "all"]:
|
||||
TransferChain().delete_files(Path(history.dest))
|
||||
# 删除记录
|
||||
self._transferhis.delete(history.id)
|
||||
# 删除源文件
|
||||
if str(self._cleantype == "src") or str(self._cleantype == "all"):
|
||||
if clean_type in ["src", "all"]:
|
||||
TransferChain().delete_files(Path(history.src))
|
||||
# 发送事件
|
||||
eventmanager.send_event(
|
||||
@@ -198,28 +232,28 @@ class AutoClean(_PluginBase):
|
||||
# 累加删除数量
|
||||
del_transferhis_cnt += len(transferhis_list)
|
||||
|
||||
# 发送消息
|
||||
if self._notify:
|
||||
self.post_message(
|
||||
mtype=NotificationType.MediaServer,
|
||||
title="【定时清理媒体库任务完成】",
|
||||
text=f"清理媒体名称 {del_media_name}\n"
|
||||
f"下载媒体用户 {del_media_user}\n"
|
||||
f"删除历史记录 {del_transferhis_cnt}",
|
||||
userid=userid)
|
||||
if del_transferhis_cnt:
|
||||
# 发送消息
|
||||
if self._notify:
|
||||
self.post_message(
|
||||
mtype=NotificationType.MediaServer,
|
||||
title="【定时清理媒体库任务完成】",
|
||||
text=f"清理媒体名称 {del_media_name}\n"
|
||||
f"下载媒体用户 {del_media_user}\n"
|
||||
f"删除历史记录 {del_transferhis_cnt}")
|
||||
|
||||
history.append({
|
||||
"type": del_media_type,
|
||||
"title": del_media_name,
|
||||
"year": del_media_year,
|
||||
"season": del_media_season,
|
||||
"episode": del_media_episode,
|
||||
"image": del_image,
|
||||
"del_time": time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(time.time()))
|
||||
})
|
||||
pulgin_history.append({
|
||||
"type": del_media_type,
|
||||
"title": del_media_name,
|
||||
"year": del_media_year,
|
||||
"season": del_media_season,
|
||||
"episode": del_media_episode,
|
||||
"image": del_image,
|
||||
"del_time": time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(time.time()))
|
||||
})
|
||||
|
||||
# 保存历史
|
||||
self.save_data("history", history)
|
||||
self.save_data("history", pulgin_history)
|
||||
|
||||
def get_state(self) -> bool:
|
||||
return self._enabled
|
||||
@@ -236,154 +270,160 @@ class AutoClean(_PluginBase):
|
||||
拼装插件配置页面,需要返回两块数据:1、页面配置;2、数据结构
|
||||
"""
|
||||
return [
|
||||
{
|
||||
'component': 'VForm',
|
||||
'content': [
|
||||
{
|
||||
'component': 'VRow',
|
||||
'content': [
|
||||
{
|
||||
'component': 'VCol',
|
||||
'props': {
|
||||
'cols': 12,
|
||||
'md': 4
|
||||
},
|
||||
'content': [
|
||||
{
|
||||
'component': 'VSwitch',
|
||||
'props': {
|
||||
'model': 'enabled',
|
||||
'label': '启用插件',
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
'component': 'VCol',
|
||||
'props': {
|
||||
'cols': 12,
|
||||
'md': 4
|
||||
},
|
||||
'content': [
|
||||
{
|
||||
'component': 'VSwitch',
|
||||
'props': {
|
||||
'model': 'onlyonce',
|
||||
'label': '立即运行一次',
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
'component': 'VCol',
|
||||
'props': {
|
||||
'cols': 12,
|
||||
'md': 4
|
||||
},
|
||||
'content': [
|
||||
{
|
||||
'component': 'VSwitch',
|
||||
'props': {
|
||||
'model': 'notify',
|
||||
'label': '开启通知',
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
'component': 'VRow',
|
||||
'content': [
|
||||
{
|
||||
'component': 'VCol',
|
||||
'props': {
|
||||
'cols': 12,
|
||||
'md': 4
|
||||
},
|
||||
'content': [
|
||||
{
|
||||
'component': 'VTextField',
|
||||
'props': {
|
||||
'model': 'cron',
|
||||
'label': '执行周期',
|
||||
'placeholder': '0 0 ? ? ?'
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
'component': 'VCol',
|
||||
'props': {
|
||||
'cols': 12,
|
||||
'md': 4
|
||||
},
|
||||
'content': [
|
||||
{
|
||||
'component': 'VSelect',
|
||||
'props': {
|
||||
'model': 'cleantype',
|
||||
'label': '清理方式',
|
||||
'items': [
|
||||
{'title': '媒体库文件', 'value': 'dest'},
|
||||
{'title': '源文件', 'value': 'src'},
|
||||
{'title': '所有文件', 'value': 'all'},
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
'component': 'VCol',
|
||||
'props': {
|
||||
'cols': 12,
|
||||
'md': 4
|
||||
},
|
||||
'content': [
|
||||
{
|
||||
'component': 'VTextField',
|
||||
'props': {
|
||||
'model': 'cleandate',
|
||||
'label': '清理媒体日期',
|
||||
'placeholder': '清理多少天之前的下载记录(天)'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
'component': 'VRow',
|
||||
'content': [
|
||||
{
|
||||
'component': 'VCol',
|
||||
'props': {
|
||||
'cols': 12,
|
||||
},
|
||||
'content': [
|
||||
{
|
||||
'component': 'VTextField',
|
||||
'props': {
|
||||
'model': 'cleanuser',
|
||||
'label': '清理下载用户',
|
||||
'placeholder': '多个用户,分割'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
], {
|
||||
"enabled": False,
|
||||
"onlyonce": False,
|
||||
"notify": False,
|
||||
"cleantype": "dest",
|
||||
"cron": "",
|
||||
"cleanuser": "",
|
||||
"cleandate": 30
|
||||
}
|
||||
{
|
||||
'component': 'VForm',
|
||||
'content': [
|
||||
{
|
||||
'component': 'VRow',
|
||||
'content': [
|
||||
{
|
||||
'component': 'VCol',
|
||||
'props': {
|
||||
'cols': 12,
|
||||
'md': 4
|
||||
},
|
||||
'content': [
|
||||
{
|
||||
'component': 'VSwitch',
|
||||
'props': {
|
||||
'model': 'enabled',
|
||||
'label': '启用插件',
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
'component': 'VCol',
|
||||
'props': {
|
||||
'cols': 12,
|
||||
'md': 4
|
||||
},
|
||||
'content': [
|
||||
{
|
||||
'component': 'VSwitch',
|
||||
'props': {
|
||||
'model': 'onlyonce',
|
||||
'label': '立即运行一次',
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
'component': 'VCol',
|
||||
'props': {
|
||||
'cols': 12,
|
||||
'md': 4
|
||||
},
|
||||
'content': [
|
||||
{
|
||||
'component': 'VSwitch',
|
||||
'props': {
|
||||
'model': 'notify',
|
||||
'label': '开启通知',
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
'component': 'VRow',
|
||||
'content': [
|
||||
{
|
||||
'component': 'VCol',
|
||||
'props': {
|
||||
'cols': 12,
|
||||
'md': 4
|
||||
},
|
||||
'content': [
|
||||
{
|
||||
'component': 'VTextField',
|
||||
'props': {
|
||||
'model': 'cron',
|
||||
'label': '执行周期',
|
||||
'placeholder': '0 0 ? ? ?'
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
'component': 'VCol',
|
||||
'props': {
|
||||
'cols': 12,
|
||||
'md': 4
|
||||
},
|
||||
'content': [
|
||||
{
|
||||
'component': 'VSelect',
|
||||
'props': {
|
||||
'model': 'cleantype',
|
||||
'label': '全局清理方式',
|
||||
'items': [
|
||||
{'title': '媒体库文件', 'value': 'dest'},
|
||||
{'title': '源文件', 'value': 'src'},
|
||||
{'title': '所有文件', 'value': 'all'},
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
'component': 'VCol',
|
||||
'props': {
|
||||
'cols': 12,
|
||||
'md': 4
|
||||
},
|
||||
'content': [
|
||||
{
|
||||
'component': 'VTextField',
|
||||
'props': {
|
||||
'model': 'cleandate',
|
||||
'label': '全局清理日期',
|
||||
'placeholder': '清理多少天之前的下载记录(天)'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
'component': 'VRow',
|
||||
'content': [
|
||||
{
|
||||
'component': 'VCol',
|
||||
'props': {
|
||||
'cols': 12,
|
||||
},
|
||||
'content': [
|
||||
{
|
||||
'component': 'VTextarea',
|
||||
'props': {
|
||||
'model': 'cleanuser',
|
||||
'label': '清理配置',
|
||||
'rows': 6,
|
||||
'placeholder': '每一行一个配置,支持以下几种配置方式,清理方式支持 src、desc、all 分别对应源文件,媒体库文件,所有文件\n'
|
||||
'用户名缺省默认清理所有用户(慎重留空),清理天数缺省默认使用全局清理天数,清理方式缺省默认使用全局清理方式\n'
|
||||
'用户名/插件名(豆瓣想看、豆瓣榜单、RSS订阅)\n'
|
||||
'用户名#清理方式\n'
|
||||
'用户名:清理天数\n'
|
||||
'用户名:清理天数#清理方式',
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
], {
|
||||
"enabled": False,
|
||||
"onlyonce": False,
|
||||
"notify": False,
|
||||
"cleantype": "dest",
|
||||
"cron": "",
|
||||
"cleanuser": "",
|
||||
"cleandate": 30
|
||||
}
|
||||
|
||||
def get_page(self) -> List[dict]:
|
||||
"""
|
||||
|
||||
@@ -96,9 +96,10 @@ class AutoSignIn(_PluginBase):
|
||||
self._clean = config.get("clean")
|
||||
|
||||
# 过滤掉已删除的站点
|
||||
all_sites = [site for site in self.sites.get_indexers() if not site.get("public")] + self.__custom_sites()
|
||||
self._sign_sites = [site.get("id") for site in all_sites if site.get("id") in self._sign_sites]
|
||||
self._login_sites = [site.get("id") for site in all_sites if site.get("id") in self._login_sites]
|
||||
all_sites = [site.id for site in self.siteoper.list_order_by_pri()] + [site.get("id") for site in
|
||||
self.__custom_sites()]
|
||||
self._sign_sites = [site_id for site_id in all_sites if site_id in self._sign_sites]
|
||||
self._login_sites = [site_id for site_id in all_sites if site_id in self._login_sites]
|
||||
# 保存配置
|
||||
self.__update_config()
|
||||
|
||||
@@ -453,6 +454,8 @@ class AutoSignIn(_PluginBase):
|
||||
{
|
||||
'component': 'VAlert',
|
||||
'props': {
|
||||
'type': 'info',
|
||||
'variant': 'tonal',
|
||||
'text': '执行周期支持:'
|
||||
'1、5位cron表达式;'
|
||||
'2、配置间隔(小时),如2.3/9-23(9-23点之间每隔2.3小时执行一次);'
|
||||
@@ -476,6 +479,8 @@ class AutoSignIn(_PluginBase):
|
||||
{
|
||||
'component': 'VAlert',
|
||||
'props': {
|
||||
'type': 'info',
|
||||
'variant': 'tonal',
|
||||
'text': '自动优选:0-关闭,命中重试关键词次数大于该数量时自动执行Cloudflare IP优选(需要开启且则正确配置Cloudflare IP优选插件和自定义Hosts插件)'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -236,6 +236,8 @@ class BestFilmVersion(_PluginBase):
|
||||
{
|
||||
'component': 'VAlert',
|
||||
'props': {
|
||||
'type': 'info',
|
||||
'variant': 'tonal',
|
||||
'text': '支持主动定时获取媒体库数据和Webhook实时触发两种方式,两者只能选其一,'
|
||||
'Webhook需要在媒体服务器设置发送Webhook报文。'
|
||||
'Plex使用主动获取时,建议执行周期设置大于1小时,'
|
||||
|
||||
@@ -249,6 +249,8 @@ class CloudDiskDel(_PluginBase):
|
||||
{
|
||||
'component': 'VAlert',
|
||||
'props': {
|
||||
'type': 'info',
|
||||
'variant': 'tonal',
|
||||
'text': '需要开启媒体库删除插件且正确配置排除路径。'
|
||||
'主要针对于strm文件删除后同步删除云盘资源。'
|
||||
'如遇删除失败,请检查文件权限问题。'
|
||||
@@ -270,6 +272,8 @@ class CloudDiskDel(_PluginBase):
|
||||
{
|
||||
'component': 'VAlert',
|
||||
'props': {
|
||||
'type': 'info',
|
||||
'variant': 'tonal',
|
||||
'text': '关于路径映射:'
|
||||
'emby:/data/series/A.mp4,'
|
||||
'moviepilot内云盘挂载路径:/mnt/link/series/A.mp4。'
|
||||
|
||||
@@ -13,6 +13,7 @@ from apscheduler.triggers.cron import CronTrigger
|
||||
from python_hosts import Hosts, HostsEntry
|
||||
from requests import Response
|
||||
|
||||
from app import schemas
|
||||
from app.core.config import settings
|
||||
from app.core.event import eventmanager, Event
|
||||
from app.log import logger
|
||||
@@ -143,7 +144,7 @@ class CloudflareSpeedTest(_PluginBase):
|
||||
self.__update_config()
|
||||
logger.warn(f"Cloudflare CDN优选未指定ip类型,默认ipv4")
|
||||
|
||||
err_flag, release_version = self.__check_envirment()
|
||||
err_flag, release_version = self.__check_environment()
|
||||
if err_flag and release_version:
|
||||
# 更新版本
|
||||
self._version = release_version
|
||||
@@ -277,7 +278,7 @@ class CloudflareSpeedTest(_PluginBase):
|
||||
self._cf_ip = max_ips[0]
|
||||
logger.info(f"获取到自定义hosts插件中ip {max_ips[0]} 出现次数最多,已自动校正优选ip")
|
||||
|
||||
def __check_envirment(self):
|
||||
def __check_environment(self):
|
||||
"""
|
||||
环境检查
|
||||
"""
|
||||
@@ -489,7 +490,13 @@ class CloudflareSpeedTest(_PluginBase):
|
||||
}]
|
||||
|
||||
def get_api(self) -> List[Dict[str, Any]]:
|
||||
pass
|
||||
return [{
|
||||
"path": "/cloudflare_speedtest",
|
||||
"endpoint": self.cloudflare_speedtest,
|
||||
"methods": ["GET"],
|
||||
"summary": "Cloudflare IP优选",
|
||||
"description": "Cloudflare IP优选",
|
||||
}]
|
||||
|
||||
def get_form(self) -> Tuple[List[dict], Dict[str, Any]]:
|
||||
"""
|
||||
@@ -695,6 +702,8 @@ class CloudflareSpeedTest(_PluginBase):
|
||||
{
|
||||
'component': 'VAlert',
|
||||
'props': {
|
||||
'type': 'info',
|
||||
'variant': 'tonal',
|
||||
'text': 'F12看请求的Server属性,如果是cloudflare说明该站点支持Cloudflare IP优选。'
|
||||
}
|
||||
}
|
||||
@@ -720,6 +729,13 @@ class CloudflareSpeedTest(_PluginBase):
|
||||
def get_page(self) -> List[dict]:
|
||||
pass
|
||||
|
||||
def cloudflare_speedtest(self) -> schemas.Response:
|
||||
"""
|
||||
API调用CloudflareSpeedTest IP优选
|
||||
"""
|
||||
self.__cloudflareSpeedTest()
|
||||
return schemas.Response(success=True)
|
||||
|
||||
@staticmethod
|
||||
def __read_system_hosts():
|
||||
"""
|
||||
|
||||
@@ -158,6 +158,8 @@ class CustomHosts(_PluginBase):
|
||||
{
|
||||
'component': 'VAlert',
|
||||
'props': {
|
||||
'type': 'info',
|
||||
'variant': 'tonal',
|
||||
'text': 'host格式ip host,中间有空格!!!'
|
||||
'(注:容器运行则更新容器hosts!非宿主机!)'
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ from watchdog.events import FileSystemEventHandler
|
||||
from watchdog.observers import Observer
|
||||
from watchdog.observers.polling import PollingObserver
|
||||
|
||||
from app import schemas
|
||||
from app.chain.tmdb import TmdbChain
|
||||
from app.chain.transfer import TransferChain
|
||||
from app.core.config import settings
|
||||
@@ -87,6 +88,7 @@ class DirMonitor(_PluginBase):
|
||||
_transfer_type = settings.TRANSFER_TYPE
|
||||
_monitor_dirs = ""
|
||||
_exclude_keywords = ""
|
||||
_interval: int = 10
|
||||
# 存储源目录与目的目录关系
|
||||
_dirconf: Dict[str, Optional[Path]] = {}
|
||||
# 存储源目录转移方式
|
||||
@@ -113,6 +115,7 @@ class DirMonitor(_PluginBase):
|
||||
self._transfer_type = config.get("transfer_type")
|
||||
self._monitor_dirs = config.get("monitor_dirs") or ""
|
||||
self._exclude_keywords = config.get("exclude_keywords") or ""
|
||||
self._interval = config.get("interval") or 10
|
||||
|
||||
# 停止现有任务
|
||||
self.stop_service()
|
||||
@@ -226,7 +229,8 @@ class DirMonitor(_PluginBase):
|
||||
"mode": self._mode,
|
||||
"transfer_type": self._transfer_type,
|
||||
"monitor_dirs": self._monitor_dirs,
|
||||
"exclude_keywords": self._exclude_keywords
|
||||
"exclude_keywords": self._exclude_keywords,
|
||||
"interval": self._interval
|
||||
})
|
||||
|
||||
@eventmanager.register(EventType.DirectorySync)
|
||||
@@ -534,8 +538,9 @@ class DirMonitor(_PluginBase):
|
||||
transferinfo = media_files[0].get("transferinfo")
|
||||
file_meta = media_files[0].get("file_meta")
|
||||
mediainfo = media_files[0].get("mediainfo")
|
||||
# 判断最后更新时间距现在是已超过10秒,超过则发送消息
|
||||
if (datetime.datetime.now() - last_update_time).total_seconds() > 10:
|
||||
# 判断剧集最后更新时间距现在是已超过10秒或者电影,发送消息
|
||||
if (datetime.datetime.now() - last_update_time).total_seconds() > int(self._interval) \
|
||||
or mediainfo.type == MediaType.MOVIE:
|
||||
# 发送通知
|
||||
if self._notify:
|
||||
|
||||
@@ -600,7 +605,20 @@ class DirMonitor(_PluginBase):
|
||||
}]
|
||||
|
||||
def get_api(self) -> List[Dict[str, Any]]:
|
||||
pass
|
||||
return [{
|
||||
"path": "/directory_sync",
|
||||
"endpoint": self.sync,
|
||||
"methods": ["GET"],
|
||||
"summary": "目录监控同步",
|
||||
"description": "目录监控同步",
|
||||
}]
|
||||
|
||||
def sync(self) -> schemas.Response:
|
||||
"""
|
||||
API调用目录同步
|
||||
"""
|
||||
self.sync_all()
|
||||
return schemas.Response(success=True)
|
||||
|
||||
def get_form(self) -> Tuple[List[dict], Dict[str, Any]]:
|
||||
return [
|
||||
@@ -667,7 +685,7 @@ class DirMonitor(_PluginBase):
|
||||
'component': 'VCol',
|
||||
'props': {
|
||||
'cols': 12,
|
||||
'md': 6
|
||||
'md': 4
|
||||
},
|
||||
'content': [
|
||||
{
|
||||
@@ -687,7 +705,7 @@ class DirMonitor(_PluginBase):
|
||||
'component': 'VCol',
|
||||
'props': {
|
||||
'cols': 12,
|
||||
'md': 6
|
||||
'md': 4
|
||||
},
|
||||
'content': [
|
||||
{
|
||||
@@ -706,6 +724,23 @@ class DirMonitor(_PluginBase):
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
'component': 'VCol',
|
||||
'props': {
|
||||
'cols': 12,
|
||||
'md': 4
|
||||
},
|
||||
'content': [
|
||||
{
|
||||
'component': 'VTextField',
|
||||
'props': {
|
||||
'model': 'interval',
|
||||
'label': '入库消息延迟',
|
||||
'placeholder': '10'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -756,6 +791,27 @@ class DirMonitor(_PluginBase):
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
'component': 'VRow',
|
||||
'content': [
|
||||
{
|
||||
'component': 'VCol',
|
||||
'props': {
|
||||
'cols': 12,
|
||||
},
|
||||
'content': [
|
||||
{
|
||||
'component': 'VAlert',
|
||||
'props': {
|
||||
'type': 'info',
|
||||
'variant': 'tonal',
|
||||
'text': '入库消息延迟默认10s,如网络较慢可酌情调大,有助于发送统一入库消息。'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -766,7 +822,8 @@ class DirMonitor(_PluginBase):
|
||||
"mode": "fast",
|
||||
"transfer_type": settings.TRANSFER_TYPE,
|
||||
"monitor_dirs": "",
|
||||
"exclude_keywords": ""
|
||||
"exclude_keywords": "",
|
||||
"interval": 10
|
||||
}
|
||||
|
||||
def get_page(self) -> List[dict]:
|
||||
|
||||
@@ -430,7 +430,7 @@ class DoubanRank(_PluginBase):
|
||||
"onlyonce": self._onlyonce,
|
||||
"vote": self._vote,
|
||||
"ranks": self._ranks,
|
||||
"rss_addrs": self._rss_addrs,
|
||||
"rss_addrs": '\n'.join(map(str, self._rss_addrs)),
|
||||
"clear": self._clear
|
||||
})
|
||||
|
||||
@@ -496,6 +496,10 @@ class DoubanRank(_PluginBase):
|
||||
if exist_flag:
|
||||
logger.info(f'{mediainfo.title_year} 媒体库中已存在')
|
||||
continue
|
||||
# 判断用户是否已经添加订阅
|
||||
if self.subscribechain.exists(mediainfo=mediainfo, meta=meta):
|
||||
logger.info(f'{mediainfo.title_year} 订阅已存在')
|
||||
continue
|
||||
# 添加订阅
|
||||
self.subscribechain.add(title=mediainfo.title,
|
||||
year=mediainfo.year,
|
||||
|
||||
@@ -506,7 +506,8 @@ class DoubanSync(_PluginBase):
|
||||
action = "subscribe"
|
||||
else:
|
||||
# 自动下载
|
||||
downloads, lefts = self.downloadchain.batch_download(contexts=contexts, no_exists=no_exists)
|
||||
downloads, lefts = self.downloadchain.batch_download(contexts=contexts, no_exists=no_exists,
|
||||
username="豆瓣想看")
|
||||
if downloads and not lefts:
|
||||
# 全部下载完成
|
||||
logger.info(f'{mediainfo.title_year} 下载完成')
|
||||
|
||||
@@ -90,8 +90,8 @@ class DownloadingMsg(_PluginBase):
|
||||
logger.error("未配置管理员用户")
|
||||
return
|
||||
|
||||
for userid in str(self._adminuser).split(","):
|
||||
self.__send_msg(torrents=torrents, userid=userid)
|
||||
for username in str(self._adminuser).split(","):
|
||||
self.__send_msg(torrents=torrents, username=username)
|
||||
|
||||
if self._type == "user" or self._type == "both":
|
||||
user_torrents = {}
|
||||
@@ -101,38 +101,38 @@ class DownloadingMsg(_PluginBase):
|
||||
if not downloadhis:
|
||||
logger.warn(f"种子 {torrent.hash} 未获取到MoviePilot下载历史,无法推送下载进度")
|
||||
continue
|
||||
if not downloadhis.userid:
|
||||
if not downloadhis.username:
|
||||
logger.debug(f"种子 {torrent.hash} 未获取到下载用户记录,无法推送下载进度")
|
||||
continue
|
||||
user_torrent = user_torrents.get(downloadhis.userid) or []
|
||||
user_torrent = user_torrents.get(downloadhis.username) or []
|
||||
user_torrent.append(torrent)
|
||||
user_torrents[downloadhis.userid] = user_torrent
|
||||
user_torrents[downloadhis.username] = user_torrent
|
||||
|
||||
if not user_torrents or not user_torrents.keys():
|
||||
logger.warn("未获取到用户下载记录,无法推送下载进度")
|
||||
return
|
||||
|
||||
# 推送用户下载任务进度
|
||||
for userid in list(user_torrents.keys()):
|
||||
if not userid:
|
||||
for username in list(user_torrents.keys()):
|
||||
if not username:
|
||||
continue
|
||||
# 如果用户是管理员,无需重复推送
|
||||
if (self._type == "admin" or self._type == "both") and self._adminuser and userid in str(
|
||||
if (self._type == "admin" or self._type == "both") and self._adminuser and username in str(
|
||||
self._adminuser).split(","):
|
||||
logger.debug("管理员已推送")
|
||||
continue
|
||||
|
||||
user_torrent = user_torrents.get(userid)
|
||||
user_torrent = user_torrents.get(username)
|
||||
if not user_torrent:
|
||||
logger.warn(f"未获取到用户 {userid} 下载任务")
|
||||
logger.warn(f"未获取到用户 {username} 下载任务")
|
||||
continue
|
||||
self.__send_msg(torrents=user_torrent,
|
||||
userid=userid)
|
||||
username=username)
|
||||
|
||||
if self._type == "all":
|
||||
self.__send_msg(torrents=torrents)
|
||||
|
||||
def __send_msg(self, torrents: Optional[List[Union[TransferTorrent, DownloadingTorrent]]], userid: str = None):
|
||||
def __send_msg(self, torrents: Optional[List[Union[TransferTorrent, DownloadingTorrent]]], username: str = None):
|
||||
"""
|
||||
发送消息
|
||||
"""
|
||||
@@ -177,7 +177,7 @@ class DownloadingMsg(_PluginBase):
|
||||
else:
|
||||
media_name = torrent.title
|
||||
|
||||
if not self._adminuser or userid not in str(self._adminuser).split(","):
|
||||
if not self._adminuser or username not in str(self._adminuser).split(","):
|
||||
# 下载用户发送精简消息
|
||||
messages.append(f"{index}. {media_name} {round(torrent.progress, 1)}%")
|
||||
else:
|
||||
@@ -197,7 +197,7 @@ class DownloadingMsg(_PluginBase):
|
||||
channel=channel,
|
||||
title=title,
|
||||
text="\n".join(messages),
|
||||
userid=userid)
|
||||
userid=username)
|
||||
|
||||
def get_state(self) -> bool:
|
||||
return self._enabled
|
||||
|
||||
@@ -277,6 +277,8 @@ class InvitesSignin(_PluginBase):
|
||||
{
|
||||
'component': 'VAlert',
|
||||
'props': {
|
||||
'type': 'info',
|
||||
'variant': 'tonal',
|
||||
'text': '整点定时签到失败?不妨换个时间试试'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,11 +11,9 @@ from lxml import etree
|
||||
from ruamel.yaml import CommentedMap
|
||||
|
||||
from app.core.config import settings
|
||||
from app.core.event import eventmanager
|
||||
from app.db.site_oper import SiteOper
|
||||
from app.helper.sites import SitesHelper
|
||||
|
||||
from app.core.event import eventmanager
|
||||
from app.db.models.site import Site
|
||||
from app.helper.torrent import TorrentHelper
|
||||
from app.log import logger
|
||||
from app.modules.qbittorrent import Qbittorrent
|
||||
@@ -117,8 +115,9 @@ class IYUUAutoSeed(_PluginBase):
|
||||
self._success_caches = [] if self._clearcache else config.get("success_caches") or []
|
||||
|
||||
# 过滤掉已删除的站点
|
||||
self._sites = [site.get("id") for site in self.sites.get_indexers() if
|
||||
not site.get("public") and site.get("id") in self._sites]
|
||||
all_sites = [site.id for site in self.siteoper.list_order_by_pri()] + [site.get("id") for site in
|
||||
self.__custom_sites()]
|
||||
self._sites = [site_id for site_id in all_sites if site_id in self._sites]
|
||||
self.__update_config()
|
||||
|
||||
# 停止现有任务
|
||||
@@ -177,9 +176,14 @@ class IYUUAutoSeed(_PluginBase):
|
||||
"""
|
||||
拼装插件配置页面,需要返回两块数据:1、页面配置;2、数据结构
|
||||
"""
|
||||
# 站点的可选项(内置站点 + 自定义站点)
|
||||
customSites = self.__custom_sites()
|
||||
|
||||
# 站点的可选项
|
||||
site_options = [{"title": site.name, "value": site.id}
|
||||
for site in self.siteoper.list_order_by_pri()]
|
||||
site_options = ([{"title": site.name, "value": site.id}
|
||||
for site in self.siteoper.list_order_by_pri()]
|
||||
+ [{"title": site.get("name"), "value": site.get("id")}
|
||||
for site in customSites])
|
||||
return [
|
||||
{
|
||||
'component': 'VForm',
|
||||
@@ -988,6 +992,13 @@ class IYUUAutoSeed(_PluginBase):
|
||||
except Exception as e:
|
||||
print(str(e))
|
||||
|
||||
def __custom_sites(self) -> List[Any]:
|
||||
custom_sites = []
|
||||
custom_sites_config = self.get_config("CustomSites")
|
||||
if custom_sites_config and custom_sites_config.get("enabled"):
|
||||
custom_sites = custom_sites_config.get("sites")
|
||||
return custom_sites
|
||||
|
||||
@eventmanager.register(EventType.SiteDeleted)
|
||||
def site_deleted(self, event):
|
||||
"""
|
||||
|
||||
@@ -266,6 +266,8 @@ class MediaSyncDel(_PluginBase):
|
||||
{
|
||||
'component': 'VAlert',
|
||||
'props': {
|
||||
'type': 'info',
|
||||
'variant': 'tonal',
|
||||
'text': '媒体库同步方式分为Webhook、日志同步和Scripter X:'
|
||||
'1、Webhook需要Emby4.8.0.45及以上开启媒体删除的Webhook。'
|
||||
'2、日志同步需要配置检查周期,默认30分钟执行一次。'
|
||||
@@ -289,10 +291,13 @@ class MediaSyncDel(_PluginBase):
|
||||
{
|
||||
'component': 'VAlert',
|
||||
'props': {
|
||||
'text': '关于路径映射:'
|
||||
'type': 'info',
|
||||
'variant': 'tonal',
|
||||
'text': '关于路径映射(转移后文件):'
|
||||
'emby:/data/series/A.mp4,'
|
||||
'moviepilot:/mnt/link/series/A.mp4。'
|
||||
'路径映射填/data:/mnt/link'
|
||||
'路径映射填/data:/mnt/link。'
|
||||
'不正确配置会导致查询不到转移记录!'
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -311,6 +316,8 @@ class MediaSyncDel(_PluginBase):
|
||||
{
|
||||
'component': 'VAlert',
|
||||
'props': {
|
||||
'type': 'info',
|
||||
'variant': 'tonal',
|
||||
'text': '排除路径:命中排除路径后请求云盘删除插件删除云盘资源。'
|
||||
}
|
||||
}
|
||||
@@ -880,7 +887,7 @@ class MediaSyncDel(_PluginBase):
|
||||
logger.info(f"正在同步删除 {msg}")
|
||||
|
||||
if not transfer_history:
|
||||
logger.info(f"未获取到 {msg} 转移记录")
|
||||
logger.info(f"未获取到 {msg} 转移记录,请检查路径映射是否配置错误,请检查tmdbid获取是否正确")
|
||||
self.save_data("last_time", last_del_time or datetime.datetime.now())
|
||||
continue
|
||||
|
||||
|
||||
@@ -232,6 +232,8 @@ class MoviePilotUpdateNotify(_PluginBase):
|
||||
{
|
||||
'component': 'VAlert',
|
||||
'props': {
|
||||
'type': 'info',
|
||||
'variant': 'tonal',
|
||||
'text': '如要开启自动重启,请确认MOVIEPILOT_AUTO_UPDATE设置为true,重启即更新。'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -624,6 +624,8 @@ class NAStoolSync(_PluginBase):
|
||||
{
|
||||
'component': 'VAlert',
|
||||
'props': {
|
||||
'type': 'info',
|
||||
'variant': 'tonal',
|
||||
'text': '开启清空记录时,会在导入历史数据之前删除MoviePilot之前的记录。'
|
||||
'如果转移记录很多,同步时间可能会长(3-10分钟),'
|
||||
'所以点击确定后页面没反应是正常现象,后台正在处理。'
|
||||
|
||||
@@ -628,7 +628,8 @@ class RssSubscribe(_PluginBase):
|
||||
media_info=mediainfo,
|
||||
torrent_info=torrentinfo,
|
||||
),
|
||||
save_path=self._save_path
|
||||
save_path=self._save_path,
|
||||
username="RSS订阅"
|
||||
)
|
||||
if not result:
|
||||
logger.error(f'{title} 下载失败')
|
||||
|
||||
@@ -198,6 +198,8 @@ class SiteRefresh(_PluginBase):
|
||||
{
|
||||
'component': 'VAlert',
|
||||
'props': {
|
||||
'type': 'info',
|
||||
'variant': 'tonal',
|
||||
'text': '站点签到提示Cookie过期时自动触发。'
|
||||
'不支持开启两步认证的站点。'
|
||||
'不是所有站点都支持,失败请手动更新。'
|
||||
|
||||
@@ -89,9 +89,9 @@ class SiteStatistic(_PluginBase):
|
||||
self._statistic_sites = config.get("statistic_sites") or []
|
||||
|
||||
# 过滤掉已删除的站点
|
||||
all_sites = [site for site in self.sites.get_indexers() if not site.get("public")] + self.__custom_sites()
|
||||
self._statistic_sites = [site.get("id") for site in all_sites if
|
||||
not site.get("public") and site.get("id") in self._statistic_sites]
|
||||
all_sites = [site.id for site in self.siteoper.list_order_by_pri()] + [site.get("id") for site in
|
||||
self.__custom_sites()]
|
||||
self._statistic_sites = [site_id for site_id in all_sites if site_id in self._statistic_sites]
|
||||
self.__update_config()
|
||||
|
||||
if self._enabled or self._onlyonce:
|
||||
|
||||
@@ -543,6 +543,8 @@ class SyncDownloadFiles(_PluginBase):
|
||||
{
|
||||
'component': 'VAlert',
|
||||
'props': {
|
||||
'type': 'info',
|
||||
'variant': 'tonal',
|
||||
'text': '适用于非MoviePilot下载的任务;下载器种子数据较多时,同步时间将会较长,请耐心等候,可查看实时日志了解同步进度;时间间隔建议最少每6小时执行一次,防止上次任务没处理完。'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -488,6 +488,8 @@ class TorrentRemover(_PluginBase):
|
||||
{
|
||||
'component': 'VAlert',
|
||||
'props': {
|
||||
'type': 'info',
|
||||
'variant': 'tonal',
|
||||
'text': '自动删种存在风险,如设置不当可能导致数据丢失!建议动作先选择暂停,确定条件正确后再改成删除。'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,6 +71,10 @@ class Scheduler(metaclass=Singleton):
|
||||
"transfer": {
|
||||
"func": TransferChain().process,
|
||||
"running": False,
|
||||
},
|
||||
"clear_cache": {
|
||||
"func": SchedulerChain().clear_cache,
|
||||
"running": False,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -202,6 +206,18 @@ class Scheduler(metaclass=Singleton):
|
||||
minutes=10
|
||||
)
|
||||
|
||||
# 缓存清理服务,每隔24小时
|
||||
self._scheduler.add_job(
|
||||
self.start,
|
||||
"interval",
|
||||
id="clear_cache",
|
||||
name="缓存清理",
|
||||
hours=settings.CACHE_CONF.get("meta") / 3600,
|
||||
kwargs={
|
||||
'job_id': 'clear_cache'
|
||||
}
|
||||
)
|
||||
|
||||
# 打印服务
|
||||
logger.debug(self._scheduler.print_jobs())
|
||||
|
||||
|
||||
@@ -38,6 +38,8 @@ class DownloadHistory(BaseModel):
|
||||
torrent_site: Optional[str] = None
|
||||
# 下载用户
|
||||
userid: Optional[str] = None
|
||||
# 下载用户名
|
||||
username: Optional[str] = None
|
||||
# 下载渠道
|
||||
channel: Optional[str] = None
|
||||
# 创建时间
|
||||
|
||||
@@ -31,6 +31,7 @@ class DownloadingTorrent(BaseModel):
|
||||
dlspeed: Optional[str] = None
|
||||
media: Optional[dict] = {}
|
||||
userid: Optional[str] = None
|
||||
left_time: Optional[str] = None
|
||||
|
||||
|
||||
class TransferInfo(BaseModel):
|
||||
|
||||
@@ -49,6 +49,10 @@ def lru_cache_without_none(maxsize=None, typed=False):
|
||||
if result is not None:
|
||||
return result
|
||||
|
||||
def cache_clear():
|
||||
cache.cache_clear()
|
||||
|
||||
wrapper.cache_clear = cache_clear
|
||||
return wrapper
|
||||
|
||||
return decorator
|
||||
|
||||
@@ -63,6 +63,23 @@ class StringUtils:
|
||||
b, u = d[index]
|
||||
return str(round(time_sec / (b + 1))) + u
|
||||
|
||||
@staticmethod
|
||||
def str_secends(time_sec: Union[str, int, float]) -> str:
|
||||
"""
|
||||
将秒转为时分秒字符串
|
||||
"""
|
||||
hours = time_sec // 3600
|
||||
remainder_seconds = time_sec % 3600
|
||||
minutes = remainder_seconds // 60
|
||||
seconds = remainder_seconds % 60
|
||||
|
||||
time: str = str(int(seconds)) + '秒'
|
||||
if minutes:
|
||||
time = str(int(minutes)) + '分' + time
|
||||
if hours:
|
||||
time = str(int(hours)) + '时' + time
|
||||
return time
|
||||
|
||||
@staticmethod
|
||||
def is_chinese(word: Union[str, list]) -> bool:
|
||||
"""
|
||||
|
||||
@@ -141,6 +141,10 @@ QB_USER=
|
||||
QB_PASSWORD=
|
||||
# Qbittorrent分类自动管理
|
||||
QB_CATEGORY=false
|
||||
# Qbittorrent按顺序下载
|
||||
QB_SEQUENTIAL=true
|
||||
# Qbittorrent忽略队列限制,强制继续
|
||||
QB_FORCE_RESUME=false
|
||||
# Transmission地址,IP:PORT
|
||||
TR_HOST=
|
||||
# Transmission用户名
|
||||
|
||||
30
database/versions/06abf3e7090b_1_0_11.py
Normal file
30
database/versions/06abf3e7090b_1_0_11.py
Normal file
@@ -0,0 +1,30 @@
|
||||
"""1.0.11
|
||||
|
||||
Revision ID: 06abf3e7090b
|
||||
Revises: d633ca6cd572
|
||||
Create Date: 2023-10-27 12:22:56.213376
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '06abf3e7090b'
|
||||
down_revision = 'd633ca6cd572'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
try:
|
||||
with op.batch_alter_table("downloadhistory") as batch_op:
|
||||
batch_op.add_column(sa.Column('username', sa.String, nullable=True))
|
||||
except Exception as e:
|
||||
pass
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
pass
|
||||
@@ -1 +1 @@
|
||||
APP_VERSION = 'v1.3.7'
|
||||
APP_VERSION = 'v1.3.8'
|
||||
|
||||
Reference in New Issue
Block a user