I have this image downloader which works as new thread and a popup which contains progress bar. Progress bar does not update during download but after(downloader is written with requests, gui app was made with kivy). Any ideas how to fix this stuff?
Downloader: It is separated in another file
class Downloader(threading.Thread):
def __init__(self, url: str, download_monitor):
super(Downloader, self).__init__(daemon=True) # daemon dies when main die
self.url = url
self.download_monitor = download_monitor # url popup
def run(self) -> None:
# Reset
self.download_monitor.reset()
file_name = self.url.split('/')[-1]
# Less RAM usage
with requests.get(self.url, stream=True) as req: # stream=True not to read at once
req.raise_for_status()
with open('temp/'+file_name, 'wb') as file:
chunks = list(enumerate(req.iter_content(chunk_size=8192)))
self.download_monitor.downloading_progress.max = chunks[-1][0] # last element
for progress, chunk in chunks:
self.download_monitor.downloading_progress.value = progress
file.write(chunk)
PopUp .py: It is separated in another file
class UrlPopup(Popup):
url_input = ObjectProperty()
downloading_progress = ObjectProperty()
def __init__(self, **kwargs):
super(UrlPopup, self).__init__(**kwargs)
def download(self):
# https://www.nasa.gov/sites/default/files/thumbnails/image/hubble_ngc2903_potw2143a.jpg.jpg
if self.url_input.text.startswith('https://'): # if it is url address
download(self.url_input.text, self)
def on_dismiss(self):
self.reset()
self.url_input.text = ''
def reset(self):
self.downloading_progress.max = 0
self.downloading_progress.value = 0
PopUp .kv: It is separated in another file
<UrlPopup>:
url_input: url_input
downloading_progress: downloading_progress
id: downloader
title: 'URL address'
size_hint: .25, None
height: 157
BoxLayout:
orientation: 'vertical'
size_hint_y: None
height: 64
TextInput:
id: url_input
multiline: False
size_hint_y: None
height: 32
font_size: 16
ProgressBar:
id: downloading_progress
size_hint_y: None
height: 32
BoxLayout:
orientation: 'horizontal'
size_hint_y: None
height: 32
Button:
text: 'Download'
on_press: root.download()
Button:
text: 'Close'
on_press: root.dismiss()
EDIT1 ApuCoder I did as you wrote but progress still updates after download. Any other ideas? PopUP .py:
class UrlPopup(Popup):
url_input = ObjectProperty()
downloading_progress = ObjectProperty()
progress_value = NumericProperty()
def update_progress(self, dt):
self.progress_value += 1
Downloader .py:
with requests.get(self.url, stream=True) as req: # stream=True not to read at once
req.raise_for_status()
with open('temp/'+file_name, 'wb') as file:
chunks = list(enumerate(req.iter_content(chunk_size=8192)))
self.download_monitor.downloading_progress.max = chunks[-1][0] # last element
Clock.schedule_interval(self.download_monitor.update_progress, .1)
for progress, chunk in chunks:
#self.download_monitor.downloading_progress.value = progress
file.write(chunk)
PopUp .kv:
ProgressBar:
id: downloading_progress
value: root.progress_value
size_hint_y: None
height: 32
EDIT2 This is in same file as class Downloader. I call this function when a button is pressed
def download(url: str, download_monitor):
"""Other thread"""
downloader = Downloader(url, download_monitor)
downloader.start()
Assuming that you want to download some content and to show the ongoing process (or current status) in kivy, I updated and modified some of your code to make a minimal example.
In this case there is no need to create a new Thread
class, instead create a new thread object each time and set the target
to some method (here, start_download
) for fetching and writing the binary data in disk. Thus the progress can be controlled within this method, thereby no scheduling is required.
from threading import Thread
import requests
from kivy.app import runTouchApp
from kivy.lang import Builder
from kivy.properties import (
BooleanProperty,
NumericProperty,
ObjectProperty,
)
from kivy.uix.popup import Popup
from kivy.uix.screenmanager import Screen
Builder.load_string("""
<DownLoadScreen>:
Button:
text: "Open Downloader"
on_release: root.open_downloader()
<UrlPopup>:
url_input: url_input
title: 'URL address'
size_hint: .75, None
height: "450dp"
BoxLayout:
orientation: "vertical"
TextInput:
id: url_input
text: "https://www.nasa.gov/sites/default/files/thumbnails/image/hubble_ngc2903_potw2143a.jpg.jpg"
multiline: False
size_hint_y: None
height: "64dp"
font_size: "16sp"
ProgressBar:
pos_hint: {"center_x" : 0.5}
value: root.prog_val
max: root.tot_size
Label:
id: lbl
text: "Downloading file...({:.0%})".format(root.prog_val/root.tot_size) if root.has_started else ""
BoxLayout:
size_hint_y: None
height: dp(48)
Button:
text: 'Download'
on_release: root.download()
Button:
text: 'Close'
on_press: root.dismiss()
""")
class UrlPopup(Popup):
url_input = ObjectProperty()
prog_val = NumericProperty(0) # To capture the current progress.
tot_size = NumericProperty(1) # Total size of the file/content. Setting the default value to 1 to avoid ZeroDivisionError, though will not affect anyhow.
has_started = BooleanProperty(False) # Just to manipulate the label text.
def start_download(self):
self.has_started = True
self.url = self.url_input.text
# file_name = self.url.split('/')[-1]
with requests.get(self.url, stream=True) as req:
if req.status_code == 200: # Here, you can create the binary file.
# chunks = list(enumerate(req.iter_content(chunk_size=8192))) # This may take more memory for larger file.
self.tot_size = int(req.headers["Content-Length"])
item_size = 2048 # Reducing the chunk size increases writing time and so needs more time in progress.
for i, chunk in enumerate(req.iter_content(chunk_size = item_size)):
self.prog_val = i*item_size
# file.write(chunk)
self.ids.lbl.text = "Download completed." # A confirmation message.
def download(self):
"""A new thread object will be created each time this method is revoked. But be careful about the threads already created."""
Thread(target = self.start_download).start()
def on_dismiss(self):
self.url_input.text = ""
self.has_started = False
class DownLoadScreen(Screen):
def open_downloader(self):
UrlPopup().open()
runTouchApp(DownLoadScreen())
Let me know if it fits your need.