Merge branch 'jxxghp:main' into main
This commit is contained in:
commit
8ebc2875ba
68
app/api/endpoints/rss.py
Normal file
68
app/api/endpoints/rss.py
Normal file
@ -0,0 +1,68 @@
|
||||
from typing import List, Any
|
||||
|
||||
from fastapi import APIRouter, Depends
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app import schemas
|
||||
from app.core.security import verify_token
|
||||
from app.db import get_db
|
||||
from app.db.models.rss import Rss
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/", summary="所有自定义订阅", response_model=List[schemas.Rss])
|
||||
def read_rsses(
|
||||
db: Session = Depends(get_db),
|
||||
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
|
||||
"""
|
||||
查询所有自定义订阅
|
||||
"""
|
||||
return Rss.list(db)
|
||||
|
||||
|
||||
@router.post("/", summary="新增自定义订阅", response_model=schemas.Response)
|
||||
def create_rss(
|
||||
*,
|
||||
db: Session = Depends(get_db),
|
||||
rss_in: schemas.Rss,
|
||||
_: schemas.TokenPayload = Depends(verify_token)
|
||||
) -> Any:
|
||||
"""
|
||||
新增自定义订阅
|
||||
"""
|
||||
rss = Rss.get_by_tmdbid(db, tmdbid=rss_in.tmdbid, season=rss_in.season)
|
||||
if rss:
|
||||
return schemas.Response(success=False, message="自定义订阅已存在")
|
||||
rss = Rss(**rss_in.dict())
|
||||
rss.create(db)
|
||||
return schemas.Response(success=True)
|
||||
|
||||
|
||||
@router.put("/", summary="更新自定义订阅", response_model=schemas.Response)
|
||||
def update_rss(
|
||||
*,
|
||||
rss_in: schemas.Rss,
|
||||
db: Session = Depends(get_db),
|
||||
_: schemas.TokenPayload = Depends(verify_token)
|
||||
) -> Any:
|
||||
"""
|
||||
更新自定义订阅信息
|
||||
"""
|
||||
rss = Rss.get(db, rss_in.id)
|
||||
if not rss:
|
||||
return schemas.Response(success=False, message="自定义订阅不存在")
|
||||
|
||||
rss.update(db, rss_in.dict())
|
||||
return schemas.Response(success=True)
|
||||
|
||||
|
||||
@router.get("/{rssid}", summary="查询订阅详情", response_model=schemas.Rss)
|
||||
def read_rss(
|
||||
rssid: int,
|
||||
db: Session = Depends(get_db),
|
||||
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
|
||||
"""
|
||||
根据ID查询自定义订阅详情
|
||||
"""
|
||||
return Rss.get(db, rssid)
|
155
app/chain/rss.py
Normal file
155
app/chain/rss.py
Normal file
@ -0,0 +1,155 @@
|
||||
import re
|
||||
import time
|
||||
|
||||
from app.chain import ChainBase
|
||||
from app.chain.download import DownloadChain
|
||||
from app.core.config import settings
|
||||
from app.core.context import Context, TorrentInfo, MediaInfo
|
||||
from app.core.metainfo import MetaInfo
|
||||
from app.db.rss_oper import RssOper
|
||||
from app.db.systemconfig_oper import SystemConfigOper
|
||||
from app.helper.rss import RssHelper
|
||||
from app.helper.sites import SitesHelper
|
||||
from app.log import logger
|
||||
from app.schemas import Notification
|
||||
from app.schemas.types import SystemConfigKey, MediaType, NotificationType
|
||||
from app.utils.string import StringUtils
|
||||
|
||||
|
||||
class RssChain(ChainBase):
|
||||
"""
|
||||
RSS处理链
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.rssoper = RssOper()
|
||||
self.sites = SitesHelper()
|
||||
self.systemconfig = SystemConfigOper()
|
||||
self.downloadchain = DownloadChain()
|
||||
|
||||
def refresh(self):
|
||||
"""
|
||||
刷新RSS订阅数据
|
||||
"""
|
||||
# 所有RSS订阅
|
||||
logger.info("开始刷新RSS订阅数据 ...")
|
||||
rss_tasks = self.rssoper.list() or []
|
||||
for rss_task in rss_tasks:
|
||||
if not rss_task.url:
|
||||
continue
|
||||
# 下载Rss报文
|
||||
items = RssHelper.parse(rss_task.url, True if rss_task.proxy else False)
|
||||
if not items:
|
||||
logger.error(f"RSS未下载到数据:{rss_task.url}")
|
||||
logger.info(f"{rss_task.name} RSS下载到数据:{len(items)}")
|
||||
# 检查站点
|
||||
domain = StringUtils.get_url_domain(rss_task.url)
|
||||
site_info = self.sites.get_indexer(domain)
|
||||
if not site_info:
|
||||
logger.error(f"{rss_task.name} 没有维护对应站点")
|
||||
continue
|
||||
# 过滤规则
|
||||
if rss_task.best_version:
|
||||
filter_rule = self.systemconfig.get(SystemConfigKey.FilterRules2)
|
||||
else:
|
||||
filter_rule = self.systemconfig.get(SystemConfigKey.FilterRules)
|
||||
# 处理RSS条目
|
||||
matched_contexts = []
|
||||
for item in items:
|
||||
if not item.get("title"):
|
||||
continue
|
||||
# 基本要素匹配
|
||||
if rss_task.include \
|
||||
and not re.search(r"%s" % rss_task.include, item.get("title")):
|
||||
logger.info(f"{item.get('title')} 未包含 {rss_task.include}")
|
||||
continue
|
||||
if rss_task.exclude \
|
||||
and re.search(r"%s" % rss_task.exclude, item.get("title")):
|
||||
logger.info(f"{item.get('title')} 包含 {rss_task.exclude}")
|
||||
continue
|
||||
# 识别媒体信息
|
||||
meta = MetaInfo(title=item.get("title"), subtitle=item.get("description"))
|
||||
if not meta.name:
|
||||
logger.error(f"{item.get('title')} 未识别到有效信息")
|
||||
continue
|
||||
mediainfo = self.recognize_media(meta=meta)
|
||||
if not mediainfo:
|
||||
logger.error(f"{item.get('title')} 未识别到TMDB媒体信息")
|
||||
continue
|
||||
if mediainfo.tmdb_id != rss_task.tmdbid:
|
||||
logger.error(f"{item.get('title')} 不匹配")
|
||||
continue
|
||||
# 种子
|
||||
torrentinfo = TorrentInfo(
|
||||
site=site_info.get("id"),
|
||||
site_name=site_info.get("name"),
|
||||
site_cookie=site_info.get("cookie"),
|
||||
site_ua=site_info.get("cookie") or settings.USER_AGENT,
|
||||
site_proxy=site_info.get("proxy"),
|
||||
site_order=site_info.get("pri"),
|
||||
title=item.get("title"),
|
||||
description=item.get("description"),
|
||||
enclosure=item.get("enclosure"),
|
||||
page_url=item.get("link"),
|
||||
size=item.get("size"),
|
||||
pubdate=time.strftime("%Y-%m-%d %H:%M:%S", item.get("pubdate")) if item.get("pubdate") else None,
|
||||
)
|
||||
# 过滤种子
|
||||
result = self.filter_torrents(
|
||||
rule_string=filter_rule,
|
||||
torrent_list=[torrentinfo]
|
||||
)
|
||||
if not result:
|
||||
logger.info(f"{rss_task.name} 不匹配过滤规则")
|
||||
continue
|
||||
# 匹配
|
||||
mediainfo.clear()
|
||||
matched_contexts.append(Context(
|
||||
meta_info=meta,
|
||||
media_info=mediainfo,
|
||||
torrent_info=torrentinfo
|
||||
))
|
||||
# 过滤规则
|
||||
if not matched_contexts:
|
||||
logger.info(f"{rss_task.name} 未匹配到数据")
|
||||
continue
|
||||
logger.info(f"{rss_task.name} 匹配到 {len(matched_contexts)} 条数据")
|
||||
# 查询本地存在情况
|
||||
if not rss_task.best_version:
|
||||
# 查询缺失的媒体信息
|
||||
rss_meta = MetaInfo(title=rss_task.title)
|
||||
rss_meta.type = MediaType(rss_task.type)
|
||||
exist_flag, no_exists = self.downloadchain.get_no_exists_info(
|
||||
meta=rss_meta,
|
||||
mediainfo=MediaInfo(
|
||||
title=rss_task.title,
|
||||
year=rss_task.year,
|
||||
tmdb_id=rss_task.tmdbid,
|
||||
season=rss_task.season
|
||||
),
|
||||
)
|
||||
if exist_flag:
|
||||
logger.info(f'{rss_task.name} 媒体库中已存在,完成订阅')
|
||||
self.rssoper.delete(rss_task.id)
|
||||
# 发送通知
|
||||
self.post_message(Notification(mtype=NotificationType.Subscribe,
|
||||
title=f'自定义订阅 {rss_task.name} 已完成',
|
||||
image=rss_task.backdrop))
|
||||
continue
|
||||
else:
|
||||
no_exists = {}
|
||||
# 开始下载
|
||||
downloads, lefts = self.downloadchain.batch_download(contexts=matched_contexts,
|
||||
no_exists=no_exists)
|
||||
if downloads and not lefts:
|
||||
if not rss_task.best_version:
|
||||
self.rssoper.delete(rss_task.id)
|
||||
# 发送通知
|
||||
self.post_message(Notification(mtype=NotificationType.Subscribe,
|
||||
title=f'自定义订阅 {rss_task.name} 已完成',
|
||||
image=rss_task.backdrop))
|
||||
# 未完成下载
|
||||
logger.info(f'{rss_task.name} 未下载未完整,继续订阅 ...')
|
||||
|
||||
logger.info("刷新RSS订阅数据完成")
|
@ -414,15 +414,15 @@ class SubscribeChain(ChainBase):
|
||||
else:
|
||||
logger.info(f'{indexer.get("name")} 获取到种子')
|
||||
# 从缓存中匹配订阅
|
||||
self.__match(torrents_cache)
|
||||
self.match(torrents_cache)
|
||||
# 保存缓存到本地
|
||||
self.save_cache(torrents_cache, self._cache_file)
|
||||
|
||||
def __match(self, torrents_cache: Dict[str, List[Context]]):
|
||||
def match(self, torrents: Dict[str, List[Context]]):
|
||||
"""
|
||||
从缓存中匹配订阅,并自动下载
|
||||
"""
|
||||
if not torrents_cache:
|
||||
if not torrents:
|
||||
logger.warn('没有缓存资源,无法匹配订阅')
|
||||
return
|
||||
# 所有订阅
|
||||
@ -482,7 +482,7 @@ class SubscribeChain(ChainBase):
|
||||
no_exists = {}
|
||||
# 遍历缓存种子
|
||||
_match_context = []
|
||||
for domain, contexts in torrents_cache.items():
|
||||
for domain, contexts in torrents.items():
|
||||
for context in contexts:
|
||||
# 检查是否匹配
|
||||
torrent_meta = context.meta_info
|
||||
|
60
app/db/models/rss.py
Normal file
60
app/db/models/rss.py
Normal file
@ -0,0 +1,60 @@
|
||||
from sqlalchemy import Column, Integer, String, Sequence
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.db.models import Base
|
||||
|
||||
|
||||
class Rss(Base):
|
||||
"""
|
||||
RSS订阅
|
||||
"""
|
||||
id = Column(Integer, Sequence('id'), primary_key=True, index=True)
|
||||
# 名称
|
||||
name = Column(String, nullable=False)
|
||||
# RSS地址
|
||||
url = Column(String, nullable=False)
|
||||
# 类型
|
||||
type = Column(String)
|
||||
# 标题
|
||||
title = Column(String)
|
||||
# 年份
|
||||
year = Column(String)
|
||||
# TMDBID
|
||||
tmdbid = Column(Integer, index=True)
|
||||
# 季号
|
||||
season = Column(Integer)
|
||||
# 海报
|
||||
poster = Column(String)
|
||||
# 背景图
|
||||
backdrop = Column(String)
|
||||
# 评分
|
||||
vote = Column(Integer)
|
||||
# 简介
|
||||
description = Column(String)
|
||||
# 包含
|
||||
include = Column(String)
|
||||
# 排除
|
||||
exclude = Column(String)
|
||||
# 洗版
|
||||
best_version = Column(Integer)
|
||||
# 是否使用代理服务器
|
||||
proxy = Column(Integer)
|
||||
# 保存路径
|
||||
save_path = Column(String)
|
||||
# 附加信息,已处理数据
|
||||
note = Column(String)
|
||||
# 最后更新时间
|
||||
last_update = Column(String)
|
||||
# 状态 0-停用,1-启用
|
||||
state = Column(Integer)
|
||||
|
||||
@staticmethod
|
||||
def get_by_tmdbid(db: Session, tmdbid: int, season: int = None):
|
||||
if season:
|
||||
return db.query(Rss).filter(Rss.tmdbid == tmdbid,
|
||||
Rss.season == season).all()
|
||||
return db.query(Rss).filter(Rss.tmdbid == tmdbid).all()
|
||||
|
||||
@staticmethod
|
||||
def get_by_title(db: Session, title: str):
|
||||
return db.query(Rss).filter(Rss.title == title).first()
|
40
app/db/rss_oper.py
Normal file
40
app/db/rss_oper.py
Normal file
@ -0,0 +1,40 @@
|
||||
from typing import List
|
||||
|
||||
from app.db import DbOper, SessionLocal
|
||||
from app.db.models.rss import Rss
|
||||
|
||||
|
||||
class RssOper(DbOper):
|
||||
"""
|
||||
RSS订阅数据管理
|
||||
"""
|
||||
|
||||
def __init__(self, db=SessionLocal()):
|
||||
super().__init__(db)
|
||||
|
||||
def add(self, **kwargs) -> bool:
|
||||
"""
|
||||
新增RSS订阅
|
||||
"""
|
||||
item = Rss(**kwargs)
|
||||
if not item.get_by_tmdbid(self._db, tmdbid=kwargs.get("tmdbid"),
|
||||
season=kwargs.get("season")):
|
||||
item.create(self._db)
|
||||
return True
|
||||
return False
|
||||
|
||||
def list(self) -> List[Rss]:
|
||||
"""
|
||||
查询所有RSS订阅
|
||||
"""
|
||||
return Rss.list(self._db)
|
||||
|
||||
def delete(self, rssid: int) -> bool:
|
||||
"""
|
||||
删除RSS订阅
|
||||
"""
|
||||
item = Rss.get(self._db, rssid)
|
||||
if item:
|
||||
item.delete(self._db)
|
||||
return True
|
||||
return False
|
@ -1,4 +1,5 @@
|
||||
import datetime
|
||||
from functools import reduce
|
||||
from pathlib import Path
|
||||
from typing import Optional, Any, List, Dict, Tuple
|
||||
from xml.dom.minidom import parseString
|
||||
@ -398,6 +399,8 @@ class BestFilmVersion(_PluginBase):
|
||||
"&Limit=20" \
|
||||
"&apikey={APIKEY}"
|
||||
resp = self.get_items(Jellyfin().get_data(url))
|
||||
if not resp:
|
||||
continue
|
||||
all_item.extend(resp)
|
||||
elif settings.MEDIASERVER == 'emby':
|
||||
# 获取所有user
|
||||
@ -417,12 +420,21 @@ class BestFilmVersion(_PluginBase):
|
||||
"&EnableTotalRecordCount=false" \
|
||||
"&Limit=20&api_key={APIKEY}"
|
||||
resp = self.get_items(Emby().get_data(url))
|
||||
if not resp:
|
||||
continue
|
||||
all_item.extend(resp)
|
||||
else:
|
||||
resp = self.plex_get_watchlist()
|
||||
if not resp:
|
||||
return
|
||||
all_item.extend(resp)
|
||||
|
||||
for data in all_item:
|
||||
def function(y, x):
|
||||
return y if (x['Name'] in [i['Name'] for i in y]) else (lambda z, u: (z.append(u), z))(y, x)[1]
|
||||
# all_item 根据电影名去重
|
||||
result = reduce(function, all_item, [])
|
||||
|
||||
for data in result:
|
||||
# 检查缓存
|
||||
if data.get('Name') in caches:
|
||||
continue
|
||||
@ -520,6 +532,8 @@ class BestFilmVersion(_PluginBase):
|
||||
elem = dom.documentElement
|
||||
# 获取 指定元素
|
||||
eles = elem.getElementsByTagName('Video')
|
||||
if not eles:
|
||||
return []
|
||||
for ele in eles:
|
||||
data = {}
|
||||
# 获取标签中内容
|
||||
@ -539,20 +553,29 @@ class BestFilmVersion(_PluginBase):
|
||||
logger.error(f"连接Plex/Watchlist 出错:" + str(e))
|
||||
return []
|
||||
|
||||
def plex_get_iteminfo(self, itemid):
|
||||
@staticmethod
|
||||
def plex_get_iteminfo(itemid):
|
||||
url = f"https://metadata.provider.plex.tv/library/metadata/{itemid}" \
|
||||
f"?X-Plex-Token={settings.PLEX_TOKEN}"
|
||||
ids = []
|
||||
try:
|
||||
resp = RequestUtils().get_res(url=url)
|
||||
resp = RequestUtils(accept_type="application/json, text/plain, */*").get_res(url=url)
|
||||
if resp:
|
||||
dom = parseString(resp.text)
|
||||
# 获取文档元素对象
|
||||
elem = dom.documentElement
|
||||
# 获取 指定元素
|
||||
eles = elem.getElementsByTagName('Video')
|
||||
for ele in eles:
|
||||
# 获取标签中内容
|
||||
return {"ExternalUrls": "TheMovieDb", "Url": f"{self.ele_get_tmdbid(ele)}"}
|
||||
metadata = resp.json().get('MediaContainer').get('Metadata')
|
||||
for item in metadata:
|
||||
_guid = item.get('Guid')
|
||||
if not _guid:
|
||||
continue
|
||||
|
||||
id_list = [h.get('id') for h in _guid if h.get('id').__contains__("tmdb")]
|
||||
if not id_list:
|
||||
continue
|
||||
|
||||
ids.append({'Name': 'TheMovieDb', 'Url': id_list[0]})
|
||||
|
||||
if not ids:
|
||||
return []
|
||||
return {'ExternalUrls': ids}
|
||||
else:
|
||||
logger.error(f"Plex/Items 未获取到返回数据")
|
||||
return []
|
||||
@ -560,19 +583,6 @@ class BestFilmVersion(_PluginBase):
|
||||
logger.error(f"连接Plex/Items 出错:" + str(e))
|
||||
return []
|
||||
|
||||
@staticmethod
|
||||
def ele_get_tmdbid(ele):
|
||||
data = []
|
||||
for h in ele.getElementsByTagName('Guid'):
|
||||
tmdbid = h.attributes['id'].nodeValue if h.attributes['id'].nodeValue.__contains__("tmdb") else ""
|
||||
if not tmdbid:
|
||||
continue
|
||||
obj = {"Name": "TheMovieDb", "Url": f"{tmdbid}"}
|
||||
data.append(obj)
|
||||
return data
|
||||
logger.warn(f"连接Plex/Guid 警告:" + "未获取到tmdbid数据")
|
||||
return data
|
||||
|
||||
@eventmanager.register(EventType.WebhookMessage)
|
||||
def webhook_message_action(self, event):
|
||||
|
||||
|
@ -16,6 +16,7 @@ from app.modules.qbittorrent import Qbittorrent
|
||||
from app.modules.transmission import Transmission
|
||||
from app.plugins import _PluginBase
|
||||
from app.plugins.iyuuautoseed.iyuu_helper import IyuuHelper
|
||||
from app.schemas import NotificationType
|
||||
from app.utils.http import RequestUtils
|
||||
from app.utils.string import StringUtils
|
||||
|
||||
@ -462,6 +463,7 @@ class IYUUAutoSeed(_PluginBase):
|
||||
if self._notify:
|
||||
if self.success or self.fail:
|
||||
self.post_message(
|
||||
mtype=NotificationType.SiteMessage,
|
||||
title="【IYUU自动辅种任务完成】",
|
||||
text=f"服务器返回可辅种总数:{self.total}\n"
|
||||
f"实际可辅种数:{self.realtotal}\n"
|
||||
|
@ -1,8 +1,22 @@
|
||||
from typing import List, Tuple, Dict, Any
|
||||
import re
|
||||
import threading
|
||||
import time
|
||||
from datetime import datetime, timedelta
|
||||
from typing import List, Tuple, Dict, Any, Optional
|
||||
|
||||
from app.core.event import eventmanager
|
||||
import pytz
|
||||
from apscheduler.schedulers.background import BackgroundScheduler
|
||||
from apscheduler.triggers.cron import CronTrigger
|
||||
|
||||
from app.core.config import settings
|
||||
from app.log import logger
|
||||
from app.modules.qbittorrent import Qbittorrent
|
||||
from app.modules.transmission import Transmission
|
||||
from app.plugins import _PluginBase
|
||||
from app.schemas.types import EventType
|
||||
from app.schemas import NotificationType
|
||||
from app.utils.string import StringUtils
|
||||
|
||||
lock = threading.Lock()
|
||||
|
||||
|
||||
class TorrentRemover(_PluginBase):
|
||||
@ -28,15 +42,97 @@ class TorrentRemover(_PluginBase):
|
||||
auth_level = 2
|
||||
|
||||
# 私有属性
|
||||
downloader = None
|
||||
qb = None
|
||||
tr = None
|
||||
_event = threading.Event()
|
||||
_scheduler = None
|
||||
_enabled = False
|
||||
_onlyonce = False
|
||||
_notify = False
|
||||
# pause/delete
|
||||
_downloaders = []
|
||||
_action = "pause"
|
||||
_cron = None
|
||||
_samedata = False
|
||||
_mponly = False
|
||||
_size = None
|
||||
_ratio = None
|
||||
_time = None
|
||||
_upspeed = None
|
||||
_labels = None
|
||||
_pathkeywords = None
|
||||
_trackerkeywords = None
|
||||
_errorkeywords = None
|
||||
_torrentstates = None
|
||||
_torrentcategorys = None
|
||||
|
||||
def init_plugin(self, config: dict = None):
|
||||
if config:
|
||||
self._enabled = config.get("enabled")
|
||||
self._onlyonce = config.get("onlyonce")
|
||||
self._notify = config.get("notify")
|
||||
self._downloaders = config.get("downloaders") or []
|
||||
self._action = config.get("action")
|
||||
self._cron = config.get("cron")
|
||||
self._samedata = config.get("samedata")
|
||||
self._mponly = config.get("mponly")
|
||||
self._size = config.get("size") or ""
|
||||
self._ratio = config.get("ratio")
|
||||
self._time = config.get("time")
|
||||
self._upspeed = config.get("upspeed")
|
||||
self._labels = config.get("labels") or ""
|
||||
self._pathkeywords = config.get("pathkeywords") or ""
|
||||
self._trackerkeywords = config.get("trackerkeywords") or ""
|
||||
self._errorkeywords = config.get("errorkeywords") or ""
|
||||
self._torrentstates = config.get("torrentstates") or ""
|
||||
self._torrentcategorys = config.get("torrentcategorys") or ""
|
||||
|
||||
self.stop_service()
|
||||
|
||||
if self.get_state() or self._onlyonce:
|
||||
self._scheduler = BackgroundScheduler(timezone=settings.TZ)
|
||||
self.qb = Qbittorrent()
|
||||
self.tr = Transmission()
|
||||
if self._cron:
|
||||
try:
|
||||
self._scheduler.add_job(self.delete_torrents,
|
||||
CronTrigger.from_crontab(self._cron))
|
||||
logger.info(f"自动删种服务启动,周期:{self._cron}")
|
||||
except Exception as err:
|
||||
logger.error(f"自动删种服务启动失败:{str(err)}")
|
||||
self.systemmessage.put(f"自动删种服务启动失败:{str(err)}")
|
||||
if self._onlyonce:
|
||||
logger.info(f"自动删种服务启动,立即运行一次")
|
||||
self._scheduler.add_job(self.delete_torrents, 'date',
|
||||
run_date=datetime.now(
|
||||
tz=pytz.timezone(settings.TZ)) + timedelta(seconds=3)
|
||||
)
|
||||
# 关闭一次性开关
|
||||
self._onlyonce = False
|
||||
# 保存设置
|
||||
self.update_config({
|
||||
"enable": self._enabled,
|
||||
"notify": self._notify,
|
||||
"onlyonce": self._onlyonce,
|
||||
"action": self._action,
|
||||
"cron": self._cron,
|
||||
"samedata": self._samedata,
|
||||
"mponly": self._mponly,
|
||||
"size": self._size,
|
||||
"ratio": self._ratio,
|
||||
"time": self._time,
|
||||
"upspeed": self._upspeed,
|
||||
"labels": self._labels,
|
||||
"pathkeywords": self._pathkeywords,
|
||||
"trackerkeywords": self._trackerkeywords,
|
||||
"errorkeywords": self._errorkeywords,
|
||||
"torrentstates": self._torrentstates,
|
||||
"torrentcategorys": self._torrentcategorys
|
||||
|
||||
})
|
||||
|
||||
def get_state(self) -> bool:
|
||||
return self._enabled
|
||||
return self._enabled and self._cron and self._downloaders
|
||||
|
||||
@staticmethod
|
||||
def get_command() -> List[Dict[str, Any]]:
|
||||
@ -46,23 +142,566 @@ class TorrentRemover(_PluginBase):
|
||||
pass
|
||||
|
||||
def get_form(self) -> Tuple[List[dict], Dict[str, Any]]:
|
||||
pass
|
||||
return [
|
||||
{
|
||||
'component': 'VForm',
|
||||
'content': [
|
||||
{
|
||||
'component': 'VRow',
|
||||
'content': [
|
||||
{
|
||||
'component': 'VCol',
|
||||
'props': {
|
||||
'cols': 12,
|
||||
'md': 6
|
||||
},
|
||||
'content': [
|
||||
{
|
||||
'component': 'VSwitch',
|
||||
'props': {
|
||||
'model': 'enabled',
|
||||
'label': '启用插件',
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
'component': 'VCol',
|
||||
'props': {
|
||||
'cols': 12,
|
||||
'md': 6
|
||||
},
|
||||
'content': [
|
||||
{
|
||||
'component': 'VSwitch',
|
||||
'props': {
|
||||
'model': 'notify',
|
||||
'label': '发送通知',
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
'component': 'VRow',
|
||||
'content': [
|
||||
{
|
||||
'component': 'VCol',
|
||||
'props': {
|
||||
'cols': 12,
|
||||
'md': 6
|
||||
},
|
||||
'content': [
|
||||
{
|
||||
'component': 'VTextField',
|
||||
'props': {
|
||||
'model': 'cron',
|
||||
'label': '执行周期',
|
||||
'placeholder': '0 0 0 ? *'
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
'component': 'VCol',
|
||||
'props': {
|
||||
'cols': 12,
|
||||
'md': 6
|
||||
},
|
||||
'content': [
|
||||
{
|
||||
'component': 'VSelect',
|
||||
'props': {
|
||||
'model': 'action',
|
||||
'label': '动作',
|
||||
'items': [
|
||||
{'title': '暂停', 'value': 'pause'},
|
||||
{'title': '删除种子', 'value': 'delete'},
|
||||
{'title': '删除种子和文件', 'value': 'deletefile'}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
'component': 'VRow',
|
||||
'content': [
|
||||
{
|
||||
'component': 'VCol',
|
||||
'props': {
|
||||
'cols': 12
|
||||
},
|
||||
'content': [
|
||||
{
|
||||
'component': 'VSelect',
|
||||
'props': {
|
||||
'chips': True,
|
||||
'multiple': True,
|
||||
'model': 'downloaders',
|
||||
'label': '下载器',
|
||||
'items': [
|
||||
{'title': 'Qbittorrent', 'value': 'qbittorrent'},
|
||||
{'title': 'Transmission', 'value': 'transmission'}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
'component': 'VRow',
|
||||
'content': [
|
||||
{
|
||||
'component': 'VCol',
|
||||
'props': {
|
||||
'cols': 6
|
||||
},
|
||||
'content': [
|
||||
{
|
||||
'component': 'VTextField',
|
||||
'props': {
|
||||
'model': 'size',
|
||||
'label': '种子大小(GB)',
|
||||
'placeholder': '例如1-10'
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
'component': 'VCol',
|
||||
'props': {
|
||||
'cols': 6
|
||||
},
|
||||
'content': [
|
||||
{
|
||||
'component': 'VTextField',
|
||||
'props': {
|
||||
'model': 'ratio',
|
||||
'label': '分享率',
|
||||
'placeholder': ''
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
'component': 'VCol',
|
||||
'props': {
|
||||
'cols': 6
|
||||
},
|
||||
'content': [
|
||||
{
|
||||
'component': 'VTextField',
|
||||
'props': {
|
||||
'model': 'time',
|
||||
'label': '做种时间(小时)',
|
||||
'placeholder': ''
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
'component': 'VCol',
|
||||
'props': {
|
||||
'cols': 6
|
||||
},
|
||||
'content': [
|
||||
{
|
||||
'component': 'VTextField',
|
||||
'props': {
|
||||
'model': 'upspeed',
|
||||
'label': '平均上传速度',
|
||||
'placeholder': ''
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
'component': 'VCol',
|
||||
'props': {
|
||||
'cols': 6
|
||||
},
|
||||
'content': [
|
||||
{
|
||||
'component': 'VTextField',
|
||||
'props': {
|
||||
'model': 'labels',
|
||||
'label': '标签',
|
||||
'placeholder': '用,分隔多个标签'
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
'component': 'VCol',
|
||||
'props': {
|
||||
'cols': 6
|
||||
},
|
||||
'content': [
|
||||
{
|
||||
'component': 'VTextField',
|
||||
'props': {
|
||||
'model': 'pathkeywords',
|
||||
'label': '保存路径关键词',
|
||||
'placeholder': '支持正式表达式'
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
'component': 'VCol',
|
||||
'props': {
|
||||
'cols': 6
|
||||
},
|
||||
'content': [
|
||||
{
|
||||
'component': 'VTextField',
|
||||
'props': {
|
||||
'model': 'trackerkeywords',
|
||||
'label': 'Tracker关键词',
|
||||
'placeholder': '支持正式表达式'
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
'component': 'VCol',
|
||||
'props': {
|
||||
'cols': 6
|
||||
},
|
||||
'content': [
|
||||
{
|
||||
'component': 'VTextField',
|
||||
'props': {
|
||||
'model': 'torrentstates',
|
||||
'label': '任务状态',
|
||||
'placeholder': '用,分隔多个状态'
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
'component': 'VCol',
|
||||
'props': {
|
||||
'cols': 6
|
||||
},
|
||||
'content': [
|
||||
{
|
||||
'component': 'VTextField',
|
||||
'props': {
|
||||
'model': 'torrentcategorys',
|
||||
'label': '任务分类',
|
||||
'placeholder': '用,分隔多个分类'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
'component': 'VRow',
|
||||
'content': [
|
||||
{
|
||||
'component': 'VCol',
|
||||
'props': {
|
||||
'cols': 12,
|
||||
'md': 4
|
||||
},
|
||||
'content': [
|
||||
{
|
||||
'component': 'VSwitch',
|
||||
'props': {
|
||||
'model': 'samedata',
|
||||
'label': '处理辅种',
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
'component': 'VCol',
|
||||
'props': {
|
||||
'cols': 12,
|
||||
'md': 4
|
||||
},
|
||||
'content': [
|
||||
{
|
||||
'component': 'VSwitch',
|
||||
'props': {
|
||||
'model': 'mponly',
|
||||
'label': '仅MoviePilot任务',
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
'component': 'VCol',
|
||||
'props': {
|
||||
'cols': 12,
|
||||
'md': 4
|
||||
},
|
||||
'content': [
|
||||
{
|
||||
'component': 'VSwitch',
|
||||
'props': {
|
||||
'model': 'onlyonce',
|
||||
'label': '立即运行一次',
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
], {
|
||||
"enable": False,
|
||||
"notify": False,
|
||||
"onlyonce": False,
|
||||
"action": 'pause',
|
||||
"cron": '0 0 0 ? *',
|
||||
"samedata": False,
|
||||
"mponly": False,
|
||||
"size": "",
|
||||
"ratio": "",
|
||||
"time": "",
|
||||
"upspeed": "",
|
||||
"labels": "",
|
||||
"pathkeywords": "",
|
||||
"trackerkeywords": "",
|
||||
"errorkeywords": "",
|
||||
"torrentstates": "",
|
||||
"torrentcategorys": ""
|
||||
}
|
||||
|
||||
def get_page(self) -> List[dict]:
|
||||
pass
|
||||
|
||||
def stop_service(self):
|
||||
pass
|
||||
|
||||
@eventmanager.register(EventType.HistoryDeleted)
|
||||
def deletetorrent(self, event):
|
||||
"""
|
||||
联动删除下载器中的下载任务
|
||||
退出插件
|
||||
"""
|
||||
if not self._enabled:
|
||||
return
|
||||
event_info = event.event_data
|
||||
if not event_info:
|
||||
return
|
||||
try:
|
||||
if self._scheduler:
|
||||
self._scheduler.remove_all_jobs()
|
||||
if self._scheduler.running:
|
||||
self._event.set()
|
||||
self._scheduler.shutdown()
|
||||
self._event.clear()
|
||||
self._scheduler = None
|
||||
except Exception as e:
|
||||
print(str(e))
|
||||
|
||||
# TODO 删除所有下载任务
|
||||
def __get_downloader(self, dtype: str):
|
||||
"""
|
||||
根据类型返回下载器实例
|
||||
"""
|
||||
if dtype == "qbittorrent":
|
||||
return self.qb
|
||||
elif dtype == "transmission":
|
||||
return self.tr
|
||||
else:
|
||||
return None
|
||||
|
||||
def delete_torrents(self):
|
||||
"""
|
||||
定时删除下载器中的下载任务
|
||||
"""
|
||||
for downloader in self._downloaders:
|
||||
try:
|
||||
with lock:
|
||||
# 获取需删除种子列表
|
||||
torrents = self.get_remove_torrents(downloader)
|
||||
logger.info(f"自动删种任务 获取符合处理条件种子数 {len(torrents)}")
|
||||
# 下载器
|
||||
downlader_obj = self.__get_downloader(downloader)
|
||||
if self._action == "pause":
|
||||
message_text = f"{downloader.title()} 共暂停{len(torrents)}个种子"
|
||||
for torrent in torrents:
|
||||
if self._event.is_set():
|
||||
logger.info(f"自动删种服务停止")
|
||||
return
|
||||
text_item = f"{torrent.get('name')} " \
|
||||
f"来自站点:{torrent.get('site')} " \
|
||||
f"大小:{StringUtils.str_filesize(torrent.get('size'))} GB"
|
||||
# 暂停种子
|
||||
downlader_obj.stop_torrents(ids=[torrent.get("id")])
|
||||
logger.info(f"自动删种任务 暂停种子:{text_item}")
|
||||
message_text = f"{message_text}\n{text_item}"
|
||||
elif self._action == "delete":
|
||||
message_text = f"{downloader.title()} 共删除{len(torrents)}个种子"
|
||||
for torrent in torrents:
|
||||
if self._event.is_set():
|
||||
logger.info(f"自动删种服务停止")
|
||||
return
|
||||
text_item = f"{torrent.get('name')} " \
|
||||
f"来自站点:{torrent.get('site')} " \
|
||||
f"大小:{StringUtils.str_filesize(torrent.get('size'))} GB"
|
||||
# 删除种子
|
||||
downlader_obj.delete_torrents(delete_file=False,
|
||||
ids=[torrent.get("id")])
|
||||
logger.info(f"自动删种任务 删除种子:{text_item}")
|
||||
message_text = f"{message_text}\n{text_item}"
|
||||
elif self._action == "deletefile":
|
||||
message_text = f"{downloader.title()} 共删除{len(torrents)}个种子及文件"
|
||||
for torrent in torrents:
|
||||
if self._event.is_set():
|
||||
logger.info(f"自动删种服务停止")
|
||||
return
|
||||
text_item = f"{torrent.get('name')} " \
|
||||
f"来自站点:{torrent.get('site')} " \
|
||||
f"大小:{StringUtils.str_filesize(torrent.get('size'))} GB"
|
||||
# 删除种子
|
||||
downlader_obj.delete_torrents(delete_file=True,
|
||||
ids=[torrent.get("id")])
|
||||
logger.info(f"自动删种任务 删除种子及文件:{text_item}")
|
||||
message_text = f"{message_text}\n{text_item}"
|
||||
else:
|
||||
continue
|
||||
if torrents and message_text and self._notify:
|
||||
self.post_message(
|
||||
mtype=NotificationType.SiteMessage,
|
||||
title=f"【自动删种任务执行完成】",
|
||||
text=message_text
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"自动删种任务异常:{str(e)}")
|
||||
|
||||
def __get_qb_torrent(self, torrent: Any) -> Optional[dict]:
|
||||
"""
|
||||
检查QB下载任务是否符合条件
|
||||
"""
|
||||
# 完成时间
|
||||
date_done = torrent.completion_on if torrent.completion_on > 0 else torrent.added_on
|
||||
# 现在时间
|
||||
date_now = int(time.mktime(datetime.now().timetuple()))
|
||||
# 做种时间
|
||||
torrent_seeding_time = date_now - date_done if date_done else 0
|
||||
# 平均上传速度
|
||||
torrent_upload_avs = torrent.uploaded / torrent_seeding_time if torrent_seeding_time else 0
|
||||
# 大小 单位:GB
|
||||
sizes = self._size.split(',') if self._size else []
|
||||
minsize = sizes[0] * 1024 * 1024 * 1024 if sizes else 0
|
||||
maxsize = sizes[-1] * 1024 * 1024 * 1024 if sizes else 0
|
||||
# 分享率
|
||||
if self._ratio and torrent.ratio <= float(self._ratio):
|
||||
return None
|
||||
# 做种时间 单位:小时
|
||||
if self._time and torrent_seeding_time <= float(self._time) * 3600:
|
||||
return None
|
||||
# 文件大小
|
||||
if self._size and (torrent.size >= maxsize or torrent.size <= minsize):
|
||||
return None
|
||||
if self._upspeed and torrent_upload_avs >= float(self._upspeed) * 1024:
|
||||
return None
|
||||
if self._pathkeywords and not re.findall(self._pathkeywords, torrent.save_path, re.I):
|
||||
return None
|
||||
if self._trackerkeywords and not re.findall(self._trackerkeywords, torrent.tracker, re.I):
|
||||
return None
|
||||
if self._torrentstates and torrent.state not in self._torrentstates:
|
||||
return None
|
||||
if self._torrentcategorys and torrent.category not in self._torrentcategorys:
|
||||
return None
|
||||
return {
|
||||
"id": torrent.hash,
|
||||
"name": torrent.name,
|
||||
"site": StringUtils.get_url_sld(torrent.tracker),
|
||||
"size": torrent.size
|
||||
}
|
||||
|
||||
def __get_tr_torrent(self, torrent: Any) -> Optional[dict]:
|
||||
"""
|
||||
检查TR下载任务是否符合条件
|
||||
"""
|
||||
# 完成时间
|
||||
date_done = torrent.date_done or torrent.date_added
|
||||
# 现在时间
|
||||
date_now = int(time.mktime(datetime.now().timetuple()))
|
||||
# 做种时间
|
||||
torrent_seeding_time = date_now - int(time.mktime(date_done.timetuple())) if date_done else 0
|
||||
# 上传量
|
||||
torrent_uploaded = torrent.ratio * torrent.total_size
|
||||
# 平均上传速茺
|
||||
torrent_upload_avs = torrent_uploaded / torrent_seeding_time if torrent_seeding_time else 0
|
||||
# 大小 单位:GB
|
||||
sizes = self._size.split(',') if self._size else []
|
||||
minsize = sizes[0] * 1024 * 1024 * 1024 if sizes else 0
|
||||
maxsize = sizes[-1] * 1024 * 1024 * 1024 if sizes else 0
|
||||
# 分享率
|
||||
if self._ratio and torrent.ratio <= float(self._ratio):
|
||||
return None
|
||||
if self._time and torrent_seeding_time <= float(self._time) * 3600:
|
||||
return None
|
||||
if self._size and (torrent.total_size >= maxsize or torrent.total_size <= minsize):
|
||||
return None
|
||||
if self._upspeed and torrent_upload_avs >= float(self._upspeed) * 1024:
|
||||
return None
|
||||
if self._pathkeywords and not re.findall(self._pathkeywords, torrent.download_dir, re.I):
|
||||
return None
|
||||
if self._trackerkeywords:
|
||||
if not torrent.trackers:
|
||||
return None
|
||||
else:
|
||||
tacker_key_flag = False
|
||||
for tracker in torrent.trackers:
|
||||
if re.findall(self._trackerkeywords, tracker.get("announce", ""), re.I):
|
||||
tacker_key_flag = True
|
||||
break
|
||||
if not tacker_key_flag:
|
||||
return None
|
||||
if self._errorkeywords and not re.findall(self._errorkeywords, torrent.error_string, re.I):
|
||||
return None
|
||||
return {
|
||||
"id": torrent.hashString,
|
||||
"name": torrent.name,
|
||||
"site": torrent.trackers[0].get("sitename") if torrent.trackers else "",
|
||||
"size": torrent.total_size
|
||||
}
|
||||
|
||||
def get_remove_torrents(self, downloader: str):
|
||||
"""
|
||||
获取自动删种任务种子
|
||||
"""
|
||||
remove_torrents = []
|
||||
# 下载器对象
|
||||
downloader_obj = self.__get_downloader(downloader)
|
||||
# 标题
|
||||
if self._labels:
|
||||
tags = self._labels.split(',')
|
||||
else:
|
||||
tags = []
|
||||
if self._mponly:
|
||||
tags.extend(settings.TORRENT_TAG)
|
||||
# 查询种子
|
||||
torrents, error_flag = downloader_obj.get_torrents(tags=tags or None)
|
||||
if error_flag:
|
||||
return []
|
||||
# 处理种子
|
||||
for torrent in torrents:
|
||||
if downloader == "qbittorrent":
|
||||
item = self.__get_qb_torrent(torrent)
|
||||
else:
|
||||
item = self.__get_tr_torrent(torrent)
|
||||
if not item:
|
||||
continue
|
||||
remove_torrents.append(item)
|
||||
# 处理辅种
|
||||
if self._samedata and remove_torrents:
|
||||
remove_torrents_plus = []
|
||||
for remove_torrent in remove_torrents:
|
||||
name = remove_torrent.get("name")
|
||||
size = remove_torrent.get("size")
|
||||
for torrent in torrents:
|
||||
if torrent.name == name \
|
||||
and torrent.size == size \
|
||||
and torrent.hash not in [t.get("id") for t in remove_torrents]:
|
||||
remove_torrents_plus.append({
|
||||
"id": torrent.hash,
|
||||
"name": torrent.name,
|
||||
"site": StringUtils.get_url_sld(torrent.tracker),
|
||||
"size": torrent.size
|
||||
})
|
||||
remove_torrents.extend(remove_torrents_plus)
|
||||
return remove_torrents
|
||||
|
@ -16,6 +16,7 @@ from app.log import logger
|
||||
from app.modules.qbittorrent import Qbittorrent
|
||||
from app.modules.transmission import Transmission
|
||||
from app.plugins import _PluginBase
|
||||
from app.schemas import NotificationType
|
||||
from app.utils.string import StringUtils
|
||||
|
||||
|
||||
@ -324,8 +325,7 @@ class TorrentTransfer(_PluginBase):
|
||||
{
|
||||
'component': 'VCol',
|
||||
'props': {
|
||||
'cols': 12,
|
||||
'md': 12
|
||||
'cols': 12
|
||||
},
|
||||
'content': [
|
||||
{
|
||||
@ -644,6 +644,7 @@ class TorrentTransfer(_PluginBase):
|
||||
# 发送通知
|
||||
if self._notify:
|
||||
self.post_message(
|
||||
mtype=NotificationType.SiteMessage,
|
||||
title="【移转做种任务执行完成】",
|
||||
text=f"总数:{total},成功:{success},失败:{fail}"
|
||||
)
|
||||
|
@ -12,3 +12,4 @@ from .mediaserver import *
|
||||
from .message import *
|
||||
from .tmdb import *
|
||||
from .transfer import *
|
||||
from .rss import *
|
||||
|
48
app/schemas/rss.py
Normal file
48
app/schemas/rss.py
Normal file
@ -0,0 +1,48 @@
|
||||
from typing import Optional
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class Rss(BaseModel):
|
||||
id: Optional[int]
|
||||
# 名称
|
||||
name: Optional[str]
|
||||
# RSS地址
|
||||
url: Optional[str]
|
||||
# 类型
|
||||
type: Optional[str]
|
||||
# 标题
|
||||
title: Optional[str]
|
||||
# 年份
|
||||
year: Optional[str]
|
||||
# TMDBID
|
||||
tmdbid: Optional[int]
|
||||
# 季号
|
||||
season: Optional[int]
|
||||
# 海报
|
||||
poster: Optional[str]
|
||||
# 背景图
|
||||
backdrop: Optional[str]
|
||||
# 评分
|
||||
vote: Optional[float]
|
||||
# 简介
|
||||
description: Optional[str]
|
||||
# 包含
|
||||
include: Optional[str]
|
||||
# 排除
|
||||
exclude: Optional[str]
|
||||
# 洗版
|
||||
best_version: Optional[int]
|
||||
# 是否使用代理服务器
|
||||
proxy: Optional[int]
|
||||
# 保存路径
|
||||
save_path: Optional[str]
|
||||
# 附加信息
|
||||
note: Optional[str]
|
||||
# 最后更新时间
|
||||
last_update: Optional[str]
|
||||
# 状态 0-停用,1-启用
|
||||
state: Optional[int]
|
||||
|
||||
class Config:
|
||||
orm_mode = True
|
Loading…
x
Reference in New Issue
Block a user