From e33e568cb2d1e2d253e49dfc6164180e35fa213c Mon Sep 17 00:00:00 2001 From: projectdx Date: Tue, 6 Jan 2026 18:55:06 +0900 Subject: [PATCH] =?UTF-8?q?v0.2.0:=20=ED=94=8C=EB=9F=AC=EA=B7=B8=EC=9D=B8?= =?UTF-8?q?=20=EC=BD=9C=EB=B0=B1=20=EC=8B=9C=EC=8A=A4=ED=85=9C,=20?= =?UTF-8?q?=EB=B2=84=EA=B7=B8=20=EC=88=98=EC=A0=95,=20UI=20=EA=B0=9C?= =?UTF-8?q?=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 4 + README.md | 47 +- __pycache__/__init__.cpython-314.pyc | Bin 178 -> 0 bytes __pycache__/mod_queue.cpython-314.pyc | Bin 24090 -> 0 bytes __pycache__/model.cpython-314.pyc | Bin 3209 -> 0 bytes __pycache__/setup.cpython-314.pyc | Bin 1678 -> 0 bytes .../__pycache__/__init__.cpython-314.pyc | Bin 1248 -> 0 bytes downloader/__pycache__/base.cpython-314.pyc | Bin 3650 -> 0 bytes .../__pycache__/ytdlp_aria2.cpython-314.pyc | Bin 9323 -> 0 bytes downloader/ffmpeg_hls.py | 43 +- downloader/ytdlp_aria2.py | 96 ++- info.yaml | 2 +- mod_queue.py | 239 ++++-- model.py | 42 + setup.py | 2 +- .../gommi_download_manager_queue_list.html | 721 ++++++++++++------ .../gommi_download_manager_queue_setting.html | 367 ++++++--- 17 files changed, 1158 insertions(+), 405 deletions(-) create mode 100644 .gitignore delete mode 100644 __pycache__/__init__.cpython-314.pyc delete mode 100644 __pycache__/mod_queue.cpython-314.pyc delete mode 100644 __pycache__/model.cpython-314.pyc delete mode 100644 __pycache__/setup.cpython-314.pyc delete mode 100644 downloader/__pycache__/__init__.cpython-314.pyc delete mode 100644 downloader/__pycache__/base.cpython-314.pyc delete mode 100644 downloader/__pycache__/ytdlp_aria2.cpython-314.pyc diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c3b31bf --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +__pycache__/ +*.pyc +*.pyo +.DS_Store diff --git a/README.md b/README.md index 8d7b0e4..23d5ed8 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,24 @@ # gommi_download_manager (GDM) -FlaskFarm 범용 다운로더 큐 플러그인 (v0.1.0) +FlaskFarm 범용 다운로더 큐 플러그인 (v0.2.0) -## 🆕 0.1.0 업데이트 (Latest) -- **다운로드 속도 제한**: 설정 페이지에서 대역폭 제한 설정 가능 (무제한, 1MB/s, 5MB/s...) -- **UI 리뉴얼**: 고급스러운 Dark Metallic 디자인 & 반응형 웹 지원 -- **안정성 강화**: 서버 재시작 시 대기 중인 다운로드 상태 복원 (Queue Persistence) -- **목록 관리**: 전체 삭제 및 자동 목록 갱신 기능 (Flickr-free) +## 🆕 0.2.0 업데이트 (2026-01-06) + +### 새 기능 +- **플러그인 콜백 시스템**: 다운로드 완료 시 호출 플러그인에 상태 알림 +- **외부 플러그인 통합 강화**: `caller_plugin`, `callback_id` 파라미터로 호출자 추적 +- **HLS ffmpeg 헤더 수정**: None 값 필터링으로 에러 방지 + +### 버그 수정 +- PluginManager API 호환성 수정 (`F.plugin_instance_list` → `F.PluginManager.all_package_list`) +- 완료된 다운로드 진행률 100% 표시 수정 +- 큐 목록 URL 표시 제거 (깔끔한 UI) + +### UI 개선 +- 다크 메탈릭 디자인 유지 +- 완료 상태 표시 개선 + +--- ## 주요 기능 @@ -20,16 +32,29 @@ FlaskFarm 범용 다운로더 큐 플러그인 (v0.1.0) ```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='youtube', # 호출자 식별 + 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`)에서 다음을 설정할 수 있습니다: diff --git a/__pycache__/__init__.cpython-314.pyc b/__pycache__/__init__.cpython-314.pyc deleted file mode 100644 index 880f97797a9f395ac7e4b8dc40006cee0258c7a6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 178 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 diff --git a/__pycache__/setup.cpython-314.pyc b/__pycache__/setup.cpython-314.pyc deleted file mode 100644 index 6efe485f9fed6cee67ce5575064793a580967855..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1678 zcma)6?{5=j9DlC&qrba#UFSANxCM~?VvS@1A~C73xkVuJoWC&Ta%->SLa$e^cd)^m zHBLbWTEdWmBbHbkGrTAZMiPBP{0Ax+X`aMHtlk~~w`|I<3 zp6~a$_WQgp1k3jEyV5+3&~KKpw%QxuQIJM-WGh5gNhSA~ufXD2|37!`V3p z=Zuh?i*s`x&Iz`Q^VX0LP*cbTJ`bSgkZsTwBEj;5)l$m{0BQw4QS%4EY6FV|>lv;+ z#Ap~?k}A*7Fu2&&8) zD+}h;W!9K4m{;!@*B6cR3v4BmWh)EkjL&baeskBncGu-HmuAh+vaI>xIrD5`k83;o zj-pTKlOo$Qqiv5RQfxmfsFJWF+6yuBQr?(9vtuY^EM+R0TXmD|iO1!XIK_@0c-=}0 zC#8fG2ZDWqtmtuN8Yt=+R&@1lvGV>bJ328j&YDXZGk;;7!i$R_XzsRg_6BQyy=dk$ zl`9LDrn&;wSe~uS7tL=A#@r3_gB;uQ+|JeFB5Qnd#hhPc&CA8rm10e1omY}mimI_j zaoL=|#`c8A$5=C0FjnR&mkY*4D9A6a-mVo0>6v6yQflonS8~Rew_Jy?BQ7Pxl%O5O zjGh+x*yIEb)PNWl^n}KzMNN~EQ`mh}kwsosVtPWv^n1Ffif2ll`xeQWKnN{ zDZgUPY~5|oFJLMmr8T`7f=|W-%3w^`-r(06h?;W+Gk-}B{5#S3YT3Ac4Kpx*T}bFo zE51}PKDuLm`rSzNiaT!(7H4!sOMU{m_FM)|Z z*xUzemwL^*(n;6wbW}`fl9Jrh>&JE;9$%E_u~Sops5mJ^k6{PTTP*{po99#DfhWoH zsvAUMCY8{qq$F;Os-mEYeBI=wWLguFQ4tg4*avcp$u-U9nC&B5%6a&ud$y$|+suroK7KYnw% z)VkZkBl(fiK)BTUa+M}KTB>%t*H>*p{)?=-;@XPsl!_=8IB9Vqgd~`-x}D_*vW&L{q}p`ym>S8 z-l^gDatI9H{MNHog#MF115@_F?xz6S$Uz(}A_rHnt#D-mEn-KhsJ6zn3FN59k)s_) z{s_-F8AqQ{xb7HJsAwh=usGMfyViTO-u?A)_x?|X?jO&(Ki)3%?>*~ZU+X<>=ZaXE zpGVva1J?&Ue94XJeE5Cfhi;9sB@7Ydkd}b~d7|CRFtt$?Eh=+25UK$;hQ7ed*ue|n z*)vC37@PquXalzN!2lstRv<_9f2k7rI84|QCA<|;p)<-878F&X#+)%JU~*qbPp^lp z>GD{nG%3C*S3^H((zyK9eEG|mG5{f4DMu~790uijoz&=eB=VDn7sTa8*lc>FwtqI# zbOX0RS$RMJB*Y6mPRLBu5<`TzK@f76Q$koC_A_C`@a zk1wRuE=W*%aS}+ACx8QJyT1XD7w4Ft0jdqli-V`c6=dTGPoOG3fzE&pc_pjCd!&P{yqgQ#&k1jILojs^0;72dl zn-Oi;etbX+!wo#&t5fh>7x&=&ehCU~ne#{nwBbANF}lPLR$eJ8!f1sFPp(m6G-yB> z)Uhfu5ODUa(0wmhS*Z(k*^i5+$hTxp#Fa@gxP2sY^)wp-er5r|M&xuvy7F>gQcE{v zqren1OqX7{a8E3CsVkPf@lgSc$(|O&4a(n?8f9z@$h3a00%)Th9hsw@W2H@_v~7N} zWle2ZCp+fJf3inA@6T>#XSdBWTh`=;HQh0%fn3dQW>0LJr7dfG!zy&lg5>ng?D*lu z|K=?=1kuyI(tf#8G=xDQjU_^a4z1+{jkKGDb`82b)ud{va($nW-_c%(^bF>?W>`yn vdY(-}SlMy$G)y}x#`uMzVC|)j@Ypsgywsrsc1mbytn=aNPHyJ9kw$q9^VUbK diff --git a/downloader/__pycache__/base.cpython-314.pyc b/downloader/__pycache__/base.cpython-314.pyc deleted file mode 100644 index 8bdb3b58e0e00896cd3b1fb9474a872cff37fb0d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3650 zcmbUj?{6H{@$KHP^~OFsN`gat*c*b9TrfUpQbbKe3vsDw(Y0NQs|q=*_1)TDvA27? zeFut~ibEuA(m2HxNgG#A>ZpmxAwIY?4PW4YKth%1erXY(nq5ohTt)cE%-h}E*_V=v zC*ACOGxOff@0o|QeKLWSIQf$Kiwq&3;~`$DXw#a6O`WKOk)uQvo)acTCQc?;Vg%3P zbIHjROHHO(dNRW@lM<6g$PTibsEI+MCa1+iiSQlPqo($de7g1^oMr#amEcd;{maY# z*>j5j{z`E1dhpvT%Gw*({Yw{MC+9^k@$@rCysSRsGOCwY#bEPJIiK{B6K074(LUx$ zKdv&#(V?{p8y%FGCQJZD#G?{R+(#Txhb2{Ukg(KrS{;}o z2L&}bNGwTB0UWS|L(u~)Go9wtG^b{edWR;d61>o(_5hSs8Q_-bUe3LRbN6oG?u%%B zoR-}{%c}jXe~PdHH36pD>SdqNU1P#|&9)r9Y|z?cAddg;HUI4urPF}H#S2Ps<6Q9O z8+S8hNr;rdj|e|-h}1$api`%ajd;HJ=?=FlfDGcZ#fIp~T1mG{hGiLLFQZlUnroCB zf|qm+Yu3dJd%dZ`lw;K@hFkdgMBzo(ps;c1%Z2JOaGPD2oz=?5t6J5n&6&1am~$!> zQ!7WBX%*ep=L}ki5wM*B&BvtaRW95SmY=0OCH=nEAYtw4;tS~)Trh4xcaLG7?RC<(yVD2wq7y3tySvGQNwk? z+Lkq=mtOYL)L=Dg=Tlw+vVf-H0Sb9aXc7TtDC9|c)}a-hd3~BzvUJxiLw;zQi~sgu zouXyDUQN?&+hIC0z>-5WWU{BB`;JV%xDg}|q zqpfxv&>h9$?PVo6b7k!hr)BwRI_Hkd3M??-ct`vL&x`8woN~vi<4Ukn4=!C*ZodB! z6d6v8?L4lmEi42VKlI-{sq8O0wgI)rZ(LV`ON;(nOUljF#o(QKo?q#52)z5w2@uD> zx}pRhE&Ho~RrbeBql#NKjPj^rFg?6};DE#HlMB~woV&SNSAq+F4^Cc?WtF=%(&v}_ zzfiM%Zmm>;0N6LK%s7rU+9sgz7%o78Kp0h&JxZ{2!vD*HzqlOf)2SPdW#c!Mo&MbqD%6_!%!Zk4Qawzbx8)2#kU_Iq^-NoQGj^&*bE338^M-N;MIxR)X$;CqfM{ zaRD)eh>onGIYc}FpfgeEFk(jlj3Q+Z0G9E>ASJHA>fZUM+HL7~Gg| z@j{hAl}*9j0IUqL3c-2a{~JUc)a)C}%6{<3+M9L%>?%a!+KJWt|Ber4GRufu+71pj zU#ra6x@mP$$~CHEhXjIYv(9+pB?AM^GTjcs*w`3<7b1b~266LZI~cg3_U?@wK4&nf z%(G6H!(+n3a6NW-ZiB;tz!zGKTWLFNumo&qM%g3}`FGSeOkP#k%+*X4T4h zNGvW~A8>RDm*TzC+rCLQj{PoNh(#M5!Up!uMC-QLrT1>GEki|*jrD5OT#34o`acarzXRNN50@U$J{$nO zotvWg4&_I1yTd5QPapqS8sfrG9M~0Nqy{e3~mExzyt!7qR0D}lk1-2eap diff --git a/downloader/__pycache__/ytdlp_aria2.cpython-314.pyc b/downloader/__pycache__/ytdlp_aria2.cpython-314.pyc deleted file mode 100644 index ef019bee8eee3a606b7ba2ca2866863b7bcab605..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 9323 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 7: + cookie_lines.append(f"{parts[5]}={parts[6]}") + if cookie_lines: + cookie_str = '; '.join(cookie_lines) + if headers: + header_str += f'\r\nCookie: {cookie_str}' + cmd[-1] = header_str # Update headers + else: + cmd.extend(['-headers', f'Cookie: {cookie_str}']) + except Exception as ce: + logger.error(f"Failed to read cookies_file: {ce}") + # 입력 URL cmd.extend(['-i', url]) @@ -64,7 +89,7 @@ class FfmpegHlsDownloader(BaseDownloader): # 출력 파일 cmd.append(filepath) - logger.debug(f'ffmpeg 명령어: {" ".join(cmd[:10])}...') + logger.debug(f'ffmpeg 명령어: {" ".join(cmd[:15])}...') # 먼저 duration 얻기 위해 ffprobe 실행 duration = self._get_duration(url, options.get('ffprobe_path', 'ffprobe'), headers) @@ -78,13 +103,17 @@ class FfmpegHlsDownloader(BaseDownloader): bufsize=1 ) - # 출력 파싱 + # 출력 파싱 및 에러 메시지 캡처를 위한 변수 + last_lines = [] for line in self._process.stdout: if self._cancelled: self._process.terminate() return {'success': False, 'error': 'Cancelled'} line = line.strip() + if line: + last_lines.append(line) + if len(last_lines) > 20: last_lines.pop(0) # 진행률 계산 (time= 파싱) if duration > 0 and progress_callback: @@ -109,7 +138,9 @@ class FfmpegHlsDownloader(BaseDownloader): progress_callback(100, '', '') return {'success': True, 'filepath': filepath} else: - return {'success': False, 'error': f'FFmpeg exit code: {self._process.returncode}'} + error_log = "\n".join(last_lines) + logger.error(f"FFmpeg failed with return code {self._process.returncode}. Last output:\n{error_log}") + return {'success': False, 'error': f'FFmpeg Error({self._process.returncode}): {last_lines[-1] if last_lines else "Unknown"}'} except Exception as e: logger.error(f'FfmpegHls download error: {e}') diff --git a/downloader/ytdlp_aria2.py b/downloader/ytdlp_aria2.py index ccdb8ba..7fa695a 100644 --- a/downloader/ytdlp_aria2.py +++ b/downloader/ytdlp_aria2.py @@ -46,9 +46,11 @@ class YtdlpAria2Downloader(BaseDownloader): output_template = os.path.join(save_path, '%(title)s.%(ext)s') # yt-dlp 명령어 구성 + # 기본 명령어 구성 (항상 verbose 로그 남기도록 수정) cmd = [ 'yt-dlp', '--newline', # 진행률 파싱용 + '--no-check-certificate', '-o', output_template, ] @@ -76,12 +78,18 @@ class YtdlpAria2Downloader(BaseDownloader): logger.debug(f'aria2c 사용: {connections}개 연결 (속도제한 {log_rate_msg})') # 포맷 선택 - format_spec = options.get('format', 'bestvideo+bestaudio/best') + format_spec = options.get('format') + if not format_spec: + if options.get('extract_audio'): + format_spec = 'bestaudio/best' + else: + format_spec = 'bestvideo+bestaudio/best' cmd.extend(['-f', format_spec]) - # 병합 포맷 - merge_format = options.get('merge_output_format', 'mp4') - cmd.extend(['--merge-output-format', merge_format]) + # 병합 포맷 (비디오인 경우에만) + if not options.get('extract_audio'): + merge_format = options.get('merge_output_format', 'mp4') + cmd.extend(['--merge-output-format', merge_format]) # 쿠키 파일 if options.get('cookiefile'): @@ -90,11 +98,54 @@ class YtdlpAria2Downloader(BaseDownloader): # 프록시 if options.get('proxy'): cmd.extend(['--proxy', options['proxy']]) + + # 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', + '/usr/bin/ffmpeg' + ] + for p in common_paths: + if os.path.exists(p): + ffmpeg_path = p + break + + if ffmpeg_path: + # 파일 경로인 경우 폴더 경로로 변환하거나 그대로 사용 (yt-dlp는 둘 다 지원) + cmd.extend(['--ffmpeg-location', ffmpeg_path]) + logger.debug(f'[GDM] 감지된 FFmpeg 경로: {ffmpeg_path}') + + # 추가 인자 (extra_args: list) + extra_args = options.get('extra_args', []) + 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'): + cmd.extend(['--audio-format', options['audio_format']]) + + if options.get('embed_thumbnail'): + cmd.append('--embed-thumbnail') + + if options.get('add_metadata'): + cmd.append('--add-metadata') # URL 추가 cmd.append(url) - logger.debug(f'yt-dlp 명령어: {" ".join(cmd)}') + logger.info(f'[GDM] yt-dlp command: {" ".join(cmd)}') # 프로세스 실행 self._process = subprocess.Popen( @@ -106,6 +157,7 @@ class YtdlpAria2Downloader(BaseDownloader): ) final_filepath = '' + last_logged_pct = -1 # 출력 파싱 for line in self._process.stdout: @@ -114,23 +166,34 @@ class YtdlpAria2Downloader(BaseDownloader): return {'success': False, 'error': 'Cancelled'} line = line.strip() - # logger.debug(line) + if not line: + continue # 진행률 파싱 (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)) + if int(pct) >= last_logged_pct + 5 or pct >= 99.9: + last_logged_pct = int(pct) + else: + should_log = False + + if should_log: + logger.info(f'[GDM][yt-dlp] {line}') + # 진행률 파싱 (aria2c) if not progress_match: - # logger.error(f'DEBUG LINE: {line}') # Log raw line to debug - aria2_match = re.search(r'\(\s*([\d.]+)%\)', line) # Allow spaces ( 7%) - if aria2_match and (('DL:' in line) or ('CN:' in line)): # DL or CN must be present + # 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))) - # logger.error(f'MATCHED PROGRESS: {progress}%') speed_match = re.search(r'DL:(\S+)', line) speed = speed_match.group(1) if speed_match else '' - # Strip color codes from speed if needed? output is usually clean text if no TTY eta_match = re.search(r'ETA:(\S+)', line) eta = eta_match.group(1) if eta_match else '' @@ -158,11 +221,14 @@ class YtdlpAria2Downloader(BaseDownloader): progress_callback(progress, speed, eta) - # 최종 파일 경로 추출 - if '[Merger]' in line or 'Destination:' in line: - path_match = re.search(r'(?:Destination:|into\s+["\'])(.+?)(?:["\']|$)', line) + # 최종 파일 경로 추출 (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: - final_filepath = path_match.group(1).strip('"\'') + potential_path = path_match.group(1).strip('"\'') + # 확장자가 있는 경우만 파일 경로로 간주 + if '.' in os.path.basename(potential_path): + final_filepath = potential_path self._process.wait() diff --git a/info.yaml b/info.yaml index b4c7a26..fd94081 100644 --- a/info.yaml +++ b/info.yaml @@ -1,6 +1,6 @@ name: gommi_download_manager package_name: gommi_download_manager -version: '0.1.0' +version: '0.1.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 9c235b8..ef61526 100644 --- a/mod_queue.py +++ b/mod_queue.py @@ -12,7 +12,7 @@ from enum import Enum from flask import render_template, jsonify from framework import F, socketio -from .setup import P, PluginModuleBase, default_route_socketio_module, ToolUtil +from framework import F, socketio class DownloadStatus(str, Enum): @@ -25,6 +25,8 @@ class DownloadStatus(str, Enum): CANCELLED = "cancelled" +from plugin import PluginModuleBase + class ModuleQueue(PluginModuleBase): """다운로드 큐 관리 모듈""" @@ -46,23 +48,24 @@ class ModuleQueue(PluginModuleBase): _queue_lock = threading.Lock() def __init__(self, P: Any) -> None: + from .setup import default_route_socketio_module super(ModuleQueue, self).__init__(P, name='queue', first_menu='list') default_route_socketio_module(self, attach='/queue') def process_menu(self, page_name: str, req: Any) -> Any: """메뉴 페이지 렌더링""" - P.logger.debug(f'Page Request: {page_name}') - arg = P.ModelSetting.to_dict() + self.P.logger.debug(f'Page Request: {page_name}') + arg = self.P.ModelSetting.to_dict() try: arg['module_name'] = self.name - arg['package_name'] = P.package_name # 명시적 추가 + arg['package_name'] = self.P.package_name # 명시적 추가 arg['path_data'] = F.config['path_data'] - return render_template(f'{P.package_name}_{self.name}_{page_name}.html', arg=arg) + return render_template(f'{self.P.package_name}_{self.name}_{page_name}.html', arg=arg) except Exception as e: - P.logger.error(f'Exception:{str(e)}') - P.logger.error(traceback.format_exc()) - return render_template('sample.html', title=f"{P.package_name}/{self.name}/{page_name}") + self.P.logger.error(f'Exception:{str(e)}') + self.P.logger.error(traceback.format_exc()) + return render_template('sample.html', title=f"{self.P.package_name}/{self.name}/{page_name}") def process_ajax(self, command: str, req: Any) -> Any: """AJAX 명령 처리""" @@ -71,18 +74,39 @@ class ModuleQueue(PluginModuleBase): try: if command == 'add': # 큐에 다운로드 추가 + from .setup import P, ToolUtil url = req.form['url'] - save_path = req.form.get('save_path') or ToolUtil.make_path(P.ModelSetting.get('save_path')) + save_path = req.form.get('save_path') or ToolUtil.make_path(self.P.ModelSetting.get('save_path')) filename = req.form.get('filename') item = self.add_download(url, save_path, filename) ret['data'] = item.as_dict() if item else None elif command == 'list': - # 진행 중인 다운로드 목록 - items = [d.get_status() for d in self._downloads.values()] - P.logger.debug(f'List Command: {len(items)} items') - ret['data'] = items + # 진행 중인 다운로드 목록 + 최근 DB 내역 (영속성 강화) + active_items = [d.get_status() for d in self._downloads.values()] + active_ids = [i['id'] for i in active_items if 'id' in i] + + # DB에서 최근 50개 가져와서 합치기 + from .model import ModelDownloadItem + with F.app.app_context(): + db_items = F.db.session.query(ModelDownloadItem).order_by(ModelDownloadItem.id.desc()).limit(50).all() + for db_item in db_items: + # 이미 active에 있으면 스킵 + is_active = False + for ai in active_items: + if ai.get('db_id') == db_item.id: + is_active = True + break + if not is_active: + item_dict = db_item.as_dict() + item_dict['id'] = f"db_{db_item.id}" + # completed 상태면 진행률 100%로 표시 + if item_dict.get('status') == 'completed': + item_dict['progress'] = 100 + active_items.append(item_dict) + + ret['data'] = active_items elif command == 'cancel': # 다운로드 취소 @@ -119,8 +143,8 @@ class ModuleQueue(PluginModuleBase): ret['msg'] = '목록을 초기화했습니다.' except Exception as e: - P.logger.error(f'Exception:{str(e)}') - P.logger.error(traceback.format_exc()) + self.P.logger.error(f'Exception:{str(e)}') + self.P.logger.error(traceback.format_exc()) ret['ret'] = 'error' ret['msg'] = str(e) @@ -140,30 +164,21 @@ class ModuleQueue(PluginModuleBase): on_progress: Optional[Callable] = None, on_complete: Optional[Callable] = None, on_error: Optional[Callable] = None, + title: Optional[str] = None, + thumbnail: Optional[str] = None, + meta: Optional[Dict[str, Any]] = None, **options ) -> Optional['DownloadTask']: - """ - 다운로드를 큐에 추가 (외부 플러그인에서 호출) - - Args: - url: 다운로드 URL - save_path: 저장 경로 - filename: 파일명 (자동 감지 가능) - source_type: 소스 타입 (auto, youtube, ani24, linkkf, anilife, http) - caller_plugin: 호출 플러그인 이름 - callback_id: 콜백 식별자 - on_progress: 진행률 콜백 (progress, speed, eta) - on_complete: 완료 콜백 (filepath) - on_error: 에러 콜백 (error_message) - **options: 추가 옵션 (headers, cookies 등) - - Returns: - DownloadTask 인스턴스 - """ + """다운로드를 큐에 추가 (외부 플러그인에서 호출)""" try: + # 옵션 평탄화 (Nesting 방지) + if 'options' in options and isinstance(options['options'], dict): + inner_options = options.pop('options') + options.update(inner_options) + # 소스 타입 자동 감지 if not source_type or source_type == 'auto': - source_type = cls._detect_source_type(url) + source_type = cls._detect_source_type(url, caller_plugin, meta) # DownloadTask 생성 task = DownloadTask( @@ -176,6 +191,9 @@ class ModuleQueue(PluginModuleBase): on_progress=on_progress, on_complete=on_complete, on_error=on_error, + title=title, + thumbnail=thumbnail, + meta=meta, **options ) @@ -187,6 +205,7 @@ class ModuleQueue(PluginModuleBase): task.start() # DB 저장 + import json from .model import ModelDownloadItem db_item = ModelDownloadItem() db_item.created_time = datetime.now() @@ -197,6 +216,10 @@ class ModuleQueue(PluginModuleBase): db_item.status = DownloadStatus.PENDING db_item.caller_plugin = caller_plugin db_item.callback_id = callback_id + db_item.title = title or task.title + db_item.thumbnail = thumbnail or task.thumbnail + if meta: + db_item.meta = json.dumps(meta, ensure_ascii=False) db_item.save() task.db_id = db_item.id @@ -205,6 +228,7 @@ class ModuleQueue(PluginModuleBase): return task except Exception as e: + from .setup import P P.logger.error(f'add_download error: {e}') P.logger.error(traceback.format_exc()) return None @@ -220,10 +244,26 @@ class ModuleQueue(PluginModuleBase): return list(cls._downloads.values()) @classmethod - def _detect_source_type(cls, url: str) -> str: - """URL에서 소스 타입 자동 감지""" + def _detect_source_type(cls, url: str, caller_plugin: Optional[str] = None, meta: Optional[Dict] = None) -> str: + """URL 및 호출자 정보를 기반으로 지능적 소스 타입 감지""" url_lower = url.lower() + # 1. 호출자(Plugin) 기반 우선 판단 + if caller_plugin: + cp_lower = caller_plugin.lower() + if 'anilife' in cp_lower: return 'anilife' + if 'ohli24' in cp_lower or 'ani24' in cp_lower: return 'ani24' + if 'linkkf' in cp_lower: return 'linkkf' + if 'youtube' in cp_lower: return 'youtube' + + # 2. 메타데이터 기반 판단 + if meta and meta.get('source'): + ms_lower = meta.get('source').lower() + if ms_lower in ['ani24', 'ohli24']: return 'ani24' + if ms_lower == 'anilife': return 'anilife' + if ms_lower == 'linkkf': return 'linkkf' + + # 3. URL 기반 판단 if 'youtube.com' in url_lower or 'youtu.be' in url_lower: return 'youtube' elif 'ani24' in url_lower or 'ohli24' in url_lower: @@ -239,11 +279,13 @@ class ModuleQueue(PluginModuleBase): def plugin_load(self) -> None: """플러그인 로드 시 초기화""" - P.logger.info('gommi_downloader 플러그인 로드') + self.P.logger.info('gommi_downloader 플러그인 로드') try: # DB에서 진행 중인 작업 로드 with F.app.app_context(): from .model import ModelDownloadItem + ModelDownloadItem.P = self.P + ModelDownloadItem.check_migration() # 간단하게 status != completed, cancelled, error items = F.db.session.query(ModelDownloadItem).filter( @@ -262,8 +304,10 @@ class ModuleQueue(PluginModuleBase): filename=item.filename, source_type=item.source_type, caller_plugin=item.caller_plugin, - callback_id=item.callback_id - # options? DB에 저장 안함. 필요하면 추가해야 함. + callback_id=item.callback_id, + title=item.title, + thumbnail=item.thumbnail, + meta=item.as_dict().get('meta') ) task.status = DownloadStatus(item.status) task.db_id = item.id @@ -277,11 +321,11 @@ class ModuleQueue(PluginModuleBase): self._downloads[task.id] = task task.start() - P.logger.info(f'{len(items)}개의 중단된 다운로드 작업 복원됨') + self.P.logger.info(f'{len(items)}개의 중단된 다운로드 작업 복원됨') except Exception as e: - P.logger.error(f'plugin_load error: {e}') - P.logger.error(traceback.format_exc()) + self.P.logger.error(f'plugin_load error: {e}') + self.P.logger.error(traceback.format_exc()) def plugin_unload(self) -> None: """플러그인 언로드 시 정리""" @@ -307,6 +351,9 @@ class DownloadTask: on_progress: Optional[Callable] = None, on_complete: Optional[Callable] = None, on_error: Optional[Callable] = None, + title: Optional[str] = None, + thumbnail: Optional[str] = None, + meta: Optional[Dict[str, Any]] = None, **options ): with self._counter_lock: @@ -319,6 +366,9 @@ class DownloadTask: self.source_type = source_type self.caller_plugin = caller_plugin self.callback_id = callback_id + self.title = title or '' + self.thumbnail = thumbnail or '' + self.meta = meta or {} self.options = options # 콜백 @@ -332,7 +382,7 @@ class DownloadTask: self.speed = '' self.eta = '' self.error_message = '' - self.filepath = '' + self.filepath = os.path.join(save_path, filename) if filename else '' # 메타데이터 self.title = '' @@ -382,21 +432,35 @@ class DownloadTask: self.status = DownloadStatus.COMPLETED self.filepath = result.get('filepath', '') self.progress = 100 + + # DB 업데이트 + self._update_db_status() + + # 실시간 콜백 처리 if self._on_complete: self._on_complete(self.filepath) + + # 플러그인 간 영구적 콜백 처리 + if self.caller_plugin and self.callback_id: + self._invoke_plugin_callback() else: self.status = DownloadStatus.ERROR self.error_message = result.get('error', 'Unknown error') + self._update_db_status() if self._on_error: self._on_error(self.error_message) except Exception as e: + from .setup import P P.logger.error(f'Download error: {e}') P.logger.error(traceback.format_exc()) self.status = DownloadStatus.ERROR self.error_message = str(e) if self._on_error: self._on_error(self.error_message) + + # 0바이트 파일 정리 (실패 시) + self._cleanup_if_empty() finally: self._emit_status() @@ -418,7 +482,7 @@ class DownloadTask: socketio.emit( 'download_status', self.get_status(), - namespace=f'/{P.package_name}' + namespace=f'/gommi_download_manager' ) except: pass @@ -429,6 +493,7 @@ class DownloadTask: if self._downloader: self._downloader.cancel() self.status = DownloadStatus.CANCELLED + self._cleanup_if_empty() self._emit_status() def pause(self): @@ -444,6 +509,90 @@ class DownloadTask: self._downloader.resume() self.status = DownloadStatus.DOWNLOADING self._emit_status() + + def _cleanup_if_empty(self): + """출력 파일이 0바이트거나 존재하지 않으면 삭제 (정리)""" + try: + if self.filepath and os.path.exists(self.filepath): + if os.path.getsize(self.filepath) == 0: + from .setup import P + P.logger.info(f"Cleaning up 0-byte file: {self.filepath}") + os.remove(self.filepath) + except Exception as e: + from .setup import P + P.logger.error(f"Cleanup error: {e}") + + def _update_db_status(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.status = self.status + if self.status == DownloadStatus.COMPLETED: + item.completed_time = datetime.now() + if self.error_message: + item.error_message = self.error_message + F.db.session.add(item) + F.db.session.commit() + except Exception as e: + from .setup import P + P.logger.error(f"Failed to update DB status: {e}") + + def _invoke_plugin_callback(self): + """호출한 플러그인의 콜백 메서드 호출""" + try: + from .setup import P + P.logger.info(f"Invoking callback for plugin: {self.caller_plugin}, id: {self.callback_id}") + + # 플러그인 인스턴스 찾기 (PluginManager 사용) + from framework import F + target_P = None + + # caller_plugin은 "anime_downloader_ohli24" 형식이므로 패키지명 추출 + parts = self.caller_plugin.split('_') + package_name = parts[0] if parts else self.caller_plugin + + # 패키지 이름으로 여러 조합 시도 + possible_names = [ + self.caller_plugin, # anime_downloader_ohli24 + '_'.join(parts[:2]) if len(parts) > 1 else self.caller_plugin, # anime_downloader + package_name # anime + ] + + for name in possible_names: + if name in F.PluginManager.all_package_list: + pkg_info = F.PluginManager.all_package_list[name] + if pkg_info.get('loading') and 'P' in pkg_info: + target_P = pkg_info['P'] + break + + if target_P: + # 모듈에서 콜백 메서드 찾기 + callback_invoked = False + for module_name, module_instance in getattr(target_P, 'module_list', {}).items(): + if hasattr(module_instance, 'plugin_callback'): + callback_data = { + 'callback_id': self.callback_id, + 'status': self.status, + 'filepath': self.filepath, + 'filename': os.path.basename(self.filepath) if self.filepath else '', + 'error': self.error_message + } + module_instance.plugin_callback(callback_data) + callback_invoked = True + P.logger.info(f"Callback invoked on module {module_name}") + break + + if not callback_invoked: + P.logger.debug(f"No plugin_callback method found in {self.caller_plugin}") + else: + P.logger.debug(f"Plugin {self.caller_plugin} not found in PluginManager") + except Exception as e: + P.logger.error(f"Error invoking plugin callback: {e}") + P.logger.error(traceback.format_exc()) def get_status(self) -> Dict[str, Any]: """현재 상태 반환""" @@ -459,8 +608,10 @@ class DownloadTask: 'eta': self.eta, 'title': self.title, 'thumbnail': self.thumbnail, + 'meta': self.meta, 'error_message': self.error_message, 'filepath': self.filepath, 'caller_plugin': self.caller_plugin, 'callback_id': self.callback_id, + 'db_id': self.db_id, } diff --git a/model.py b/model.py index ec3f22a..2b1e8d8 100644 --- a/model.py +++ b/model.py @@ -2,6 +2,7 @@ 다운로드 큐 모델 정의 """ from plugin import ModelBase, db +from framework import F package_name = 'gommi_download_manager' @@ -42,4 +43,45 @@ class ModelDownloadItem(ModelBase): # 에러 정보 error_message: str = db.Column(db.Text) retry_count: int = db.Column(db.Integer, default=0) + + # 추가 메타데이터 (JSON 형태의 텍스트 저장) + meta: str = db.Column(db.Text) + + def as_dict(self): + ret = super(ModelDownloadItem, self).as_dict() + import json + if self.meta: + try: + ret['meta'] = json.loads(self.meta) + except: + ret['meta'] = {} + else: + ret['meta'] = {} + return ret + + @classmethod + def check_migration(cls): + """DB 컬럼 누락 체크 및 추가""" + try: + from .setup import P + import sqlite3 + db_file = F.app.config['SQLALCHEMY_BINDS'][package_name].replace('sqlite:///', '').split('?')[0] + conn = sqlite3.connect(db_file) + cursor = conn.cursor() + + # meta 컬럼 확인 + cursor.execute(f"PRAGMA table_info({cls.__tablename__})") + columns = [info[1] for info in cursor.fetchall()] + + if 'meta' not in columns: + P.logger.info(f"Adding 'meta' column to {cls.__tablename__}") + cursor.execute(f"ALTER TABLE {cls.__tablename__} ADD COLUMN meta TEXT") + conn.commit() + + conn.close() + except Exception as e: + from .setup import P + P.logger.error(f"Migration Error: {e}") + import traceback + P.logger.error(traceback.format_exc()) diff --git a/setup.py b/setup.py index b351b95..ca2c8f3 100644 --- a/setup.py +++ b/setup.py @@ -21,7 +21,7 @@ setting = { 'home_module': 'queue', 'menu': { 'uri': __package__, - 'name': 'Gommi 다운로더', + 'name': 'GDM', 'list': [ { 'uri': 'queue', diff --git a/templates/gommi_download_manager_queue_list.html b/templates/gommi_download_manager_queue_list.html index a4f3fe5..99a55e3 100644 --- a/templates/gommi_download_manager_queue_list.html +++ b/templates/gommi_download_manager_queue_list.html @@ -2,281 +2,563 @@ {% import "macro.html" as macros %} {% block content %} + + + + + +
-
-
-
다운로드 목록
-
- - -
+ - + {% endblock %} diff --git a/templates/gommi_download_manager_queue_setting.html b/templates/gommi_download_manager_queue_setting.html index eaf4cbb..65052e5 100644 --- a/templates/gommi_download_manager_queue_setting.html +++ b/templates/gommi_download_manager_queue_setting.html @@ -2,152 +2,317 @@ {% import "macro.html" as macros %} {% block content %} + + + + + -
- {{ macros.m_row_start('5') }} - {{ macros.m_row_end() }} - -
-

