diff --git a/README.md b/README.md
index e521c1a..fe127ba 100644
--- a/README.md
+++ b/README.md
@@ -3,6 +3,10 @@
FlaskFarm용 범용 다운로드 매니저 플러그인입니다.
여러 다운로더 플러그인(YouTube, Anime 등)의 다운로드 요청을 통합 관리하고 큐(Queue)를 제공합니다.
+## v0.2.27 변경사항 (2026-01-09)
+- **자가 업데이트 기능 추가**: 설정 페이지에서 "Update" 버튼 클릭으로 Git Pull 및 플러그인 핫 리로드 지원
+- **버전 체크 API**: GitHub에서 최신 버전 정보를 가져와 업데이트 알림 표시 (1시간 캐싱)
+
## v0.2.24 변경사항 (2026-01-08)
- **Chrome 확장프로그램 추가**: YouTube에서 GDM으로 바로 다운로드 전송
- **Public API 추가**: `/public/youtube/formats`, `/public/youtube/add` (로그인 불필요)
diff --git a/info.yaml b/info.yaml
index dac9142..da693c4 100644
--- a/info.yaml
+++ b/info.yaml
@@ -1,6 +1,6 @@
title: "GDM"
package_name: gommi_downloader_manager
-version: '0.2.26'
+version: '0.2.28'
description: FlaskFarm 범용 다운로더 큐 - YouTube, 애니24, 링크애니, Anilife 지원
developer: projectdx
home: https://gitea.yommi.duckdns.org/projectdx/gommi_downloader_manager
diff --git a/mod_queue.py b/mod_queue.py
index bdf04fc..f1c2395 100644
--- a/mod_queue.py
+++ b/mod_queue.py
@@ -47,6 +47,10 @@ class ModuleQueue(PluginModuleBase):
_downloads: Dict[str, 'DownloadTask'] = {}
_queue_lock = threading.Lock()
+ # 업데이트 체크 캐싱
+ _last_update_check = 0
+ _latest_version = None
+
def __init__(self, P: Any) -> None:
from .setup import default_route_socketio_module
super(ModuleQueue, self).__init__(P, name='queue', first_menu='list')
@@ -283,6 +287,39 @@ class ModuleQueue(PluginModuleBase):
self.P.logger.error(f'YouTube format extraction error: {e}')
ret['ret'] = 'error'
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"업데이트 및 리로드 완료!
{stdout}"
+ 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:
self.P.logger.error(f'Exception:{str(e)}')
@@ -476,6 +513,96 @@ class ModuleQueue(PluginModuleBase):
for task in self._downloads.values():
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:
"""개별 다운로드 태스크"""
diff --git a/templates/gommi_downloader_manager_queue_setting.html b/templates/gommi_downloader_manager_queue_setting.html
index 65052e5..493e9cf 100644
--- a/templates/gommi_downloader_manager_queue_setting.html
+++ b/templates/gommi_downloader_manager_queue_setting.html
@@ -196,6 +196,9 @@