重大界面美化和功能优化
Browse files新功能:
- 现代化渐变背景和卡片式布局
- 智能下载队列管理
- 实时进度条和状态同步
- 美观的浮动通知系统
- 键盘快捷键支持 (Enter提交)
修复问题:
- 修复进度条不同步问题
- 优化任务状态实时更新
- 改进队列处理逻辑
- 增强错误处理和重试机制
- 修复文件下载编码问题
用户体验提升:
- 输入ID立即显示在队列中
- 自动队列管理和任务调度
- 当前下载任务高亮显示
- 更频繁的状态刷新 (2秒)
- 响应式设计支持移动端
界面改进:
- 使用Font Awesome图标库
- 毛玻璃效果和阴影
- 状态徽章和动画效果
- 现代化按钮和表单设计
- 优化的颜色主题和排版
- __pycache__/server.cpython-313.pyc +0 -0
- server.py +399 -104
__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
|
@@ -17,24 +17,41 @@ NAMES = {}
|
|
| 17 |
# 后台下载任务处理线程
|
| 18 |
def process_queue():
|
| 19 |
while True:
|
| 20 |
-
book_id = TASK_QUEUE.get()
|
| 21 |
-
STATUS[book_id] = "in-progress"
|
| 22 |
-
save_path = os.path.join(DOWNLOAD_ROOT, book_id)
|
| 23 |
-
os.makedirs(save_path, exist_ok=True)
|
| 24 |
try:
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 35 |
STATUS[book_id] = "error"
|
| 36 |
-
|
| 37 |
-
|
|
|
|
|
|
|
|
|
|
| 38 |
|
| 39 |
|
| 40 |
app = FastAPI(
|
|
@@ -316,6 +333,22 @@ def root():
|
|
| 316 |
color: #742a2a;
|
| 317 |
}
|
| 318 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 319 |
.search-container {
|
| 320 |
display: flex;
|
| 321 |
gap: 0.5rem;
|
|
@@ -484,10 +517,71 @@ def root():
|
|
| 484 |
<script>
|
| 485 |
// 工具函数
|
| 486 |
function showNotification(message, type = 'info') {
|
| 487 |
-
//
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 488 |
console.log(`${type.toUpperCase()}: ${message}`);
|
| 489 |
}
|
| 490 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 491 |
function setButtonLoading(button, loading = true) {
|
| 492 |
if (loading) {
|
| 493 |
button.disabled = true;
|
|
@@ -515,25 +609,79 @@ def root():
|
|
| 515 |
setButtonLoading(btn, true);
|
| 516 |
|
| 517 |
try {
|
| 518 |
-
//
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 519 |
const tasks = await fetch('/tasks').then(res => res.json());
|
| 520 |
-
|
| 521 |
-
|
| 522 |
-
|
| 523 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 524 |
}
|
| 525 |
-
fetchTasks();
|
| 526 |
-
showNotification(`已添加 ${ids.length} 个任务到队列`, 'success');
|
| 527 |
-
setButtonLoading(btn, false);
|
| 528 |
-
return;
|
| 529 |
}
|
| 530 |
|
| 531 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 532 |
const progressContainer = document.getElementById('progress');
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 533 |
progressContainer.style.display = 'block';
|
| 534 |
progressContainer.classList.add('fade-in');
|
| 535 |
|
| 536 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 537 |
const total = response.headers.get('Content-Length');
|
| 538 |
const reader = response.body.getReader();
|
| 539 |
let received = 0;
|
|
@@ -542,64 +690,135 @@ def root():
|
|
| 542 |
function read() {
|
| 543 |
return reader.read().then(({done, value})=>{
|
| 544 |
if (done) {
|
| 545 |
-
|
| 546 |
-
|
| 547 |
-
|
| 548 |
-
|
| 549 |
-
|
| 550 |
-
|
| 551 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 552 |
}
|
| 553 |
-
|
| 554 |
-
// 生成下载链接
|
| 555 |
-
const link = document.createElement('a');
|
| 556 |
-
link.href = url;
|
| 557 |
-
link.download = filename;
|
| 558 |
-
link.className = 'download-link fade-in';
|
| 559 |
-
link.innerHTML = `<i class="fas fa-download"></i> ${filename}`;
|
| 560 |
-
|
| 561 |
-
const resultDiv = document.getElementById('downloadResult');
|
| 562 |
-
resultDiv.innerHTML = '';
|
| 563 |
-
resultDiv.appendChild(link);
|
| 564 |
-
|
| 565 |
-
setButtonLoading(btn, false);
|
| 566 |
-
progressContainer.style.display = 'none';
|
| 567 |
-
showNotification('下载完成!', 'success');
|
| 568 |
return;
|
| 569 |
}
|
| 570 |
|
| 571 |
chunks.push(value);
|
| 572 |
received += value.length;
|
| 573 |
-
|
| 574 |
-
|
| 575 |
-
|
| 576 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 577 |
}
|
|
|
|
| 578 |
return read();
|
| 579 |
});
|
| 580 |
}
|
| 581 |
-
|
|
|
|
|
|
|
| 582 |
} catch (error) {
|
| 583 |
-
|
| 584 |
-
|
| 585 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 586 |
}
|
| 587 |
-
}
|
|
|
|
|
|
|
|
|
|
| 588 |
// 刷新队列和已下载列表
|
| 589 |
function fetchTasks() {
|
| 590 |
fetch('/tasks').then(res => res.json()).then(data => {
|
| 591 |
const container = document.getElementById('taskList');
|
| 592 |
const activeTasks = data.filter(task => task.status !== 'done');
|
| 593 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 594 |
if (activeTasks.length === 0) {
|
| 595 |
container.innerHTML = '<div class="list-item"><span style="color: #a0aec0;"><i class="fas fa-check"></i> 暂无下载任务</span></div>';
|
| 596 |
return;
|
| 597 |
}
|
| 598 |
|
| 599 |
container.innerHTML = '';
|
| 600 |
-
activeTasks.forEach(task => {
|
| 601 |
const div = document.createElement('div');
|
| 602 |
div.className = 'list-item fade-in';
|
|
|
|
| 603 |
|
| 604 |
const statusClass = {
|
| 605 |
'queued': 'status-queued',
|
|
@@ -614,14 +833,18 @@ def root():
|
|
| 614 |
}[task.status] || 'fas fa-clock';
|
| 615 |
|
| 616 |
const statusText = {
|
| 617 |
-
'queued': '排队中',
|
| 618 |
'in-progress': '下载中',
|
| 619 |
'error': '失败'
|
| 620 |
}[task.status] || '未知';
|
| 621 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 622 |
div.innerHTML = `
|
| 623 |
<span><i class="fas fa-book"></i> ${task.name || task.book_id}</span>
|
| 624 |
-
<span class="status-badge ${statusClass}">
|
| 625 |
<i class="${statusIcon}"></i> ${statusText}
|
| 626 |
</span>
|
| 627 |
`;
|
|
@@ -629,6 +852,8 @@ def root():
|
|
| 629 |
});
|
| 630 |
}).catch(error => {
|
| 631 |
console.error('获取任务列表失败:', error);
|
|
|
|
|
|
|
| 632 |
});
|
| 633 |
}
|
| 634 |
|
|
@@ -748,8 +973,22 @@ def root():
|
|
| 748 |
}
|
| 749 |
}, 10000);
|
| 750 |
|
| 751 |
-
// 自动刷新队列
|
| 752 |
-
setInterval(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 753 |
|
| 754 |
// 添加一些交互效果
|
| 755 |
document.querySelectorAll('.card').forEach(card => {
|
|
@@ -760,6 +999,14 @@ def root():
|
|
| 760 |
card.style.transform = 'translateY(0)';
|
| 761 |
});
|
| 762 |
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 763 |
});
|
| 764 |
</script>
|
| 765 |
</body>
|
|
@@ -772,6 +1019,21 @@ def enqueue(book_id: str):
|
|
| 772 |
# 如果已有任务在队列或处理中
|
| 773 |
if STATUS.get(book_id) in ("queued", "in-progress"):
|
| 774 |
return {"message": "已在队列", "status": STATUS[book_id]}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 775 |
TASK_QUEUE.put(book_id)
|
| 776 |
STATUS[book_id] = "queued"
|
| 777 |
return {"message": "已添加到下载队列", "status": "queued"}
|
|
@@ -808,51 +1070,84 @@ def download(book_ids: str):
|
|
| 808 |
ids = [bid.strip() for bid in re.split(r'[\s,;]+', book_ids) if bid.strip()]
|
| 809 |
if not ids:
|
| 810 |
raise HTTPException(status_code=400, detail="请提供至少一个 book_id")
|
|
|
|
| 811 |
# 在临时目录创建下载文件夹
|
| 812 |
save_path = tempfile.mkdtemp(prefix="tomato_")
|
| 813 |
-
# mkdtemp 已创建空目录,无需清理
|
| 814 |
-
|
| 815 |
-
# 调用下载核心 - 支持批量ID
|
| 816 |
-
for bid in ids:
|
| 817 |
-
try:
|
| 818 |
-
Run(bid, save_path)
|
| 819 |
-
except Exception as e:
|
| 820 |
-
raise HTTPException(status_code=500, detail=f"下载 {bid} 失败: {str(e)}")
|
| 821 |
|
| 822 |
-
|
| 823 |
-
|
| 824 |
-
|
| 825 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 826 |
|
| 827 |
-
|
| 828 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 829 |
try:
|
| 830 |
-
|
| 831 |
-
filename = f"{name}.txt"
|
| 832 |
-
src = os.path.join(save_path, filename)
|
| 833 |
-
dest_dir = os.path.join(DOWNLOAD_ROOT, bid)
|
| 834 |
-
os.makedirs(dest_dir, exist_ok=True)
|
| 835 |
-
dst = os.path.join(dest_dir, filename)
|
| 836 |
-
if os.path.exists(src):
|
| 837 |
-
shutil.copy(src, dst)
|
| 838 |
-
NAMES[bid] = name
|
| 839 |
-
FILE_PATHS[bid] = f"/files/{bid}/{filename}"
|
| 840 |
-
STATUS[bid] = "done"
|
| 841 |
except:
|
| 842 |
pass
|
| 843 |
-
|
| 844 |
-
if not txt_files:
|
| 845 |
-
raise HTTPException(status_code=404, detail="未生成txt文件,下载失败")
|
| 846 |
-
|
| 847 |
-
# 如果只有一个文件,直接返回
|
| 848 |
-
if len(txt_files) == 1:
|
| 849 |
-
file_path = txt_files[0]
|
| 850 |
-
return FileResponse(path=file_path, filename=os.path.basename(file_path), media_type="text/plain")
|
| 851 |
-
|
| 852 |
-
# 多文件时打包成 ZIP 并返回
|
| 853 |
-
zip_path = os.path.join(save_path, "novels.zip")
|
| 854 |
-
with zipfile.ZipFile(zip_path, "w") as zf:
|
| 855 |
-
for fpath in txt_files:
|
| 856 |
-
zf.write(fpath, arcname=os.path.basename(fpath))
|
| 857 |
-
return FileResponse(path=zip_path, filename="novels.zip", media_type="application/zip")
|
| 858 |
|
|
|
|
| 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)
|
| 28 |
+
|
| 29 |
+
try:
|
| 30 |
+
fetch_api_endpoints_from_server()
|
| 31 |
+
Run(book_id, save_path)
|
| 32 |
+
name, _, _ = get_book_info(book_id, get_headers())
|
| 33 |
+
if not name:
|
| 34 |
+
name = f"小说_{book_id}"
|
| 35 |
+
|
| 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:
|
| 53 |
+
# 队列为空时的超时,继续循环
|
| 54 |
+
continue
|
| 55 |
|
| 56 |
|
| 57 |
app = FastAPI(
|
|
|
|
| 333 |
color: #742a2a;
|
| 334 |
}
|
| 335 |
|
| 336 |
+
.current-download {
|
| 337 |
+
animation: pulse 2s infinite;
|
| 338 |
+
}
|
| 339 |
+
|
| 340 |
+
@keyframes pulse {
|
| 341 |
+
0% { opacity: 1; }
|
| 342 |
+
50% { opacity: 0.7; }
|
| 343 |
+
100% { opacity: 1; }
|
| 344 |
+
}
|
| 345 |
+
|
| 346 |
+
.progress-info {
|
| 347 |
+
font-size: 0.8rem;
|
| 348 |
+
color: #4a5568;
|
| 349 |
+
margin-top: 0.25rem;
|
| 350 |
+
}
|
| 351 |
+
|
| 352 |
.search-container {
|
| 353 |
display: flex;
|
| 354 |
gap: 0.5rem;
|
|
|
|
| 517 |
<script>
|
| 518 |
// 工具函数
|
| 519 |
function showNotification(message, type = 'info') {
|
| 520 |
+
// 创建通知元素
|
| 521 |
+
const notification = document.createElement('div');
|
| 522 |
+
notification.className = `notification notification-${type}`;
|
| 523 |
+
notification.innerHTML = `
|
| 524 |
+
<i class="fas ${getNotificationIcon(type)}"></i>
|
| 525 |
+
<span>${message}</span>
|
| 526 |
+
`;
|
| 527 |
+
|
| 528 |
+
// 添加样式
|
| 529 |
+
notification.style.cssText = `
|
| 530 |
+
position: fixed;
|
| 531 |
+
top: 20px;
|
| 532 |
+
right: 20px;
|
| 533 |
+
padding: 12px 16px;
|
| 534 |
+
border-radius: 8px;
|
| 535 |
+
color: white;
|
| 536 |
+
font-weight: 500;
|
| 537 |
+
z-index: 1000;
|
| 538 |
+
max-width: 300px;
|
| 539 |
+
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
|
| 540 |
+
transform: translateX(100%);
|
| 541 |
+
transition: transform 0.3s ease;
|
| 542 |
+
background: ${getNotificationColor(type)};
|
| 543 |
+
`;
|
| 544 |
+
|
| 545 |
+
document.body.appendChild(notification);
|
| 546 |
+
|
| 547 |
+
// 显示动画
|
| 548 |
+
setTimeout(() => {
|
| 549 |
+
notification.style.transform = 'translateX(0)';
|
| 550 |
+
}, 100);
|
| 551 |
+
|
| 552 |
+
// 自动隐藏
|
| 553 |
+
setTimeout(() => {
|
| 554 |
+
notification.style.transform = 'translateX(100%)';
|
| 555 |
+
setTimeout(() => {
|
| 556 |
+
if (notification.parentNode) {
|
| 557 |
+
notification.parentNode.removeChild(notification);
|
| 558 |
+
}
|
| 559 |
+
}, 300);
|
| 560 |
+
}, type === 'error' ? 5000 : 3000);
|
| 561 |
+
|
| 562 |
console.log(`${type.toUpperCase()}: ${message}`);
|
| 563 |
}
|
| 564 |
|
| 565 |
+
function getNotificationIcon(type) {
|
| 566 |
+
const icons = {
|
| 567 |
+
'success': 'fa-check-circle',
|
| 568 |
+
'error': 'fa-exclamation-circle',
|
| 569 |
+
'warning': 'fa-exclamation-triangle',
|
| 570 |
+
'info': 'fa-info-circle'
|
| 571 |
+
};
|
| 572 |
+
return icons[type] || icons.info;
|
| 573 |
+
}
|
| 574 |
+
|
| 575 |
+
function getNotificationColor(type) {
|
| 576 |
+
const colors = {
|
| 577 |
+
'success': 'linear-gradient(135deg, #10b981, #059669)',
|
| 578 |
+
'error': 'linear-gradient(135deg, #ef4444, #dc2626)',
|
| 579 |
+
'warning': 'linear-gradient(135deg, #f59e0b, #d97706)',
|
| 580 |
+
'info': 'linear-gradient(135deg, #3b82f6, #2563eb)'
|
| 581 |
+
};
|
| 582 |
+
return colors[type] || colors.info;
|
| 583 |
+
}
|
| 584 |
+
|
| 585 |
function setButtonLoading(button, loading = true) {
|
| 586 |
if (loading) {
|
| 587 |
button.disabled = true;
|
|
|
|
| 609 |
setButtonLoading(btn, true);
|
| 610 |
|
| 611 |
try {
|
| 612 |
+
// 解析输入的ID
|
| 613 |
+
const ids = book_ids.split(/[\s,;]+/).filter(id => id);
|
| 614 |
+
|
| 615 |
+
// 先将所有ID加入队列
|
| 616 |
+
for (const bid of ids) {
|
| 617 |
+
await fetch(`/enqueue?book_id=${encodeURIComponent(bid)}`);
|
| 618 |
+
}
|
| 619 |
+
|
| 620 |
+
// 清空输入框
|
| 621 |
+
document.getElementById('book_ids').value = '';
|
| 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) {
|
| 643 |
+
setButtonLoading(btn, false);
|
| 644 |
+
showNotification('添加任务失败:' + error.message, 'error');
|
| 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;
|
|
|
|
| 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 |
+
|
| 799 |
// 刷新队列和已下载列表
|
| 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 => {
|
| 807 |
+
if (task.name) {
|
| 808 |
+
NAMES[task.book_id] = task.name;
|
| 809 |
+
}
|
| 810 |
+
});
|
| 811 |
+
|
| 812 |
if (activeTasks.length === 0) {
|
| 813 |
container.innerHTML = '<div class="list-item"><span style="color: #a0aec0;"><i class="fas fa-check"></i> 暂无下载任务</span></div>';
|
| 814 |
return;
|
| 815 |
}
|
| 816 |
|
| 817 |
container.innerHTML = '';
|
| 818 |
+
activeTasks.forEach((task, index) => {
|
| 819 |
const div = document.createElement('div');
|
| 820 |
div.className = 'list-item fade-in';
|
| 821 |
+
div.setAttribute('data-book-id', task.book_id);
|
| 822 |
|
| 823 |
const statusClass = {
|
| 824 |
'queued': 'status-queued',
|
|
|
|
| 833 |
}[task.status] || 'fas fa-clock';
|
| 834 |
|
| 835 |
const statusText = {
|
| 836 |
+
'queued': index === 0 ? '即将开始' : '排队中',
|
| 837 |
'in-progress': '下载中',
|
| 838 |
'error': '失败'
|
| 839 |
}[task.status] || '未知';
|
| 840 |
|
| 841 |
+
// 如果是当前正在下载的任务,添加特殊标识
|
| 842 |
+
const isCurrentDownload = task.book_id === currentDownloadId;
|
| 843 |
+
const extraClass = isCurrentDownload ? ' current-download' : '';
|
| 844 |
+
|
| 845 |
div.innerHTML = `
|
| 846 |
<span><i class="fas fa-book"></i> ${task.name || task.book_id}</span>
|
| 847 |
+
<span class="status-badge ${statusClass}${extraClass}">
|
| 848 |
<i class="${statusIcon}"></i> ${statusText}
|
| 849 |
</span>
|
| 850 |
`;
|
|
|
|
| 852 |
});
|
| 853 |
}).catch(error => {
|
| 854 |
console.error('获取任务列表失败:', error);
|
| 855 |
+
const container = document.getElementById('taskList');
|
| 856 |
+
container.innerHTML = '<div class="list-item"><span style="color: #ef4444;"><i class="fas fa-exclamation-triangle"></i> 获取任务列表失败</span></div>';
|
| 857 |
});
|
| 858 |
}
|
| 859 |
|
|
|
|
| 973 |
}
|
| 974 |
}, 10000);
|
| 975 |
|
| 976 |
+
// 自动刷新队列 - 更频繁的刷新以保持同步
|
| 977 |
+
setInterval(() => {
|
| 978 |
+
fetchTasks();
|
| 979 |
+
// 如果有下载任务在进行,检查是否需要启动下一个
|
| 980 |
+
if (!isDownloading) {
|
| 981 |
+
fetch('/tasks').then(res => res.json()).then(tasks => {
|
| 982 |
+
const queuedTasks = tasks.filter(t => t.status === 'queued');
|
| 983 |
+
const inProgressTasks = tasks.filter(t => t.status === 'in-progress');
|
| 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 |
// 添加一些交互效果
|
| 994 |
document.querySelectorAll('.card').forEach(card => {
|
|
|
|
| 999 |
card.style.transform = 'translateY(0)';
|
| 1000 |
});
|
| 1001 |
});
|
| 1002 |
+
|
| 1003 |
+
// 添加键盘快捷键
|
| 1004 |
+
document.getElementById('book_ids').addEventListener('keypress', (e) => {
|
| 1005 |
+
if (e.key === 'Enter' && !e.shiftKey) {
|
| 1006 |
+
e.preventDefault();
|
| 1007 |
+
document.getElementById('downloadBtn').click();
|
| 1008 |
+
}
|
| 1009 |
+
});
|
| 1010 |
});
|
| 1011 |
</script>
|
| 1012 |
</body>
|
|
|
|
| 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())
|
| 1032 |
+
if name:
|
| 1033 |
+
NAMES[book_id] = name
|
| 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"}
|
|
|
|
| 1070 |
ids = [bid.strip() for bid in re.split(r'[\s,;]+', book_ids) if bid.strip()]
|
| 1071 |
if not ids:
|
| 1072 |
raise HTTPException(status_code=400, detail="请提供至少一个 book_id")
|
| 1073 |
+
|
| 1074 |
# 在临时目录创建下载文件夹
|
| 1075 |
save_path = tempfile.mkdtemp(prefix="tomato_")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1076 |
|
| 1077 |
+
try:
|
| 1078 |
+
# 调用下载核心 - 支持批量ID
|
| 1079 |
+
for bid in ids:
|
| 1080 |
+
try:
|
| 1081 |
+
# 更新状态为下载中
|
| 1082 |
+
STATUS[bid] = "in-progress"
|
| 1083 |
+
|
| 1084 |
+
# 获取书籍信息
|
| 1085 |
+
name, _, _ = get_book_info(bid, get_headers())
|
| 1086 |
+
if not name:
|
| 1087 |
+
name = f"小说_{bid}"
|
| 1088 |
+
NAMES[bid] = name
|
| 1089 |
|
| 1090 |
+
# 执行下载
|
| 1091 |
+
fetch_api_endpoints_from_server()
|
| 1092 |
+
Run(bid, save_path)
|
| 1093 |
+
|
| 1094 |
+
# 检查文件是否生成
|
| 1095 |
+
expected_filename = f"{name}.txt"
|
| 1096 |
+
expected_path = os.path.join(save_path, expected_filename)
|
| 1097 |
+
|
| 1098 |
+
if os.path.exists(expected_path):
|
| 1099 |
+
# 复制到共享目录
|
| 1100 |
+
dest_dir = os.path.join(DOWNLOAD_ROOT, bid)
|
| 1101 |
+
os.makedirs(dest_dir, exist_ok=True)
|
| 1102 |
+
dst = os.path.join(dest_dir, expected_filename)
|
| 1103 |
+
shutil.copy(expected_path, dst)
|
| 1104 |
+
|
| 1105 |
+
# 更新状态
|
| 1106 |
+
FILE_PATHS[bid] = f"/files/{bid}/{expected_filename}"
|
| 1107 |
+
STATUS[bid] = "done"
|
| 1108 |
+
else:
|
| 1109 |
+
STATUS[bid] = "error"
|
| 1110 |
+
raise Exception(f"文件未生成: {expected_filename}")
|
| 1111 |
+
|
| 1112 |
+
except Exception as e:
|
| 1113 |
+
STATUS[bid] = "error"
|
| 1114 |
+
print(f"下载 {bid} 失败: {str(e)}")
|
| 1115 |
+
raise HTTPException(status_code=500, detail=f"下载 {bid} 失败: {str(e)}")
|
| 1116 |
+
|
| 1117 |
+
# 收集生成的 txt 文件
|
| 1118 |
+
txt_files = glob.glob(os.path.join(save_path, "*.txt"))
|
| 1119 |
+
if not txt_files:
|
| 1120 |
+
raise HTTPException(status_code=404, detail="未生成txt文件,下载失败")
|
| 1121 |
+
|
| 1122 |
+
# 如果只有一个文件,直接返回
|
| 1123 |
+
if len(txt_files) == 1:
|
| 1124 |
+
file_path = txt_files[0]
|
| 1125 |
+
filename = os.path.basename(file_path)
|
| 1126 |
+
return FileResponse(
|
| 1127 |
+
path=file_path,
|
| 1128 |
+
filename=filename,
|
| 1129 |
+
media_type="text/plain; charset=utf-8",
|
| 1130 |
+
headers={"Content-Disposition": f"attachment; filename*=UTF-8''{filename}"}
|
| 1131 |
+
)
|
| 1132 |
+
|
| 1133 |
+
# 多文件时打包成 ZIP 并返回
|
| 1134 |
+
zip_path = os.path.join(save_path, "novels.zip")
|
| 1135 |
+
with zipfile.ZipFile(zip_path, "w", zipfile.ZIP_DEFLATED) as zf:
|
| 1136 |
+
for fpath in txt_files:
|
| 1137 |
+
zf.write(fpath, arcname=os.path.basename(fpath))
|
| 1138 |
+
|
| 1139 |
+
return FileResponse(
|
| 1140 |
+
path=zip_path,
|
| 1141 |
+
filename="novels.zip",
|
| 1142 |
+
media_type="application/zip",
|
| 1143 |
+
headers={"Content-Disposition": "attachment; filename=novels.zip"}
|
| 1144 |
+
)
|
| 1145 |
+
|
| 1146 |
+
except Exception as e:
|
| 1147 |
+
# 清理临时文件
|
| 1148 |
try:
|
| 1149 |
+
shutil.rmtree(save_path)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1150 |
except:
|
| 1151 |
pass
|
| 1152 |
+
raise
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1153 |
|