Merge branch 'jxxghp:main' into main

This commit is contained in:
thsrite 2023-08-09 08:29:59 +08:00 committed by GitHub
commit 8ebc2875ba
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 1071 additions and 47 deletions

68
app/api/endpoints/rss.py Normal file
View 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
View 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订阅数据完成")

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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