Compare commits

..

5 Commits

5 changed files with 199 additions and 22 deletions

View File

@@ -3,7 +3,13 @@
FlaskFarm용 범용 다운로드 매니저 플러그인입니다. FlaskFarm용 범용 다운로드 매니저 플러그인입니다.
여러 다운로더 플러그인(YouTube, Anime 등)의 다운로드 요청을 통합 관리하고 큐(Queue)를 제공합니다. 여러 다운로더 플러그인(YouTube, Anime 등)의 다운로드 요청을 통합 관리하고 큐(Queue)를 제공합니다.
## v0.2.0 변경사항 ## v0.2.8 변경사항 (2026-01-07)
- **콜백 시스템 개선**: `module_list`가 리스트 형태인 플러그인(애니 다운로더 등)과의 콜백 연동 호환성 해결 (`AttributeError` 수정).
- **메타데이터 강화**: 다운로드 시작/종료 시간 및 최종 파일 크기 추적 기능 추가.
- **UI 상세 정보 보강**: GDM 큐 목록에서 시작 시간, 종료 시간, 파일 크기를 상세 패널에 표시.
- **DB 정밀 동기화**: 다운로드 완료 시 실제 파일 크기를 DB에 영구 저장.
## v0.2.7 변경사항
- **패키지명 수정**: `gommi_download_manager` -> `gommi_downloader_manager`로 폴더명과 일치시켜 Bind Key 오류 해결. - **패키지명 수정**: `gommi_download_manager` -> `gommi_downloader_manager`로 폴더명과 일치시켜 Bind Key 오류 해결.
- **안정성 개선**: DB 테이블 생성 로직 강화 (`setup.py` 명시적 모델 import). - **안정성 개선**: DB 테이블 생성 로직 강화 (`setup.py` 명시적 모델 import).
- **YouTube 제목 지원**: `yt-dlp` 다운로드 시작 시 영상의 진짜 제목과 썸네일을 실시간으로 DB에 업데이트합니다. - **YouTube 제목 지원**: `yt-dlp` 다운로드 시작 시 영상의 진짜 제목과 썸네일을 실시간으로 DB에 업데이트합니다.

View File

@@ -1,6 +1,6 @@
title: "GDM" title: "GDM"
package_name: gommi_downloader_manager package_name: gommi_downloader_manager
version: '0.2.3' version: '0.2.9'
description: FlaskFarm 범용 다운로더 큐 - YouTube, 애니24, 링크애니, Anilife 지원 description: FlaskFarm 범용 다운로더 큐 - YouTube, 애니24, 링크애니, Anilife 지원
developer: projectdx developer: projectdx
home: https://gitea.yommi.duckdns.org/projectdx/gommi_downloader_manager home: https://gitea.yommi.duckdns.org/projectdx/gommi_downloader_manager

View File

@@ -396,6 +396,9 @@ class DownloadTask:
self._downloader = None self._downloader = None
self._cancelled = False self._cancelled = False
self.db_id: Optional[int] = None self.db_id: Optional[int] = None
self.start_time: Optional[str] = None
self.end_time: Optional[str] = None
self.created_time: str = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
def start(self): def start(self):
"""다운로드 시작 (비동기)""" """다운로드 시작 (비동기)"""
@@ -406,6 +409,8 @@ class DownloadTask:
"""다운로드 실행""" """다운로드 실행"""
try: try:
self.status = DownloadStatus.EXTRACTING self.status = DownloadStatus.EXTRACTING
if not self.start_time:
self.start_time = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
self._emit_status() self._emit_status()
# 다운로더 선택 및 실행 # 다운로더 선택 및 실행
@@ -434,13 +439,17 @@ class DownloadTask:
self.status = DownloadStatus.COMPLETED self.status = DownloadStatus.COMPLETED
self.filepath = result.get('filepath', '') self.filepath = result.get('filepath', '')
self.progress = 100 self.progress = 100
self.end_time = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
if self.filepath and os.path.exists(self.filepath):
self.filesize = os.path.getsize(self.filepath)
# DB 업데이트 # DB 업데이트
self._update_db_status() self._update_db_status()
# 실시간 콜백 처리 # 실시간 콜백 처리
if self._on_complete: if self._on_complete:
self._on_complete(self.filepath) try: self._on_complete(self.filepath)
except: pass
# 플러그인 간 영구적 콜백 처리 # 플러그인 간 영구적 콜백 처리
if self.caller_plugin and self.callback_id: if self.caller_plugin and self.callback_id:
@@ -566,6 +575,7 @@ class DownloadTask:
item.status = self.status item.status = self.status
if self.status == DownloadStatus.COMPLETED: if self.status == DownloadStatus.COMPLETED:
item.completed_time = datetime.now() item.completed_time = datetime.now()
item.filesize = self.filesize
if self.error_message: if self.error_message:
item.error_message = self.error_message item.error_message = self.error_message
F.db.session.add(item) F.db.session.add(item)
@@ -605,7 +615,15 @@ class DownloadTask:
if target_P: if target_P:
# 모듈에서 콜백 메서드 찾기 # 모듈에서 콜백 메서드 찾기
callback_invoked = False callback_invoked = False
for module_name, module_instance in getattr(target_P, 'module_list', {}).items(): module_list = getattr(target_P, 'module_list', [])
if isinstance(module_list, dict):
modules = module_list.items()
elif isinstance(module_list, list):
modules = [(getattr(m, 'name', str(i)), m) for i, m in enumerate(module_list)]
else:
modules = []
for module_name, module_instance in modules:
if hasattr(module_instance, 'plugin_callback'): if hasattr(module_instance, 'plugin_callback'):
callback_data = { callback_data = {
'callback_id': self.callback_id, 'callback_id': self.callback_id,
@@ -647,4 +665,8 @@ class DownloadTask:
'caller_plugin': self.caller_plugin, 'caller_plugin': self.caller_plugin,
'callback_id': self.callback_id, 'callback_id': self.callback_id,
'db_id': self.db_id, 'db_id': self.db_id,
'start_time': self.start_time,
'end_time': self.end_time,
'created_time': self.created_time,
'file_size': self.filesize,
} }

View File

@@ -57,6 +57,10 @@ class ModelDownloadItem(ModelBase):
ret['meta'] = {} ret['meta'] = {}
else: else:
ret['meta'] = {} ret['meta'] = {}
if self.created_time:
ret['created_time'] = self.created_time.strftime('%Y-%m-%d %H:%M:%S')
# JS UI expects file_size (with underscore)
ret['file_size'] = self.filesize or 0
return ret return ret
@classmethod @classmethod

View File

