Home > Blockchain >  Python tkinter freezing and doesn't update when some function takes a long time - tkinter & pyt
Python tkinter freezing and doesn't update when some function takes a long time - tkinter & pyt

Time:08-20

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()
  • Related