From d6819447d76c7109807dfafe54777cf9501b26d5 Mon Sep 17 00:00:00 2001 From: projectdx Date: Wed, 7 Jan 2026 22:35:58 +0900 Subject: [PATCH] v0.2.14: FFmpeg HLS resilience and aria2c multi-threading fixes --- README.md | 5 + downloader/ffmpeg_hls.py | 9 + downloader/ytdlp_aria2.py | 5 + info.yaml | 2 +- mod_queue.py | 41 +- .../gommi_downloader_manager_queue_list.html | 466 +++++++++++++++--- 6 files changed, 461 insertions(+), 67 deletions(-) diff --git a/README.md b/README.md index 634a1b8..f1ac7a7 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,11 @@ FlaskFarm용 범용 다운로드 매니저 플러그인입니다. 여러 다운로더 플러그인(YouTube, Anime 등)의 다운로드 요청을 통합 관리하고 큐(Queue)를 제공합니다. +## v0.2.14 변경사항 (2026-01-07) +- **FFmpeg HLS 안정화**: Ohli24 분산 호스트 환경 대응을 위해 `-http_persistent 0` 및 재연결 옵션(`-reconnect`) 추가. +- **aria2c 멀티쓰레드 활성화**: `yt-dlp`에서 `aria2c`를 외부 다운로더로 정상 호출하도록 수정하여 고속 분할 다운로드 지원. +- **GDM 위임 로직 버그 수정**: `DownloadTask` 객체의 `as_dict` 누락 및 메타데이터 초기화 버그 수정 (이전 버전 패치 포함). + ## v0.2.12 변경사항 (2026-01-07) - **안정성 개선**: `ffmpeg_hls` 다운로더에서 URL이 비어있을 경우 로그 기록 시 발생하는 `TypeError` 수정. diff --git a/downloader/ffmpeg_hls.py b/downloader/ffmpeg_hls.py index c0186b1..64f05e5 100644 --- a/downloader/ffmpeg_hls.py +++ b/downloader/ffmpeg_hls.py @@ -80,6 +80,15 @@ class FfmpegHlsDownloader(BaseDownloader): except Exception as ce: logger.error(f"Failed to read cookies_file: {ce}") + # 입력 전 설정 (Reconnection & HTTP persistence fix) + cmd.extend([ + '-reconnect', '1', + '-reconnect_at_eof', '1', + '-reconnect_streamed', '1', + '-reconnect_delay_max', '5', + '-http_persistent', '0' + ]) + # 입력 URL cmd.extend(['-i', url]) diff --git a/downloader/ytdlp_aria2.py b/downloader/ytdlp_aria2.py index 304ab67..3943040 100644 --- a/downloader/ytdlp_aria2.py +++ b/downloader/ytdlp_aria2.py @@ -62,6 +62,11 @@ class YtdlpAria2Downloader(BaseDownloader): aria2c_path = options.get('aria2c_path', 'aria2c') connections = options.get('connections', 4) + if self._check_aria2c(aria2c_path): + cmd.extend(['--external-downloader', aria2c_path]) + cmd.extend(['--external-downloader-args', f'aria2c:-x{connections} -s{connections} -j{connections} -k1M']) + logger.info(f'[GDM] Using aria2c for multi-threaded download (connections: {connections})') + # 속도 제한 설정 max_rate = P.ModelSetting.get('max_download_rate') if max_rate == '0': diff --git a/info.yaml b/info.yaml index 36e6b7d..e08d8ad 100644 --- a/info.yaml +++ b/info.yaml @@ -1,6 +1,6 @@ title: "GDM" package_name: gommi_downloader_manager -version: '0.2.13' +version: '0.2.15' 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 d50a5e0..ed5d1a1 100644 --- a/mod_queue.py +++ b/mod_queue.py @@ -141,6 +141,39 @@ class ModuleQueue(PluginModuleBase): P.logger.error(f'DB Clear Error: {e}') ret['msg'] = '목록을 초기화했습니다.' + + elif command == 'delete': + # 특정 항목 완전 삭제 (메모리 + DB) + download_id = req.form.get('id', '') + + # 메모리에서 삭제 + if download_id in self._downloads: + self._downloads[download_id].cancel() + del self._downloads[download_id] + + # DB에서 삭제 (db_XXX 형태인 경우) + if download_id.startswith('db_'): + db_id = int(download_id.replace('db_', '')) + try: + from .model import ModelDownloadItem + with F.app.app_context(): + F.db.session.query(ModelDownloadItem).filter_by(id=db_id).delete() + F.db.session.commit() + except Exception as e: + self.P.logger.error(f'DB Delete Error: {e}') + else: + # 메모리 기반 ID에서 db_id 추출 시도 + try: + task = self._downloads.get(download_id) + if task and hasattr(task, 'db_id') and task.db_id: + from .model import ModelDownloadItem + with F.app.app_context(): + F.db.session.query(ModelDownloadItem).filter_by(id=task.db_id).delete() + F.db.session.commit() + except Exception as e: + self.P.logger.error(f'DB Delete Error: {e}') + + ret['msg'] = '항목이 삭제되었습니다.' except Exception as e: self.P.logger.error(f'Exception:{str(e)}') @@ -385,9 +418,7 @@ class DownloadTask: self.error_message = '' self.filepath = os.path.join(save_path, filename) if filename else '' - # 메타데이터 - self.title = '' - self.thumbnail = '' + # 메타데이터 (이미 __init__ 상단에서 인자로 받은 title, thumbnail을 self.title, self.thumbnail에 할당함) self.duration = 0 self.filesize = 0 @@ -670,3 +701,7 @@ class DownloadTask: 'created_time': self.created_time, 'file_size': self.filesize, } + + def as_dict(self) -> Dict[str, Any]: + """데이터 직렬화 (get_status 별칭)""" + return self.get_status() diff --git a/templates/gommi_downloader_manager_queue_list.html b/templates/gommi_downloader_manager_queue_list.html index cf5ecf0..529efc6 100644 --- a/templates/gommi_downloader_manager_queue_list.html +++ b/templates/gommi_downloader_manager_queue_list.html @@ -192,11 +192,11 @@ margin-right: 0.5rem; } - /* Card List Layout */ + /* Card List Layout - Single Column for clear order */ .download-grid { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(380px, 1fr)); - gap: 1.25rem; + display: flex; + flex-direction: column; + gap: 0.75rem; } @media (max-width: 576px) { @@ -211,11 +211,11 @@ backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px); border: 1px solid var(--border); - border-radius: 16px; - padding: 1.25rem; + border-radius: 12px; + padding: 0.75rem; display: flex; flex-direction: column; - gap: 1rem; + gap: 0.5rem; transition: all 0.3s ease; position: relative; overflow: hidden; @@ -281,7 +281,7 @@ /* Progress Section */ .dl-progress-container { - margin-top: 0.5rem; + margin-top: 0.25rem; } .dl-progress-header { @@ -317,7 +317,7 @@ display: flex; justify-content: space-between; align-items: center; - margin-top: 0.5rem; + margin-top: 0.25rem; } .dl-status-label { @@ -467,12 +467,265 @@ .dl-card.expanded .dl-expand-hint { display: none; } + + /* Selection Checkbox */ + .dl-select-checkbox { + width: 16px; + height: 16px; + margin-right: 6px; + cursor: pointer; + accent-color: var(--accent-primary); + flex-shrink: 0; + } + + .dl-card.selected { + border-color: var(--accent-primary); + box-shadow: 0 0 0 2px var(--glow); + } + + /* ===== NEW FANTASTIC CARD DESIGN ===== */ + + /* Card Header */ + .dl-card-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 6px; + } + + .dl-header-left { + display: flex; + align-items: center; + gap: 6px; + flex-wrap: wrap; + } + + .dl-source-tag { + padding: 3px 8px; + border-radius: 6px; + font-size: 10px; + font-weight: 800; + color: white; + text-transform: uppercase; + letter-spacing: 0.5px; + text-shadow: 0 1px 2px rgba(0,0,0,0.3); + } + + .dl-index-badge { + background: rgba(255, 255, 255, 0.1); + border: 1px solid rgba(255, 255, 255, 0.2); + padding: 2px 8px; + border-radius: 4px; + font-size: 11px; + font-weight: 700; + color: var(--text-main); + font-family: 'SF Mono', 'Menlo', monospace; + min-width: 32px; + text-align: center; + } + + .dl-episode-tag { + background: rgba(255, 255, 255, 0.15); + border: 1px solid rgba(255, 255, 255, 0.2); + padding: 2px 8px; + border-radius: 6px; + font-size: 11px; + font-weight: 700; + color: var(--text-main); + font-family: 'SF Mono', 'Menlo', monospace; + } + + .dl-status-pill { + display: flex; + align-items: center; + gap: 5px; + padding: 3px 10px; + border-radius: 20px; + font-size: 10px; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.3px; + } + + .dl-status-pill .status-dot { + width: 6px; + height: 6px; + border-radius: 50%; + } + + .dl-status-pill.status-downloading { background: rgba(78, 159, 217, 0.2); color: var(--accent-primary); } + .dl-status-pill.status-downloading .status-dot { background: var(--accent-primary); box-shadow: 0 0 6px var(--accent-primary); animation: pulse 1.5s infinite; } + .dl-status-pill.status-completed { background: rgba(78, 191, 115, 0.2); color: var(--success); } + .dl-status-pill.status-completed .status-dot { background: var(--success); } + .dl-status-pill.status-pending { background: rgba(171, 182, 194, 0.15); color: var(--text-muted); } + .dl-status-pill.status-pending .status-dot { background: var(--text-muted); opacity: 0.5; } + .dl-status-pill.status-extracting { background: rgba(168, 85, 247, 0.2); color: #c084fc; } + .dl-status-pill.status-extracting .status-dot { background: #c084fc; animation: pulse 1s infinite; } + .dl-status-pill.status-error { background: rgba(217, 83, 79, 0.2); color: var(--danger); } + .dl-status-pill.status-error .status-dot { background: var(--danger); } + .dl-status-pill.status-cancelled { background: rgba(107, 114, 128, 0.2); color: #9ca3af; } + .dl-status-pill.status-cancelled .status-dot { background: #9ca3af; } + + /* Card Body */ + .dl-card-body { + display: flex; + gap: 10px; + align-items: flex-start; + } + + .dl-thumb { + width: 64px; + height: 36px; + object-fit: cover; + border-radius: 6px; + border: 1px solid var(--border); + flex-shrink: 0; + } + + .dl-content { + flex: 1; + min-width: 0; + } + + .dl-series { + font-size: 13px; + font-weight: 700; + color: var(--accent-secondary); + margin-bottom: 2px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + + .dl-filename { + font-size: 11px; + color: var(--text-muted); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + font-family: 'SF Mono', 'Menlo', monospace; + opacity: 0.8; + } + + /* Progress Section */ + .dl-progress-section { + margin-top: 8px; + } + + .dl-progress-info { + display: flex; + justify-content: space-between; + align-items: baseline; + margin-bottom: 4px; + } + + .dl-percent-big { + font-size: 18px; + font-weight: 700; + color: var(--text-main); + } + + .dl-percent-big small { + font-size: 11px; + font-weight: 600; + opacity: 0.6; + } + + .dl-speed-text { + font-size: 11px; + color: var(--accent-primary); + font-weight: 600; + } + + .dl-progress-track { + height: 6px; + background: rgba(255, 255, 255, 0.08); + border-radius: 10px; + overflow: hidden; + } + + .dl-progress-fill { + height: 100%; + background: linear-gradient(90deg, var(--accent-primary), var(--accent-secondary)); + border-radius: 10px; + transition: width 0.4s cubic-bezier(0.4, 0, 0.2, 1); + box-shadow: 0 0 8px var(--glow); + } + + /* Card Footer */ + .dl-card-footer { + display: flex; + justify-content: space-between; + align-items: center; + margin-top: 8px; + padding-top: 8px; + border-top: 1px solid rgba(255, 255, 255, 0.06); + } + + .dl-time-info { + font-size: 11px; + color: var(--text-muted); + display: flex; + align-items: center; + gap: 4px; + } + + .dl-time-info i { + font-size: 10px; + opacity: 0.6; + } + + .dl-actions { + display: flex; + gap: 6px; + } + + .dl-btn { + width: 28px; + height: 28px; + display: flex; + align-items: center; + justify-content: center; + border-radius: 6px; + border: 1px solid rgba(255, 255, 255, 0.1); + background: rgba(255, 255, 255, 0.05); + color: var(--text-muted); + font-size: 12px; + cursor: pointer; + transition: all 0.2s; + } + + .dl-btn:hover:not(:disabled) { + background: rgba(255, 255, 255, 0.15); + color: var(--text-main); + transform: scale(1.05); + } + + .dl-btn.cancel:hover:not(:disabled) { + background: rgba(240, 173, 78, 0.2); + color: var(--warning); + border-color: var(--warning); + } + + .dl-btn.delete:hover { + background: rgba(217, 83, 79, 0.2); + color: var(--danger); + border-color: var(--danger); + } + + .dl-btn:disabled { + opacity: 0.25; + cursor: not-allowed; + }