Release v0.2.0: Fix package name, improve DB stability, support realtime youtube title update
This commit is contained in:
81
README.md
81
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 개선**: 큐 리스트 템플릿 오류 수정.
|
||||||
|
|
||||||
### 새 기능
|
## 설치 및 업데이트
|
||||||
- **플러그인 콜백 시스템**: 다운로드 완료 시 호출 플러그인에 상태 알림
|
1. `git pull`
|
||||||
- **외부 플러그인 통합 강화**: `caller_plugin`, `callback_id` 파라미터로 호출자 추적
|
2. FlaskFarm 재시작 (DB 마이그레이션 적용을 위해 필수)
|
||||||
- **HLS ffmpeg 헤더 수정**: None 값 필터링으로 에러 방지
|
|
||||||
|
|
||||||
### 버그 수정
|
## 지원 플러그인
|
||||||
- PluginManager API 호환성 수정 (`F.plugin_instance_list` → `F.PluginManager.all_package_list`)
|
- youtube-dl
|
||||||
- 완료된 다운로드 진행률 100% 표시 수정
|
- anime_downloader (Ohli24, Linkkf 등)
|
||||||
- 큐 목록 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 영상 저장에 사용 |
|
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ class YtdlpAria2Downloader(BaseDownloader):
|
|||||||
save_path: str,
|
save_path: str,
|
||||||
filename: Optional[str] = None,
|
filename: Optional[str] = None,
|
||||||
progress_callback: Optional[Callable] = None,
|
progress_callback: Optional[Callable] = None,
|
||||||
|
info_callback: Optional[Callable] = None,
|
||||||
**options
|
**options
|
||||||
) -> Dict[str, Any]:
|
) -> Dict[str, Any]:
|
||||||
"""yt-dlp + aria2c로 다운로드"""
|
"""yt-dlp + aria2c로 다운로드"""
|
||||||
@@ -46,7 +47,6 @@ class YtdlpAria2Downloader(BaseDownloader):
|
|||||||
output_template = os.path.join(save_path, '%(title)s.%(ext)s')
|
output_template = os.path.join(save_path, '%(title)s.%(ext)s')
|
||||||
|
|
||||||
# yt-dlp 명령어 구성
|
# yt-dlp 명령어 구성
|
||||||
# 기본 명령어 구성 (항상 verbose 로그 남기도록 수정)
|
|
||||||
cmd = [
|
cmd = [
|
||||||
'yt-dlp',
|
'yt-dlp',
|
||||||
'--newline', # 진행률 파싱용
|
'--newline', # 진행률 파싱용
|
||||||
@@ -54,9 +54,12 @@ class YtdlpAria2Downloader(BaseDownloader):
|
|||||||
'-o', output_template,
|
'-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 사용 (설치되어 있으면)
|
||||||
aria2c_path = options.get('aria2c_path', 'aria2c')
|
aria2c_path = options.get('aria2c_path', 'aria2c')
|
||||||
# TODO: 나중에 설정에서 쓰레드 수 지정 (기본값 4로 변경)
|
|
||||||
connections = options.get('connections', 4)
|
connections = options.get('connections', 4)
|
||||||
|
|
||||||
# 속도 제한 설정
|
# 속도 제한 설정
|
||||||
@@ -69,14 +72,6 @@ class YtdlpAria2Downloader(BaseDownloader):
|
|||||||
log_rate_msg = max_rate
|
log_rate_msg = max_rate
|
||||||
cmd.extend(['--limit-rate', max_rate]) # Native downloader limit
|
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')
|
format_spec = options.get('format')
|
||||||
if not format_spec:
|
if not format_spec:
|
||||||
@@ -102,14 +97,12 @@ class YtdlpAria2Downloader(BaseDownloader):
|
|||||||
# FFmpeg 경로 자동 감지 및 설정
|
# FFmpeg 경로 자동 감지 및 설정
|
||||||
ffmpeg_path = options.get('ffmpeg_path') or P.ModelSetting.get('ffmpeg_path')
|
ffmpeg_path = options.get('ffmpeg_path') or P.ModelSetting.get('ffmpeg_path')
|
||||||
|
|
||||||
# 경로가 비어있거나 'ffmpeg' 같은 단순 이름인 경우 자동 감지 시도
|
|
||||||
if not ffmpeg_path or ffmpeg_path == 'ffmpeg':
|
if not ffmpeg_path or ffmpeg_path == 'ffmpeg':
|
||||||
import shutil
|
import shutil
|
||||||
detected_path = shutil.which('ffmpeg')
|
detected_path = shutil.which('ffmpeg')
|
||||||
if detected_path:
|
if detected_path:
|
||||||
ffmpeg_path = detected_path
|
ffmpeg_path = detected_path
|
||||||
else:
|
else:
|
||||||
# Mac Homebrew 등 일반적인 경로 추가 탐색
|
|
||||||
common_paths = [
|
common_paths = [
|
||||||
'/opt/homebrew/bin/ffmpeg',
|
'/opt/homebrew/bin/ffmpeg',
|
||||||
'/usr/local/bin/ffmpeg',
|
'/usr/local/bin/ffmpeg',
|
||||||
@@ -121,7 +114,6 @@ class YtdlpAria2Downloader(BaseDownloader):
|
|||||||
break
|
break
|
||||||
|
|
||||||
if ffmpeg_path:
|
if ffmpeg_path:
|
||||||
# 파일 경로인 경우 폴더 경로로 변환하거나 그대로 사용 (yt-dlp는 둘 다 지원)
|
|
||||||
cmd.extend(['--ffmpeg-location', ffmpeg_path])
|
cmd.extend(['--ffmpeg-location', ffmpeg_path])
|
||||||
logger.debug(f'[GDM] 감지된 FFmpeg 경로: {ffmpeg_path}')
|
logger.debug(f'[GDM] 감지된 FFmpeg 경로: {ffmpeg_path}')
|
||||||
|
|
||||||
@@ -130,7 +122,6 @@ class YtdlpAria2Downloader(BaseDownloader):
|
|||||||
if isinstance(extra_args, list):
|
if isinstance(extra_args, list):
|
||||||
cmd.extend(extra_args)
|
cmd.extend(extra_args)
|
||||||
|
|
||||||
# 후처리 옵션 간편 지원 (예: {'extract_audio': True, 'audio_format': 'mp3'})
|
|
||||||
if options.get('extract_audio'):
|
if options.get('extract_audio'):
|
||||||
cmd.append('--extract-audio')
|
cmd.append('--extract-audio')
|
||||||
if options.get('audio_format'):
|
if options.get('audio_format'):
|
||||||
@@ -142,6 +133,14 @@ class YtdlpAria2Downloader(BaseDownloader):
|
|||||||
if options.get('add_metadata'):
|
if options.get('add_metadata'):
|
||||||
cmd.append('--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 추가
|
# URL 추가
|
||||||
cmd.append(url)
|
cmd.append(url)
|
||||||
|
|
||||||
@@ -169,10 +168,23 @@ class YtdlpAria2Downloader(BaseDownloader):
|
|||||||
if not line:
|
if not line:
|
||||||
continue
|
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)
|
# 진행률 파싱 (yt-dlp default)
|
||||||
progress_match = re.search(r'\[download\]\s+(\d+\.?\d*)%', line)
|
progress_match = re.search(r'\[download\]\s+(\d+\.?\d*)%', line)
|
||||||
|
|
||||||
# 로그 출력 여부 결정 (진행률은 5% 단위로만)
|
|
||||||
should_log = True
|
should_log = True
|
||||||
if progress_match:
|
if progress_match:
|
||||||
pct = float(progress_match.group(1))
|
pct = float(progress_match.group(1))
|
||||||
@@ -184,20 +196,15 @@ class YtdlpAria2Downloader(BaseDownloader):
|
|||||||
if should_log:
|
if should_log:
|
||||||
logger.info(f'[GDM][yt-dlp] {line}')
|
logger.info(f'[GDM][yt-dlp] {line}')
|
||||||
|
|
||||||
# 진행률 파싱 (aria2c)
|
|
||||||
if not progress_match:
|
if not progress_match:
|
||||||
# aria2c match
|
|
||||||
aria2_match = re.search(r'\(\s*([\d.]+)%\)', line)
|
aria2_match = re.search(r'\(\s*([\d.]+)%\)', line)
|
||||||
if aria2_match and (('DL:' in line) or ('CN:' in line)):
|
if aria2_match and (('DL:' in line) or ('CN:' in line)):
|
||||||
try:
|
try:
|
||||||
progress = int(float(aria2_match.group(1)))
|
progress = int(float(aria2_match.group(1)))
|
||||||
|
|
||||||
speed_match = re.search(r'DL:(\S+)', line)
|
speed_match = re.search(r'DL:(\S+)', line)
|
||||||
speed = speed_match.group(1) if speed_match else ''
|
speed = speed_match.group(1) if speed_match else ''
|
||||||
|
|
||||||
eta_match = re.search(r'ETA:(\S+)', line)
|
eta_match = re.search(r'ETA:(\S+)', line)
|
||||||
eta = eta_match.group(1) if eta_match else ''
|
eta = eta_match.group(1) if eta_match else ''
|
||||||
|
|
||||||
if progress_callback:
|
if progress_callback:
|
||||||
progress_callback(progress, speed, eta)
|
progress_callback(progress, speed, eta)
|
||||||
continue
|
continue
|
||||||
@@ -206,27 +213,20 @@ class YtdlpAria2Downloader(BaseDownloader):
|
|||||||
|
|
||||||
if progress_match and progress_callback:
|
if progress_match and progress_callback:
|
||||||
progress = int(float(progress_match.group(1)))
|
progress = int(float(progress_match.group(1)))
|
||||||
|
|
||||||
# 속도 파싱
|
|
||||||
speed = ''
|
speed = ''
|
||||||
speed_match = re.search(r'at\s+([\d.]+\s*[KMG]?i?B/s)', line)
|
speed_match = re.search(r'at\s+([\d.]+\s*[KMG]?i?B/s)', line)
|
||||||
if speed_match:
|
if speed_match:
|
||||||
speed = speed_match.group(1)
|
speed = speed_match.group(1)
|
||||||
|
|
||||||
# ETA 파싱
|
|
||||||
eta = ''
|
eta = ''
|
||||||
eta_match = re.search(r'ETA\s+([\d:]+)', line)
|
eta_match = re.search(r'ETA\s+([\d:]+)', line)
|
||||||
if eta_match:
|
if eta_match:
|
||||||
eta = eta_match.group(1)
|
eta = eta_match.group(1)
|
||||||
|
|
||||||
progress_callback(progress, speed, eta)
|
progress_callback(progress, speed, eta)
|
||||||
|
|
||||||
# 최종 파일 경로 추출 (Merger, VideoConvertor, Destination 모두 대응)
|
|
||||||
if any(x in line for x in ['[Merger]', '[VideoConvertor]', 'Destination:']):
|
if any(x in line for x in ['[Merger]', '[VideoConvertor]', 'Destination:']):
|
||||||
path_match = re.search(r'(?:Destination:|into|to)\s+["\']?(.+?)(?:["\']|$)', line)
|
path_match = re.search(r'(?:Destination:|into|to)\s+["\']?(.+?)(?:["\']|$)', line)
|
||||||
if path_match:
|
if path_match:
|
||||||
potential_path = path_match.group(1).strip('"\'')
|
potential_path = path_match.group(1).strip('"\'')
|
||||||
# 확장자가 있는 경우만 파일 경로로 간주
|
|
||||||
if '.' in os.path.basename(potential_path):
|
if '.' in os.path.basename(potential_path):
|
||||||
final_filepath = potential_path
|
final_filepath = potential_path
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
name: GDM
|
name: GDM
|
||||||
package_name: gommi_downloader_manager
|
package_name: gommi_downloader_manager
|
||||||
version: '0.1.16'
|
version: '0.2.1'
|
||||||
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
|
||||||
|
|||||||
32
mod_queue.py
32
mod_queue.py
@@ -424,6 +424,7 @@ class DownloadTask:
|
|||||||
save_path=self.save_path,
|
save_path=self.save_path,
|
||||||
filename=self.filename,
|
filename=self.filename,
|
||||||
progress_callback=self._progress_callback,
|
progress_callback=self._progress_callback,
|
||||||
|
info_callback=self._info_update_callback,
|
||||||
**self.options
|
**self.options
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -488,6 +489,37 @@ class DownloadTask:
|
|||||||
except:
|
except:
|
||||||
pass
|
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):
|
def cancel(self):
|
||||||
"""다운로드 취소"""
|
"""다운로드 취소"""
|
||||||
self._cancelled = True
|
self._cancelled = True
|
||||||
|
|||||||
Reference in New Issue
Block a user