diff --git a/README.md b/README.md index e12774c..edae162 100644 --- a/README.md +++ b/README.md @@ -81,6 +81,13 @@ ## π λ³κ²½ μ΄λ ₯ (Changelog) +### v0.6.20 (2026-01-07) +- **GDM μ°λ κ³ λν λ° λ²κ·Έ μμ **: + - **App Context μ€λ₯ ν΄κ²°**: λ°±κ·ΈλΌμ΄λ μ°λ λ(μΌκ΄ μΆκ°, Camoufox μ€μΉ, μλ§ ν©μΉ¨)μμ λ°μνλ `RuntimeError: Working outside of application context` μμ . + - **λ€μ΄λ‘λ μ€μ μ°λ**: Ohli24 μ€μ μ λ€μ΄λ‘λ λ°©μ(aria2c/ytdlp) λ° μ°λ λ μλ₯Ό GDMμ κ·Έλλ‘ μ λ¬νμ¬ λ©ν°μ°λ λ λ€μ΄λ‘λ μ§μ. +- **μμ μ± κ°μ **: + - GDM μμ μ νμΌλͺ /μ λͺ©/μΈλ€μΌ λ± λ©νλ°μ΄ν° κ°κ³΅ λ‘μ§ λ³΄κ°. + ### v0.6.15 (2026-01-07) - **Zendriver Daemon λΉλκΈ° λ€λΉκ²μ΄μ μ΅μ ν**: - `browser.get(url)` λκΈ° μκ°μΌλ‘ μΈν 17μ΄ μ§μ° ν΄κ²° diff --git a/info.yaml b/info.yaml index 88bdbab..59f6987 100644 --- a/info.yaml +++ b/info.yaml @@ -1,5 +1,5 @@ title: "μ λ λ€μ΄λ‘λ" -version: "0.6.19" +version: "0.6.20" package_name: "anime_downloader" developer: "projectdx" description: "anime downloader" diff --git a/mod_anilife.py b/mod_anilife.py index 3eb493c..2b1755f 100644 --- a/mod_anilife.py +++ b/mod_anilife.py @@ -100,6 +100,8 @@ class LogicAniLife(AnimeModuleBase): "anilife_image_url_prefix_series": "https://www.jetcloud.cc/series/", "anilife_image_url_prefix_episode": "https://www.jetcloud-list.cc/thumbnail/", "anilife_camoufox_installed": "False", + "anilife_cache_minutes": "5", # HTML μΊμ μκ° (λΆ) + "anilife_zendriver_browser_path": "", # Zendriver λΈλΌμ°μ κ²½λ‘ } # Class variables for caching @@ -160,29 +162,30 @@ class LogicAniLife(AnimeModuleBase): # 3. μ€μ μ€μΉ/ν¨μΉ κ³Όμ μ§ν try: - # μμ€ν ν¨ν€μ§ xvfb μ€μΉ νμΈ (Linux/Docker μ μ©) - if platform.system() == 'Linux' and shutil.which('Xvfb') is None: - logger.info("Xvfb not found. Attempting to background install system package...") - try: - sp.run(['apt-get', 'update', '-qq'], capture_output=True) - sp.run(['apt-get', 'install', '-y', 'xvfb', '-qq'], capture_output=True) - except Exception as e: - logger.error(f"Failed to install xvfb system package: {e}") + with F.app.app_context(): + # μμ€ν ν¨ν€μ§ xvfb μ€μΉ νμΈ (Linux/Docker μ μ©) + if platform.system() == 'Linux' and shutil.which('Xvfb') is None: + logger.info("Xvfb not found. Attempting to background install system package...") + try: + sp.run(['apt-get', 'update', '-qq'], capture_output=True) + sp.run(['apt-get', 'install', '-y', 'xvfb', '-qq'], capture_output=True) + except Exception as e: + logger.error(f"Failed to install xvfb system package: {e}") - # Camoufox ν¨ν€μ§ νμΈ λ° μ€μΉ - if not lib_exists: - logger.info("Camoufox NOT found in DB or system. Installing in background...") - cmd = [sys.executable, "-m", "pip", "install", "camoufox[geoip]", "-q"] - sp.run(cmd, capture_output=True, text=True, timeout=120) - - logger.info("Ensuring Camoufox browser binary is fetched (pre-warming)...") - sp.run([sys.executable, "-m", "camoufox", "fetch"], capture_output=True, text=True, timeout=300) - - # μ±κ³΅ μ DBμ κΈ°λ‘νμ¬ λ€μ μ¬μμ μμλ μμ μ΄ κ³Όμ μ 건λλ - LogicAniLife.camoufox_setup_done = True - P.ModelSetting.set("anilife_camoufox_installed", "True") - logger.info("Camoufox setup finished and persisted to DB") - return True + # Camoufox ν¨ν€μ§ νμΈ λ° μ€μΉ + if not lib_exists: + logger.info("Camoufox NOT found in DB or system. Installing in background...") + cmd = [sys.executable, "-m", "pip", "install", "camoufox[geoip]", "-q"] + sp.run(cmd, capture_output=True, text=True, timeout=120) + + logger.info("Ensuring Camoufox browser binary is fetched (pre-warming)...") + sp.run([sys.executable, "-m", "camoufox", "fetch"], capture_output=True, text=True, timeout=300) + + # μ±κ³΅ μ DBμ κΈ°λ‘νμ¬ λ€μ μ¬μμ μμλ μμ μ΄ κ³Όμ μ 건λλ + LogicAniLife.camoufox_setup_done = True + P.ModelSetting.set("anilife_camoufox_installed", "True") + logger.info("Camoufox setup finished and persisted to DB") + return True except Exception as install_err: logger.error(f"Failed during Camoufox setup: {install_err}") return lib_exists @@ -232,7 +235,10 @@ class LogicAniLife(AnimeModuleBase): db_item = ModelAniLifeItem.get_by_id(db_id) if db_item and db_item.status == 'completed': import threading - threading.Thread(target=AniUtil.merge_subtitle, args=(self.P, db_item)).start() + def merge_with_ctx(P, db_item): + with F.app.app_context(): + AniUtil.merge_subtitle(P, db_item) + threading.Thread(target=merge_with_ctx, args=(self.P, db_item)).start() return jsonify({"ret": "success", "log": "μλ§ ν©μΉ©μ μμν©λλ€."}) return jsonify({"ret": "fail", "log": "νμΌμ μ°Ύμ μ μκ±°λ μλ£λ μνκ° μλλλ€."}) @@ -718,19 +724,20 @@ class LogicAniLife(AnimeModuleBase): data = json.loads(request.form["data"]) def func(): - count = 0 - for tmp in data: - add_ret = self.add(tmp) - if add_ret.startswith("enqueue"): - self.socketio_callback("list_refresh", "") - count += 1 - notify = { - "type": "success", - "msg": "%s κ°μ μνΌμλλ₯Ό νμ μΆκ° νμμ΅λλ€." % count, - } - socketio.emit( - "notify", notify, namespace="/framework" - ) + with F.app.app_context(): + count = 0 + for tmp in data: + add_ret = self.add(tmp) + if add_ret.startswith("enqueue"): + self.socketio_callback("list_refresh", "") + count += 1 + notify = { + "type": "success", + "msg": "%s κ°μ μνΌμλλ₯Ό νμ μΆκ° νμμ΅λλ€." % count, + } + socketio.emit( + "notify", notify, namespace="/framework" + ) thread = threading.Thread(target=func, args=()) thread.daemon = True thread.start() @@ -739,10 +746,124 @@ class LogicAniLife(AnimeModuleBase): image_url = request.args.get("url") or request.args.get("image_url") return self.proxy_image(image_url) elif sub == "entity_list": + # GDM μ°λ: ModuleQueueμμ anilife νλ¬κ·ΈμΈ νλͺ©λ§ νν°λ§ + if ModuleQueue: + caller_id = f"{P.package_name}_{self.name}" + all_items: List[Dict[str, Any]] = [d.get_status() for d in ModuleQueue._downloads.values()] + plugin_items = [i for i in all_items if i.get('caller_plugin') == caller_id] + + # μν νκΈ λ§€ν + status_map: Dict[str, str] = { + 'pending': 'λκΈ°μ€', + 'extracting': 'μΆμΆμ€', + 'downloading': 'λ€μ΄λ‘λμ€', + 'paused': 'μΌμμ μ§', + 'completed': 'μλ£', + 'error': 'μ€ν¨', + 'cancelled': 'μ·¨μλ¨' + } + + mapped_items: List[Dict[str, Any]] = [] + active_ids: set = set() + + for item in plugin_items: + active_ids.add(item.get('callback_id')) + mapped: Dict[str, Any] = { + 'idx': item['id'], + 'filename': item.get('filename') or item.get('title') or 'νμΌλͺ μμ', + 'percent': item.get('progress', 0), + 'status_str': str(item.get('status', 'pending')).upper(), + 'status_kor': status_map.get(str(item.get('status', 'pending')), 'μ μ μμ'), + 'current_speed': item.get('speed', ''), + 'start_time': item.get('start_time', item.get('created_time', '')), + 'download_time': item.get('eta', ''), + 'callback_id': item.get('caller_plugin', '').split('_')[-1] if item.get('caller_plugin') else 'anilife', + } + mapped_items.append(mapped) + + # DBμμ μ΅κ·Ό μλ£ νλͺ© μΆκ° (μμμ±) + try: + from framework import F + with F.app.app_context(): + db_items = F.db.session.query(ModelAniLifeItem).order_by(ModelAniLifeItem.id.desc()).limit(50).all() + for db_item in db_items: + if db_item.anilife_id in active_ids: + continue + if db_item.status == 'completed': + mapped: Dict[str, Any] = { + 'idx': f"db_{db_item.id}", + 'filename': db_item.filename or 'νμΌλͺ μμ', + 'percent': 100, + 'status_str': 'COMPLETED', + 'status_kor': 'μλ£', + 'current_speed': '', + 'start_time': str(db_item.created_time) if db_item.created_time else '', + 'download_time': '', + 'callback_id': 'anilife', + } + mapped_items.append(mapped) + except Exception as db_err: + logger.warning(f"Failed to add DB items: {db_err}") + + return jsonify(mapped_items) + + # Fallback: κΈ°μ‘΄ ν μμ€ν if self.queue is not None: return jsonify(self.queue.get_entity_list()) - else: - return jsonify([]) + return jsonify([]) + + elif sub == "queue_command": + command = req.form.get("command", "") + entity_id = req.form.get("entity_id", "") + + if ModuleQueue: + if command in ["stop", "cancel"]: + # νΉμ λ€μ΄λ‘λ μ·¨μ + if entity_id and entity_id in ModuleQueue._downloads: + ModuleQueue._downloads[entity_id].cancel() + return jsonify({"ret": "success", "log": "λ€μ΄λ‘λκ° μ·¨μλμμ΅λλ€."}) + return jsonify({"ret": "error", "log": "λ€μ΄λ‘λλ₯Ό μ°Ύμ μ μμ΅λλ€."}) + + elif command == "reset": + # Anilife λͺ¨λμ λ€μ΄λ‘λλ§ μ·¨μ (λ€λ₯Έ νλ¬κ·ΈμΈ νλͺ©μ κ·Έλλ‘) + caller_id = f"{P.package_name}_{self.name}" + cancelled_count = 0 + for task_id, task in list(ModuleQueue._downloads.items()): + if task.caller_plugin == caller_id: + task.cancel() + del ModuleQueue._downloads[task_id] + cancelled_count += 1 + + # Anilife DBλ μ 리 + try: + from framework import F + with F.app.app_context(): + F.db.session.query(ModelAniLifeItem).delete() + F.db.session.commit() + except Exception as e: + logger.error(f"Failed to clear Anilife DB: {e}") + return jsonify({"ret": "notify", "log": f"{cancelled_count}κ° Anilife νλͺ©μ΄ μ΄κΈ°νλμμ΅λλ€."}) + + elif command == "delete_completed": + # μλ£λ νλͺ©λ§ DBμμ μμ + try: + from framework import F + with F.app.app_context(): + deleted = F.db.session.query(ModelAniLifeItem).filter( + ModelAniLifeItem.status == 'completed' + ).delete() + F.db.session.commit() + return jsonify({"ret": "success", "log": f"{deleted}κ° μλ£ νλͺ©μ΄ μμ λμμ΅λλ€."}) + except Exception as e: + logger.error(f"Failed to delete completed: {e}") + return jsonify({"ret": "error", "log": str(e)}) + + # Fallback: κΈ°μ‘΄ ν μμ€ν + if self.queue: + ret = self.queue.command(command, int(entity_id) if entity_id.isdigit() else 0) + return jsonify(ret) + return jsonify({"ret": "error", "log": "Queue not initialized"}) + elif sub == "web_list": return jsonify(ModelAniLifeItem.web_list(request)) elif sub == "db_remove": @@ -818,6 +939,37 @@ class LogicAniLife(AnimeModuleBase): logger.error(f"browse_dir error: {e}") return jsonify({"ret": "error", "error": str(e)}), 500 + elif sub == "system_check": + # μμ€ν μ²΄ν¬ (Zendriver λΈλΌμ°μ μ€μΉ μν) + from .mod_ohli24 import LogicOhli24 + result: Dict[str, Any] = LogicOhli24.system_check() + return jsonify(result) + + elif sub == "install_browser": + # μμ€ν λΈλΌμ°μ μ€μΉ (Ubuntu/Docker) + from .mod_ohli24 import LogicOhli24 + result: Dict[str, Any] = LogicOhli24.install_system_browser() + if result.get("ret") == "success" and result.get("path"): + P.ModelSetting.set("anilife_zendriver_browser_path", result["path"]) + return jsonify(result) + + elif sub == "immediately_execute": + # μ€μΌμ€λ¬ 1ν μ€ν + try: + self.scheduler_function() + return jsonify({"ret": "success"}) + except Exception as e: + logger.error(f"immediately_execute error: {e}") + return jsonify({"ret": "error", "msg": str(e)}) + + elif sub == "reset_db": + # DB μ΄κΈ°ν + try: + self.reset_db() + return jsonify({"ret": "success"}) + except Exception as e: + logger.error(f"reset_db error: {e}") + return jsonify({"ret": "error", "msg": str(e)}) # Fallback to base class for common subs (queue_command, entity_list, browse_dir, command, etc.) return super().process_ajax(sub, req) diff --git a/mod_ohli24.py b/mod_ohli24.py index 36004ce..1213d2f 100644 --- a/mod_ohli24.py +++ b/mod_ohli24.py @@ -2294,11 +2294,20 @@ class LogicOhli24(AnimeModuleBase): return "extract_failed" # μΆμΆλ μ 보λ₯Ό λ°νμΌλ‘ GDM μ΅μ μ€λΉ (νμ€νλ νλλͺ μ¬μ©) + download_method = P.ModelSetting.get("ohli24_download_method") + download_threads = P.ModelSetting.get_int("ohli24_download_threads") + + # GDM μμ€ νμ κ²°μ (λ©ν°μ°λ λ/aria2c μ¬μ© μ¬λΆμ λ°λΌ) + # GDMμ 'general'μ yt-dlp + aria2cλ₯Ό μ¬μ©ν¨ + gdm_source_type = "ani24" + if download_method in ['ytdlp', 'aria2c']: + gdm_source_type = "general" + gdm_options = { "url": entity.url, # μΆμΆλ m3u8 URL "save_path": entity.savepath, "filename": entity.filename, - "source_type": "ani24", + "source_type": gdm_source_type, "caller_plugin": f"{P.package_name}_{self.name}", "callback_id": episode_info["_id"], "title": entity.filename or episode_info.get('title'), @@ -2309,6 +2318,7 @@ class LogicOhli24(AnimeModuleBase): "episode": entity.epi_queue, "source": "ohli24" }, + "connections": download_threads, # λ©ν°μ°λ λ κ°μ μ λ¬ # options λ΄λΆκ° μλ μμ λ λ²¨λ‘ headers/cookies μ λ¬ (GDM ννν λμ) "headers": entity.headers, "subtitles": entity.srt_url or entity.vtt, diff --git a/templates/anime_downloader_anilife_queue.html b/templates/anime_downloader_anilife_queue.html index 30720ec..f344411 100644 --- a/templates/anime_downloader_anilife_queue.html +++ b/templates/anime_downloader_anilife_queue.html @@ -3,31 +3,24 @@ - -
| IDX | -Plugin | +νμΌλͺ | +μν | +μ§νλ₯ | +μλ | μμμκ° | -νμΌλͺ | -μν | -μ§νλ₯ | -κΈΈμ΄ | -PF | -λ°°μ | -μ§νμκ° | Action |
|---|