Compare commits

...

56 Commits

Author SHA1 Message Date
jxxghp
b23f78e94d v1.3.8 2023-10-31 11:50:18 +08:00
jxxghp
812a9a55d0 fix Plugin Alerts UI 2023-10-31 11:48:36 +08:00
jxxghp
2e289e80d1 fix 2023-10-31 10:37:36 +08:00
jxxghp
0d3dfdcbda feat 服务增加清理缓存 2023-10-31 10:32:56 +08:00
jxxghp
87eae72f51 fix #1024 2023-10-31 07:08:28 +08:00
jxxghp
17fa7101bd fix so 2023-10-30 13:35:19 +08:00
jxxghp
312bd53079 fix #1012 2023-10-29 16:08:34 +08:00
jxxghp
4bc7d47576 Merge pull request #1012 from Shurelol/main 2023-10-29 15:56:34 +08:00
Shurelol
71445b56f1 feat: 名称识别支持tmdbid等标记 2023-10-29 13:49:08 +08:00
jxxghp
9ce9e0a4ef fix #1006 2023-10-28 20:27:48 +08:00
jxxghp
ae196f1aeb Merge pull request #1006 from thsrite/main
fix bug
2023-10-28 20:27:25 +08:00
thsrite
38e09b894d fix bug 2023-10-28 20:24:16 +08:00
jxxghp
247d5ff255 Merge pull request #999 from honue/main
enhance 定期清理插件
2023-10-28 20:20:28 +08:00
jxxghp
0091e462fa Merge pull request #1004 from thsrite/main
fix 下载进度推送username
2023-10-28 20:19:12 +08:00
thsrite
7b314970b5 fix 下载进度推送username 2023-10-28 19:47:27 +08:00
jxxghp
7ac881e3e3 Merge pull request #1003 from thsrite/main 2023-10-28 19:31:29 +08:00
thsrite
8874723632 fix 认证失败后插件站点缺失bug 2023-10-28 19:14:08 +08:00
thsrite
262bda94c4 fix 目录监控入库消息延迟支持自定义 2023-10-28 18:52:46 +08:00
honue
d6e2cab5ef 兼容1.3.7版本清理插件配置 2023-10-28 18:40:27 +08:00
Summer⛱
6d3e33a05d Merge branch 'main' into main 2023-10-28 18:30:01 +08:00
jxxghp
f2d0bec0ac fix README 2023-10-28 17:46:57 +08:00
jxxghp
dea78f4bfd fix 2023-10-28 17:45:28 +08:00
jxxghp
f85f4b1342 Merge pull request #1000 from WithdewHua/qb
feat: qb 支持强制继续
2023-10-28 17:45:03 +08:00
WithdewHua
d03771f8ab feat: qb 支持强制继续 2023-10-28 17:41:31 +08:00
jxxghp
4b655dfac4 fix #957
fix #982
2023-10-28 17:41:22 +08:00
honue
cdfcdd80bf fix 2023-10-28 17:16:59 +08:00
honue
64d3942ba9 enhance 定期清理插件 2023-10-28 17:11:48 +08:00
jxxghp
16cce73f82 Merge pull request #996 from honue/main 2023-10-28 13:15:36 +08:00
honue
846edff84a fix 豆瓣榜单插件 2023-10-28 13:13:06 +08:00
jxxghp
d038bf31d3 Merge pull request #995 from honue/main 2023-10-28 12:41:50 +08:00
honue
376a69af5c fix 豆瓣榜单插件 2023-10-28 12:36:06 +08:00
jxxghp
380bb9bb3d Merge pull request #994 from thsrite/main 2023-10-28 12:26:27 +08:00
thsrite
f59e10ae1d fix qb按顺序下载支持变量配置 2023-10-28 12:24:24 +08:00
jxxghp
c8d2d80cc5 feat 支持配置多个认证站点 2023-10-28 11:50:50 +08:00
jxxghp
f0bb9ddfca Merge pull request #993 from thsrite/main
fix 目录监控消息电影不用等待直接发送入库消息
2023-10-28 11:46:23 +08:00
thsrite
9ab86e4a85 fix 目录监控消息电影不用等待直接发送入库消息 2023-10-28 11:41:44 +08:00
jxxghp
e33f1a3ffc Merge pull request #992 from thsrite/main
fix plugin api
2023-10-28 11:28:36 +08:00
jxxghp
e2213e1ef6 fix 远程搜索选择序号问题 2023-10-28 11:25:24 +08:00
thsrite
bbc4a1bfa5 fix plugin api 2023-10-28 11:22:37 +08:00
jxxghp
61e7ec9a36 Merge pull request #991 from honue/main 2023-10-28 11:06:25 +08:00
jxxghp
534ad0bad6 Merge pull request #987 from thsrite/main 2023-10-28 11:04:21 +08:00
thsrite
db3040a50e fix 2023-10-28 11:02:34 +08:00
honue
8dd74e7dd8 fix 完善页面download传参username 2023-10-28 11:02:26 +08:00
jxxghp
206cdb2663 Merge pull request #988 from khalid586/main 2023-10-28 10:58:24 +08:00
jxxghp
ca334813b7 更新 __init__.py 2023-10-28 10:55:02 +08:00
jxxghp
5fc93ee8e6 Merge pull request #986 from WithdewHua/fix-mediaserver 2023-10-28 10:52:53 +08:00
Khalid Abdullah
9cef7b2615 Update __init__.py(typos fixed) 2023-10-27 23:56:37 +06:00
thsrite
a3916207ae fix division by zero 2023-10-27 23:13:56 +08:00
thsrite
b6e1702051 fix add plugins api 2023-10-27 21:28:37 +08:00
WithdewHua
2cfc8b1ec7 fix: 重连判断 2023-10-27 20:31:52 +08:00
jxxghp
2f7570eec1 Merge pull request #983 from thsrite/main 2023-10-27 17:06:36 +08:00
thsrite
070481cab0 fix 正在下载显示剩余下载时间 2023-10-27 16:52:47 +08:00
jxxghp
26cd2c6cfe Merge pull request #982 from honue/main 2023-10-27 16:25:34 +08:00
honue
1ff571eb46 fix 定时清理媒体库,增加username字段 2023-10-27 15:12:10 +08:00
jxxghp
d8fcb4d240 Merge pull request #980 from thsrite/main 2023-10-27 10:42:45 +08:00
thsrite
778f97c1f3 fix log友好提示 2023-10-27 10:41:31 +08:00
52 changed files with 674 additions and 283 deletions

View File

@@ -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`
| 站点 | 参数 |

View File

@@ -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
})

View File

@@ -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,

View File

@@ -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)

View File

@@ -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,

View File

@@ -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

View File

@@ -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} 下载完成')

View File

@@ -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)

View File

@@ -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, ""

View File

@@ -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用户名

View File

@@ -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

View File

@@ -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)

View File

@@ -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.

View File

@@ -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]:

View File

@@ -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):

View File

@@ -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]:

View File

@@ -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

View File

@@ -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)

View File

@@ -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

View File

@@ -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。'
}
}

View File

@@ -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]:
"""

View File

@@ -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-239-23点之间每隔2.3小时执行一次);'
@@ -476,6 +479,8 @@ class AutoSignIn(_PluginBase):
{
'component': 'VAlert',
'props': {
'type': 'info',
'variant': 'tonal',
'text': '自动优选0-关闭命中重试关键词次数大于该数量时自动执行Cloudflare IP优选需要开启且则正确配置Cloudflare IP优选插件和自定义Hosts插件'
}
}

View File

@@ -236,6 +236,8 @@ class BestFilmVersion(_PluginBase):
{
'component': 'VAlert',
'props': {
'type': 'info',
'variant': 'tonal',
'text': '支持主动定时获取媒体库数据和Webhook实时触发两种方式两者只能选其一'
'Webhook需要在媒体服务器设置发送Webhook报文。'
'Plex使用主动获取时建议执行周期设置大于1小时'

View File

@@ -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。'

View File

@@ -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():
"""

View File

@@ -158,6 +158,8 @@ class CustomHosts(_PluginBase):
{
'component': 'VAlert',
'props': {
'type': 'info',
'variant': 'tonal',
'text': 'host格式ip host中间有空格'
'容器运行则更新容器hosts非宿主机'
}

View File

@@ -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]:

View File

@@ -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,

View File

@@ -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} 下载完成')

View File

@@ -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

View File

@@ -277,6 +277,8 @@ class InvitesSignin(_PluginBase):
{
'component': 'VAlert',
'props': {
'type': 'info',
'variant': 'tonal',
'text': '整点定时签到失败?不妨换个时间试试'
}
}

View File

@@ -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):
"""

View File

@@ -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

View File

@@ -232,6 +232,8 @@ class MoviePilotUpdateNotify(_PluginBase):
{
'component': 'VAlert',
'props': {
'type': 'info',
'variant': 'tonal',
'text': '如要开启自动重启请确认MOVIEPILOT_AUTO_UPDATE设置为true重启即更新。'
}
}

View File

@@ -624,6 +624,8 @@ class NAStoolSync(_PluginBase):
{
'component': 'VAlert',
'props': {
'type': 'info',
'variant': 'tonal',
'text': '开启清空记录时会在导入历史数据之前删除MoviePilot之前的记录。'
'如果转移记录很多同步时间可能会长3-10分钟'
'所以点击确定后页面没反应是正常现象,后台正在处理。'

View File

@@ -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} 下载失败')

View File

@@ -198,6 +198,8 @@ class SiteRefresh(_PluginBase):
{
'component': 'VAlert',
'props': {
'type': 'info',
'variant': 'tonal',
'text': '站点签到提示Cookie过期时自动触发。'
'不支持开启两步认证的站点。'
'不是所有站点都支持,失败请手动更新。'

View File

@@ -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:

View File

@@ -543,6 +543,8 @@ class SyncDownloadFiles(_PluginBase):
{
'component': 'VAlert',
'props': {
'type': 'info',
'variant': 'tonal',
'text': '适用于非MoviePilot下载的任务下载器种子数据较多时同步时间将会较长请耐心等候可查看实时日志了解同步进度时间间隔建议最少每6小时执行一次防止上次任务没处理完。'
}
}

View File

@@ -488,6 +488,8 @@ class TorrentRemover(_PluginBase):
{
'component': 'VAlert',
'props': {
'type': 'info',
'variant': 'tonal',
'text': '自动删种存在风险,如设置不当可能导致数据丢失!建议动作先选择暂停,确定条件正确后再改成删除。'
}
}

View File

@@ -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())

View File

@@ -38,6 +38,8 @@ class DownloadHistory(BaseModel):
torrent_site: Optional[str] = None
# 下载用户
userid: Optional[str] = None
# 下载用户名
username: Optional[str] = None
# 下载渠道
channel: Optional[str] = None
# 创建时间

View File

@@ -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):

View File

@@ -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

View File

@@ -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:
"""

View File

@@ -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用户名

View 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

View File

@@ -1 +1 @@
APP_VERSION = 'v1.3.7'
APP_VERSION = 'v1.3.8'