Compare commits
11 Commits
254a387cbd
...
afce36640d
| Author | SHA1 | Date | |
|---|---|---|---|
| afce36640d | |||
| 5f7521eb6b | |||
| 2f0523b70d | |||
| e75e34dadd | |||
| 62dfb2a8b2 | |||
| def2b5b3c5 | |||
| 72e0882308 | |||
| 0a2bb86504 | |||
| f2aa78fa48 | |||
| 92276396ce | |||
| a6affc5b2b |
13
README.md
13
README.md
@@ -81,6 +81,19 @@
|
||||
|
||||
## 📝 변경 이력 (Changelog)
|
||||
|
||||
### v0.6.0 (2026-01-07)
|
||||
- **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 단축 파일명 생성 방지
|
||||
- **Git 워크플로우 개선**:
|
||||
- GitHub + Gitea 양방향 동시 푸시 설정 (GitHub 우선)
|
||||
|
||||
### v0.5.3 (2026-01-04)
|
||||
- **보안 스트리밍 토큰 시스템 도입**:
|
||||
- 외부 플레이어 연동 시 API 키 노출 방지를 위한 **임시 토큰(TTL 5분)** 발급 로직 구현
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
title: "애니 다운로더"
|
||||
version: "0.5.37"
|
||||
version: "0.6.6"
|
||||
package_name: "anime_downloader"
|
||||
developer: "projectdx"
|
||||
description: "anime downloader"
|
||||
|
||||
20
lib/util.py
20
lib/util.py
@@ -58,11 +58,21 @@ class Util(object):
|
||||
|
||||
@staticmethod
|
||||
def change_text_for_use_filename(text):
|
||||
# text = text.replace('/', '')
|
||||
# 2021-07-31 X:X
|
||||
# text = text.replace(':', ' ')
|
||||
text = re.sub('[\\/:*?\"<>|]', ' ', text).strip()
|
||||
text = re.sub("\s{2,}", ' ', text)
|
||||
# 1. Remove/replace Windows-forbidden characters
|
||||
text = re.sub('[\\/:*?"<>|]', ' ', text)
|
||||
|
||||
# 2. Remove consecutive dots (.. → .)
|
||||
text = re.sub(r'\.{2,}', '.', text)
|
||||
|
||||
# 3. Remove leading/trailing dots and spaces
|
||||
text = text.strip('. ')
|
||||
|
||||
# 4. Collapse multiple spaces to single space
|
||||
text = re.sub(r'\s{2,}', ' ', text)
|
||||
|
||||
# 5. Remove any remaining trailing dots (after space collapse)
|
||||
text = text.rstrip('.')
|
||||
|
||||
return text
|
||||
|
||||
@staticmethod
|
||||
|
||||
@@ -65,6 +65,13 @@ from typing import Awaitable, TypeVar
|
||||
T = TypeVar("T")
|
||||
|
||||
from .setup import *
|
||||
from requests_cache import CachedSession
|
||||
|
||||
# GDM Integration
|
||||
try:
|
||||
from gommi_downloader_manager.mod_queue import ModuleQueue
|
||||
except ImportError:
|
||||
ModuleQueue = None
|
||||
|
||||
logger = P.logger
|
||||
name = "anilife"
|
||||
@@ -74,6 +81,8 @@ class LogicAniLife(AnimeModuleBase):
|
||||
db_default = {
|
||||
"anilife_db_version": "1",
|
||||
"anilife_url": "https://anilife.live",
|
||||
"anilife_proxy_url": "",
|
||||
"anilife_cache_ttl": "300", # HTTP cache TTL in seconds (5 minutes)
|
||||
"anilife_download_path": os.path.join(path_data, P.package_name, "ohli24"),
|
||||
"anilife_auto_make_folder": "True",
|
||||
"anilife_auto_make_season_folder": "True",
|
||||
@@ -93,6 +102,35 @@ class LogicAniLife(AnimeModuleBase):
|
||||
"anilife_camoufox_installed": "False",
|
||||
}
|
||||
|
||||
# Class variables for caching
|
||||
cache_path = os.path.dirname(__file__)
|
||||
session = None
|
||||
|
||||
@classmethod
|
||||
def get_proxy(cls) -> str:
|
||||
return P.ModelSetting.get("anilife_proxy_url")
|
||||
|
||||
@classmethod
|
||||
def get_proxies(cls) -> Optional[Dict[str, str]]:
|
||||
proxy = cls.get_proxy()
|
||||
if proxy:
|
||||
return {"http": proxy, "https": proxy}
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
def get_session(cls):
|
||||
"""Get or create a cached session for HTTP requests."""
|
||||
if cls.session is None:
|
||||
cache_ttl = P.ModelSetting.get_int("anilife_cache_ttl")
|
||||
cls.session = CachedSession(
|
||||
os.path.join(cls.cache_path, "anilife_cache"),
|
||||
backend="sqlite",
|
||||
expire_after=cache_ttl,
|
||||
cache_control=True,
|
||||
)
|
||||
logger.info(f"[Anilife] CachedSession initialized with TTL: {cache_ttl}s")
|
||||
return cls.session
|
||||
|
||||
current_headers = None
|
||||
current_data = None
|
||||
referer = None
|
||||
@@ -150,6 +188,7 @@ class LogicAniLife(AnimeModuleBase):
|
||||
return lib_exists
|
||||
|
||||
session = requests.Session()
|
||||
cached_session = None # Will be initialized on first use
|
||||
headers = {
|
||||
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.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.9",
|
||||
@@ -1318,7 +1357,40 @@ class LogicAniLife(AnimeModuleBase):
|
||||
db_entity.save()
|
||||
return "file_exists"
|
||||
|
||||
# 4. Proceed with queue addition
|
||||
# 4. Try GDM if available (like Ohli24)
|
||||
if ModuleQueue is not None:
|
||||
entity = AniLifeQueueEntity(P, self, episode_info)
|
||||
logger.debug("entity:::> %s", entity.as_dict())
|
||||
|
||||
# Save to DB first
|
||||
if db_entity is None:
|
||||
ModelAniLifeItem.append(entity.as_dict())
|
||||
|
||||
# Prepare GDM options (same pattern as Ohli24)
|
||||
gdm_options = {
|
||||
"url": entity.url,
|
||||
"save_path": entity.savepath,
|
||||
"filename": entity.filename,
|
||||
"source_type": "anilife",
|
||||
"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": "anilife"
|
||||
},
|
||||
}
|
||||
|
||||
task = ModuleQueue.add_download(**gdm_options)
|
||||
if task:
|
||||
logger.info(f"Delegated Anilife download to GDM: {entity.filename}")
|
||||
return "enqueue_gdm_success"
|
||||
|
||||
# 5. Fallback to FfmpegQueue if GDM not available
|
||||
logger.warning("GDM Module not found, falling back to FfmpegQueue")
|
||||
if db_entity is None:
|
||||
logger.debug(f"episode_info:: {episode_info}")
|
||||
entity = AniLifeQueueEntity(P, self, episode_info)
|
||||
|
||||
@@ -30,6 +30,12 @@ from lxml import html
|
||||
from .mod_base import AnimeModuleBase
|
||||
from requests_cache import CachedSession
|
||||
|
||||
# GDM Integration
|
||||
try:
|
||||
from gommi_downloader_manager.mod_queue import ModuleQueue
|
||||
except ImportError:
|
||||
ModuleQueue = None
|
||||
|
||||
# cloudscraper는 lazy import로 처리
|
||||
import cloudscraper
|
||||
|
||||
@@ -1569,7 +1575,40 @@ class LogicLinkkf(AnimeModuleBase):
|
||||
db_entity.save()
|
||||
return "file_exists"
|
||||
|
||||
# 4. Proceed with queue addition
|
||||
# 4. Try GDM if available (like Ohli24/Anilife)
|
||||
if ModuleQueue is not None:
|
||||
entity = LinkkfQueueEntity(P, self, episode_info)
|
||||
logger.debug("entity:::> %s", entity.as_dict())
|
||||
|
||||
# Save to DB first
|
||||
if db_entity is None:
|
||||
ModelLinkkfItem.append(entity.as_dict())
|
||||
|
||||
# Prepare GDM options
|
||||
gdm_options = {
|
||||
"url": entity.url,
|
||||
"save_path": entity.savepath,
|
||||
"filename": entity.filename,
|
||||
"source_type": "linkkf",
|
||||
"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": "linkkf"
|
||||
},
|
||||
}
|
||||
|
||||
task = ModuleQueue.add_download(**gdm_options)
|
||||
if task:
|
||||
logger.info(f"Delegated Linkkf download to GDM: {entity.filename}")
|
||||
return "enqueue_gdm_success"
|
||||
|
||||
# 5. Fallback to FfmpegQueue if GDM not available
|
||||
logger.warning("GDM Module not found, falling back to FfmpegQueue")
|
||||
queue_len = len(self.queue.entity_list) if self.queue else 0
|
||||
logger.info(f"add() - Queue length: {queue_len}, episode _id: {episode_info.get('_id')}")
|
||||
|
||||
|
||||
@@ -59,7 +59,7 @@ from .mod_base import AnimeModuleBase
|
||||
from .model_base import AnimeQueueEntity
|
||||
|
||||
try:
|
||||
from gommi_download_manager.mod_queue import ModuleQueue
|
||||
from gommi_downloader_manager.mod_queue import ModuleQueue
|
||||
except ImportError:
|
||||
ModuleQueue = None
|
||||
|
||||
@@ -1222,6 +1222,8 @@ class LogicOhli24(AnimeModuleBase):
|
||||
|
||||
def setting_save_after(self, change_list: List[str]) -> None:
|
||||
"""설정 저장 후 처리."""
|
||||
if self.queue is None:
|
||||
return
|
||||
if self.queue.get_max_ffmpeg_count() != P.ModelSetting.get_int("ohli24_max_ffmpeg_process_count"):
|
||||
self.queue.set_max_ffmpeg_count(P.ModelSetting.get_int("ohli24_max_ffmpeg_process_count"))
|
||||
|
||||
@@ -2669,6 +2671,14 @@ class Ohli24QueueEntity(AnimeQueueEntity):
|
||||
def download_completed(self) -> None:
|
||||
super().download_completed()
|
||||
logger.debug("download completed.......!!")
|
||||
|
||||
# Verify file actually exists before marking as completed
|
||||
if not self.filepath or not os.path.exists(self.filepath):
|
||||
logger.warning(f"[DB_COMPLETE] File does not exist after download_completed: {self.filepath}")
|
||||
# Call download_failed instead
|
||||
self.download_failed("File not found after download")
|
||||
return
|
||||
|
||||
logger.debug(f"[DB_COMPLETE] Looking up entity by ohli24_id: {self.info.get('_id')}")
|
||||
db_entity = ModelOhli24Item.get_by_ohli24_id(self.info["_id"])
|
||||
logger.debug(f"[DB_COMPLETE] Found db_entity: {db_entity}")
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
<div class="d-flex justify-content-start align-items-center gap-2 mb-4">
|
||||
<button id="reset_btn" class="btn custom-btn btn-reset-queue"><i class="fa fa-refresh mr-2"></i> 초기화</button>
|
||||
<button id="delete_completed_btn" class="btn custom-btn btn-delete-completed"><i class="fa fa-trash-o mr-2"></i> 완료 목록 삭제</button>
|
||||
<button id="go_ffmpeg_btn" class="btn custom-btn btn-ffmpeg"><i class="fa fa-film mr-2"></i> Go FFMPEG</button>
|
||||
<button id="go_gdm_btn" class="btn custom-btn btn-gdm"><i class="fa fa-download mr-2"></i> Go GDM</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id='page1'></div>
|
||||
@@ -329,9 +329,9 @@
|
||||
queue_command(send_data)
|
||||
});
|
||||
|
||||
$("body").on('click', '#go_ffmpeg_btn', function (e) {
|
||||
$("body").on('click', '#go_gdm_btn', function (e) {
|
||||
e.preventDefault();
|
||||
window.location.href = '/ffmpeg/list';
|
||||
window.location.href = '/gommi_downloader_manager/queue/list';
|
||||
});
|
||||
|
||||
function queue_command(data) {
|
||||
@@ -412,8 +412,8 @@
|
||||
.btn-delete-completed { background: rgba(239, 68, 68, 0.2); border-color: rgba(239, 68, 68, 0.3); }
|
||||
.btn-delete-completed:hover { background: rgba(239, 68, 68, 0.4); transform: translateY(-2px); box-shadow: 0 6px 20px rgba(239, 68, 68, 0.3); }
|
||||
|
||||
.btn-ffmpeg { background: rgba(139, 92, 246, 0.2); border-color: rgba(139, 92, 246, 0.3); }
|
||||
.btn-ffmpeg:hover { background: rgba(139, 92, 246, 0.4); transform: translateY(-2px); box-shadow: 0 6px 20px rgba(139, 92, 246, 0.3); }
|
||||
.btn-gdm { background: rgba(56, 189, 248, 0.2); border-color: rgba(56, 189, 248, 0.3); }
|
||||
.btn-gdm:hover { background: rgba(56, 189, 248, 0.4); transform: translateY(-2px); box-shadow: 0 6px 20px rgba(56, 189, 248, 0.3); }
|
||||
|
||||
/* Action buttons inside table */
|
||||
.action-btn {
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
<form id="setting" class="mt-4">
|
||||
<div class="tab-content" id="nav-tabContent">
|
||||
{{ macros.m_tab_content_start('normal', true) }}
|
||||
{{ macros.setting_input_text_and_buttons('anilife_url', '애니라이프 URL', [['go_btn', 'GO']], value=arg['anilife_url']) }}
|
||||
{{ macros.setting_input_text_and_buttons('anilife_url', '애니라이프 URL', [['go_btn', 'GO']], value=arg['anilife_url']) }}\n {{ macros.setting_input_text('anilife_proxy_url', '프록시 URL', col='4', value=arg.get('anilife_proxy_url', ''), desc='차단 시 프록시 서버를 입력하세요. 예: http://IP:PORT') }}\n {{ macros.setting_input_int('anilife_cache_ttl', 'HTTP 캐시 TTL (초)', value=arg.get('anilife_cache_ttl', 300), desc='HTTP 응답 캐시 유지 시간 (초 단위, 기본: 300초 = 5분)') }}
|
||||
|
||||
<!-- 저장 폴더 (탐색 버튼 포함) -->
|
||||
<div class="row" style="padding-top: 10px; padding-bottom:10px; align-items: center;">
|
||||
|
||||
Reference in New Issue
Block a user