v0.2.27: Add self-update feature with hot reload

This commit is contained in:
2026-01-09 22:18:36 +09:00
parent 7beb536ca0
commit c38a7ae39b
4 changed files with 165 additions and 1 deletions

View File

@@ -3,6 +3,10 @@
FlaskFarm용 범용 다운로드 매니저 플러그인입니다. FlaskFarm용 범용 다운로드 매니저 플러그인입니다.
여러 다운로더 플러그인(YouTube, Anime 등)의 다운로드 요청을 통합 관리하고 큐(Queue)를 제공합니다. 여러 다운로더 플러그인(YouTube, Anime 등)의 다운로드 요청을 통합 관리하고 큐(Queue)를 제공합니다.
## v0.2.27 변경사항 (2026-01-09)
- **자가 업데이트 기능 추가**: 설정 페이지에서 "Update" 버튼 클릭으로 Git Pull 및 플러그인 핫 리로드 지원
- **버전 체크 API**: GitHub에서 최신 버전 정보를 가져와 업데이트 알림 표시 (1시간 캐싱)
## v0.2.24 변경사항 (2026-01-08) ## v0.2.24 변경사항 (2026-01-08)
- **Chrome 확장프로그램 추가**: YouTube에서 GDM으로 바로 다운로드 전송 - **Chrome 확장프로그램 추가**: YouTube에서 GDM으로 바로 다운로드 전송
- **Public API 추가**: `/public/youtube/formats`, `/public/youtube/add` (로그인 불필요) - **Public API 추가**: `/public/youtube/formats`, `/public/youtube/add` (로그인 불필요)

View File

@@ -1,6 +1,6 @@
title: "GDM" title: "GDM"
package_name: gommi_downloader_manager package_name: gommi_downloader_manager
version: '0.2.26' version: '0.2.28'
description: FlaskFarm 범용 다운로더 큐 - YouTube, 애니24, 링크애니, Anilife 지원 description: FlaskFarm 범용 다운로더 큐 - YouTube, 애니24, 링크애니, Anilife 지원
developer: projectdx developer: projectdx
home: https://gitea.yommi.duckdns.org/projectdx/gommi_downloader_manager home: https://gitea.yommi.duckdns.org/projectdx/gommi_downloader_manager

View File

