import base64
import datetime
import importlib
import json
import os.path
import re
import shutil
import signal
from math import floor
from urllib.parse import unquote
import cn2an
from flask_login import logout_user, current_user
from werkzeug.security import generate_password_hash
import log
from app.brushtask import BrushTask
from app.conf import SystemConfig, ModuleConf
from app.doubansync import DoubanSync
from app.downloader import Downloader
from app.downloader.client import Qbittorrent, Transmission
from app.filetransfer import FileTransfer
from app.filter import Filter
from app.helper import DbHelper, ProgressHelper, ThreadHelper, \
MetaHelper, DisplayHelper, WordsHelper, CookieCloudHelper
from app.indexer import Indexer
from app.media import Category, Media, Bangumi, DouBan
from app.media.meta import MetaInfo, MetaBase
from app.mediaserver import MediaServer
from app.message import Message, MessageCenter
from app.plugins import PluginManager, EventManager
from app.rss import Rss
from app.rsschecker import RssChecker
from app.scheduler import stop_scheduler
from app.sites import Sites, SiteUserInfo, SiteSignin, SiteCookie
from app.subscribe import Subscribe
from app.sync import Sync, stop_monitor
from app.torrentremover import TorrentRemover
from app.utils import StringUtils, EpisodeFormat, RequestUtils, PathUtils, \
SystemUtils, ExceptionUtils, Torrent
from app.utils.types import RmtMode, OsType, SearchType, DownloaderType, SyncType, MediaType, MovieTypes, TvTypes, \
EventType
from config import RMT_MEDIAEXT, TMDB_IMAGE_W500_URL, RMT_SUBEXT, Config
from web.backend.search_torrents import search_medias_for_web, search_media_by_message
from web.backend.web_utils import WebUtils
class WebAction:
dbhelper = None
_actions = {}
def __init__(self):
self.dbhelper = DbHelper()
self._actions = {
"sch": self.__sch,
"search": self.__search,
"download": self.__download,
"download_link": self.__download_link,
"download_torrent": self.__download_torrent,
"pt_start": self.__pt_start,
"pt_stop": self.__pt_stop,
"pt_remove": self.__pt_remove,
"pt_info": self.__pt_info,
"del_unknown_path": self.__del_unknown_path,
"rename": self.__rename,
"rename_udf": self.__rename_udf,
"delete_history": self.__delete_history,
"logging": self.__logging,
"version": self.__version,
"update_site": self.__update_site,
"get_site": self.__get_site,
"del_site": self.__del_site,
"get_site_favicon": self.__get_site_favicon,
"restart": self.__restart,
"update_system": self.update_system,
"reset_db_version": self.__reset_db_version,
"logout": self.__logout,
"update_config": self.__update_config,
"update_directory": self.__update_directory,
"add_or_edit_sync_path": self.__add_or_edit_sync_path,
"get_sync_path": self.__get_sync_path,
"delete_sync_path": self.__delete_sync_path,
"check_sync_path": self.__check_sync_path,
"remove_rss_media": self.__remove_rss_media,
"add_rss_media": self.__add_rss_media,
"re_identification": self.re_identification,
"media_info": self.__media_info,
"test_connection": self.__test_connection,
"user_manager": self.__user_manager,
"refresh_rss": self.__refresh_rss,
"refresh_message": self.__refresh_message,
"delete_tmdb_cache": self.__delete_tmdb_cache,
"movie_calendar_data": self.__movie_calendar_data,
"tv_calendar_data": self.__tv_calendar_data,
"modify_tmdb_cache": self.__modify_tmdb_cache,
"rss_detail": self.__rss_detail,
"truncate_blacklist": self.truncate_blacklist,
"truncate_rsshistory": self.truncate_rsshistory,
"add_brushtask": self.__add_brushtask,
"del_brushtask": self.__del_brushtask,
"brushtask_detail": self.__brushtask_detail,
"add_downloader": self.__add_downloader,
"delete_downloader": self.__delete_downloader,
"get_downloader": self.__get_downloader,
"name_test": self.__name_test,
"rule_test": self.__rule_test,
"net_test": self.__net_test,
"add_filtergroup": self.__add_filtergroup,
"restore_filtergroup": self.__restore_filtergroup,
"set_default_filtergroup": self.__set_default_filtergroup,
"del_filtergroup": self.__del_filtergroup,
"add_filterrule": self.__add_filterrule,
"del_filterrule": self.__del_filterrule,
"filterrule_detail": self.__filterrule_detail,
"get_site_activity": self.__get_site_activity,
"get_site_history": self.__get_site_history,
"get_recommend": self.get_recommend,
"get_downloaded": self.get_downloaded,
"get_site_seeding_info": self.__get_site_seeding_info,
"clear_tmdb_cache": self.__clear_tmdb_cache,
"check_site_attr": self.__check_site_attr,
"refresh_process": self.__refresh_process,
"restory_backup": self.__restory_backup,
"start_mediasync": self.__start_mediasync,
"mediasync_state": self.__mediasync_state,
"get_tvseason_list": self.__get_tvseason_list,
"get_userrss_task": self.__get_userrss_task,
"delete_userrss_task": self.__delete_userrss_task,
"update_userrss_task": self.__update_userrss_task,
"get_rssparser": self.__get_rssparser,
"delete_rssparser": self.__delete_rssparser,
"update_rssparser": self.__update_rssparser,
"run_userrss": self.__run_userrss,
"run_brushtask": self.__run_brushtask,
"list_site_resources": self.__list_site_resources,
"list_rss_articles": self.__list_rss_articles,
"rss_article_test": self.__rss_article_test,
"list_rss_history": self.__list_rss_history,
"rss_articles_check": self.__rss_articles_check,
"rss_articles_download": self.__rss_articles_download,
"add_custom_word_group": self.__add_custom_word_group,
"delete_custom_word_group": self.__delete_custom_word_group,
"add_or_edit_custom_word": self.__add_or_edit_custom_word,
"get_custom_word": self.__get_custom_word,
"delete_custom_word": self.__delete_custom_word,
"check_custom_words": self.__check_custom_words,
"export_custom_words": self.__export_custom_words,
"analyse_import_custom_words_code": self.__analyse_import_custom_words_code,
"import_custom_words": self.__import_custom_words,
"get_categories": self.__get_categories,
"re_rss_history": self.__re_rss_history,
"delete_rss_history": self.__delete_rss_history,
"share_filtergroup": self.__share_filtergroup,
"import_filtergroup": self.__import_filtergroup,
"get_transfer_statistics": self.get_transfer_statistics,
"get_library_spacesize": self.get_library_spacesize,
"get_library_mediacount": self.get_library_mediacount,
"get_library_playhistory": self.get_library_playhistory,
"get_search_result": self.get_search_result,
"search_media_infos": self.search_media_infos,
"get_movie_rss_list": self.get_movie_rss_list,
"get_tv_rss_list": self.get_tv_rss_list,
"get_rss_history": self.get_rss_history,
"get_transfer_history": self.get_transfer_history,
"get_unknown_list": self.get_unknown_list,
"get_unknown_list_by_page": self.get_unknown_list_by_page,
"get_customwords": self.get_customwords,
"get_directorysync": self.get_directorysync,
"get_users": self.get_users,
"get_filterrules": self.get_filterrules,
"get_downloading": self.get_downloading,
"test_site": self.__test_site,
"get_sub_path": self.__get_sub_path,
"rename_file": self.__rename_file,
"delete_files": self.__delete_files,
"download_subtitle": self.__download_subtitle,
"get_download_setting": self.__get_download_setting,
"update_download_setting": self.__update_download_setting,
"delete_download_setting": self.__delete_download_setting,
"update_message_client": self.__update_message_client,
"delete_message_client": self.__delete_message_client,
"check_message_client": self.__check_message_client,
"get_message_client": self.__get_message_client,
"test_message_client": self.__test_message_client,
"get_sites": self.__get_sites,
"get_indexers": self.__get_indexers,
"get_download_dirs": self.__get_download_dirs,
"find_hardlinks": self.__find_hardlinks,
"update_sites_cookie_ua": self.__update_sites_cookie_ua,
"update_site_cookie_ua": self.__update_site_cookie_ua,
"set_site_captcha_code": self.__set_site_captcha_code,
"update_torrent_remove_task": self.__update_torrent_remove_task,
"get_torrent_remove_task": self.__get_torrent_remove_task,
"delete_torrent_remove_task": self.__delete_torrent_remove_task,
"get_remove_torrents": self.__get_remove_torrents,
"auto_remove_torrents": self.__auto_remove_torrents,
"get_douban_history": self.get_douban_history,
"delete_douban_history": self.__delete_douban_history,
"list_brushtask_torrents": self.__list_brushtask_torrents,
"set_system_config": self.__set_system_config,
"get_site_user_statistics": self.get_site_user_statistics,
"send_custom_message": self.send_custom_message,
"cookiecloud_sync": self.__cookiecloud_sync,
"media_detail": self.media_detail,
"media_similar": self.__media_similar,
"media_recommendations": self.__media_recommendations,
"media_person": self.__media_person,
"person_medias": self.__person_medias,
"save_user_script": self.__save_user_script,
"run_directory_sync": self.__run_directory_sync,
"update_plugin_config": self.__update_plugin_config
}
def action(self, cmd, data=None):
func = self._actions.get(cmd)
if not func:
return {"code": -1, "msg": "非授权访问!"}
else:
return func(data)
def api_action(self, cmd, data=None):
result = self.action(cmd, data)
if not result:
return {
"code": -1,
"success": False,
"message": "服务异常,未获取到返回结果"
}
code = result.get("code", result.get("retcode", 0))
if not code or str(code) == "0":
success = True
else:
success = False
message = result.get("msg", result.get("retmsg", ""))
for key in ['code', 'retcode', 'msg', 'retmsg']:
if key in result:
result.pop(key)
return {
"code": code,
"success": success,
"message": message,
"data": result
}
@staticmethod
def restart_server():
"""
停止进程
"""
# 停止定时服务
stop_scheduler()
# 停止监控
stop_monitor()
# 关闭虚拟显示
DisplayHelper().stop_service()
# 关闭刷流
BrushTask().stop_service()
# 关闭自定义订阅
RssChecker().stop_service()
# 关闭插件
PluginManager().stop_service()
# 签退
logout_user()
# 重启进程
if os.name == "nt":
os.kill(os.getpid(), getattr(signal, "SIGKILL", signal.SIGTERM))
elif SystemUtils.is_synology():
os.system(
"ps -ef | grep -v grep | grep 'python run.py'|awk '{print $2}'|xargs kill -9")
else:
os.system("pm2 restart NAStool")
@staticmethod
def handle_message_job(msg, in_from=SearchType.OT, user_id=None, user_name=None):
"""
处理消息事件
"""
if not msg:
return
commands = {
"/ptr": {"func": TorrentRemover().auto_remove_torrents, "desp": "删种"},
"/ptt": {"func": Downloader().transfer, "desp": "下载文件转移"},
"/pts": {"func": SiteSignin().signin, "desp": "站点签到"},
"/rst": {"func": Sync().transfer_all_sync, "desp": "目录同步"},
"/rss": {"func": Rss().rssdownload, "desp": "RSS订阅"},
"/db": {"func": DoubanSync().sync, "desp": "豆瓣同步"},
"/ssa": {"func": Subscribe().subscribe_search_all, "desp": "订阅搜索"},
"/tbl": {"func": WebAction().truncate_blacklist, "desp": "清理转移缓存"},
"/trh": {"func": WebAction().truncate_rsshistory, "desp": "清理RSS缓存"},
"/utf": {"func": WebAction().unidentification, "desp": "重新识别"},
"/udt": {"func": WebAction().update_system, "desp": "系统更新"}
}
# 触发事件
EventManager().send_event(EventType.MessageIncoming, {
"channel": in_from.value,
"user_id": user_id,
"user_name": user_name,
"message": msg
})
command = commands.get(msg)
message = Message()
if command:
# 启动服务
ThreadHelper().start_thread(command.get("func"), ())
message.send_channel_msg(
channel=in_from, title="正在运行 %s ..." % command.get("desp"), user_id=user_id)
else:
# 站点检索或者添加订阅
ThreadHelper().start_thread(search_media_by_message,
(msg, in_from, user_id, user_name))
@staticmethod
def set_config_value(cfg, cfg_key, cfg_value):
"""
根据Key设置配置值
"""
# 密码
if cfg_key == "app.login_password":
if cfg_value and not cfg_value.startswith("[hash]"):
cfg['app']['login_password'] = "[hash]%s" % generate_password_hash(
cfg_value)
else:
cfg['app']['login_password'] = cfg_value or "password"
return cfg
# 代理
if cfg_key == "app.proxies":
if cfg_value:
if not cfg_value.startswith("http") and not cfg_value.startswith("sock"):
cfg['app']['proxies'] = {
"https": "http://%s" % cfg_value, "http": "http://%s" % cfg_value}
else:
cfg['app']['proxies'] = {"https": "%s" %
cfg_value, "http": "%s" % cfg_value}
else:
cfg['app']['proxies'] = {"https": None, "http": None}
return cfg
# 豆瓣用户列表
if cfg_key == "douban.users":
vals = cfg_value.split(",")
cfg['douban']['users'] = vals
return cfg
# 最大支持三层赋值
keys = cfg_key.split(".")
if keys:
if len(keys) == 1:
cfg[keys[0]] = cfg_value
elif len(keys) == 2:
if not cfg.get(keys[0]):
cfg[keys[0]] = {}
cfg[keys[0]][keys[1]] = cfg_value
elif len(keys) == 3:
if cfg.get(keys[0]):
if not cfg[keys[0]].get(keys[1]) or isinstance(cfg[keys[0]][keys[1]], str):
cfg[keys[0]][keys[1]] = {}
cfg[keys[0]][keys[1]][keys[2]] = cfg_value
else:
cfg[keys[0]] = {}
cfg[keys[0]][keys[1]] = {}
cfg[keys[0]][keys[1]][keys[2]] = cfg_value
return cfg
@staticmethod
def set_config_directory(cfg, oper, cfg_key, cfg_value, update_value=None):
"""
更新目录数据
"""
def remove_sync_path(obj, key):
if not isinstance(obj, list):
return []
ret_obj = []
for item in obj:
if item.split("@")[0].replace("\\", "/") != key.split("@")[0].replace("\\", "/"):
ret_obj.append(item)
return ret_obj
# 最大支持二层赋值
keys = cfg_key.split(".")
if keys:
if len(keys) == 1:
if cfg.get(keys[0]):
if not isinstance(cfg[keys[0]], list):
cfg[keys[0]] = [cfg[keys[0]]]
if oper == "add":
cfg[keys[0]].append(cfg_value)
elif oper == "sub":
cfg[keys[0]].remove(cfg_value)
if not cfg[keys[0]]:
cfg[keys[0]] = None
elif oper == "set":
cfg[keys[0]].remove(cfg_value)
if update_value:
cfg[keys[0]].append(update_value)
else:
cfg[keys[0]] = cfg_value
elif len(keys) == 2:
if cfg.get(keys[0]):
if not cfg[keys[0]].get(keys[1]):
cfg[keys[0]][keys[1]] = []
if not isinstance(cfg[keys[0]][keys[1]], list):
cfg[keys[0]][keys[1]] = [cfg[keys[0]][keys[1]]]
if oper == "add":
cfg[keys[0]][keys[1]].append(
cfg_value.replace("\\", "/"))
elif oper == "sub":
cfg[keys[0]][keys[1]] = remove_sync_path(
cfg[keys[0]][keys[1]], cfg_value)
if not cfg[keys[0]][keys[1]]:
cfg[keys[0]][keys[1]] = None
elif oper == "set":
cfg[keys[0]][keys[1]] = remove_sync_path(
cfg[keys[0]][keys[1]], cfg_value)
if update_value:
cfg[keys[0]][keys[1]].append(
update_value.replace("\\", "/"))
else:
cfg[keys[0]] = {}
cfg[keys[0]][keys[1]] = cfg_value.replace("\\", "/")
return cfg
@staticmethod
def __sch(data):
"""
启动定时服务
"""
commands = {
"autoremovetorrents": TorrentRemover().auto_remove_torrents,
"pttransfer": Downloader().transfer,
"ptsignin": SiteSignin().signin,
"sync": Sync().transfer_all_sync,
"rssdownload": Rss().rssdownload,
"douban": DoubanSync().sync,
"subscribe_search_all": Subscribe().subscribe_search_all,
}
sch_item = data.get("item")
if sch_item and commands.get(sch_item):
ThreadHelper().start_thread(commands.get(sch_item), ())
return {"retmsg": "服务已启动", "item": sch_item}
@staticmethod
def __search(data):
"""
WEB检索资源
"""
search_word = data.get("search_word")
ident_flag = False if data.get("unident") else True
filters = data.get("filters")
tmdbid = data.get("tmdbid")
media_type = data.get("media_type")
if media_type:
if media_type in MovieTypes:
media_type = MediaType.MOVIE
else:
media_type = MediaType.TV
if search_word:
ret, ret_msg = search_medias_for_web(content=search_word,
ident_flag=ident_flag,
filters=filters,
tmdbid=tmdbid,
media_type=media_type)
if ret != 0:
return {"code": ret, "msg": ret_msg}
return {"code": 0}
def __download(self, data):
"""
从WEB添加下载
"""
dl_id = data.get("id")
dl_dir = data.get("dir")
dl_setting = data.get("setting")
results = self.dbhelper.get_search_result_by_id(dl_id)
for res in results:
media = Media().get_media_info(title=res.TORRENT_NAME, subtitle=res.DESCRIPTION)
if not media:
continue
media.set_torrent_info(enclosure=res.ENCLOSURE,
size=res.SIZE,
site=res.SITE,
page_url=res.PAGEURL,
upload_volume_factor=float(
res.UPLOAD_VOLUME_FACTOR),
download_volume_factor=float(res.DOWNLOAD_VOLUME_FACTOR))
# 添加下载
ret, ret_msg = Downloader().download(media_info=media,
download_dir=dl_dir,
download_setting=dl_setting)
if ret:
# 发送消息
media.user_name = current_user.username
Message().send_download_message(in_from=SearchType.WEB,
can_item=media)
else:
return {"retcode": -1, "retmsg": ret_msg}
return {"retcode": 0, "retmsg": ""}
@staticmethod
def __download_link(data):
"""
从WEB添加下载链接
"""
site = data.get("site")
enclosure = data.get("enclosure")
title = data.get("title")
description = data.get("description")
page_url = data.get("page_url")
size = data.get("size")
seeders = data.get("seeders")
uploadvolumefactor = data.get("uploadvolumefactor")
downloadvolumefactor = data.get("downloadvolumefactor")
dl_dir = data.get("dl_dir")
dl_setting = data.get("dl_setting")
if not title or not enclosure:
return {"code": -1, "msg": "种子信息有误"}
media = Media().get_media_info(title=title, subtitle=description)
media.site = site
media.enclosure = enclosure
media.page_url = page_url
media.size = size
media.upload_volume_factor = float(uploadvolumefactor)
media.download_volume_factor = float(downloadvolumefactor)
media.seeders = seeders
# 添加下载
ret, ret_msg = Downloader().download(media_info=media,
download_dir=dl_dir,
download_setting=dl_setting)
if ret:
# 发送消息
media.user_name = current_user.username
Message().send_download_message(SearchType.WEB, media)
return {"code": 0, "msg": "下载成功"}
else:
return {"code": 1, "msg": ret_msg or "如连接正常,请检查下载任务是否存在"}
@staticmethod
def __download_torrent(data):
"""
从种子文件添加下载
"""
def __download(_media_info, _file_path):
_media_info.site = "WEB"
# 添加下载
ret, ret_msg = Downloader().download(media_info=_media_info,
download_dir=dl_dir,
download_setting=dl_setting,
torrent_file=_file_path)
# 发送消息
_media_info.user_name = current_user.username
if ret:
Message().send_download_message(SearchType.WEB, _media_info)
else:
Message().send_download_fail_message(_media_info, ret_msg)
dl_dir = data.get("dl_dir")
dl_setting = data.get("dl_setting")
files = data.get("files")
magnets = data.get("magnets")
if not files and not magnets:
return {"code": -1, "msg": "没有种子文件或磁链"}
for file_item in files:
if not file_item:
continue
file_name = file_item.get("upload", {}).get("filename")
file_path = os.path.join(Config().get_temp_path(), file_name)
media_info = Media().get_media_info(title=file_name)
__download(media_info, file_path)
for magnet in magnets:
if not magnet:
continue
file_path = None
title = Torrent().get_magnet_title(magnet)
if title:
media_info = Media().get_media_info(title=title)
else:
media_info = MetaInfo(title="磁力链接")
media_info.org_string = magnet
media_info.set_torrent_info(enclosure=magnet,
download_volume_factor=0,
upload_volume_factor=1)
__download(media_info, file_path)
return {"code": 0, "msg": "添加下载完成!"}
@staticmethod
def __pt_start(data):
"""
开始下载
"""
tid = data.get("id")
if id:
Downloader().start_torrents(ids=tid)
return {"retcode": 0, "id": tid}
@staticmethod
def __pt_stop(data):
"""
停止下载
"""
tid = data.get("id")
if id:
Downloader().stop_torrents(ids=tid)
return {"retcode": 0, "id": tid}
@staticmethod
def __pt_remove(data):
"""
删除下载
"""
tid = data.get("id")
if id:
Downloader().delete_torrents(ids=tid, delete_file=True)
return {"retcode": 0, "id": tid}
@staticmethod
def __pt_info(data):
"""
查询具体种子的信息
"""
ids = data.get("ids")
Client, Torrents = Downloader().get_torrents(torrent_ids=ids)
DispTorrents = []
for torrent in Torrents:
if not torrent:
continue
if Client == DownloaderType.QB:
if torrent.get('state') in ['pausedDL']:
state = "Stoped"
speed = "已暂停"
else:
state = "Downloading"
dlspeed = StringUtils.str_filesize(torrent.get('dlspeed'))
eta = StringUtils.str_timelong(torrent.get('eta'))
upspeed = StringUtils.str_filesize(torrent.get('upspeed'))
speed = "%s%sB/s %s%sB/s %s" % (chr(8595),
dlspeed, chr(8593), upspeed, eta)
# 进度
progress = round(torrent.get('progress') * 100)
# 主键
key = torrent.get('hash')
elif Client == DownloaderType.TR:
if torrent.status in ['stopped']:
state = "Stoped"
speed = "已暂停"
else:
state = "Downloading"
dlspeed = StringUtils.str_filesize(torrent.rateDownload)
upspeed = StringUtils.str_filesize(torrent.rateUpload)
speed = "%s%sB/s %s%sB/s" % (chr(8595),
dlspeed, chr(8593), upspeed)
# 进度
progress = round(torrent.progress, 1)
# 主键
key = torrent.id
else:
continue
torrent_info = {
'id': key,
'speed': speed,
'state': state,
'progress': progress
}
if torrent_info not in DispTorrents:
DispTorrents.append(torrent_info)
return {"retcode": 0, "torrents": DispTorrents}
def __del_unknown_path(self, data):
"""
删除路径
"""
tids = data.get("id")
if isinstance(tids, list):
for tid in tids:
if not tid:
continue
self.dbhelper.delete_transfer_unknown(tid)
return {"retcode": 0}
else:
retcode = self.dbhelper.delete_transfer_unknown(tids)
return {"retcode": retcode}
def __rename(self, data):
"""
手工转移
"""
path = dest_dir = None
syncmod = ModuleConf.RMT_MODES.get(data.get("syncmod"))
logid = data.get("logid")
if logid:
paths = self.dbhelper.get_transfer_path_by_id(logid)
if paths:
path = os.path.join(
paths[0].SOURCE_PATH, paths[0].SOURCE_FILENAME)
dest_dir = paths[0].DEST
else:
return {"retcode": -1, "retmsg": "未查询到转移日志记录"}
else:
unknown_id = data.get("unknown_id")
if unknown_id:
paths = self.dbhelper.get_unknown_path_by_id(unknown_id)
if paths:
path = paths[0].PATH
dest_dir = paths[0].DEST
else:
return {"retcode": -1, "retmsg": "未查询到未识别记录"}
if not dest_dir:
dest_dir = ""
if not path:
return {"retcode": -1, "retmsg": "输入路径有误"}
tmdbid = data.get("tmdb")
mtype = data.get("type")
season = data.get("season")
episode_format = data.get("episode_format")
episode_details = data.get("episode_details")
episode_offset = data.get("episode_offset")
min_filesize = data.get("min_filesize")
if mtype in MovieTypes:
media_type = MediaType.MOVIE
elif mtype in TvTypes:
media_type = MediaType.TV
else:
media_type = MediaType.ANIME
# 如果改次手动修复时一个单文件,自动修复改目录下同名文件,需要配合episode_format生效
need_fix_all = False
if os.path.splitext(path)[-1].lower() in RMT_MEDIAEXT and episode_format:
path = os.path.dirname(path)
need_fix_all = True
# 开始转移
succ_flag, ret_msg = self.__manual_transfer(inpath=path,
syncmod=syncmod,
outpath=dest_dir,
media_type=media_type,
episode_format=episode_format,
episode_details=episode_details,
episode_offset=episode_offset,
need_fix_all=need_fix_all,
min_filesize=min_filesize,
tmdbid=tmdbid,
season=season)
if succ_flag:
if not need_fix_all and not logid:
# 更新记录状态
self.dbhelper.update_transfer_unknown_state(path)
return {"retcode": 0, "retmsg": "转移成功"}
else:
return {"retcode": 2, "retmsg": ret_msg}
def __rename_udf(self, data):
"""
自定义识别
"""
inpath = data.get("inpath")
if not os.path.exists(inpath):
return {"retcode": -1, "retmsg": "输入路径不存在"}
outpath = data.get("outpath")
syncmod = ModuleConf.RMT_MODES.get(data.get("syncmod"))
tmdbid = data.get("tmdb")
mtype = data.get("type")
season = data.get("season")
episode_format = data.get("episode_format")
episode_details = data.get("episode_details")
episode_offset = data.get("episode_offset")
min_filesize = data.get("min_filesize")
if mtype in MovieTypes:
media_type = MediaType.MOVIE
elif mtype in TvTypes:
media_type = MediaType.TV
else:
media_type = MediaType.ANIME
# 开始转移
succ_flag, ret_msg = self.__manual_transfer(inpath=inpath,
syncmod=syncmod,
outpath=outpath,
media_type=media_type,
episode_format=episode_format,
episode_details=episode_details,
episode_offset=episode_offset,
min_filesize=min_filesize,
tmdbid=tmdbid,
season=season)
if succ_flag:
return {"retcode": 0, "retmsg": "转移成功"}
else:
return {"retcode": 2, "retmsg": ret_msg}
@staticmethod
def __manual_transfer(inpath,
syncmod,
outpath=None,
media_type=None,
episode_format=None,
episode_details=None,
episode_offset=None,
min_filesize=None,
tmdbid=None,
season=None,
need_fix_all=False
):
"""
开始手工转移文件
"""
inpath = os.path.normpath(inpath)
if outpath:
outpath = os.path.normpath(outpath)
if not os.path.exists(inpath):
return False, "输入路径不存在"
if tmdbid:
# 有输入TMDBID
tmdb_info = Media().get_tmdb_info(mtype=media_type, tmdbid=tmdbid)
if not tmdb_info:
return False, "识别失败,无法查询到TMDB信息"
# 按识别的信息转移
succ_flag, ret_msg = FileTransfer().transfer_media(in_from=SyncType.MAN,
in_path=inpath,
rmt_mode=syncmod,
target_dir=outpath,
tmdb_info=tmdb_info,
media_type=media_type,
season=season,
episode=(
EpisodeFormat(episode_format,
episode_details,
episode_offset),
need_fix_all),
min_filesize=min_filesize,
udf_flag=True)
else:
# 按识别的信息转移
succ_flag, ret_msg = FileTransfer().transfer_media(in_from=SyncType.MAN,
in_path=inpath,
rmt_mode=syncmod,
target_dir=outpath,
media_type=media_type,
episode=(
EpisodeFormat(episode_format,
episode_details,
episode_offset),
need_fix_all),
min_filesize=min_filesize,
udf_flag=True)
return succ_flag, ret_msg
def __delete_history(self, data):
"""
删除识别记录及文件
"""
logids = data.get('logids')
flag = data.get('flag')
for logid in logids:
# 读取历史记录
paths = self.dbhelper.get_transfer_path_by_id(logid)
if paths:
# 删除记录
self.dbhelper.delete_transfer_log_by_id(logid)
# 根据flag删除文件
source_path = paths[0].SOURCE_PATH
source_filename = paths[0].SOURCE_FILENAME
# 删除该识别记录对应的转移记录
self.dbhelper.delete_transfer_blacklist("%s/%s" % (source_path, source_filename))
dest = paths[0].DEST
dest_path = paths[0].DEST_PATH
dest_filename = paths[0].DEST_FILENAME
if flag in ["del_source", "del_all"]:
del_flag, del_msg = self.delete_media_file(
source_path, source_filename)
if not del_flag:
log.error(f"【History】{del_msg}")
else:
log.info(f"【History】{del_msg}")
if flag in ["del_dest", "del_all"]:
if dest_path and dest_filename:
del_flag, del_msg = self.delete_media_file(
dest_path, dest_filename)
if not del_flag:
log.error(f"【History】{del_msg}")
else:
log.info(f"【History】{del_msg}")
else:
meta_info = MetaInfo(title=source_filename)
meta_info.title = paths[0].TITLE
meta_info.category = paths[0].CATEGORY
meta_info.year = paths[0].YEAR
if paths[0].SEASON_EPISODE:
meta_info.begin_season = int(
str(paths[0].SEASON_EPISODE).replace("S", ""))
if paths[0].TYPE == MediaType.MOVIE.value:
meta_info.type = MediaType.MOVIE
else:
meta_info.type = MediaType.TV
# 删除文件
dest_path = FileTransfer().get_dest_path_by_info(dest=dest, meta_info=meta_info)
if dest_path and dest_path.find(meta_info.title) != -1:
rm_parent_dir = False
if not meta_info.get_season_list():
# 电影,删除整个目录
try:
shutil.rmtree(dest_path)
except Exception as e:
ExceptionUtils.exception_traceback(e)
elif not meta_info.get_episode_string():
# 电视剧但没有集数,删除季目录
try:
shutil.rmtree(dest_path)
except Exception as e:
ExceptionUtils.exception_traceback(e)
rm_parent_dir = True
else:
# 有集数的电视剧,删除对应的集数文件
for dest_file in PathUtils.get_dir_files(dest_path):
file_meta_info = MetaInfo(
os.path.basename(dest_file))
if file_meta_info.get_episode_list() and set(
file_meta_info.get_episode_list()
).issubset(set(meta_info.get_episode_list())):
try:
os.remove(dest_file)
except Exception as e:
ExceptionUtils.exception_traceback(
e)
rm_parent_dir = True
if rm_parent_dir \
and not PathUtils.get_dir_files(os.path.dirname(dest_path), exts=RMT_MEDIAEXT):
# 没有媒体文件时,删除整个目录
try:
shutil.rmtree(os.path.dirname(dest_path))
except Exception as e:
ExceptionUtils.exception_traceback(e)
return {"retcode": 0}
@staticmethod
def delete_media_file(filedir, filename):
"""
删除媒体文件,空目录也支被删除
"""
filedir = os.path.normpath(filedir).replace("\\", "/")
file = os.path.join(filedir, filename)
try:
if not os.path.exists(file):
return False, f"{file} 不存在"
os.remove(file)
nfoname = f"{os.path.splitext(filename)[0]}.nfo"
nfofile = os.path.join(filedir, nfoname)
if os.path.exists(nfofile):
os.remove(nfofile)
# 检查空目录并删除
if re.findall(r"^S\d{2}|^Season", os.path.basename(filedir), re.I):
# 当前是季文件夹,判断并删除
seaon_dir = filedir
if seaon_dir.count('/') > 1 and not PathUtils.get_dir_files(seaon_dir, exts=RMT_MEDIAEXT):
shutil.rmtree(seaon_dir)
# 媒体文件夹
media_dir = os.path.dirname(seaon_dir)
else:
media_dir = filedir
# 检查并删除媒体文件夹,非根目录且目录大于二级,且没有媒体文件时才会删除
if media_dir != '/' \
and media_dir.count('/') > 1 \
and not re.search(r'[a-zA-Z]:/$', media_dir) \
and not PathUtils.get_dir_files(media_dir, exts=RMT_MEDIAEXT):
shutil.rmtree(media_dir)
return True, f"{file} 删除成功"
except Exception as e:
ExceptionUtils.exception_traceback(e)
return True, f"{file} 删除失败"
@staticmethod
def __logging(data):
"""
查询实时日志
"""
log_list = []
refresh_new = data.get('refresh_new')
source = data.get('source')
if not source:
if not refresh_new:
log_list = list(log.LOG_QUEUE)
elif log.LOG_INDEX:
if log.LOG_INDEX > len(list(log.LOG_QUEUE)):
log_list = list(log.LOG_QUEUE)
else:
log_list = list(log.LOG_QUEUE)[-log.LOG_INDEX:]
log.LOG_INDEX = 0
else:
queue_logs = list(log.LOG_QUEUE)
for message in queue_logs:
if str(message.get("source")) == source:
log_list.append(message)
else:
continue
if refresh_new:
if int(refresh_new) < len(log_list):
log_list = log_list[int(refresh_new):]
elif int(refresh_new) >= len(log_list):
log_list = []
return {"loglist": log_list}
@staticmethod
def __version(data):
"""
检查新版本
"""
version, url, flag = WebUtils.get_latest_version()
if flag:
return {"code": 0, "version": version, "url": url}
return {"code": -1, "version": "", "url": ""}
def __update_site(self, data):
"""
维护站点信息
"""
def __is_site_duplicate(query_name, query_tid):
# 检查是否重名
_sites = self.dbhelper.get_site_by_name(name=query_name)
for site in _sites:
site_id = site.ID
if str(site_id) != str(query_tid):
return True
return False
tid = data.get('site_id')
name = data.get('site_name')
site_pri = data.get('site_pri')
rssurl = data.get('site_rssurl')
signurl = data.get('site_signurl')
cookie = data.get('site_cookie')
note = data.get('site_note')
if isinstance(note, dict):
note = json.dumps(note)
rss_uses = data.get('site_include')
if __is_site_duplicate(name, tid):
return {"code": 400, "msg": "站点名称重复"}
if tid:
sites = self.dbhelper.get_site_by_id(tid)
# 站点不存在
if not sites:
return {"code": 400, "msg": "站点不存在"}
old_name = sites[0].NAME
ret = self.dbhelper.update_config_site(tid=tid,
name=name,
site_pri=site_pri,
rssurl=rssurl,
signurl=signurl,
cookie=cookie,
note=note,
rss_uses=rss_uses)
if ret and (name != old_name):
# 更新历史站点数据信息
self.dbhelper.update_site_user_statistics_site_name(
name, old_name)
self.dbhelper.update_site_seed_info_site_name(name, old_name)
self.dbhelper.update_site_statistics_site_name(name, old_name)
else:
ret = self.dbhelper.insert_config_site(name=name,
site_pri=site_pri,
rssurl=rssurl,
signurl=signurl,
cookie=cookie,
note=note,
rss_uses=rss_uses)
# 生效站点配置
Sites().init_config()
# 初始化刷流任务
BrushTask().init_config()
return {"code": ret}
@staticmethod
def __get_site(data):
"""
查询单个站点信息
"""
tid = data.get("id")
site_free = False
site_2xfree = False
site_hr = False
if tid:
ret = Sites().get_sites(siteid=tid)
if ret.get("rssurl"):
site_attr = Sites().get_grapsite_conf(ret.get("rssurl"))
if site_attr.get("FREE"):
site_free = True
if site_attr.get("2XFREE"):
site_2xfree = True
if site_attr.get("HR"):
site_hr = True
else:
ret = []
return {"code": 0, "site": ret, "site_free": site_free, "site_2xfree": site_2xfree, "site_hr": site_hr}
@staticmethod
def __get_sites(data):
"""
查询多个站点信息
"""
rss = True if data.get("rss") else False
brush = True if data.get("brush") else False
signin = True if data.get("signin") else False
statistic = True if data.get("statistic") else False
basic = True if data.get("basic") else False
if basic:
sites = Sites().get_site_dict(rss=rss,
brush=brush,
signin=signin,
statistic=statistic)
else:
sites = Sites().get_sites(rss=rss,
brush=brush,
signin=signin,
statistic=statistic)
return {"code": 0, "sites": sites}
def __del_site(self, data):
"""
删除单个站点信息
"""
tid = data.get("id")
if tid:
ret = self.dbhelper.delete_config_site(tid)
Sites().init_config()
BrushTask().init_config()
return {"code": ret}
else:
return {"code": 0}
def __restart(self, data):
"""
重启
"""
# 退出主进程
self.restart_server()
return {"code": 0}
def update_system(self, data=None):
"""
更新
"""
# 升级
if SystemUtils.is_synology():
if SystemUtils.execute('/bin/ps -w -x | grep -v grep | grep -w "nastool update" | wc -l') == '0':
# 调用群晖套件内置命令升级
os.system('nastool update')
# 重启
self.restart_server()
else:
# 清除git代理
os.system("git config --global --unset http.proxy")
os.system("git config --global --unset https.proxy")
# 设置git代理
proxy = Config().get_proxies() or {}
http_proxy = proxy.get("http")
https_proxy = proxy.get("https")
if http_proxy or https_proxy:
os.system(
f"git config --global http.proxy {http_proxy or https_proxy}")
os.system(
f"git config --global https.proxy {https_proxy or http_proxy}")
# 清理
os.system("git clean -dffx")
# 升级
branch = "dev" if os.environ.get(
"NASTOOL_VERSION") == "dev" else "master"
os.system(f"git fetch --depth 1 origin {branch}")
os.system(f"git reset --hard origin/{branch}")
os.system("git submodule update --init --recursive")
# 安装依赖
os.system('pip install -r /nas-tools/requirements.txt')
# 重启
self.restart_server()
return {"code": 0}
def __reset_db_version(self, data):
"""
重置数据库版本
"""
try:
self.dbhelper.drop_table("alembic_version")
return {"code": 0}
except Exception as e:
ExceptionUtils.exception_traceback(e)
return {"code": 1, "msg": str(e)}
@staticmethod
def __logout(data):
"""
注销
"""
logout_user()
return {"code": 0}
def __update_config(self, data):
"""
更新配置信息
"""
cfg = Config().get_config()
cfgs = dict(data).items()
# 仅测试不保存
config_test = False
# 修改配置
for key, value in cfgs:
if key == "test" and value:
config_test = True
continue
# 生效配置
cfg = self.set_config_value(cfg, key, value)
# 保存配置
if not config_test:
Config().save_config(cfg)
return {"code": 0}
def __add_or_edit_sync_path(self, data):
"""
维护同步目录
"""
sid = data.get("sid")
source = data.get("from")
dest = data.get("to")
unknown = data.get("unknown")
mode = data.get("syncmod")
rename = 1 if StringUtils.to_bool(data.get("rename"), False) else 0
enabled = 1 if StringUtils.to_bool(data.get("enabled"), False) else 0
# 源目录检查
if not source:
return {"code": 1, "msg": f'源目录不能为空'}
if not os.path.exists(source):
return {"code": 1, "msg": f'{source}目录不存在'}
# windows目录用\,linux目录用/
source = os.path.normpath(source)
# 目的目录检查,目的目录可为空
if dest:
dest = os.path.normpath(dest)
if PathUtils.is_path_in_path(source, dest):
return {"code": 1, "msg": "目的目录不可包含在源目录中"}
if unknown:
unknown = os.path.normpath(unknown)
# 硬链接不能跨盘
if mode == "link" and dest:
common_path = os.path.commonprefix([source, dest])
if not common_path or common_path == "/":
return {"code": 1, "msg": "硬链接不能跨盘"}
# 编辑先删再增
if sid:
self.dbhelper.delete_config_sync_path(sid)
# 若启用,则关闭其他相同源目录的同步目录
if enabled == 1:
self.dbhelper.check_config_sync_paths(source=source,
enabled=0)
# 插入数据库
self.dbhelper.insert_config_sync_path(source=source,
dest=dest,
unknown=unknown,
mode=mode,
rename=rename,
enabled=enabled)
Sync().init_config()
return {"code": 0, "msg": ""}
def __get_sync_path(self, data):
"""
查询同步目录
"""
try:
sid = data.get("sid")
sync_item = self.dbhelper.get_config_sync_paths(sid=sid)[0]
syncpath = {'id': sync_item.ID,
'from': sync_item.SOURCE,
'to': sync_item.DEST or "",
'unknown': sync_item.UNKNOWN or "",
'syncmod': sync_item.MODE,
'rename': sync_item.RENAME,
'enabled': sync_item.ENABLED}
return {"code": 0, "data": syncpath}
except Exception as e:
ExceptionUtils.exception_traceback(e)
return {"code": 1, "msg": "查询识别词失败"}
def __delete_sync_path(self, data):
"""
移出同步目录
"""
sid = data.get("sid")
self.dbhelper.delete_config_sync_path(sid)
Sync().init_config()
return {"code": 0}
def __check_sync_path(self, data):
"""
维护同步目录
"""
flag = data.get("flag")
sid = data.get("sid")
checked = data.get("checked")
if flag == "rename":
self.dbhelper.check_config_sync_paths(sid=sid,
rename=1 if checked else 0)
Sync().init_config()
return {"code": 0}
elif flag == "enable":
# 若启用,则关闭其他相同源目录的同步目录
if checked:
sync_item = self.dbhelper.get_config_sync_paths(sid=sid)[0]
self.dbhelper.check_config_sync_paths(source=sync_item.SOURCE,
enabled=0)
self.dbhelper.check_config_sync_paths(sid=sid,
enabled=1 if checked else 0)
Sync().init_config()
return {"code": 0}
else:
return {"code": 1}
def __remove_rss_media(self, data):
"""
移除RSS订阅
"""
name = data.get("name")
mtype = data.get("type")
year = data.get("year")
season = data.get("season")
rssid = data.get("rssid")
page = data.get("page")
tmdbid = data.get("tmdbid")
if not str(tmdbid).isdigit():
tmdbid = None
if name:
name = MetaInfo(title=name).get_name()
if mtype:
if mtype in MovieTypes:
self.dbhelper.delete_rss_movie(
title=name, year=year, rssid=rssid, tmdbid=tmdbid)
else:
self.dbhelper.delete_rss_tv(
title=name, season=season, rssid=rssid, tmdbid=tmdbid)
return {"code": 0, "page": page, "name": name}
def __add_rss_media(self, data):
"""
添加RSS订阅
"""
name = data.get("name")
_subscribe = Subscribe()
year = data.get("year")
keyword = data.get("keyword")
season = data.get("season")
fuzzy_match = data.get("fuzzy_match")
mediaid = data.get("mediaid")
rss_sites = data.get("rss_sites")
search_sites = data.get("search_sites")
over_edition = data.get("over_edition")
filter_restype = data.get("filter_restype")
filter_pix = data.get("filter_pix")
filter_team = data.get("filter_team")
filter_rule = data.get("filter_rule")
save_path = data.get("save_path")
download_setting = data.get("download_setting")
total_ep = data.get("total_ep")
current_ep = data.get("current_ep")
rssid = data.get("rssid")
page = data.get("page")
mtype = MediaType.MOVIE if data.get(
"type") in MovieTypes else MediaType.TV
media_info = None
if isinstance(season, list):
code = 0
msg = ""
for sea in season:
code, msg, media_info = _subscribe.add_rss_subscribe(mtype=mtype,
name=name,
year=year,
keyword=keyword,
season=sea,
fuzzy_match=fuzzy_match,
mediaid=mediaid,
rss_sites=rss_sites,
search_sites=search_sites,
over_edition=over_edition,
filter_restype=filter_restype,
filter_pix=filter_pix,
filter_team=filter_team,
filter_rule=filter_rule,
save_path=save_path,
download_setting=download_setting,
rssid=rssid)
if code != 0:
break
else:
code, msg, media_info = _subscribe.add_rss_subscribe(mtype=mtype,
name=name,
year=year,
keyword=keyword,
season=season,
fuzzy_match=fuzzy_match,
mediaid=mediaid,
rss_sites=rss_sites,
search_sites=search_sites,
over_edition=over_edition,
filter_restype=filter_restype,
filter_pix=filter_pix,
filter_team=filter_team,
filter_rule=filter_rule,
save_path=save_path,
download_setting=download_setting,
total_ep=total_ep,
current_ep=current_ep,
rssid=rssid)
if not rssid and media_info:
if mtype == MediaType.MOVIE:
rssid = self.dbhelper.get_rss_movie_id(
title=name, tmdbid=media_info.tmdb_id)
else:
rssid = self.dbhelper.get_rss_tv_id(
title=name, tmdbid=media_info.tmdb_id)
return {"code": code, "msg": msg, "page": page, "name": name, "rssid": rssid}
def re_identification(self, data):
"""
未识别的重新识别
"""
flag = data.get("flag")
ids = data.get("ids")
ret_flag = True
ret_msg = []
if flag == "unidentification":
for wid in ids:
paths = self.dbhelper.get_unknown_path_by_id(wid)
if paths:
path = paths[0].PATH
dest_dir = paths[0].DEST
rmt_mode = ModuleConf.get_enum_item(
RmtMode, paths[0].MODE) if paths[0].MODE else None
else:
return {"retcode": -1, "retmsg": "未查询到未识别记录"}
if not dest_dir:
dest_dir = ""
if not path:
return {"retcode": -1, "retmsg": "未识别路径有误"}
succ_flag, msg = FileTransfer().transfer_media(in_from=SyncType.MAN,
rmt_mode=rmt_mode,
in_path=path,
target_dir=dest_dir)
if succ_flag:
self.dbhelper.update_transfer_unknown_state(path)
else:
ret_flag = False
if msg not in ret_msg:
ret_msg.append(msg)
elif flag == "history":
for wid in ids:
paths = self.dbhelper.get_transfer_path_by_id(wid)
if paths:
path = os.path.join(
paths[0].SOURCE_PATH, paths[0].SOURCE_FILENAME)
dest_dir = paths[0].DEST
rmt_mode = ModuleConf.get_enum_item(
RmtMode, paths[0].MODE) if paths[0].MODE else None
else:
return {"retcode": -1, "retmsg": "未查询到转移日志记录"}
if not dest_dir:
dest_dir = ""
if not path:
return {"retcode": -1, "retmsg": "未识别路径有误"}
succ_flag, msg = FileTransfer().transfer_media(in_from=SyncType.MAN,
rmt_mode=rmt_mode,
in_path=path,
target_dir=dest_dir)
if not succ_flag:
ret_flag = False
if msg not in ret_msg:
ret_msg.append(msg)
if ret_flag:
return {"retcode": 0, "retmsg": "转移成功"}
else:
return {"retcode": 2, "retmsg": "、".join(ret_msg)}
def __media_info(self, data):
"""
查询媒体信息
"""
mediaid = data.get("id")
mtype = data.get("type")
title = data.get("title")
year = data.get("year")
page = data.get("page")
rssid = data.get("rssid")
seasons = []
link_url = ""
vote_average = 0
poster_path = ""
release_date = ""
overview = ""
# 类型
if mtype in MovieTypes:
media_type = MediaType.MOVIE
else:
media_type = MediaType.TV
# 先取订阅信息
rssid_ok = False
if rssid:
rssid = str(rssid)
if media_type == MediaType.MOVIE:
rssinfo = Subscribe().get_subscribe_movies(rid=rssid)
else:
rssinfo = Subscribe().get_subscribe_tvs(rid=rssid)
if not rssinfo:
return {
"code": 1,
"retmsg": "无法查询到订阅信息",
"rssid": rssid,
"type_str": media_type.value
}
overview = rssinfo[rssid].get("overview")
poster_path = rssinfo[rssid].get("poster")
title = rssinfo[rssid].get("name")
vote_average = rssinfo[rssid].get("vote")
year = rssinfo[rssid].get("year")
release_date = rssinfo[rssid].get("release_date")
link_url = Media().get_detail_url(mtype=media_type,
tmdbid=rssinfo[rssid].get("tmdbid"))
if overview and poster_path:
rssid_ok = True
# 订阅信息不足
if not rssid_ok:
if mediaid:
media = WebUtils.get_mediainfo_from_id(
mtype=media_type, mediaid=mediaid)
else:
media = Media().get_media_info(
title=f"{title} {year}", mtype=media_type)
if not media or not media.tmdb_info:
return {
"code": 1,
"retmsg": "无法查询到TMDB信息",
"rssid": rssid,
"type_str": media_type.value
}
if not mediaid:
mediaid = media.tmdb_id
link_url = media.get_detail_url()
overview = media.overview
poster_path = media.get_poster_image()
title = media.title
vote_average = round(float(media.vote_average or 0), 1)
year = media.year
if media_type != MediaType.MOVIE:
release_date = media.tmdb_info.get('first_air_date')
seasons = [{
"text": "第%s季" % cn2an.an2cn(season.get("season_number"), mode='low'),
"num": season.get("season_number")} for season in
Media().get_tmdb_tv_seasons(tv_info=media.tmdb_info)]
else:
release_date = media.tmdb_info.get('release_date')
# 查订阅信息
if not rssid:
if media_type == MediaType.MOVIE:
rssid = self.dbhelper.get_rss_movie_id(
title=title, tmdbid=mediaid)
else:
rssid = self.dbhelper.get_rss_tv_id(
title=title, tmdbid=mediaid)
return {
"code": 0,
"type": mtype,
"type_str": media_type.value,
"page": page,
"title": title,
"vote_average": vote_average,
"poster_path": poster_path,
"release_date": release_date,
"year": year,
"overview": overview,
"link_url": link_url,
"tmdbid": mediaid,
"rssid": rssid,
"seasons": seasons
}
@staticmethod
def __test_connection(data):
"""
测试连通性
"""
# 支持两种传入方式:命令数组或单个命令,单个命令时xx|xx模式解析为模块和类,进行动态引入
command = data.get("command")
ret = None
if command:
try:
module_obj = None
if isinstance(command, list):
for cmd_str in command:
ret = eval(cmd_str)
if not ret:
break
else:
if command.find("|") != -1:
module = command.split("|")[0]
class_name = command.split("|")[1]
module_obj = getattr(
importlib.import_module(module), class_name)()
if hasattr(module_obj, "init_config"):
module_obj.init_config()
ret = module_obj.get_status()
else:
ret = eval(command)
# 重载配置
Config().init_config()
if module_obj:
if hasattr(module_obj, "init_config"):
module_obj.init_config()
except Exception as e:
ret = None
ExceptionUtils.exception_traceback(e)
return {"code": 0 if ret else 1}
return {"code": 0}
def __user_manager(self, data):
"""
用户管理
"""
oper = data.get("oper")
name = data.get("name")
if oper == "add":
password = generate_password_hash(str(data.get("password")))
pris = data.get("pris")
if isinstance(pris, list):
pris = ",".join(pris)
ret = self.dbhelper.insert_user(name, password, pris)
else:
ret = self.dbhelper.delete_user(name)
if ret == 1 or ret:
return {"code": 0, "success": False}
return {"code": -1, "success": False, 'message': '操作失败'}
@staticmethod
def __refresh_rss(data):
"""
重新搜索RSS
"""
mtype = data.get("type")
rssid = data.get("rssid")
page = data.get("page")
if mtype == "MOV":
ThreadHelper().start_thread(Subscribe().subscribe_search_movie, (rssid,))
else:
ThreadHelper().start_thread(Subscribe().subscribe_search_tv, (rssid,))
return {"code": 0, "page": page}
@staticmethod
def get_system_message(lst_time):
messages = MessageCenter().get_system_messages(lst_time=lst_time)
if messages:
lst_time = messages[0].get("time")
return {
"code": 0,
"message": messages,
"lst_time": lst_time
}
def __refresh_message(self, data):
"""
刷新首页消息中心
"""
lst_time = data.get("lst_time")
system_msg = self.get_system_message(lst_time=lst_time)
messages = system_msg.get("message")
lst_time = system_msg.get("lst_time")
message_html = []
for message in list(reversed(messages)):
level = "bg-red" if message.get("level") == "ERROR" else ""
content = re.sub(r"#+", "
",
re.sub(r"<[^>]+>", "",
re.sub(r"
", "####", message.get("content"), flags=re.IGNORECASE)))
message_html.append(f"""