重构下载队列显示逻辑和控制机制
Browse files新功能:
- 队列显示所有任务状态(排队中、下载中、已完成、失败)
- 详细任务信息显示:小说名称 - ID - 添加时间 - 状态
- 完整的任务生命周期管理和时间记录
- 任务管理中心替代简单下载队列
控制机制改进:
- 移除前端队列判断逻辑,完全由后台队列系统管理
- 自动任务调度和优先级控制
- 智能队列处理和状态同步
状态管理优化:
- 实时状态更新(排队中/下载中/已完成/失败)
- 不同状态的视觉标识(颜色、图标、动画)
- 当前下载任务高亮显示
时间记录功能:
- 记录任务添加时间、开始时间、完成时间
- 本地时间格式显示
- 完整的任务历史追踪
用户体验提升:
- 任务管理中心成为核心功能
- 清晰的任务生命周期可视化
- 更好的错误处理和重试机制
- 已完成任务直接下载链接
- __pycache__/server.cpython-313.pyc +0 -0
- server.py +97 -193
__pycache__/server.cpython-313.pyc
CHANGED
|
Binary files a/__pycache__/server.cpython-313.pyc and b/__pycache__/server.cpython-313.pyc differ
|
|
|
server.py
CHANGED
|
@@ -13,15 +13,23 @@ TASK_QUEUE = queue.Queue()
|
|
| 13 |
STATUS = {}
|
| 14 |
FILE_PATHS = {}
|
| 15 |
NAMES = {}
|
|
|
|
| 16 |
|
| 17 |
# 后台下载任务处理线程
|
| 18 |
def process_queue():
|
|
|
|
|
|
|
| 19 |
while True:
|
| 20 |
try:
|
| 21 |
book_id = TASK_QUEUE.get(timeout=1) # 添加超时避免无限等待
|
| 22 |
if book_id in STATUS and STATUS[book_id] != "queued":
|
| 23 |
continue # 跳过已经处理过的任务
|
| 24 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 25 |
STATUS[book_id] = "in-progress"
|
| 26 |
save_path = os.path.join(DOWNLOAD_ROOT, book_id)
|
| 27 |
os.makedirs(save_path, exist_ok=True)
|
|
@@ -36,17 +44,28 @@ def process_queue():
|
|
| 36 |
filename = f"{name}.txt"
|
| 37 |
path = os.path.join(save_path, filename)
|
| 38 |
|
|
|
|
|
|
|
|
|
|
| 39 |
if os.path.exists(path):
|
| 40 |
FILE_PATHS[book_id] = f"/files/{book_id}/{filename}"
|
| 41 |
NAMES[book_id] = name
|
| 42 |
STATUS[book_id] = "done"
|
|
|
|
|
|
|
|
|
|
| 43 |
print(f"下载完成: {name}")
|
| 44 |
else:
|
| 45 |
STATUS[book_id] = "error"
|
|
|
|
|
|
|
| 46 |
print(f"下载失败: 文件不存在 - {book_id}")
|
| 47 |
|
| 48 |
except Exception as e:
|
| 49 |
STATUS[book_id] = "error"
|
|
|
|
|
|
|
|
|
|
| 50 |
print(f"下载异常: {book_id} - {str(e)}")
|
| 51 |
|
| 52 |
except:
|
|
@@ -293,9 +312,10 @@ def root():
|
|
| 293 |
padding: 1rem;
|
| 294 |
border-bottom: 1px solid #e2e8f0;
|
| 295 |
display: flex;
|
| 296 |
-
align-items:
|
| 297 |
justify-content: space-between;
|
| 298 |
transition: background 0.2s ease;
|
|
|
|
| 299 |
}
|
| 300 |
|
| 301 |
.list-item:hover {
|
|
@@ -453,11 +473,11 @@ def root():
|
|
| 453 |
<div id="downloadResult" class="download-result"></div>
|
| 454 |
</div>
|
| 455 |
|
| 456 |
-
<!--
|
| 457 |
<div class="card">
|
| 458 |
<div class="card-title">
|
| 459 |
-
<i class="fas fa-
|
| 460 |
-
|
| 461 |
<button id="refreshTasks" class="btn btn-secondary btn-small" style="margin-left: auto;">
|
| 462 |
<i class="fas fa-sync-alt"></i>
|
| 463 |
</button>
|
|
@@ -612,7 +632,7 @@ def root():
|
|
| 612 |
// 解析输入的ID
|
| 613 |
const ids = book_ids.split(/[\s,;]+/).filter(id => id);
|
| 614 |
|
| 615 |
-
//
|
| 616 |
for (const bid of ids) {
|
| 617 |
await fetch(`/enqueue?book_id=${encodeURIComponent(bid)}`);
|
| 618 |
}
|
|
@@ -622,21 +642,7 @@ def root():
|
|
| 622 |
|
| 623 |
// 立即刷新队列显示
|
| 624 |
fetchTasks();
|
| 625 |
-
showNotification(`已添加 ${ids.length} 个任务到队列`, 'success');
|
| 626 |
-
|
| 627 |
-
// 检查是否有正在进行的任务
|
| 628 |
-
const tasks = await fetch('/tasks').then(res => res.json());
|
| 629 |
-
const activeTasks = tasks.filter(t => t.status === 'in-progress');
|
| 630 |
-
|
| 631 |
-
// 如果没有正在进行的任务,开始处理第一个排队的任务
|
| 632 |
-
if (activeTasks.length === 0) {
|
| 633 |
-
const queuedTasks = tasks.filter(t => t.status === 'queued');
|
| 634 |
-
if (queuedTasks.length > 0) {
|
| 635 |
-
// 开始下载第一个排队的任务
|
| 636 |
-
const firstTask = queuedTasks[0];
|
| 637 |
-
startDirectDownload(firstTask.book_id);
|
| 638 |
-
}
|
| 639 |
-
}
|
| 640 |
|
| 641 |
setButtonLoading(btn, false);
|
| 642 |
} catch (error) {
|
|
@@ -645,154 +651,8 @@ def root():
|
|
| 645 |
}
|
| 646 |
});
|
| 647 |
|
| 648 |
-
// 全局变量跟踪下载状态
|
| 649 |
-
let isDownloading = false;
|
| 650 |
let currentDownloadId = null;
|
| 651 |
-
|
| 652 |
-
// 直接下载函数
|
| 653 |
-
async function startDirectDownload(bookId) {
|
| 654 |
-
if (isDownloading) {
|
| 655 |
-
console.log('已有下载任务在进行中,跳过');
|
| 656 |
-
return;
|
| 657 |
-
}
|
| 658 |
-
|
| 659 |
-
isDownloading = true;
|
| 660 |
-
currentDownloadId = bookId;
|
| 661 |
-
|
| 662 |
-
try {
|
| 663 |
-
// 重置并显示进度条
|
| 664 |
-
const progressContainer = document.getElementById('progress');
|
| 665 |
-
const progBar = document.getElementById('progBar');
|
| 666 |
-
const progText = document.getElementById('progText');
|
| 667 |
-
|
| 668 |
-
progBar.style.width = '0%';
|
| 669 |
-
progText.innerText = '0%';
|
| 670 |
-
progressContainer.style.display = 'block';
|
| 671 |
-
progressContainer.classList.add('fade-in');
|
| 672 |
-
|
| 673 |
-
// 清空之前的下载结果
|
| 674 |
-
const resultDiv = document.getElementById('downloadResult');
|
| 675 |
-
resultDiv.innerHTML = '';
|
| 676 |
-
|
| 677 |
-
showNotification(`开始下载小说 ${NAMES[bookId] || bookId}...`, 'info');
|
| 678 |
-
|
| 679 |
-
const response = await fetch(`/download?book_ids=${encodeURIComponent(bookId)}`);
|
| 680 |
-
|
| 681 |
-
if (!response.ok) {
|
| 682 |
-
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
| 683 |
-
}
|
| 684 |
-
|
| 685 |
-
const total = response.headers.get('Content-Length');
|
| 686 |
-
const reader = response.body.getReader();
|
| 687 |
-
let received = 0;
|
| 688 |
-
const chunks = [];
|
| 689 |
-
|
| 690 |
-
function read() {
|
| 691 |
-
return reader.read().then(({done, value})=>{
|
| 692 |
-
if (done) {
|
| 693 |
-
try {
|
| 694 |
-
const blob = new Blob(chunks, {type: response.headers.get('Content-Type')});
|
| 695 |
-
const url = URL.createObjectURL(blob);
|
| 696 |
-
const cd = response.headers.get('Content-Disposition');
|
| 697 |
-
let filename = 'download.txt';
|
| 698 |
-
if (cd) {
|
| 699 |
-
const match = cd.match(/filename[*]?=['"]?([^'";\r\n]+)['"]?/);
|
| 700 |
-
if (match) filename = decodeURIComponent(match[1]);
|
| 701 |
-
}
|
| 702 |
-
|
| 703 |
-
// 生成下载链接
|
| 704 |
-
const link = document.createElement('a');
|
| 705 |
-
link.href = url;
|
| 706 |
-
link.download = filename;
|
| 707 |
-
link.className = 'download-link fade-in';
|
| 708 |
-
link.innerHTML = `<i class="fas fa-download"></i> ${filename}`;
|
| 709 |
-
|
| 710 |
-
resultDiv.innerHTML = '';
|
| 711 |
-
resultDiv.appendChild(link);
|
| 712 |
-
|
| 713 |
-
// 隐藏进度条
|
| 714 |
-
progressContainer.style.display = 'none';
|
| 715 |
-
|
| 716 |
-
showNotification(`下载完成:${filename}`, 'success');
|
| 717 |
-
|
| 718 |
-
// 标记下载完成
|
| 719 |
-
isDownloading = false;
|
| 720 |
-
currentDownloadId = null;
|
| 721 |
-
|
| 722 |
-
// 刷新任务列表和已下载列表
|
| 723 |
-
fetchTasks();
|
| 724 |
-
fetchCompleted();
|
| 725 |
-
|
| 726 |
-
// 检查是否还有排队的任务
|
| 727 |
-
setTimeout(async () => {
|
| 728 |
-
try {
|
| 729 |
-
const tasks = await fetch('/tasks').then(res => res.json());
|
| 730 |
-
const queuedTasks = tasks.filter(t => t.status === 'queued');
|
| 731 |
-
if (queuedTasks.length > 0 && !isDownloading) {
|
| 732 |
-
startDirectDownload(queuedTasks[0].book_id);
|
| 733 |
-
}
|
| 734 |
-
} catch (error) {
|
| 735 |
-
console.error('检查队列任务失败:', error);
|
| 736 |
-
}
|
| 737 |
-
}, 1000);
|
| 738 |
-
|
| 739 |
-
} catch (error) {
|
| 740 |
-
console.error('处理下载结果失败:', error);
|
| 741 |
-
showNotification('处理下载结果失败', 'error');
|
| 742 |
-
}
|
| 743 |
-
return;
|
| 744 |
-
}
|
| 745 |
-
|
| 746 |
-
chunks.push(value);
|
| 747 |
-
received += value.length;
|
| 748 |
-
|
| 749 |
-
// 更新进度条
|
| 750 |
-
if (total && parseInt(total) > 0) {
|
| 751 |
-
const percent = Math.min(100, Math.round((received / parseInt(total)) * 100));
|
| 752 |
-
progBar.style.width = percent + '%';
|
| 753 |
-
progText.innerText = percent + '%';
|
| 754 |
-
} else {
|
| 755 |
-
// 如果没有总长度信息,显示已接收的数据量
|
| 756 |
-
const mb = (received / (1024 * 1024)).toFixed(1);
|
| 757 |
-
progText.innerText = `已下载 ${mb} MB`;
|
| 758 |
-
// 使用动画效果显示进度
|
| 759 |
-
const animatedPercent = Math.min(90, (received / 1000000) * 10); // 假设进度
|
| 760 |
-
progBar.style.width = animatedPercent + '%';
|
| 761 |
-
}
|
| 762 |
-
|
| 763 |
-
return read();
|
| 764 |
-
});
|
| 765 |
-
}
|
| 766 |
-
|
| 767 |
-
await read();
|
| 768 |
-
|
| 769 |
-
} catch (error) {
|
| 770 |
-
console.error('下载失败:', error);
|
| 771 |
-
|
| 772 |
-
// 隐藏进度条
|
| 773 |
-
const progressContainer = document.getElementById('progress');
|
| 774 |
-
progressContainer.style.display = 'none';
|
| 775 |
-
|
| 776 |
-
showNotification(`下载失败:${error.message}`, 'error');
|
| 777 |
-
|
| 778 |
-
// 标记下载完成(即使失败)
|
| 779 |
-
isDownloading = false;
|
| 780 |
-
currentDownloadId = null;
|
| 781 |
-
|
| 782 |
-
// 即使失败也要继续处理下一个任务
|
| 783 |
-
setTimeout(async () => {
|
| 784 |
-
try {
|
| 785 |
-
const tasks = await fetch('/tasks').then(res => res.json());
|
| 786 |
-
const queuedTasks = tasks.filter(t => t.status === 'queued');
|
| 787 |
-
if (queuedTasks.length > 0 && !isDownloading) {
|
| 788 |
-
startDirectDownload(queuedTasks[0].book_id);
|
| 789 |
-
}
|
| 790 |
-
} catch (error) {
|
| 791 |
-
console.error('检查队列任务失败:', error);
|
| 792 |
-
}
|
| 793 |
-
}, 2000);
|
| 794 |
-
}
|
| 795 |
-
}
|
| 796 |
// 全局变量存储任务名称
|
| 797 |
let NAMES = {};
|
| 798 |
|
|
@@ -800,7 +660,6 @@ def root():
|
|
| 800 |
function fetchTasks() {
|
| 801 |
fetch('/tasks').then(res => res.json()).then(data => {
|
| 802 |
const container = document.getElementById('taskList');
|
| 803 |
-
const activeTasks = data.filter(task => task.status !== 'done');
|
| 804 |
|
| 805 |
// 更新全局名称缓存
|
| 806 |
data.forEach(task => {
|
|
@@ -809,13 +668,13 @@ def root():
|
|
| 809 |
}
|
| 810 |
});
|
| 811 |
|
| 812 |
-
if (
|
| 813 |
-
container.innerHTML = '<div class="list-item"><span style="color: #a0aec0;"><i class="fas fa-
|
| 814 |
return;
|
| 815 |
}
|
| 816 |
|
| 817 |
container.innerHTML = '';
|
| 818 |
-
|
| 819 |
const div = document.createElement('div');
|
| 820 |
div.className = 'list-item fade-in';
|
| 821 |
div.setAttribute('data-book-id', task.book_id);
|
|
@@ -823,18 +682,21 @@ def root():
|
|
| 823 |
const statusClass = {
|
| 824 |
'queued': 'status-queued',
|
| 825 |
'in-progress': 'status-progress',
|
|
|
|
| 826 |
'error': 'status-error'
|
| 827 |
}[task.status] || 'status-queued';
|
| 828 |
|
| 829 |
const statusIcon = {
|
| 830 |
'queued': 'fas fa-clock',
|
| 831 |
'in-progress': 'fas fa-spinner fa-spin',
|
|
|
|
| 832 |
'error': 'fas fa-exclamation-triangle'
|
| 833 |
}[task.status] || 'fas fa-clock';
|
| 834 |
|
| 835 |
const statusText = {
|
| 836 |
-
'queued':
|
| 837 |
'in-progress': '下载中',
|
|
|
|
| 838 |
'error': '失败'
|
| 839 |
}[task.status] || '未知';
|
| 840 |
|
|
@@ -842,12 +704,41 @@ def root():
|
|
| 842 |
const isCurrentDownload = task.book_id === currentDownloadId;
|
| 843 |
const extraClass = isCurrentDownload ? ' current-download' : '';
|
| 844 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 845 |
div.innerHTML = `
|
| 846 |
-
<
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 847 |
<span class="status-badge ${statusClass}${extraClass}">
|
| 848 |
-
<i class="${statusIcon}"></i> ${
|
| 849 |
</span>
|
| 850 |
`;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 851 |
container.appendChild(div);
|
| 852 |
});
|
| 853 |
}).catch(error => {
|
|
@@ -976,18 +867,11 @@ def root():
|
|
| 976 |
// 自动刷新队列 - 更频繁的刷新以保持同步
|
| 977 |
setInterval(() => {
|
| 978 |
fetchTasks();
|
| 979 |
-
//
|
| 980 |
-
|
| 981 |
-
|
| 982 |
-
|
| 983 |
-
|
| 984 |
-
|
| 985 |
-
// 如果没有进行中的任务但有排队的任务,启动下载
|
| 986 |
-
if (inProgressTasks.length === 0 && queuedTasks.length > 0) {
|
| 987 |
-
startDirectDownload(queuedTasks[0].book_id);
|
| 988 |
-
}
|
| 989 |
-
}).catch(console.error);
|
| 990 |
-
}
|
| 991 |
}, 2000);
|
| 992 |
|
| 993 |
// 添加一些交互效果
|
|
@@ -1016,16 +900,12 @@ def root():
|
|
| 1016 |
@app.get("/enqueue")
|
| 1017 |
def enqueue(book_id: str):
|
| 1018 |
"""添加到下载队列"""
|
|
|
|
|
|
|
| 1019 |
# 如果已有任务在队列或处理中
|
| 1020 |
if STATUS.get(book_id) in ("queued", "in-progress"):
|
| 1021 |
return {"message": "已在队列", "status": STATUS[book_id]}
|
| 1022 |
|
| 1023 |
-
# 如果任务已完成,重新设置为排队状态
|
| 1024 |
-
if STATUS.get(book_id) == "done":
|
| 1025 |
-
STATUS[book_id] = "queued"
|
| 1026 |
-
TASK_QUEUE.put(book_id)
|
| 1027 |
-
return {"message": "重新添加到下载队列", "status": "queued"}
|
| 1028 |
-
|
| 1029 |
# 获取书籍信息并设置名称
|
| 1030 |
try:
|
| 1031 |
name, _, _ = get_book_info(book_id, get_headers())
|
|
@@ -1034,6 +914,15 @@ def enqueue(book_id: str):
|
|
| 1034 |
except:
|
| 1035 |
NAMES[book_id] = f"小说_{book_id}"
|
| 1036 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1037 |
TASK_QUEUE.put(book_id)
|
| 1038 |
STATUS[book_id] = "queued"
|
| 1039 |
return {"message": "已添加到下载队列", "status": "queued"}
|
|
@@ -1053,7 +942,22 @@ def task_status(book_id: str):
|
|
| 1053 |
@app.get("/tasks")
|
| 1054 |
def tasks():
|
| 1055 |
"""获取所有任务"""
|
| 1056 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1057 |
|
| 1058 |
@app.get("/search")
|
| 1059 |
def search(name: str = ""):
|
|
|
|
| 13 |
STATUS = {}
|
| 14 |
FILE_PATHS = {}
|
| 15 |
NAMES = {}
|
| 16 |
+
TASK_DETAILS = {} # 存储任务详细信息,包括时间戳等
|
| 17 |
|
| 18 |
# 后台下载任务处理线程
|
| 19 |
def process_queue():
|
| 20 |
+
from datetime import datetime
|
| 21 |
+
|
| 22 |
while True:
|
| 23 |
try:
|
| 24 |
book_id = TASK_QUEUE.get(timeout=1) # 添加超时避免无限等待
|
| 25 |
if book_id in STATUS and STATUS[book_id] != "queued":
|
| 26 |
continue # 跳过已经处理过的任务
|
| 27 |
|
| 28 |
+
# 记录开始时间
|
| 29 |
+
start_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
| 30 |
+
if book_id in TASK_DETAILS:
|
| 31 |
+
TASK_DETAILS[book_id]["start_time"] = start_time
|
| 32 |
+
|
| 33 |
STATUS[book_id] = "in-progress"
|
| 34 |
save_path = os.path.join(DOWNLOAD_ROOT, book_id)
|
| 35 |
os.makedirs(save_path, exist_ok=True)
|
|
|
|
| 44 |
filename = f"{name}.txt"
|
| 45 |
path = os.path.join(save_path, filename)
|
| 46 |
|
| 47 |
+
# 记录完成时间
|
| 48 |
+
complete_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
| 49 |
+
|
| 50 |
if os.path.exists(path):
|
| 51 |
FILE_PATHS[book_id] = f"/files/{book_id}/{filename}"
|
| 52 |
NAMES[book_id] = name
|
| 53 |
STATUS[book_id] = "done"
|
| 54 |
+
if book_id in TASK_DETAILS:
|
| 55 |
+
TASK_DETAILS[book_id]["complete_time"] = complete_time
|
| 56 |
+
TASK_DETAILS[book_id]["name"] = name
|
| 57 |
print(f"下载完成: {name}")
|
| 58 |
else:
|
| 59 |
STATUS[book_id] = "error"
|
| 60 |
+
if book_id in TASK_DETAILS:
|
| 61 |
+
TASK_DETAILS[book_id]["complete_time"] = complete_time
|
| 62 |
print(f"下载失败: 文件不存在 - {book_id}")
|
| 63 |
|
| 64 |
except Exception as e:
|
| 65 |
STATUS[book_id] = "error"
|
| 66 |
+
complete_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
| 67 |
+
if book_id in TASK_DETAILS:
|
| 68 |
+
TASK_DETAILS[book_id]["complete_time"] = complete_time
|
| 69 |
print(f"下载异常: {book_id} - {str(e)}")
|
| 70 |
|
| 71 |
except:
|
|
|
|
| 312 |
padding: 1rem;
|
| 313 |
border-bottom: 1px solid #e2e8f0;
|
| 314 |
display: flex;
|
| 315 |
+
align-items: flex-start;
|
| 316 |
justify-content: space-between;
|
| 317 |
transition: background 0.2s ease;
|
| 318 |
+
min-height: 80px;
|
| 319 |
}
|
| 320 |
|
| 321 |
.list-item:hover {
|
|
|
|
| 473 |
<div id="downloadResult" class="download-result"></div>
|
| 474 |
</div>
|
| 475 |
|
| 476 |
+
<!-- 任务管理中心 -->
|
| 477 |
<div class="card">
|
| 478 |
<div class="card-title">
|
| 479 |
+
<i class="fas fa-tasks"></i>
|
| 480 |
+
任务管理中心
|
| 481 |
<button id="refreshTasks" class="btn btn-secondary btn-small" style="margin-left: auto;">
|
| 482 |
<i class="fas fa-sync-alt"></i>
|
| 483 |
</button>
|
|
|
|
| 632 |
// 解析输入的ID
|
| 633 |
const ids = book_ids.split(/[\s,;]+/).filter(id => id);
|
| 634 |
|
| 635 |
+
// 将所有ID加入队列,队列系统会自动管理执行
|
| 636 |
for (const bid of ids) {
|
| 637 |
await fetch(`/enqueue?book_id=${encodeURIComponent(bid)}`);
|
| 638 |
}
|
|
|
|
| 642 |
|
| 643 |
// 立即刷新队列显示
|
| 644 |
fetchTasks();
|
| 645 |
+
showNotification(`已添加 ${ids.length} 个任务到队列,队列系统将自动处理`, 'success');
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 646 |
|
| 647 |
setButtonLoading(btn, false);
|
| 648 |
} catch (error) {
|
|
|
|
| 651 |
}
|
| 652 |
});
|
| 653 |
|
| 654 |
+
// 全局变量跟踪当前下载状态(仅用于UI显示)
|
|
|
|
| 655 |
let currentDownloadId = null;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 656 |
// 全局变量存储任务名称
|
| 657 |
let NAMES = {};
|
| 658 |
|
|
|
|
| 660 |
function fetchTasks() {
|
| 661 |
fetch('/tasks').then(res => res.json()).then(data => {
|
| 662 |
const container = document.getElementById('taskList');
|
|
|
|
| 663 |
|
| 664 |
// 更新全局名称缓存
|
| 665 |
data.forEach(task => {
|
|
|
|
| 668 |
}
|
| 669 |
});
|
| 670 |
|
| 671 |
+
if (data.length === 0) {
|
| 672 |
+
container.innerHTML = '<div class="list-item"><span style="color: #a0aec0;"><i class="fas fa-inbox"></i> 暂无任务</span></div>';
|
| 673 |
return;
|
| 674 |
}
|
| 675 |
|
| 676 |
container.innerHTML = '';
|
| 677 |
+
data.forEach((task, index) => {
|
| 678 |
const div = document.createElement('div');
|
| 679 |
div.className = 'list-item fade-in';
|
| 680 |
div.setAttribute('data-book-id', task.book_id);
|
|
|
|
| 682 |
const statusClass = {
|
| 683 |
'queued': 'status-queued',
|
| 684 |
'in-progress': 'status-progress',
|
| 685 |
+
'done': 'status-done',
|
| 686 |
'error': 'status-error'
|
| 687 |
}[task.status] || 'status-queued';
|
| 688 |
|
| 689 |
const statusIcon = {
|
| 690 |
'queued': 'fas fa-clock',
|
| 691 |
'in-progress': 'fas fa-spinner fa-spin',
|
| 692 |
+
'done': 'fas fa-check-circle',
|
| 693 |
'error': 'fas fa-exclamation-triangle'
|
| 694 |
}[task.status] || 'fas fa-clock';
|
| 695 |
|
| 696 |
const statusText = {
|
| 697 |
+
'queued': '排队中',
|
| 698 |
'in-progress': '下载中',
|
| 699 |
+
'done': '已完成',
|
| 700 |
'error': '失败'
|
| 701 |
}[task.status] || '未知';
|
| 702 |
|
|
|
|
| 704 |
const isCurrentDownload = task.book_id === currentDownloadId;
|
| 705 |
const extraClass = isCurrentDownload ? ' current-download' : '';
|
| 706 |
|
| 707 |
+
// 构建任务信息显示
|
| 708 |
+
const taskInfo = `${task.name || task.book_id} - ID: [${task.book_id}]`;
|
| 709 |
+
const timeInfo = `添加时间: ${task.add_time}`;
|
| 710 |
+
const statusInfo = `状态: ${statusText}`;
|
| 711 |
+
|
| 712 |
div.innerHTML = `
|
| 713 |
+
<div style="flex: 1;">
|
| 714 |
+
<div style="font-weight: 500; margin-bottom: 4px;">
|
| 715 |
+
<i class="fas fa-book"></i> ${taskInfo}
|
| 716 |
+
</div>
|
| 717 |
+
<div style="font-size: 0.8rem; color: #6b7280; margin-bottom: 2px;">
|
| 718 |
+
<i class="fas fa-clock"></i> ${timeInfo}
|
| 719 |
+
</div>
|
| 720 |
+
<div style="font-size: 0.8rem; color: #6b7280;">
|
| 721 |
+
${task.start_time ? `<i class="fas fa-play"></i> 开始: ${task.start_time}` : ''}
|
| 722 |
+
${task.complete_time ? `<i class="fas fa-flag-checkered"></i> 完成: ${task.complete_time}` : ''}
|
| 723 |
+
</div>
|
| 724 |
+
</div>
|
| 725 |
<span class="status-badge ${statusClass}${extraClass}">
|
| 726 |
+
<i class="${statusIcon}"></i> ${statusInfo}
|
| 727 |
</span>
|
| 728 |
`;
|
| 729 |
+
|
| 730 |
+
// 如果是已完成的任务,添加下载链接
|
| 731 |
+
if (task.status === 'done') {
|
| 732 |
+
const downloadBtn = document.createElement('a');
|
| 733 |
+
downloadBtn.href = `/files/${task.book_id}/${task.name}.txt`;
|
| 734 |
+
downloadBtn.download = `${task.name}.txt`;
|
| 735 |
+
downloadBtn.className = 'btn btn-secondary btn-small';
|
| 736 |
+
downloadBtn.style.marginLeft = '8px';
|
| 737 |
+
downloadBtn.innerHTML = '<i class="fas fa-download"></i>';
|
| 738 |
+
downloadBtn.title = '下载文件';
|
| 739 |
+
div.querySelector('.status-badge').parentNode.appendChild(downloadBtn);
|
| 740 |
+
}
|
| 741 |
+
|
| 742 |
container.appendChild(div);
|
| 743 |
});
|
| 744 |
}).catch(error => {
|
|
|
|
| 867 |
// 自动刷新队列 - 更频繁的刷新以保持同步
|
| 868 |
setInterval(() => {
|
| 869 |
fetchTasks();
|
| 870 |
+
// 更新当前下载任务ID用于UI显示
|
| 871 |
+
fetch('/tasks').then(res => res.json()).then(tasks => {
|
| 872 |
+
const inProgressTasks = tasks.filter(t => t.status === 'in-progress');
|
| 873 |
+
currentDownloadId = inProgressTasks.length > 0 ? inProgressTasks[0].book_id : null;
|
| 874 |
+
}).catch(console.error);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 875 |
}, 2000);
|
| 876 |
|
| 877 |
// 添加一些交互效果
|
|
|
|
| 900 |
@app.get("/enqueue")
|
| 901 |
def enqueue(book_id: str):
|
| 902 |
"""添加到下载队列"""
|
| 903 |
+
from datetime import datetime
|
| 904 |
+
|
| 905 |
# 如果已有任务在队列或处理中
|
| 906 |
if STATUS.get(book_id) in ("queued", "in-progress"):
|
| 907 |
return {"message": "已在队列", "status": STATUS[book_id]}
|
| 908 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 909 |
# 获取书籍信息并设置名称
|
| 910 |
try:
|
| 911 |
name, _, _ = get_book_info(book_id, get_headers())
|
|
|
|
| 914 |
except:
|
| 915 |
NAMES[book_id] = f"小说_{book_id}"
|
| 916 |
|
| 917 |
+
# 记录任务详细信息
|
| 918 |
+
current_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
| 919 |
+
TASK_DETAILS[book_id] = {
|
| 920 |
+
"add_time": current_time,
|
| 921 |
+
"start_time": None,
|
| 922 |
+
"complete_time": None,
|
| 923 |
+
"name": NAMES[book_id]
|
| 924 |
+
}
|
| 925 |
+
|
| 926 |
TASK_QUEUE.put(book_id)
|
| 927 |
STATUS[book_id] = "queued"
|
| 928 |
return {"message": "已添加到下载队列", "status": "queued"}
|
|
|
|
| 942 |
@app.get("/tasks")
|
| 943 |
def tasks():
|
| 944 |
"""获取所有任务"""
|
| 945 |
+
task_list = []
|
| 946 |
+
for book_id, status in STATUS.items():
|
| 947 |
+
task_detail = TASK_DETAILS.get(book_id, {})
|
| 948 |
+
task = {
|
| 949 |
+
"book_id": book_id,
|
| 950 |
+
"name": NAMES.get(book_id, book_id),
|
| 951 |
+
"status": status,
|
| 952 |
+
"add_time": task_detail.get("add_time", "未知"),
|
| 953 |
+
"start_time": task_detail.get("start_time"),
|
| 954 |
+
"complete_time": task_detail.get("complete_time")
|
| 955 |
+
}
|
| 956 |
+
task_list.append(task)
|
| 957 |
+
|
| 958 |
+
# 按添加时间排序,最新的在前面
|
| 959 |
+
task_list.sort(key=lambda x: x["add_time"], reverse=True)
|
| 960 |
+
return task_list
|
| 961 |
|
| 962 |
@app.get("/search")
|
| 963 |
def search(name: str = ""):
|