more plugins

This commit is contained in:
jxxghp 2023-08-02 21:26:25 +08:00
parent 54ffacf743
commit 0c2dbd5c2d
7 changed files with 635 additions and 1 deletions

View File

@ -0,0 +1,154 @@
from functools import lru_cache
from pathlib import Path
from typing import List, Tuple, Dict, Any
from app.core.config import settings
from app.core.event import eventmanager
from app.log import logger
from app.plugins import _PluginBase
from app.schemas.types import EventType, MediaType
from app.utils.http import RequestUtils
class ChineseSubFinder(_PluginBase):
# 插件名称
plugin_name = "ChineseSubFinder"
# 插件描述
plugin_desc = "通知ChineseSubFinder下载字幕。"
# 插件图标
plugin_icon = "chinesesubfinder.png"
# 主题色
plugin_color = "#83BE39"
# 插件版本
plugin_version = "1.0"
# 插件作者
plugin_author = "jxxghp"
# 作者主页
author_url = "https://github.com/jxxghp"
# 插件配置项ID前缀
plugin_config_prefix = "chinesesubfinder_"
# 加载顺序
plugin_order = 3
# 可使用的用户级别
auth_level = 1
# 私有属性
_save_tmp_path = None
_enable = False
_host = None
_api_key = None
_remote_path = None
_local_path = None
_remote_path2 = None
_local_path2 = None
_remote_path3 = None
_local_path3 = None
def init_plugin(self, config: dict = None):
self._save_tmp_path = settings.TEMP_PATH
if config:
self._enable = config.get("enable")
self._api_key = config.get("api_key")
self._host = config.get('host')
if self._host:
if not self._host.startswith('http'):
self._host = "http://" + self._host
if not self._host.endswith('/'):
self._host = self._host + "/"
self._local_path = config.get("local_path")
self._remote_path = config.get("remote_path")
self._local_path2 = config.get("local_path2")
self._remote_path2 = config.get("remote_path2")
self._local_path3 = config.get("local_path3")
self._remote_path3 = config.get("remote_path3")
@staticmethod
def get_command() -> List[Dict[str, Any]]:
pass
def get_api(self) -> List[Dict[str, Any]]:
pass
def get_form(self) -> Tuple[List[dict], Dict[str, Any]]:
pass
def get_page(self) -> List[dict]:
pass
def stop_service(self):
pass
@eventmanager.register(EventType.TransferComplete)
def download(self, event):
"""
调用ChineseSubFinder下载字幕
"""
if not self._host or not self._api_key:
return
item = event.event_data
if not item:
return
# FIXME
req_url = "%sapi/v1/add-job" % self._host
item_media = item.get("media_info")
item_type = item_media.get("type")
item_bluray = item.get("bluray")
item_file = item.get("file")
item_file_ext = item.get("file_ext")
if item_bluray:
file_path = "%s.mp4" % item_file
else:
if Path(item_file).suffix != item_file_ext:
file_path = "%s%s" % (item_file, item_file_ext)
else:
file_path = item_file
# 路径替换
if self._local_path and self._remote_path and file_path.startswith(self._local_path):
file_path = file_path.replace(self._local_path, self._remote_path).replace('\\', '/')
if self._local_path2 and self._remote_path2 and file_path.startswith(self._local_path2):
file_path = file_path.replace(self._local_path2, self._remote_path2).replace('\\', '/')
if self._local_path3 and self._remote_path3 and file_path.startswith(self._local_path3):
file_path = file_path.replace(self._local_path3, self._remote_path3).replace('\\', '/')
# 调用CSF下载字幕
self.__request_csf(req_url=req_url,
file_path=file_path,
item_type=0 if item_type == MediaType.MOVIE.value else 1,
item_bluray=item_bluray)
@lru_cache(maxsize=128)
def __request_csf(self, req_url, file_path, item_type, item_bluray):
# 一个名称只建一个任务
logger.info("通知ChineseSubFinder下载字幕: %s" % file_path)
params = {
"video_type": item_type,
"physical_video_file_full_path": file_path,
"task_priority_level": 3,
"media_server_inside_video_id": "",
"is_bluray": item_bluray
}
try:
res = RequestUtils(headers={
"Authorization": "Bearer %s" % self._api_key
}).post(req_url, json=params)
if not res or res.status_code != 200:
logger.error("调用ChineseSubFinder API失败")
else:
# 如果文件目录没有识别的nfo元数据 此接口会返回控制符推测是ChineseSubFinder的原因
# emby refresh元数据时异步的
if res.text:
job_id = res.json().get("job_id")
message = res.json().get("message")
if not job_id:
logger.warn("ChineseSubFinder下载字幕出错%s" % message)
else:
logger.info("ChineseSubFinder任务添加成功%s" % job_id)
else:
logger.error("%s 目录缺失nfo元数据" % file_path)
except Exception as e:
logger.error("连接ChineseSubFinder出错" + str(e))

