Files
yuming cf40343c51
部署到群晖 / deploy (push) Failing after 10m45s
初始化 telegram-downloader 并接入群晖 CI/CD
2026-04-22 21:29:03 +08:00

1324 lines
49 KiB
HTML

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Telegram 下载控制台</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%);
min-height: 100vh;
color: #e8e8e8;
padding: 20px;
}
.container {
max-width: 900px;
margin: 0 auto;
}
.header {
text-align: center;
margin-bottom: 30px;
padding: 20px;
}
.header h1 {
font-size: 2rem;
background: linear-gradient(90deg, #00d9ff, #00ff88);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
margin-bottom: 8px;
}
.header p {
color: #888;
font-size: 0.9rem;
}
.card {
background: rgba(255, 255, 255, 0.05);
border-radius: 16px;
padding: 24px;
margin-bottom: 20px;
border: 1px solid rgba(255, 255, 255, 0.1);
backdrop-filter: blur(10px);
}
.card-title {
font-size: 1.1rem;
font-weight: 600;
margin-bottom: 20px;
color: #00d9ff;
display: flex;
align-items: center;
gap: 8px;
}
.card-title::before {
content: '';
width: 4px;
height: 20px;
background: linear-gradient(180deg, #00d9ff, #00ff88);
border-radius: 2px;
}
.form-group {
margin-bottom: 20px;
}
.form-group label {
display: block;
margin-bottom: 8px;
color: #aaa;
font-size: 0.9rem;
}
.form-group input, .form-group select {
width: 100%;
padding: 12px 16px;
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 8px;
background: rgba(0, 0, 0, 0.3);
color: #fff;
font-size: 1rem;
transition: all 0.3s ease;
}
.form-group input:focus, .form-group select:focus {
outline: none;
border-color: #00d9ff;
box-shadow: 0 0 0 3px rgba(0, 217, 255, 0.1);
}
.form-group input::placeholder {
color: #666;
}
.date-row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px;
}
.btn {
padding: 12px 24px;
border: none;
border-radius: 8px;
font-size: 1rem;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 8px;
}
.btn-primary {
background: linear-gradient(90deg, #00d9ff, #00ff88);
color: #1a1a2e;
}
.btn-primary:hover {
transform: translateY(-2px);
box-shadow: 0 8px 20px rgba(0, 217, 255, 0.3);
}
.btn-primary:disabled {
opacity: 0.5;
cursor: not-allowed;
transform: none;
}
.btn-secondary {
background: rgba(255, 255, 255, 0.1);
color: #fff;
border: 1px solid rgba(255, 255, 255, 0.2);
}
.btn-secondary:hover {
background: rgba(255, 255, 255, 0.15);
}
.btn-warning {
background: linear-gradient(90deg, #ff9500, #ff5e3a);
color: #fff;
}
.btn-warning:hover {
transform: translateY(-2px);
box-shadow: 0 8px 20px rgba(255, 149, 0, 0.3);
}
.btn-success {
background: linear-gradient(90deg, #00ff88, #00d9ff);
color: #1a1a2e;
}
.btn-success:hover {
transform: translateY(-2px);
box-shadow: 0 8px 20px rgba(0, 255, 136, 0.3);
}
.btn-large {
padding: 16px 32px;
font-size: 1.1rem;
width: 100%;
}
.btn-group {
display: flex;
gap: 12px;
margin-top: 20px;
}
.status-bar {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px;
background: rgba(0, 0, 0, 0.3);
border-radius: 8px;
margin-bottom: 20px;
}
.status-item {
text-align: center;
}
.status-value {
font-size: 1.5rem;
font-weight: 700;
color: #00ff88;
}
.status-label {
font-size: 0.8rem;
color: #888;
margin-top: 4px;
}
.download-list {
max-height: 400px;
overflow-y: auto;
}
.download-item {
display: flex;
align-items: center;
padding: 12px;
background: rgba(0, 0, 0, 0.2);
border-radius: 8px;
margin-bottom: 8px;
}
.download-item:last-child {
margin-bottom: 0;
}
.download-info {
flex: 1;
min-width: 0;
}
.download-name {
font-size: 0.9rem;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
margin-bottom: 8px;
}
.progress-bar {
height: 6px;
background: rgba(255, 255, 255, 0.1);
border-radius: 3px;
overflow: hidden;
}
.progress-fill {
height: 100%;
background: linear-gradient(90deg, #00d9ff, #00ff88);
border-radius: 3px;
transition: width 0.3s ease;
}
.download-stats {
margin-left: 16px;
text-align: right;
min-width: 100px;
}
.download-percent {
font-size: 1rem;
font-weight: 600;
color: #00d9ff;
}
.download-speed {
font-size: 0.8rem;
color: #888;
}
.empty-state {
text-align: center;
padding: 40px;
color: #666;
}
.empty-state svg {
width: 64px;
height: 64px;
margin-bottom: 16px;
opacity: 0.5;
}
.current-config {
background: rgba(0, 217, 255, 0.1);
border: 1px solid rgba(0, 217, 255, 0.2);
border-radius: 8px;
padding: 16px;
margin-bottom: 20px;
}
.config-item {
display: flex;
justify-content: space-between;
padding: 8px 0;
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
}
.config-item:last-child {
border-bottom: none;
}
.config-key {
color: #888;
}
.config-value {
color: #00ff88;
font-family: 'Monaco', 'Menlo', monospace;
}
.alert {
padding: 12px 16px;
border-radius: 8px;
margin-bottom: 16px;
display: none;
}
.alert-success {
background: rgba(0, 255, 136, 0.1);
border: 1px solid rgba(0, 255, 136, 0.3);
color: #00ff88;
}
.alert-error {
background: rgba(255, 100, 100, 0.1);
border: 1px solid rgba(255, 100, 100, 0.3);
color: #ff6464;
}
.hint {
font-size: 0.8rem;
color: #666;
margin-top: 6px;
}
.input-with-btn {
display: flex;
gap: 12px;
}
.input-with-btn input {
flex: 1;
}
.input-with-btn .btn {
white-space: nowrap;
}
.validation-result {
margin-top: 12px;
padding: 12px 16px;
border-radius: 8px;
display: none;
}
.validation-success {
background: rgba(0, 255, 136, 0.1);
border: 1px solid rgba(0, 255, 136, 0.3);
}
.validation-error {
background: rgba(255, 100, 100, 0.1);
border: 1px solid rgba(255, 100, 100, 0.3);
}
.channel-info {
display: flex;
align-items: center;
gap: 12px;
}
.channel-icon {
width: 48px;
height: 48px;
background: linear-gradient(135deg, #00d9ff, #00ff88);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.5rem;
}
.channel-details h3 {
font-size: 1.1rem;
color: #fff;
margin-bottom: 4px;
}
.channel-details p {
font-size: 0.85rem;
color: #00ff88;
}
.spinner {
display: inline-block;
width: 16px;
height: 16px;
border: 2px solid rgba(255,255,255,0.3);
border-radius: 50%;
border-top-color: #00d9ff;
animation: spin 1s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
/* History List Styles */
.history-list {
max-height: 300px;
overflow-y: auto;
}
.history-item {
display: flex;
align-items: center;
padding: 12px;
background: rgba(0, 0, 0, 0.2);
border-radius: 8px;
margin-bottom: 8px;
cursor: pointer;
transition: all 0.2s ease;
border: 1px solid transparent;
}
.history-item:hover {
background: rgba(0, 217, 255, 0.1);
border-color: rgba(0, 217, 255, 0.3);
}
.history-item:last-child {
margin-bottom: 0;
}
.history-icon {
width: 40px;
height: 40px;
background: linear-gradient(135deg, #00d9ff33, #00ff8833);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.2rem;
margin-right: 12px;
flex-shrink: 0;
}
.history-info {
flex: 1;
min-width: 0;
}
.history-title {
font-size: 0.95rem;
color: #fff;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
margin-bottom: 4px;
}
.history-meta {
font-size: 0.8rem;
color: #888;
display: flex;
gap: 12px;
}
.history-meta span {
display: flex;
align-items: center;
gap: 4px;
}
.history-delete {
width: 32px;
height: 32px;
border: none;
background: rgba(255, 100, 100, 0.1);
border-radius: 6px;
color: #ff6464;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
opacity: 0;
transition: all 0.2s ease;
flex-shrink: 0;
margin-left: 8px;
}
.history-item:hover .history-delete {
opacity: 1;
}
.history-delete:hover {
background: rgba(255, 100, 100, 0.2);
}
.history-empty {
text-align: center;
padding: 30px;
color: #666;
}
.history-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
}
.history-header .card-title {
margin-bottom: 0;
}
.btn-text {
background: none;
border: none;
color: #ff6464;
cursor: pointer;
font-size: 0.85rem;
padding: 4px 8px;
border-radius: 4px;
transition: all 0.2s ease;
}
.btn-text:hover {
background: rgba(255, 100, 100, 0.1);
}
@media (max-width: 600px) {
.date-row {
grid-template-columns: 1fr;
}
.status-bar {
flex-wrap: wrap;
gap: 16px;
}
.status-item {
flex: 1;
min-width: 80px;
}
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>📥 Telegram 下载控制台</h1>
<p>配置频道和时间范围,一键下载媒体文件</p>
</div>
<div class="card">
<div class="card-title">当前配置</div>
<div id="alert-box" class="alert"></div>
<div class="current-config" id="current-config">
<div class="config-item">
<span class="config-key">频道</span>
<span class="config-value" id="cfg-channel">加载中...</span>
</div>
<div class="config-item">
<span class="config-key">时间过滤</span>
<span class="config-value" id="cfg-filter">加载中...</span>
</div>
<div class="config-item">
<span class="config-key">保存路径</span>
<span class="config-value" id="cfg-path">加载中...</span>
</div>
</div>
</div>
<!-- 历史频道 -->
<div class="card" id="history-card">
<div class="history-header">
<div class="card-title">📜 历史频道</div>
<button type="button" class="btn-text" id="clear-history-btn" onclick="clearHistory()" style="display: none;">
🗑️ 清空历史
</button>
</div>
<div class="history-list" id="history-list">
<div class="history-empty">
<p>暂无历史记录</p>
<p style="font-size: 0.8rem; margin-top: 8px;">配置过的频道会显示在这里</p>
</div>
</div>
</div>
<div class="card">
<div class="card-title">新建下载任务</div>
<form id="download-form">
<div class="form-group">
<label>频道链接或用户名</label>
<div class="input-with-btn">
<input type="text" id="channel-input" placeholder="例如: https://t.me/happycat03 或 happycat03" required>
<button type="button" class="btn btn-secondary" id="validate-btn" onclick="validateChannel()">
🔍 验证频道
</button>
</div>
<p class="hint">支持 t.me 链接、@用户名 或直接输入用户名</p>
</div>
<!-- 频道验证结果 -->
<div id="validation-result" class="validation-result">
<div class="channel-info">
<div class="channel-icon" id="channel-icon">📺</div>
<div class="channel-details">
<h3 id="channel-title">频道名称</h3>
<p id="channel-type">类型: 频道</p>
</div>
</div>
</div>
<div class="date-row">
<div class="form-group">
<label>开始日期</label>
<input type="date" id="start-date" required>
</div>
<div class="form-group">
<label>结束日期(可选)</label>
<input type="date" id="end-date">
<p class="hint">留空表示下载到最新</p>
</div>
</div>
<div class="btn-group" style="flex-wrap: wrap;">
<button type="button" class="btn btn-primary" id="restart-btn" onclick="saveAndRestart()">
🔄 保存并重启下载
</button>
<button type="button" class="btn btn-secondary" onclick="location.href='/'">
📊 查看下载进度
</button>
</div>
<p class="hint" style="margin-top: 12px; text-align: center;">
💡 建议先点击"验证频道"确认频道有效后,再保存配置
</p>
</form>
</div>
<div class="card">
<div class="card-title">下载控制</div>
<div style="margin-bottom: 20px;">
<button type="button" class="btn btn-large btn-warning" id="pause-btn" onclick="toggleDownload()">
⏸️ 暂停下载
</button>
</div>
<div id="pause-status" style="text-align: center; margin-bottom: 16px; color: #888; font-size: 0.9rem;">
状态: <span id="current-state" style="color: #00ff88;">下载中</span>
</div>
</div>
<div class="card">
<div class="card-title">📊 任务进度</div>
<!-- 当前任务信息 -->
<div id="task-info" class="current-config" style="display: none;">
<div class="config-item">
<span class="config-key">当前频道</span>
<span class="config-value" id="task-chat">-</span>
</div>
<div class="config-item">
<span class="config-key">任务状态</span>
<span class="config-value" id="task-status">-</span>
</div>
</div>
<div class="status-bar">
<div class="status-item">
<div class="status-value" id="download-speed">0 B/s</div>
<div class="status-label">下载速度</div>
</div>
<div class="status-item">
<div class="status-value" id="downloading-count">0</div>
<div class="status-label">下载中</div>
</div>
<div class="status-item">
<div class="status-value" id="completed-count">0</div>
<div class="status-label">已完成</div>
</div>
<div class="status-item">
<div class="status-value" id="skipped-count" style="color: #ff9500;">0</div>
<div class="status-label">已跳过</div>
</div>
</div>
<div class="download-list" id="download-list">
<div class="empty-state" id="empty-state">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
<polyline points="7 10 12 15 17 10"></polyline>
<line x1="12" y1="15" x2="12" y2="3"></line>
</svg>
<p id="empty-state-text">暂无下载任务</p>
</div>
</div>
</div>
<div style="text-align: center; padding: 20px; color: #666; font-size: 0.8rem;">
Telegram Media Downloader v<span id="version">2.2.5</span>
<br>
<span style="color: #00ff88;">✅ 点击"保存并重启下载"将自动保存配置并重启程序开始下载</span>
</div>
</div>
<script>
// 存储验证后的频道信息
let validatedChannel = null;
// 解析频道链接
function parseChannelId(input) {
input = input.trim();
// https://t.me/xxx 格式
const tmeMatch = input.match(/t\.me\/([a-zA-Z0-9_]+)/);
if (tmeMatch) return tmeMatch[1];
// @xxx 格式
if (input.startsWith('@')) return input.substring(1);
// 直接返回
return input;
}
// 加载当前配置
function loadConfig() {
fetch('/api/get_config')
.then(r => r.json())
.then(data => {
document.getElementById('cfg-channel').textContent = data.chat_id || '未配置';
document.getElementById('cfg-filter').textContent = data.download_filter || '无';
document.getElementById('cfg-path').textContent = data.save_path || '默认路径';
})
.catch(() => {
document.getElementById('cfg-channel').textContent = '获取失败';
});
}
// 类型图标映射
const typeIconMap = {
'CHANNEL': '📺',
'SUPERGROUP': '👥',
'GROUP': '👥',
'PRIVATE': '👤',
'BOT': '🤖'
};
// 格式化时间
function formatTime(isoString) {
if (!isoString) return '';
const date = new Date(isoString);
const now = new Date();
const diff = now - date;
if (diff < 60000) return '刚刚';
if (diff < 3600000) return Math.floor(diff / 60000) + '分钟前';
if (diff < 86400000) return Math.floor(diff / 3600000) + '小时前';
if (diff < 604800000) return Math.floor(diff / 86400000) + '天前';
return date.toLocaleDateString('zh-CN');
}
// 加载历史频道
function loadChannelHistory() {
fetch('/api/channel_history')
.then(r => r.json())
.then(data => {
const listEl = document.getElementById('history-list');
const clearBtn = document.getElementById('clear-history-btn');
if (!data.success || !data.history || data.history.length === 0) {
listEl.innerHTML = `
<div class="history-empty">
<p>暂无历史记录</p>
<p style="font-size: 0.8rem; margin-top: 8px;">配置过的频道会显示在这里</p>
</div>
`;
clearBtn.style.display = 'none';
return;
}
clearBtn.style.display = 'block';
listEl.innerHTML = data.history.map(item => `
<div class="history-item" onclick="selectHistory('${item.chat_id}', '${(item.chat_title || '').replace(/'/g, "\\'")}', '${item.chat_type || ''}')">
<div class="history-icon">${typeIconMap[item.chat_type] || '📁'}</div>
<div class="history-info">
<div class="history-title">${item.chat_title || item.chat_id}</div>
<div class="history-meta">
<span>@${item.chat_id}</span>
<span>🕐 ${formatTime(item.last_used)}</span>
${item.use_count > 1 ? `<span>📊 使用 ${item.use_count} 次</span>` : ''}
</div>
</div>
<button class="history-delete" onclick="event.stopPropagation(); deleteHistory('${item.chat_id}')" title="删除">
🗑️
</button>
</div>
`).join('');
})
.catch(err => {
console.error('Failed to load history:', err);
});
}
// 选择历史频道
function selectHistory(chatId, chatTitle, chatType) {
document.getElementById('channel-input').value = chatId;
// 设置已验证状态
validatedChannel = {
valid: true,
chat_id: chatId,
chat_title: chatTitle || chatId,
chat_type: chatType
};
// 显示验证结果
showValidationResult(validatedChannel);
showAlert(`已选择: ${chatTitle || chatId}`, 'success');
// 滚动到表单
document.getElementById('download-form').scrollIntoView({ behavior: 'smooth', block: 'start' });
}
// 删除历史记录
function deleteHistory(chatId) {
if (!confirm('确定要删除这条历史记录吗?')) return;
fetch(`/api/channel_history/${encodeURIComponent(chatId)}`, {
method: 'DELETE'
})
.then(r => r.json())
.then(data => {
if (data.success) {
loadChannelHistory();
showAlert('已删除', 'success');
} else {
showAlert('删除失败: ' + (data.error || '未知错误'), 'error');
}
})
.catch(err => {
showAlert('删除失败: ' + err.message, 'error');
});
}
// 清空历史记录
function clearHistory() {
if (!confirm('确定要清空所有历史记录吗?此操作不可恢复。')) return;
fetch('/api/channel_history/clear', {
method: 'POST'
})
.then(r => r.json())
.then(data => {
if (data.success) {
loadChannelHistory();
showAlert('历史记录已清空', 'success');
} else {
showAlert('清空失败: ' + (data.error || '未知错误'), 'error');
}
})
.catch(err => {
showAlert('清空失败: ' + err.message, 'error');
});
}
// 显示提示
function showAlert(message, type) {
const alertBox = document.getElementById('alert-box');
alertBox.textContent = message;
alertBox.className = 'alert alert-' + type;
alertBox.style.display = 'block';
setTimeout(() => {
alertBox.style.display = 'none';
}, 5000);
}
// 显示验证结果
function showValidationResult(result) {
const resultEl = document.getElementById('validation-result');
const titleEl = document.getElementById('channel-title');
const typeEl = document.getElementById('channel-type');
const iconEl = document.getElementById('channel-icon');
if (result.valid) {
validatedChannel = result;
resultEl.className = 'validation-result validation-success';
titleEl.textContent = result.chat_title;
// 根据类型显示不同图标
const typeMap = {
'CHANNEL': { icon: '📺', name: '频道' },
'SUPERGROUP': { icon: '👥', name: '超级群组' },
'GROUP': { icon: '👥', name: '群组' },
'PRIVATE': { icon: '👤', name: '私聊' },
'BOT': { icon: '🤖', name: '机器人' }
};
const typeInfo = typeMap[result.chat_type] || { icon: '📁', name: result.chat_type };
iconEl.textContent = typeInfo.icon;
typeEl.textContent = `类型: ${typeInfo.name}`;
typeEl.style.color = '#00ff88';
} else {
validatedChannel = null;
resultEl.className = 'validation-result validation-error';
titleEl.textContent = '验证失败';
iconEl.textContent = '❌';
typeEl.textContent = result.error;
typeEl.style.color = '#ff6464';
}
resultEl.style.display = 'block';
}
// 验证频道
function validateChannel() {
const channelInput = document.getElementById('channel-input').value;
const channelId = parseChannelId(channelInput);
if (!channelId) {
showAlert('请输入频道链接或用户名', 'error');
return;
}
const btn = document.getElementById('validate-btn');
btn.disabled = true;
btn.innerHTML = '<span class="spinner"></span> 验证中...';
// 隐藏之前的结果
document.getElementById('validation-result').style.display = 'none';
validatedChannel = null;
fetch('/api/validate_chat', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ chat_id: channelId })
})
.then(r => r.json())
.then(data => {
showValidationResult(data);
if (data.valid) {
showAlert(`✅ 已验证: ${data.chat_title}`, 'success');
}
})
.catch(err => {
showValidationResult({ valid: false, error: err.message });
showAlert('验证失败: ' + err.message, 'error');
})
.finally(() => {
btn.disabled = false;
btn.textContent = '🔍 验证频道';
});
}
// 当输入框内容变化时,清除验证状态
document.addEventListener('DOMContentLoaded', function() {
document.getElementById('channel-input').addEventListener('input', function() {
validatedChannel = null;
document.getElementById('validation-result').style.display = 'none';
});
});
// 保存并重启
function saveAndRestart() {
const channelInput = document.getElementById('channel-input').value;
const startDate = document.getElementById('start-date').value;
const endDate = document.getElementById('end-date').value;
const channelId = parseChannelId(channelInput);
if (!channelId) {
showAlert('请输入有效的频道链接或用户名', 'error');
return;
}
if (!startDate) {
showAlert('请选择开始日期', 'error');
return;
}
// 如果还没验证,先进行验证
if (!validatedChannel) {
const btn = document.getElementById('restart-btn');
btn.disabled = true;
btn.innerHTML = '<span class="spinner"></span> 验证频道中...';
fetch('/api/validate_chat', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ chat_id: channelId })
})
.then(r => r.json())
.then(data => {
if (data.valid) {
showValidationResult(data);
// 验证成功,继续保存
doSaveAndRestart(channelId, startDate, endDate, data.chat_title, data.chat_type);
} else {
showValidationResult(data);
showAlert('频道验证失败: ' + data.error, 'error');
btn.disabled = false;
btn.textContent = '🔄 保存并重启下载';
}
})
.catch(err => {
showAlert('验证失败: ' + err.message, 'error');
btn.disabled = false;
btn.textContent = '🔄 保存并重启下载';
});
} else {
// 已验证,直接保存
doSaveAndRestart(channelId, startDate, endDate, validatedChannel.chat_title, validatedChannel.chat_type);
}
}
// 执行保存并重启
function doSaveAndRestart(channelId, startDate, endDate, chatTitle, chatType) {
const btn = document.getElementById('restart-btn');
btn.disabled = true;
btn.innerHTML = '<span class="spinner"></span> 正在重启...';
fetch('/api/save_and_restart', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
chat_id: channelId,
start_date: startDate,
end_date: endDate,
chat_title: chatTitle,
chat_type: chatType || ''
})
})
.then(r => r.json())
.then(data => {
if (data.success) {
showAlert('✅ ' + data.message + ' 页面将在5秒后刷新...', 'success');
// 等待程序重启后刷新页面
setTimeout(() => {
location.reload();
}, 5000);
} else {
showAlert('操作失败: ' + (data.error || '未知错误'), 'error');
btn.disabled = false;
btn.textContent = '🔄 保存并重启下载';
}
})
.catch(err => {
showAlert('操作失败: ' + err.message, 'error');
btn.disabled = false;
btn.textContent = '🔄 保存并重启下载';
});
}
// 表单提交时使用保存并重启
document.getElementById('download-form').addEventListener('submit', function(e) {
e.preventDefault();
saveAndRestart();
});
// 更新下载状态
function updateDownloadStatus() {
fetch('/get_download_status')
.then(r => r.json())
.then(data => {
// 后端已处理:暂停时返回0,直接显示即可
document.getElementById('download-speed').textContent = data.download_speed || '0 B/s';
// 保存当前暂停状态供任务进度使用
window.currentDownloadState = data.state; // 'pause' = 下载中, 'continue' = 已暂停
})
.catch(() => {});
// 获取任务进度
fetch('/api/task_progress')
.then(r => r.json())
.then(progress => {
const taskInfo = document.getElementById('task-info');
const taskChat = document.getElementById('task-chat');
const taskStatus = document.getElementById('task-status');
const skippedCount = document.getElementById('skipped-count');
const emptyStateText = document.getElementById('empty-state-text');
// 更新跳过计数
skippedCount.textContent = progress.skipped_files || 0;
// 检查是否暂停
const isPaused = window.currentDownloadState === 'continue';
// 如果有当前任务
if (progress.current_chat) {
taskInfo.style.display = 'block';
taskChat.textContent = progress.current_chat_title || progress.current_chat;
const downloading = progress.downloading_files || 0;
const completed = progress.completed_files || 0;
const skipped = progress.skipped_files || 0;
// 根据检查状态、下载状态和暂停状态综合判断
if (progress.is_checking && isPaused) {
// 检查中 + 用户暂停了下载
taskStatus.textContent = `⏸️🔍 已暂停下载,正在检查文件... (已跳过${skipped}个)`;
taskStatus.style.color = '#ff9500';
if (emptyStateText) {
emptyStateText.textContent = `下载已暂停,文件检查继续中...`;
}
} else if (progress.is_checking && downloading > 0) {
// 边检查边下载
taskStatus.textContent = `🔍🚀 正在检查文件并下载 (${downloading}个下载中, 已跳过${skipped}个)`;
taskStatus.style.color = '#00d9ff';
if (emptyStateText) {
emptyStateText.textContent = `正在检查文件并下载...`;
}
} else if (progress.is_checking) {
// 仅检查中
taskStatus.textContent = `🔍 正在检查文件... (已跳过 ${skipped} 个)`;
taskStatus.style.color = '#00d9ff';
if (emptyStateText) {
emptyStateText.textContent = `正在检查文件,已跳过 ${skipped} 个已下载文件...`;
}
} else if (isPaused) {
// 检查完成,用户点击了暂停
taskStatus.textContent = `⏸️ 已暂停下载 (已完成${completed}个, 跳过${skipped}个)`;
taskStatus.style.color = '#ff9500';
if (emptyStateText) {
emptyStateText.textContent = '检查完成,下载已暂停';
}
} else if (downloading > 0) {
// 检查完成,正在下载
taskStatus.textContent = `🚀 下载中 (${downloading}个下载中, 已完成${completed}个, 跳过${skipped}个)`;
taskStatus.style.color = '#00ff88';
if (emptyStateText) {
emptyStateText.textContent = `正在下载...`;
}
} else if (completed > 0 || skipped > 0) {
// 全部完成
taskStatus.textContent = `✅ 任务完成 (已完成${completed}个, 跳过${skipped}个)`;
taskStatus.style.color = '#00ff88';
if (emptyStateText) {
emptyStateText.textContent = `所有任务已完成`;
}
} else {
taskStatus.textContent = '⏳ 等待中';
taskStatus.style.color = '#888';
}
} else {
// 无当前任务时,根据暂停状态显示
if (isPaused) {
taskInfo.style.display = 'block';
taskChat.textContent = '-';
taskStatus.textContent = '⏸️ 已暂停下载';
taskStatus.style.color = '#ff9500';
} else {
taskInfo.style.display = 'none';
}
}
})
.catch(() => {});
fetch('/get_download_list?already_down=false')
.then(r => r.json())
.then(data => {
const downloadingCount = data.filter(d => d.download_progress < 100).length;
const completedCount = data.filter(d => d.download_progress >= 100).length;
document.getElementById('downloading-count').textContent = downloadingCount;
document.getElementById('completed-count').textContent = completedCount;
const listEl = document.getElementById('download-list');
if (data.length === 0) {
// 保留空状态但不覆盖检查中的提示
const existingEmpty = listEl.querySelector('.empty-state');
if (!existingEmpty) {
listEl.innerHTML = `
<div class="empty-state" id="empty-state">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
<polyline points="7 10 12 15 17 10"></polyline>
<line x1="12" y1="15" x2="12" y2="3"></line>
</svg>
<p id="empty-state-text">暂无下载任务</p>
</div>
`;
}
return;
}
listEl.innerHTML = data.map(item => `
<div class="download-item">
<div class="download-info">
<div class="download-name" title="${item.filename}">${item.filename}</div>
<div class="progress-bar">
<div class="progress-fill" style="width: ${item.download_progress}%"></div>
</div>
</div>
<div class="download-stats">
<div class="download-percent">${item.download_progress}%</div>
<div class="download-speed">${item.download_speed}</div>
</div>
</div>
`).join('');
})
.catch(() => {});
}
// 设置默认日期
document.getElementById('start-date').value = '2024-01-01';
// 初始化
loadConfig();
loadChannelHistory();
updateDownloadStatus();
setInterval(updateDownloadStatus, 2000);
// 获取版本
fetch('/get_app_version')
.then(r => r.text())
.then(v => document.getElementById('version').textContent = v)
.catch(() => {});
// 当前下载状态
let isDownloading = true;
// 切换下载状态(暂停/继续)
function toggleDownload() {
const btn = document.getElementById('pause-btn');
const stateEl = document.getElementById('current-state');
btn.disabled = true;
const action = isDownloading ? 'pause' : 'continue';
fetch('/set_download_state?state=' + action, {
method: 'POST',
credentials: 'include'
})
.then(r => {
if (r.redirected || r.status === 401 || r.status === 302) {
// 需要登录,跳转到登录页
window.location.href = '/login';
throw new Error('需要登录');
}
return r.text();
})
.then(response => {
// 响应是下一个操作: "pause" 表示当前是下载中,"continue" 表示当前是暂停
const taskStatus = document.getElementById('task-status');
if (response === 'pause') {
isDownloading = true;
window.currentDownloadState = 'pause';
btn.textContent = '⏸️ 暂停下载';
btn.className = 'btn btn-large btn-warning';
stateEl.textContent = '下载中';
stateEl.style.color = '#00ff88';
// 任务状态由 updateDownloadStatus 自动更新(保持检查状态或显示下载中)
showAlert('已恢复下载', 'success');
} else if (response === 'continue') {
isDownloading = false;
window.currentDownloadState = 'continue';
btn.textContent = '▶️ 继续下载';
btn.className = 'btn btn-large btn-success';
stateEl.textContent = '已暂停';
stateEl.style.color = '#ff9500';
// 暂停时清零速度显示
document.getElementById('download-speed').textContent = '0 B/s';
// 任务状态由 updateDownloadStatus 自动更新(检查中继续显示检查,检查完成后显示暂停)
showAlert('已暂停下载(文件检查将继续完成)', 'success');
}
})
.catch(err => {
if (err.message !== '需要登录') {
showAlert('操作失败: ' + err.message, 'error');
}
})
.finally(() => {
btn.disabled = false;
});
}
// 获取当前下载状态
function checkDownloadState() {
fetch('/get_download_status')
.then(r => r.json())
.then(data => {
const btn = document.getElementById('pause-btn');
const stateEl = document.getElementById('current-state');
// 保存全局状态
window.currentDownloadState = data.state;
// data.state 是 "pause" 表示当前下载中可以暂停,"continue" 表示当前暂停可以继续
if (data.state === 'pause') {
isDownloading = true;
btn.textContent = '⏸️ 暂停下载';
btn.className = 'btn btn-large btn-warning';
stateEl.textContent = '下载中';
stateEl.style.color = '#00ff88';
} else {
isDownloading = false;
btn.textContent = '▶️ 继续下载';
btn.className = 'btn btn-large btn-success';
stateEl.textContent = '已暂停';
stateEl.style.color = '#ff9500';
// 暂停时清零速度显示
document.getElementById('download-speed').textContent = '0 B/s';
// 任务状态由 updateDownloadStatus 自动更新(根据检查状态和暂停状态综合判断)
}
})
.catch(() => {});
}
// 初始化时检查状态
checkDownloadState();
</script>
</body>
</html>