diff --git a/lib/camoufox_anilife.py b/lib/camoufox_anilife.py index 7634031..7f7dfb7 100644 --- a/lib/camoufox_anilife.py +++ b/lib/camoufox_anilife.py @@ -31,7 +31,12 @@ def extract_aldata(detail_url: str, episode_num: str) -> dict: try: # Camoufox 시작 (자동 fingerprint 생성) - with Camoufox(headless=False) as browser: + # Docker/서버 환경에서는 DISPLAY가 없으므로 headless 모드 사용 + import os + has_display = os.environ.get('DISPLAY') is not None + use_headless = not has_display + + with Camoufox(headless=use_headless) as browser: page = browser.new_page() try: diff --git a/lib/ffmpeg_queue_v1.py b/lib/ffmpeg_queue_v1.py index b29f45d..fcb9b36 100644 --- a/lib/ffmpeg_queue_v1.py +++ b/lib/ffmpeg_queue_v1.py @@ -88,7 +88,15 @@ class FfmpegQueueEntity(abc.ABCMeta("ABC", (object,), {"__slots__": ()})): tmp["callback_id"] = getattr(self, 'name', 'anilife') if hasattr(self, 'name') else 'anilife' tmp["start_time"] = self.created_time tmp["status_kor"] = self.ffmpeg_status_kor - tmp["status_str"] = str(self.ffmpeg_status) if self.ffmpeg_status != -1 else "WAITING" + # status_str: 템플릿에서 문자열 비교에 사용 (DOWNLOADING, COMPLETED, WAITING) + status_map = { + 0: "WAITING", + 1: "STARTED", + 5: "DOWNLOADING", + 7: "COMPLETED", + -1: "FAILED" + } + tmp["status_str"] = status_map.get(self.ffmpeg_status, "WAITING") tmp["percent"] = self.ffmpeg_percent tmp["duration_str"] = "" tmp["duration"] = "" diff --git a/lib/ytdlp_downloader.py b/lib/ytdlp_downloader.py index 20dd9f7..aa4104f 100644 --- a/lib/ytdlp_downloader.py +++ b/lib/ytdlp_downloader.py @@ -26,6 +26,7 @@ class YtdlpDownloader: self.cancelled = False self.process = None self.error_output = [] # 에러 메시지 저장 + self.total_duration_seconds = 0 # 전체 영상 길이 (초) # 속도 및 시간 계산용 self.start_time = None @@ -59,9 +60,53 @@ class YtdlpDownloader: else: return f"{bytes_per_sec / (1024 * 1024):.2f} MB/s" + def time_to_seconds(self, time_str): + """HH:MM:SS.ms 형식을 초로 변환""" + try: + if not time_str: + return 0 + parts = time_str.split(':') + if len(parts) != 3: + return 0 + h = float(parts[0]) + m = float(parts[1]) + s = float(parts[2]) + return h * 3600 + m * 60 + s + except Exception: + return 0 + + def _ensure_ytdlp_installed(self): + """yt-dlp가 설치되어 있는지 확인하고, 없으면 자동 설치""" + import shutil + + # yt-dlp binary가 PATH에 있는지 확인 + if shutil.which('yt-dlp') is not None: + return True + + logger.info("yt-dlp not found in PATH. Installing via pip...") + try: + result = subprocess.run( + [sys.executable, "-m", "pip", "install", "yt-dlp", "-q"], + capture_output=True, + text=True, + timeout=120 + ) + if result.returncode != 0: + logger.error(f"Failed to install yt-dlp: {result.stderr}") + return False + logger.info("yt-dlp installed successfully") + return True + except Exception as e: + logger.error(f"yt-dlp installation error: {e}") + return False + def download(self): """yt-dlp CLI를 통한 브라우저 흉내(Impersonate) 방식 다운로드 수행""" try: + # yt-dlp 설치 확인 + if not self._ensure_ytdlp_installed(): + return False, "yt-dlp installation failed" + self.start_time = time.time() # 출력 디렉토리 생성 @@ -76,14 +121,26 @@ class YtdlpDownloader: concat_char = '&' if '?' in current_url else '?' current_url = f"{current_url}{concat_char}dummy=.m3u8" - # 1. 기본 명령어 구성 (Impersonate & HLS 강제) + # 1. 기본 명령어 구성 (Impersonate & HLS 옵션) + # hlz CDN (linkkf)은 .jpg 확장자로 위장된 TS 세그먼트를 사용 + # ffmpeg 8.0에서 이를 인식하지 못하므로 native HLS 다운로더 사용 + use_native_hls = 'hlz' in current_url and '.top/' in current_url + cmd = [ 'yt-dlp', '--newline', '--no-playlist', '--no-part', - '--hls-prefer-ffmpeg', - '--hls-use-mpegts', + ] + + if use_native_hls: + # hlz CDN: native HLS 다운로더 사용 (ffmpeg의 확장자 제한 우회) + cmd += ['--hls-prefer-native'] + else: + # 기타 CDN: ffmpeg 사용 (더 안정적) + cmd += ['--hls-prefer-ffmpeg', '--hls-use-mpegts'] + + cmd += [ '--no-check-certificate', '--progress', '--verbose', # 디버깅용 상세 로그 @@ -121,6 +178,17 @@ class YtdlpDownloader: cmd += ['--referer', 'https://cdndania.com/'] cmd += ['--add-header', 'Origin:https://cdndania.com'] cmd += ['--add-header', 'X-Requested-With:XMLHttpRequest'] + + # linkkf CDN (hlz3.top, hlz2.top 등) 헤더 보강 + if 'hlz' in current_url and '.top/' in current_url: + # hlz CDN은 자체 도메인을 Referer로 요구함 + from urllib.parse import urlparse + parsed = urlparse(current_url) + cdn_origin = f"{parsed.scheme}://{parsed.netloc}" + if not has_referer: + cmd += ['--referer', cdn_origin + '/'] + cmd += ['--add-header', f'Origin:{cdn_origin}'] + cmd += ['--add-header', 'Accept:*/*'] cmd.append(current_url) @@ -136,13 +204,23 @@ class YtdlpDownloader: ) # 여러 진행률 형식 매칭 - # [download] 10.5% of ~100.00MiB at 2.45MiB/s - # [download] 10.5% of 100.00MiB at 2.45MiB/s ETA 00:30 - # [download] 100% of 100.00MiB + # yt-dlp native: [download] 10.5% of ~100.00MiB at 2.45MiB/s + # yt-dlp native: [download] 10.5% of 100.00MiB at 2.45MiB/s ETA 00:30 + # yt-dlp native: [download] 100% of 100.00MiB + # ffmpeg: frame= 1234 fps= 30 size= 12345kB time=00:01:23.45 bitrate=1234.5kbits/s + # ffmpeg: size= 123456kB time=00:01:23.45 prog_patterns = [ re.compile(r'\[download\]\s+(?P[\d\.]+)%\s+of\s+.*?(?:\s+at\s+(?P[\d\.]+\s*\w+/s))?'), re.compile(r'\[download\]\s+(?P[\d\.]+)%'), + # ffmpeg time 출력 파싱 (time=HH:MM:SS.ms) + re.compile(r'time=(?P