v0.6.10: Fix Ohli24 GDM integration and update README

This commit is contained in:
2026-01-07 15:09:04 +09:00
parent 759f772ca8
commit c532ffaef8
7 changed files with 368 additions and 191 deletions

View File

@@ -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 단축 파일명 생성 방지

View File

@@ -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()

View File

@@ -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"

View File

@@ -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 {}
b_resp = fetch_url(url) # 기본적인 헤더 보강 (Ohli24 대응 - Cloudflare 우회 시도)
elapsed = time.time() - start_time 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: 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))

View File

@@ -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()
app_dirs = ["/Applications", "/Volumes/WD/Users/Applications"]
common_paths = []
if system == "Darwin": # Mac
for base in app_dirs:
common_paths.extend([
f"{base}/Google Chrome.app/Contents/MacOS/Google Chrome",
f"{base}/Chromium.app/Contents/MacOS/Chromium",
f"{base}/Microsoft Edge.app/Contents/MacOS/Microsoft Edge",
])
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",
"/usr/bin/google-chrome-stable", "/usr/bin/google-chrome-stable",
"/usr/bin/chromium-browser", "/usr/bin/chromium-browser",
"/usr/bin/chromium", "/usr/bin/chromium",
"/usr/lib/chromium-browser/chromium-browser", "/usr/lib/chromium-browser/chromium-browser",
"google-chrome", # PATH에서 찾기
"chromium-browser",
"chromium",
] ]
# 먼저 절대 경로 확인 # 존재하는 모든 후보들 반환
for path in common_paths: candidates = [p for p in common_paths if os.path.exists(p)]
if path.startswith("/") and os.path.exists(path):
log_debug(f"[ZendriverDaemon] Found browser at absolute path: {path}")
return path
# shutil.which로 PATH 확인 # PATH에서 찾기 추가
import shutil for cmd in ["google-chrome", "google-chrome-stable", "chromium-browser", "chromium", "chrome", "microsoft-edge"]:
for cmd in ["google-chrome", "google-chrome-stable", "chromium-browser", "chromium"]:
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",
"--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",
]
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)
try:
log_debug(f"[ZendriverDaemon] Trying browser at: {exec_path}")
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:
log_debug("[ZendriverDaemon] Starting browser with default path")
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"]
) )
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
log_debug("[ZendriverDaemon] Browser started successfully") 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

View File

@@ -15,11 +15,31 @@ 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
system = platform.system()
app_dirs = ["/Applications", "/Volumes/WD/Users/Applications"]
common_paths = []
if system == "Darwin": # Mac
for base in app_dirs:
common_paths.extend([
f"{base}/Google Chrome.app/Contents/MacOS/Google Chrome",
f"{base}/Chromium.app/Contents/MacOS/Chromium",
f"{base}/Microsoft Edge.app/Contents/MacOS/Microsoft Edge",
])
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 = [ common_paths = [
"/usr/bin/google-chrome", "/usr/bin/google-chrome",
"/usr/bin/google-chrome-stable", "/usr/bin/google-chrome-stable",
@@ -28,18 +48,16 @@ def find_browser_executable(manual_path=None):
"/usr/lib/chromium-browser/chromium-browser", "/usr/lib/chromium-browser/chromium-browser",
] ]
# 먼저 절대 경로 확인 # 존재하는 모든 후보들 반환
for path in common_paths: candidates = [p for p in common_paths if os.path.exists(p)]
if os.path.exists(path):
return path
# shutil.which로 PATH 확인 # PATH에서 찾기 추가
for cmd in ["google-chrome", "google-chrome-stable", "chromium-browser", "chromium"]: 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,30 +71,68 @@ 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 권한 이슈 대응)
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:
# 브라우저 시작 # 브라우저 시작
if exec_path:
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초) # 페이지 로드 대기 - 지능형 폴링 (최대 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 = "" html = ""
@@ -85,8 +141,14 @@ async def fetch_html(url: str, timeout: int = 60, browser_path: str = None) -> d
waited += poll_interval waited += poll_interval
html = await page.get_content() html = await page.get_content()
# cdndania iframe이 로드되었는지 확인 # 리스트 페이지 마커 확인 (발견 즉시 탈출)
if "post-list" in html or "list-box" in html or "post-row" in html:
# log_debug(f"[Zendriver] List page detected in {waited:.1f}s")
break
# cdndania/fireplayer iframe이 로드되었는지 확인 (에피소드 페이지)
if "cdndania" in html or "fireplayer" in html: if "cdndania" in html or "fireplayer" in html:
# log_debug(f"[Zendriver] Player detected in {waited:.1f}s")
break break
elapsed = asyncio.get_event_loop().time() - start_time elapsed = asyncio.get_event_loop().time() - start_time
@@ -97,13 +159,14 @@ async def fetch_html(url: str, timeout: int = 60, browser_path: str = None) -> d
"html": html, "html": html,
"elapsed": round(elapsed, 2) "elapsed": round(elapsed, 2)
}) })
# 성공했으므로 루프 종료
await browser.stop()
return result
else: else:
result["error"] = f"Short response: {len(html) if html else 0} bytes" last_error = f"Short response from {exec_path}: {len(html) if html else 0} bytes"
result["elapsed"] = round(elapsed, 2)
except Exception as e: except Exception as e:
result["error"] = str(e) last_error = f"Failed with {exec_path}: {str(e)}"
result["elapsed"] = round(asyncio.get_event_loop().time() - start_time, 2)
finally: finally:
if browser: if browser:
try: try:
@@ -111,6 +174,10 @@ async def fetch_html(url: str, timeout: int = 60, browser_path: str = None) -> d
except: except:
pass pass
result["error"] = last_error
result["elapsed"] = round(asyncio.get_event_loop().time() - start_time, 2)
return result
return result return result

View File

@@ -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')}")
@@ -2250,9 +2255,10 @@ class LogicOhli24(AnimeModuleBase):
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()
logger.debug(f"entity.prepare_extra() done. URL found: {entity.url is not None}")
except Exception as e: except Exception as e:
logger.error(f"Failed to extract video info: {e}") logger.error(f"Failed to extract video info: {e}")
# 추출 실패 시 기존 방식(전체 큐)으로 넘기거나 에러 반환 # 추출 실패 시 기존 방식(전체 큐)으로 넘기거나 에러 반환
@@ -2280,9 +2286,19 @@ class LogicOhli24(AnimeModuleBase):
"cookies_file": entity.cookies_file "cookies_file": entity.cookies_file
} }
try:
logger.debug(f"Calling ModuleQueue.add_download with options: {list(gdm_options.keys())}")
task = ModuleQueue.add_download(**gdm_options) task = ModuleQueue.add_download(**gdm_options)
if task: if task:
logger.info(f"Delegated Ohli24 download to GDM: {entity.filename}") logger.info(f"Delegated Ohli24 download to GDM: {entity.filename} (Task ID: {task.id})")
else:
logger.error("ModuleQueue.add_download returned None")
except Exception as e:
logger.error(f"Error calling ModuleQueue.add_download: {e}")
logger.error(traceback.format_exc())
task = None
if task:
# DB 상태 업데이트 (prepare_extra에서도 이미 수행하지만 명시적 상태 변경) # DB 상태 업데이트 (prepare_extra에서도 이미 수행하지만 명시적 상태 변경)
if db_entity is None: if db_entity is None:
# append는 이미 prepare_extra 상단에서 db_entity를 조회하므로 # append는 이미 prepare_extra 상단에서 db_entity를 조회하므로