I am building an app using tkinter and pytube, but I'm struggling with a feature: the progress bar.
The problem is, when I am using tkinter, because of the mainloop function, I can't use things like while, time.sleep... When the pytube function "download" is executed, the app freezes until the download is complete. For this reason, the progress bar doesn't load, just jumps to 100% complete when the app unfreezes at the end of download.
I've tried a threading function, to try to run the download function at the same time as the screen main loop updates. I've tried using the after function on tkinter, or some alternatives for mainloop. I also tried to run the download on another tk screen. Nothing works, the code is still running during the download, the progress is printed by a function in terminal, but as the tk screen freezes, I can't show it on screen, just in terminal.
How can I fix it?
ps: this is not the final version of this code, so it is not yet in OOP, but it is possible to understand
import sys
import threading
import time
from tkinter import *
from tkinter import filedialog, ttk, messagebox
from PIL import ImageTk, Image
from urllib.request import urlopen
from PIL.Image import Resampling
from pytube import YouTube
from io import BytesIO
import pyperclip
download_path = None
# -------------------- COMMANDS -------------------- #
def get_thumnail(url):
image_url = urlopen(url)
image_raw = image_url.read()
image_url.close()
image = Image.open(BytesIO(image_raw))
thumbnail = image.resize((300, 200), resample=Resampling.LANCZOS)
image_tk = ImageTk.PhotoImage(thumbnail)
return image_tk
def paste_url():
clipboard = pyperclip.paste()
video_url_entry.delete(0, END)
video_url_entry.insert(0, clipboard)
def select_path():
global download_path
download_path = filedialog.askdirectory()
destination_path_label_entry.config(text=download_path)
def show_progress_bar(s, chunk, bytes_remaining):
download_percent = int((s.filesize - bytes_remaining) / s.filesize * 100)
progressbar.after(200, progressbar.config(value=download_percent))
#progressbar.config(value=download_percent)
root.title(f'\rProgress: {download_percent} %')
print(f'\rProgress: {download_percent} %')
#threading.Thread(target=after).start()
def complete(s, file_path):
print(f'Download complete: {file_path}')
def download_video():
link = video_url_entry.get()
path = download_path
yt = YouTube(link)
threading.Thread(target=yt.register_on_progress_callback(show_progress_bar)).start()
new_icon = get_thumnail(yt.thumbnail_url)
main_image.configure(image=new_icon)
main_image.image = new_icon
is_ok = messagebox.askokcancel(title=yt.title,
message=f'Title: {yt.title}\n'
f'Length: {yt.length//60}m{yt.length&60}s\n'
f'Views: {yt.views}\n'
f'Author: {yt.author}')
if is_ok:
resolution = current_var.get()
if resolution == 'mp3':
video_stream = yt.streams.get_audio_only()
elif resolution == 'Highest':
video_stream = yt.streams.get_highest_resolution()
else:
video_stream = yt.streams.get_by_resolution(resolution)
print('start')
#threading.Thread(target=video_stream.download(path, filename_prefix=f'({resolution}) ')).start()
video_stream.download(path, filename_prefix=f'({resolution}) ')
print('done')
else:
main_image.configure(image=icon)
main_image.image = icon
# Screen settings
root = Tk()
icon = PhotoImage(file='logo.png')
root.iconphoto(False, icon)
root.title('Youtube Video Downloader')
root.config(padx=50, pady=50)
# Main Canvas
main_image = Label(root, image=icon, width=350, height=200)
main_image.grid(row=1, column=1, columnspan=3)
# Labels
video_url_label = Label(text='Video URL:')
video_url_label.grid(row=2, column=1)
done_label = Label(text='', width=47, fg='green')
done_label.grid(row=6, column=2, columnspan=2)
destination_path_label_entry = Label(width=40, bg='white')
destination_path_label_entry.grid(row=3, column=1, columnspan=2)
# Entry's
video_url_entry = Entry(width=35)
video_url_entry.focus()
video_url_entry.grid(row=2, column=2)
# Buttons
search_button = Button(text='Paste', width=15, command=paste_url)
search_button.grid(row=2, column=3)
destination_path_button = Button(text='Select Path', width=15, command=select_path)
destination_path_button.grid(row=3, column=3)
current_var = StringVar(value='Resolution')
combobox = ttk.Combobox(textvariable=current_var, state='readonly', values=('mp3', '144p', '240p', '360p', '480p', '720p', '1080p', 'Highest'), width=9)
combobox.grid(row=4, column=1)
download_button = Button(text='Download as MP4', width=46, command=download_video)
download_button.grid(row=4, column=2, columnspan=2)
# Progress bar
progressbar = ttk.Progressbar(root, orient=HORIZONTAL, length=410, mode='determinate')
progressbar.grid(row=5, column=1, columnspan=3)
root.mainloop()
CodePudding user response:
Problem of your solution is, that you're calling video_stream.download
from within download_video
function.
Once video_stream.download
is called, it is blocking GUI, so nothing got updated, until video download is finished. You had commented line in your code threading.Thread(target=video_stream.download(path, filename_prefix=f'({resolution}) ')).start()
, so you were trying to call download in the Thread, but that would yield the same result as target=video_stream.download(path, filename_prefix=f'({resolution}) ')
would first download the video and only then start the Thread itself. You don't want that.
Have a look at Thread documentation, where you must use callable object (function or method) as a target without any arguments. Those must be passed to the Thread by using args and kwargs.
The rest of your code is fine and you were properly using .after
as well to avoid calling GUI from different Thread, which you should never do.
Here is your modified code, now properly calling download Thread and updating progress bar as well:
import threading
from io import BytesIO
from tkinter import *
from tkinter import filedialog, ttk, messagebox
from urllib.request import urlopen
import pyperclip
from PIL import ImageTk, Image
from PIL.Image import Resampling
from pytube import YouTube
download_path = None
# -------------------- COMMANDS -------------------- #
def get_thumnail(url):
image_url = urlopen(url)
image_raw = image_url.read()
image_url.close()
image = Image.open(BytesIO(image_raw))
thumbnail = image.resize((300, 200), resample=Resampling.LANCZOS)
image_tk = ImageTk.PhotoImage(thumbnail)
return image_tk
def paste_url():
clipboard = pyperclip.paste()
video_url_entry.delete(0, END)
video_url_entry.insert(0, clipboard)
def select_path():
global download_path
download_path = filedialog.askdirectory()
destination_path_label_entry.config(text=download_path)
def show_progress_bar(s, chunk, bytes_remaining):
download_percent = int((s.filesize - bytes_remaining) / s.filesize * 100)
root.after(0, update_gui(download_percent))
print(f'\rProgress: {download_percent} %')
def update_gui(download_percent):
progressbar.config(value=download_percent)
root.title(f'\rProgress: {download_percent} %')
def complete(s, file_path):
print(f'Download complete: {file_path}')
def download_video():
link = video_url_entry.get()
path = download_path
yt = YouTube(link)
# Don't put callback into Thread
yt.register_on_progress_callback(show_progress_bar)
yt.register_on_complete_callback(complete)
new_icon = get_thumnail(yt.thumbnail_url)
main_image.configure(image=new_icon)
main_image.image = new_icon
is_ok = messagebox.askokcancel(title=yt.title,
message=f'Title: {yt.title}\n'
f'Length: {yt.length // 60}m{yt.length & 60}s\n'
f'Views: {yt.views}\n'
f'Author: {yt.author}')
if is_ok:
resolution = current_var.get()
if resolution == 'mp3':
video_stream = yt.streams.get_audio_only()
elif resolution == 'Highest':
video_stream = yt.streams.get_highest_resolution()
else:
video_stream = yt.streams.get_by_resolution(resolution)
# You cant just call video_stream.download() with params, because it will execute and block outside Thread
threading.Thread(target=video_stream.download,
kwargs={"output_path": path, "filename_prefix": f'({resolution}) '}).start()
else:
main_image.configure(image=icon)
main_image.image = icon
# Screen settings
root = Tk()
icon = PhotoImage(file='logo.png')
root.iconphoto(False, icon)
root.title('Youtube Video Downloader')
root.config(padx=50, pady=50)
# Main Canvas
main_image = Label(root, image=icon, width=350, height=200)
main_image.grid(row=1, column=1, columnspan=3)
# Labels
video_url_label = Label(text='Video URL:')
video_url_label.grid(row=2, column=1)
done_label = Label(text='', width=47, fg='green')
done_label.grid(row=6, column=2, columnspan=2)
destination_path_label_entry = Label(width=40, bg='white')
destination_path_label_entry.grid(row=3, column=1, columnspan=2)
# Entry's
video_url_entry = Entry(width=35)
video_url_entry.focus()
video_url_entry.grid(row=2, column=2)
# Buttons
search_button = Button(text='Paste', width=15, command=paste_url)
search_button.grid(row=2, column=3)
destination_path_button = Button(text='Select Path', width=15, command=select_path)
destination_path_button.grid(row=3, column=3)
current_var = StringVar(value='Resolution')
combobox = ttk.Combobox(textvariable=current_var, state='readonly',
values=('mp3', '144p', '240p', '360p', '480p', '720p', '1080p', 'Highest'), width=9)
combobox.grid(row=4, column=1)
download_button = Button(text='Download as MP4', width=46, command=download_video)
download_button.grid(row=4, column=2, columnspan=2)
# Progress bar
progressbar = ttk.Progressbar(root, orient=HORIZONTAL, length=410, mode='determinate')
progressbar.grid(row=5, column=1, columnspan=3)
root.mainloop()