356 lines
16 KiB
Python
356 lines
16 KiB
Python
from flask import render_template, request, jsonify
|
|
from plugin import PluginModuleBase
|
|
import framework
|
|
import os, traceback, time, json
|
|
from datetime import datetime
|
|
|
|
class AnimeModuleBase(PluginModuleBase):
|
|
# 업데이트 체크 캐싱 (클래스 레벨)
|
|
_last_update_check = 0
|
|
_latest_version = None
|
|
|
|
def __init__(self, P, setup_default=None, **kwargs):
|
|
super(AnimeModuleBase, self).__init__(P, **kwargs)
|
|
self.P = P # Ensure P is available via self.P
|
|
if setup_default:
|
|
self.init_module_settings(setup_default)
|
|
|
|
def init_module_settings(self, setup_default):
|
|
try:
|
|
for key, value in setup_default.items():
|
|
if self.P.ModelSetting.get(key) is None:
|
|
self.P.ModelSetting.set(key, value)
|
|
except Exception as e:
|
|
self.P.logger.error(f"Settings Init Error: {e}")
|
|
self.P.logger.error(traceback.format_exc())
|
|
|
|
def process_menu(self, sub, req):
|
|
from framework import F
|
|
try:
|
|
# sub can be None from first_menu
|
|
if sub is None:
|
|
sub = self.first_menu
|
|
|
|
arg = self.P.ModelSetting.to_dict() if self.P.ModelSetting is not None else {}
|
|
arg["sub"] = self.name
|
|
arg["sub2"] = sub
|
|
arg["package_name"] = self.P.package_name
|
|
arg["module_name"] = self.name
|
|
arg['path_data'] = F.config['path_data']
|
|
|
|
# job_id for scheduler
|
|
job_id = f"{self.P.package_name}_{self.name}"
|
|
arg['is_include'] = F.scheduler.is_include(job_id)
|
|
arg['is_running'] = F.scheduler.is_running(job_id)
|
|
# Legacy compatibility for some templates
|
|
arg["scheduler"] = str(arg['is_include'])
|
|
|
|
code = req.args.get("content_code") or req.args.get("code")
|
|
if sub == "request" and code is not None:
|
|
arg[f"{self.name}_current_code"] = code
|
|
|
|
# Check template existence
|
|
template_name = f"{self.P.package_name}_{self.name}_{sub}.html"
|
|
return render_template(template_name, arg=arg)
|
|
|
|
except Exception as e:
|
|
self.P.logger.error(f"Menu Error: {e}")
|
|
self.P.logger.error(traceback.format_exc())
|
|
return render_template("sample.html", title=f"Error: {e}")
|
|
|
|
def process_ajax(self, sub, req):
|
|
try:
|
|
if sub == 'setting_save':
|
|
ret = self.P.ModelSetting.setting_save(req)
|
|
return jsonify(ret)
|
|
|
|
elif sub == 'scheduler':
|
|
go = req.form['scheduler']
|
|
job_id = f"{self.P.package_name}_{self.name}"
|
|
if go == 'true':
|
|
framework.scheduler.manage_process(job_id, 'sched', {'sub': self.name})
|
|
else:
|
|
framework.scheduler.manage_process(job_id, 'cancel', None)
|
|
return jsonify(go)
|
|
|
|
elif sub in ['immediately_execute', 'one_execute']:
|
|
job_id = f"{self.P.package_name}_{self.name}"
|
|
framework.scheduler.manage_process(job_id, 'execute', {'sub': self.name})
|
|
return jsonify({'ret': 'success', 'msg': '작업을 시작합니다.'})
|
|
|
|
elif sub == 'reset_db':
|
|
return jsonify(self.reset_db())
|
|
|
|
elif sub == 'browse_dir':
|
|
# Folder Browser Logic (Matches UI expectation)
|
|
path = req.form.get('path')
|
|
if not path:
|
|
path = '/'
|
|
|
|
current_path = os.path.abspath(path)
|
|
if not os.path.exists(current_path):
|
|
current_path = '/'
|
|
|
|
parent_path = os.path.dirname(current_path)
|
|
if parent_path == current_path:
|
|
parent_path = None
|
|
|
|
dirs = []
|
|
try:
|
|
for name in os.listdir(current_path):
|
|
full_path = os.path.join(current_path, name)
|
|
if os.path.isdir(full_path) and not name.startswith('.'):
|
|
dirs.append({'name': name, 'path': full_path})
|
|
|
|
dirs.sort(key=lambda x: x['name'])
|
|
return jsonify({'ret': 'success', 'directories': dirs, 'current_path': current_path, 'parent_path': parent_path})
|
|
except Exception as e:
|
|
return jsonify({'ret': 'fail', 'error': str(e)})
|
|
|
|
elif sub == 'queue_command':
|
|
cmd = request.form.get('command')
|
|
if not cmd:
|
|
cmd = request.args.get('command')
|
|
entity_id_str = request.form.get('entity_id') or request.args.get('entity_id')
|
|
entity_id = int(entity_id_str) if entity_id_str else -1
|
|
ret = self.queue.command(cmd, entity_id) if self.queue else {'ret': 'fail', 'log': 'No queue'}
|
|
return jsonify(ret)
|
|
|
|
elif sub == 'entity_list':
|
|
return jsonify(self.queue.get_entity_list())
|
|
|
|
elif sub == 'add_whitelist':
|
|
# Common whitelist addition
|
|
data = req.get_json() if req.is_json else req.form
|
|
data_code = data.get('data_code')
|
|
if hasattr(self, 'add_whitelist'):
|
|
return self.add_whitelist(data_code)
|
|
else:
|
|
return jsonify({'ret': False, 'log': 'Not implemented'})
|
|
|
|
elif sub == 'command':
|
|
command = request.form.get('command') or request.args.get('command')
|
|
arg1 = request.form.get('arg1') or request.args.get('arg1')
|
|
arg2 = request.form.get('arg2') or request.args.get('arg2')
|
|
arg3 = request.form.get('arg3') or request.args.get('arg3')
|
|
return self.process_command(command, arg1, arg2, arg3, req)
|
|
|
|
elif sub == 'self_update':
|
|
# 자가 업데이트 (Git Pull) 및 모듈 리로드
|
|
try:
|
|
import subprocess
|
|
plugin_path = os.path.dirname(__file__)
|
|
self.P.logger.info(f"애니 다운로더 자가 업데이트 시작: {plugin_path}")
|
|
|
|
# 먼저 변경될 파일 목록 확인 (model 파일 변경 감지)
|
|
diff_cmd = ['git', '-C', plugin_path, 'diff', '--name-only', 'HEAD', 'origin/main']
|
|
subprocess.run(['git', '-C', plugin_path, 'fetch'], capture_output=True) # fetch first
|
|
diff_result = subprocess.run(diff_cmd, capture_output=True, text=True)
|
|
changed_files = diff_result.stdout.strip().split('\n') if diff_result.stdout.strip() else []
|
|
|
|
# 모델 파일 변경 여부 확인
|
|
model_patterns = ['model', 'db', 'migration']
|
|
needs_restart = any(
|
|
any(pattern in f.lower() for pattern in model_patterns)
|
|
for f in changed_files if f
|
|
)
|
|
|
|
# Git Pull 실행
|
|
cmd = ['git', '-C', plugin_path, 'pull']
|
|
process = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
|
|
stdout, stderr = process.communicate()
|
|
|
|
if process.returncode != 0:
|
|
raise Exception(f"Git pull 실패: {stderr}")
|
|
|
|
self.P.logger.info(f"Git pull 결과: {stdout}")
|
|
|
|
# 모델 변경 없으면 리로드 시도
|
|
if not needs_restart:
|
|
self.reload_plugin()
|
|
msg = f"업데이트 완료! 새로고침하세요.<br><pre>{stdout}</pre>"
|
|
else:
|
|
self.P.logger.warning("모델 파일 변경 감지 - 서버 재시작 필요")
|
|
msg = f"<strong>모델 변경 감지!</strong> 서버 재시작이 필요합니다.<br><pre>{stdout}</pre>"
|
|
|
|
return jsonify({
|
|
'ret': 'success',
|
|
'msg': msg,
|
|
'data': stdout,
|
|
'needs_restart': needs_restart
|
|
})
|
|
except Exception as e:
|
|
self.P.logger.error(f"자가 업데이트 중 오류: {str(e)}")
|
|
self.P.logger.error(traceback.format_exc())
|
|
return jsonify({'ret': 'danger', 'msg': f"업데이트 실패: {str(e)}"})
|
|
|
|
elif sub == 'check_update':
|
|
force = req.form.get('force') == 'true'
|
|
return jsonify({'ret': 'success', 'data': self.get_update_info(force=force)})
|
|
|
|
return jsonify({'ret': 'fail', 'log': f"Unknown sub: {sub}"})
|
|
|
|
except Exception as e:
|
|
self.P.logger.error(f"AJAX Error: {e}")
|
|
self.P.logger.error(traceback.format_exc())
|
|
return jsonify({'ret': 'fail', 'log': str(e)})
|
|
|
|
def process_command(self, command, arg1, arg2, arg3, req):
|
|
try:
|
|
if not command:
|
|
return jsonify({"ret": "fail", "log": "No command specified"})
|
|
|
|
if command == "list":
|
|
ret = self.queue.get_entity_list() if self.queue else []
|
|
return jsonify(ret)
|
|
elif command == "stop":
|
|
entity_id = int(arg1) if arg1 else -1
|
|
result = self.queue.command("cancel", entity_id) if self.queue else {"ret": "error"}
|
|
return jsonify(result)
|
|
elif command == "remove":
|
|
entity_id = int(arg1) if arg1 else -1
|
|
result = self.queue.command("remove", entity_id) if self.queue else {"ret": "error"}
|
|
return jsonify(result)
|
|
elif command in ["reset", "delete_completed"]:
|
|
result = self.queue.command(command, 0) if self.queue else {"ret": "error"}
|
|
return jsonify(result)
|
|
|
|
return jsonify({"ret": "fail", "log": f"Unknown command: {command}"})
|
|
except Exception as e:
|
|
self.P.logger.error(f"process_command Error: {e}")
|
|
self.P.logger.error(traceback.format_exc())
|
|
return jsonify({'ret': 'fail', 'log': str(e)})
|
|
|
|
def socketio_callback(self, refresh_type, data):
|
|
"""
|
|
socketio를 통해 클라이언트에 상태 업데이트 전송
|
|
refresh_type: 'add', 'status', 'last', 'list_refresh' 등
|
|
data: entity.as_dict() 데이터 또는 리스트 갱신용 빈 문자열
|
|
"""
|
|
try:
|
|
from framework import socketio
|
|
|
|
# /package_name/module_name/queue 네임스페이스로 emit
|
|
namespace = f"/{self.P.package_name}/{self.name}/queue"
|
|
|
|
# 큐 페이지 소켓에 직접 emit
|
|
socketio.emit(refresh_type, data, namespace=namespace)
|
|
|
|
except Exception as e:
|
|
self.P.logger.error(f"socketio_callback error: {e}")
|
|
|
|
def reset_db(self):
|
|
try:
|
|
# Drop tables logic or delete all rows
|
|
# This requires access to specific Models.
|
|
# Child class should implement or pass Models?
|
|
# Or use self.web_list_model if set
|
|
if self.web_list_model:
|
|
framework.db.session.query(self.web_list_model).delete()
|
|
|
|
# Delete queue items?
|
|
# ...
|
|
framework.db.session.commit()
|
|
return {'ret': 'success', 'msg': 'DB가 초기화되었습니다.'}
|
|
except Exception as e:
|
|
return {'ret': 'fail', 'msg': str(e)}
|
|
|
|
def get_update_info(self, force=False):
|
|
"""GitHub에서 최신 버전 정보 가져오기 (캐싱 활용)"""
|
|
import requests
|
|
now = time.time()
|
|
|
|
# 실제 로컬 파일에서 현재 버전 읽기
|
|
current_version = self.P.plugin_info.get('version', '0.0.0')
|
|
try:
|
|
info_path = os.path.join(os.path.dirname(__file__), 'info.yaml')
|
|
if os.path.exists(info_path):
|
|
import yaml
|
|
with open(info_path, 'r', encoding='utf-8') as f:
|
|
local_info = yaml.safe_load(f)
|
|
current_version = str(local_info.get('version', current_version))
|
|
except: pass
|
|
|
|
# 1시간마다 체크 (force=True면 즉시)
|
|
if not force and AnimeModuleBase._latest_version and (now - AnimeModuleBase._last_update_check < 3600):
|
|
return {
|
|
'current': current_version,
|
|
'latest': AnimeModuleBase._latest_version,
|
|
'has_update': self._is_newer(AnimeModuleBase._latest_version, current_version)
|
|
}
|
|
|
|
try:
|
|
url = "https://raw.githubusercontent.com/projectdx75/anime_downloader/master/info.yaml"
|
|
res = requests.get(url, timeout=5)
|
|
if res.status_code == 200:
|
|
import yaml
|
|
data = yaml.safe_load(res.text)
|
|
AnimeModuleBase._latest_version = str(data.get('version', ''))
|
|
AnimeModuleBase._last_update_check = now
|
|
|
|
return {
|
|
'current': current_version,
|
|
'latest': AnimeModuleBase._latest_version,
|
|
'has_update': self._is_newer(AnimeModuleBase._latest_version, current_version)
|
|
}
|
|
except Exception as e:
|
|
self.P.logger.error(f"Update check failed: {e}")
|
|
|
|
return {
|
|
'current': current_version,
|
|
'latest': AnimeModuleBase._latest_version or current_version,
|
|
'has_update': False
|
|
}
|
|
|
|
def _is_newer(self, latest, current):
|
|
"""버전 비교 (0.7.8 vs 0.7.7)"""
|
|
if not latest or not current: return False
|
|
try:
|
|
l_parts = [int(p) for p in latest.split('.')]
|
|
c_parts = [int(p) for p in current.split('.')]
|
|
return l_parts > c_parts
|
|
except:
|
|
return latest != current
|
|
|
|
def reload_plugin(self):
|
|
"""플러그인 모듈 핫 리로드"""
|
|
import sys
|
|
import importlib
|
|
|
|
try:
|
|
package_name = self.P.package_name
|
|
self.P.logger.info(f"플러그인 리로드 시작: {package_name}")
|
|
|
|
# 리로드에서 제외할 패턴 (모델/DB 관련 - SQLAlchemy 충돌 방지)
|
|
skip_patterns = ['model', 'db', 'migration', 'setup', 'create_plugin']
|
|
|
|
# 관련 모듈 찾기 및 리로드
|
|
modules_to_reload = []
|
|
for module_name in list(sys.modules.keys()):
|
|
if module_name.startswith(package_name):
|
|
# 모델 관련 모듈은 건너뛰기
|
|
should_skip = any(pattern in module_name.lower() for pattern in skip_patterns)
|
|
if not should_skip:
|
|
modules_to_reload.append(module_name)
|
|
|
|
# 의존성 역순으로 정렬 (깊은 모듈 먼저)
|
|
modules_to_reload.sort(key=lambda x: x.count('.'), reverse=True)
|
|
|
|
reloaded_count = 0
|
|
for module_name in modules_to_reload:
|
|
try:
|
|
module = sys.modules[module_name]
|
|
importlib.reload(module)
|
|
self.P.logger.debug(f"Reloaded: {module_name}")
|
|
reloaded_count += 1
|
|
except Exception as e:
|
|
self.P.logger.warning(f"Skip reload {module_name}: {e}")
|
|
|
|
self.P.logger.info(f"플러그인 [{package_name}] 리로드 완료: {reloaded_count}개 모듈")
|
|
self.P.logger.info("템플릿/정적 파일은 새로고침 시 자동 적용됩니다.")
|
|
return True
|
|
except Exception as e:
|
|
self.P.logger.error(f"모듈 리로드 중 실패: {str(e)}")
|
|
self.P.logger.error(traceback.format_exc())
|
|
return False
|