I am trying to use youtube-dl
module to download videos from youtube. I created a simple GUI to do a little job, I need when user clicks start button the thread will be called and download starts and sends data using emit
method, when this data arrives to read
function in Main
class, the thread must stops after call stop
function from GUI, I tried to make event loop in qthread using exec_()
and stop thread with exit
, and I tried too use terminate
but the GUI freezes.
The code that I used is:
from PyQt5.QtGui import *
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
from youtube_dl import *
class Worker(QThread):
data = pyqtSignal(object)
def __init__(self):
super(Worker, self).__init__()
self.flag = True
def sendHook(self, data, status = {'status':'downloading'}):
self.data.emit(data)
def stop(self):
self.quit()
self.exit()
def run(self):
self.y = YoutubeDL({'progress_hooks':[self.sendHook]})
self.y.download(['https://www.youtube.com/watch?v=LKIXbNW-B5g'])
self.exec_()
class MainWindow(QMainWindow):
def __init__(self, *args, **kwargs):
super(MainWindow, self).__init__(*args, **kwargs)
layout = QVBoxLayout()
self.l = QLabel("Hello")
b = QPushButton("Start!")
b.pressed.connect(self.connectthread)
layout.addWidget(self.l)
layout.addWidget(b)
w = QWidget()
w.setLayout(layout)
self.setCentralWidget(w)
self.show()
def read(self, data):
self.thread.stop()
def connectthread(self):
self.thread = Worker()
self.thread.data.connect(self.read)
self.thread.start()
app = QApplication([])
window = MainWindow()
app.exec_()
By calling self.exec_()
in the run()
method of your worker you start a new event loop on this thread after the download finished, and this event loop then just keeps running. You don't need an event loop here, you only need a separate event loop if you want to move QObjects to it using their moveToThread()
method to decouple them from the main event loop, but that is not needed here, you're not doing anything that requires a Qt event loop. That's also why calling stop()
or exit()
doesn't do anything, it only affects an event loop. The only way to stop this thread would be its terminate()
method, and this also kind of works:
from PyQt5.QtGui import *
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
from youtube_dl import *
class Worker(QThread):
data = pyqtSignal(object)
def __init__(self):
super(Worker, self).__init__()
self.flag = True
def sendHook(self, data, status = {'status':'downloading'}):
self.data.emit(data)
def stop(self):
self.terminate()
print("QThread terminated")
def run(self):
self.y = YoutubeDL({'progress_hooks':[self.sendHook]})
self.y.download(['https://www.youtube.com/watch?v=LKIXbNW-B5g'])
print("finished")
class MainWindow(QMainWindow):
def __init__(self, *args, **kwargs):
super(MainWindow, self).__init__(*args, **kwargs)
layout = QVBoxLayout()
self.l = QLabel("Hello")
b = QPushButton("Start!")
b.pressed.connect(self.connectthread)
layout.addWidget(self.l)
layout.addWidget(b)
w = QWidget()
w.setLayout(layout)
self.setCentralWidget(w)
self.thread = None
self.show()
def read(self, data):
print("read:", data)
def connectthread(self):
if self.thread is not None:
# already running
self.thread.stop()
self.thread = None
return
self.thread = Worker()
self.thread.data.connect(self.read)
self.thread.start()
app = QApplication([])
window = MainWindow()
app.exec_()
Here I've altered your program so the first time the button is clicked the worker is started, the second time the thread is terminated and so on.
However terminating a thread this way is dangerous and discouraged. Python threads usually need to cooperate to be stoppable because by design they don't have a way to be interrupted. In this case it only works because PyQt is in control of the threads.
Unfortunately there isn't a way to gracefully stop a youtube-dl download, see this related issue for more information. In general there is no guarantee that killing the thread that called download()
will actually stop the download. YoutubeDL supports a plugin system with different downloaders. To download a hls stream for example an external ffmpeg
(or avconv
) process is started, which would not be stopped by killing the worker thread. The same would be true for a downloader that uses other threads or processes internally, or for the post-processing steps that are also performed using ffmpeg
.
If you want to be able to safely stop a download you'd have to use a separate process so you can use the SIGINT
signal (same as pressing Ctrl-C) to tell it to stop.