@@ -47,6 +47,10 @@ class ModuleQueue(PluginModuleBase):
_downloads: Dict[str, 'DownloadTask'] = {} _downloads: Dict[str, 'DownloadTask'] = {}
_queue_lock = threading.Lock() _queue_lock = threading.Lock()
# 업데이트 체크 캐싱
_last_update_check = 0
_latest_version = None
def __init__(self, P: Any) -> None: def __init__(self, P: Any) -> None:
from .setup import default_route_socketio_module from .setup import default_route_socketio_module
super(ModuleQueue, self).__init__(P, name='queue', first_menu='list') super(ModuleQueue, self).__init__(P, name='queue', first_menu='list')
@@ -284,6 +288,39 @@ class ModuleQueue(PluginModuleBase):
ret['ret'] = 'error' ret['ret'] = 'error'
ret['msg'] = str(e) ret['msg'] = str(e)
elif command == 'self_update':
# 자가 업데이트 (Git Pull) 및 모듈 리로드
try:
import subprocess
plugin_path = os.path.dirname(__file__)
self.P.logger.info(f"GDM 자가 업데이트 시작: {plugin_path}")
# 1. 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}")
# 2. 모듈 리로드 (Hot-Reload)
self.reload_plugin()
ret['msg'] = f"업데이트 및 리로드 완료!<br><pre>{stdout}</pre>"
ret['data'] = stdout
except Exception as e:
self.P.logger.error(f"GDM 자가 업데이트 중 오류: {str(e)}")
self.P.logger.error(traceback.format_exc())
ret['ret'] = 'danger'
ret['msg'] = f"업데이트 실패: {str(e)}"
elif command == 'check_update':
# 업데이트 확인
force = req.form.get('force') == 'true'
ret['data'] = self.get_update_info(force=force)
except Exception as e: except Exception as e:
self.P.logger.error(f'Exception:{str(e)}') self.P.logger.error(f'Exception:{str(e)}')
self.P.logger.error(traceback.format_exc()) self.P.logger.error(traceback.format_exc())
@@ -476,6 +513,96 @@ class ModuleQueue(PluginModuleBase):
for task in self._downloads.values(): for task in self._downloads.values():
task.cancel() task.cancel()
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 ModuleQueue._latest_version and (now - ModuleQueue._last_update_check < 3600):
return {
'current': current_version,
'latest': ModuleQueue._latest_version,
'has_update': self._is_newer(ModuleQueue._latest_version, current_version)
}
try:
url = "https://raw.githubusercontent.com/projectdx75/gommi_downloader_manager/master/info.yaml"
res = requests.get(url, timeout=5)
if res.status_code == 200:
import yaml
data = yaml.safe_load(res.text)
ModuleQueue._latest_version = str(data.get('version', ''))
ModuleQueue._last_update_check = now
return {
'current': current_version,
'latest': ModuleQueue._latest_version,
'has_update': self._is_newer(ModuleQueue._latest_version, current_version)
}
except Exception as e:
self.P.logger.error(f"Update check failed: {e}")
return {
'current': current_version,
'latest': ModuleQueue._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}")
# 1. 관련 모듈 찾기 및 리로드
modules_to_reload = []
for module_name in list(sys.modules.keys()):
if module_name.startswith(package_name):
modules_to_reload.append(module_name)
# 의존성 역순으로 정렬 (깊은 모듈 먼저)
modules_to_reload.sort(key=lambda x: x.count('.'), reverse=True)
for module_name in modules_to_reload:
try:
module = sys.modules[module_name]
importlib.reload(module)
self.P.logger.debug(f"Reloaded: {module_name}")
except Exception as e:
self.P.logger.warning(f"Failed to reload {module_name}: {e}")
self.P.logger.info(f"플러그인 모듈 [{package_name}] 리로드 완료")
return True
except Exception as e:
self.P.logger.error(f"모듈 리로드 중 실패: {str(e)}")
self.P.logger.error(traceback.format_exc())
return False
class DownloadTask: class DownloadTask:
"""개별 다운로드 태스크""" """개별 다운로드 태스크"""

View File

@@ -196,6 +196,9 @@
<div class="page-header"> <div class="page-header">
<h1 class="page-title">GDM Settings</h1> <h1 class="page-title">GDM Settings</h1>
<div class="header-actions"> <div class="header-actions">
<button type="button" class="btn-premium" id="btn-self-update" style="background: var(--accent-secondary); border-color: var(--accent-secondary);">
<i class="fa fa-refresh"></i> Update
</button>
<button type="button" class="btn-premium" id="globalSettingSaveBtn"> <button type="button" class="btn-premium" id="globalSettingSaveBtn">
<i class="fa fa-save"></i> Save Changes <i class="fa fa-save"></i> Save Changes
</button> </button>
@@ -317,5 +320,35 @@
} }
}); });
}); });
// Self Update
$("body").on('click', '#btn-self-update', function(e){
e.preventDefault();
if (!confirm('최신 코드를 다운로드하고 플러그인을 리로드하시겠습니까?')) return;
var btn = $(this);
var originalText = btn.html();
btn.prop('disabled', true).html('<i class="fa fa-spinner fa-spin"></i> 업데이트 중...');
$.ajax({
url: `/${package_name}/ajax/${sub}/self_update`,
type: "POST",
dataType: "json",
success: function(ret) {
if (ret.ret == 'success') {
$.notify('<strong>업데이트 완료!</strong> 페이지를 새로고침합니다.', {type:'success'});
setTimeout(function() { location.reload(); }, 1500);
} else {
$.notify('<strong>업데이트 실패: ' + ret.msg + '</strong>', {type:'danger'});
}
},
error: function() {
$.notify('<strong>업데이트 중 오류 발생</strong>', {type:'danger'});
},
complete: function() {
btn.prop('disabled', false).html(originalText);
}
});
});
</script> </script>
{% endblock %} {% endblock %}