From 1cdf68cc597f63fa3b3a21a38c7fbf6fabe0a2fb Mon Sep 17 00:00:00 2001 From: projectdx Date: Tue, 3 Mar 2026 18:25:05 +0900 Subject: [PATCH] chore: bump 0.2.32 and apply plugin loading override --- downloader/ffmpeg_hls.py | 14 +++- downloader/http_direct.py | 3 +- downloader/ytdlp_aria2.py | 52 +++++++------ info.yaml | 2 +- mod_queue.py | 10 +-- .../gommi_downloader_manager_queue_list.html | 76 ++++++++++++++++++- ...ommi_downloader_manager_queue_setting.html | 62 +++++++++++++++ 7 files changed, 185 insertions(+), 34 deletions(-) diff --git a/downloader/ffmpeg_hls.py b/downloader/ffmpeg_hls.py index 80a93a5..0d61d55 100644 --- a/downloader/ffmpeg_hls.py +++ b/downloader/ffmpeg_hls.py @@ -42,7 +42,8 @@ class FfmpegHlsDownloader(BaseDownloader): if not filename: filename = f"download_{int(__import__('time').time())}.mp4" - filepath = os.path.join(save_path, filename) + filepath = os.path.abspath(os.path.join(save_path, filename)) + filepath = os.path.normpath(filepath) # ffmpeg 명령어 구성 ffmpeg_path = options.get('ffmpeg_path', 'ffmpeg') @@ -173,7 +174,16 @@ class FfmpegHlsDownloader(BaseDownloader): """다운로드 취소""" super().cancel() if self._process: - self._process.terminate() + try: + # [FIX] 파이프 명시적으로 닫기 + if self._process.stdout: self._process.stdout.close() + if self._process.stderr: self._process.stderr.close() + + self._process.terminate() + # 짧은 대기 후 여전히 살아있으면 kill + try: self._process.wait(timeout=1) + except: self._process.kill() + except: pass def _get_duration(self, url: str, ffprobe_path: str, headers: Dict) -> float: """ffprobe로 영상 길이 획득""" diff --git a/downloader/http_direct.py b/downloader/http_direct.py index 2b3c5b8..c39bed0 100644 --- a/downloader/http_direct.py +++ b/downloader/http_direct.py @@ -38,7 +38,8 @@ class HttpDirectDownloader(BaseDownloader): if not filename: filename = url.split('/')[-1].split('?')[0] or f"download_{int(__import__('time').time())}" - filepath = os.path.join(save_path, filename) + filepath = os.path.abspath(os.path.join(save_path, filename)) + filepath = os.path.normpath(filepath) # 헤더 설정 headers = options.get('headers', {}) diff --git a/downloader/ytdlp_aria2.py b/downloader/ytdlp_aria2.py index 009ac5d..adce00b 100644 --- a/downloader/ytdlp_aria2.py +++ b/downloader/ytdlp_aria2.py @@ -40,12 +40,14 @@ class YtdlpAria2Downloader(BaseDownloader): try: os.makedirs(save_path, exist_ok=True) - # 출력 템플릿 - if filename: - output_template = os.path.normpath(os.path.join(save_path, filename)) - else: - output_template = os.path.normpath(os.path.join(save_path, '%(title)s.%(ext)s')) + # 출력 템플릿 (outtmpl 옵션 우선 처리) + raw_outtmpl = options.get('outtmpl') or filename or '%(title)s.%(ext)s' + # 경로와 템플릿 결합 후 정규화 + output_template = os.path.abspath(os.path.join(save_path, raw_outtmpl)) + # 윈도우/리눅스 구분 없이 중복 슬래시 제거 및 절대 경로 확보 + output_template = os.path.normpath(output_template) + # yt-dlp 명령어 구성 cmd = [ 'yt-dlp', @@ -58,6 +60,10 @@ class YtdlpAria2Downloader(BaseDownloader): cmd.extend(['--print', 'before_dl:GDM_FIX:title:%(title)s']) cmd.extend(['--print', 'before_dl:GDM_FIX:thumb:%(thumbnail)s']) + # 속도 제한 설정 + max_rate = P.ModelSetting.get('max_download_rate') + rate_limited = bool(max_rate and max_rate != '0') + # aria2c 사용 (설치되어 있으면) aria2c_path = options.get('aria2c_path', 'aria2c') connections = options.get('connections', 4) @@ -65,21 +71,18 @@ class YtdlpAria2Downloader(BaseDownloader): if self._check_aria2c(aria2c_path): cmd.extend(['--external-downloader', aria2c_path]) # aria2c 설정: -x=연결수, -s=분할수, -j=병렬, -k=조각크기, --console-log-level=notice로 진행률 출력 - cmd.extend(['--external-downloader-args', f'aria2c:-x{connections} -s{connections} -j{connections} -k1M --summary-interval=1 --console-log-level=notice']) + aria2_args = f'aria2c:-x{connections} -s{connections} -j{connections} -k1M --summary-interval=1 --console-log-level=notice' + if rate_limited: + aria2_args = f'{aria2_args} --max-download-limit={max_rate}' + cmd.extend(['--external-downloader-args', aria2_args]) logger.info(f'[GDM] Using aria2c for multi-threaded download (connections: {connections})') # 진행률 템플릿 추가 (yt-dlp native downloader) cmd.extend(['--progress-template', 'download:GDM_PROGRESS:%(progress._percent_str)s:%(progress._speed_str)s:%(progress._eta_str)s']) - # 속도 제한 설정 - max_rate = P.ModelSetting.get('max_download_rate') - if max_rate == '0': - max_rate_arg = '' - log_rate_msg = '무제한' - else: - max_rate_arg = f'--max-download-limit={max_rate}' - log_rate_msg = max_rate - cmd.extend(['--limit-rate', max_rate]) # Native downloader limit + # yt-dlp native downloader 제한 (external-downloader 미사용/보조 경로) + if rate_limited: + cmd.extend(['--limit-rate', max_rate]) # 포맷 선택 format_spec = options.get('format') @@ -147,14 +150,6 @@ class YtdlpAria2Downloader(BaseDownloader): if options.get('add_metadata'): cmd.append('--add-metadata') - if options.get('outtmpl'): - # outtmpl 옵션이 별도로 전달된 경우 덮어쓰기 (output_template는 -o가 이미 차지함) - # 하지만 yt-dlp -o 옵션이 곧 outtmpl임. - # 파일명 템플릿 문제 해결을 위해 filename 인자 대신 outtmpl 옵션을 우선시 - # 위에서 -o output_template를 이미 넣었으므로, 여기서 다시 넣으면 중복될 수 있음. - # 따라서 로직 수정: filename 없이 outtmpl만 온 경우 - pass - # URL 추가 cmd.append(url) @@ -314,7 +309,16 @@ class YtdlpAria2Downloader(BaseDownloader): """다운로드 취소""" super().cancel() if self._process: - self._process.terminate() + try: + # [FIX] 파이프 명시적으로 닫기 + if self._process.stdout: self._process.stdout.close() + if self._process.stderr: self._process.stderr.close() + + self._process.terminate() + # 짧은 대기 후 여전히 살아있으면 kill + try: self._process.wait(timeout=1) + except: self._process.kill() + except: pass def _check_aria2c(self, aria2c_path: str) -> bool: """aria2c 설치 확인""" diff --git a/info.yaml b/info.yaml index 4d63699..b753494 100644 --- a/info.yaml +++ b/info.yaml @@ -1,6 +1,6 @@ title: "GDM" package_name: gommi_downloader_manager -version: '0.2.31' +version: '0.2.33' 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 65e2c34..844bbe8 100644 --- a/mod_queue.py +++ b/mod_queue.py @@ -297,13 +297,13 @@ class ModuleQueue(PluginModuleBase): # 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() + result = subprocess.run(cmd, capture_output=True, text=True, timeout=60) - if process.returncode != 0: - raise Exception(f"Git pull 실패: {stderr}") + if result.returncode != 0: + raise Exception(f"Git pull 실패: {result.stderr}") - self.P.logger.info(f"Git pull 결과: {stdout}") + self.P.logger.info(f"Git pull 결과: {result.stdout}") + stdout = result.stdout # 2. 모듈 리로드 (Hot-Reload) self.reload_plugin() diff --git a/templates/gommi_downloader_manager_queue_list.html b/templates/gommi_downloader_manager_queue_list.html index fcb30e3..f1b927a 100644 --- a/templates/gommi_downloader_manager_queue_list.html +++ b/templates/gommi_downloader_manager_queue_list.html @@ -46,7 +46,63 @@ background-color: #3e5770 !important; } - #loading { display: none !important; } + /* Plugin-owned loading override (independent from Flaskfarm default loader assets) */ + #loading, + #modal_loading { + display: none; + position: fixed; + inset: 0; + z-index: 3000010 !important; + background: rgba(10, 22, 36, 0.46); + backdrop-filter: blur(2px); + } + + #loading img, + #modal_loading img { + display: none !important; + } + + #loading::before, + #modal_loading::before { + content: ""; + position: absolute; + left: 50%; + top: 50%; + width: 58px; + height: 58px; + margin-left: -29px; + margin-top: -29px; + border-radius: 50%; + border: 3px solid rgba(255, 255, 255, 0.24); + border-top-color: var(--accent-primary); + border-right-color: var(--accent-secondary); + animation: gdm-loader-spin 0.9s linear infinite; + } + + #loading::after, + #modal_loading::after { + content: "LOADING"; + position: absolute; + left: 50%; + top: calc(50% + 44px); + transform: translateX(-50%); + color: var(--text-main); + font-size: 11px; + font-weight: 700; + letter-spacing: 0.12em; + } + + #loading[style*="display: block"], + #loading[style*="display: inline-block"], + #modal_loading[style*="display: block"], + #modal_loading[style*="display: inline-block"] { + display: block !important; + } + + @keyframes gdm-loader-spin { + from { transform: rotate(0deg); } + to { transform: rotate(360deg); } + } #gommi_download_manager_queue_list { font-family: var(--font-sans); @@ -143,6 +199,24 @@ } } + /* Mobile 5px Padding - Maximum Screen Usage */ + @media (max-width: 768px) { + .container, .container-fluid, #main_container, #gommi_download_manager_queue_list { + padding-left: 5px !important; + padding-right: 5px !important; + margin-left: 0 !important; + margin-right: 0 !important; + } + .row { + margin-left: 0 !important; + margin-right: 0 !important; + } + [class*="col-"] { + padding-left: 4px !important; + padding-right: 4px !important; + } + } + .page-title { font-size: 1.75rem; font-weight: 700; diff --git a/templates/gommi_downloader_manager_queue_setting.html b/templates/gommi_downloader_manager_queue_setting.html index 493e9cf..ff277a7 100644 --- a/templates/gommi_downloader_manager_queue_setting.html +++ b/templates/gommi_downloader_manager_queue_setting.html @@ -23,6 +23,64 @@ --font-sans: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; } + /* Plugin-owned loading override (independent from Flaskfarm default loader assets) */ + #loading, + #modal_loading { + display: none; + position: fixed; + inset: 0; + z-index: 3000010 !important; + background: rgba(7, 16, 35, 0.46); + backdrop-filter: blur(2px); + } + + #loading img, + #modal_loading img { + display: none !important; + } + + #loading::before, + #modal_loading::before { + content: ""; + position: absolute; + left: 50%; + top: 50%; + width: 58px; + height: 58px; + margin-left: -29px; + margin-top: -29px; + border-radius: 50%; + border: 3px solid rgba(255, 255, 255, 0.24); + border-top-color: var(--accent-primary); + border-right-color: var(--accent-secondary); + animation: gdm-loader-spin 0.9s linear infinite; + } + + #loading::after, + #modal_loading::after { + content: "LOADING"; + position: absolute; + left: 50%; + top: calc(50% + 44px); + transform: translateX(-50%); + color: var(--text-main); + font-size: 11px; + font-weight: 700; + letter-spacing: 0.12em; + } + + #loading[style*="display: block"], + #loading[style*="display: inline-block"], + #modal_loading[style*="display: block"], + #modal_loading[style*="display: inline-block"] { + display: block !important; + } + + @keyframes gdm-loader-spin { + from { transform: rotate(0deg); } + to { transform: rotate(360deg); } + } + #gommi_download_manager_queue_setting { font-family: var(--font-sans); color: var(--text-main); @@ -237,6 +295,10 @@ + + + +