Skip to content

Instantly share code, notes, and snippets.

@dataserver
Last active August 10, 2019 23:38
Show Gist options
  • Save dataserver/9a7bffda013aef59253dde9d2f7af9fb to your computer and use it in GitHub Desktop.
Save dataserver/9a7bffda013aef59253dde9d2f7af9fb to your computer and use it in GitHub Desktop.
youtube-dl gui
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>MainWindow</class>
<widget class="QMainWindow" name="MainWindow">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>454</width>
<height>507</height>
</rect>
</property>
<property name="windowTitle">
<string>MainWindow</string>
</property>
<widget class="QWidget" name="centralwidget">
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="0">
<layout class="QVBoxLayout" name="verticalLayout">
<property name="leftMargin">
<number>10</number>
</property>
<property name="topMargin">
<number>10</number>
</property>
<property name="rightMargin">
<number>10</number>
</property>
<property name="bottomMargin">
<number>10</number>
</property>
<item>
<widget class="QTableWidget" name="tableList"/>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_2">
<property name="topMargin">
<number>10</number>
</property>
<property name="bottomMargin">
<number>10</number>
</property>
<item>
<widget class="QLabel" name="label_2">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Add Link : </string>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="inputLink"/>
</item>
<item>
<widget class="QPushButton" name="btnAddLink">
<property name="text">
<string>Add Link</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>60</height>
</size>
</property>
</spacer>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_6">
<item>
<widget class="QCheckBox" name="checkOnlyAudio">
<property name="text">
<string>Only Audio (Convert video files to audio-only files)</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<property name="topMargin">
<number>10</number>
</property>
<property name="bottomMargin">
<number>10</number>
</property>
<item>
<widget class="QLabel" name="label">
<property name="text">
<string>Output Directory : </string>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="fieldOutputDir"/>
</item>
<item>
<widget class="QPushButton" name="btnChangeOutputDir">
<property name="text">
<string>Change</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_4">
<item>
<widget class="QLabel" name="label_3">
<property name="text">
<string>Audio Format: </string>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="dropDownAudioFormat"/>
</item>
</layout>
</item>
<item>
<spacer name="verticalSpacer_2">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_3">
<property name="topMargin">
<number>20</number>
</property>
<item>
<widget class="QPushButton" name="btnStartDownload">
<property name="text">
<string>Download</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="btnClose">
<property name="text">
<string>Close</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</item>
</layout>
</widget>
<widget class="QMenuBar" name="menubar">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>454</width>
<height>21</height>
</rect>
</property>
</widget>
<widget class="QStatusBar" name="statusbar"/>
</widget>
<resources/>
<connections/>
</ui>
import os
import sys
import glob
import shlex
import subprocess
import shutil
import json
from configparser import ConfigParser
from PyQt5 import QtCore, QtGui, QtWidgets
from PyQt5.QtCore import pyqtSlot, Qt, QThread, QSize, pyqtSignal, QCoreApplication, QRect
from PyQt5.QtWidgets import QMainWindow, QApplication, QMessageBox, QWidget, QAction, QTableWidget, QTableWidgetItem, QPushButton, QVBoxLayout, QLineEdit, QLabel
from PyQt5.QtGui import QIcon
from PyQt5.uic import loadUi
# https://github.com/ytdl-org/youtube-dl
"""
Original code: Faeris95
https://github.com/Faeris95/YoutubeToMp3-PythonGUI
REQUIREMENT:
PyQt5
ffmpeg https://ffmpeg.org/
youtube-dl.exe https://ytdl-org.github.io/youtube-dl/download.html
"""
# subprocess returncode
# 0 = success
# -N unknow error
FETCH_SUCCESS = 0
FETCH_FAIL = -1
class Music:
def __init__(self, url, index=0):
self.name = None
self.setUrl(url)
self.title = ''
self.filename = ''
self.state = ''
self.idx = index
def getUrl(self):
return self.url
def setUrl(self, url):
self.url = url
def getName(self):
if(self.name):
return self.name
else:
return self.url
def setName(self, name):
self.name = name
def setState(self, state):
self.state = state
def getState(self):
return self.state
def getIndex(self):
return self.idx
def setTitle(self, title):
self.title = title
def getTitle(self):
return self.title
def setFilename(self, filename):
self.filename = filename
def getFilename(self):
return self.filename
def setId(self, id):
self.id = id
def getId(self):
return self.id
class mySignal():
def __init__(self, nb, state, code=''):
self.nb = nb
self.state = state
self.code = code
def getNb(self):
return self.nb
def getState(self):
return self.state
def getCode(self):
return self.code
class InfoThread(QThread):
sig = pyqtSignal(Music)
def __init__(self, parent=None):
super(InfoThread, self).__init__(parent)
self.musicList = []
def __del__(self):
self.wait()
def add(self, music):
self.musicList.append(music)
def run(self):
while(self.musicList):
self.music = self.musicList.pop(0)
self.music.setState('retriving info')
self.sig.emit(self.music)
info = self.getJsonInfo()
self.music.setId(info['id'])
self.music.setName(info['title'])
self.music.setFilename(info['title'] + ".mp3")
self.music.setState('ready to start')
self.sig.emit(self.music)
def getJsonInfo(self):
exe_path = os.path.join(os.getcwd(), 'youtube-dl.exe')
json_string = os.popen(
exe_path + ' --simulate --dump-json ' + self.music.getUrl(), 'r').read().rstrip()
data = json.loads(json_string)
# with open("dump.json", 'w') as f:
# json.dump(data, f)
return data
class DownloaderThread(QThread):
sig = pyqtSignal(mySignal)
def __init__(self, parent=None):
super(DownloaderThread, self).__init__(None)
self.parent = parent
def setDownloader(self, instance):
self.Downloader = instance
def setList(self, dic):
self.mp3List = dic
def addWidgetsList(self, dic):
self.lists_widget = dic
def run(self):
index = 0
for music in self.mp3List:
music.setState("downloading...")
self.sig.emit(mySignal(index, "downloading...", 0))
resultcode = self.Downloader.fetch(music)
if (resultcode == FETCH_SUCCESS):
music.setState("downloaded")
self.sig.emit(
mySignal(index, "downloaded", FETCH_SUCCESS))
else:
self.sig.emit(mySignal(index, "failed", resultcode))
index += 1
# self.sig.emit(mySignal(-1, "asdf", FETCH_FAIL))
def __del__(self):
self.wait()
class Downloader:
def __init__(self):
pass
def setOptions(self, options):
self.onlyAudio = options['onlyAudio']
self.audioFormat = options['audioFormat']
def buildCmd(self):
exe_path = os.path.join(os.getcwd(), 'youtube-dl.exe')
cmd = ' --quiet'
if self.onlyAudio == True:
cmd += ' --extract-audio'
audioFormat = str(self.audioFormat)
cmd += ' --audio-format ' + audioFormat
self.cmdArgs = shlex.split(cmd)
self.cmdArgs.insert(0, exe_path) # split fuckyup blackslash, fix it
def getOutputDir(self):
config = ConfigParser()
config.read('config.ini')
return config['DEFAULT']['outputdir']
def setOutputDir(self, txt):
config = ConfigParser()
config.read('config.ini')
config['DEFAULT'] = {'outputdir': txt}
with open('config.ini', 'w') as configfile:
config.write(configfile)
def fetch(self, music):
self.buildCmd()
outputdir = self.getOutputDir()
url = music.getUrl()
if 'index' in url:
url = url[0:url.find('&index')]
if 'list' in url:
url = url[0:url.find('&list')]
self.cmdArgs.append(url) # last arg is Youtube URL
# print(self.cmdArgs)
sresult = subprocess.run(self.cmdArgs, creationflags=0x08000000,
stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
# print(sresult)
if (sresult.returncode == FETCH_SUCCESS):
if self.audioFormat != "best":
self.file = glob.glob('*.' + self.audioFormat)
newFileName = self.file[0][:-16] + '.' + self.audioFormat
dest_path = os.path.join(outputdir, newFileName)
shutil.move(self.file[0], dest_path)
return sresult.returncode
else:
return sresult.returncode
class UI(QMainWindow):
def __init__(self):
super(UI, self).__init__()
loadUi("gui.ui", self)
self.hasConfig()
self.hasExe()
self.title = 'GUI Youtube-dl MP3 1.5'
self.left = 0
self.top = 0
self.width = 640
self.height = 480
self.rowId = 0
self.lists_music = []
self.lists_widget = []
self.lists_widgetTxt = []
self.Downloader = Downloader()
self.InfoThread = InfoThread()
self.InfoThread.sig[Music].connect(self.updateMusicList)
self.DownloaderThread = DownloaderThread(self)
self.DownloaderThread.sig[mySignal].connect(self.showFinishDialog)
self.initUI()
self.center()
def initUI(self):
self.setWindowIcon(QIcon('app.png'))
self.setGeometry(self.left, self.top, self.width, self.height)
columns = ["Title", "Status"]
self.tableList.setColumnCount(2)
self.tableList.setHorizontalHeaderLabels(columns)
self.tableList.setColumnWidth(0, 420)
self.tableList.setColumnWidth(1, 180)
self.fieldOutputDir.setText(self.Downloader.getOutputDir())
self.btnStartDownload.clicked.connect(lambda: self.activateDownload())
self.btnChangeOutputDir.clicked.connect(
lambda: self.actionChangeOutputDir())
self.btnAddLink.clicked.connect(lambda: self.doAddLink())
self.btnClose.clicked.connect(QCoreApplication.instance().quit)
audioformats = ["best", "aac", "flac",
"mp3", "m4a", "opus", "vorbis", "wav"]
for i in range(len(audioformats)):
self.dropDownAudioFormat.addItem(audioformats[i])
index = self.dropDownAudioFormat.findText('mp3')
self.dropDownAudioFormat.setCurrentIndex(index)
self.setWindowTitle(self.title)
self.show()
def center(self):
qr = self.frameGeometry()
cp = QtWidgets.QDesktopWidget().availableGeometry().center()
qr.moveCenter(cp)
self.move(qr.topLeft())
def enableWidget(self, button):
for but in button:
but.setEnabled(True)
def disableWidget(self, button):
for but in button:
but.setEnabled(False)
def showFinishDialog(self, signal):
# subprocess returncode
# 0 = success
# -N unknow error
if(signal.getCode() == -2):
QMessageBox.critical(
self, "Error", "Error downloading, wrong link or youtube-dl is out of date")
QCoreApplication.instance().quit
elif(signal.getCode() > -1):
self.lists_widget[signal.getNb()].setText(signal.getState())
QtWidgets.QApplication.processEvents()
else:
self.enableWidget(
[self.btnChangeOutputDir, self.btnStartDownload, self.btnAddLink])
QMessageBox.information(
self, "Completed", "All music downloaded !")
def hasConfig(self):
if not (os.path.isfile('config.ini')):
config = ConfigParser()
outputdir = os.path.join(os.getcwd(), 'download')
config['DEFAULT'] = {'outputdir': outputdir}
with open('config.ini', 'w') as configfile:
config.write(configfile)
def hasExe(self):
missings = []
if not (os.path.isfile('youtube-dl.exe')):
missings.append('youtube-dl.exe')
if not (os.path.isfile('ffmpeg.exe')):
missings.append('ffmpeg.exe')
if not (os.path.isfile('ffplay.exe')):
missings.append('ffplay.exe')
if not (os.path.isfile('ffprobe.exe')):
missings.append('ffprobe.exe')
if len(missings) > 0:
QMessageBox.critical(
self, "Error", "Missing files: " + " , ".join(missings))
sys.exit()
def activateDownload(self):
buttons = [self.btnStartDownload,
self.btnChangeOutputDir, self.btnAddLink]
self.disableWidget(buttons)
self.Downloader.setOptions({
'onlyAudio': self.checkOnlyAudio.isChecked(),
'audioFormat': str(self.dropDownAudioFormat.currentText())
})
self.DownloaderThread.setDownloader(self.Downloader)
self.DownloaderThread.setList(self.lists_music)
self.DownloaderThread.addWidgetsList(self.lists_widget)
self.DownloaderThread.start()
def actionChangeOutputDir(self):
directory = str(QtWidgets.QFileDialog.getExistingDirectory(
self, "Select Folder"))
self.Downloader.setOutputDir(directory)
self.fieldOutputDir.setText(directory)
def clearInputLink(self):
self.inputLink.clear()
def doAddLink(self):
url = self.inputLink.text()
self.clearInputLink()
if (url == ""):
QMessageBox.warning(self, "Input Error",
"No link!")
elif not ("youtube" in url):
QMessageBox.warning(self, "Input Error",
"Add youtube link!")
else:
rowIdx = self.tableList.rowCount()
music = Music(url, rowIdx)
self.tableList.insertRow(rowIdx)
cell_0 = QtWidgets.QTableWidgetItem()
cell_1 = QtWidgets.QTableWidgetItem()
cell_0.setFlags(Qt.ItemIsEnabled)
cell_1.setFlags(Qt.ItemIsEnabled)
cell_0.setText(music.getName())
cell_1.setText(music.getState())
self.tableList.setItem(rowIdx, 0, cell_0)
self.tableList.setItem(rowIdx, 1, cell_1)
self.lists_music.append(music)
self.lists_widget.append(cell_1)
self.lists_widgetTxt.append(cell_0)
self.InfoThread.add(music)
self.InfoThread.start()
def updateMusicList(self, music):
self.lists_widgetTxt[music.getIndex()].setText(music.getName())
self.lists_widget[music.getIndex()].setText(music.getState())
if __name__ == '__main__':
app = QApplication(sys.argv)
ex = UI()
sys.exit(app.exec_())
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment