You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何为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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.06 05:10:20