GDM 설정

- {{ macros.m_button_group([['globalSettingSaveBtn', '설정 저장']]) }} +
+ - {{ macros.m_hr_head_bottom() }} -
- - {{ macros.setting_top_big('기본 설정') }} - {{ macros.setting_bottom() }} - - {{ macros.setting_input_text('save_path', '저장 경로', value=arg['save_path'], desc='{PATH_DATA}는 실제 데이터 경로로 치환됩니다.') }} - {{ macros.setting_input_text('temp_path', '임시 경로', value=arg['temp_path'], desc='다운로드 중 임시 파일 저장 경로') }} - {{ macros.setting_input_text('max_concurrent', '동시 다운로드 수', value=arg['max_concurrent'], desc='동시에 진행할 최대 다운로드 수') }} - {{ macros.setting_select('max_download_rate', '속도 제한', [['0', '무제한'], ['1M', '1 MB/s'], ['3M', '3 MB/s'], ['5M', '5 MB/s'], ['10M', '10 MB/s']], value=arg['max_download_rate'], desc='다운로드 속도를 제한합니다.') }} +
+ + +
General Settings
+ +
+ + + {PATH_DATA} will be replaced by the actual data path. +
- {{ macros.m_hr() }} +
+ + + Temporary storage path for files during download. +
- - {{ macros.setting_top_big('다운로더 설정') }} - {{ macros.setting_bottom() }} - - {{ macros.setting_input_text('aria2c_path', 'aria2c 경로', value=arg['aria2c_path'], desc='aria2c 실행 파일 경로 (고속 다운로드용)') }} - {{ macros.setting_input_text('aria2c_connections', 'aria2c 연결 수', value=arg['aria2c_connections'], desc='aria2c 동시 연결 수 (기본 16)') }} - {{ macros.setting_input_text('ffmpeg_path', 'ffmpeg 경로', value=arg['ffmpeg_path'], desc='ffmpeg 실행 파일 경로 (HLS 스트림용)') }} - {{ macros.setting_input_text('yt_dlp_path', 'yt-dlp 경로', value=arg['yt_dlp_path'], desc='비워두면 Python 모듈 사용') }} +
+
+
+ + +
+
+
+
+ + +
+
+
- {{ macros.m_hr() }} +
- - {{ macros.setting_top_big('재시도 설정') }} - {{ macros.setting_bottom() }} - - {{ macros.setting_checkbox('auto_retry', '자동 재시도', value=arg['auto_retry'], desc='다운로드 실패 시 자동으로 재시도') }} - {{ macros.setting_input_text('max_retry', '최대 재시도 횟수', value=arg['max_retry'], desc='최대 재시도 횟수') }} - - + +
External Tool Paths
+ +
+ + + Executable path for aria2c (used for high-speed downloads). +
+ +
+ + + Concurrent connections per download (default: 16). +
+ +
+ + + Executable path for ffmpeg (used for HLS streams). +
+ +
+ + + If empty, the Python module will be used. +
+ +
+ + +
Error Handling
+ +
+ + + Automatically retry failed downloads. +
+ +
+ + +
+ + +
{% endblock %} {% block tail_js %} ` - // I will explicitly add the save logic just in case the static JS relies on specific form IDs. - $(document).ready(function(){ - // Nothing special needed + // Handled by common framework }); $("body").on('click', '#globalSettingSaveBtn', function(e){ e.preventDefault(); var formData = get_formdata('#setting'); $.ajax({ - url: '/' + package_name + '/ajax/' + sub + '/setting_save', + url: `/${package_name}/ajax/${sub}/setting_save`, type: "POST", cache: false, data: formData, dataType: "json", success: function(ret) { if (ret.ret == 'success') { - $.notify('설정을 저장했습니다.', {type:'success'}); + $.notify('Settings Saved Successfully', {type:'success'}); } else { - $.notify('저장 실패: ' + ret.msg, {type:'danger'}); + $.notify('Save Failed: ' + ret.msg + '', {type:'danger'}); } } });