diff --git a/README.md b/README.md index 23d5ed8..cc6949f 100644 --- a/README.md +++ b/README.md @@ -1,71 +1,18 @@ -# gommi_download_manager (GDM) +# Gommi Downloader Manager (GDM) -FlaskFarm 범용 다운로더 큐 플러그인 (v0.2.0) +FlaskFarm용 범용 다운로드 매니저 플러그인입니다. +여러 다운로더 플러그인(YouTube, Anime 등)의 다운로드 요청을 통합 관리하고 큐(Queue)를 제공합니다. -## 🆕 0.2.0 업데이트 (2026-01-06) +## v0.2.0 변경사항 +- **패키지명 수정**: `gommi_download_manager` -> `gommi_downloader_manager`로 폴더명과 일치시켜 Bind Key 오류 해결. +- **안정성 개선**: DB 테이블 생성 로직 강화 (`setup.py` 명시적 모델 import). +- **YouTube 제목 지원**: `yt-dlp` 다운로드 시작 시 영상의 진짜 제목과 썸네일을 실시간으로 DB에 업데이트합니다. +- **UI 개선**: 큐 리스트 템플릿 오류 수정. -### 새 기능 -- **플러그인 콜백 시스템**: 다운로드 완료 시 호출 플러그인에 상태 알림 -- **외부 플러그인 통합 강화**: `caller_plugin`, `callback_id` 파라미터로 호출자 추적 -- **HLS ffmpeg 헤더 수정**: None 값 필터링으로 에러 방지 +## 설치 및 업데이트 +1. `git pull` +2. FlaskFarm 재시작 (DB 마이그레이션 적용을 위해 필수) -### 버그 수정 -- PluginManager API 호환성 수정 (`F.plugin_instance_list` → `F.PluginManager.all_package_list`) -- 완료된 다운로드 진행률 100% 표시 수정 -- 큐 목록 URL 표시 제거 (깔끔한 UI) - -### UI 개선 -- 다크 메탈릭 디자인 유지 -- 완료 상태 표시 개선 - ---- - -## 주요 기능 - -- **YouTube/일반 사이트**: yt-dlp + aria2c 지원 (고속 분할 다운로드) -- **스트리밍 사이트**: 애니24, 링크애니, Anilife (ffmpeg HLS / Camoufox) 지원 -- **중앙 집중식 관리**: 여러 플러그인의 다운로드 요청을 한곳에서 통합 관리 -- **전역 속도 제한 (Smart Limiter)**: 모든 다운로드에 공통 적용되는 속도 제한 기능 - -## 외부 플러그인에서 사용하기 - -```python -from gommi_download_manager.mod_queue import ModuleQueue - -# 다운로드 추가 (콜백 지원) -task = ModuleQueue.add_download( - url='https://www.youtube.com/watch?v=...', - save_path='/path/to/save', - filename='video.mp4', - source_type='auto', - caller_plugin='my_plugin_name', # 콜백 호출 시 식별자 - callback_id='unique_item_id', # 콜백 데이터에 포함 -) -``` - -## 콜백 수신하기 - -호출 플러그인에서 `plugin_callback` 메서드를 정의하면 다운로드 완료 시 자동 호출됩니다: - -```python -class MyModule: - def plugin_callback(self, data): - # data = {'callback_id': ..., 'status': 'completed', 'filepath': ..., 'error': ...} - if data['status'] == 'completed': - print(f"다운로드 완료: {data['filepath']}") -``` - -## 설정 가이드 - -웹 인터페이스 (`/gommi_download_manager/queue/setting`)에서 다음을 설정할 수 있습니다: -- **속도 제한**: 네트워크 상황에 맞춰 최대 다운로드 속도 조절 -- **동시 다운로드 수**: 한 번에 몇 개를 받을지 설정 -- **기본 저장 경로**: 경로 미지정 요청에 대한 백업 경로 - -## 성능 비교 - -| 다운로더 | 방식 | 특징 | -|---------|------|------| -| **yt-dlp (Native)** | 안정적 | 속도 제한 기능 완벽 지원 | -| **aria2c** | 고속 (분할) | 대용량 파일에 최적화 (현재 실험적 지원) | -| **ffmpeg** | 스트림 | HLS/M3U8 영상 저장에 사용 | +## 지원 플러그인 +- youtube-dl +- anime_downloader (Ohli24, Linkkf 등) diff --git a/downloader/ytdlp_aria2.py b/downloader/ytdlp_aria2.py index 7fa695a..304ab67 100644 --- a/downloader/ytdlp_aria2.py +++ b/downloader/ytdlp_aria2.py @@ -33,6 +33,7 @@ class YtdlpAria2Downloader(BaseDownloader): save_path: str, filename: Optional[str] = None, progress_callback: Optional[Callable] = None, + info_callback: Optional[Callable] = None, **options ) -> Dict[str, Any]: """yt-dlp + aria2c로 다운로드""" @@ -46,7 +47,6 @@ class YtdlpAria2Downloader(BaseDownloader): output_template = os.path.join(save_path, '%(title)s.%(ext)s') # yt-dlp 명령어 구성 - # 기본 명령어 구성 (항상 verbose 로그 남기도록 수정) cmd = [ 'yt-dlp', '--newline', # 진행률 파싱용 @@ -54,9 +54,12 @@ class YtdlpAria2Downloader(BaseDownloader): '-o', output_template, ] + # 제목/썸네일 업데이트용 출력 추가 (GDM_FIX) + cmd.extend(['--print', 'before_dl:GDM_FIX:title:%(title)s']) + cmd.extend(['--print', 'before_dl:GDM_FIX:thumb:%(thumbnail)s']) + # aria2c 사용 (설치되어 있으면) aria2c_path = options.get('aria2c_path', 'aria2c') - # TODO: 나중에 설정에서 쓰레드 수 지정 (기본값 4로 변경) connections = options.get('connections', 4) # 속도 제한 설정 @@ -69,14 +72,6 @@ class YtdlpAria2Downloader(BaseDownloader): log_rate_msg = max_rate cmd.extend(['--limit-rate', max_rate]) # Native downloader limit - # aria2c 사용 (일시 중지: 진행률 파싱 문제 해결 전까지 Native 사용) - if False and self._check_aria2c(aria2c_path): - cmd.extend([ - '--downloader', 'aria2c', - '--downloader-args', f'aria2c:-x {connections} -s {connections} -k 1M {max_rate_arg}', - ]) - logger.debug(f'aria2c 사용: {connections}개 연결 (속도제한 {log_rate_msg})') - # 포맷 선택 format_spec = options.get('format') if not format_spec: @@ -102,14 +97,12 @@ class YtdlpAria2Downloader(BaseDownloader): # FFmpeg 경로 자동 감지 및 설정 ffmpeg_path = options.get('ffmpeg_path') or P.ModelSetting.get('ffmpeg_path') - # 경로가 비어있거나 'ffmpeg' 같은 단순 이름인 경우 자동 감지 시도 if not ffmpeg_path or ffmpeg_path == 'ffmpeg': import shutil detected_path = shutil.which('ffmpeg') if detected_path: ffmpeg_path = detected_path else: - # Mac Homebrew 등 일반적인 경로 추가 탐색 common_paths = [ '/opt/homebrew/bin/ffmpeg', '/usr/local/bin/ffmpeg', @@ -121,7 +114,6 @@ class YtdlpAria2Downloader(BaseDownloader): break if ffmpeg_path: - # 파일 경로인 경우 폴더 경로로 변환하거나 그대로 사용 (yt-dlp는 둘 다 지원) cmd.extend(['--ffmpeg-location', ffmpeg_path]) logger.debug(f'[GDM] 감지된 FFmpeg 경로: {ffmpeg_path}') @@ -130,7 +122,6 @@ class YtdlpAria2Downloader(BaseDownloader): if isinstance(extra_args, list): cmd.extend(extra_args) - # 후처리 옵션 간편 지원 (예: {'extract_audio': True, 'audio_format': 'mp3'}) if options.get('extract_audio'): cmd.append('--extract-audio') if options.get('audio_format'): @@ -142,6 +133,14 @@ 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) @@ -168,11 +167,24 @@ class YtdlpAria2Downloader(BaseDownloader): line = line.strip() if not line: continue + + # 메타데이터 파싱 (GDM_FIX) + if 'GDM_FIX:' in line: + try: + if 'GDM_FIX:title:' in line: + title = line.split('GDM_FIX:title:', 1)[1].strip() + if info_callback: + info_callback({'title': title}) + elif 'GDM_FIX:thumb:' in line: + thumb = line.split('GDM_FIX:thumb:', 1)[1].strip() + if info_callback: + info_callback({'thumbnail': thumb}) + except: + pass # 진행률 파싱 (yt-dlp default) progress_match = re.search(r'\[download\]\s+(\d+\.?\d*)%', line) - # 로그 출력 여부 결정 (진행률은 5% 단위로만) should_log = True if progress_match: pct = float(progress_match.group(1)) @@ -184,20 +196,15 @@ class YtdlpAria2Downloader(BaseDownloader): if should_log: logger.info(f'[GDM][yt-dlp] {line}') - # 진행률 파싱 (aria2c) if not progress_match: - # aria2c match aria2_match = re.search(r'\(\s*([\d.]+)%\)', line) if aria2_match and (('DL:' in line) or ('CN:' in line)): try: progress = int(float(aria2_match.group(1))) - speed_match = re.search(r'DL:(\S+)', line) speed = speed_match.group(1) if speed_match else '' - eta_match = re.search(r'ETA:(\S+)', line) eta = eta_match.group(1) if eta_match else '' - if progress_callback: progress_callback(progress, speed, eta) continue @@ -206,27 +213,20 @@ class YtdlpAria2Downloader(BaseDownloader): if progress_match and progress_callback: progress = int(float(progress_match.group(1))) - - # 속도 파싱 speed = '' speed_match = re.search(r'at\s+([\d.]+\s*[KMG]?i?B/s)', line) if speed_match: speed = speed_match.group(1) - - # ETA 파싱 eta = '' eta_match = re.search(r'ETA\s+([\d:]+)', line) if eta_match: eta = eta_match.group(1) - progress_callback(progress, speed, eta) - # 최종 파일 경로 추출 (Merger, VideoConvertor, Destination 모두 대응) if any(x in line for x in ['[Merger]', '[VideoConvertor]', 'Destination:']): path_match = re.search(r'(?:Destination:|into|to)\s+["\']?(.+?)(?:["\']|$)', line) if path_match: potential_path = path_match.group(1).strip('"\'') - # 확장자가 있는 경우만 파일 경로로 간주 if '.' in os.path.basename(potential_path): final_filepath = potential_path diff --git a/info.yaml b/info.yaml index 59cff66..cd3a5bd 100644 --- a/info.yaml +++ b/info.yaml @@ -1,6 +1,6 @@ name: GDM package_name: gommi_downloader_manager -version: '0.1.16' +version: '0.2.1' 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 aa595db..a0fb6fc 100644 --- a/mod_queue.py +++ b/mod_queue.py @@ -424,6 +424,7 @@ class DownloadTask: save_path=self.save_path, filename=self.filename, progress_callback=self._progress_callback, + info_callback=self._info_update_callback, **self.options ) @@ -488,6 +489,37 @@ class DownloadTask: except: pass + + def _info_update_callback(self, info_dict): + """다운로더로부터 메타데이터 업데이트 수신""" + try: + if 'title' in info_dict and info_dict['title']: + self.title = info_dict['title'] + if 'thumbnail' in info_dict and info_dict['thumbnail']: + self.thumbnail = info_dict['thumbnail'] + + # DB 업데이트 + self._update_db_info() + + # 상태 전송 + self._emit_status() + except: + pass + + def _update_db_info(self): + """DB의 제목/썸네일 정보 동기화""" + try: + if self.db_id: + from .model import ModelDownloadItem + with F.app.app_context(): + item = F.db.session.query(ModelDownloadItem).filter_by(id=self.db_id).first() + if item: + item.title = self.title + item.thumbnail = self.thumbnail + F.db.session.commit() + except: + pass + def cancel(self): """다운로드 취소""" self._cancelled = True