如何为Python GUI YouTube歌曲下载器添加进度条并解决hook报错?
为带GUI的YouTube歌曲下载器添加进度条并解决hook引用错误
问题背景
正在开发一款带GUI的YouTube歌曲下载器,已实现批量获取链接并下载到指定目录的功能,但下载时程序会卡顿,希望添加进度条让用户查看下载进度。尝试添加progress_hooks时出现UnboundLocalError,不清楚my_hook函数的正确声明位置,也不知道如何整合代码实现进度条功能。
现有代码
def Download_Songs(): links=LinkText.get("1.0",'end-1c').split() directory=SaveEntry.get() # importing packages import youtube_dl import shutil import os ydl_opts = {'postprocessors': [{'key': 'FFmpegExtractAudio', 'preferredcodec': 'mp3', 'preferredquality': '192'}]} for link in links: with youtube_dl.YoutubeDL(ydl_opts) as ydl: ydl.download([link]) for file in os.listdir("."): if file.endswith(".mp3"): newfile = file[:-16]+".mp3" shutil.move(file, os.path.join(directory, newfile)) # move the file without the search title def SearchDir(): location = filedialog.askdirectory() directory.set(location) from tkinter import filedialog import tkinter as tk root = tk.Tk() # Label for the links to download LinksFrame = tk.Frame(root) LinkLabel = tk.Label(LinksFrame, text="Enter links here") LinkText = tk.Text(LinksFrame, width=65, height=15) LinksFrame.pack(pady=6) LinkLabel.pack(side=tk.LEFT) LinkText.pack(side=tk.RIGHT) # This frame is for the downloaded directory. directory = tk.StringVar() directory.set("") LocationFrame = tk.Frame(root) SaveLabel = tk.Label(LocationFrame, text=" Download to:", padx=3) SaveEntry = tk.Entry(LocationFrame, text=directory, width=80) SaveButton = tk.Button(LocationFrame, text="Browse", command=SearchDir) LocationFrame.pack(pady=3) SaveLabel.pack(side=tk.LEFT) SaveEntry.pack(side=tk.LEFT) SaveButton.pack(side=tk.RIGHT, padx=5) # download button DownloadButton = tk.Button(root, text="Download", command=Download_Songs) DownloadButton.pack(pady=7) root.mainloop()
尝试的解决方案及报错
尝试添加以下代码来实现进度监听:
ydl_opts = { 'format': 'bestaudio/best', 'outtmpl': '%(id)s', 'noplaylist' : True, 'progress_hooks': [my_hook], } def my_hook(d): if d['status'] == 'finished': print('Done downloading, now converting ...')
但出现如下错误:
Exception in Tkinter callback Traceback (most recent call last): File "C:\Users\Admin\AppData\Local\Programs\Python\Python310\lib\tkinter\__init__.py", line 1921, in __call__ return self.func(*args) File "Z:\BackUp\programs by me\Python progs\YtDown.py", line 13, in Download_Songs 'progress_hooks': [my_hook]}]} UnboundLocalError: local variable 'my_hook' referenced before assignment
解决方案
1. 错误原因与hook正确声明位置
UnboundLocalError是因为my_hook函数在被ydl_opts引用之后才定义,Python解释器找不到它。需要先定义my_hook,再创建ydl_opts。
另外,由于tkinter是单线程GUI框架,下载任务会阻塞主线程导致界面卡顿,所以必须把下载逻辑放到子线程中执行,同时通过root.after()方法在主线程更新进度条(tkinter控件只能在主线程操作)。
2. 完整修改后的代码
from tkinter import filedialog import tkinter as tk from tkinter import ttk import youtube_dl import shutil import os import threading # 全局变量用于更新进度 progress_var = tk.DoubleVar() status_label = None def my_hook(d): global progress_var, status_label if d['status'] == 'downloading': # 计算下载进度:已下载字节数 / 总字节数 * 100 downloaded = d.get('_downloaded_bytes', 0) total = d.get('total_bytes', 0) if total > 0: progress = (downloaded / total) * 100 # 用after在主线程更新进度条 root.after(0, lambda: progress_var.set(progress)) status_text = f"下载中: {d['filename']} ({progress:.1f}%)" root.after(0, lambda: status_label.config(text=status_text)) elif d['status'] == 'finished': root.after(0, lambda: status_label.config(text="下载完成,正在转换音频...")) elif d['status'] == 'error': root.after(0, lambda: status_label.config(text=f"下载出错: {d['error']}")) def Download_Songs(): links = LinkText.get("1.0", 'end-1c').split() directory = SaveEntry.get() if not directory or not links: status_label.config(text="请填写下载目录和链接") return # 重置进度条和状态 progress_var.set(0) status_label.config(text="准备开始下载...") def download_task(): ydl_opts = { 'postprocessors': [{ 'key': 'FFmpegExtractAudio', 'preferredcodec': 'mp3', 'preferredquality': '192' }], 'format': 'bestaudio/best', 'noplaylist': True, 'progress_hooks': [my_hook], 'quiet': True, # 关闭控制台输出,避免干扰 'no_warnings': True } for link in links: try: with youtube_dl.YoutubeDL(ydl_opts) as ydl: ydl.download([link]) except Exception as e: root.after(0, lambda: status_label.config(text=f"处理链接失败: {str(e)}")) continue # 移动下载好的MP3文件 for file in os.listdir("."): if file.endswith(".mp3"): # 修正文件名截取逻辑,避免长度不足报错 newfile = file[:-16] + ".mp3" if len(file)>=16 else file try: shutil.move(file, os.path.join(directory, newfile)) except Exception as e: root.after(0, lambda: status_label.config(text=f"移动文件失败: {str(e)}")) root.after(0, lambda: status_label.config(text="所有任务完成!")) root.after(0, lambda: progress_var.set(100)) # 启动子线程执行下载任务,避免阻塞GUI threading.Thread(target=download_task, daemon=True).start() def SearchDir(): location = filedialog.askdirectory() directory.set(location) root = tk.Tk() root.title("YouTube歌曲下载器") # 链接输入区域 LinksFrame = tk.Frame(root) LinkLabel = tk.Label(LinksFrame, text="输入下载链接:") LinkText = tk.Text(LinksFrame, width=65, height=15) LinksFrame.pack(pady=6) LinkLabel.pack(side=tk.LEFT, padx=5) LinkText.pack(side=tk.RIGHT) # 下载目录区域 directory = tk.StringVar() directory.set("") LocationFrame = tk.Frame(root) SaveLabel = tk.Label(LocationFrame, text="下载至:", padx=3) SaveEntry = tk.Entry(LocationFrame, textvariable=directory, width=80) SaveButton = tk.Button(LocationFrame, text="浏览", command=SearchDir) LocationFrame.pack(pady=3) SaveLabel.pack(side=tk.LEFT) SaveEntry.pack(side=tk.LEFT, padx=5) SaveButton.pack(side=tk.RIGHT, padx=5) # 进度显示区域 ProgressFrame = tk.Frame(root) status_label = tk.Label(ProgressFrame, text="等待开始下载", width=80) progress_bar = ttk.Progressbar(ProgressFrame, variable=progress_var, maximum=100) ProgressFrame.pack(pady=5) status_label.pack(pady=2) progress_bar.pack(fill=tk.X, padx=10) # 下载按钮 DownloadButton = tk.Button(root, text="开始下载", command=Download_Songs) DownloadButton.pack(pady=7) root.mainloop()
关键修改说明
- hook函数位置:
my_hook定义在Download_Songs之前,确保被ydl_opts引用时已存在。 - 子线程处理:用
threading.Thread把下载任务放到子线程,避免GUI卡顿。 - 进度条更新:在
my_hook中通过root.after(0, ...)将控件更新操作抛回主线程,符合tkinter的线程安全要求。 - 状态提示:添加
status_label显示当前下载状态,提升用户体验。 - 错误处理:增加了目录和链接为空的校验,以及下载、移动文件时的异常捕获,避免程序崩溃。
内容的提问来源于stack exchange,提问作者Eitan
相关产品推荐
相关产品推荐