View File

@ -0,0 +1,143 @@
from typing import List, Tuple, Dict, Any
from app.core.event import eventmanager
from app.log import logger
from app.plugins import _PluginBase
from app.schemas.types import EventType
from app.utils.ip import IpUtils
from app.utils.system import SystemUtils
from python_hosts import Hosts, HostsEntry
class CustomHosts(_PluginBase):
# 插件名称
plugin_name = "自定义Hosts"
# 插件描述
plugin_desc = "修改系统hosts文件加速网络访问。"
# 插件图标
plugin_icon = "hosts.png"
# 主题色
plugin_color = "#02C4E0"
# 插件版本
plugin_version = "1.0"
# 插件作者
plugin_author = "thsrite"
# 作者主页
author_url = "https://github.com/thsrite"
# 插件配置项ID前缀
plugin_config_prefix = "customhosts_"
# 加载顺序
plugin_order = 11
# 可使用的用户级别
auth_level = 1
# 私有属性
_hosts = []
_enable = False
def init_plugin(self, config: dict = None):
# 读取配置
if config:
self._enable = config.get("enable")
self._hosts = config.get("hosts")
if isinstance(self._hosts, str):
self._hosts = str(self._hosts).split('\n')
if self._enable and self._hosts:
# 排除空的host
new_hosts = []
for host in self._hosts:
if host and host != '\n':
new_hosts.append(host.replace("\n", "") + "\n")
self._hosts = new_hosts
# 添加到系统
error_flag, error_hosts = self.__add_hosts_to_system(self._hosts)
self._enable = self._enable and not error_flag
# 更新错误Hosts
self.update_config({
"hosts": self._hosts,
"err_hosts": error_hosts,
"enable": self._enable
})
@staticmethod
def get_command() -> List[Dict[str, Any]]:
pass
def get_api(self) -> List[Dict[str, Any]]:
pass
def get_form(self) -> Tuple[List[dict], Dict[str, Any]]:
pass
def get_page(self) -> List[dict]:
pass
@staticmethod
def __read_system_hosts():
"""
读取系统hosts对象
"""
# 获取本机hosts路径
if SystemUtils.is_windows():
hosts_path = r"c:\windows\system32\drivers\etc\hosts"
else:
hosts_path = '/etc/hosts'
# 读取系统hosts
return Hosts(path=hosts_path)
def __add_hosts_to_system(self, hosts):
"""
添加hosts到系统
"""
# 系统hosts对象
system_hosts = self.__read_system_hosts()
# 过滤掉插件添加的hosts
orgin_entries = []
for entry in system_hosts.entries:
if entry.entry_type == "comment" and entry.comment == "# CustomHostsPlugin":
break
orgin_entries.append(entry)
system_hosts.entries = orgin_entries
# 新的有效hosts
new_entrys = []
# 新的错误的hosts
err_hosts = []
err_flag = False
for host in hosts:
if not host:
continue
host_arr = str(host).split()
try:
host_entry = HostsEntry(entry_type='ipv4' if IpUtils.is_ipv4(str(host_arr[0])) else 'ipv6',
address=host_arr[0],
names=host_arr[1:])
new_entrys.append(host_entry)
except Exception as err:
err_hosts.append(host + "\n")
logger.error(f"{host} 格式转换错误:{str(err)}")
# 写入系统hosts
if new_entrys:
try:
# 添加分隔标识
system_hosts.add([HostsEntry(entry_type='comment', comment="# CustomHostsPlugin")])
# 添加新的Hosts
system_hosts.add(new_entrys)
system_hosts.write()
logger.info("更新系统hosts文件成功")
except Exception as err:
err_flag = True
logger.error(f"更新系统hosts文件失败{str(err) or '请检查权限'}")
return err_flag, err_hosts
def get_state(self):
return self._enable and self._hosts and self._hosts[0]
def stop_service(self):
"""
退出插件
"""
pass

View File

