mirror of
https://github.com/hanxi/xiaomusic.git
synced 2026-05-10 00:44:18 +08:00
feat: tailwind主题添加下载进度以及下载控制按钮 (#830)
This commit is contained in:
@@ -53,6 +53,154 @@ from xiaomusic.utils.system_utils import try_add_access_control_param
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
# 下载任务状态管理
|
||||
download_tasks = {} # {task_id: {"total": 总数, "completed": 已完成数, "status": "pending|downloading|paused|stopped|completed|failed", "current_song": "当前歌曲名", "process": 进程对象}}
|
||||
|
||||
|
||||
async def monitor_download_progress(task_id: str, dirname: str):
|
||||
"""后台监控下载进度,通过统计mp3文件数量来更新
|
||||
|
||||
Args:
|
||||
task_id: 任务ID
|
||||
dirname: 下载目录名
|
||||
"""
|
||||
try:
|
||||
dir_path = safe_join_path(config.download_path, dirname)
|
||||
last_count = 0
|
||||
|
||||
log.info(f"Monitor task started for {task_id}, watching directory: {dir_path}")
|
||||
|
||||
while True:
|
||||
await asyncio.sleep(1) # 每1秒检查一次
|
||||
|
||||
# 检查任务是否还在进行中
|
||||
if task_id not in download_tasks:
|
||||
log.info(f"Task {task_id} not found, stopping monitor")
|
||||
break
|
||||
|
||||
task = download_tasks[task_id]
|
||||
# 允许在 paused 状态下也继续监控(为继续做准备)
|
||||
if task["status"] not in ["downloading", "pending", "paused"]:
|
||||
log.info(f"Task {task_id} status is {task['status']}, stopping monitor")
|
||||
break
|
||||
|
||||
# 统计已下载的mp3文件数量
|
||||
if os.path.exists(dir_path):
|
||||
try:
|
||||
# 只统计.mp3文件,排除.m4a等临时文件
|
||||
mp3_files = [f for f in os.listdir(dir_path) if f.endswith('.mp3')]
|
||||
current_count = len(mp3_files)
|
||||
|
||||
# 只有当数量变化时才更新
|
||||
if current_count > last_count:
|
||||
download_tasks[task_id]["completed"] = current_count
|
||||
download_tasks[task_id]["current_song"] = f"已下载 {current_count} 首歌曲..."
|
||||
last_count = current_count
|
||||
log.info(f"Progress updated: {current_count} mp3 files downloaded")
|
||||
except Exception as e:
|
||||
log.warning(f"Monitor progress error: {e}")
|
||||
except asyncio.CancelledError:
|
||||
log.info(f"Monitor task cancelled for {task_id}")
|
||||
except Exception as e:
|
||||
log.exception(f"Monitor task error: {e}")
|
||||
|
||||
|
||||
async def monitor_download_progress_with_output(task_id: str, dirname: str, process):
|
||||
"""后台监控下载进度,通过解析yt-dlp输出来获取总数和进度
|
||||
|
||||
Args:
|
||||
task_id: 任务ID
|
||||
dirname: 下载目录名
|
||||
process: yt-dlp进程对象
|
||||
"""
|
||||
try:
|
||||
dir_path = safe_join_path(config.download_path, dirname)
|
||||
last_count = 0
|
||||
import re
|
||||
|
||||
log.info(f"Monitor task started for {task_id}, watching stderr")
|
||||
|
||||
# 读取进程输出,解析总数和当前进度(yt-dlp输出到stderr)
|
||||
if process.stderr:
|
||||
line_count = 0
|
||||
while True:
|
||||
await asyncio.sleep(0.5) # 每0.5秒检查一次,更实时
|
||||
|
||||
# 检查任务状态
|
||||
if task_id not in download_tasks:
|
||||
log.info(f"Task {task_id} not found, stopping monitor")
|
||||
break
|
||||
|
||||
task = download_tasks[task_id]
|
||||
if task["status"] not in ["downloading", "pending", "paused"]:
|
||||
log.info(f"Task {task_id} status is {task['status']}, stopping monitor")
|
||||
break
|
||||
|
||||
# 尝试从stderr读取一行
|
||||
try:
|
||||
line = await asyncio.wait_for(process.stderr.readline(), timeout=0.3)
|
||||
if not line:
|
||||
# 没有更多输出,检查进程是否结束
|
||||
if process.returncode is not None:
|
||||
log.info(f"Process ended with code {process.returncode}")
|
||||
break
|
||||
continue
|
||||
|
||||
line_count += 1
|
||||
line_str = line.decode('utf-8', errors='ignore').strip()
|
||||
|
||||
# 实时输出到日志,让用户看到进度
|
||||
if line_str:
|
||||
log.info(f"[yt-dlp] {line_str}")
|
||||
|
||||
# 每10行输出一次计数日志,避免太多
|
||||
if line_count % 10 == 0:
|
||||
log.debug(f"Read {line_count} lines from stderr")
|
||||
|
||||
# 解析 "Downloading X of Y" 或 "Downloading item X of Y"
|
||||
match = re.search(r'(?:Downloading|item)\s+(\d+)\s+of\s+(\d+)', line_str)
|
||||
if match:
|
||||
current = int(match.group(1))
|
||||
total = int(match.group(2))
|
||||
|
||||
# 更新任务状态
|
||||
download_tasks[task_id]["completed"] = current
|
||||
download_tasks[task_id]["total"] = total
|
||||
download_tasks[task_id]["current_song"] = f"正在下载第 {current}/{total} 首..."
|
||||
log.info(f"Parsed progress: {current}/{total}")
|
||||
|
||||
# 备用方案:统计已完成的mp3文件数(排除临时文件)
|
||||
if os.path.exists(dir_path):
|
||||
try:
|
||||
# 只统计.mp3文件,排除.m4a等临时文件
|
||||
mp3_files = [f for f in os.listdir(dir_path) if f.endswith('.mp3')]
|
||||
file_count = len(mp3_files)
|
||||
|
||||
if file_count > last_count:
|
||||
# 如果还没有从输出中解析到total,使用文件数
|
||||
if download_tasks[task_id]["total"] == 0:
|
||||
download_tasks[task_id]["completed"] = file_count
|
||||
download_tasks[task_id]["current_song"] = f"已下载 {file_count} 首歌曲..."
|
||||
last_count = file_count
|
||||
log.info(f"File count updated: {file_count} mp3 files")
|
||||
except Exception as e:
|
||||
log.warning(f"Count files error: {e}")
|
||||
|
||||
except asyncio.TimeoutError:
|
||||
# 读取超时,继续循环
|
||||
continue
|
||||
except Exception as e:
|
||||
log.warning(f"Read output error: {e}")
|
||||
else:
|
||||
log.warning(f"Process stderr is None, cannot monitor output")
|
||||
# 回退到简单的文件统计
|
||||
await monitor_download_progress(task_id, dirname)
|
||||
|
||||
except asyncio.CancelledError:
|
||||
log.info(f"Monitor with output task cancelled for {task_id}")
|
||||
except Exception as e:
|
||||
log.exception(f"Monitor with output task error: {e}")
|
||||
|
||||
|
||||
def _process_m3u8_content(m3u8_content: str, base_url: str, is_radio: bool) -> str:
|
||||
"""处理 m3u8 文件内容,将资源 URL 替换为代理 URL
|
||||
@@ -125,44 +273,160 @@ async def downloadjson(data: UrlInfo, Verifcation=Depends(verification)):
|
||||
@router.post("/downloadplaylist")
|
||||
async def downloadplaylist(data: DownloadPlayList, Verifcation=Depends(verification)):
|
||||
"""下载歌单"""
|
||||
task_id = str(uuid.uuid4())
|
||||
try:
|
||||
# 初始化任务状态
|
||||
download_tasks[task_id] = {
|
||||
"total": 0,
|
||||
"completed": 0,
|
||||
"status": "pending",
|
||||
"current_song": "",
|
||||
"dirname": data.dirname,
|
||||
"url": data.url, # 保存URL以便重新开始
|
||||
"task_type": "playlist", # 标记任务类型
|
||||
"created_at": asyncio.get_event_loop().time()
|
||||
}
|
||||
|
||||
bili_fav_list = await check_bili_fav_list(data.url)
|
||||
download_proc_list = []
|
||||
if bili_fav_list:
|
||||
for bvid, title in bili_fav_list.items():
|
||||
total_songs = len(bili_fav_list)
|
||||
download_tasks[task_id]["total"] = total_songs
|
||||
download_tasks[task_id]["status"] = "downloading"
|
||||
|
||||
for idx, (bvid, title) in enumerate(bili_fav_list.items(), 1):
|
||||
bvurl = f"https://www.bilibili.com/video/{bvid}"
|
||||
download_tasks[task_id]["current_song"] = f"{title} ({idx}/{total_songs})"
|
||||
download_proc_list[title] = await download_one_music(
|
||||
config, bvurl, os.path.join(data.dirname, title)
|
||||
)
|
||||
|
||||
for title, download_proc_sigle in download_proc_list.items():
|
||||
exit_code = await download_proc_sigle.wait()
|
||||
log.info(f"Download completed {title} with exit code {exit_code}")
|
||||
download_tasks[task_id]["completed"] += 1
|
||||
|
||||
dir_path = safe_join_path(config.download_path, data.dirname)
|
||||
log.debug(f"Download dir_path: {dir_path}")
|
||||
# 可能只是部分失败,都需要整理下载目录
|
||||
remove_common_prefix(dir_path)
|
||||
chmoddir(dir_path)
|
||||
return {"ret": "OK"}
|
||||
|
||||
download_tasks[task_id]["status"] = "completed"
|
||||
download_tasks[task_id]["current_song"] = ""
|
||||
return {"ret": "OK", "task_id": task_id}
|
||||
else:
|
||||
download_tasks[task_id]["status"] = "downloading"
|
||||
download_tasks[task_id]["current_song"] = "正在获取歌单信息..."
|
||||
download_tasks[task_id]["total"] = 0
|
||||
download_tasks[task_id]["completed"] = 0
|
||||
log.info(f"Starting download playlist: {data.url}")
|
||||
|
||||
# 使用 yt-dlp Python API 快速获取总数
|
||||
try:
|
||||
import yt_dlp
|
||||
|
||||
ydl_opts = {
|
||||
'quiet': True,
|
||||
'no_warnings': True,
|
||||
'extract_flat': 'in_playlist', # 只提取播放列表结构,不下载
|
||||
'skip_download': True,
|
||||
}
|
||||
|
||||
log.info(f"Getting playlist count via yt-dlp API...")
|
||||
with yt_dlp.YoutubeDL(ydl_opts) as ydl:
|
||||
info = ydl.extract_info(data.url, download=False)
|
||||
|
||||
if info and 'entries' in info:
|
||||
entries = list(info['entries'])
|
||||
total_count = len(entries)
|
||||
if total_count > 0:
|
||||
download_tasks[task_id]["total"] = total_count
|
||||
log.info(f"✓ Playlist has {total_count} items")
|
||||
else:
|
||||
log.warning("Empty playlist")
|
||||
elif info and 'playlist_count' in info:
|
||||
total_count = info['playlist_count']
|
||||
if total_count > 0:
|
||||
download_tasks[task_id]["total"] = total_count
|
||||
log.info(f"✓ Playlist has {total_count} items")
|
||||
else:
|
||||
log.warning("Could not determine playlist size")
|
||||
except Exception as e:
|
||||
log.warning(f"Error getting playlist count via API: {e} (continuing anyway)")
|
||||
|
||||
# 确保下载目录存在
|
||||
dir_path = safe_join_path(config.download_path, data.dirname)
|
||||
os.makedirs(dir_path, exist_ok=True)
|
||||
log.info(f"Download directory ensured: {dir_path}")
|
||||
|
||||
download_tasks[task_id]["current_song"] = "正在解析歌单..."
|
||||
download_proc = await download_playlist(config, data.url, data.dirname)
|
||||
# 保存进程对象以便后续控制
|
||||
download_tasks[task_id]["process"] = download_proc
|
||||
log.info(f"Download process started, PID: {download_proc.pid}")
|
||||
|
||||
async def check_download_proc():
|
||||
# 等待子进程完成
|
||||
exit_code = await download_proc.wait()
|
||||
log.info(f"Download completed with exit code {exit_code}")
|
||||
# 启动后台监控任务,通过文件统计来更新进度
|
||||
log.info(f"Starting monitor task for {task_id}")
|
||||
monitor_task = asyncio.create_task(monitor_download_progress(task_id, data.dirname))
|
||||
|
||||
try:
|
||||
# 等待子进程完成
|
||||
log.info(f"Waiting for download process to complete...")
|
||||
exit_code = await download_proc.wait()
|
||||
log.info(f"Download completed with exit code {exit_code}")
|
||||
|
||||
dir_path = safe_join_path(config.download_path, data.dirname)
|
||||
log.debug(f"Download dir_path: {dir_path}")
|
||||
# 可能只是部分失败,都需要整理下载目录
|
||||
remove_common_prefix(dir_path)
|
||||
chmoddir(dir_path)
|
||||
# 停止监控任务
|
||||
monitor_task.cancel()
|
||||
try:
|
||||
await monitor_task
|
||||
except asyncio.CancelledError:
|
||||
pass
|
||||
|
||||
dir_path = safe_join_path(config.download_path, data.dirname)
|
||||
log.debug(f"Download dir_path: {dir_path}")
|
||||
# 可能只是部分失败,都需要整理下载目录
|
||||
remove_common_prefix(dir_path)
|
||||
chmoddir(dir_path)
|
||||
|
||||
# 检查是否已被手动停止
|
||||
if download_tasks[task_id]["status"] == "stopped":
|
||||
# 已经被停止,保持stopped状态
|
||||
log.info(f"Task was already stopped, keeping status")
|
||||
else:
|
||||
download_tasks[task_id]["status"] = "completed" if exit_code == 0 else "failed"
|
||||
download_tasks[task_id]["current_song"] = ""
|
||||
# 更新最终的完成数量
|
||||
if os.path.exists(dir_path):
|
||||
final_count = len([f for f in os.listdir(dir_path) if f.endswith('.mp3')])
|
||||
download_tasks[task_id]["completed"] = final_count
|
||||
download_tasks[task_id]["total"] = final_count
|
||||
except asyncio.CancelledError:
|
||||
log.info(f"Download task cancelled: {task_id}")
|
||||
monitor_task.cancel()
|
||||
download_tasks[task_id]["status"] = "stopped"
|
||||
download_tasks[task_id]["current_song"] = "已停止"
|
||||
except Exception as e:
|
||||
# 检查是否是因为被停止而导致的异常
|
||||
if download_tasks.get(task_id, {}).get("status") == "stopped":
|
||||
log.info(f"Task was stopped, exception is expected: {e}")
|
||||
# 已经被停止,保持stopped状态,不覆盖
|
||||
else:
|
||||
log.exception(f"Download task error: {e}")
|
||||
monitor_task.cancel()
|
||||
download_tasks[task_id]["status"] = "failed"
|
||||
download_tasks[task_id]["current_song"] = str(e)
|
||||
|
||||
asyncio.create_task(check_download_proc())
|
||||
return {"ret": "OK"}
|
||||
return {"ret": "OK", "task_id": task_id}
|
||||
except Exception as e:
|
||||
log.exception(f"Execption {e}")
|
||||
if task_id in download_tasks:
|
||||
download_tasks[task_id]["status"] = "failed"
|
||||
download_tasks[task_id]["current_song"] = str(e)
|
||||
|
||||
return {"ret": "Failed download"}
|
||||
return {"ret": "Failed download", "task_id": task_id}
|
||||
|
||||
|
||||
@router.post("/downloadonemusic")
|
||||
@@ -175,7 +439,22 @@ async def downloadonemusic(data: DownloadOneMusic, Verifcation=Depends(verificat
|
||||
data.dirname: 子目录名(可选,兼容字段),相对于 music 根目录
|
||||
data.playlist_name: 下载成功后要关联的歌单名(可选)
|
||||
"""
|
||||
task_id = str(uuid.uuid4())
|
||||
try:
|
||||
# 初始化任务状态
|
||||
download_tasks[task_id] = {
|
||||
"total": 1,
|
||||
"completed": 0,
|
||||
"status": "downloading",
|
||||
"current_song": data.name or "正在下载...",
|
||||
"url": data.url, # 保存URL以便重新开始
|
||||
"name": data.name, # 保存文件名
|
||||
"dirname": data.dirname or "", # 保存目录名
|
||||
"playlist_name": data.playlist_name or "", # 保存歌单名
|
||||
"task_type": "single", # 标记任务类型
|
||||
"created_at": asyncio.get_event_loop().time()
|
||||
}
|
||||
|
||||
pre_all_music_names = set(xiaomusic.music_library.all_music.keys())
|
||||
playlist_name = (data.playlist_name or "").strip()
|
||||
|
||||
@@ -190,72 +469,377 @@ async def downloadonemusic(data: DownloadOneMusic, Verifcation=Depends(verificat
|
||||
data.name,
|
||||
download_root=download_root,
|
||||
)
|
||||
# 保存进程对象以便后续控制
|
||||
download_tasks[task_id]["process"] = download_proc
|
||||
|
||||
async def check_download_proc():
|
||||
# 等待子进程完成
|
||||
exit_code = await download_proc.wait()
|
||||
log.info(f"Download completed with exit code {exit_code}")
|
||||
|
||||
if exit_code != 0:
|
||||
return
|
||||
|
||||
# 对于单曲下载,设置初始状态
|
||||
download_tasks[task_id]["completed"] = 0
|
||||
download_tasks[task_id]["total"] = 1
|
||||
|
||||
try:
|
||||
chmoddir(download_root)
|
||||
except Exception:
|
||||
pass
|
||||
# 等待子进程完成
|
||||
exit_code = await download_proc.wait()
|
||||
log.info(f"Download completed with exit code {exit_code}")
|
||||
|
||||
try:
|
||||
xiaomusic.music_library.gen_all_music_list()
|
||||
xiaomusic.update_all_playlist()
|
||||
if exit_code != 0:
|
||||
download_tasks[task_id]["status"] = "failed"
|
||||
download_tasks[task_id]["current_song"] = "下载失败"
|
||||
return
|
||||
|
||||
# 下载成功,更新为完成
|
||||
download_tasks[task_id]["completed"] = 1
|
||||
download_tasks[task_id]["current_song"] = "下载完成"
|
||||
|
||||
try:
|
||||
chmoddir(download_root)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
try:
|
||||
xiaomusic.music_library.gen_all_music_list()
|
||||
xiaomusic.update_all_playlist()
|
||||
except Exception as e:
|
||||
log.exception(f"refresh music list failed after download: {e}")
|
||||
download_tasks[task_id]["status"] = "failed"
|
||||
return
|
||||
|
||||
if not playlist_name:
|
||||
download_tasks[task_id]["status"] = "completed"
|
||||
download_tasks[task_id]["completed"] = 1
|
||||
download_tasks[task_id]["current_song"] = ""
|
||||
return
|
||||
|
||||
resolved_music_name = ""
|
||||
if data.name and data.name in xiaomusic.music_library.all_music:
|
||||
resolved_music_name = data.name
|
||||
else:
|
||||
new_music_names = [
|
||||
name
|
||||
for name in xiaomusic.music_library.all_music.keys()
|
||||
if name not in pre_all_music_names
|
||||
]
|
||||
if len(new_music_names) == 1:
|
||||
resolved_music_name = new_music_names[0]
|
||||
elif data.name:
|
||||
for name in new_music_names:
|
||||
if name.startswith(data.name):
|
||||
resolved_music_name = name
|
||||
break
|
||||
|
||||
if not resolved_music_name:
|
||||
log.warning(
|
||||
f"download succeeded but failed to resolve music name for playlist: {playlist_name}"
|
||||
)
|
||||
download_tasks[task_id]["status"] = "completed"
|
||||
download_tasks[task_id]["completed"] = 1
|
||||
download_tasks[task_id]["current_song"] = ""
|
||||
return
|
||||
|
||||
added = xiaomusic.music_library.play_list_add_music(
|
||||
playlist_name, [resolved_music_name]
|
||||
)
|
||||
if added:
|
||||
xiaomusic.update_all_playlist()
|
||||
log.info(
|
||||
f"downloadonemusic auto add success: {resolved_music_name} -> {playlist_name}"
|
||||
)
|
||||
else:
|
||||
log.warning(
|
||||
f"downloadonemusic auto add failed: {resolved_music_name} -> {playlist_name}"
|
||||
)
|
||||
|
||||
|
||||
download_tasks[task_id]["status"] = "completed"
|
||||
download_tasks[task_id]["completed"] = 1
|
||||
download_tasks[task_id]["current_song"] = ""
|
||||
except asyncio.CancelledError:
|
||||
log.info(f"Download task cancelled: {task_id}")
|
||||
download_tasks[task_id]["status"] = "stopped"
|
||||
download_tasks[task_id]["current_song"] = "已停止"
|
||||
except Exception as e:
|
||||
log.exception(f"refresh music list failed after download: {e}")
|
||||
return
|
||||
|
||||
if not playlist_name:
|
||||
return
|
||||
|
||||
resolved_music_name = ""
|
||||
if data.name and data.name in xiaomusic.music_library.all_music:
|
||||
resolved_music_name = data.name
|
||||
else:
|
||||
new_music_names = [
|
||||
name
|
||||
for name in xiaomusic.music_library.all_music.keys()
|
||||
if name not in pre_all_music_names
|
||||
]
|
||||
if len(new_music_names) == 1:
|
||||
resolved_music_name = new_music_names[0]
|
||||
elif data.name:
|
||||
for name in new_music_names:
|
||||
if name.startswith(data.name):
|
||||
resolved_music_name = name
|
||||
break
|
||||
|
||||
if not resolved_music_name:
|
||||
log.warning(
|
||||
f"download succeeded but failed to resolve music name for playlist: {playlist_name}"
|
||||
)
|
||||
return
|
||||
|
||||
added = xiaomusic.music_library.play_list_add_music(
|
||||
playlist_name, [resolved_music_name]
|
||||
)
|
||||
if added:
|
||||
xiaomusic.update_all_playlist()
|
||||
log.info(
|
||||
f"downloadonemusic auto add success: {resolved_music_name} -> {playlist_name}"
|
||||
)
|
||||
else:
|
||||
log.warning(
|
||||
f"downloadonemusic auto add failed: {resolved_music_name} -> {playlist_name}"
|
||||
)
|
||||
log.exception(f"Download task error: {e}")
|
||||
download_tasks[task_id]["status"] = "failed"
|
||||
download_tasks[task_id]["current_song"] = str(e)
|
||||
|
||||
asyncio.create_task(check_download_proc())
|
||||
return {"ret": "OK"}
|
||||
return {"ret": "OK", "task_id": task_id}
|
||||
except Exception as e:
|
||||
log.exception(f"Execption {e}")
|
||||
if task_id in download_tasks:
|
||||
download_tasks[task_id]["status"] = "failed"
|
||||
download_tasks[task_id]["current_song"] = str(e)
|
||||
|
||||
return {"ret": "Failed download"}
|
||||
return {"ret": "Failed download", "task_id": task_id}
|
||||
|
||||
|
||||
@router.get("/download_progress")
|
||||
async def get_download_progress(task_id: str = ""):
|
||||
"""获取下载任务进度
|
||||
|
||||
Args:
|
||||
task_id: 任务ID,如果为空则返回所有任务
|
||||
|
||||
Returns:
|
||||
dict: 下载任务进度信息
|
||||
"""
|
||||
if task_id:
|
||||
# 返回指定任务
|
||||
if task_id in download_tasks:
|
||||
task = download_tasks[task_id]
|
||||
progress = 0
|
||||
if task["total"] > 0:
|
||||
progress = int((task["completed"] / task["total"]) * 100)
|
||||
|
||||
return {
|
||||
"ret": "OK",
|
||||
"task_id": task_id,
|
||||
"total": task["total"],
|
||||
"completed": task["completed"],
|
||||
"progress": progress,
|
||||
"status": task["status"],
|
||||
"current_song": task["current_song"]
|
||||
}
|
||||
else:
|
||||
return {"ret": "Not found", "task_id": task_id}
|
||||
else:
|
||||
# 返回所有活跃任务
|
||||
active_tasks = {}
|
||||
current_time = asyncio.get_event_loop().time()
|
||||
|
||||
for tid, task in list(download_tasks.items()):
|
||||
# 只返回未完成或最近5分钟完成的任务
|
||||
task_age = current_time - task.get("created_at", 0)
|
||||
if task["status"] in ["pending", "downloading"] or task_age < 300:
|
||||
progress = 0
|
||||
if task["total"] > 0:
|
||||
progress = int((task["completed"] / task["total"]) * 100)
|
||||
|
||||
active_tasks[tid] = {
|
||||
"total": task["total"],
|
||||
"completed": task["completed"],
|
||||
"progress": progress,
|
||||
"status": task["status"],
|
||||
"current_song": task["current_song"],
|
||||
"dirname": task.get("dirname", "")
|
||||
}
|
||||
|
||||
return {
|
||||
"ret": "OK",
|
||||
"tasks": active_tasks
|
||||
}
|
||||
|
||||
|
||||
@router.post("/clear_completed_tasks")
|
||||
async def clear_completed_tasks(Verifcation=Depends(verification)):
|
||||
"""清理已完成的下载任务记录"""
|
||||
cleared_count = 0
|
||||
current_time = asyncio.get_event_loop().time()
|
||||
|
||||
for task_id in list(download_tasks.keys()):
|
||||
task = download_tasks[task_id]
|
||||
task_age = current_time - task.get("created_at", 0)
|
||||
|
||||
# 清理已完成超过5分钟的任务
|
||||
if task["status"] in ["completed", "failed", "stopped"] and task_age >= 300:
|
||||
del download_tasks[task_id]
|
||||
cleared_count += 1
|
||||
|
||||
return {
|
||||
"ret": "OK",
|
||||
"cleared_count": cleared_count
|
||||
}
|
||||
|
||||
|
||||
@router.post("/pause_download")
|
||||
async def pause_download(task_id: str, Verifcation=Depends(verification)):
|
||||
"""暂停下载任务
|
||||
|
||||
Args:
|
||||
task_id: 任务ID
|
||||
|
||||
Returns:
|
||||
dict: 操作结果
|
||||
"""
|
||||
if task_id not in download_tasks:
|
||||
return {"ret": "Not found", "message": "任务不存在"}
|
||||
|
||||
task = download_tasks[task_id]
|
||||
|
||||
if task["status"] != "downloading":
|
||||
return {"ret": "Failed", "message": f"当前状态为{task['status']},无法暂停"}
|
||||
|
||||
try:
|
||||
# 暂停进程
|
||||
if "process" in task and task["process"]:
|
||||
import os
|
||||
import signal
|
||||
# Windows下使用CTRL_BREAK_EVENT,Unix下使用SIGSTOP
|
||||
if os.name == 'nt':
|
||||
task["process"].send_signal(signal.CTRL_BREAK_EVENT)
|
||||
else:
|
||||
task["process"].send_signal(signal.SIGSTOP)
|
||||
|
||||
task["status"] = "paused"
|
||||
task["current_song"] = "已暂停"
|
||||
|
||||
log.info(f"Download task paused: {task_id}")
|
||||
return {"ret": "OK", "message": "已暂停下载"}
|
||||
except Exception as e:
|
||||
log.exception(f"Pause download failed: {e}")
|
||||
return {"ret": "Failed", "message": str(e)}
|
||||
|
||||
|
||||
@router.post("/resume_download")
|
||||
async def resume_download(task_id: str, Verifcation=Depends(verification)):
|
||||
"""继续下载任务
|
||||
|
||||
Args:
|
||||
task_id: 任务ID
|
||||
|
||||
Returns:
|
||||
dict: 操作结果
|
||||
"""
|
||||
if task_id not in download_tasks:
|
||||
return {"ret": "Not found", "message": "任务不存在"}
|
||||
|
||||
task = download_tasks[task_id]
|
||||
|
||||
if task["status"] != "paused":
|
||||
return {"ret": "Failed", "message": f"当前状态为{task['status']},无法继续"}
|
||||
|
||||
try:
|
||||
# 恢复进程
|
||||
if "process" in task and task["process"]:
|
||||
import os
|
||||
import signal
|
||||
# Windows下不支持SIGCONT,需要重新创建进程,这里简化处理
|
||||
if os.name != 'nt':
|
||||
task["process"].send_signal(signal.SIGCONT)
|
||||
|
||||
task["status"] = "downloading"
|
||||
task["current_song"] = "继续下载中..."
|
||||
|
||||
log.info(f"Download task resumed: {task_id}")
|
||||
return {"ret": "OK", "message": "已继续下载"}
|
||||
except Exception as e:
|
||||
log.exception(f"Resume download failed: {e}")
|
||||
return {"ret": "Failed", "message": str(e)}
|
||||
|
||||
|
||||
@router.post("/stop_download")
|
||||
async def stop_download(task_id: str, Verifcation=Depends(verification)):
|
||||
"""停止下载任务
|
||||
|
||||
Args:
|
||||
task_id: 任务ID
|
||||
|
||||
Returns:
|
||||
dict: 操作结果
|
||||
"""
|
||||
if task_id not in download_tasks:
|
||||
return {"ret": "Not found", "message": "任务不存在"}
|
||||
|
||||
task = download_tasks[task_id]
|
||||
|
||||
if task["status"] in ["completed", "failed", "stopped"]:
|
||||
return {"ret": "Failed", "message": f"当前状态为{task['status']},无法停止"}
|
||||
|
||||
try:
|
||||
# 终止进程
|
||||
if "process" in task and task["process"]:
|
||||
try:
|
||||
task["process"].kill()
|
||||
await task["process"].wait()
|
||||
except Exception as e:
|
||||
log.warning(f"Kill process warning: {e}")
|
||||
|
||||
task["status"] = "stopped"
|
||||
task["current_song"] = "已停止"
|
||||
|
||||
log.info(f"Download task stopped: {task_id}")
|
||||
return {"ret": "OK", "message": "已停止下载"}
|
||||
except Exception as e:
|
||||
log.exception(f"Stop download failed: {e}")
|
||||
return {"ret": "Failed", "message": str(e)}
|
||||
|
||||
|
||||
@router.post("/delete_download_task")
|
||||
async def delete_download_task(task_id: str, Verifcation=Depends(verification)):
|
||||
"""删除下载任务记录(仅针对已停止/完成/失败的任务)
|
||||
|
||||
Args:
|
||||
task_id: 任务ID
|
||||
|
||||
Returns:
|
||||
dict: 操作结果
|
||||
"""
|
||||
if task_id not in download_tasks:
|
||||
return {"ret": "Not found", "message": "任务不存在"}
|
||||
|
||||
task = download_tasks[task_id]
|
||||
|
||||
# 只允许删除已停止、已完成或失败的任务
|
||||
if task["status"] in ["downloading", "paused", "pending"]:
|
||||
return {"ret": "Failed", "message": f"当前状态为{task['status']},无法删除。请先停止任务。"}
|
||||
|
||||
try:
|
||||
del download_tasks[task_id]
|
||||
log.info(f"Download task deleted: {task_id}")
|
||||
return {"ret": "OK", "message": "已删除任务记录"}
|
||||
except Exception as e:
|
||||
log.exception(f"Delete task failed: {e}")
|
||||
return {"ret": "Failed", "message": str(e)}
|
||||
|
||||
|
||||
@router.post("/restart_download")
|
||||
async def restart_download(task_id: str, Verifcation=Depends(verification)):
|
||||
"""重新开始下载任务(仅针对已停止的任务)
|
||||
|
||||
Args:
|
||||
task_id: 任务ID
|
||||
|
||||
Returns:
|
||||
dict: 操作结果,包含新的task_id
|
||||
"""
|
||||
if task_id not in download_tasks:
|
||||
return {"ret": "Not found", "message": "任务不存在"}
|
||||
|
||||
old_task = download_tasks[task_id]
|
||||
|
||||
# 只允许重新开始已停止的任务
|
||||
if old_task["status"] != "stopped":
|
||||
return {"ret": "Failed", "message": f"当前状态为{old_task['status']},无法重新开始。"}
|
||||
|
||||
try:
|
||||
task_type = old_task.get("task_type", "single")
|
||||
|
||||
if task_type == "playlist":
|
||||
# 歌单下载:重新发起歌单下载请求
|
||||
from xiaomusic.api.models import DownloadPlayList
|
||||
data = DownloadPlayList(
|
||||
dirname=old_task["dirname"],
|
||||
url=old_task["url"]
|
||||
)
|
||||
# 调用原有的下载函数
|
||||
result = await downloadplaylist(data, Verifcation)
|
||||
return result
|
||||
else:
|
||||
# 单曲下载:重新发起单曲下载请求
|
||||
from xiaomusic.api.models import DownloadOneMusic
|
||||
data = DownloadOneMusic(
|
||||
name=old_task.get("name", ""),
|
||||
url=old_task["url"],
|
||||
dirname=old_task.get("dirname", ""),
|
||||
playlist_name=old_task.get("playlist_name", "")
|
||||
)
|
||||
# 调用原有的下载函数
|
||||
result = await downloadonemusic(data, Verifcation)
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
log.exception(f"Restart download failed: {e}")
|
||||
return {"ret": "Failed", "message": str(e)}
|
||||
|
||||
|
||||
@router.post("/uploadytdlpcookie")
|
||||
|
||||
391
xiaomusic/static/tailwind/downloadtool.html
vendored
391
xiaomusic/static/tailwind/downloadtool.html
vendored
@@ -9,19 +9,6 @@
|
||||
<script src="./libs/tailwind.js"></script>
|
||||
<link rel="icon" href="./favicon.ico">
|
||||
|
||||
<!-- Google tag (gtag.js) -->
|
||||
<script async src="https://www.googletagmanager.com/gtag/js?id=G-Z09NC1K7ZW"></script>
|
||||
<script>
|
||||
window.dataLayer = window.dataLayer || [];
|
||||
function gtag() { dataLayer.push(arguments) };
|
||||
gtag('js', new Date());
|
||||
gtag('config', 'G-Z09NC1K7ZW');
|
||||
</script>
|
||||
|
||||
<!-- umami -->
|
||||
<script async defer src="https://umami.hanxi.cc/script.js"
|
||||
data-website-id="7bfb0890-4115-4260-8892-b391513e7e99"></script>
|
||||
|
||||
</head>
|
||||
|
||||
<body class="bg-gray-100 min-h-screen p-6">
|
||||
@@ -80,6 +67,17 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 下载进度显示区域 -->
|
||||
<div id="progressSection" class="bg-white rounded-lg shadow-md p-6 mt-6 hidden">
|
||||
<h2 class="text-lg font-medium text-gray-900 mb-4">下载进度</h2>
|
||||
<div id="progressList" class="space-y-4">
|
||||
<!-- 动态插入进度条 -->
|
||||
</div>
|
||||
<button id="clearCompletedBtn" class="mt-4 px-4 py-2 bg-gray-600 text-white rounded-md hover:bg-gray-700 transition-colors">
|
||||
清理已完成任务
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 使用说明部分 -->
|
||||
<div class="bg-white rounded-lg shadow-md p-6 mt-6">
|
||||
<h2 class="text-lg font-medium text-gray-900 mb-4">使用说明</h2>
|
||||
@@ -175,8 +173,164 @@
|
||||
|
||||
|
||||
<script>
|
||||
// 存储当前任务ID列表
|
||||
let currentTaskIds = [];
|
||||
let progressUpdateInterval = null;
|
||||
|
||||
// 开始定时更新进度
|
||||
function startProgressUpdates() {
|
||||
if (progressUpdateInterval) {
|
||||
clearInterval(progressUpdateInterval);
|
||||
}
|
||||
updateProgress(); // 立即更新一次
|
||||
progressUpdateInterval = setInterval(updateProgress, 2000); // 每2秒更新一次
|
||||
}
|
||||
|
||||
// 停止定时更新
|
||||
function stopProgressUpdates() {
|
||||
if (progressUpdateInterval) {
|
||||
clearInterval(progressUpdateInterval);
|
||||
progressUpdateInterval = null;
|
||||
}
|
||||
}
|
||||
|
||||
// 更新进度
|
||||
async function updateProgress() {
|
||||
try {
|
||||
const response = await fetch('/download_progress');
|
||||
const data = await response.json();
|
||||
|
||||
if (data.ret === 'OK' && data.tasks) {
|
||||
const tasks = data.tasks;
|
||||
const taskIds = Object.keys(tasks);
|
||||
|
||||
if (taskIds.length > 0) {
|
||||
$('#progressSection').removeClass('hidden');
|
||||
renderProgressList(tasks);
|
||||
currentTaskIds = taskIds;
|
||||
|
||||
// 如果有活跃任务,确保定时器在运行
|
||||
if (!progressUpdateInterval) {
|
||||
startProgressUpdates();
|
||||
}
|
||||
} else {
|
||||
$('#progressSection').addClass('hidden');
|
||||
currentTaskIds = [];
|
||||
|
||||
// 如果没有任务了,停止定时器以节省资源
|
||||
stopProgressUpdates();
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取下载进度失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// 渲染进度列表
|
||||
function renderProgressList(tasks) {
|
||||
const progressList = $('#progressList');
|
||||
progressList.empty();
|
||||
|
||||
Object.entries(tasks).forEach(([taskId, task]) => {
|
||||
const progressItem = createProgressItem(taskId, task);
|
||||
progressList.append(progressItem);
|
||||
});
|
||||
}
|
||||
|
||||
// 创建进度项
|
||||
function createProgressItem(taskId, task) {
|
||||
const statusText = getStatusText(task.status);
|
||||
const statusColor = getStatusColor(task.status);
|
||||
const progressPercent = task.progress || 0;
|
||||
|
||||
// 根据状态显示不同的按钮
|
||||
let actionButtons = '';
|
||||
if (task.status === 'downloading') {
|
||||
// 下载中:暂停 + 停止
|
||||
actionButtons = `
|
||||
<button onclick="pauseTask('${taskId}')" class="px-3 py-1 bg-yellow-500 text-white text-xs rounded hover:bg-yellow-600 transition-colors">
|
||||
暂停
|
||||
</button>
|
||||
<button onclick="stopTask('${taskId}')" class="px-3 py-1 bg-red-500 text-white text-xs rounded hover:bg-red-600 transition-colors">
|
||||
停止
|
||||
</button>
|
||||
`;
|
||||
} else if (task.status === 'paused') {
|
||||
// 已暂停:继续 + 停止
|
||||
actionButtons = `
|
||||
<button onclick="resumeTask('${taskId}')" class="px-3 py-1 bg-green-500 text-white text-xs rounded hover:bg-green-600 transition-colors">
|
||||
继续
|
||||
</button>
|
||||
<button onclick="stopTask('${taskId}')" class="px-3 py-1 bg-red-500 text-white text-xs rounded hover:bg-red-600 transition-colors">
|
||||
停止
|
||||
</button>
|
||||
`;
|
||||
} else if (task.status === 'stopped') {
|
||||
// 已停止:开始 + 删除
|
||||
actionButtons = `
|
||||
<button onclick="restartTask('${taskId}')" class="px-3 py-1 bg-green-500 text-white text-xs rounded hover:bg-green-600 transition-colors">
|
||||
开始
|
||||
</button>
|
||||
<button onclick="deleteTask('${taskId}')" class="px-3 py-1 bg-gray-500 text-white text-xs rounded hover:bg-gray-600 transition-colors">
|
||||
删除
|
||||
</button>
|
||||
`;
|
||||
}
|
||||
|
||||
// 处理总数显示:如果total为0,显示 "X/?" 表示未知总数
|
||||
const countDisplay = task.total > 0 ? `${task.completed}/${task.total}` : `${task.completed}/?`;
|
||||
|
||||
return `
|
||||
<div class="border border-gray-200 rounded-lg p-4">
|
||||
<div class="flex justify-between items-center mb-2">
|
||||
<div class="flex items-center space-x-2">
|
||||
<span class="text-sm font-medium text-gray-700">${task.dirname || '下载任务'}</span>
|
||||
<span class="px-2 py-1 text-xs rounded-full ${statusColor}">${statusText}</span>
|
||||
</div>
|
||||
<div class="flex items-center space-x-2">
|
||||
<span class="text-sm text-gray-600">${countDisplay}</span>
|
||||
<div class="flex space-x-1">
|
||||
${actionButtons}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-full bg-gray-200 rounded-full h-2.5 mb-2">
|
||||
<div class="bg-blue-600 h-2.5 rounded-full transition-all duration-300" style="width: ${progressPercent}%"></div>
|
||||
</div>
|
||||
${task.current_song ? `<div class="text-xs text-gray-500">当前: ${task.current_song}</div>` : ''}
|
||||
<div class="text-xs text-gray-400 mt-1">进度: ${progressPercent}%</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
// 获取状态文本
|
||||
function getStatusText(status) {
|
||||
const statusMap = {
|
||||
'pending': '等待中',
|
||||
'downloading': '下载中',
|
||||
'paused': '已暂停',
|
||||
'stopped': '已停止',
|
||||
'completed': '已完成',
|
||||
'failed': '失败'
|
||||
};
|
||||
return statusMap[status] || status;
|
||||
}
|
||||
|
||||
// 获取状态颜色
|
||||
function getStatusColor(status) {
|
||||
const colorMap = {
|
||||
'pending': 'bg-yellow-100 text-yellow-800',
|
||||
'downloading': 'bg-blue-100 text-blue-800',
|
||||
'paused': 'bg-orange-100 text-orange-800',
|
||||
'stopped': 'bg-gray-100 text-gray-800',
|
||||
'completed': 'bg-green-100 text-green-800',
|
||||
'failed': 'bg-red-100 text-red-800'
|
||||
};
|
||||
return colorMap[status] || 'bg-gray-100 text-gray-800';
|
||||
}
|
||||
|
||||
// 下载歌单
|
||||
$('#downloadPlaylistBtn').click(function () {
|
||||
$('#downloadPlaylistBtn').click(async function () {
|
||||
var playlistUrl = $('#playlistUrl').val();
|
||||
var dirname = $('#dirname').val();
|
||||
|
||||
@@ -189,23 +343,32 @@
|
||||
dirname: dirname,
|
||||
url: playlistUrl
|
||||
};
|
||||
$.ajax({
|
||||
type: "POST",
|
||||
url: "/downloadplaylist",
|
||||
contentType: "application/json",
|
||||
data: JSON.stringify(data),
|
||||
success: (msg) => {
|
||||
alert('歌单下载请求已发送!');
|
||||
console.log(response);
|
||||
},
|
||||
error: (msg) => {
|
||||
alert('歌单下载请求失败,请重试。');
|
||||
|
||||
try {
|
||||
const response = await fetch('/downloadplaylist', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
const result = await response.json();
|
||||
|
||||
if (result.ret === 'OK') {
|
||||
alert('歌单下载任务已创建!');
|
||||
// 立即更新一次进度,后续由updateProgress自动管理
|
||||
updateProgress();
|
||||
} else {
|
||||
alert('歌单下载请求失败: ' + (result.message || '未知错误'));
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
alert('歌单下载请求失败,请重试。');
|
||||
}
|
||||
});
|
||||
|
||||
// 下载单曲
|
||||
$('#downloadSongBtn').click(function () {
|
||||
$('#downloadSongBtn').click(async function () {
|
||||
var songName = $('#songName').val();
|
||||
var songUrl = $('#songUrl').val();
|
||||
|
||||
@@ -218,19 +381,169 @@
|
||||
name: songName,
|
||||
url: songUrl
|
||||
};
|
||||
$.ajax({
|
||||
type: "POST",
|
||||
url: "/downloadonemusic",
|
||||
contentType: "application/json",
|
||||
data: JSON.stringify(data),
|
||||
success: (msg) => {
|
||||
alert('单曲下载请求已发送!');
|
||||
console.log(response);
|
||||
},
|
||||
error: (msg) => {
|
||||
alert('单曲下载请求失败,请重试。');
|
||||
|
||||
try {
|
||||
const response = await fetch('/downloadonemusic', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
const result = await response.json();
|
||||
|
||||
if (result.ret === 'OK') {
|
||||
alert('单曲下载任务已创建!');
|
||||
// 立即更新一次进度,后续由updateProgress自动管理
|
||||
updateProgress();
|
||||
} else {
|
||||
alert('单曲下载请求失败: ' + (result.message || '未知错误'));
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
alert('单曲下载请求失败,请重试。');
|
||||
}
|
||||
});
|
||||
|
||||
// 清理已完成任务
|
||||
$('#clearCompletedBtn').click(async function () {
|
||||
try {
|
||||
const response = await fetch('/clear_completed_tasks', {
|
||||
method: 'POST'
|
||||
});
|
||||
const result = await response.json();
|
||||
|
||||
if (result.ret === 'OK') {
|
||||
alert(`已清理 ${result.cleared_count} 个已完成任务`);
|
||||
updateProgress();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('清理任务失败:', error);
|
||||
alert('清理任务失败');
|
||||
}
|
||||
});
|
||||
|
||||
// 暂停任务
|
||||
async function pauseTask(taskId) {
|
||||
try {
|
||||
const response = await fetch(`/pause_download?task_id=${taskId}`, {
|
||||
method: 'POST'
|
||||
});
|
||||
const result = await response.json();
|
||||
|
||||
if (result.ret === 'OK') {
|
||||
console.log('任务已暂停');
|
||||
updateProgress(); // 立即更新显示
|
||||
} else {
|
||||
alert('暂停失败: ' + (result.message || '未知错误'));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('暂停任务失败:', error);
|
||||
alert('暂停任务失败');
|
||||
}
|
||||
}
|
||||
|
||||
// 继续任务
|
||||
async function resumeTask(taskId) {
|
||||
try {
|
||||
const response = await fetch(`/resume_download?task_id=${taskId}`, {
|
||||
method: 'POST'
|
||||
});
|
||||
const result = await response.json();
|
||||
|
||||
if (result.ret === 'OK') {
|
||||
console.log('任务已继续');
|
||||
updateProgress(); // 立即更新显示
|
||||
} else {
|
||||
alert('继续失败: ' + (result.message || '未知错误'));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('继续任务失败:', error);
|
||||
alert('继续任务失败');
|
||||
}
|
||||
}
|
||||
|
||||
// 停止任务
|
||||
async function stopTask(taskId) {
|
||||
if (!confirm('确定要停止此下载任务吗?')) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`/stop_download?task_id=${taskId}`, {
|
||||
method: 'POST'
|
||||
});
|
||||
const result = await response.json();
|
||||
|
||||
if (result.ret === 'OK') {
|
||||
console.log('任务已停止');
|
||||
updateProgress(); // 立即更新显示
|
||||
} else {
|
||||
alert('停止失败: ' + (result.message || '未知错误'));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('停止任务失败:', error);
|
||||
alert('停止任务失败');
|
||||
}
|
||||
}
|
||||
|
||||
// 删除任务(仅针对已停止的任务)
|
||||
async function deleteTask(taskId) {
|
||||
if (!confirm('确定要删除此任务记录吗?')) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`/delete_download_task?task_id=${taskId}`, {
|
||||
method: 'POST'
|
||||
});
|
||||
const result = await response.json();
|
||||
|
||||
if (result.ret === 'OK') {
|
||||
console.log('任务记录已删除');
|
||||
updateProgress(); // 立即更新显示
|
||||
} else {
|
||||
alert('删除失败: ' + (result.message || '未知错误'));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('删除任务失败:', error);
|
||||
alert('删除任务失败');
|
||||
}
|
||||
}
|
||||
|
||||
// 重新开始任务(仅针对已停止的任务)
|
||||
async function restartTask(taskId) {
|
||||
if (!confirm('确定要重新开始此下载任务吗?\n将会创建一个新的下载任务。')) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`/restart_download?task_id=${taskId}`, {
|
||||
method: 'POST'
|
||||
});
|
||||
const result = await response.json();
|
||||
|
||||
if (result.ret === 'OK') {
|
||||
console.log('任务已重新开始,新任务ID:', result.task_id);
|
||||
alert('下载任务已重新开始!');
|
||||
updateProgress(); // 立即更新显示
|
||||
} else {
|
||||
alert('重新开始失败: ' + (result.message || '未知错误'));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('重新开始任务失败:', error);
|
||||
alert('重新开始任务失败');
|
||||
}
|
||||
}
|
||||
|
||||
// 页面加载时检查是否有进行中的任务
|
||||
$(document).ready(function() {
|
||||
updateProgress();
|
||||
});
|
||||
|
||||
// 页面卸载时停止定时器
|
||||
$(window).on('beforeunload', function() {
|
||||
stopProgressUpdates();
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
|
||||
Reference in New Issue
Block a user