diff --git a/README.md b/README.md index 20258e2..bbb6f51 100644 --- a/README.md +++ b/README.md @@ -81,13 +81,15 @@ ## ๐Ÿ“ ๋ณ€๊ฒฝ ์ด๋ ฅ (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 ์—ฐ๋™**: - `ModuleQueue` ์—ฐ๋™์œผ๋กœ Anilife ๋‹ค์šด๋กœ๋“œ๊ฐ€ GDM (Gommi Downloader Manager)์œผ๋กœ ํ†ตํ•ฉ - Ohli24์™€ ๋™์ผํ•œ ํŒจํ„ด์œผ๋กœ `source_type: "anilife"` ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ ํฌํ•จ - Go FFMPEG ๋ฒ„ํŠผ โ†’ **Go GDM** ๋ฒ„ํŠผ์œผ๋กœ ๋ณ€๊ฒฝ ๋ฐ GDM ํ ํŽ˜์ด์ง€๋กœ ๋งํฌ -- **HTTP ์บ์‹ฑ ์ค€๋น„**: - - `CachedSession` import ์ถ”๊ฐ€ (ํ–ฅํ›„ requests ์บ์‹ฑ ํ™•์žฅ ๊ฐ€๋Šฅ) - **ํŒŒ์ผ๋ช… ์ •๋ฆฌ ๊ฐœ์„ **: - `Util.change_text_for_use_filename()` ํ•จ์ˆ˜์—์„œ ์—ฐ์† ์ (`..`) โ†’ ๋‹จ์ผ ์ (`.`) ๋ณ€ํ™˜ - ๋์— ์˜ค๋Š” ์ /๊ณต๋ฐฑ ์ž๋™ ์ œ๊ฑฐ๋กœ Synology NAS์—์„œ Windows 8.3 ๋‹จ์ถ• ํŒŒ์ผ๋ช… ์ƒ์„ฑ ๋ฐฉ์ง€ diff --git a/__init__.py b/__init__.py index 76e21fd..29c1deb 100644 --- a/__init__.py +++ b/__init__.py @@ -4,9 +4,15 @@ # @Site : # @File : __init__ # @Software: PyCharm -# from .plugin import P -# blueprint = P.blueprint -# menu = P.menu -# plugin_load = P.logic.plugin_load -# plugin_unload = P.logic.plugin_unload -# plugin_info = P.plugin_info +from .setup import P +blueprint = P.blueprint +menu = P.menu +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() diff --git a/info.yaml b/info.yaml index d2872f8..e6866b8 100644 --- a/info.yaml +++ b/info.yaml @@ -1,5 +1,5 @@ title: "์• ๋‹ˆ ๋‹ค์šด๋กœ๋”" -version: "0.6.9" +version: "0.6.10" package_name: "anime_downloader" developer: "projectdx" description: "anime downloader" diff --git a/lib/botasaurus_ohli24.py b/lib/botasaurus_ohli24.py index 514db2f..57da630 100644 --- a/lib/botasaurus_ohli24.py +++ b/lib/botasaurus_ohli24.py @@ -7,22 +7,55 @@ Botasaurus ๊ธฐ๋ฐ˜ Ohli24 HTML ํŽ˜์นญ ์Šคํฌ๋ฆฝํŠธ import sys import json +import os import time -import traceback +from typing import Dict, Any, Optional -def fetch_html(url, headers=None, proxy=None): - result = {"success": False, "html": "", "elapsed": 0} - start_time = time.time() +# ๋ด‡์‚ฌ์šฐ๋ฃจ์Šค ๋””๋ฒ„๊น… ์ผ์‹œ์ •์ง€ ๋ฐฉ์ง€ ๋ฐ ์ž๋™ ์ข…๋ฃŒ ์„ค์ • +os.environ["BOTASAURUS_ENV"] = "production" + +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: from botasaurus.request import request as b_request - @b_request(headers=headers, use_stealth=True, proxy=proxy) - def fetch_url(request, data): - return request.get(data) + # raise_exception=True๋Š” ์—๋Ÿฌ ์‹œ exception์„ ๋ฐœ์ƒ์‹œํ‚ค๊ฒŒ ํ•จ + # close_on_crash=True๋Š” ์—๋Ÿฌ ๋ฐœ์ƒ ์‹œ ๋Œ€๊ธฐํ•˜์ง€ ์•Š๊ณ  ์ฆ‰์‹œ ์ข…๋ฃŒ (๋ฐฐํฌ ํ™˜๊ฒฝ์šฉ) + @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: result.update({ @@ -36,7 +69,6 @@ def fetch_html(url, headers=None, proxy=None): except Exception as e: result["error"] = str(e) - result["traceback"] = traceback.format_exc() result["elapsed"] = round(time.time() - start_time, 2) return result @@ -46,9 +78,9 @@ if __name__ == "__main__": print(json.dumps({"success": False, "error": "Usage: python botasaurus_ohli24.py [headers_json] [proxy]"})) sys.exit(1) - target_url = sys.argv[1] - headers_arg = 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 + target_url: str = sys.argv[1] + headers_arg: Optional[Dict[str, str]] = json.loads(sys.argv[2]) if len(sys.argv) > 2 and sys.argv[2] 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)) diff --git a/lib/zendriver_daemon.py b/lib/zendriver_daemon.py index fd3b9a8..9703885 100644 --- a/lib/zendriver_daemon.py +++ b/lib/zendriver_daemon.py @@ -16,6 +16,7 @@ import traceback from http.server import HTTPServer, BaseHTTPRequestHandler from threading import Thread, Lock from typing import Any, Optional, Dict, List, Type, cast +import zendriver as zd # ํ„ฐ๋ฏธ๋„ ๋ฐ ํŒŒ์ผ๋กœ ๋กœ๊ทธ ์ถœ๋ ฅ ์„ค์ • LOG_FILE: str = "/tmp/zendriver_daemon.log" @@ -38,38 +39,51 @@ loop: Optional[asyncio.AbstractEventLoop] = None manual_browser_path: Optional[str] = None -def find_browser_executable() -> Optional[str]: - """์‹œ์Šคํ…œ์—์„œ ๋ธŒ๋ผ์šฐ์ € ์‹คํ–‰ ํŒŒ์ผ ์ฐพ๊ธฐ (Docker/Ubuntu ํ™˜๊ฒฝ ๋Œ€์‘)""" +def find_browser_executable() -> List[str]: + """์‹œ์Šคํ…œ์—์„œ ๋ธŒ๋ผ์šฐ์ € ์‹คํ–‰ ํŒŒ์ผ ์ฐพ๊ธฐ (OS๋ณ„ ๋Œ€์‘)""" + import platform + import shutil + # ์ˆ˜๋™ ์„ค์ •๋œ ๊ฒฝ๋กœ ์ตœ์šฐ์„  if manual_browser_path and os.path.exists(manual_browser_path): - return manual_browser_path + return [manual_browser_path] - common_paths: List[str] = [ - "/usr/bin/google-chrome", - "/usr/bin/google-chrome-stable", - "/usr/bin/chromium-browser", - "/usr/bin/chromium", - "/usr/lib/chromium-browser/chromium-browser", - "google-chrome", # PATH์—์„œ ์ฐพ๊ธฐ - "chromium-browser", - "chromium", - ] + system = platform.system() + app_dirs = ["/Applications", "/Volumes/WD/Users/Applications"] + common_paths = [] - # ๋จผ์ € ์ ˆ๋Œ€ ๊ฒฝ๋กœ ํ™•์ธ - for path in common_paths: - if path.startswith("/") and os.path.exists(path): - log_debug(f"[ZendriverDaemon] Found browser at absolute path: {path}") - return path - - # shutil.which๋กœ PATH ํ™•์ธ - import shutil - for cmd in ["google-chrome", "google-chrome-stable", "chromium-browser", "chromium"]: + 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-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) - if found: - log_debug(f"[ZendriverDaemon] Found browser via shutil.which: {found}") - return found + if found and found not in candidates: + candidates.append(found) - return None + return candidates class ZendriverHandler(BaseHTTPRequestHandler): @@ -154,30 +168,64 @@ async def ensure_browser() -> Any: with browser_lock: if browser is None: 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 - # ์‹คํ–‰ ๊ฐ€๋Šฅํ•œ ๋ธŒ๋ผ์šฐ์ € ์ฐพ๊ธฐ - exec_path = find_browser_executable() - log_debug(f"[ZendriverDaemon] Startup params: headless=True, no_sandbox=True, path={exec_path}") + # ์‚ฌ์šฉ์ž ๋ฐ์ดํ„ฐ ๋””๋ ‰ํ† ๋ฆฌ ์„ค์ • (Mac/Root ๊ถŒํ•œ ์ด์Šˆ ๋Œ€์‘) + import tempfile + uid = os.getuid() if hasattr(os, 'getuid') else 'win' - if exec_path: - log_debug(f"[ZendriverDaemon] Starting browser at: {exec_path}") - browser = await zd.start( - headless=True, - browser_executable_path=exec_path, - no_sandbox=True, - browser_args=["--no-sandbox", "--disable-setuid-sandbox", "--disable-dev-shm-usage", "--disable-gpu", "--no-first-run"] - ) - 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"] - ) + 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", + ] + + 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: log_debug(f"[ZendriverDaemon] Failed to start browser: {e}") browser = None @@ -209,9 +257,10 @@ async def fetch_with_browser(url: str, timeout: int = 30) -> Dict[str, Any]: # browser.get(url)์€ ์ƒˆ ํƒญ์„ ์—ด๊ฑฐ๋‚˜ ๊ธฐ์กด ํƒญ์„ ์‚ฌ์šฉํ•จ page: Any = await browser.get(url) - # ํŽ˜์ด์ง€ ๋กœ๋“œ ๋Œ€๊ธฐ - cdndania iframe ๋กœ๋”ฉ๋  ๋•Œ๊นŒ์ง€ ํด๋ง (์ตœ๋Œ€ 15์ดˆ) - max_wait = 15 - poll_interval = 1 + # ํŽ˜์ด์ง€ ๋กœ๋“œ ๋Œ€๊ธฐ - ์ง€๋Šฅํ˜• ํด๋ง (์ตœ๋Œ€ 10์ดˆ) + # 1. ๋ฆฌ์ŠคํŠธ ํŽ˜์ด์ง€๋Š” ๋ฐ”๋กœ ๋ฐ˜ํ™˜, 2. ์—ํ”ผ์†Œ๋“œ ํŽ˜์ด์ง€๋Š” ํ”Œ๋ ˆ์ด์–ด ๋กœ๋”ฉ ๋Œ€๊ธฐ + max_wait = 10 + poll_interval = 0.2 # 1.0s -> 0.2s๋กœ ๋‹จ์ถ•ํ•˜์—ฌ ๋ฐ˜์‘์†๋„ ํ–ฅ์ƒ waited = 0 html_content = "" @@ -220,9 +269,14 @@ async def fetch_with_browser(url: str, timeout: int = 30) -> Dict[str, Any]: waited += poll_interval 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: - log_debug(f"[ZendriverDaemon] cdndania/fireplayer found after {waited}s") + log_debug(f"[ZendriverDaemon] Player detected in {waited:.1f}s") break elapsed: float = time.time() - start_time diff --git a/lib/zendriver_ohli24.py b/lib/zendriver_ohli24.py index 2abc80f..dfc8979 100644 --- a/lib/zendriver_ohli24.py +++ b/lib/zendriver_ohli24.py @@ -15,31 +15,49 @@ import shutil def find_browser_executable(manual_path=None): - """์‹œ์Šคํ…œ์—์„œ ๋ธŒ๋ผ์šฐ์ € ์‹คํ–‰ ํŒŒ์ผ ์ฐพ๊ธฐ (Docker/Ubuntu ํ™˜๊ฒฝ ๋Œ€์‘)""" + """์‹œ์Šคํ…œ์—์„œ ๋ธŒ๋ผ์šฐ์ € ์‹คํ–‰ ํŒŒ์ผ ์ฐพ๊ธฐ (OS๋ณ„ ๋Œ€์‘)""" + import platform + # ์ˆ˜๋™ ์„ค์ • ์‹œ ์šฐ์„  if manual_path and os.path.exists(manual_path): return manual_path - common_paths = [ - "/usr/bin/google-chrome", - "/usr/bin/google-chrome-stable", - "/usr/bin/chromium-browser", - "/usr/bin/chromium", - "/usr/lib/chromium-browser/chromium-browser", - ] + system = platform.system() + app_dirs = ["/Applications", "/Volumes/WD/Users/Applications"] + common_paths = [] - # ๋จผ์ € ์ ˆ๋Œ€ ๊ฒฝ๋กœ ํ™•์ธ - for path in common_paths: - if os.path.exists(path): - return path - - # shutil.which๋กœ PATH ํ™•์ธ - for cmd in ["google-chrome", "google-chrome-stable", "chromium-browser", "chromium"]: + 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-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) - if found: - return found + if found and found not in candidates: + candidates.append(found) - return None + return candidates 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() browser = None - try: - # ์‹คํ–‰ ๊ฐ€๋Šฅํ•œ ๋ธŒ๋ผ์šฐ์ € ์ฐพ๊ธฐ - exec_path = find_browser_executable(browser_path) + # ์‹คํ–‰ ๊ฐ€๋Šฅํ•œ ๋ธŒ๋ผ์šฐ์ € ํ›„๋ณด๋“ค ์ฐพ๊ธฐ + candidates = find_browser_executable(browser_path) + if not candidates: + return {"success": False, "error": "No browser executable found", "html": ""} - # ๋ธŒ๋ผ์šฐ์ € ์‹œ์ž‘ - if exec_path: + # ์‚ฌ์šฉ์ž ๋ฐ์ดํ„ฐ ๋””๋ ‰ํ† ๋ฆฌ ์„ค์ • (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: + # ๋ธŒ๋ผ์šฐ์ € ์‹œ์ž‘ browser = await zd.start( headless=True, browser_executable_path=exec_path, no_sandbox=True, - browser_args=["--no-sandbox", "--disable-setuid-sandbox", "--disable-dev-shm-usage", "--disable-gpu", "--no-first-run"] - ) - 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"] + user_data_dir=user_data_dir, + browser_args=browser_args ) - 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() + page = await browser.get(url) - # cdndania iframe์ด ๋กœ๋“œ๋˜์—ˆ๋Š”์ง€ ํ™•์ธ - if "cdndania" in html or "fireplayer" in html: - 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) - }) - else: - result["error"] = f"Short response: {len(html) if html else 0} bytes" - result["elapsed"] = round(elapsed, 2) + # ํŽ˜์ด์ง€ ๋กœ๋“œ ๋Œ€๊ธฐ - ์ง€๋Šฅํ˜• ํด๋ง (์ตœ๋Œ€ 10์ดˆ) + # 1. ๋ฆฌ์ŠคํŠธ ํŽ˜์ด์ง€๋Š” ๋ฐ”๋กœ ๋ฐ˜ํ™˜, 2. ์—ํ”ผ์†Œ๋“œ ํŽ˜์ด์ง€๋Š” ํ”Œ๋ ˆ์ด์–ด ๋กœ๋”ฉ ๋Œ€๊ธฐ + max_wait = 10 + poll_interval = 0.2 # 1.0s -> 0.2s๋กœ ๋‹จ์ถ•ํ•˜์—ฌ ๋ฐ˜์‘์†๋„ ํ–ฅ์ƒ + waited = 0 + html = "" + + while waited < max_wait: + await asyncio.sleep(poll_interval) + waited += poll_interval + html = await page.get_content() - except Exception as e: - result["error"] = str(e) - result["elapsed"] = round(asyncio.get_event_loop().time() - start_time, 2) - finally: - if browser: - try: + # ๋ฆฌ์ŠคํŠธ ํŽ˜์ด์ง€ ๋งˆ์ปค ํ™•์ธ (๋ฐœ๊ฒฌ ์ฆ‰์‹œ ํƒˆ์ถœ) + 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: + # 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() - except: - pass + return result + 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 diff --git a/mod_ohli24.py b/mod_ohli24.py index 11a7d59..4c3e6dd 100644 --- a/mod_ohli24.py +++ b/mod_ohli24.py @@ -411,8 +411,6 @@ class LogicOhli24(AnimeModuleBase): return {"ret": "error", "msg": f"์„ค์น˜ ์ค‘ ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค: {str(e)}"} def __init__(self, P: Any) -> None: - self.name: str = name - self.db_default = { "ohli24_db_version": "1", "ohli24_proxy_url": "", @@ -420,11 +418,11 @@ class LogicOhli24(AnimeModuleBase): "ohli24_url": "https://ani.ohli24.com", "ohli24_download_path": os.path.join(path_data, P.package_name, "ohli24"), "ohli24_auto_make_folder": "True", - f"{self.name}_recent_code": "", + f"{name}_recent_code": "", "ohli24_auto_make_season_folder": "True", "ohli24_finished_insert": "[์™„๊ฒฐ]", "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_order_desc": "False", "ohli24_auto_start": "False", @@ -1878,6 +1876,8 @@ class LogicOhli24(AnimeModuleBase): import time from urllib import parse + total_start = time.time() + # URL ์ธ์ฝ”๋”ฉ (ํ•œ๊ธ€ ์ฃผ์†Œ ๋Œ€์‘) if '://' in url: try: @@ -1948,6 +1948,8 @@ class LogicOhli24(AnimeModuleBase): # === [Layer 1: Botasaurus @request (๋น ๋ฆ„ - HTTP Request)] === + # Ohli24์—์„œ Connection Reset ์ด์Šˆ๋กœ ์ธํ•ด ํ˜„์žฌ๋Š” ์ฃผ์„ ์ฒ˜๋ฆฌ (Zendriver ์ตœ์ ํ™” ์ง‘์ค‘) + """ if not response_data or len(response_data) < 10: if LogicOhli24.ensure_essential_dependencies(): import platform @@ -1994,6 +1996,7 @@ class LogicOhli24(AnimeModuleBase): logger.warning(f"[Layer1] Botasaurus short response: {len(b_resp) if b_resp else 0}") except Exception as e: logger.warning(f"[Layer1] Botasaurus failed: {e}") + """ # === [TEST MODE] Layer 1 (๊ธฐ์กด ๊ฒƒ๋“ค) ์ผ์‹œ ๋น„ํ™œ์„ฑํ™” - Layer 3, 4๋งŒ ํ…Œ์ŠคํŠธ === response_data = "" # ๋ฐ”๋กœ Layer 3๋กœ ์ด๋™ @@ -2054,7 +2057,8 @@ class LogicOhli24(AnimeModuleBase): daemon_result = LogicOhli24.fetch_via_daemon(url, 30) 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 return daemon_result["html"] @@ -2110,7 +2114,8 @@ class LogicOhli24(AnimeModuleBase): if result.returncode == 0 and result.stdout.strip(): zd_result = json.loads(result.stdout.strip()) 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"] else: logger.warning(f"[Layer3B] Zendriver failed: {zd_result.get('error', 'Unknown error')}") @@ -2245,52 +2250,63 @@ class LogicOhli24(AnimeModuleBase): # GDM ๋ชจ๋“ˆ ์‚ฌ์šฉ ์‹œ๋‚˜๋ฆฌ์˜ค if ModuleQueue: - logger.info(f"Preparing GDM delegation for: {episode_info.get('title')}") - # Entity ์ธ์Šคํ„ด์Šค๋ฅผ ์ƒ์„ฑํ•˜์—ฌ ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ ํŒŒ์‹ฑ ๋ฐ URL ์ถ”์ถœ ์ˆ˜ํ–‰ - entity = Ohli24QueueEntity(P, self, episode_info) - - # URL/์ž๋ง‰/์ฟ ํ‚ค ์ถ”์ถœ ์ˆ˜ํ–‰ (๋™๊ธฐ์‹ - ์ƒ์œ„์—์„œ ๋น„๋™๊ธฐ๋กœ ํ˜ธ์ถœ ๊ถŒ์žฅ๋˜๋‚˜ ํ˜„์žฌ ajax_process๋Š” ๋™๊ธฐ) - # ๋งŒ์•ฝ ์ด๊ฒŒ ๋„ˆ๋ฌด ๋А๋ ค์ง€๋ฉด ๋ณ„๋„ ์“ฐ๋ ˆ๋“œ๋กœ ๋นผ์•ผ ํ•˜์ง€๋งŒ, ์ผ๋‹จ ์ž‘๋™ ํ™•์ธ์„ ์œ„ํ•ด ๋™๊ธฐ ์ฒ˜๋ฆฌ - try: - entity.prepare_extra() - except Exception as e: - logger.error(f"Failed to extract video info: {e}") - # ์ถ”์ถœ ์‹คํŒจ ์‹œ ๊ธฐ์กด ๋ฐฉ์‹(์ „์ฒด ํ)์œผ๋กœ ๋„˜๊ธฐ๊ฑฐ๋‚˜ ์—๋Ÿฌ ๋ฐ˜ํ™˜ - return "extract_failed" + logger.info(f"Preparing GDM delegation for: {episode_info.get('title')}") + # Entity ์ธ์Šคํ„ด์Šค๋ฅผ ์ƒ์„ฑํ•˜์—ฌ ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ ํŒŒ์‹ฑ ๋ฐ URL ์ถ”์ถœ ์ˆ˜ํ–‰ + entity = Ohli24QueueEntity(P, self, episode_info) + + # URL/์ž๋ง‰/์ฟ ํ‚ค ์ถ”์ถœ ์ˆ˜ํ–‰ (๋™๊ธฐ์‹ - ์ƒ์œ„์—์„œ ๋น„๋™๊ธฐ๋กœ ํ˜ธ์ถœ ๊ถŒ์žฅ๋˜๋‚˜ ํ˜„์žฌ ajax_process๋Š” ๋™๊ธฐ) + try: + logger.debug(f"Calling entity.prepare_extra() for {episode_info.get('_id')}") + entity.prepare_extra() + logger.debug(f"entity.prepare_extra() done. URL found: {entity.url is not None}") + except Exception as e: + logger.error(f"Failed to extract video info: {e}") + # ์ถ”์ถœ ์‹คํŒจ ์‹œ ๊ธฐ์กด ๋ฐฉ์‹(์ „์ฒด ํ)์œผ๋กœ ๋„˜๊ธฐ๊ฑฐ๋‚˜ ์—๋Ÿฌ ๋ฐ˜ํ™˜ + return "extract_failed" - # ์ถ”์ถœ๋œ ์ •๋ณด๋ฅผ ๋ฐ”ํƒ•์œผ๋กœ GDM ์˜ต์…˜ ์ค€๋น„ (ํ‘œ์ค€ํ™”๋œ ํ•„๋“œ๋ช… ์‚ฌ์šฉ) - gdm_options = { - "url": entity.url, # ์ถ”์ถœ๋œ m3u8 URL - "save_path": entity.savepath, - "filename": entity.filename, - "source_type": "ani24", - "caller_plugin": f"{P.package_name}_{self.name}", - "callback_id": episode_info["_id"], - "title": entity.filename or episode_info.get('title'), - "thumbnail": episode_info.get('image'), - "meta": { - "series": entity.content_title, - "season": entity.season, - "episode": entity.epi_queue, - "source": "ohli24" - }, - # options ๋‚ด๋ถ€๊ฐ€ ์•„๋‹Œ ์ƒ์œ„ ๋ ˆ๋ฒจ๋กœ headers/cookies ์ „๋‹ฌ (GDM ํ‰ํƒ„ํ™” ๋Œ€์‘) - "headers": entity.headers, - "subtitles": entity.srt_url or entity.vtt, - "cookies_file": entity.cookies_file - } + # ์ถ”์ถœ๋œ ์ •๋ณด๋ฅผ ๋ฐ”ํƒ•์œผ๋กœ GDM ์˜ต์…˜ ์ค€๋น„ (ํ‘œ์ค€ํ™”๋œ ํ•„๋“œ๋ช… ์‚ฌ์šฉ) + gdm_options = { + "url": entity.url, # ์ถ”์ถœ๋œ m3u8 URL + "save_path": entity.savepath, + "filename": entity.filename, + "source_type": "ani24", + "caller_plugin": f"{P.package_name}_{self.name}", + "callback_id": episode_info["_id"], + "title": entity.filename or episode_info.get('title'), + "thumbnail": episode_info.get('image'), + "meta": { + "series": entity.content_title, + "season": entity.season, + "episode": entity.epi_queue, + "source": "ohli24" + }, + # options ๋‚ด๋ถ€๊ฐ€ ์•„๋‹Œ ์ƒ์œ„ ๋ ˆ๋ฒจ๋กœ headers/cookies ์ „๋‹ฌ (GDM ํ‰ํƒ„ํ™” ๋Œ€์‘) + "headers": entity.headers, + "subtitles": entity.srt_url or entity.vtt, + "cookies_file": entity.cookies_file + } - task = ModuleQueue.add_download(**gdm_options) - if task: - logger.info(f"Delegated Ohli24 download to GDM: {entity.filename}") - # 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" + try: + logger.debug(f"Calling ModuleQueue.add_download with options: {list(gdm_options.keys())}") + task = ModuleQueue.add_download(**gdm_options) + if task: + 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์—์„œ๋„ ์ด๋ฏธ ์ˆ˜ํ–‰ํ•˜์ง€๋งŒ ๋ช…์‹œ์  ์ƒํƒœ ๋ณ€๊ฒฝ) + 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 (๋˜๋Š” ์—๋Ÿฌ ์ฒ˜๋ฆฌ) logger.warning("GDM Module not found, falling back to FfmpegQueue")