v0.6.10: Fix Ohli24 GDM integration and update README
This commit is contained in:
@@ -81,13 +81,15 @@
|
|||||||
|
|
||||||
## 📝 변경 이력 (Changelog)
|
## 📝 변경 이력 (Changelog)
|
||||||
|
|
||||||
### v0.6.0 (2026-01-07)
|
### v0.6.10 (2026-01-07)
|
||||||
|
- **Ohli24 GDM 연동 버그 수정**:
|
||||||
|
- `LogicOhli24.add` 메서드의 인덴트 오류 및 문법 오류 해결
|
||||||
|
- 다운로드 완료 시 Ohli24 DB 자동 업데이트 로직 안정화
|
||||||
|
- `__init__.py` 안정성 강화 (P.logic 지연 로딩 대응)
|
||||||
- **Anilife GDM 연동**:
|
- **Anilife GDM 연동**:
|
||||||
- `ModuleQueue` 연동으로 Anilife 다운로드가 GDM (Gommi Downloader Manager)으로 통합
|
- `ModuleQueue` 연동으로 Anilife 다운로드가 GDM (Gommi Downloader Manager)으로 통합
|
||||||
- Ohli24와 동일한 패턴으로 `source_type: "anilife"` 메타데이터 포함
|
- Ohli24와 동일한 패턴으로 `source_type: "anilife"` 메타데이터 포함
|
||||||
- Go FFMPEG 버튼 → **Go GDM** 버튼으로 변경 및 GDM 큐 페이지로 링크
|
- Go FFMPEG 버튼 → **Go GDM** 버튼으로 변경 및 GDM 큐 페이지로 링크
|
||||||
- **HTTP 캐싱 준비**:
|
|
||||||
- `CachedSession` import 추가 (향후 requests 캐싱 확장 가능)
|
|
||||||
- **파일명 정리 개선**:
|
- **파일명 정리 개선**:
|
||||||
- `Util.change_text_for_use_filename()` 함수에서 연속 점(`..`) → 단일 점(`.`) 변환
|
- `Util.change_text_for_use_filename()` 함수에서 연속 점(`..`) → 단일 점(`.`) 변환
|
||||||
- 끝에 오는 점/공백 자동 제거로 Synology NAS에서 Windows 8.3 단축 파일명 생성 방지
|
- 끝에 오는 점/공백 자동 제거로 Synology NAS에서 Windows 8.3 단축 파일명 생성 방지
|
||||||
|
|||||||
18
__init__.py
18
__init__.py
@@ -4,9 +4,15 @@
|
|||||||
# @Site :
|
# @Site :
|
||||||
# @File : __init__
|
# @File : __init__
|
||||||
# @Software: PyCharm
|
# @Software: PyCharm
|
||||||
# from .plugin import P
|
from .setup import P
|
||||||
# blueprint = P.blueprint
|
blueprint = P.blueprint
|
||||||
# menu = P.menu
|
menu = P.menu
|
||||||
# plugin_load = P.logic.plugin_load
|
plugin_info = P.plugin_info
|
||||||
# plugin_unload = P.logic.plugin_unload
|
|
||||||
# plugin_info = P.plugin_info
|
def plugin_load():
|
||||||
|
if P.logic:
|
||||||
|
P.logic.plugin_load()
|
||||||
|
|
||||||
|
def plugin_unload():
|
||||||
|
if P.logic:
|
||||||
|
P.logic.plugin_unload()
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
title: "애니 다운로더"
|
title: "애니 다운로더"
|
||||||
version: "0.6.9"
|
version: "0.6.10"
|
||||||
package_name: "anime_downloader"
|
package_name: "anime_downloader"
|
||||||
developer: "projectdx"
|
developer: "projectdx"
|
||||||
description: "anime downloader"
|
description: "anime downloader"
|
||||||
|
|||||||
@@ -7,22 +7,55 @@ Botasaurus 기반 Ohli24 HTML 페칭 스크립트
|
|||||||
|
|
||||||
import sys
|
import sys
|
||||||
import json
|
import json
|
||||||
|
import os
|
||||||
import time
|
import time
|
||||||
import traceback
|
from typing import Dict, Any, Optional
|
||||||
|
|
||||||
def fetch_html(url, headers=None, proxy=None):
|
# 봇사우루스 디버깅 일시정지 방지 및 자동 종료 설정
|
||||||
result = {"success": False, "html": "", "elapsed": 0}
|
os.environ["BOTASAURUS_ENV"] = "production"
|
||||||
start_time = time.time()
|
|
||||||
|
def fetch_html(url: str, headers: Optional[Dict[str, str]] = None, proxy: Optional[str] = None) -> Dict[str, Any]:
|
||||||
|
result: Dict[str, Any] = {"success": False, "html": "", "elapsed": 0}
|
||||||
|
start_time: float = time.time()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from botasaurus.request import request as b_request
|
from botasaurus.request import request as b_request
|
||||||
|
|
||||||
@b_request(headers=headers, use_stealth=True, proxy=proxy)
|
# raise_exception=True는 에러 시 exception을 발생시키게 함
|
||||||
def fetch_url(request, data):
|
# close_on_crash=True는 에러 발생 시 대기하지 않고 즉시 종료 (배포 환경용)
|
||||||
return request.get(data)
|
@b_request(proxy=proxy, raise_exception=True, close_on_crash=True)
|
||||||
|
def fetch_url(request: Any, data: Dict[str, Any]) -> str:
|
||||||
|
target_url = data.get('url')
|
||||||
|
headers = data.get('headers') or {}
|
||||||
|
|
||||||
|
# 기본적인 헤더 보강 (Ohli24 대응 - Cloudflare 우회 시도)
|
||||||
|
default_headers = {
|
||||||
|
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36",
|
||||||
|
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7",
|
||||||
|
"Accept-Language": "ko-KR,ko;q=0.9,en-US;q=0.8,en;q=0.7",
|
||||||
|
"Accept-Encoding": "gzip, deflate, br, zstd",
|
||||||
|
"Connection": "keep-alive",
|
||||||
|
"Upgrade-Insecure-Requests": "1",
|
||||||
|
"Sec-Fetch-Dest": "document",
|
||||||
|
"Sec-Fetch-Mode": "navigate",
|
||||||
|
"Sec-Fetch-Site": "none",
|
||||||
|
"Sec-Fetch-User": "?1",
|
||||||
|
"Cache-Control": "max-age=0",
|
||||||
|
"sec-ch-ua": '"Chromium";v="124", "Google Chrome";v="124", "Not-A.Brand";v="99"',
|
||||||
|
"sec-ch-ua-mobile": "?0",
|
||||||
|
"sec-ch-ua-platform": '"macOS"',
|
||||||
|
}
|
||||||
|
|
||||||
|
for k, v in default_headers.items():
|
||||||
|
if k not in headers and k.lower() not in [hk.lower() for hk in headers]:
|
||||||
|
headers[k] = v
|
||||||
|
|
||||||
|
return request.get(target_url, headers=headers, timeout=30)
|
||||||
|
|
||||||
b_resp = fetch_url(url)
|
# 봇사우루스는 실패 시 자동 재시도 등을 하기도 함.
|
||||||
elapsed = time.time() - start_time
|
# 여기서는 단발성 요청이므로 직접 호출.
|
||||||
|
b_resp: str = fetch_url({'url': url, 'headers': headers})
|
||||||
|
elapsed: float = time.time() - start_time
|
||||||
|
|
||||||
if b_resp and len(b_resp) > 10:
|
if b_resp and len(b_resp) > 10:
|
||||||
result.update({
|
result.update({
|
||||||
@@ -36,7 +69,6 @@ def fetch_html(url, headers=None, proxy=None):
|
|||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
result["error"] = str(e)
|
result["error"] = str(e)
|
||||||
result["traceback"] = traceback.format_exc()
|
|
||||||
result["elapsed"] = round(time.time() - start_time, 2)
|
result["elapsed"] = round(time.time() - start_time, 2)
|
||||||
|
|
||||||
return result
|
return result
|
||||||
@@ -46,9 +78,9 @@ if __name__ == "__main__":
|
|||||||
print(json.dumps({"success": False, "error": "Usage: python botasaurus_ohli24.py <url> [headers_json] [proxy]"}))
|
print(json.dumps({"success": False, "error": "Usage: python botasaurus_ohli24.py <url> [headers_json] [proxy]"}))
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
target_url = sys.argv[1]
|
target_url: str = sys.argv[1]
|
||||||
headers_arg = json.loads(sys.argv[2]) if len(sys.argv) > 2 and sys.argv[2] else None
|
headers_arg: Optional[Dict[str, str]] = json.loads(sys.argv[2]) if len(sys.argv) > 2 and sys.argv[2] else None
|
||||||
proxy_arg = sys.argv[3] if len(sys.argv) > 3 and sys.argv[3] else None
|
proxy_arg: Optional[str] = sys.argv[3] if len(sys.argv) > 3 and sys.argv[3] else None
|
||||||
|
|
||||||
res = fetch_html(target_url, headers_arg, proxy_arg)
|
res: Dict[str, Any] = fetch_html(target_url, headers_arg, proxy_arg)
|
||||||
print(json.dumps(res, ensure_ascii=False))
|
print(json.dumps(res, ensure_ascii=False))
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import traceback
|
|||||||
from http.server import HTTPServer, BaseHTTPRequestHandler
|
from http.server import HTTPServer, BaseHTTPRequestHandler
|
||||||
from threading import Thread, Lock
|
from threading import Thread, Lock
|
||||||
from typing import Any, Optional, Dict, List, Type, cast
|
from typing import Any, Optional, Dict, List, Type, cast
|
||||||
|
import zendriver as zd
|
||||||
|
|
||||||
# 터미널 및 파일로 로그 출력 설정
|
# 터미널 및 파일로 로그 출력 설정
|
||||||
LOG_FILE: str = "/tmp/zendriver_daemon.log"
|
LOG_FILE: str = "/tmp/zendriver_daemon.log"
|
||||||
@@ -38,38 +39,51 @@ loop: Optional[asyncio.AbstractEventLoop] = None
|
|||||||
manual_browser_path: Optional[str] = None
|
manual_browser_path: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
def find_browser_executable() -> Optional[str]:
|
def find_browser_executable() -> List[str]:
|
||||||
"""시스템에서 브라우저 실행 파일 찾기 (Docker/Ubuntu 환경 대응)"""
|
"""시스템에서 브라우저 실행 파일 찾기 (OS별 대응)"""
|
||||||
|
import platform
|
||||||
|
import shutil
|
||||||
|
|
||||||
# 수동 설정된 경로 최우선
|
# 수동 설정된 경로 최우선
|
||||||
if manual_browser_path and os.path.exists(manual_browser_path):
|
if manual_browser_path and os.path.exists(manual_browser_path):
|
||||||
return manual_browser_path
|
return [manual_browser_path]
|
||||||
|
|
||||||
common_paths: List[str] = [
|
system = platform.system()
|
||||||
"/usr/bin/google-chrome",
|
app_dirs = ["/Applications", "/Volumes/WD/Users/Applications"]
|
||||||
"/usr/bin/google-chrome-stable",
|
common_paths = []
|
||||||
"/usr/bin/chromium-browser",
|
|
||||||
"/usr/bin/chromium",
|
|
||||||
"/usr/lib/chromium-browser/chromium-browser",
|
|
||||||
"google-chrome", # PATH에서 찾기
|
|
||||||
"chromium-browser",
|
|
||||||
"chromium",
|
|
||||||
]
|
|
||||||
|
|
||||||
# 먼저 절대 경로 확인
|
if system == "Darwin": # Mac
|
||||||
for path in common_paths:
|
for base in app_dirs:
|
||||||
if path.startswith("/") and os.path.exists(path):
|
common_paths.extend([
|
||||||
log_debug(f"[ZendriverDaemon] Found browser at absolute path: {path}")
|
f"{base}/Google Chrome.app/Contents/MacOS/Google Chrome",
|
||||||
return path
|
f"{base}/Chromium.app/Contents/MacOS/Chromium",
|
||||||
|
f"{base}/Microsoft Edge.app/Contents/MacOS/Microsoft Edge",
|
||||||
# shutil.which로 PATH 확인
|
])
|
||||||
import shutil
|
elif system == "Windows":
|
||||||
for cmd in ["google-chrome", "google-chrome-stable", "chromium-browser", "chromium"]:
|
common_paths = [
|
||||||
|
os.path.expandvars(r"%ProgramFiles%\Google\Chrome\Application\chrome.exe"),
|
||||||
|
os.path.expandvars(r"%ProgramFiles(x86)%\Google\Chrome\Application\chrome.exe"),
|
||||||
|
os.path.expandvars(r"%LocalAppData%\Google\Chrome\Application\chrome.exe"),
|
||||||
|
]
|
||||||
|
else: # Linux/Other
|
||||||
|
common_paths = [
|
||||||
|
"/usr/bin/google-chrome",
|
||||||
|
"/usr/bin/google-chrome-stable",
|
||||||
|
"/usr/bin/chromium-browser",
|
||||||
|
"/usr/bin/chromium",
|
||||||
|
"/usr/lib/chromium-browser/chromium-browser",
|
||||||
|
]
|
||||||
|
|
||||||
|
# 존재하는 모든 후보들 반환
|
||||||
|
candidates = [p for p in common_paths if os.path.exists(p)]
|
||||||
|
|
||||||
|
# PATH에서 찾기 추가
|
||||||
|
for cmd in ["google-chrome", "google-chrome-stable", "chromium-browser", "chromium", "chrome", "microsoft-edge"]:
|
||||||
found = shutil.which(cmd)
|
found = shutil.which(cmd)
|
||||||
if found:
|
if found and found not in candidates:
|
||||||
log_debug(f"[ZendriverDaemon] Found browser via shutil.which: {found}")
|
candidates.append(found)
|
||||||
return found
|
|
||||||
|
|
||||||
return None
|
return candidates
|
||||||
|
|
||||||
|
|
||||||
class ZendriverHandler(BaseHTTPRequestHandler):
|
class ZendriverHandler(BaseHTTPRequestHandler):
|
||||||
@@ -154,30 +168,64 @@ async def ensure_browser() -> Any:
|
|||||||
with browser_lock:
|
with browser_lock:
|
||||||
if browser is None:
|
if browser is None:
|
||||||
try:
|
try:
|
||||||
import zendriver as zd
|
# 존재하는 후보군 가져오기
|
||||||
log_debug("[ZendriverDaemon] Starting new browser instance...")
|
candidates = find_browser_executable()
|
||||||
|
if not candidates:
|
||||||
|
log_debug("[ZendriverDaemon] No browser candidates found!")
|
||||||
|
return None
|
||||||
|
|
||||||
# 실행 가능한 브라우저 찾기
|
# 사용자 데이터 디렉토리 설정 (Mac/Root 권한 이슈 대응)
|
||||||
exec_path = find_browser_executable()
|
import tempfile
|
||||||
log_debug(f"[ZendriverDaemon] Startup params: headless=True, no_sandbox=True, path={exec_path}")
|
uid = os.getuid() if hasattr(os, 'getuid') else 'win'
|
||||||
|
|
||||||
if exec_path:
|
browser_args = [
|
||||||
log_debug(f"[ZendriverDaemon] Starting browser at: {exec_path}")
|
"--no-sandbox",
|
||||||
browser = await zd.start(
|
"--disable-setuid-sandbox",
|
||||||
headless=True,
|
"--disable-dev-shm-usage",
|
||||||
browser_executable_path=exec_path,
|
"--disable-gpu",
|
||||||
no_sandbox=True,
|
"--no-first-run",
|
||||||
browser_args=["--no-sandbox", "--disable-setuid-sandbox", "--disable-dev-shm-usage", "--disable-gpu", "--no-first-run"]
|
"--no-service-autorun",
|
||||||
)
|
"--password-store=basic",
|
||||||
else:
|
"--mute-audio",
|
||||||
log_debug("[ZendriverDaemon] Starting browser with default path")
|
"--disable-notifications",
|
||||||
browser = await zd.start(
|
"--disable-background-networking",
|
||||||
headless=True,
|
"--disable-background-timer-throttling",
|
||||||
no_sandbox=True,
|
"--disable-backgrounding-occluded-windows",
|
||||||
browser_args=["--no-sandbox", "--disable-setuid-sandbox", "--disable-dev-shm-usage", "--disable-gpu", "--no-first-run"]
|
"--disable-breakpad",
|
||||||
)
|
"--disable-client-side-phishing-detection",
|
||||||
|
"--disable-default-apps",
|
||||||
|
"--disable-hang-monitor",
|
||||||
|
"--disable-popup-blocking",
|
||||||
|
"--disable-prompt-on-repost",
|
||||||
|
"--disable-sync",
|
||||||
|
"--disable-translate",
|
||||||
|
"--metrics-recording-only",
|
||||||
|
"--no-default-browser-check",
|
||||||
|
"--safebrowsing-disable-auto-update",
|
||||||
|
"--remote-allow-origins=*",
|
||||||
|
"--blink-settings=imagesEnabled=false",
|
||||||
|
]
|
||||||
|
|
||||||
|
for exec_path in candidates:
|
||||||
|
user_data_dir = os.path.join(tempfile.gettempdir(), f"zd_daemon_{uid}_{os.path.basename(exec_path).replace(' ', '_')}")
|
||||||
|
os.makedirs(user_data_dir, exist_ok=True)
|
||||||
|
|
||||||
log_debug("[ZendriverDaemon] Browser started successfully")
|
try:
|
||||||
|
log_debug(f"[ZendriverDaemon] Trying browser at: {exec_path}")
|
||||||
|
browser = await zd.start(
|
||||||
|
headless=True,
|
||||||
|
browser_executable_path=exec_path,
|
||||||
|
no_sandbox=True,
|
||||||
|
user_data_dir=user_data_dir,
|
||||||
|
browser_args=browser_args
|
||||||
|
)
|
||||||
|
log_debug(f"[ZendriverDaemon] Browser started successfully with: {exec_path}")
|
||||||
|
return browser
|
||||||
|
except Exception as e:
|
||||||
|
log_debug(f"[ZendriverDaemon] Failed to start {exec_path}: {e}")
|
||||||
|
browser = None
|
||||||
|
|
||||||
|
raise Exception("All browser candidates failed to start")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log_debug(f"[ZendriverDaemon] Failed to start browser: {e}")
|
log_debug(f"[ZendriverDaemon] Failed to start browser: {e}")
|
||||||
browser = None
|
browser = None
|
||||||
@@ -209,9 +257,10 @@ async def fetch_with_browser(url: str, timeout: int = 30) -> Dict[str, Any]:
|
|||||||
# browser.get(url)은 새 탭을 열거나 기존 탭을 사용함
|
# browser.get(url)은 새 탭을 열거나 기존 탭을 사용함
|
||||||
page: Any = await browser.get(url)
|
page: Any = await browser.get(url)
|
||||||
|
|
||||||
# 페이지 로드 대기 - cdndania iframe 로딩될 때까지 폴링 (최대 15초)
|
# 페이지 로드 대기 - 지능형 폴링 (최대 10초)
|
||||||
max_wait = 15
|
# 1. 리스트 페이지는 바로 반환, 2. 에피소드 페이지는 플레이어 로딩 대기
|
||||||
poll_interval = 1
|
max_wait = 10
|
||||||
|
poll_interval = 0.2 # 1.0s -> 0.2s로 단축하여 반응속도 향상
|
||||||
waited = 0
|
waited = 0
|
||||||
html_content = ""
|
html_content = ""
|
||||||
|
|
||||||
@@ -220,9 +269,14 @@ async def fetch_with_browser(url: str, timeout: int = 30) -> Dict[str, Any]:
|
|||||||
waited += poll_interval
|
waited += poll_interval
|
||||||
html_content = await page.get_content()
|
html_content = await page.get_content()
|
||||||
|
|
||||||
# cdndania iframe이 로드되었는지 확인
|
# 리스트 페이지 마커 확인 (발견 즉시 탈출)
|
||||||
|
if "post-list" in html_content or "list-box" in html_content or "post-row" in html_content:
|
||||||
|
log_debug(f"[ZendriverDaemon] List page detected in {waited:.1f}s")
|
||||||
|
break
|
||||||
|
|
||||||
|
# cdndania/fireplayer iframe이 로드되었는지 확인 (에피소드 페이지)
|
||||||
if "cdndania" in html_content or "fireplayer" in html_content:
|
if "cdndania" in html_content or "fireplayer" in html_content:
|
||||||
log_debug(f"[ZendriverDaemon] cdndania/fireplayer found after {waited}s")
|
log_debug(f"[ZendriverDaemon] Player detected in {waited:.1f}s")
|
||||||
break
|
break
|
||||||
|
|
||||||
elapsed: float = time.time() - start_time
|
elapsed: float = time.time() - start_time
|
||||||
|
|||||||
@@ -15,31 +15,49 @@ import shutil
|
|||||||
|
|
||||||
|
|
||||||
def find_browser_executable(manual_path=None):
|
def find_browser_executable(manual_path=None):
|
||||||
"""시스템에서 브라우저 실행 파일 찾기 (Docker/Ubuntu 환경 대응)"""
|
"""시스템에서 브라우저 실행 파일 찾기 (OS별 대응)"""
|
||||||
|
import platform
|
||||||
|
|
||||||
# 수동 설정 시 우선
|
# 수동 설정 시 우선
|
||||||
if manual_path and os.path.exists(manual_path):
|
if manual_path and os.path.exists(manual_path):
|
||||||
return manual_path
|
return manual_path
|
||||||
|
|
||||||
common_paths = [
|
system = platform.system()
|
||||||
"/usr/bin/google-chrome",
|
app_dirs = ["/Applications", "/Volumes/WD/Users/Applications"]
|
||||||
"/usr/bin/google-chrome-stable",
|
common_paths = []
|
||||||
"/usr/bin/chromium-browser",
|
|
||||||
"/usr/bin/chromium",
|
|
||||||
"/usr/lib/chromium-browser/chromium-browser",
|
|
||||||
]
|
|
||||||
|
|
||||||
# 먼저 절대 경로 확인
|
if system == "Darwin": # Mac
|
||||||
for path in common_paths:
|
for base in app_dirs:
|
||||||
if os.path.exists(path):
|
common_paths.extend([
|
||||||
return path
|
f"{base}/Google Chrome.app/Contents/MacOS/Google Chrome",
|
||||||
|
f"{base}/Chromium.app/Contents/MacOS/Chromium",
|
||||||
# shutil.which로 PATH 확인
|
f"{base}/Microsoft Edge.app/Contents/MacOS/Microsoft Edge",
|
||||||
for cmd in ["google-chrome", "google-chrome-stable", "chromium-browser", "chromium"]:
|
])
|
||||||
|
elif system == "Windows":
|
||||||
|
common_paths = [
|
||||||
|
os.path.expandvars(r"%ProgramFiles%\Google\Chrome\Application\chrome.exe"),
|
||||||
|
os.path.expandvars(r"%ProgramFiles(x86)%\Google\Chrome\Application\chrome.exe"),
|
||||||
|
os.path.expandvars(r"%LocalAppData%\Google\Chrome\Application\chrome.exe"),
|
||||||
|
]
|
||||||
|
else: # Linux/Other
|
||||||
|
common_paths = [
|
||||||
|
"/usr/bin/google-chrome",
|
||||||
|
"/usr/bin/google-chrome-stable",
|
||||||
|
"/usr/bin/chromium-browser",
|
||||||
|
"/usr/bin/chromium",
|
||||||
|
"/usr/lib/chromium-browser/chromium-browser",
|
||||||
|
]
|
||||||
|
|
||||||
|
# 존재하는 모든 후보들 반환
|
||||||
|
candidates = [p for p in common_paths if os.path.exists(p)]
|
||||||
|
|
||||||
|
# PATH에서 찾기 추가
|
||||||
|
for cmd in ["google-chrome", "google-chrome-stable", "chromium-browser", "chromium", "chrome", "microsoft-edge"]:
|
||||||
found = shutil.which(cmd)
|
found = shutil.which(cmd)
|
||||||
if found:
|
if found and found not in candidates:
|
||||||
return found
|
candidates.append(found)
|
||||||
|
|
||||||
return None
|
return candidates
|
||||||
|
|
||||||
|
|
||||||
async def fetch_html(url: str, timeout: int = 60, browser_path: str = None) -> dict:
|
async def fetch_html(url: str, timeout: int = 60, browser_path: str = None) -> dict:
|
||||||
@@ -53,63 +71,112 @@ async def fetch_html(url: str, timeout: int = 60, browser_path: str = None) -> d
|
|||||||
start_time = asyncio.get_event_loop().time()
|
start_time = asyncio.get_event_loop().time()
|
||||||
browser = None
|
browser = None
|
||||||
|
|
||||||
try:
|
# 실행 가능한 브라우저 후보들 찾기
|
||||||
# 실행 가능한 브라우저 찾기
|
candidates = find_browser_executable(browser_path)
|
||||||
exec_path = find_browser_executable(browser_path)
|
if not candidates:
|
||||||
|
return {"success": False, "error": "No browser executable found", "html": ""}
|
||||||
|
|
||||||
# 브라우저 시작
|
# 사용자 데이터 디렉토리 설정 (Mac/Root 권한 이슈 대응)
|
||||||
if exec_path:
|
import tempfile
|
||||||
|
uid = os.getuid() if hasattr(os, 'getuid') else 'win'
|
||||||
|
|
||||||
|
# 공통 브라우저 인자
|
||||||
|
browser_args = [
|
||||||
|
"--no-sandbox",
|
||||||
|
"--disable-setuid-sandbox",
|
||||||
|
"--disable-dev-shm-usage",
|
||||||
|
"--disable-gpu",
|
||||||
|
"--no-first-run",
|
||||||
|
"--no-service-autorun",
|
||||||
|
"--password-store=basic",
|
||||||
|
"--mute-audio",
|
||||||
|
"--disable-notifications",
|
||||||
|
"--disable-background-networking",
|
||||||
|
"--disable-background-timer-throttling",
|
||||||
|
"--disable-backgrounding-occluded-windows",
|
||||||
|
"--disable-breakpad",
|
||||||
|
"--disable-client-side-phishing-detection",
|
||||||
|
"--disable-default-apps",
|
||||||
|
"--disable-hang-monitor",
|
||||||
|
"--disable-popup-blocking",
|
||||||
|
"--disable-prompt-on-repost",
|
||||||
|
"--disable-sync",
|
||||||
|
"--disable-translate",
|
||||||
|
"--metrics-recording-only",
|
||||||
|
"--no-default-browser-check",
|
||||||
|
"--safebrowsing-disable-auto-update",
|
||||||
|
"--remote-allow-origins=*",
|
||||||
|
"--blink-settings=imagesEnabled=false",
|
||||||
|
]
|
||||||
|
|
||||||
|
last_error = "All candidates failed"
|
||||||
|
|
||||||
|
# 여러 브라우저 후보들 시도 (크롬이 이미 실행 중일 때 등의 상황 대비)
|
||||||
|
for exec_path in candidates:
|
||||||
|
browser = None
|
||||||
|
user_data_dir = os.path.join(tempfile.gettempdir(), f"zd_ohli_{uid}_{os.path.basename(exec_path).replace(' ', '_')}")
|
||||||
|
os.makedirs(user_data_dir, exist_ok=True)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# 브라우저 시작
|
||||||
browser = await zd.start(
|
browser = await zd.start(
|
||||||
headless=True,
|
headless=True,
|
||||||
browser_executable_path=exec_path,
|
browser_executable_path=exec_path,
|
||||||
no_sandbox=True,
|
no_sandbox=True,
|
||||||
browser_args=["--no-sandbox", "--disable-setuid-sandbox", "--disable-dev-shm-usage", "--disable-gpu", "--no-first-run"]
|
user_data_dir=user_data_dir,
|
||||||
)
|
browser_args=browser_args
|
||||||
else:
|
|
||||||
browser = await zd.start(
|
|
||||||
headless=True,
|
|
||||||
no_sandbox=True,
|
|
||||||
browser_args=["--no-sandbox", "--disable-setuid-sandbox", "--disable-dev-shm-usage", "--disable-gpu", "--no-first-run"]
|
|
||||||
)
|
)
|
||||||
|
|
||||||
page = await browser.get(url)
|
page = await browser.get(url)
|
||||||
|
|
||||||
# 페이지 로드 대기 - cdndania iframe 로딩될 때까지 폴링 (최대 15초)
|
|
||||||
max_wait = 15
|
|
||||||
poll_interval = 1
|
|
||||||
waited = 0
|
|
||||||
html = ""
|
|
||||||
|
|
||||||
while waited < max_wait:
|
|
||||||
await asyncio.sleep(poll_interval)
|
|
||||||
waited += poll_interval
|
|
||||||
html = await page.get_content()
|
|
||||||
|
|
||||||
# cdndania iframe이 로드되었는지 확인
|
# 페이지 로드 대기 - 지능형 폴링 (최대 10초)
|
||||||
if "cdndania" in html or "fireplayer" in html:
|
# 1. 리스트 페이지는 바로 반환, 2. 에피소드 페이지는 플레이어 로딩 대기
|
||||||
break
|
max_wait = 10
|
||||||
|
poll_interval = 0.2 # 1.0s -> 0.2s로 단축하여 반응속도 향상
|
||||||
elapsed = asyncio.get_event_loop().time() - start_time
|
waited = 0
|
||||||
|
html = ""
|
||||||
if html and len(html) > 100:
|
|
||||||
result.update({
|
while waited < max_wait:
|
||||||
"success": True,
|
await asyncio.sleep(poll_interval)
|
||||||
"html": html,
|
waited += poll_interval
|
||||||
"elapsed": round(elapsed, 2)
|
html = await page.get_content()
|
||||||
})
|
|
||||||
else:
|
|
||||||
result["error"] = f"Short response: {len(html) if html else 0} bytes"
|
|
||||||
result["elapsed"] = round(elapsed, 2)
|
|
||||||
|
|
||||||
except Exception as e:
|
# 리스트 페이지 마커 확인 (발견 즉시 탈출)
|
||||||
result["error"] = str(e)
|
if "post-list" in html or "list-box" in html or "post-row" in html:
|
||||||
result["elapsed"] = round(asyncio.get_event_loop().time() - start_time, 2)
|
# log_debug(f"[Zendriver] List page detected in {waited:.1f}s")
|
||||||
finally:
|
break
|
||||||
if browser:
|
|
||||||
try:
|
# cdndania/fireplayer iframe이 로드되었는지 확인 (에피소드 페이지)
|
||||||
|
if "cdndania" in html or "fireplayer" in html:
|
||||||
|
# log_debug(f"[Zendriver] Player detected in {waited:.1f}s")
|
||||||
|
break
|
||||||
|
|
||||||
|
elapsed = asyncio.get_event_loop().time() - start_time
|
||||||
|
|
||||||
|
if html and len(html) > 100:
|
||||||
|
result.update({
|
||||||
|
"success": True,
|
||||||
|
"html": html,
|
||||||
|
"elapsed": round(elapsed, 2)
|
||||||
|
})
|
||||||
|
# 성공했으므로 루프 종료
|
||||||
await browser.stop()
|
await browser.stop()
|
||||||
except:
|
return result
|
||||||
pass
|
else:
|
||||||
|
last_error = f"Short response from {exec_path}: {len(html) if html else 0} bytes"
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
last_error = f"Failed with {exec_path}: {str(e)}"
|
||||||
|
finally:
|
||||||
|
if browser:
|
||||||
|
try:
|
||||||
|
await browser.stop()
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
result["error"] = last_error
|
||||||
|
result["elapsed"] = round(asyncio.get_event_loop().time() - start_time, 2)
|
||||||
|
return result
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|||||||
116
mod_ohli24.py
116
mod_ohli24.py
@@ -411,8 +411,6 @@ class LogicOhli24(AnimeModuleBase):
|
|||||||
return {"ret": "error", "msg": f"설치 중 예외가 발생했습니다: {str(e)}"}
|
return {"ret": "error", "msg": f"설치 중 예외가 발생했습니다: {str(e)}"}
|
||||||
|
|
||||||
def __init__(self, P: Any) -> None:
|
def __init__(self, P: Any) -> None:
|
||||||
self.name: str = name
|
|
||||||
|
|
||||||
self.db_default = {
|
self.db_default = {
|
||||||
"ohli24_db_version": "1",
|
"ohli24_db_version": "1",
|
||||||
"ohli24_proxy_url": "",
|
"ohli24_proxy_url": "",
|
||||||
@@ -420,11 +418,11 @@ class LogicOhli24(AnimeModuleBase):
|
|||||||
"ohli24_url": "https://ani.ohli24.com",
|
"ohli24_url": "https://ani.ohli24.com",
|
||||||
"ohli24_download_path": os.path.join(path_data, P.package_name, "ohli24"),
|
"ohli24_download_path": os.path.join(path_data, P.package_name, "ohli24"),
|
||||||
"ohli24_auto_make_folder": "True",
|
"ohli24_auto_make_folder": "True",
|
||||||
f"{self.name}_recent_code": "",
|
f"{name}_recent_code": "",
|
||||||
"ohli24_auto_make_season_folder": "True",
|
"ohli24_auto_make_season_folder": "True",
|
||||||
"ohli24_finished_insert": "[완결]",
|
"ohli24_finished_insert": "[완결]",
|
||||||
"ohli24_max_ffmpeg_process_count": "1",
|
"ohli24_max_ffmpeg_process_count": "1",
|
||||||
f"{self.name}_download_method": "cdndania", # cdndania (default), ffmpeg, ytdlp, aria2c
|
f"{name}_download_method": "cdndania", # cdndania (default), ffmpeg, ytdlp, aria2c
|
||||||
"ohli24_download_threads": "2", # 기본값 2 (안정성 권장)
|
"ohli24_download_threads": "2", # 기본값 2 (안정성 권장)
|
||||||
"ohli24_order_desc": "False",
|
"ohli24_order_desc": "False",
|
||||||
"ohli24_auto_start": "False",
|
"ohli24_auto_start": "False",
|
||||||
@@ -1878,6 +1876,8 @@ class LogicOhli24(AnimeModuleBase):
|
|||||||
import time
|
import time
|
||||||
from urllib import parse
|
from urllib import parse
|
||||||
|
|
||||||
|
total_start = time.time()
|
||||||
|
|
||||||
# URL 인코딩 (한글 주소 대응)
|
# URL 인코딩 (한글 주소 대응)
|
||||||
if '://' in url:
|
if '://' in url:
|
||||||
try:
|
try:
|
||||||
@@ -1948,6 +1948,8 @@ class LogicOhli24(AnimeModuleBase):
|
|||||||
|
|
||||||
|
|
||||||
# === [Layer 1: Botasaurus @request (빠름 - HTTP Request)] ===
|
# === [Layer 1: Botasaurus @request (빠름 - HTTP Request)] ===
|
||||||
|
# Ohli24에서 Connection Reset 이슈로 인해 현재는 주석 처리 (Zendriver 최적화 집중)
|
||||||
|
"""
|
||||||
if not response_data or len(response_data) < 10:
|
if not response_data or len(response_data) < 10:
|
||||||
if LogicOhli24.ensure_essential_dependencies():
|
if LogicOhli24.ensure_essential_dependencies():
|
||||||
import platform
|
import platform
|
||||||
@@ -1994,6 +1996,7 @@ class LogicOhli24(AnimeModuleBase):
|
|||||||
logger.warning(f"[Layer1] Botasaurus short response: {len(b_resp) if b_resp else 0}")
|
logger.warning(f"[Layer1] Botasaurus short response: {len(b_resp) if b_resp else 0}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning(f"[Layer1] Botasaurus failed: {e}")
|
logger.warning(f"[Layer1] Botasaurus failed: {e}")
|
||||||
|
"""
|
||||||
|
|
||||||
# === [TEST MODE] Layer 1 (기존 것들) 일시 비활성화 - Layer 3, 4만 테스트 ===
|
# === [TEST MODE] Layer 1 (기존 것들) 일시 비활성화 - Layer 3, 4만 테스트 ===
|
||||||
response_data = "" # 바로 Layer 3로 이동
|
response_data = "" # 바로 Layer 3로 이동
|
||||||
@@ -2054,7 +2057,8 @@ class LogicOhli24(AnimeModuleBase):
|
|||||||
daemon_result = LogicOhli24.fetch_via_daemon(url, 30)
|
daemon_result = LogicOhli24.fetch_via_daemon(url, 30)
|
||||||
|
|
||||||
if daemon_result.get("success") and daemon_result.get("html"):
|
if daemon_result.get("success") and daemon_result.get("html"):
|
||||||
logger.info(f"[Layer3A] Daemon success in {daemon_result.get('elapsed', '?')}s, HTML len: {len(daemon_result['html'])}")
|
elapsed = time.time() - total_start
|
||||||
|
logger.info(f"[Ohli24] Fetch success via Layer3A: {url} in {elapsed:.2f}s (HTML: {len(daemon_result['html'])})")
|
||||||
# 성공 시 연속 실패 카운트 초기화
|
# 성공 시 연속 실패 카운트 초기화
|
||||||
LogicOhli24.daemon_fail_count = 0
|
LogicOhli24.daemon_fail_count = 0
|
||||||
return daemon_result["html"]
|
return daemon_result["html"]
|
||||||
@@ -2110,7 +2114,8 @@ class LogicOhli24(AnimeModuleBase):
|
|||||||
if result.returncode == 0 and result.stdout.strip():
|
if result.returncode == 0 and result.stdout.strip():
|
||||||
zd_result = json.loads(result.stdout.strip())
|
zd_result = json.loads(result.stdout.strip())
|
||||||
if zd_result.get("success") and zd_result.get("html"):
|
if zd_result.get("success") and zd_result.get("html"):
|
||||||
logger.info(f"[Layer3B] Zendriver success in {zd_result.get('elapsed', '?')}s, HTML len: {len(zd_result['html'])}")
|
elapsed = time.time() - total_start
|
||||||
|
logger.info(f"[Ohli24] Fetch success via Layer3B: {url} in {elapsed:.2f}s (HTML: {len(zd_result['html'])})")
|
||||||
return zd_result["html"]
|
return zd_result["html"]
|
||||||
else:
|
else:
|
||||||
logger.warning(f"[Layer3B] Zendriver failed: {zd_result.get('error', 'Unknown error')}")
|
logger.warning(f"[Layer3B] Zendriver failed: {zd_result.get('error', 'Unknown error')}")
|
||||||
@@ -2245,52 +2250,63 @@ class LogicOhli24(AnimeModuleBase):
|
|||||||
|
|
||||||
# GDM 모듈 사용 시나리오
|
# GDM 모듈 사용 시나리오
|
||||||
if ModuleQueue:
|
if ModuleQueue:
|
||||||
logger.info(f"Preparing GDM delegation for: {episode_info.get('title')}")
|
logger.info(f"Preparing GDM delegation for: {episode_info.get('title')}")
|
||||||
# Entity 인스턴스를 생성하여 메타데이터 파싱 및 URL 추출 수행
|
# Entity 인스턴스를 생성하여 메타데이터 파싱 및 URL 추출 수행
|
||||||
entity = Ohli24QueueEntity(P, self, episode_info)
|
entity = Ohli24QueueEntity(P, self, episode_info)
|
||||||
|
|
||||||
# URL/자막/쿠키 추출 수행 (동기식 - 상위에서 비동기로 호출 권장되나 현재 ajax_process는 동기)
|
# URL/자막/쿠키 추출 수행 (동기식 - 상위에서 비동기로 호출 권장되나 현재 ajax_process는 동기)
|
||||||
# 만약 이게 너무 느려지면 별도 쓰레드로 빼야 하지만, 일단 작동 확인을 위해 동기 처리
|
try:
|
||||||
try:
|
logger.debug(f"Calling entity.prepare_extra() for {episode_info.get('_id')}")
|
||||||
entity.prepare_extra()
|
entity.prepare_extra()
|
||||||
except Exception as e:
|
logger.debug(f"entity.prepare_extra() done. URL found: {entity.url is not None}")
|
||||||
logger.error(f"Failed to extract video info: {e}")
|
except Exception as e:
|
||||||
# 추출 실패 시 기존 방식(전체 큐)으로 넘기거나 에러 반환
|
logger.error(f"Failed to extract video info: {e}")
|
||||||
return "extract_failed"
|
# 추출 실패 시 기존 방식(전체 큐)으로 넘기거나 에러 반환
|
||||||
|
return "extract_failed"
|
||||||
|
|
||||||
# 추출된 정보를 바탕으로 GDM 옵션 준비 (표준화된 필드명 사용)
|
# 추출된 정보를 바탕으로 GDM 옵션 준비 (표준화된 필드명 사용)
|
||||||
gdm_options = {
|
gdm_options = {
|
||||||
"url": entity.url, # 추출된 m3u8 URL
|
"url": entity.url, # 추출된 m3u8 URL
|
||||||
"save_path": entity.savepath,
|
"save_path": entity.savepath,
|
||||||
"filename": entity.filename,
|
"filename": entity.filename,
|
||||||
"source_type": "ani24",
|
"source_type": "ani24",
|
||||||
"caller_plugin": f"{P.package_name}_{self.name}",
|
"caller_plugin": f"{P.package_name}_{self.name}",
|
||||||
"callback_id": episode_info["_id"],
|
"callback_id": episode_info["_id"],
|
||||||
"title": entity.filename or episode_info.get('title'),
|
"title": entity.filename or episode_info.get('title'),
|
||||||
"thumbnail": episode_info.get('image'),
|
"thumbnail": episode_info.get('image'),
|
||||||
"meta": {
|
"meta": {
|
||||||
"series": entity.content_title,
|
"series": entity.content_title,
|
||||||
"season": entity.season,
|
"season": entity.season,
|
||||||
"episode": entity.epi_queue,
|
"episode": entity.epi_queue,
|
||||||
"source": "ohli24"
|
"source": "ohli24"
|
||||||
},
|
},
|
||||||
# options 내부가 아닌 상위 레벨로 headers/cookies 전달 (GDM 평탄화 대응)
|
# options 내부가 아닌 상위 레벨로 headers/cookies 전달 (GDM 평탄화 대응)
|
||||||
"headers": entity.headers,
|
"headers": entity.headers,
|
||||||
"subtitles": entity.srt_url or entity.vtt,
|
"subtitles": entity.srt_url or entity.vtt,
|
||||||
"cookies_file": entity.cookies_file
|
"cookies_file": entity.cookies_file
|
||||||
}
|
}
|
||||||
|
|
||||||
task = ModuleQueue.add_download(**gdm_options)
|
try:
|
||||||
if task:
|
logger.debug(f"Calling ModuleQueue.add_download with options: {list(gdm_options.keys())}")
|
||||||
logger.info(f"Delegated Ohli24 download to GDM: {entity.filename}")
|
task = ModuleQueue.add_download(**gdm_options)
|
||||||
# DB 상태 업데이트 (prepare_extra에서도 이미 수행하지만 명시적 상태 변경)
|
if task:
|
||||||
if db_entity is None:
|
logger.info(f"Delegated Ohli24 download to GDM: {entity.filename} (Task ID: {task.id})")
|
||||||
# append는 이미 prepare_extra 상단에서 db_entity를 조회하므로
|
else:
|
||||||
# 이미 DB에 entry가 생겼을 가능성 높음 (만약 없다면 여기서 추가)
|
logger.error("ModuleQueue.add_download returned None")
|
||||||
db_entity = ModelOhli24Item.get_by_ohli24_id(episode_info["_id"])
|
except Exception as e:
|
||||||
if not db_entity:
|
logger.error(f"Error calling ModuleQueue.add_download: {e}")
|
||||||
ModelOhli24Item.append(entity.as_dict())
|
logger.error(traceback.format_exc())
|
||||||
return "enqueue_gdm_success"
|
task = None
|
||||||
|
|
||||||
|
if task:
|
||||||
|
# DB 상태 업데이트 (prepare_extra에서도 이미 수행하지만 명시적 상태 변경)
|
||||||
|
if db_entity is None:
|
||||||
|
# append는 이미 prepare_extra 상단에서 db_entity를 조회하므로
|
||||||
|
# 이미 DB에 entry가 생겼을 가능성 높음 (만약 없다면 여기서 추가)
|
||||||
|
db_entity = ModelOhli24Item.get_by_ohli24_id(episode_info["_id"])
|
||||||
|
if not db_entity:
|
||||||
|
ModelOhli24Item.append(entity.as_dict())
|
||||||
|
return "enqueue_gdm_success"
|
||||||
|
|
||||||
# GDM 미설치 시 기존 방식 fallback (또는 에러 처리)
|
# GDM 미설치 시 기존 방식 fallback (또는 에러 처리)
|
||||||
logger.warning("GDM Module not found, falling back to FfmpegQueue")
|
logger.warning("GDM Module not found, falling back to FfmpegQueue")
|
||||||
|
|||||||
Reference in New Issue
Block a user