222 lines
9.4 KiB
Python
222 lines
9.4 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Playwright 기반 Anilife 비디오 URL 추출 스크립트
|
|
FlaskFarm의 gevent와 충돌을 피하기 위해 별도의 subprocess로 실행됩니다.
|
|
|
|
사용법:
|
|
python playwright_anilife.py <detail_url> <episode_num>
|
|
|
|
출력:
|
|
JSON 형식으로 _aldata 또는 에러 메시지 출력
|
|
"""
|
|
|
|
import sys
|
|
import json
|
|
import time
|
|
import re
|
|
|
|
def extract_aldata(detail_url: str, episode_num: str) -> dict:
|
|
"""Detail 페이지에서 에피소드를 클릭하고 _aldata를 추출합니다."""
|
|
|
|
try:
|
|
from playwright.sync_api import sync_playwright
|
|
except ImportError as e:
|
|
return {"error": f"Playwright not installed: {e}"}
|
|
|
|
result = {
|
|
"success": False,
|
|
"aldata": None,
|
|
"html": None,
|
|
"current_url": None,
|
|
"error": None,
|
|
"player_url": None
|
|
}
|
|
|
|
try:
|
|
with sync_playwright() as p:
|
|
# 시스템에 설치된 Chrome 사용
|
|
browser = p.chromium.launch(
|
|
headless=False, # visible 모드
|
|
channel="chrome", # 시스템 Chrome 사용
|
|
args=[
|
|
"--disable-blink-features=AutomationControlled",
|
|
"--disable-automation",
|
|
"--no-sandbox",
|
|
]
|
|
)
|
|
|
|
# 브라우저 컨텍스트 생성 (스텔스 설정)
|
|
context = browser.new_context(
|
|
viewport={"width": 1920, "height": 1080},
|
|
user_agent="Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
|
|
locale="ko-KR",
|
|
)
|
|
|
|
# navigator.webdriver 숨기기
|
|
context.add_init_script("""
|
|
Object.defineProperty(navigator, 'webdriver', {
|
|
get: () => undefined
|
|
});
|
|
""")
|
|
|
|
page = context.new_page()
|
|
|
|
try:
|
|
# 1. Detail 페이지 방문
|
|
page.goto(detail_url, wait_until="domcontentloaded", timeout=30000)
|
|
time.sleep(2)
|
|
|
|
# 2. 에피소드 찾아서 클릭 (episode_num을 포함하는 provider 링크)
|
|
episode_clicked = False
|
|
|
|
# 스크롤하여 에피소드 목록 로드
|
|
page.mouse.wheel(0, 800)
|
|
time.sleep(1)
|
|
|
|
# JavaScript로 에피소드 링크 찾아 클릭
|
|
try:
|
|
episode_href = page.evaluate(f"""
|
|
(() => {{
|
|
const links = Array.from(document.querySelectorAll('a[href*="/ani/provider/"]'));
|
|
const ep = links.find(a => a.innerText.includes('{episode_num}'));
|
|
if (ep) {{
|
|
ep.click();
|
|
return ep.href;
|
|
}}
|
|
return null;
|
|
}})()
|
|
""")
|
|
if episode_href:
|
|
episode_clicked = True
|
|
time.sleep(2)
|
|
except Exception as e:
|
|
result["error"] = f"Episode click failed: {e}"
|
|
|
|
if not episode_clicked:
|
|
result["error"] = f"Episode {episode_num} not found"
|
|
result["html"] = page.content()
|
|
return result
|
|
|
|
# 3. Provider 페이지에서 player_guid 추출 (버튼 클릭 대신)
|
|
# moveCloudvideo() 또는 moveJawcloud() 함수에서 GUID 추출
|
|
try:
|
|
player_info = page.evaluate("""
|
|
(() => {
|
|
// 함수 소스에서 GUID 추출 시도
|
|
let playerUrl = null;
|
|
|
|
// moveCloudvideo 함수 확인
|
|
if (typeof moveCloudvideo === 'function') {
|
|
const funcStr = moveCloudvideo.toString();
|
|
// URL 패턴 찾기
|
|
const match = funcStr.match(/['"]([^'"]+\\/h\\/live[^'"]+)['"]/);
|
|
if (match) {
|
|
playerUrl = match[1];
|
|
}
|
|
}
|
|
|
|
// moveJawcloud 함수 확인
|
|
if (!playerUrl && typeof moveJawcloud === 'function') {
|
|
const funcStr = moveJawcloud.toString();
|
|
const match = funcStr.match(/['"]([^'"]+\\/h\\/live[^'"]+)['"]/);
|
|
if (match) {
|
|
playerUrl = match[1];
|
|
}
|
|
}
|
|
|
|
// 페이지 변수 확인
|
|
if (!playerUrl && typeof _player_guid !== 'undefined') {
|
|
playerUrl = '/h/live?p=' + _player_guid + '&player=jawcloud';
|
|
}
|
|
|
|
// onclick 속성에서 추출
|
|
if (!playerUrl) {
|
|
const btn = document.querySelector('a[onclick*="moveCloudvideo"], a[onclick*="moveJawcloud"]');
|
|
if (btn) {
|
|
const onclick = btn.getAttribute('onclick');
|
|
// 함수 이름 확인 후 페이지 소스에서 URL 추출
|
|
}
|
|
}
|
|
|
|
// 전역 변수 검색
|
|
if (!playerUrl) {
|
|
for (const key of Object.keys(window)) {
|
|
if (key.includes('player') || key.includes('guid')) {
|
|
const val = window[key];
|
|
if (typeof val === 'string' && val.match(/^[a-f0-9-]{36}$/)) {
|
|
playerUrl = '/h/live?p=' + val + '&player=jawcloud';
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// _aldata 직접 확인 (provider 페이지에 있을 수 있음)
|
|
if (typeof _aldata !== 'undefined') {
|
|
return { aldata: _aldata, playerUrl: null };
|
|
}
|
|
|
|
return { aldata: null, playerUrl: playerUrl };
|
|
})()
|
|
""")
|
|
|
|
if player_info.get("aldata"):
|
|
result["aldata"] = player_info["aldata"]
|
|
result["success"] = True
|
|
result["current_url"] = page.url
|
|
return result
|
|
|
|
result["player_url"] = player_info.get("playerUrl")
|
|
|
|
except Exception as e:
|
|
result["error"] = f"Player info extraction failed: {e}"
|
|
|
|
# 4. Player URL이 있으면 해당 페이지로 이동하여 _aldata 추출
|
|
if result.get("player_url"):
|
|
player_full_url = "https://anilife.live" + result["player_url"] if result["player_url"].startswith("/") else result["player_url"]
|
|
page.goto(player_full_url, wait_until="domcontentloaded", timeout=30000)
|
|
time.sleep(2)
|
|
|
|
# _aldata 추출
|
|
try:
|
|
aldata_value = page.evaluate("typeof _aldata !== 'undefined' ? _aldata : null")
|
|
if aldata_value:
|
|
result["aldata"] = aldata_value
|
|
result["success"] = True
|
|
except Exception as e:
|
|
pass
|
|
|
|
# 현재 URL 기록
|
|
result["current_url"] = page.url
|
|
|
|
# HTML에서 _aldata 패턴 추출 시도
|
|
if not result["aldata"]:
|
|
html = page.content()
|
|
# _aldata = "..." 패턴 찾기
|
|
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
|
|
else:
|
|
result["html"] = html
|
|
|
|
finally:
|
|
context.close()
|
|
browser.close()
|
|
|
|
except Exception as e:
|
|
result["error"] = str(e)
|
|
|
|
return result
|
|
|
|
|
|
if __name__ == "__main__":
|
|
if len(sys.argv) < 3:
|
|
print(json.dumps({"error": "Usage: python playwright_anilife.py <detail_url> <episode_num>"}))
|
|
sys.exit(1)
|
|
|
|
detail_url = sys.argv[1]
|
|
episode_num = sys.argv[2]
|
|
result = extract_aldata(detail_url, episode_num)
|
|
print(json.dumps(result, ensure_ascii=False))
|