@@ -8,21 +8,42 @@
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet"> <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<style> <style>
/* Premium Modern Design System */ /* ========================================
:root { GDM FORCED SUPERHERO THEME
--bg-body: #0f172a; (Overrides global FlaskFarm theme)
--surface: rgba(30, 41, 59, 0.7); ======================================== */
--surface-opaque: #1e293b;
--border: rgba(255, 255, 255, 0.1); /* Superhero Color Palette - HARDCODED */
--text-main: #f8fafc; :root, #gommi_download_manager_queue_list, #gommi_download_manager_queue_list * {
--text-muted: #94a3b8; --bg-body: #2b3e50 !important;
--accent-primary: #38bdf8; /* Sky Blue */ --surface: rgba(62, 88, 112, 0.9) !important;
--accent-secondary: #818cf8; /* Indigo */ --surface-opaque: #3e5870 !important;
--success: #10b981; --border: rgba(255, 255, 255, 0.15) !important;
--warning: #f59e0b; --text-main: #ebebeb !important;
--danger: #ef4444; --text-muted: #abb6c2 !important;
--glow: rgba(56, 189, 248, 0.3); --accent-primary: #4e9fd9 !important;
--font-sans: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; --accent-secondary: #5bc0de !important;
--success: #4ebf73 !important;
--warning: #f0ad4e !important;
--danger: #d9534f !important;
--glow: rgba(78, 159, 217, 0.4) !important;
--font-sans: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif !important;
}
/* FORCE body background (overrides global theme) */
body {
background-color: #2b3e50 !important;
color: #ebebeb !important;
}
/* Also override main containers that might have theme colors */
.main, #main, .container, .container-fluid, #main_container {
background-color: #2b3e50 !important;
}
/* Navigation bar override */
.navbar, .navbar-dark, .bg-dark, .bg-primary {
background-color: #3e5770 !important;
} }
#loading { display: none !important; } #loading { display: none !important; }
@@ -30,7 +51,7 @@
#gommi_download_manager_queue_list { #gommi_download_manager_queue_list {
font-family: var(--font-sans); font-family: var(--font-sans);
color: var(--text-main); color: var(--text-main);
background-color: transparent; background-color: #2b3e50 !important;
padding-bottom: 2rem; padding-bottom: 2rem;
} }
@@ -128,6 +149,7 @@
letter-spacing: -0.025em; letter-spacing: -0.025em;
background: linear-gradient(135deg, #fff 0%, #cbd5e1 100%); background: linear-gradient(135deg, #fff 0%, #cbd5e1 100%);
-webkit-background-clip: text; -webkit-background-clip: text;
background-clip: text;
-webkit-text-fill-color: transparent; -webkit-text-fill-color: transparent;
} }
@@ -242,6 +264,7 @@
line-height: 1.4; line-height: 1.4;
display: -webkit-box; display: -webkit-box;
-webkit-line-clamp: 2; -webkit-line-clamp: 2;
line-clamp: 2;
-webkit-box-orient: vertical; -webkit-box-orient: vertical;
overflow: hidden; overflow: hidden;
word-break: break-all; word-break: break-all;
@@ -379,6 +402,71 @@
margin-bottom: 1rem; margin-bottom: 1rem;
opacity: 0.3; opacity: 0.3;
} }
/* Expandable Detail Panel */
.dl-card {
cursor: pointer;
}
.dl-detail-panel {
max-height: 0;
overflow: hidden;
transition: max-height 0.3s ease-out, padding 0.3s ease-out, margin 0.3s ease-out;
background: rgba(0, 0, 0, 0.2);
border-radius: 10px;
margin-top: 0;
padding: 0 1rem;
}
.dl-card.expanded .dl-detail-panel {
max-height: 300px;
padding: 1rem;
margin-top: 1rem;
}
.dl-detail-row {
display: flex;
justify-content: space-between;
align-items: flex-start;
padding: 0.4rem 0;
border-bottom: 1px solid rgba(255,255,255,0.05);
font-size: 0.75rem;
}
.dl-detail-row:last-child {
border-bottom: none;
}
.dl-detail-label {
color: var(--text-muted);
flex-shrink: 0;
margin-right: 1rem;
}
.dl-detail-value {
color: var(--text-main);
word-break: break-all;
text-align: right;
max-width: 70%;
}
.dl-detail-value.url {
font-family: monospace;
font-size: 0.65rem;
opacity: 0.8;
}
.dl-expand-hint {
font-size: 0.65rem;
color: var(--text-muted);
text-align: center;
margin-top: 0.5rem;
opacity: 0.6;
}
.dl-card.expanded .dl-expand-hint {
display: none;
}
</style> </style>
<div id="gommi_download_manager_queue_list" class="mt-4"> <div id="gommi_download_manager_queue_list" class="mt-4">
@@ -434,6 +522,12 @@
const container = document.getElementById('download_list'); const container = document.getElementById('download_list');
if (!container) return; if (!container) return;
// Save expanded card IDs before re-rendering
const expandedIds = [];
container.querySelectorAll('.dl-card.expanded').forEach(function(card) {
expandedIds.push(card.id);
});
if (!items || items.length === 0) { if (!items || items.length === 0) {
container.innerHTML = ` container.innerHTML = `
<div class="empty-state"> <div class="empty-state">
@@ -448,6 +542,12 @@
html += createDownloadCard(item); html += createDownloadCard(item);
}); });
container.innerHTML = html; container.innerHTML = html;
// Restore expanded state
expandedIds.forEach(function(cardId) {
const card = document.getElementById(cardId);
if (card) card.classList.add('expanded');
});
} }
function createDownloadCard(item) { function createDownloadCard(item) {
@@ -465,8 +565,15 @@
if (item.meta.episode) metaHtml += `<span class="badge badge-outline mr-1">${item.meta.episode}화</span>`; if (item.meta.episode) metaHtml += `<span class="badge badge-outline mr-1">${item.meta.episode}화</span>`;
} }
// Format times
const startTime = item.start_time || item.created_time || '-';
const endTime = item.end_time || item.completed_time || '-';
const filePath = item.save_path || item.filepath || '-';
const fileSize = item.file_size ? formatFileSize(item.file_size) : '-';
const downloadUrl = item.url || '-';
return ` return `
<div class="dl-card" id="card_${item.id}"> <div class="dl-card" id="card_${item.id}" onclick="toggleCardDetail(this, event)">
<div class="dl-meta"> <div class="dl-meta">
<span class="dl-id-badge">#${item.id.toString().split('_').pop()}</span> <span class="dl-id-badge">#${item.id.toString().split('_').pop()}</span>
<span class="dl-source-pill">${source.toUpperCase()}</span> <span class="dl-source-pill">${source.toUpperCase()}</span>
@@ -496,12 +603,35 @@
</div> </div>
<div class="dl-actions"> <div class="dl-actions">
${status === 'downloading' || status === 'pending' || status === 'paused' ? ${status === 'downloading' || status === 'pending' || status === 'paused' ?
`<button class="btn-action-small cancel" title="Cancel Download" onclick="cancelDownload('${item.id}')"> `<button class="btn-action-small cancel" title="Cancel Download" onclick="event.stopPropagation(); cancelDownload('${item.id}')">
<i class="fa fa-stop"></i> <i class="fa fa-stop"></i>
</button>` : '' </button>` : ''
} }
</div> </div>
</div> </div>
<div class="dl-expand-hint"><i class="fa fa-chevron-down"></i> 클릭하여 상세 정보 보기</div>
<div class="dl-detail-panel">
<div class="dl-detail-row">
<span class="dl-detail-label">시작 시간</span>
<span class="dl-detail-value">${startTime}</span>
</div>
<div class="dl-detail-row">
<span class="dl-detail-label">종료 시간</span>
<span class="dl-detail-value">${endTime}</span>
</div>
<div class="dl-detail-row">
<span class="dl-detail-label">파일 크기</span>
<span class="dl-detail-value">${fileSize}</span>
</div>
<div class="dl-detail-row">
<span class="dl-detail-label">저장 경로</span>
<span class="dl-detail-value">${filePath}</span>
</div>
<div class="dl-detail-row">
<span class="dl-detail-label">URL</span>
<span class="dl-detail-value url">${downloadUrl.length > 80 ? downloadUrl.substring(0, 80) + '...' : downloadUrl}</span>
</div>
</div>
</div> </div>
`; `;
} }
@@ -563,6 +693,21 @@
}); });
} }
// Toggle card detail panel
function toggleCardDetail(card, event) {
// Don't toggle if clicking on action buttons
if (event.target.closest('.dl-actions')) return;
card.classList.toggle('expanded');
}
// Format file size to human readable
function formatFileSize(bytes) {
if (!bytes || bytes === 0) return '-';
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
const i = Math.floor(Math.log(bytes) / Math.log(1024));
return (bytes / Math.pow(1024, i)).toFixed(2) + ' ' + sizes[i];
}
// Socket Init // Socket Init
try { try {
if (typeof io !== 'undefined') { if (typeof io !== 'undefined') {