Compare commits
5 Commits
f4b99f7d67
...
d0dfef1445
| Author | SHA1 | Date | |
|---|---|---|---|
| d0dfef1445 | |||
| 68d12372ad | |||
| 890ed46e1c | |||
| bbdafb4ce0 | |||
| 77b37e8675 |
@@ -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에 업데이트합니다.
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
26
mod_queue.py
26
mod_queue.py
@@ -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,
|
||||||
}
|
}
|
||||||
|
|||||||
4
model.py
4
model.py
@@ -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
|
||||||
|
|||||||
@@ -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') {
|
||||||
|
|||||||
Reference in New Issue
Block a user