diff --git a/lib/camoufox_anilife.py b/lib/camoufox_anilife.py index 5bd1d96..996d3d0 100644 --- a/lib/camoufox_anilife.py +++ b/lib/camoufox_anilife.py @@ -11,6 +11,136 @@ import sys import json import time import re +import os + +def _run_browser(browser, detail_url, episode_num, result): + """실제 브라우저 작업을 수행하는 내부 함수""" + page = browser.new_page() + try: + # 1. Detail 페이지로 이동 + print(f"1. Navigating to detail page: {detail_url}", file=sys.stderr) + page.goto(detail_url, wait_until="domcontentloaded", timeout=30000) + time.sleep(2) + + print(f" Current URL: {page.url}", file=sys.stderr) + + # 2. 에피소드 목록으로 스크롤 + page.mouse.wheel(0, 800) + time.sleep(1) + + # 3. 해당 에피소드 찾아서 클릭 + print(f"2. Looking for episode {episode_num}", file=sys.stderr) + + episode_clicked = False + try: + # epl-num 클래스의 div에서 에피소드 번호 찾기 + episode_link = page.locator(f'a:has(.epl-num:text("{episode_num}"))').first + if episode_link.is_visible(timeout=5000): + href = episode_link.get_attribute("href") + print(f" Found episode link: {href}", file=sys.stderr) + episode_link.click() + episode_clicked = True + time.sleep(3) + except Exception as e: + print(f" Method 1 failed: {e}", file=sys.stderr) + + if not episode_clicked: + try: + # provider 링크들 중에서 에피소드 번호가 포함된 것 클릭 + links = page.locator('a[href*="/ani/provider/"]').all() + for link in links: + text = link.inner_text() + if episode_num in text: + print(f" Found: {text}", file=sys.stderr) + link.click() + episode_clicked = True + time.sleep(3) + break + except Exception as e: + print(f" Method 2 failed: {e}", file=sys.stderr) + + if not episode_clicked: + result["error"] = f"Episode {episode_num} not found" + result["html"] = page.content() + return result + + # 4. Provider 페이지에서 _aldata 추출 + print(f"3. Provider page URL: {page.url}", file=sys.stderr) + result["current_url"] = page.url + + # 리다이렉트 확인 + if "/ani/provider/" not in page.url: + result["error"] = f"Redirected to {page.url}" + result["html"] = page.content() + return result + + # _aldata 추출 시도 + try: + aldata_value = page.evaluate("typeof _aldata !== 'undefined' ? _aldata : null") + if aldata_value: + result["aldata"] = aldata_value + result["success"] = True + print(f" SUCCESS! _aldata found: {aldata_value[:60]}...", file=sys.stderr) + return result + except Exception as js_err: + print(f" JS error: {js_err}", file=sys.stderr) + + # HTML에서 _aldata 패턴 추출 시도 + html = page.content() + aldata_match = re.search(r'_aldata\s*=\s*["\']([A-Za-z0-9+/=]+)["\']', html) + if aldata_match: + result["aldata"] = aldata_match.group(1) + result["success"] = True + print(f" SUCCESS! _aldata from HTML: {result['aldata'][:60]}...", file=sys.stderr) + return result + + # 5. CloudVideo 버튼 클릭 시도 + print("4. Trying CloudVideo button click...", file=sys.stderr) + try: + page.mouse.wheel(0, 500) + time.sleep(1) + + cloudvideo_btn = page.locator('a[onclick*="moveCloudvideo"], a[onclick*="moveJawcloud"]').first + if cloudvideo_btn.is_visible(timeout=3000): + cloudvideo_btn.click() + time.sleep(3) + + result["current_url"] = page.url + print(f" After click URL: {page.url}", file=sys.stderr) + + # 리다이렉트 확인 (구글로 갔는지) + if "google.com" in page.url: + result["error"] = "Redirected to Google - bot detected" + return result + + # 플레이어 페이지에서 _aldata 추출 + try: + aldata_value = page.evaluate("typeof _aldata !== 'undefined' ? _aldata : null") + if aldata_value: + result["aldata"] = aldata_value + result["success"] = True + print(f" SUCCESS! _aldata: {aldata_value[:60]}...", file=sys.stderr) + return result + except: + pass + + # HTML에서 추출 + html = page.content() + aldata_match = re.search(r'_aldata\s*=\s*["\']([A-Za-z0-9+/=]+)["\']', html) + if aldata_match: + result["aldata"] = aldata_match.group(1) + result["success"] = True + return result + + result["html"] = html + except Exception as click_err: + print(f" Click error: {click_err}", file=sys.stderr) + result["html"] = page.content() + + finally: + page.close() + + return result def extract_aldata(detail_url: str, episode_num: str) -> dict: """Camoufox로 Detail 페이지에서 _aldata 추출""" @@ -21,155 +151,31 @@ def extract_aldata(detail_url: str, episode_num: str) -> dict: return {"error": f"Camoufox not installed: {e}"} result = { - "success": False, - "aldata": None, - "html": None, - "current_url": None, - "error": None, - "vod_url": None + "success": False, "aldata": None, "html": None, + "current_url": None, "error": None, "vod_url": None } try: - # Camoufox 시작 (자동 fingerprint 생성) - # Docker/서버 환경에서는 DISPLAY가 없으므로 Xvfb 가상 디스플레이 사용 - import os + # Docker/서버 환경에서는 DISPLAY가 없으므로 Xvfb 가상 디스플레이 사용 시도 has_display = os.environ.get('DISPLAY') is not None if not has_display: print(" No DISPLAY detected. Using Virtual Display (Xvfb) for better stealth.", file=sys.stderr) - # Docker 등 GUI 없는 환경에서는 xvfb=True, headless=False 조합이 가장 스텔스성이 높음 camou_args = {"headless": False, "xvfb": True} else: - # 로컬 GUI 환경에서는 일반 실행 camou_args = {"headless": False} - with Camoufox(**camou_args) as browser: - page = browser.new_page() + # xvfb 인자 지원 여부에 따른 안전한 실행 (Try-Except Fallback) + try: + with Camoufox(**camou_args) as browser: + return _run_browser(browser, detail_url, episode_num, result) + except TypeError as e: + if "xvfb" in str(e): + print(f" Warning: Local Camoufox version too old for 'xvfb'. Falling back to headless.", file=sys.stderr) + with Camoufox(headless=True) as browser: + return _run_browser(browser, detail_url, episode_num, result) + raise e - try: - # 1. Detail 페이지로 이동 - print(f"1. Navigating to detail page: {detail_url}", file=sys.stderr) - page.goto(detail_url, wait_until="domcontentloaded", timeout=30000) - time.sleep(2) - - print(f" Current URL: {page.url}", file=sys.stderr) - - # 2. 에피소드 목록으로 스크롤 - page.mouse.wheel(0, 800) - time.sleep(1) - - # 3. 해당 에피소드 찾아서 클릭 - print(f"2. Looking for episode {episode_num}", file=sys.stderr) - - episode_clicked = False - try: - # epl-num 클래스의 div에서 에피소드 번호 찾기 - episode_link = page.locator(f'a:has(.epl-num:text("{episode_num}"))').first - if episode_link.is_visible(timeout=5000): - href = episode_link.get_attribute("href") - print(f" Found episode link: {href}", file=sys.stderr) - episode_link.click() - episode_clicked = True - time.sleep(3) - except Exception as e: - print(f" Method 1 failed: {e}", file=sys.stderr) - - if not episode_clicked: - try: - # provider 링크들 중에서 에피소드 번호가 포함된 것 클릭 - links = page.locator('a[href*="/ani/provider/"]').all() - for link in links: - text = link.inner_text() - if episode_num in text: - print(f" Found: {text}", file=sys.stderr) - link.click() - episode_clicked = True - time.sleep(3) - break - except Exception as e: - print(f" Method 2 failed: {e}", file=sys.stderr) - - if not episode_clicked: - result["error"] = f"Episode {episode_num} not found" - result["html"] = page.content() - return result - - # 4. Provider 페이지에서 _aldata 추출 - print(f"3. Provider page URL: {page.url}", file=sys.stderr) - result["current_url"] = page.url - - # 리다이렉트 확인 - if "/ani/provider/" not in page.url: - result["error"] = f"Redirected to {page.url}" - result["html"] = page.content() - return result - - # _aldata 추출 시도 - try: - aldata_value = page.evaluate("typeof _aldata !== 'undefined' ? _aldata : null") - if aldata_value: - result["aldata"] = aldata_value - result["success"] = True - print(f" SUCCESS! _aldata found: {aldata_value[:60]}...", file=sys.stderr) - return result - except Exception as js_err: - print(f" JS error: {js_err}", file=sys.stderr) - - # HTML에서 _aldata 패턴 추출 시도 - html = page.content() - aldata_match = re.search(r'_aldata\s*=\s*["\']([A-Za-z0-9+/=]+)["\']', html) - if aldata_match: - result["aldata"] = aldata_match.group(1) - result["success"] = True - print(f" SUCCESS! _aldata from HTML: {result['aldata'][:60]}...", file=sys.stderr) - return result - - # 5. CloudVideo 버튼 클릭 시도 - print("4. Trying CloudVideo button click...", file=sys.stderr) - try: - page.mouse.wheel(0, 500) - time.sleep(1) - - cloudvideo_btn = page.locator('a[onclick*="moveCloudvideo"], a[onclick*="moveJawcloud"]').first - if cloudvideo_btn.is_visible(timeout=3000): - cloudvideo_btn.click() - time.sleep(3) - - result["current_url"] = page.url - print(f" After click URL: {page.url}", file=sys.stderr) - - # 리다이렉트 확인 (구글로 갔는지) - if "google.com" in page.url: - result["error"] = "Redirected to Google - bot detected" - return result - - # 플레이어 페이지에서 _aldata 추출 - try: - aldata_value = page.evaluate("typeof _aldata !== 'undefined' ? _aldata : null") - if aldata_value: - result["aldata"] = aldata_value - result["success"] = True - print(f" SUCCESS! _aldata: {aldata_value[:60]}...", file=sys.stderr) - return result - except: - pass - - # HTML에서 추출 - html = page.content() - aldata_match = re.search(r'_aldata\s*=\s*["\']([A-Za-z0-9+/=]+)["\']', html) - if aldata_match: - result["aldata"] = aldata_match.group(1) - result["success"] = True - return result - - result["html"] = html - except Exception as click_err: - print(f" Click error: {click_err}", file=sys.stderr) - result["html"] = page.content() - - finally: - page.close() - except Exception as e: result["error"] = str(e) import traceback @@ -177,7 +183,6 @@ def extract_aldata(detail_url: str, episode_num: str) -> dict: return result - if __name__ == "__main__": if len(sys.argv) < 3: print(json.dumps({"error": "Usage: python camoufox_anilife.py "})) diff --git a/mod_anilife.py b/mod_anilife.py index 37226be..fcb7085 100644 --- a/mod_anilife.py +++ b/mod_anilife.py @@ -1234,51 +1234,53 @@ class AniLifeQueueEntity(FfmpegQueueEntity): # Camoufox 설치 확인 및 자동 설치 def ensure_camoufox_installed(): - """Camoufox가 설치되어 있는지 확인하고, 없으면 자동 설치 - - Note: Docker 환경에서 import camoufox 시 trio/epoll 문제가 발생할 수 있으므로 - 실제 import 대신 importlib.util.find_spec으로 패키지 존재만 확인 - """ + """Camoufox 및 필수 시스템 패키지(xvfb) 설치 확인 및 자동 설치""" import importlib.util + import subprocess as sp + import shutil - # 패키지 존재 여부만 확인 (import 하지 않음) - if importlib.util.find_spec("camoufox") is not None: - return True + # 1. 시스템 패키지 xvfb 설치 확인 (Linux/Docker 전용) + if platform.system() == 'Linux' and shutil.which('Xvfb') is None: + logger.info("Xvfb not found. Attempting to install system package...") + try: + # apt-get을 사용하는 데비안/우분투 계열 가정 (Docker 환경) + # sudo 없이 시도 (Docker는 보통 root 권한) + sp.run(['apt-get', 'update', '-qq'], capture_output=True) + sp.run(['apt-get', 'install', '-y', 'xvfb', '-qq'], capture_output=True) + if shutil.which('Xvfb') is not None: + logger.info("Xvfb system package installed successfully") + except Exception as e: + logger.error(f"Failed to install xvfb system package: {e}") + + # 2. Camoufox 패키지 확인 및 업그레이드 + # 'xvfb' 인자는 최신 버전에서 지원되므로 존재하더라도 업그레이드 시도 + need_install = importlib.util.find_spec("camoufox") is None + + if need_install: + logger.info("Camoufox not installed. Installing latest version...") + else: + # 이미 설치되어 있어도 최신 버전으로 업그레이드 (xvfb 지원을 위해) + logger.info("Checking for Camoufox updates to ensure Xvfb support...") - logger.info("Camoufox not installed. Installing...") try: - import subprocess as sp + # pip 멤버로 설치/업그레이드 (camoufox[geoip] 포함) + cmd = [sys.executable, "-m", "pip", "install", "--upgrade", "camoufox[geoip]", "-q"] + pip_result = sp.run(cmd, capture_output=True, text=True, timeout=120) - # pip로 camoufox[geoip] 설치 - pip_result = sp.run( - [sys.executable, "-m", "pip", "install", "camoufox[geoip]", "-q"], - capture_output=True, - text=True, - timeout=120 - ) if pip_result.returncode != 0: - logger.error(f"Failed to install camoufox: {pip_result.stderr}") - return False - logger.info("Camoufox package installed successfully") + logger.error(f"Failed to install/upgrade camoufox: {pip_result.stderr}") + if need_install: return False # 새로 설치 중이었으면 중단 + else: + logger.info("Camoufox package installed/upgraded successfully") # Camoufox 브라우저 바이너리 다운로드 logger.info("Downloading Camoufox browser binary...") - fetch_result = sp.run( - [sys.executable, "-m", "camoufox", "fetch"], - capture_output=True, - text=True, - timeout=300 # 브라우저 다운로드는 시간이 걸릴 수 있음 - ) - if fetch_result.returncode != 0: - logger.warning(f"Camoufox browser fetch warning: {fetch_result.stderr}") - # fetch 실패해도 이미 있을 수 있으므로 계속 진행 - else: - logger.info("Camoufox browser binary installed successfully") + sp.run([sys.executable, "-m", "camoufox", "fetch"], capture_output=True, text=True, timeout=300) return True except Exception as install_err: - logger.error(f"Failed to install Camoufox: {install_err}") - return False + logger.error(f"Failed during Camoufox setup: {install_err}") + return not need_install # 이미 설치되어 있었으면 에러나도 일단 진행 시도 # Camoufox를 subprocess로 실행 (스텔스 Firefox - 봇 감지 우회) try: