refactor: Extract browser interaction logic from extract_aldata into a new _run_browser helper function.
This commit is contained in:
@@ -11,6 +11,136 @@ import sys
|
|||||||
import json
|
import json
|
||||||
import time
|
import time
|
||||||
import re
|
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:
|
def extract_aldata(detail_url: str, episode_num: str) -> dict:
|
||||||
"""Camoufox로 Detail 페이지에서 _aldata 추출"""
|
"""Camoufox로 Detail 페이지에서 _aldata 추출"""
|
||||||
@@ -21,155 +151,31 @@ def extract_aldata(detail_url: str, episode_num: str) -> dict:
|
|||||||
return {"error": f"Camoufox not installed: {e}"}
|
return {"error": f"Camoufox not installed: {e}"}
|
||||||
|
|
||||||
result = {
|
result = {
|
||||||
"success": False,
|
"success": False, "aldata": None, "html": None,
|
||||||
"aldata": None,
|
"current_url": None, "error": None, "vod_url": None
|
||||||
"html": None,
|
|
||||||
"current_url": None,
|
|
||||||
"error": None,
|
|
||||||
"vod_url": None
|
|
||||||
}
|
}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Camoufox 시작 (자동 fingerprint 생성)
|
# Docker/서버 환경에서는 DISPLAY가 없으므로 Xvfb 가상 디스플레이 사용 시도
|
||||||
# Docker/서버 환경에서는 DISPLAY가 없으므로 Xvfb 가상 디스플레이 사용
|
|
||||||
import os
|
|
||||||
has_display = os.environ.get('DISPLAY') is not None
|
has_display = os.environ.get('DISPLAY') is not None
|
||||||
|
|
||||||
if not has_display:
|
if not has_display:
|
||||||
print(" No DISPLAY detected. Using Virtual Display (Xvfb) for better stealth.", file=sys.stderr)
|
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}
|
camou_args = {"headless": False, "xvfb": True}
|
||||||
else:
|
else:
|
||||||
# 로컬 GUI 환경에서는 일반 실행
|
|
||||||
camou_args = {"headless": False}
|
camou_args = {"headless": False}
|
||||||
|
|
||||||
with Camoufox(**camou_args) as browser:
|
# xvfb 인자 지원 여부에 따른 안전한 실행 (Try-Except Fallback)
|
||||||
page = browser.new_page()
|
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:
|
except Exception as e:
|
||||||
result["error"] = str(e)
|
result["error"] = str(e)
|
||||||
import traceback
|
import traceback
|
||||||
@@ -177,7 +183,6 @@ def extract_aldata(detail_url: str, episode_num: str) -> dict:
|
|||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
if len(sys.argv) < 3:
|
if len(sys.argv) < 3:
|
||||||
print(json.dumps({"error": "Usage: python camoufox_anilife.py <detail_url> <episode_num>"}))
|
print(json.dumps({"error": "Usage: python camoufox_anilife.py <detail_url> <episode_num>"}))
|
||||||
|
|||||||
@@ -1234,51 +1234,53 @@ class AniLifeQueueEntity(FfmpegQueueEntity):
|
|||||||
|
|
||||||
# Camoufox 설치 확인 및 자동 설치
|
# Camoufox 설치 확인 및 자동 설치
|
||||||
def ensure_camoufox_installed():
|
def ensure_camoufox_installed():
|
||||||
"""Camoufox가 설치되어 있는지 확인하고, 없으면 자동 설치
|
"""Camoufox 및 필수 시스템 패키지(xvfb) 설치 확인 및 자동 설치"""
|
||||||
|
|
||||||
Note: Docker 환경에서 import camoufox 시 trio/epoll 문제가 발생할 수 있으므로
|
|
||||||
실제 import 대신 importlib.util.find_spec으로 패키지 존재만 확인
|
|
||||||
"""
|
|
||||||
import importlib.util
|
import importlib.util
|
||||||
|
import subprocess as sp
|
||||||
|
import shutil
|
||||||
|
|
||||||
# 패키지 존재 여부만 확인 (import 하지 않음)
|
# 1. 시스템 패키지 xvfb 설치 확인 (Linux/Docker 전용)
|
||||||
if importlib.util.find_spec("camoufox") is not None:
|
if platform.system() == 'Linux' and shutil.which('Xvfb') is None:
|
||||||
return True
|
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:
|
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:
|
if pip_result.returncode != 0:
|
||||||
logger.error(f"Failed to install camoufox: {pip_result.stderr}")
|
logger.error(f"Failed to install/upgrade camoufox: {pip_result.stderr}")
|
||||||
return False
|
if need_install: return False # 새로 설치 중이었으면 중단
|
||||||
logger.info("Camoufox package installed successfully")
|
else:
|
||||||
|
logger.info("Camoufox package installed/upgraded successfully")
|
||||||
|
|
||||||
# Camoufox 브라우저 바이너리 다운로드
|
# Camoufox 브라우저 바이너리 다운로드
|
||||||
logger.info("Downloading Camoufox browser binary...")
|
logger.info("Downloading Camoufox browser binary...")
|
||||||
fetch_result = sp.run(
|
sp.run([sys.executable, "-m", "camoufox", "fetch"], capture_output=True, text=True, timeout=300)
|
||||||
[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")
|
|
||||||
|
|
||||||
return True
|
return True
|
||||||
except Exception as install_err:
|
except Exception as install_err:
|
||||||
logger.error(f"Failed to install Camoufox: {install_err}")
|
logger.error(f"Failed during Camoufox setup: {install_err}")
|
||||||
return False
|
return not need_install # 이미 설치되어 있었으면 에러나도 일단 진행 시도
|
||||||
|
|
||||||
# Camoufox를 subprocess로 실행 (스텔스 Firefox - 봇 감지 우회)
|
# Camoufox를 subprocess로 실행 (스텔스 Firefox - 봇 감지 우회)
|
||||||
try:
|
try:
|
||||||
|
|||||||
Reference in New Issue
Block a user