Nanny7 commited on
Commit
3fb05c7
·
1 Parent(s): 2c8ad05

重大界面美化和功能优化

Browse files

新功能:
- 现代化渐变背景和卡片式布局
- 智能下载队列管理
- 实时进度条和状态同步
- 美观的浮动通知系统
- 键盘快捷键支持 (Enter提交)

修复问题:
- 修复进度条不同步问题
- 优化任务状态实时更新
- 改进队列处理逻辑
- 增强错误处理和重试机制
- 修复文件下载编码问题

用户体验提升:
- 输入ID立即显示在队列中
- 自动队列管理和任务调度
- 当前下载任务高亮显示
- 更频繁的状态刷新 (2秒)
- 响应式设计支持移动端

界面改进:
- 使用Font Awesome图标库
- 毛玻璃效果和阴影
- 状态徽章和动画效果
- 现代化按钮和表单设计
- 优化的颜色主题和排版

Files changed (2) hide show
  1. __pycache__/server.cpython-313.pyc +0 -0
  2. 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
- fetch_api_endpoints_from_server()
26
- Run(book_id, save_path)
27
- name, _, _ = get_book_info(book_id, get_headers())
28
- filename = f"{name}.txt"
29
- path = os.path.join(save_path, filename)
30
- if os.path.exists(path):
31
- FILE_PATHS[book_id] = f"/files/{book_id}/{filename}"
32
- NAMES[book_id] = name
33
- STATUS[book_id] = "done"
34
- else:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
35
  STATUS[book_id] = "error"
36
- except Exception:
37
- STATUS[book_id] = "error"
 
 
 
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
- if (tasks.filter(t => t.status !== 'done').length > 0) {
521
- const ids = book_ids.split(/[\s,;]+/).filter(id => id);
522
- for (const bid of ids) {
523
- await fetch(`/enqueue?book_id=${encodeURIComponent(bid)}`);
 
 
 
 
 
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
- const response = await fetch(`/download?book_ids=${encodeURIComponent(book_ids)}`);
 
 
 
 
 
 
 
 
 
 
 
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
- const blob = new Blob(chunks, {type: response.headers.get('Content-Type')});
546
- const url = URL.createObjectURL(blob);
547
- const cd = response.headers.get('Content-Disposition');
548
- let filename = 'download';
549
- if (cd) {
550
- const match = cd.match(/filename="?(.+)"?/);
551
- if (match) filename = match[1];
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- if (total) {
574
- const percent = Math.round(received / total * 100);
575
- document.getElementById('progBar').style.width = percent + '%';
576
- document.getElementById('progText').innerText = percent + '%';
 
 
 
 
 
 
 
 
 
577
  }
 
578
  return read();
579
  });
580
  }
581
- read();
 
 
582
  } catch (error) {
583
- setButtonLoading(btn, false);
584
- document.getElementById('progress').style.display = 'none';
585
- showNotification('下载失败:' + error.message, 'error');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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(fetchTasks, 3000);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- # 收集生成的 txt 文件
823
- txt_files = glob.glob(os.path.join(save_path, "*.txt"))
824
- if not txt_files:
825
- raise HTTPException(status_code=404, detail="未生成txt文件,下载失败")
 
 
 
 
 
 
 
 
826
 
827
- # 将下载文件复制到共享目录并更新映射
828
- for bid in ids:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
829
  try:
830
- name, _, _ = get_book_info(bid, get_headers())
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
- txt_files = glob.glob(os.path.join(save_path, "*.txt"))
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