@ -0,0 +1,59 @@
from typing import List, Tuple, Dict, Any
from app.plugins import _PluginBase
class DirMonitor(_PluginBase):
# 插件名称
plugin_name = "目录监控"
# 插件描述
plugin_desc = "监控目录,文件发生变化时实时整理到媒体库。"
# 插件图标
plugin_icon = "synctimer.png"
# 主题色
plugin_color = "#53BA48"
# 插件版本
plugin_version = "1.0"
# 插件作者
plugin_author = "jxxghp"
# 作者主页
author_url = "https://github.com/jxxghp"
# 插件配置项ID前缀
plugin_config_prefix = "dirmonitor_"
# 加载顺序
plugin_order = 5
# 可使用的用户级别
user_level = 1
# 私有属性
_monitor = None
_enable = False
def init_plugin(self, config: dict = None):
# 读取配置
if config:
self._enable = config.get("enable")
# 停止现有任务
self.stop_service()
# TODO 启动任务
@staticmethod
def get_command() -> List[Dict[str, Any]]:
pass
def get_api(self) -> List[Dict[str, Any]]:
pass
def get_form(self) -> Tuple[List[dict], Dict[str, Any]]:
pass
def get_page(self) -> List[dict]:
pass
def stop_service(self):
"""
退出插件
"""
pass

View File

@ -0,0 +1,210 @@
import re
import xml.dom.minidom
from threading import Event
from typing import Tuple, List, Dict, Any
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.plugins import _PluginBase
from app.utils.dom import DomUtils
from app.utils.http import RequestUtils
class DoubanRank(_PluginBase):
# 插件名称
plugin_name = "豆瓣榜单订阅"
# 插件描述
plugin_desc = "监控豆瓣热门榜单,自动添加订阅。"
# 插件图标
plugin_icon = "movie.jpg"
# 主题色
plugin_color = "#01B3E3"
# 插件版本
plugin_version = "1.0"
# 插件作者
plugin_author = "jxxghp"
# 作者主页
author_url = "https://github.com/jxxghp"
# 插件配置项ID前缀
plugin_config_prefix = "doubanrank_"
# 加载顺序
plugin_order = 16
# 可使用的用户级别
auth_level = 2
# 退出事件
_event = Event()
# 私有属性
mediaserver = None
subscribe = None
rsshelper = None
media = None
_douban_address = {
'movie-ustop': 'https://rsshub.app/douban/movie/ustop',
'movie-weekly': 'https://rsshub.app/douban/movie/weekly',
'movie-real-time': 'https://rsshub.app/douban/movie/weekly/subject_real_time_hotest',
'show-domestic': 'https://rsshub.app/douban/movie/weekly/show_domestic',
'movie-hot-gaia': 'https://rsshub.app/douban/movie/weekly/movie_hot_gaia',
'tv-hot': 'https://rsshub.app/douban/movie/weekly/tv_hot',
'movie-top250': 'https://rsshub.app/douban/movie/weekly/movie_top250',
}
_enable = False
_cron = ""
_rss_addrs = []
_ranks = []
_vote = 0
_scheduler = None
def init_plugin(self, config: dict = None):
if config:
self._enable = config.get("enable")
self._cron = config.get("cron")
self._vote = float(config.get("vote")) if config.get("vote") else 0
rss_addrs = config.get("rss_addrs")
if rss_addrs:
if isinstance(rss_addrs, str):
self._rss_addrs = rss_addrs.split('\n')
else:
self._rss_addrs = rss_addrs
else:
self._rss_addrs = []
self._ranks = config.get("ranks") or []
# 停止现有任务
self.stop_service()
# 启动服务
if self._enable:
self._scheduler = BackgroundScheduler(timezone=settings.TZ)
if self._cron:
logger.info(f"豆瓣榜单订阅服务启动,周期:{self._cron}")
self._scheduler.add_job(self.__refresh_rss,
CronTrigger.from_crontab(self._cron))
if self._scheduler.get_jobs():
# 启动服务
self._scheduler.print_jobs()
self._scheduler.start()
@staticmethod
def get_command() -> List[Dict[str, Any]]:
pass
def get_api(self) -> List[Dict[str, Any]]:
pass
def get_form(self) -> Tuple[List[dict], Dict[str, Any]]:
pass
def get_page(self) -> List[dict]:
"""
拼装插件详情页面需要返回页面配置同时附带数据
"""
pass
def stop_service(self):
"""
停止服务
"""
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))
def __refresh_rss(self):
"""
刷新RSS
"""
logger.info(f"开始刷新RSS ...")
addr_list = self._rss_addrs + [self._douban_address.get(rank) for rank in self._ranks]
if not addr_list:
logger.info(f"未设置RSS地址")
return
else:
logger.info(f"{len(addr_list)} 个RSS地址需要刷新")
for addr in addr_list:
if not addr:
continue
try:
logger.info(f"获取RSS{addr} ...")
rss_infos = self.__get_rss_info(addr)
if not rss_infos:
logger.error(f"RSS地址{addr} ,未查询到数据")
continue
else:
logger.info(f"RSS地址{addr} ,共 {len(rss_infos)} 条数据")
for rss_info in rss_infos:
if self._event.is_set():
logger.info(f"订阅服务停止")
return
title = rss_info.get('title')
douban_id = rss_info.get('doubanid')
mtype = rss_info.get('type')
unique_flag = f"doubanrank: {title} (DB:{douban_id})"
# TODO 检查是否已处理过
# TODO 识别媒体信息
# TODO 检查媒体服务器是否存在
# TODO 检查是否已订阅过
# TODO 添加处理历史
# TODO 添加订阅
# TODO 发送通知
# TODO 更新历史记录
except Exception as e:
logger.error(str(e))
logger.info(f"所有榜单RSS刷新完成")
@staticmethod
def __get_rss_info(addr):
"""
获取RSS
"""
try:
ret = RequestUtils().get_res(addr)
if not ret:
return []
ret.encoding = ret.apparent_encoding
ret_xml = ret.text
ret_array = []
# 解析XML
dom_tree = xml.dom.minidom.parseString(ret_xml)
rootNode = dom_tree.documentElement
items = rootNode.getElementsByTagName("item")
for item in items:
try:
# 标题
title = DomUtils.tag_value(item, "title", default="")
# 链接
link = DomUtils.tag_value(item, "link", default="")
if not title and not link:
logger.warn(f"条目标题和链接均为空,无法处理")
continue
doubanid = re.findall(r"/(\d+)/", link)
if doubanid:
doubanid = doubanid[0]
if doubanid and not str(doubanid).isdigit():
logger.warn(f"解析的豆瓣ID格式不正确{doubanid}")
continue
# 返回对象
ret_array.append({
'title': title,
'link': link,
'doubanid': doubanid
})
except Exception as e1:
logger.error("解析RSS条目失败" + str(e1))
continue
return ret_array
except Exception as e:
logger.error("获取RSS失败" + str(e))
return []

