Compare commits

...

11 Commits

8 changed files with 159 additions and 15 deletions

View File

@@ -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분)** 발급 로직 구현

View File

@@ -1,5 +1,5 @@
title: "애니 다운로더"
version: "0.5.37"
version: "0.6.6"
package_name: "anime_downloader"
developer: "projectdx"
description: "anime downloader"

View File

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

View File

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

View File

@@ -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')}")

View File

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

View File

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

View File

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