v0.2.14: FFmpeg HLS resilience and aria2c multi-threading fixes
This commit is contained in:
@@ -3,6 +3,11 @@
|
||||
FlaskFarm용 범용 다운로드 매니저 플러그인입니다.
|
||||
여러 다운로더 플러그인(YouTube, Anime 등)의 다운로드 요청을 통합 관리하고 큐(Queue)를 제공합니다.
|
||||
|
||||
## v0.2.14 변경사항 (2026-01-07)
|
||||
- **FFmpeg HLS 안정화**: Ohli24 분산 호스트 환경 대응을 위해 `-http_persistent 0` 및 재연결 옵션(`-reconnect`) 추가.
|
||||
- **aria2c 멀티쓰레드 활성화**: `yt-dlp`에서 `aria2c`를 외부 다운로더로 정상 호출하도록 수정하여 고속 분할 다운로드 지원.
|
||||
- **GDM 위임 로직 버그 수정**: `DownloadTask` 객체의 `as_dict` 누락 및 메타데이터 초기화 버그 수정 (이전 버전 패치 포함).
|
||||
|
||||
## v0.2.12 변경사항 (2026-01-07)
|
||||
- **안정성 개선**: `ffmpeg_hls` 다운로더에서 URL이 비어있을 경우 로그 기록 시 발생하는 `TypeError` 수정.
|
||||
|
||||
|
||||
@@ -80,6 +80,15 @@ class FfmpegHlsDownloader(BaseDownloader):
|
||||
except Exception as ce:
|
||||
logger.error(f"Failed to read cookies_file: {ce}")
|
||||
|
||||
# 입력 전 설정 (Reconnection & HTTP persistence fix)
|
||||
cmd.extend([
|
||||
'-reconnect', '1',
|
||||
'-reconnect_at_eof', '1',
|
||||
'-reconnect_streamed', '1',
|
||||
'-reconnect_delay_max', '5',
|
||||
'-http_persistent', '0'
|
||||
])
|
||||
|
||||
# 입력 URL
|
||||
cmd.extend(['-i', url])
|
||||
|
||||
|
||||
@@ -62,6 +62,11 @@ class YtdlpAria2Downloader(BaseDownloader):
|
||||
aria2c_path = options.get('aria2c_path', 'aria2c')
|
||||
connections = options.get('connections', 4)
|
||||
|
||||
if self._check_aria2c(aria2c_path):
|
||||
cmd.extend(['--external-downloader', aria2c_path])
|
||||
cmd.extend(['--external-downloader-args', f'aria2c:-x{connections} -s{connections} -j{connections} -k1M'])
|
||||
logger.info(f'[GDM] Using aria2c for multi-threaded download (connections: {connections})')
|
||||
|
||||
# 속도 제한 설정
|
||||
max_rate = P.ModelSetting.get('max_download_rate')
|
||||
if max_rate == '0':
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
title: "GDM"
|
||||
package_name: gommi_downloader_manager
|
||||
version: '0.2.13'
|
||||
version: '0.2.15'
|
||||
description: FlaskFarm 범용 다운로더 큐 - YouTube, 애니24, 링크애니, Anilife 지원
|
||||
developer: projectdx
|
||||
home: https://gitea.yommi.duckdns.org/projectdx/gommi_downloader_manager
|
||||
|
||||
41
mod_queue.py
41
mod_queue.py
@@ -141,6 +141,39 @@ class ModuleQueue(PluginModuleBase):
|
||||
P.logger.error(f'DB Clear Error: {e}')
|
||||
|
||||
ret['msg'] = '목록을 초기화했습니다.'
|
||||
|
||||
elif command == 'delete':
|
||||
# 특정 항목 완전 삭제 (메모리 + DB)
|
||||
download_id = req.form.get('id', '')
|
||||
|
||||
# 메모리에서 삭제
|
||||
if download_id in self._downloads:
|
||||
self._downloads[download_id].cancel()
|
||||
del self._downloads[download_id]
|
||||
|
||||
# DB에서 삭제 (db_XXX 형태인 경우)
|
||||
if download_id.startswith('db_'):
|
||||
db_id = int(download_id.replace('db_', ''))
|
||||
try:
|
||||
from .model import ModelDownloadItem
|
||||
with F.app.app_context():
|
||||
F.db.session.query(ModelDownloadItem).filter_by(id=db_id).delete()
|
||||
F.db.session.commit()
|
||||
except Exception as e:
|
||||
self.P.logger.error(f'DB Delete Error: {e}')
|
||||
else:
|
||||
# 메모리 기반 ID에서 db_id 추출 시도
|
||||
try:
|
||||
task = self._downloads.get(download_id)
|
||||
if task and hasattr(task, 'db_id') and task.db_id:
|
||||
from .model import ModelDownloadItem
|
||||
with F.app.app_context():
|
||||
F.db.session.query(ModelDownloadItem).filter_by(id=task.db_id).delete()
|
||||
F.db.session.commit()
|
||||
except Exception as e:
|
||||
self.P.logger.error(f'DB Delete Error: {e}')
|
||||
|
||||
ret['msg'] = '항목이 삭제되었습니다.'
|
||||
|
||||
except Exception as e:
|
||||
self.P.logger.error(f'Exception:{str(e)}')
|
||||
@@ -385,9 +418,7 @@ class DownloadTask:
|
||||
self.error_message = ''
|
||||
self.filepath = os.path.join(save_path, filename) if filename else ''
|
||||
|
||||
# 메타데이터
|
||||
self.title = ''
|
||||
self.thumbnail = ''
|
||||
# 메타데이터 (이미 __init__ 상단에서 인자로 받은 title, thumbnail을 self.title, self.thumbnail에 할당함)
|
||||
self.duration = 0
|
||||
self.filesize = 0
|
||||
|
||||
@@ -670,3 +701,7 @@ class DownloadTask:
|
||||
'created_time': self.created_time,
|
||||
'file_size': self.filesize,
|
||||
}
|
||||
|
||||
def as_dict(self) -> Dict[str, Any]:
|
||||
"""데이터 직렬화 (get_status 별칭)"""
|
||||
return self.get_status()
|
||||
|
||||
@@ -192,11 +192,11 @@
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
|
||||
/* Card List Layout */
|
||||
/* Card List Layout - Single Column for clear order */
|
||||
.download-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(380px, 1fr));
|
||||
gap: 1.25rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
@media (max-width: 576px) {
|
||||
@@ -211,11 +211,11 @@
|
||||
backdrop-filter: blur(12px);
|
||||
-webkit-backdrop-filter: blur(12px);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 16px;
|
||||
padding: 1.25rem;
|
||||
border-radius: 12px;
|
||||
padding: 0.75rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
gap: 0.5rem;
|
||||
transition: all 0.3s ease;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
@@ -281,7 +281,7 @@
|
||||
|
||||
/* Progress Section */
|
||||
.dl-progress-container {
|
||||
margin-top: 0.5rem;
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
|
||||
.dl-progress-header {
|
||||
@@ -317,7 +317,7 @@
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-top: 0.5rem;
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
|
||||
.dl-status-label {
|
||||
@@ -467,12 +467,265 @@
|
||||
.dl-card.expanded .dl-expand-hint {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Selection Checkbox */
|
||||
.dl-select-checkbox {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
margin-right: 6px;
|
||||
cursor: pointer;
|
||||
accent-color: var(--accent-primary);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.dl-card.selected {
|
||||
border-color: var(--accent-primary);
|
||||
box-shadow: 0 0 0 2px var(--glow);
|
||||
}
|
||||
|
||||
/* ===== NEW FANTASTIC CARD DESIGN ===== */
|
||||
|
||||
/* Card Header */
|
||||
.dl-card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.dl-header-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.dl-source-tag {
|
||||
padding: 3px 8px;
|
||||
border-radius: 6px;
|
||||
font-size: 10px;
|
||||
font-weight: 800;
|
||||
color: white;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
text-shadow: 0 1px 2px rgba(0,0,0,0.3);
|
||||
}
|
||||
|
||||
.dl-index-badge {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
padding: 2px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
color: var(--text-main);
|
||||
font-family: 'SF Mono', 'Menlo', monospace;
|
||||
min-width: 32px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.dl-episode-tag {
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
padding: 2px 8px;
|
||||
border-radius: 6px;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
color: var(--text-main);
|
||||
font-family: 'SF Mono', 'Menlo', monospace;
|
||||
}
|
||||
|
||||
.dl-status-pill {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
padding: 3px 10px;
|
||||
border-radius: 20px;
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.3px;
|
||||
}
|
||||
|
||||
.dl-status-pill .status-dot {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.dl-status-pill.status-downloading { background: rgba(78, 159, 217, 0.2); color: var(--accent-primary); }
|
||||
.dl-status-pill.status-downloading .status-dot { background: var(--accent-primary); box-shadow: 0 0 6px var(--accent-primary); animation: pulse 1.5s infinite; }
|
||||
.dl-status-pill.status-completed { background: rgba(78, 191, 115, 0.2); color: var(--success); }
|
||||
.dl-status-pill.status-completed .status-dot { background: var(--success); }
|
||||
.dl-status-pill.status-pending { background: rgba(171, 182, 194, 0.15); color: var(--text-muted); }
|
||||
.dl-status-pill.status-pending .status-dot { background: var(--text-muted); opacity: 0.5; }
|
||||
.dl-status-pill.status-extracting { background: rgba(168, 85, 247, 0.2); color: #c084fc; }
|
||||
.dl-status-pill.status-extracting .status-dot { background: #c084fc; animation: pulse 1s infinite; }
|
||||
.dl-status-pill.status-error { background: rgba(217, 83, 79, 0.2); color: var(--danger); }
|
||||
.dl-status-pill.status-error .status-dot { background: var(--danger); }
|
||||
.dl-status-pill.status-cancelled { background: rgba(107, 114, 128, 0.2); color: #9ca3af; }
|
||||
.dl-status-pill.status-cancelled .status-dot { background: #9ca3af; }
|
||||
|
||||
/* Card Body */
|
||||
.dl-card-body {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.dl-thumb {
|
||||
width: 64px;
|
||||
height: 36px;
|
||||
object-fit: cover;
|
||||
border-radius: 6px;
|
||||
border: 1px solid var(--border);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.dl-content {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.dl-series {
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
color: var(--accent-secondary);
|
||||
margin-bottom: 2px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.dl-filename {
|
||||
font-size: 11px;
|
||||
color: var(--text-muted);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
font-family: 'SF Mono', 'Menlo', monospace;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
/* Progress Section */
|
||||
.dl-progress-section {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.dl-progress-info {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: baseline;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.dl-percent-big {
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
color: var(--text-main);
|
||||
}
|
||||
|
||||
.dl-percent-big small {
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.dl-speed-text {
|
||||
font-size: 11px;
|
||||
color: var(--accent-primary);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.dl-progress-track {
|
||||
height: 6px;
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.dl-progress-fill {
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, var(--accent-primary), var(--accent-secondary));
|
||||
border-radius: 10px;
|
||||
transition: width 0.4s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
box-shadow: 0 0 8px var(--glow);
|
||||
}
|
||||
|
||||
/* Card Footer */
|
||||
.dl-card-footer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-top: 8px;
|
||||
padding-top: 8px;
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.06);
|
||||
}
|
||||
|
||||
.dl-time-info {
|
||||
font-size: 11px;
|
||||
color: var(--text-muted);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.dl-time-info i {
|
||||
font-size: 10px;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.dl-actions {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.dl-btn {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 6px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
color: var(--text-muted);
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.dl-btn:hover:not(:disabled) {
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
color: var(--text-main);
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.dl-btn.cancel:hover:not(:disabled) {
|
||||
background: rgba(240, 173, 78, 0.2);
|
||||
color: var(--warning);
|
||||
border-color: var(--warning);
|
||||
}
|
||||
|
||||
.dl-btn.delete:hover {
|
||||
background: rgba(217, 83, 79, 0.2);
|
||||
color: var(--danger);
|
||||
border-color: var(--danger);
|
||||
}
|
||||
|
||||
.dl-btn:disabled {
|
||||
opacity: 0.25;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div id="gommi_download_manager_queue_list" class="mt-4">
|
||||
<div class="page-header">
|
||||
<h1 class="page-title">GDM Queue</h1>
|
||||
<div class="header-actions">
|
||||
<button type="button" class="btn-premium" onclick="deleteSelected()" id="delete_selected_btn" style="display: none;">
|
||||
<i class="fa fa-trash-o"></i> 선택삭제 (<span id="selected_count">0</span>)
|
||||
</button>
|
||||
<button type="button" class="btn-premium danger" onclick="resetList()">
|
||||
<i class="fa fa-trash"></i> Reset All
|
||||
</button>
|
||||
@@ -528,6 +781,12 @@
|
||||
expandedIds.push(card.id);
|
||||
});
|
||||
|
||||
// Save checked checkbox IDs before re-rendering
|
||||
const checkedIds = [];
|
||||
container.querySelectorAll('.dl-select-checkbox:checked').forEach(function(cb) {
|
||||
checkedIds.push(cb.dataset.id);
|
||||
});
|
||||
|
||||
if (!items || items.length === 0) {
|
||||
container.innerHTML = `
|
||||
<div class="empty-state">
|
||||
@@ -548,6 +807,19 @@
|
||||
const card = document.getElementById(cardId);
|
||||
if (card) card.classList.add('expanded');
|
||||
});
|
||||
|
||||
// Restore checkbox state
|
||||
checkedIds.forEach(function(id) {
|
||||
const cb = container.querySelector('.dl-select-checkbox[data-id="' + id + '"]');
|
||||
if (cb) {
|
||||
cb.checked = true;
|
||||
const card = cb.closest('.dl-card');
|
||||
if (card) card.classList.add('selected');
|
||||
}
|
||||
});
|
||||
|
||||
// Update selected count
|
||||
updateSelectedCount();
|
||||
}
|
||||
|
||||
function createDownloadCard(item) {
|
||||
@@ -558,12 +830,12 @@
|
||||
const thumbnail = item.thumbnail || '';
|
||||
|
||||
let statusClass = `status-${status}`;
|
||||
let metaHtml = '';
|
||||
if (item.meta) {
|
||||
if (item.meta.series) metaHtml += `<span class="badge badge-outline mr-1">${item.meta.series}</span>`;
|
||||
if (item.meta.season) metaHtml += `<span class="badge badge-outline mr-1">${item.meta.season}기</span>`;
|
||||
if (item.meta.episode) metaHtml += `<span class="badge badge-outline mr-1">${item.meta.episode}화</span>`;
|
||||
}
|
||||
|
||||
// Build meta info from meta object
|
||||
const series = item.meta?.series || '';
|
||||
const season = item.meta?.season ? `S${String(item.meta.season).padStart(2, '0')}` : '';
|
||||
const episode = item.meta?.episode ? `E${String(item.meta.episode).padStart(2, '0')}` : '';
|
||||
const episodeTag = (season || episode) ? `${season}${episode}` : '';
|
||||
|
||||
// Format times
|
||||
const startTime = item.start_time || item.created_time || '-';
|
||||
@@ -572,65 +844,70 @@
|
||||
const fileSize = item.file_size ? formatFileSize(item.file_size) : '-';
|
||||
const downloadUrl = item.url || '-';
|
||||
|
||||
// Source badge color based on type
|
||||
const sourceColors = {
|
||||
'ani24': 'background: linear-gradient(135deg, #f59e0b, #d97706);',
|
||||
'ohli24': 'background: linear-gradient(135deg, #f59e0b, #d97706);',
|
||||
'anilife': 'background: linear-gradient(135deg, #ec4899, #be185d);',
|
||||
'youtube': 'background: linear-gradient(135deg, #ef4444, #b91c1c);',
|
||||
'hls': 'background: linear-gradient(135deg, #10b981, #059669);',
|
||||
'auto': 'background: linear-gradient(135deg, #6366f1, #4f46e5);',
|
||||
};
|
||||
const sourceStyle = sourceColors[source.toLowerCase()] || sourceColors['auto'];
|
||||
|
||||
return `
|
||||
<div class="dl-card" id="card_${item.id}" onclick="toggleCardDetail(this, event)">
|
||||
<div class="dl-meta">
|
||||
<span class="dl-id-badge">#${item.id.toString().split('_').pop()}</span>
|
||||
<span class="dl-source-pill">${source.toUpperCase()}</span>
|
||||
</div>
|
||||
<div style="display: flex; gap: 1rem; align-items: flex-start;">
|
||||
${thumbnail ? `<img src="${thumbnail}" style="width: 80px; height: 45px; object-fit: cover; border-radius: 8px; border: 1px solid var(--border);" onerror="this.style.display='none'">` : ''}
|
||||
<div class="dl-info" style="flex: 1; min-width: 0;">
|
||||
<div class="dl-title" title="${displayTitle}">${displayTitle}</div>
|
||||
<div class="mt-2" style="font-size: 0.7rem; display: flex; flex-wrap: wrap; gap: 4px;">
|
||||
${metaHtml}
|
||||
</div>
|
||||
<div class="dl-card-header">
|
||||
<div class="dl-header-left">
|
||||
<input type="checkbox" class="dl-select-checkbox" data-id="${item.id}" onclick="event.stopPropagation(); updateSelectedCount();">
|
||||
<span class="dl-index-badge">${item.id.toString().split('_').pop()}</span>
|
||||
<span class="dl-source-tag" style="${sourceStyle}">${source.toUpperCase()}</span>
|
||||
${episodeTag ? `<span class="dl-episode-tag">${episodeTag}</span>` : ''}
|
||||
</div>
|
||||
<div class="dl-status-pill ${statusClass}">
|
||||
<span class="status-dot"></span>
|
||||
${status.charAt(0).toUpperCase() + status.slice(1)}
|
||||
</div>
|
||||
</div>
|
||||
<div class="dl-progress-container">
|
||||
<div class="dl-progress-header">
|
||||
<span class="dl-percent">${percent}%</span>
|
||||
<span class="dl-speed">${item.speed || ''}</span>
|
||||
</div>
|
||||
<div class="dl-progress-bar-bg">
|
||||
<div class="dl-progress-bar-fill" style="width: ${percent}%;"></div>
|
||||
|
||||
<div class="dl-card-body">
|
||||
${thumbnail ? `<img src="${thumbnail}" class="dl-thumb" onerror="this.style.display='none'">` : ''}
|
||||
<div class="dl-content">
|
||||
${series ? `<div class="dl-series">${series}</div>` : ''}
|
||||
<div class="dl-filename" title="${displayTitle}">${displayTitle}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="dl-footer">
|
||||
<div class="dl-status-label ${statusClass}">
|
||||
<div class="status-dot"></div>
|
||||
<span>${status.charAt(0).toUpperCase() + status.slice(1)}</span>
|
||||
|
||||
<div class="dl-progress-section">
|
||||
<div class="dl-progress-info">
|
||||
<span class="dl-percent-big">${percent}<small>%</small></span>
|
||||
<span class="dl-speed-text">${item.speed || ''}</span>
|
||||
</div>
|
||||
<div class="dl-progress-track">
|
||||
<div class="dl-progress-fill" style="width: ${percent}%;"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="dl-card-footer">
|
||||
<div class="dl-time-info">
|
||||
<i class="fa fa-clock-o"></i> ${startTime !== '-' ? startTime.split(' ')[1] || startTime : '-'}
|
||||
</div>
|
||||
<div class="dl-actions">
|
||||
${status === 'downloading' || status === 'pending' || status === 'paused' ?
|
||||
`<button class="btn-action-small cancel" title="Cancel Download" onclick="event.stopPropagation(); cancelDownload('${item.id}')">
|
||||
<i class="fa fa-stop"></i>
|
||||
</button>` : ''
|
||||
}
|
||||
<button class="dl-btn cancel" title="취소" onclick="event.stopPropagation(); cancelDownload('${item.id}')" ${status === 'downloading' || status === 'pending' || status === 'paused' || status === 'extracting' ? '' : 'disabled'}>
|
||||
<i class="fa fa-stop"></i>
|
||||
</button>
|
||||
<button class="dl-btn delete" title="삭제" onclick="event.stopPropagation(); deleteDownload('${item.id}')">
|
||||
<i class="fa fa-trash-o"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="dl-expand-hint"><i class="fa fa-chevron-down"></i> 클릭하여 상세 정보 보기</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 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>
|
||||
</div>
|
||||
`;
|
||||
@@ -693,6 +970,69 @@
|
||||
});
|
||||
}
|
||||
|
||||
function deleteDownload(id) {
|
||||
$.ajax({
|
||||
url: '/{{ arg["package_name"] }}/ajax/{{ arg["module_name"] }}/delete',
|
||||
type: 'POST',
|
||||
data: { id: id },
|
||||
dataType: 'json',
|
||||
success: function(ret) {
|
||||
$.notify('<strong>Item Deleted</strong>', {type: 'success'});
|
||||
refreshList(false);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function updateSelectedCount() {
|
||||
const checked = document.querySelectorAll('.dl-select-checkbox:checked');
|
||||
const btn = document.getElementById('delete_selected_btn');
|
||||
const countSpan = document.getElementById('selected_count');
|
||||
if (checked.length > 0) {
|
||||
btn.style.display = 'inline-flex';
|
||||
countSpan.textContent = checked.length;
|
||||
} else {
|
||||
btn.style.display = 'none';
|
||||
}
|
||||
// Toggle selected class on cards
|
||||
document.querySelectorAll('.dl-card').forEach(function(card) {
|
||||
const checkbox = card.querySelector('.dl-select-checkbox');
|
||||
if (checkbox && checkbox.checked) {
|
||||
card.classList.add('selected');
|
||||
} else {
|
||||
card.classList.remove('selected');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function deleteSelected() {
|
||||
const checked = document.querySelectorAll('.dl-select-checkbox:checked');
|
||||
if (checked.length === 0) return;
|
||||
if (!confirm(checked.length + '개 항목을 삭제하시겠습니까?')) return;
|
||||
|
||||
const count = checked.length;
|
||||
let pending = count;
|
||||
|
||||
// Clear checkboxes immediately so they don't get restored
|
||||
checked.forEach(function(cb) { cb.checked = false; });
|
||||
document.getElementById('delete_selected_btn').style.display = 'none';
|
||||
|
||||
checked.forEach(function(cb) {
|
||||
$.ajax({
|
||||
url: '/{{ arg["package_name"] }}/ajax/{{ arg["module_name"] }}/delete',
|
||||
type: 'POST',
|
||||
data: { id: cb.dataset.id },
|
||||
dataType: 'json',
|
||||
complete: function() {
|
||||
pending--;
|
||||
if (pending === 0) {
|
||||
$.notify('<strong>' + count + '개 항목 삭제됨</strong>', {type: 'success'});
|
||||
refreshList(false); // Non-silent refresh to ensure complete UI update
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Toggle card detail panel
|
||||
function toggleCardDetail(card, event) {
|
||||
// Don't toggle if clicking on action buttons
|
||||
|
||||
Reference in New Issue
Block a user