View File

@ -0,0 +1,65 @@
from typing import List, Tuple, Dict, Any
from app.core.event import eventmanager
from app.plugins import _PluginBase
from app.schemas.types import EventType
class TorrentRemover(_PluginBase):
# 插件名称
plugin_name = "下载任务联动删除"
# 插件描述
plugin_desc = "历史记录被删除时,同步删除下载器中的下载任务。"
# 插件图标
plugin_icon = "torrentremover.png"
# 主题色
plugin_color = "#F44336"
# 插件版本
plugin_version = "1.0"
# 插件作者
plugin_author = "jxxghp"
# 作者主页
author_url = "https://github.com/jxxghp"
# 插件配置项ID前缀
plugin_config_prefix = "torrentremover_"
# 加载顺序
plugin_order = 9
# 可使用的用户级别
auth_level = 2
# 私有属性
downloader = None
_enable = False
def init_plugin(self, config: dict = None):
if config:
self._enable = config.get("enable")
@staticmethod
def get_command() -> List[Dict[str, Any]]:
pass
def get_api(self) -> List[Dict[str, Any]]:
pass
def get_form(self) -> Tuple[List[dict], Dict[str, Any]]:
pass
def get_page(self) -> List[dict]:
pass
def stop_service(self):
pass
@eventmanager.register(EventType.HistoryDeleted)
def deletetorrent(self, event):
"""
联动删除下载器中的下载任务
"""
if not self._enable:
return
event_info = event.event_data
if not event_info:
return
# TODO 删除所有下载任务

View File

@ -30,6 +30,8 @@ class EventType(Enum):
TransferComplete = "transfer.complete"
# 添加下载
DownloadAdded = "download.added"
# 删除历史记录
HistoryDeleted = "history.deleted"
# 系统配置Key字典

View File

@ -42,4 +42,5 @@ starlette~=0.27.0
PyVirtualDisplay~=3.0
Cython~=0.29.35
tvdb_api~=3.1
psutil==5.9.4
psutil~=5.9.4
python_hosts~=1.0.3